// Copyright (c) 2020 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Xml;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Util;
#nullable enable
namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
{
///
/// A implementation that creates the projects in the SDK style format.
///
public class ProjectFileWriterSdkStyle : IProjectFileWriter
{
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",
"System",
"System.Diagnostics.Debug",
"System.Diagnostics.Tools",
"System.Drawing",
"System.Runtime",
"System.Runtime.Extensions",
};
///
/// Item types that must not be listed as project items: source files are covered by the
/// SDK's default Compile glob, the icon and the manifest are written as properties, and
/// embedded resources have their own remove/include phase.
///
static readonly HashSet ItemTypesWrittenElsewhere = new HashSet {
"ApplicationIcon",
"ApplicationManifest",
"Compile",
"EmbeddedResource",
};
///
/// 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.
///
public static ProjectFileWriterSdkStyle Default { get; } = new();
///
public void Write(
TextWriter target,
IProjectInfoProvider project,
IEnumerable files,
MetadataFile module)
{
using (XmlTextWriter xmlWriter = new XmlTextWriter(target))
{
xmlWriter.Formatting = Formatting.Indented;
Write(xmlWriter, project, files, module);
}
}
void Write(XmlTextWriter xml, IProjectInfoProvider project, IEnumerable files, MetadataFile module)
{
xml.WriteStartElement("Project");
var projectType = GetProjectType(module);
xml.WriteAttributeString("Sdk", GetSdkString(projectType, TargetServices.DetectTargetFramework(module)));
using (new Group(xml, "PropertyGroup"))
{
WriteAssemblyInfo(xml, module, project, projectType);
}
using (new Group(xml, "PropertyGroup"))
{
WriteProjectInfo(xml, project);
}
using (new Group(xml, "PropertyGroup"))
{
WriteMiscellaneousPropertyGroup(xml, files);
}
var customProperties = GetCustomProperties(project, files, module);
if (customProperties != null)
{
using (new Group(xml, "PropertyGroup"))
{
foreach (var (name, value) in customProperties)
{
xml.WriteElementString(name, value);
}
}
}
using (new Group(xml, "ItemGroup"))
{
WriteResources(xml, files);
}
using (new Group(xml, "ItemGroup"))
{
WriteReferences(xml, module, project);
}
xml.WriteEndElement();
}
void WriteAssemblyInfo(XmlTextWriter xml, MetadataFile module, IProjectInfoProvider project, ProjectType projectType)
{
xml.WriteElementString("AssemblyName", module.Name);
// Since we create AssemblyInfo.cs manually, we need to disable the auto-generation
xml.WriteElementString("GenerateAssemblyInfo", FalseString);
string platformName;
CorFlags flags;
if (module is PEFile { Reader.PEHeaders: var headers } peFile)
{
WriteOutputType(xml, headers.IsDll, headers.PEHeader!.Subsystem, projectType);
platformName = TargetServices.GetPlatformName(peFile);
flags = headers.CorHeader!.Flags;
}
else
{
WriteOutputType(xml, isDll: true, Subsystem.Unknown, projectType);
platformName = AnyCpuString;
flags = 0;
}
WriteDesktopExtensions(xml, projectType);
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)
{
xml.WriteElementString("PlatformTarget", platformName);
}
if (platformName == AnyCpuString && (flags & CorFlags.Prefers32Bit) != 0)
{
xml.WriteElementString("Prefer32Bit", TrueString);
}
}
///
/// 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.
///
/// The module for which to get the target framework moniker.
/// The project information provider.
/// The target framework moniker.
/// Thrown if the target framework moniker cannot be determined.
protected virtual string GetTargetFrameworkMoniker(MetadataFile module, IProjectInfoProvider project)
{
var targetFramework = TargetServices.DetectTargetFramework(module);
if (targetFramework.Identifier == ".NETFramework" && targetFramework.VersionNumber == 200)
targetFramework = TargetServices.DetectTargetFrameworkNET20(module, project.AssemblyResolver, targetFramework);
if (targetFramework.Moniker == null)
{
throw new NotSupportedException($"Cannot decompile this assembly to a SDK style project. Use default project format instead.");
}
return targetFramework.Moniker;
}
static void WriteOutputType(XmlTextWriter xml, bool isDll, Subsystem moduleSubsystem, ProjectType projectType)
{
if (!isDll)
{
switch (moduleSubsystem)
{
case Subsystem.WindowsGui:
xml.WriteElementString("OutputType", "WinExe");
break;
case Subsystem.WindowsCui:
xml.WriteElementString("OutputType", "Exe");
break;
}
}
else
{
// 'Library' is default, so only need to specify output type for executables (excludes ProjectType.Web)
if (projectType.HasFlag(ProjectType.Web))
{
xml.WriteElementString("OutputType", "Library");
}
}
}
static void WriteDesktopExtensions(XmlTextWriter xml, ProjectType projectType)
{
if (projectType.HasFlag(ProjectType.Wpf))
{
xml.WriteElementString("UseWPF", TrueString);
}
if (projectType.HasFlag(ProjectType.WinForms))
{
xml.WriteElementString("UseWindowsForms", TrueString);
}
}
static void WriteProjectInfo(XmlTextWriter xml, IProjectInfoProvider project)
{
xml.WriteElementString("LangVersion", project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.'));
xml.WriteElementString("AllowUnsafeBlocks", TrueString);
xml.WriteElementString("CheckForOverflowUnderflow", project.CheckForOverflowUnderflow ? TrueString : FalseString);
if (project.StrongNameKeyFile != null)
{
xml.WriteElementString("SignAssembly", TrueString);
xml.WriteElementString("AssemblyOriginatorKeyFile", Path.GetFileName(project.StrongNameKeyFile));
}
}
static void WriteMiscellaneousPropertyGroup(XmlTextWriter xml, IEnumerable files)
{
var (itemType, fileName) = files.FirstOrDefault(t => t.ItemType == "ApplicationIcon");
if (fileName != null)
xml.WriteElementString("ApplicationIcon", fileName);
(itemType, fileName) = files.FirstOrDefault(t => t.ItemType == "ApplicationManifest");
if (fileName != null)
xml.WriteElementString("ApplicationManifest", fileName);
if (files.Any(t => t.ItemType == "EmbeddedResource"))
xml.WriteElementString("RootNamespace", string.Empty);
// TODO: We should add CustomToolNamespace for resources, otherwise we should add empty RootNamespace
}
///
/// Gets custom properties to be added to the project file. Override this method to provide additional properties.
///
/// The project information provider.
/// The collection of project item information.
/// The metadata file representing the module.
/// An enumerable of custom properties as name-value pairs. Null if no custom properties are provided.
protected virtual IEnumerable<(string, string)>? GetCustomProperties(IProjectInfoProvider project, IEnumerable files, MetadataFile module)
{
return null;
}
static void WriteResources(XmlTextWriter xml, IEnumerable files)
{
// remove phase
foreach (var item in files.Where(t => t.ItemType == "EmbeddedResource"))
{
string buildAction = Path.GetExtension(item.FileName).ToUpperInvariant() switch {
".CS" => "Compile",
".RESX" => "EmbeddedResource",
_ => "None"
};
if (buildAction == "EmbeddedResource")
continue;
xml.WriteStartElement(buildAction);
xml.WriteAttributeString("Remove", item.FileName);
xml.WriteEndElement();
}
// include phase
foreach (var item in files.Where(t => t.ItemType == "EmbeddedResource"))
{
if (Path.GetExtension(item.FileName) == ".resx")
continue;
xml.WriteStartElement("EmbeddedResource");
xml.WriteAttributeString("Include", item.FileName);
if (item.AdditionalProperties != null)
{
foreach (var (key, value) in item.AdditionalProperties)
xml.WriteAttributeString(key, value);
}
xml.WriteEndElement();
}
// Every other item type the export produced, "Page" for XAML recovered from BAML above
// all. Files the project does not list are files the exported project cannot build.
foreach (var group in files.Where(t => !ItemTypesWrittenElsewhere.Contains(t.ItemType))
.GroupBy(t => t.ItemType).OrderBy(g => g.Key, StringComparer.Ordinal))
{
var items = group.OrderBy(t => t.FileName, StringComparer.OrdinalIgnoreCase).ToList();
// remove phase: an SDK glob may already have claimed these files - a WPF project
// (UseWPF) globs **/*.xaml into Page - and the same file in an item type twice is
// a build error. Removing first, in the same item group, keeps the explicit item
// with its metadata and drops the globbed one.
foreach (var item in items)
{
xml.WriteStartElement(group.Key);
xml.WriteAttributeString("Remove", item.FileName);
xml.WriteEndElement();
}
// include phase
foreach (var item in items)
{
xml.WriteStartElement(group.Key);
xml.WriteAttributeString("Include", item.FileName);
if (item.AdditionalProperties != null)
{
foreach (var (key, value) in item.AdditionalProperties)
xml.WriteAttributeString(key, value);
}
xml.WriteEndElement();
}
}
}
void WriteReferences(XmlTextWriter xml, MetadataFile module, IProjectInfoProvider project)
{
foreach (var reference in GetReferences(module, project))
{
WriteReference(xml, reference, project);
}
}
///
/// Gets the assembly references for the specified module and project, excluding implicit references and shared assemblies.
///
/// The module for which to get the assembly references.
/// The project information provider.
/// An enumerable of assembly references.
protected virtual IEnumerable GetReferences(MetadataFile module, IProjectInfoProvider project)
{
bool isNetCoreApp = TargetServices.DetectTargetFramework(module).Identifier == NetCoreAppIdentifier;
var projectType = GetProjectType(module);
var targetPacks = new HashSet();
if (isNetCoreApp)
{
targetPacks.Add("Microsoft.NETCore.App");
if ((projectType & ProjectType.Desktop) != 0)
{
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 (IsSuppliedBySdk(reference.Name, projectType))
{
continue;
}
if (isNetCoreApp && project.AssemblyReferenceClassifier.IsSharedAssembly(reference, out string? runtimePack) && targetPacks.Contains(runtimePack))
{
continue;
}
yield return reference;
}
}
///
/// 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.
///
/// The XML writer used to write the project file.
/// The assembly reference to write.
/// The project information provider.
protected virtual void WriteReference(XmlTextWriter xml, AssemblyReference reference, IProjectInfoProvider project)
{
xml.WriteStartElement("Reference");
xml.WriteAttributeString("Include", reference.Name);
var assembly = project.AssemblyResolver.Resolve(reference);
if (assembly != null && !project.AssemblyReferenceClassifier.IsGacAssembly(reference))
{
xml.WriteElementString("HintPath", FileUtility.GetRelativePath(project.TargetDirectory, assembly.FileName));
}
xml.WriteEndElement();
}
static string GetSdkString(ProjectType projectType, TargetFramework targetFramework)
{
if (projectType.HasFlag(ProjectType.Web))
{
// 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))
{
projectType |= ProjectType.Web;
}
else if (referenceName == PresentationFrameworkName)
{
projectType |= ProjectType.Wpf;
}
else if (referenceName == WindowsFormsName)
{
projectType |= ProjectType.WinForms;
}
}
return projectType;
}
readonly struct Group : IDisposable
{
readonly XmlTextWriter xml;
public Group(XmlTextWriter xml, string name)
{
this.xml = xml;
xml.WriteStartElement(name);
}
public void Dispose()
{
xml.WriteEndElement();
}
}
}
}