diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs index 9a3b8b124..d484714f8 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs @@ -88,5 +88,34 @@ namespace ICSharpCode.Decompiler.Tests Assert.AreEqual(identifier, targetFramework.Identifier); Assert.AreEqual(profile, targetFramework.Profile); } + + [TestCase(null, 350, "net35")] + [TestCase(".NETFramework", 350, "net35")] + [TestCase(".NETFramework", 400, "net40")] + [TestCase(".NETFramework", 451, "net451")] + [TestCase(".NETCoreApp", 200, "netcoreapp2.0")] + [TestCase(".NETCoreApp", 310, "netcoreapp3.1")] + [TestCase(".NETStandard", 130, "netstandard1.3")] + [TestCase(".NETStandard", 200, "netstandard2.0")] + [TestCase("Silverlight", 400, "sl4")] + [TestCase("Silverlight", 550, "sl5")] + [TestCase(".NETCore", 450, "netcore45")] + [TestCase(".NETCore", 451, "netcore451")] + [TestCase("WindowsPhone", 700, "wp7")] + [TestCase("WindowsPhone", 810, "wp81")] + [TestCase(".NETMicroFramework", 100, "netmf")] + [TestCase(".NETMicroFramework", 210, "netmf")] + [TestCase(".NETPortable", 100, null)] + [TestCase("Unsupported", 100, null)] + public void VerifyMoniker(string identifier, int version, string expectedMoniker) + { + // Arrange - nothing + + // Act + var targetFramework = new TargetFramework(identifier, version, profile: null); + + // Assert + Assert.AreEqual(expectedMoniker, targetFramework.Moniker); + } } } diff --git a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs index 2139269c9..3b369e3fc 100644 --- a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs +++ b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs @@ -55,7 +55,7 @@ namespace ICSharpCode.Decompiler.Tests public void NewtonsoftJson_pcl_debug() { try { - RunWithTest("Newtonsoft.Json-pcl-debug", "Newtonsoft.Json.dll", "Newtonsoft.Json.Tests.dll"); + RunWithTest("Newtonsoft.Json-pcl-debug", "Newtonsoft.Json.dll", "Newtonsoft.Json.Tests.dll", useOldProjectFormat: true); } catch (CompilationFailedException) { Assert.Ignore("Cannot yet re-compile PCL projects."); } @@ -103,9 +103,9 @@ namespace ICSharpCode.Decompiler.Tests RunWithOutput("Random Tests\\TestCases", "TestCase-1.exe"); } - void RunWithTest(string dir, string fileToRoundtrip, string fileToTest, string keyFile = null) + void RunWithTest(string dir, string fileToRoundtrip, string fileToTest, string keyFile = null, bool useOldProjectFormat = false) { - RunInternal(dir, fileToRoundtrip, outputDir => RunTest(outputDir, fileToTest), keyFile); + RunInternal(dir, fileToRoundtrip, outputDir => RunTest(outputDir, fileToTest), keyFile, useOldProjectFormat); } void RunWithOutput(string dir, string fileToRoundtrip) @@ -120,7 +120,7 @@ namespace ICSharpCode.Decompiler.Tests RunInternal(dir, fileToRoundtrip, outputDir => { }); } - void RunInternal(string dir, string fileToRoundtrip, Action testAction, string snkFilePath = null) + void RunInternal(string dir, string fileToRoundtrip, Action testAction, string snkFilePath = null, bool useOldProjectFormat = false) { if (!Directory.Exists(TestDir)) { Assert.Ignore($"Assembly-roundtrip test ignored: test directory '{TestDir}' needs to be checked out separately." + Environment.NewLine + @@ -158,6 +158,9 @@ namespace ICSharpCode.Decompiler.Tests // Let's limit the roundtrip tests to C# 7.3 for now; because 8.0 is still in preview // and the generated project doesn't build as-is. var settings = new DecompilerSettings(LanguageVersion.CSharp7_3); + if (useOldProjectFormat) { + settings.UseSdkStyleProjectFormat = false; + } var decompiler = new TestProjectDecompiler(projectGuid, resolver, settings); @@ -212,7 +215,7 @@ namespace ICSharpCode.Decompiler.Tests static void Compile(string projectFile, string outputDir) { var info = new ProcessStartInfo(FindMSBuild()); - info.Arguments = $"/nologo /v:minimal /p:OutputPath=\"{outputDir}\" \"{projectFile}\""; + info.Arguments = $"/nologo /v:minimal /restore /p:OutputPath=\"{outputDir}\" \"{projectFile}\""; info.CreateNoWindow = true; info.UseShellExecute = false; info.RedirectStandardOutput = true; diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs new file mode 100644 index 000000000..479acc419 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -0,0 +1,212 @@ +// Copyright (c) 2020 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.PortableExecutable; +using System.Xml; +using ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler +{ + /// + /// A implementation that creates the projects in the SDK style format. + /// + sealed class ProjectFileWriterSdkStyle : IProjectFileWriter + { + const string AspNetCorePrefix = "Microsoft.AspNetCore"; + const string PresentationFrameworkName = "PresentationFramework"; + const string WindowsFormsName = "System.Windows.Forms"; + const string TrueString = "True"; + const string FalseString = "False"; + const string AnyCpuString = "AnyCPU"; + + static readonly HashSet ImplicitReferences = new HashSet { + "mscorlib", + "netstandard", + "PresentationFramework", + "System", + "System.Diagnostics.Debug", + "System.Diagnostics.Tools", + "System.Drawing", + "System.Runtime", + "System.Runtime.Extensions", + "System.Windows.Forms", + "System.Xaml", + }; + + enum ProjectType { Default, WinForms, Wpf, Web } + + /// + /// Creates a new instance of the class. + /// + /// A new instance of the class. + public static IProjectFileWriter Create() => new ProjectFileWriterSdkStyle(); + + /// + public void Write( + TextWriter target, + IProjectInfoProvider project, + IEnumerable<(string itemType, string fileName)> files, + PEFile module) + { + using (XmlTextWriter xmlWriter = new XmlTextWriter(target)) { + xmlWriter.Formatting = Formatting.Indented; + Write(xmlWriter, project, module); + } + } + + static void Write(XmlTextWriter xml, IProjectInfoProvider project, PEFile module) + { + xml.WriteStartElement("Project"); + + var projectType = GetProjectType(module); + xml.WriteAttributeString("Sdk", GetSdkString(projectType)); + + PlaceIntoTag("PropertyGroup", xml, () => WriteAssemblyInfo(xml, module, projectType)); + PlaceIntoTag("PropertyGroup", xml, () => WriteProjectInfo(xml, project)); + PlaceIntoTag("ItemGroup", xml, () => WriteReferences(xml, module, project)); + + xml.WriteEndElement(); + } + + static void PlaceIntoTag(string tagName, XmlTextWriter xml, Action content) + { + xml.WriteStartElement(tagName); + try { + content(); + } finally { + xml.WriteEndElement(); + } + } + + static void WriteAssemblyInfo(XmlTextWriter xml, PEFile module, ProjectType projectType) + { + xml.WriteElementString("AssemblyName", module.Name); + + // Since we create AssemblyInfo.cs manually, we need to disable the auto-generation + xml.WriteElementString("GenerateAssemblyInfo", FalseString); + + // 'Library' is default, so only need to specify output type for executables + if (!module.Reader.PEHeaders.IsDll) { + WriteOutputType(xml, module.Reader.PEHeaders.PEHeader.Subsystem); + } + + WriteDesktopExtensions(xml, projectType); + + string platformName = TargetServices.GetPlatformName(module); + var targetFramework = TargetServices.DetectTargetFramework(module); + + if (targetFramework.Moniker == null) { + throw new NotSupportedException($"Cannot decompile this assembly to a SDK style project. Use default project format instead."); + } + + xml.WriteElementString("TargetFramework", targetFramework.Moniker); + + // 'AnyCPU' is default, so only need to specify platform if it differs + if (platformName != AnyCpuString) { + xml.WriteElementString("PlatformTarget", platformName); + } + + if (platformName == AnyCpuString && (module.Reader.PEHeaders.CorHeader.Flags & CorFlags.Prefers32Bit) != 0) { + xml.WriteElementString("Prefer32Bit", TrueString); + } + } + + static void WriteOutputType(XmlTextWriter xml, Subsystem moduleSubsystem) + { + switch (moduleSubsystem) { + case Subsystem.WindowsGui: + xml.WriteElementString("OutputType", "WinExe"); + break; + case Subsystem.WindowsCui: + xml.WriteElementString("OutputType", "Exe"); + break; + } + } + + static void WriteDesktopExtensions(XmlTextWriter xml, ProjectType projectType) + { + if (projectType == ProjectType.Wpf) { + xml.WriteElementString("UseWPF", TrueString); + } else if (projectType == ProjectType.WinForms) { + xml.WriteElementString("UseWindowsForms", TrueString); + } + } + + static void WriteProjectInfo(XmlTextWriter xml, IProjectInfoProvider project) + { + xml.WriteElementString("LangVersion", project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); + xml.WriteElementString("AllowUnsafeBlocks", TrueString); + + if (project.StrongNameKeyFile != null) { + xml.WriteElementString("SignAssembly", TrueString); + xml.WriteElementString("AssemblyOriginatorKeyFile", Path.GetFileName(project.StrongNameKeyFile)); + } + } + + static void WriteReferences(XmlTextWriter xml, PEFile module, IProjectInfoProvider project) + { + foreach (var reference in module.AssemblyReferences.Where(r => !ImplicitReferences.Contains(r.Name))) { + xml.WriteStartElement("Reference"); + xml.WriteAttributeString("Include", reference.Name); + + var asembly = project.AssemblyResolver.Resolve(reference); + if (asembly != null) { + xml.WriteElementString("HintPath", asembly.FileName); + } + + xml.WriteEndElement(); + } + } + + static string GetSdkString(ProjectType projectType) + { + switch (projectType) { + case ProjectType.WinForms: + case ProjectType.Wpf: + return "Microsoft.NET.Sdk.WindowsDesktop"; + case ProjectType.Web: + return "Microsoft.NET.Sdk.Web"; + default: + return "Microsoft.NET.Sdk"; + } + } + + static ProjectType GetProjectType(PEFile module) + { + foreach (var referenceName in module.AssemblyReferences.Select(r => r.Name)) { + if (referenceName.StartsWith(AspNetCorePrefix, StringComparison.Ordinal)) { + return ProjectType.Web; + } + + if (referenceName == PresentationFrameworkName) { + return ProjectType.Wpf; + } + + if (referenceName == WindowsFormsName) { + return ProjectType.WinForms; + } + } + + return ProjectType.Default; + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs index 43e8642ee..c340578e3 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs @@ -42,7 +42,8 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler Identifier = identifier; VersionNumber = version; - VersionString = "v" + GetVersionString(version); + VersionString = "v" + GetVersionString(version, withDots: true); + Moniker = GetTargetFrameworkMoniker(Identifier, version); Profile = profile; IsPortableClassLibrary = identifier == DotNetPortableIdentifier; } @@ -52,6 +53,11 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// public string Identifier { get; } + /// + /// Gets the target framework moniker. Can be null if not supported. + /// + public string Moniker { get; } + /// /// Gets the target framework version, e.g. "v4.5". /// @@ -72,19 +78,61 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// public bool IsPortableClassLibrary { get; } - static string GetVersionString(int version) + static string GetTargetFrameworkMoniker(string frameworkIdentifier, int version) + { + // Reference: https://docs.microsoft.com/en-us/dotnet/standard/frameworks + switch (frameworkIdentifier) { + case null: + case ".NETFramework": + return "net" + GetVersionString(version, withDots: false); + + case ".NETCoreApp": + return "netcoreapp" + GetVersionString(version, withDots: true); + + case ".NETStandard": + return "netstandard" + GetVersionString(version, withDots: true); + + case "Silverlight": + return "sl" + version / 100; + + case ".NETCore": + return "netcore" + GetVersionString(version, withDots: false); + + case "WindowsPhone": + return "wp" + GetVersionString(version, withDots: false, omitMinorWhenZero: true); + + case ".NETMicroFramework": + return "netmf"; + + default: + return null; + } + } + + static string GetVersionString(int version, bool withDots, bool omitMinorWhenZero = false) { int major = version / 100; int minor = version % 100 / 10; int patch = version % 10; + if (omitMinorWhenZero && minor == 0 && patch == 0) { + return major.ToString(); + } + var versionBuilder = new StringBuilder(8); versionBuilder.Append(major); - versionBuilder.Append('.'); + + if (withDots) { + versionBuilder.Append('.'); + } + versionBuilder.Append(minor); if (patch != 0) { - versionBuilder.Append('.'); + if (withDots) { + versionBuilder.Append('.'); + } + versionBuilder.Append(patch); } diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index cc6b4503e..56394502f 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -103,7 +103,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler ProjectGuid = projectGuid; AssemblyResolver = assemblyResolver ?? throw new ArgumentNullException(nameof(assemblyResolver)); DebugInfoProvider = debugInfoProvider; - projectWriter = ProjectFileWriterDefault.Create(); + projectWriter = Settings.UseSdkStyleProjectFormat ? ProjectFileWriterSdkStyle.Create() : ProjectFileWriterDefault.Create(); } // per-run members diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 1a62ad667..b207c5f6d 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -64,6 +64,7 @@ +