diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs new file mode 100644 index 000000000..50b30d556 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs @@ -0,0 +1,449 @@ +// Copyright (c) 2026 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.Text; +using System.Threading; + +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +using NUnit.Framework; + +namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler; + +/// +/// Exporting a WPF assembly has to produce a project the .NET SDK actually builds. The inputs are +/// compiled on the fly: what the export writes depends on the assembly references and the +/// versioning attributes of the module, so the tests need real modules carrying them, not stubs. +/// +[TestFixture] +public sealed class WpfProjectExportTests +{ + /// + /// The nine assemblies the .NET SDK references by itself for a project that sets UseWPF + /// (item _WpfCommonNetFxReference in Microsoft.NET.Sdk.WindowsDesktop.props). + /// + static readonly string[] UseWpfReferences = { + "PresentationCore", + "PresentationFramework", + "System.Windows.Controls.Ribbon", + "System.Xaml", + "UIAutomationClient", + "UIAutomationClientSideProviders", + "UIAutomationProvider", + "UIAutomationTypes", + "WindowsBase", + }; + + readonly List openedModules = new(); + readonly Dictionary stubAssemblies = new(); + string tempDirectory; + string presentationFramework; + string presentationCore; + int assemblyCounter; + + [OneTimeSetUp] + public void SetUp() + { + tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + + // Stand-ins for the two WPF assemblies the exporter recognizes by name. They only have to + // carry the right assembly name and the types the fixture assemblies derive from; the real + // ones are not available on non-Windows hosts, which is exactly the situation that made the + // export depend on the machine it ran on. + presentationFramework = CompileTo("PresentationFramework.dll", "PresentationFramework", """ + namespace System.Windows + { + public class Application { } + public class Window { } + } + namespace Stubs + { + public class PresentationFramework { } + } + """); + presentationCore = CompileTo("PresentationCore.dll", "PresentationCore", """ + namespace System.Windows.Media + { + public class Brush { } + } + namespace Stubs + { + public class PresentationCore { } + } + """); + stubAssemblies.Add("PresentationFramework", presentationFramework); + stubAssemblies.Add("PresentationCore", presentationCore); + } + + [OneTimeTearDown] + public void TearDown() + { + foreach (var module in openedModules) + module.Dispose(); + if (Directory.Exists(tempDirectory)) + Directory.Delete(tempDirectory, recursive: true); + } + + /// + /// NETSDK1136: a .NET 5+ project that sets UseWPF or UseWindowsForms is rejected outright + /// unless its target framework names the Windows platform. + /// + [Test] + public void WpfTargetFrameworkKeepsThePlatformSuffix() + { + string project = WriteProjectFile(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: "Windows7.0")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project, Does.Contain("True")); + Assert.That(project, Does.Contain("net10.0-windows7.0")); + } + } + + /// + /// The platform belongs to the assembly, not to WPF: a plain library built for Windows keeps + /// its suffix as well, and one built for no particular platform does not grow one. + /// + [TestCase("Windows7.0", "net10.0-windows7.0")] + [TestCase(null, "net10.0")] + public void PlainLibraryFollowsItsTargetPlatformAttribute(string targetPlatform, string expectedMoniker) + { + string project = WriteProjectFile(Compile("PlainLibrary", + AssemblyAttributes(".NETCoreApp,Version=v10.0", targetPlatform) + "public class C { }")); + + Assert.That(project, Does.Contain($"{expectedMoniker}")); + } + + /// + /// Assemblies built before the platform-suffixed monikers existed carry no TargetPlatform + /// attribute; a WPF project still has to name Windows to build at all. + /// + [Test] + public void WpfWithoutTargetPlatformAttributeStillNamesWindows() + { + string project = WriteProjectFile(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: null)); + + Assert.That(project, Does.Contain("net10.0-windows")); + } + + /// + /// Platform suffixes only exist for .NET 5 and later. A .NET Framework moniker that grew one + /// would no longer resolve to any target pack. + /// + [Test] + public void NetFrameworkMonikerTakesNoPlatformSuffix() + { + string project = WriteProjectFile(WpfApplication(targetFramework: ".NETFramework,Version=v4.7.2", targetPlatform: "Windows7.0")); + + Assert.That(project, Does.Contain("net472")); + } + + /// + /// An assembly whose minimum supported OS version is lower than the version it targets says so + /// through SupportedOSPlatform; without TargetPlatformMinVersion the exported project would + /// silently raise its own floor to the target version. + /// + [Test] + public void LowerSupportedOSPlatformBecomesTargetPlatformMinVersion() + { + string project = WriteProjectFile(Compile("VersionedLibrary", + AssemblyAttributes(".NETCoreApp,Version=v10.0", "Windows10.0.19041.0", supportedOSPlatform: "Windows10.0.17763.0") + + "public class C { }")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project, Does.Contain("net10.0-windows10.0.19041.0")); + Assert.That(project, Does.Contain("10.0.17763.0")); + } + } + + /// + /// NETSDK1137: Microsoft.NET.Sdk carries the Windows Desktop targets itself since .NET 5, and + /// says so whenever a project still asks for the separate SDK. + /// + [TestCase(".NETCoreApp,Version=v10.0", "Microsoft.NET.Sdk")] + [TestCase(".NETFramework,Version=v4.7.2", "Microsoft.NET.Sdk")] + [TestCase(".NETCoreApp,Version=v3.1", "Microsoft.NET.Sdk.WindowsDesktop")] + public void WindowsDesktopSdkOnlyForNetCore3(string targetFramework, string expectedSdk) + { + string project = WriteProjectFile(WpfApplication(targetFramework, targetPlatform: null)); + + Assert.That(project, Does.Contain($@"")); + } + + /// + /// UseWPF brings in the whole Windows Desktop framework reference. Listing one of its + /// assemblies again is a duplicate reference (MSB3243), or a missing one (MSB3245) when the + /// hint path does not survive the move to the machine that rebuilds the project. + /// + [Test] + public void ReferencesSuppliedByUseWpfAreNotListed() + { + string project = WriteProjectFile(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: "Windows7.0")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project, Does.Not.Contain("PresentationCore")); + Assert.That(project, Does.Not.Contain("PresentationFramework")); + } + } + + /// + /// The markup compiler generates the program entry point into the application definition, so + /// the XAML file of the Application subclass is the one item that must not be a Page (#2253). + /// + [Test] + public void ApplicationSubclassOfAnExecutableIsTheApplicationDefinition() + { + var (module, typeSystem) = Load(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: "Windows7.0")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.App"), module), Is.True); + Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.MainWindow"), module), Is.False, + "a Window is a Page like any other"); + } + } + + /// + /// A library has no entry point for the markup compiler to generate a Main into; an Application + /// subclass shipped inside one stays a Page. + /// + [Test] + public void ApplicationSubclassOfALibraryIsNotTheApplicationDefinition() + { + var (module, typeSystem) = Load(Compile("WpfLibrary", """ + namespace Fixture + { + public class App : System.Windows.Application { } + } + """, OutputKind.DynamicallyLinkedLibrary, presentationFramework)); + + Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.App"), module), Is.False); + } + + /// + /// UseWPF is what makes System.Xaml implicit; an assembly that uses the XAML services without + /// WPF gets no UseWPF, so nothing supplies the reference and the export has to keep it. + /// + [Test] + public void SystemXamlIsKeptWhenTheAssemblyIsNotWpf() + { + string project = WriteProjectFile(LibraryReferencing("System.Xaml")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project, Does.Contain(@"Include=""System.Xaml""")); + Assert.That(project, Does.Not.Contain("UseWPF")); + } + } + + /// + /// Every assembly of the UseWPF set is a duplicate reference (MSB3243) once the project sets + /// the property. + /// + [Test] + public void WpfProjectDropsEveryReferenceUseWpfSupplies() + { + string project = WriteProjectFile(LibraryReferencing(UseWpfReferences)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project, Does.Contain("True")); + foreach (string name in UseWpfReferences) + { + Assert.That(project, Does.Not.Contain(name), $"{name} is supplied by UseWPF"); + } + } + } + + /// + /// WPF and Windows Forms are not alternatives: an assembly can use both, and then both sets of + /// implicit references are supplied only if both properties are written. + /// + [Test] + public void AssemblyUsingWpfAndWindowsFormsSetsBothProperties() + { + string project = WriteProjectFile(LibraryReferencing("PresentationFramework", "System.Windows.Forms", "System.Drawing")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(project, Does.Contain("True")); + Assert.That(project, Does.Contain("True")); + Assert.That(project, Does.Not.Contain("PresentationFramework")); + Assert.That(project, Does.Not.Contain("System.Windows.Forms")); + Assert.That(project, Does.Not.Contain("System.Drawing")); + } + } + + /// + /// The interop assembly is implicit only where both worlds meet; a project that sets just one + /// of the two properties has to reference it itself. + /// + [Test] + public void WindowsFormsIntegrationIsImplicitOnlyWhenBothAreInPlay() + { + string wpfOnly = WriteProjectFile(LibraryReferencing("PresentationFramework", "WindowsFormsIntegration")); + string windowsFormsOnly = WriteProjectFile(LibraryReferencing("System.Windows.Forms", "WindowsFormsIntegration")); + string both = WriteProjectFile(LibraryReferencing("PresentationFramework", "System.Windows.Forms", "WindowsFormsIntegration")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(wpfOnly, Does.Contain(@"Include=""WindowsFormsIntegration""")); + Assert.That(windowsFormsOnly, Does.Contain(@"Include=""WindowsFormsIntegration""")); + Assert.That(both, Does.Not.Contain("WindowsFormsIntegration")); + } + } + + static ITypeDefinition FindType(IDecompilerTypeSystem typeSystem, string fullTypeName) + { + var type = typeSystem.FindType(new FullTypeName(fullTypeName)).GetDefinition(); + Assert.That(type, Is.Not.Null, $"the fixture assembly is expected to contain {fullTypeName}"); + return type; + } + + string WpfApplication(string targetFramework, string targetPlatform) + { + return Compile("WpfFixture", AssemblyAttributes(targetFramework, targetPlatform) + """ + namespace Fixture + { + public class App : System.Windows.Application { } + public class MainWindow : System.Windows.Window + { + public System.Windows.Media.Brush Brush; + } + public static class Program + { + public static void Main() { } + } + } + """, OutputKind.WindowsApplication, presentationFramework, presentationCore); + } + + /// + /// Compiles a .NET Framework library referencing exactly the named assemblies. Which references + /// the export keeps is decided by assembly name, so the referenced assemblies only need the + /// right name and one public type each to survive as a reference in the compiled metadata. + /// + string LibraryReferencing(params string[] assemblyNames) + { + string fields = string.Join(Environment.NewLine, + assemblyNames.Select((name, index) => $"public Stubs.{MarkerTypeName(name)} field{index};")); + return Compile("ReferenceFixture", + AssemblyAttributes(".NETFramework,Version=v4.7.2", targetPlatform: null) + + "public class UsesEverything { " + fields + " }", + OutputKind.DynamicallyLinkedLibrary, + assemblyNames.Select(Stub).ToArray()); + } + + string Stub(string assemblyName) + { + if (!stubAssemblies.TryGetValue(assemblyName, out string path)) + { + path = CompileTo(assemblyName + ".dll", assemblyName, + $"namespace Stubs {{ public class {MarkerTypeName(assemblyName)} {{ }} }}"); + stubAssemblies.Add(assemblyName, path); + } + return path; + } + + static string MarkerTypeName(string assemblyName) => assemblyName.Replace(".", "_"); + + static string AssemblyAttributes(string targetFramework, string targetPlatform, string supportedOSPlatform = null) + { + var attributes = new StringBuilder(); + attributes.AppendLine($"[assembly: System.Runtime.Versioning.TargetFramework(\"{targetFramework}\")]"); + if (targetPlatform != null) + attributes.AppendLine($"[assembly: System.Runtime.Versioning.TargetPlatform(\"{targetPlatform}\")]"); + if (supportedOSPlatform != null) + attributes.AppendLine($"[assembly: System.Runtime.Versioning.SupportedOSPlatform(\"{supportedOSPlatform}\")]"); + return attributes.ToString(); + } + + /// + /// Each compilation lands in a file of its own: the modules stay open until the fixture is torn + /// down, so the same path must not be written twice. + /// + string Compile(string assemblyName, string source, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, params string[] references) + { + string extension = outputKind == OutputKind.DynamicallyLinkedLibrary ? ".dll" : ".exe"; + string fileName = assemblyName + "." + Interlocked.Increment(ref assemblyCounter) + extension; + return CompileTo(fileName, assemblyName, source, outputKind, references); + } + + string CompileTo(string fileName, string assemblyName, string source, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, params string[] references) + { + var compilation = CSharpCompilation.Create(assemblyName, + new[] { CSharpSyntaxTree.ParseText(SourceText.From(source, Encoding.UTF8)) }, + RuntimeReferences.Concat(references.Select(r => MetadataReference.CreateFromFile(r))), + new CSharpCompilationOptions(outputKind, deterministic: true)); + + string fullPath = Path.Combine(tempDirectory, fileName); + var result = compilation.Emit(fullPath); + Assert.That(result.Success, Is.True, () => string.Join(Environment.NewLine, result.Diagnostics)); + return fullPath; + } + + static IEnumerable RuntimeReferences + => ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")) + .Split(Path.PathSeparator) + .Where(path => path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + .Select(path => MetadataReference.CreateFromFile(path)); + + (MetadataFile Module, IDecompilerTypeSystem TypeSystem) Load(string fileName) + { + var module = OpenModule(fileName); + return (module, new DecompilerTypeSystem(module, new UniversalAssemblyResolver(fileName, throwOnError: false, targetFramework: null))); + } + + MetadataFile OpenModule(string fileName) + { + var module = new PEFile(fileName); + openedModules.Add(module); + return module; + } + + string WriteProjectFile(string fileName) + { + StringWriter output = new(); + ProjectFileWriterSdkStyle.Default.Write(output, new TestProjectInfoProvider(tempDirectory), Array.Empty(), OpenModule(fileName)); + return output.ToString(); + } + + sealed class TestProjectInfoProvider(string targetDirectory) : IProjectInfoProvider + { + public IAssemblyResolver AssemblyResolver { get; } = new UniversalAssemblyResolver(null, false, null); + public IAssemblyReferenceClassifier AssemblyReferenceClassifier { get; } = new AssemblyReferenceClassifier(); + public CSharp.LanguageVersion LanguageVersion => CSharp.LanguageVersion.Latest; + public bool CheckForOverflowUnderflow => false; + public Guid ProjectGuid { get; } = Guid.NewGuid(); + public string TargetDirectory => targetDirectory; + public string StrongNameKeyFile => null; + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs index 5af87d04c..73ac74f5a 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -38,22 +38,50 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler const string AspNetCorePrefix = "Microsoft.AspNetCore"; const string PresentationFrameworkName = "PresentationFramework"; const string WindowsFormsName = "System.Windows.Forms"; + const string WindowsFormsIntegrationName = "WindowsFormsIntegration"; + const string NetCoreAppIdentifier = ".NETCoreApp"; + const string WindowsPlatformName = "Windows"; const string TrueString = "True"; const string FalseString = "False"; const string AnyCpuString = "AnyCPU"; + /// + /// References the .NET SDK adds by itself when UseWPF is set, both through the implicit + /// Microsoft.WindowsDesktop.App framework reference on .NET Core and through the implicit + /// .NET Framework references. Listing them a second time is a duplicate reference (MSB3243), + /// or an unresolvable one (MSB3245) once the hint path no longer points anywhere. + /// Membership must not be decided by probing the machine that runs the export: the + /// Windows Desktop runtime pack is absent on non-Windows hosts, and the exported project + /// has to come out the same everywhere. + /// The SDK gates the version-specific members of this set on the target framework version, + /// which an assembly satisfies by construction: it can only reference what its own target + /// framework ships. + /// + static readonly HashSet WpfImplicitReferences = new HashSet { + "PresentationCore", + "PresentationFramework", + "System.Windows.Controls.Ribbon", + "System.Xaml", + "UIAutomationClient", + "UIAutomationClientSideProviders", + "UIAutomationProvider", + "UIAutomationTypes", + "WindowsBase", + }; + + /// + /// References the SDK adds for every project, whatever it uses: they are either implicit + /// for .NET Framework targets or part of the Microsoft.NETCore.App shared framework. + /// 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", }; /// @@ -68,7 +96,20 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler "EmbeddedResource", }; - enum ProjectType { Default, WinForms, Wpf, Web } + /// + /// What an assembly uses of the Windows desktop stacks. WPF and Windows Forms are not + /// alternatives: an assembly can use both, and each brings its own set of implicit + /// references and its own SDK property. + /// + [Flags] + enum ProjectType + { + Default = 0, + WinForms = 1, + Wpf = 2, + Web = 4, + Desktop = WinForms | Wpf, + } /// /// Gets the default instance of the class. @@ -94,7 +135,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler xml.WriteStartElement("Project"); var projectType = GetProjectType(module); - xml.WriteAttributeString("Sdk", GetSdkString(projectType)); + xml.WriteAttributeString("Sdk", GetSdkString(projectType, TargetServices.DetectTargetFramework(module))); using (new Group(xml, "PropertyGroup")) { @@ -155,7 +196,19 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler WriteDesktopExtensions(xml, projectType); - xml.WriteElementString("TargetFramework", GetTargetFrameworkMoniker(module, project)); + string moniker = GetTargetFrameworkMoniker(module, project); + string? platform = GetTargetPlatform(module, projectType); + if (platform != null) + { + moniker += "-" + platform; + } + xml.WriteElementString("TargetFramework", moniker); + + string? minVersion = platform != null ? GetTargetPlatformMinVersion(module, platform) : null; + if (minVersion != null) + { + xml.WriteElementString("TargetPlatformMinVersion", minVersion); + } // 'AnyCPU' is default, so only need to specify platform if it differs if (platformName != AnyCpuString) @@ -169,6 +222,68 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler } } + /// + /// Gets the target platform part of the target framework moniker, e.g. "windows7.0" in + /// "net10.0-windows7.0". Only .NET 5 and later monikers carry one; "net472" or + /// "netcoreapp3.1" with a platform suffix names no target pack at all. + /// + /// The lower-case platform, or null when the moniker takes none. + static string? GetTargetPlatform(MetadataFile module, ProjectType projectType) + { + var targetFramework = TargetServices.DetectTargetFramework(module); + if (targetFramework.Identifier != NetCoreAppIdentifier || targetFramework.VersionNumber < 500) + { + return null; + } + + string? platform = TargetServices.DetectTargetPlatform(module); + if (platform == null && (projectType & ProjectType.Desktop) != 0) + { + // Assemblies built before platform-suffixed monikers existed carry no + // TargetPlatformAttribute, but WPF and Windows Forms are Windows-only and the SDK + // rejects the project outright (NETSDK1136) unless the moniker says so. + platform = WindowsPlatformName; + } + + return platform?.ToLowerInvariant(); + } + + /// + /// Gets the lowest platform version the assembly supports, when it is lower than the version + /// it targets. Without it the exported project would raise its own floor to the target version. + /// + static string? GetTargetPlatformMinVersion(MetadataFile module, string platform) + { + string? supported = TargetServices.DetectSupportedOSPlatform(module)?.ToLowerInvariant(); + if (supported == null) + { + return null; + } + + var (supportedName, supportedVersion) = SplitPlatform(supported); + var (platformName, platformVersion) = SplitPlatform(platform); + if (supportedName != platformName || supportedVersion.Length == 0 || supportedVersion == platformVersion) + { + return null; + } + + return supportedVersion; + } + + /// + /// Splits a platform such as "windows10.0.17763.0" into its name and its version. + /// + static (string Name, string Version) SplitPlatform(string platform) + { + int versionStart = 0; + while (versionStart < platform.Length && !char.IsDigit(platform[versionStart])) + { + versionStart++; + } + + return (platform.Substring(0, versionStart), platform.Substring(versionStart)); + } + /// /// Gets the target framework moniker for the specified module and project. /// @@ -207,7 +322,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler else { // 'Library' is default, so only need to specify output type for executables (excludes ProjectType.Web) - if (projectType == ProjectType.Web) + if (projectType.HasFlag(ProjectType.Web)) { xml.WriteElementString("OutputType", "Library"); } @@ -216,11 +331,11 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler static void WriteDesktopExtensions(XmlTextWriter xml, ProjectType projectType) { - if (projectType == ProjectType.Wpf) + if (projectType.HasFlag(ProjectType.Wpf)) { xml.WriteElementString("UseWPF", TrueString); } - else if (projectType == ProjectType.WinForms) + if (projectType.HasFlag(ProjectType.WinForms)) { xml.WriteElementString("UseWindowsForms", TrueString); } @@ -349,26 +464,25 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// An enumerable of assembly references. protected virtual IEnumerable GetReferences(MetadataFile module, IProjectInfoProvider project) { - bool isNetCoreApp = TargetServices.DetectTargetFramework(module).Identifier == ".NETCoreApp"; + bool isNetCoreApp = TargetServices.DetectTargetFramework(module).Identifier == NetCoreAppIdentifier; + var projectType = GetProjectType(module); var targetPacks = new HashSet(); if (isNetCoreApp) { targetPacks.Add("Microsoft.NETCore.App"); - switch (GetProjectType(module)) + if ((projectType & ProjectType.Desktop) != 0) { - case ProjectType.WinForms: - case ProjectType.Wpf: - targetPacks.Add("Microsoft.WindowsDesktop.App"); - break; - case ProjectType.Web: - targetPacks.Add("Microsoft.AspNetCore.App"); - targetPacks.Add("Microsoft.AspNetCore.All"); - break; + targetPacks.Add("Microsoft.WindowsDesktop.App"); + } + if (projectType.HasFlag(ProjectType.Web)) + { + targetPacks.Add("Microsoft.AspNetCore.App"); + targetPacks.Add("Microsoft.AspNetCore.All"); } } foreach (var reference in module.AssemblyReferences) { - if (ImplicitReferences.Contains(reference.Name)) + if (IsSuppliedBySdk(reference.Name, projectType)) { continue; } @@ -380,6 +494,28 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler } } + /// + /// Determines whether the SDK adds the named reference by itself, given what the project uses. + /// Each set matches an _SDKImplicitReference group of the SDK and carries the same condition. + /// + static bool IsSuppliedBySdk(string referenceName, ProjectType projectType) + { + if (ImplicitReferences.Contains(referenceName)) + { + return true; + } + if (projectType.HasFlag(ProjectType.Wpf) && WpfImplicitReferences.Contains(referenceName)) + { + return true; + } + if (projectType.HasFlag(ProjectType.WinForms) && referenceName == WindowsFormsName) + { + return true; + } + // The interop assembly is implicit only where both stacks meet. + return projectType.HasFlag(ProjectType.Desktop) && referenceName == WindowsFormsIntegrationName; + } + /// /// Writes an assembly reference to the project file. /// @@ -400,41 +536,51 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler xml.WriteEndElement(); } - static string GetSdkString(ProjectType projectType) + static string GetSdkString(ProjectType projectType, TargetFramework targetFramework) { - switch (projectType) + if (projectType.HasFlag(ProjectType.Web)) { - case ProjectType.WinForms: - case ProjectType.Wpf: - return "Microsoft.NET.Sdk.WindowsDesktop"; - case ProjectType.Web: - return "Microsoft.NET.Sdk.Web"; - default: - return "Microsoft.NET.Sdk"; + // A project names a single SDK, and Microsoft.NET.Sdk.Web is the wider one: it + // imports Microsoft.NET.Sdk, which carries the desktop targets UseWPF and + // UseWindowsForms need. The desktop SDK carries no web targets, so an assembly + // that looks like both is exported as a web project. + return "Microsoft.NET.Sdk.Web"; } + + // Microsoft.NET.Sdk carries the Windows Desktop targets itself since .NET 5 and + // warns (NETSDK1137) about projects that still name the separate SDK; only + // .NET Core 3.x, where the desktop targets are not imported for a plain + // framework moniker, still needs it. + if ((projectType & ProjectType.Desktop) != 0 + && targetFramework.Identifier == NetCoreAppIdentifier + && targetFramework.VersionNumber >= 300 && targetFramework.VersionNumber < 500) + { + return "Microsoft.NET.Sdk.WindowsDesktop"; + } + + return "Microsoft.NET.Sdk"; } static ProjectType GetProjectType(MetadataFile module) { + var projectType = ProjectType.Default; foreach (var referenceName in module.AssemblyReferences.Select(r => r.Name)) { if (referenceName.StartsWith(AspNetCorePrefix, StringComparison.Ordinal)) { - return ProjectType.Web; + projectType |= ProjectType.Web; } - - if (referenceName == PresentationFrameworkName) + else if (referenceName == PresentationFrameworkName) { - return ProjectType.Wpf; + projectType |= ProjectType.Wpf; } - - if (referenceName == WindowsFormsName) + else if (referenceName == WindowsFormsName) { - return ProjectType.WinForms; + projectType |= ProjectType.WinForms; } } - return ProjectType.Default; + return projectType; } readonly struct Group : IDisposable diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs index dbd9eae1e..dc7060df5 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs @@ -22,6 +22,7 @@ using System.Linq; using System.Reflection.PortableExecutable; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { @@ -91,6 +92,58 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler return new TargetFramework(targetFrameworkIdentifier, versionNumber, targetFrameworkProfile); } + /// + /// Gets the OS platform the specified was built for, as recorded by + /// [assembly: TargetPlatform(...)], e.g. "Windows7.0". Null if the assembly names no platform. + /// + public static string DetectTargetPlatform(MetadataFile module) + { + return GetAssemblyAttributeArgument(module, "System.Runtime.Versioning.TargetPlatformAttribute"); + } + + /// + /// Gets the lowest OS platform version the specified supports, as recorded + /// by [assembly: SupportedOSPlatform(...)], e.g. "Windows10.0.17763.0". Null if the assembly + /// declares no minimum. + /// + public static string DetectSupportedOSPlatform(MetadataFile module) + { + return GetAssemblyAttributeArgument(module, "System.Runtime.Versioning.SupportedOSPlatformAttribute"); + } + + /// + /// Reads the single string argument of an assembly-level attribute of the given type. + /// + static string GetAssemblyAttributeArgument(MetadataFile module, string attributeTypeName) + { + if (module is null) + { + throw new ArgumentNullException(nameof(module)); + } + + var metadata = module.Metadata; + foreach (var handle in metadata.GetCustomAttributes(System.Reflection.Metadata.Handle.AssemblyDefinition)) + { + try + { + var attribute = metadata.GetCustomAttribute(handle); + if (attribute.GetAttributeType(metadata).GetFullTypeName(metadata).ToString() != attributeTypeName) + continue; + var blobReader = metadata.GetBlobReader(attribute.Value); + if (blobReader.ReadUInt16() == 0x0001) + { + return blobReader.ReadSerializedString(); + } + } + catch (BadImageFormatException) + { + // ignore malformed attributes + } + } + + return null; + } + /// /// Gets the string representation (name) of the target platform of the specified . /// diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index 3f5da58e5..5cb703218 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -1177,6 +1177,27 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { return TargetServices.DetectTargetFramework(module).Moniker != null; } + + /// + /// Determines whether the XAML file whose root object is belongs + /// into an MSBuild <ApplicationDefinition> item instead of a <Page> item. + /// The WPF markup compiler generates the program entry point from the application definition, + /// so a module that has no entry point of its own - a library that happens to contain an + /// Application subclass - must not get one. + /// + public static bool IsApplicationDefinition(ITypeDefinition? rootType, MetadataFile? module) + { + if (rootType == null) + return false; + if (module is not PEFile { Reader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress: not 0 }) + return false; + foreach (var baseType in rootType.GetNonInterfaceBaseTypes()) + { + if (baseType.FullName == "System.Windows.Application") + return true; + } + return false; + } } public record struct ProjectItemInfo(string ItemType, string FileName) diff --git a/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs b/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs index c03334e09..4f450a35f 100644 --- a/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs +++ b/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs @@ -83,7 +83,10 @@ namespace ICSharpCode.ILSpyCmd Directory.CreateDirectory(directory); result.Xaml.Save(fullPath); - var item = new ProjectItemInfo("Page", xamlFileName) + string itemType = IsApplicationDefinition(typeDefinition, bamlTypeSystem.MainModule.MetadataFile) + ? "ApplicationDefinition" + : "Page"; + var item = new ProjectItemInfo(itemType, xamlFileName) .With("Generator", "MSBuild:Compile") .With("SubType", "Designer"); if (partialTypeInfo != null) diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 10a46c120..5b77f9310 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -616,7 +616,7 @@ namespace ICSharpCode.ILSpy.Languages continue; entryStream.Position = 0; fileName = handler.WriteResourceToFile(assembly, fileName, entryStream, context); - var item = new ProjectItemInfo(handler.EntryType, fileName) { PartialTypes = context.PartialTypes }; + var item = new ProjectItemInfo(context.EntryTypeOverride ?? handler.EntryType, fileName) { PartialTypes = context.PartialTypes }; item = item.With(context.AdditionalProperties); return new[] { item }; } diff --git a/ILSpy/Languages/IResourceFileHandler.cs b/ILSpy/Languages/IResourceFileHandler.cs index 98ebddd1b..5937dc206 100644 --- a/ILSpy/Languages/IResourceFileHandler.cs +++ b/ILSpy/Languages/IResourceFileHandler.cs @@ -68,6 +68,13 @@ namespace ICSharpCode.ILSpy.Languages public DecompilationOptions DecompilationOptions { get; } + /// + /// MSBuild item type for the entry currently being written, when it differs from the + /// handler's . XAML recovered from BAML is a + /// "Page", except for the file defining the application, which is an "ApplicationDefinition". + /// + public string? EntryTypeOverride { get; set; } + public ResourceFileHandlerContext(DecompilationOptions options) { this.DecompilationOptions = options; diff --git a/ILSpy/TreeNodes/BamlResourceNodeFactory.cs b/ILSpy/TreeNodes/BamlResourceNodeFactory.cs index 8d38f741d..def7ad3b9 100644 --- a/ILSpy/TreeNodes/BamlResourceNodeFactory.cs +++ b/ILSpy/TreeNodes/BamlResourceNodeFactory.cs @@ -92,6 +92,8 @@ namespace ICSharpCode.ILSpy.Baml foreach (var member in result.GeneratedMembers) partialTypeInfo.AddDeclaredMember(member); context.AddPartialTypeInfo(partialTypeInfo); + if (WholeProjectDecompiler.IsApplicationDefinition(typeDefinition, typeSystem.MainModule.MetadataFile)) + context.EntryTypeOverride = "ApplicationDefinition"; } else {