Browse Source

Merge pull request #4103 from icsharpcode/fix/wpf-project-export-gaps

#3315, #2253: make exported WPF projects build without hand edits
pull/4104/head
Siegfried Pammer 2 weeks ago committed by GitHub
parent
commit
b30eb779e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 449
      ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs
  2. 220
      ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
  3. 53
      ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs
  4. 21
      ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
  5. 5
      ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs
  6. 2
      ILSpy/Languages/CSharpLanguage.cs
  7. 7
      ILSpy/Languages/IResourceFileHandler.cs
  8. 2
      ILSpy/TreeNodes/BamlResourceNodeFactory.cs

449
ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs

@ -0,0 +1,449 @@ @@ -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;
/// <summary>
/// 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.
/// </summary>
[TestFixture]
public sealed class WpfProjectExportTests
{
/// <summary>
/// The nine assemblies the .NET SDK references by itself for a project that sets UseWPF
/// (item _WpfCommonNetFxReference in Microsoft.NET.Sdk.WindowsDesktop.props).
/// </summary>
static readonly string[] UseWpfReferences = {
"PresentationCore",
"PresentationFramework",
"System.Windows.Controls.Ribbon",
"System.Xaml",
"UIAutomationClient",
"UIAutomationClientSideProviders",
"UIAutomationProvider",
"UIAutomationTypes",
"WindowsBase",
};
readonly List<MetadataFile> openedModules = new();
readonly Dictionary<string, string> 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);
}
/// <summary>
/// NETSDK1136: a .NET 5+ project that sets UseWPF or UseWindowsForms is rejected outright
/// unless its target framework names the Windows platform.
/// </summary>
[Test]
public void WpfTargetFrameworkKeepsThePlatformSuffix()
{
string project = WriteProjectFile(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: "Windows7.0"));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain("<UseWPF>True</UseWPF>"));
Assert.That(project, Does.Contain("<TargetFramework>net10.0-windows7.0</TargetFramework>"));
}
}
/// <summary>
/// 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.
/// </summary>
[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($"<TargetFramework>{expectedMoniker}</TargetFramework>"));
}
/// <summary>
/// Assemblies built before the platform-suffixed monikers existed carry no TargetPlatform
/// attribute; a WPF project still has to name Windows to build at all.
/// </summary>
[Test]
public void WpfWithoutTargetPlatformAttributeStillNamesWindows()
{
string project = WriteProjectFile(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: null));
Assert.That(project, Does.Contain("<TargetFramework>net10.0-windows</TargetFramework>"));
}
/// <summary>
/// Platform suffixes only exist for .NET 5 and later. A .NET Framework moniker that grew one
/// would no longer resolve to any target pack.
/// </summary>
[Test]
public void NetFrameworkMonikerTakesNoPlatformSuffix()
{
string project = WriteProjectFile(WpfApplication(targetFramework: ".NETFramework,Version=v4.7.2", targetPlatform: "Windows7.0"));
Assert.That(project, Does.Contain("<TargetFramework>net472</TargetFramework>"));
}
/// <summary>
/// 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.
/// </summary>
[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("<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>"));
Assert.That(project, Does.Contain("<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>"));
}
}
/// <summary>
/// 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.
/// </summary>
[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($@"<Project Sdk=""{expectedSdk}"">"));
}
/// <summary>
/// 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.
/// </summary>
[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"));
}
}
/// <summary>
/// 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).
/// </summary>
[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");
}
}
/// <summary>
/// A library has no entry point for the markup compiler to generate a Main into; an Application
/// subclass shipped inside one stays a Page.
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
[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"));
}
}
/// <summary>
/// Every assembly of the UseWPF set is a duplicate reference (MSB3243) once the project sets
/// the property.
/// </summary>
[Test]
public void WpfProjectDropsEveryReferenceUseWpfSupplies()
{
string project = WriteProjectFile(LibraryReferencing(UseWpfReferences));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain("<UseWPF>True</UseWPF>"));
foreach (string name in UseWpfReferences)
{
Assert.That(project, Does.Not.Contain(name), $"{name} is supplied by UseWPF");
}
}
}
/// <summary>
/// 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.
/// </summary>
[Test]
public void AssemblyUsingWpfAndWindowsFormsSetsBothProperties()
{
string project = WriteProjectFile(LibraryReferencing("PresentationFramework", "System.Windows.Forms", "System.Drawing"));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain("<UseWPF>True</UseWPF>"));
Assert.That(project, Does.Contain("<UseWindowsForms>True</UseWindowsForms>"));
Assert.That(project, Does.Not.Contain("PresentationFramework"));
Assert.That(project, Does.Not.Contain("System.Windows.Forms"));
Assert.That(project, Does.Not.Contain("System.Drawing"));
}
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>
/// 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.
/// </summary>
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<MetadataReference> 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<ProjectItemInfo>(), 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;
}
}

220
ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs

@ -38,22 +38,50 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -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";
/// <summary>
/// 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.
/// </summary>
static readonly HashSet<string> WpfImplicitReferences = new HashSet<string> {
"PresentationCore",
"PresentationFramework",
"System.Windows.Controls.Ribbon",
"System.Xaml",
"UIAutomationClient",
"UIAutomationClientSideProviders",
"UIAutomationProvider",
"UIAutomationTypes",
"WindowsBase",
};
/// <summary>
/// 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.
/// </summary>
static readonly HashSet<string> ImplicitReferences = new HashSet<string> {
"mscorlib",
"netstandard",
"PresentationFramework",
"System",
"System.Diagnostics.Debug",
"System.Diagnostics.Tools",
"System.Drawing",
"System.Runtime",
"System.Runtime.Extensions",
"System.Windows.Forms",
"System.Xaml",
};
/// <summary>
@ -68,7 +96,20 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -68,7 +96,20 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
"EmbeddedResource",
};
enum ProjectType { Default, WinForms, Wpf, Web }
/// <summary>
/// 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.
/// </summary>
[Flags]
enum ProjectType
{
Default = 0,
WinForms = 1,
Wpf = 2,
Web = 4,
Desktop = WinForms | Wpf,
}
/// <summary>
/// Gets the default instance of the <see cref="ProjectFileWriterSdkStyle"/> class.
@ -94,7 +135,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -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 @@ -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 @@ -169,6 +222,68 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The lower-case platform, or null when the moniker takes none.</returns>
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();
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Splits a platform such as "windows10.0.17763.0" into its name and its version.
/// </summary>
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));
}
/// <summary>
/// Gets the target framework moniker for the specified module and project.
/// </summary>
@ -207,7 +322,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -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 @@ -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 @@ -349,26 +464,25 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
/// <returns>An enumerable of assembly references.</returns>
protected virtual IEnumerable<AssemblyReference> 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<string>();
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 @@ -380,6 +494,28 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Writes an assembly reference to the project file.
/// </summary>
@ -400,41 +536,51 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -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

53
ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs

@ -22,6 +22,7 @@ using System.Linq; @@ -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 @@ -91,6 +92,58 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
return new TargetFramework(targetFrameworkIdentifier, versionNumber, targetFrameworkProfile);
}
/// <summary>
/// Gets the OS platform the specified <paramref name="module"/> was built for, as recorded by
/// <c>[assembly: TargetPlatform(...)]</c>, e.g. "Windows7.0". Null if the assembly names no platform.
/// </summary>
public static string DetectTargetPlatform(MetadataFile module)
{
return GetAssemblyAttributeArgument(module, "System.Runtime.Versioning.TargetPlatformAttribute");
}
/// <summary>
/// Gets the lowest OS platform version the specified <paramref name="module"/> supports, as recorded
/// by <c>[assembly: SupportedOSPlatform(...)]</c>, e.g. "Windows10.0.17763.0". Null if the assembly
/// declares no minimum.
/// </summary>
public static string DetectSupportedOSPlatform(MetadataFile module)
{
return GetAssemblyAttributeArgument(module, "System.Runtime.Versioning.SupportedOSPlatformAttribute");
}
/// <summary>
/// Reads the single string argument of an assembly-level attribute of the given type.
/// </summary>
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;
}
/// <summary>
/// Gets the string representation (name) of the target platform of the specified <paramref name="module"/>.
/// </summary>

21
ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs

@ -1177,6 +1177,27 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -1177,6 +1177,27 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
{
return TargetServices.DetectTargetFramework(module).Moniker != null;
}
/// <summary>
/// Determines whether the XAML file whose root object is <paramref name="rootType"/> belongs
/// into an MSBuild &lt;ApplicationDefinition&gt; item instead of a &lt;Page&gt; 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
/// <c>Application</c> subclass - must not get one.
/// </summary>
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)

5
ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs

@ -83,7 +83,10 @@ namespace ICSharpCode.ILSpyCmd @@ -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)

2
ILSpy/Languages/CSharpLanguage.cs

@ -616,7 +616,7 @@ namespace ICSharpCode.ILSpy.Languages @@ -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 };
}

7
ILSpy/Languages/IResourceFileHandler.cs

@ -68,6 +68,13 @@ namespace ICSharpCode.ILSpy.Languages @@ -68,6 +68,13 @@ namespace ICSharpCode.ILSpy.Languages
public DecompilationOptions DecompilationOptions { get; }
/// <summary>
/// MSBuild item type for the entry currently being written, when it differs from the
/// handler's <see cref="IResourceFileHandler.EntryType"/>. XAML recovered from BAML is a
/// "Page", except for the file defining the application, which is an "ApplicationDefinition".
/// </summary>
public string? EntryTypeOverride { get; set; }
public ResourceFileHandlerContext(DecompilationOptions options)
{
this.DecompilationOptions = options;

2
ILSpy/TreeNodes/BamlResourceNodeFactory.cs

@ -92,6 +92,8 @@ namespace ICSharpCode.ILSpy.Baml @@ -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
{

Loading…
Cancel
Save