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
{