diff --git a/src/tools/illink/src/ILLink.Tasks/LinkTask.cs b/src/tools/illink/src/ILLink.Tasks/LinkTask.cs new file mode 100644 index 00000000000..b89b893be68 --- /dev/null +++ b/src/tools/illink/src/ILLink.Tasks/LinkTask.cs @@ -0,0 +1,511 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace ILLink.Tasks +{ + public class ILLink : ToolTask + { + /// + /// Paths to the assembly files that should be considered as + /// input to the linker. + /// Optional metadata: + /// TrimMode ("copy", "link", etc...): sets the illink action to take for this assembly. + /// There is an optional metadata for each optimization that can be set to "True" or "False" to + /// enable or disable it per-assembly: + /// BeforeFieldInit + /// OverrideRemoval + /// UnreachableBodies + /// UnusedInterfaces + /// IPConstProp + /// Sealer + /// Optional metadata "TrimmerSingleWarn" may also be set to "True"/"False" to control + /// whether the linker produces granular warnings for this assembly. + /// Maps to '-reference', and possibly '--action', '--enable-opt', '--disable-opt', '--verbose' + /// + [Required] + public ITaskItem[] AssemblyPaths { get; set; } + + /// + /// Paths to assembly files that are reference assemblies, + /// representing the surface area for compilation. + /// Maps to '-reference', with action set to 'skip' via '--action'. + /// + public ITaskItem[] ReferenceAssemblyPaths { get; set; } + + /// + /// The names of the assemblies to root. This should contain + /// assembly names without an extension, not file names or + /// paths. The default is to root everything in these assemblies. + // For more fine-grained control, set RootMode metadata. + /// + [Required] + public ITaskItem[] RootAssemblyNames { get; set; } + + /// + /// The directory in which to place linked assemblies. + /// Maps to '-out'. + /// + [Required] + public ITaskItem OutputDirectory { get; set; } + + /// + /// The subset of warnings that have to be turned off. + /// Maps to '--nowarn'. + /// + public string NoWarn { get; set; } + + /// + /// The warning version to use. + /// Maps to '--warn'. + /// + public string Warn { get; set; } + + /// + /// Treat all warnings as errors. + /// Maps to '--warnaserror' if true, '--warnaserror-' if false. + /// + public bool TreatWarningsAsErrors { set => _treatWarningsAsErrors = value; } + bool? _treatWarningsAsErrors; + + /// + /// Produce at most one trim analysis warning per assembly. + /// Maps to '--singlewarn' if true, '--singlewarn-' if false. + /// + public bool SingleWarn { set => _singleWarn = value; } + bool? _singleWarn; + + /// + /// The list of warnings to report as errors. + /// Maps to '--warnaserror LIST-OF-WARNINGS'. + /// + public string WarningsAsErrors { get; set; } + + /// + /// The list of warnings to report as usual. + /// Maps to '--warnaserror- LIST-OF-WARNINGS'. + /// + public string WarningsNotAsErrors { get; set; } + + /// + /// A list of XML root descriptor files specifying linker + /// roots at a granular level. See the dotnet/linker + /// documentation for details about the format. + /// Maps to '-x'. + /// + public ITaskItem[] RootDescriptorFiles { get; set; } + + /// + /// Boolean specifying whether to enable beforefieldinit optimization globally. + /// Maps to '--enable-opt beforefieldinit' or '--disable-opt beforefieldinit'. + /// + public bool BeforeFieldInit { set => _beforeFieldInit = value; } + bool? _beforeFieldInit; + + /// + /// Boolean specifying whether to enable overrideremoval optimization globally. + /// Maps to '--enable-opt overrideremoval' or '--disable-opt overrideremoval'. + /// + public bool OverrideRemoval { set => _overrideRemoval = value; } + bool? _overrideRemoval; + + /// + /// Boolean specifying whether to enable unreachablebodies optimization globally. + /// Maps to '--enable-opt unreachablebodies' or '--disable-opt unreachablebodies'. + /// + public bool UnreachableBodies { set => _unreachableBodies = value; } + bool? _unreachableBodies; + + /// + /// Boolean specifying whether to enable unusedinterfaces optimization globally. + /// Maps to '--enable-opt unusedinterfaces' or '--disable-opt unusedinterfaces'. + /// + public bool UnusedInterfaces { set => _unusedInterfaces = value; } + bool? _unusedInterfaces; + + /// + /// Boolean specifying whether to enable ipconstprop optimization globally. + /// Maps to '--enable-opt ipconstprop' or '--disable-opt ipconstprop'. + /// + public bool IPConstProp { set => _iPConstProp = value; } + bool? _iPConstProp; + + /// + /// A list of feature names used by the body substitution logic. + /// Each Item requires "Value" boolean metadata with the value of + /// the feature setting. + /// Maps to '--feature'. + /// + public ITaskItem[] FeatureSettings { get; set; } + + /// + /// Boolean specifying whether to enable sealer optimization globally. + /// Maps to '--enable-opt sealer' or '--disable-opt sealer'. + /// + public bool Sealer { set => _sealer = value; } + bool? _sealer; + + static readonly string[] _optimizationNames = new string[] { + "BeforeFieldInit", + "OverrideRemoval", + "UnreachableBodies", + "UnusedInterfaces", + "IPConstProp", + "Sealer" + }; + + /// + /// Custom data key-value pairs to pass to the linker. + /// The name of the item is the key, and the required "Value" + /// metadata is the value. Maps to '--custom-data key=value'. + /// + public ITaskItem[] CustomData { get; set; } + + /// + /// Extra arguments to pass to illink, delimited by spaces. + /// + public string ExtraArgs { get; set; } + + /// + /// Make illink dump dependencies file for linker analyzer tool. + /// Maps to '--dump-dependencies'. + /// + public bool DumpDependencies { get; set; } + + /// + /// Make illink dump dependencies to the specified file type. + /// Maps to '--dependencies-file-format'. + /// + public string DependenciesFileFormat { get; set; } + + /// + /// Remove debug symbols from linked assemblies. + /// Maps to '-b' if false. + /// Default if not specified is to remove symbols, like + /// the command-line. (Target files will likely set their own defaults to keep symbols.) + /// + public bool RemoveSymbols { set => _removeSymbols = value; } + bool? _removeSymbols; + + /// + /// Sets the default action for trimmable assemblies. + /// Maps to '--trim-mode' + /// + public string TrimMode { get; set; } + + /// + /// Sets the default action for assemblies which have not opted into trimming. + /// Maps to '--action' + /// + public string DefaultAction { get; set; } + + /// + /// A list of custom steps to insert into the linker pipeline. + /// Each ItemSpec should be the path to the assembly containing the custom step. + /// Each Item requires "Type" metadata with the name of the custom step type. + /// Optional metadata: + /// BeforeStep: The name of a linker step. The custom step will be inserted before it. + /// AfterStep: The name of a linker step. The custom step will be inserted after it. + /// The default (if neither BeforeStep or AfterStep is specified) is to insert the + /// custom step at the end of the pipeline. + /// It is an error to specify both BeforeStep and AfterStep. + /// Maps to '--custom-step'. + /// + public ITaskItem[] CustomSteps { get; set; } + + /// + /// A list selected metadata which should not be trimmed. It maps to 'keep-metadata' option + /// + public ITaskItem[] KeepMetadata { get; set; } + + private const string DotNetHostPathEnvironmentName = "DOTNET_HOST_PATH"; + + private string _dotnetPath; + + private string DotNetPath { + get { + if (!String.IsNullOrEmpty (_dotnetPath)) + return _dotnetPath; + + _dotnetPath = Environment.GetEnvironmentVariable (DotNetHostPathEnvironmentName); + if (String.IsNullOrEmpty (_dotnetPath)) + throw new InvalidOperationException ($"{DotNetHostPathEnvironmentName} is not set"); + + return _dotnetPath; + } + } + + + /// ToolTask implementation + + protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High; + + protected override string ToolName => Path.GetFileName (DotNetPath); + + protected override string GenerateFullPathToTool () => DotNetPath; + + private string _illinkPath = ""; + + public string ILLinkPath { + get { + if (!String.IsNullOrEmpty (_illinkPath)) + return _illinkPath; + + var taskDirectory = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location); + // The linker always runs on .NET Core, even when using desktop MSBuild to host ILLink.Tasks. + _illinkPath = Path.Combine (Path.GetDirectoryName (taskDirectory), "net7.0", "illink.dll"); + return _illinkPath; + } + set => _illinkPath = value; + } + + private static string Quote (string path) + { + return $"\"{path.TrimEnd ('\\')}\""; + } + + protected override string GenerateCommandLineCommands () + { + var args = new StringBuilder (); + args.Append (Quote (ILLinkPath)); + return args.ToString (); + } + + private static void SetOpt (StringBuilder args, string opt, bool enabled) + { + args.Append (enabled ? "--enable-opt " : "--disable-opt ").AppendLine (opt); + } + + private static void SetOpt (StringBuilder args, string opt, string assembly, bool enabled) + { + args.Append (enabled ? "--enable-opt " : "--disable-opt ").Append (opt).Append (' ').AppendLine (assembly); + } + + protected override string GenerateResponseFileCommands () + { + var args = new StringBuilder (); + + if (RootDescriptorFiles != null) { + foreach (var rootFile in RootDescriptorFiles) + args.Append ("-x ").AppendLine (Quote (rootFile.ItemSpec)); + } + + foreach (var assemblyItem in RootAssemblyNames) { + args.Append ("-a ").Append (Quote (assemblyItem.ItemSpec)); + + string rootMode = assemblyItem.GetMetadata ("RootMode"); + if (!string.IsNullOrEmpty (rootMode)) { + args.Append (' '); + args.Append (rootMode); + } + + args.AppendLine (); + } + + if (_singleWarn is bool generalSingleWarn) { + if (generalSingleWarn) + args.AppendLine ("--singlewarn"); + else + args.AppendLine ("--singlewarn-"); + } + + string trimMode = TrimMode switch { + "full" => "link", + "partial" => "link", + var x => x + }; + if (trimMode != null) + args.Append ("--trim-mode ").AppendLine (trimMode); + + string defaultAction = TrimMode switch { + "full" => "link", + "partial" => "copy", + _ => DefaultAction + }; + if (defaultAction != null) + args.Append ("--action ").AppendLine (defaultAction); + + + HashSet assemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); + foreach (var assembly in AssemblyPaths) { + var assemblyPath = assembly.ItemSpec; + var assemblyName = Path.GetFileNameWithoutExtension (assemblyPath); + + // If there are multiple paths with the same assembly name, only use the first one. + if (!assemblyNames.Add (assemblyName)) + continue; + + args.Append ("-reference ").AppendLine (Quote (assemblyPath)); + + string assemblyTrimMode = assembly.GetMetadata ("TrimMode"); + string isTrimmable = assembly.GetMetadata ("IsTrimmable"); + if (string.IsNullOrEmpty (assemblyTrimMode)) { + if (isTrimmable.Equals ("true", StringComparison.OrdinalIgnoreCase)) { + // isTrimmable ~= true + assemblyTrimMode = trimMode; + } else if (isTrimmable.Equals ("false", StringComparison.OrdinalIgnoreCase)) { + // isTrimmable ~= false + assemblyTrimMode = "copy"; + } + } + if (!string.IsNullOrEmpty (assemblyTrimMode)) { + args.Append ("--action "); + args.Append (assemblyTrimMode); + args.Append (' ').AppendLine (Quote (assemblyName)); + } + + // Add per-assembly optimization arguments + foreach (var optimization in _optimizationNames) { + string optimizationValue = assembly.GetMetadata (optimization); + if (String.IsNullOrEmpty (optimizationValue)) + continue; + + if (!Boolean.TryParse (optimizationValue, out bool enabled)) + throw new ArgumentException ($"optimization metadata {optimization} must be True or False"); + + SetOpt (args, optimization, assemblyName, enabled); + } + + // Add per-assembly verbosity arguments + string singleWarn = assembly.GetMetadata ("TrimmerSingleWarn"); + if (!String.IsNullOrEmpty (singleWarn)) { + if (!Boolean.TryParse (singleWarn, out bool value)) + throw new ArgumentException ($"TrimmerSingleWarn metadata must be True or False"); + + if (value) + args.Append ("--singlewarn ").AppendLine (Quote (assemblyName)); + else + args.Append ("--singlewarn- ").AppendLine (Quote (assemblyName)); + } + } + + if (ReferenceAssemblyPaths != null) { + foreach (var assembly in ReferenceAssemblyPaths) { + var assemblyPath = assembly.ItemSpec; + var assemblyName = Path.GetFileNameWithoutExtension (assemblyPath); + + // Don't process references for which we already have + // implementation assemblies. + if (assemblyNames.Contains (assemblyName)) + continue; + + args.Append ("-reference ").AppendLine (Quote (assemblyPath)); + + // Treat reference assemblies as "skip". Ideally we + // would not even look at the IL, but only use them to + // resolve surface area. + args.Append ("--action skip ").AppendLine (Quote (assemblyName)); + } + } + + if (OutputDirectory != null) + args.Append ("-out ").AppendLine (Quote (OutputDirectory.ItemSpec)); + + if (NoWarn != null) + args.Append ("--nowarn ").AppendLine (Quote (NoWarn)); + + if (Warn != null) + args.Append ("--warn ").AppendLine (Quote (Warn)); + + if (_treatWarningsAsErrors is bool treatWarningsAsErrors && treatWarningsAsErrors) + args.Append ("--warnaserror "); + else + args.Append ("--warnaserror- "); + + if (WarningsAsErrors != null) + args.Append ("--warnaserror ").AppendLine (Quote (WarningsAsErrors)); + + if (WarningsNotAsErrors != null) + args.Append ("--warnaserror- ").AppendLine (Quote (WarningsNotAsErrors)); + + // Add global optimization arguments + if (_beforeFieldInit is bool beforeFieldInit) + SetOpt (args, "beforefieldinit", beforeFieldInit); + + if (_overrideRemoval is bool overrideRemoval) + SetOpt (args, "overrideremoval", overrideRemoval); + + if (_unreachableBodies is bool unreachableBodies) + SetOpt (args, "unreachablebodies", unreachableBodies); + + if (_unusedInterfaces is bool unusedInterfaces) + SetOpt (args, "unusedinterfaces", unusedInterfaces); + + if (_iPConstProp is bool iPConstProp) + SetOpt (args, "ipconstprop", iPConstProp); + + if (_sealer is bool sealer) + SetOpt (args, "sealer", sealer); + + if (CustomData != null) { + foreach (var customData in CustomData) { + var key = customData.ItemSpec; + var value = customData.GetMetadata ("Value"); + if (String.IsNullOrEmpty (value)) + throw new ArgumentException ("custom data requires \"Value\" metadata"); + args.Append ("--custom-data ").Append (' ').Append (key).Append ('=').AppendLine (Quote (value)); + } + } + + if (FeatureSettings != null) { + foreach (var featureSetting in FeatureSettings) { + var feature = featureSetting.ItemSpec; + var featureValue = featureSetting.GetMetadata ("Value"); + if (String.IsNullOrEmpty (featureValue)) + throw new ArgumentException ("feature settings require \"Value\" metadata"); + args.Append ("--feature ").Append (feature).Append (' ').AppendLine (featureValue); + } + } + + if (KeepMetadata != null) { + foreach (var metadata in KeepMetadata) + args.Append ("--keep-metadata ").AppendLine (Quote (metadata.ItemSpec)); + } + + if (_removeSymbols == false) + args.AppendLine ("-b"); + + if (CustomSteps != null) { + foreach (var customStep in CustomSteps) { + args.Append ("--custom-step "); + var stepPath = customStep.ItemSpec; + var stepType = customStep.GetMetadata ("Type"); + if (stepType == null) + throw new ArgumentException ("custom step requires \"Type\" metadata"); + var customStepString = $"{stepType},{stepPath}"; + + // handle optional before/aftersteps + var beforeStep = customStep.GetMetadata ("BeforeStep"); + var afterStep = customStep.GetMetadata ("AfterStep"); + if (!String.IsNullOrEmpty (beforeStep) && !String.IsNullOrEmpty (afterStep)) + throw new ArgumentException ("custom step may not have both \"BeforeStep\" and \"AfterStep\" metadata"); + if (!String.IsNullOrEmpty (beforeStep)) + customStepString = $"-{beforeStep}:{customStepString}"; + if (!String.IsNullOrEmpty (afterStep)) + customStepString = $"+{afterStep}:{customStepString}"; + + args.AppendLine (Quote (customStepString)); + } + } + + if (ExtraArgs != null) + args.AppendLine (ExtraArgs); + + if (DumpDependencies) + args.AppendLine ("--dump-dependencies"); + + if (DependenciesFileFormat != null) { + args.Append ("--dependencies-file-format ").AppendLine (DependenciesFileFormat); + } + + return args.ToString (); + } + } +} diff --git a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets new file mode 100644 index 00000000000..108c5c25bd6 --- /dev/null +++ b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets @@ -0,0 +1,352 @@ + + + + + + $(IntermediateOutputPath)linked\ + $(IntermediateLinkDir)\ + + <_LinkSemaphore>$(IntermediateLinkDir)Link.semaphore + + + + + <_Parameter1>IsTrimmable + <_Parameter2>True + + + + + + false + false + false + false + false + false + false + false + + <_EnableConsumingManagedCodeFromNativeHosting Condition="'$(EnableCppCLIHostActivation)' == 'true'">true + <_EnableConsumingManagedCodeFromNativeHosting Condition="'$(_EnableConsumingManagedCodeFromNativeHosting)' == ''">false + true + + + + + + true + + true + + false + + + + + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --notrimwarn + false + + + + + $(NoWarn);IL2121 + + + + + + + + + <_LinkedResolvedFileToPublish Include="@(_LinkedResolvedFileToPublishCandidate)" Condition="Exists('%(Identity)')" /> + + + + + + + + <_RemovedManagedAssembly Include="@(ManagedAssemblyToLink)" Condition="!Exists('$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + + + + + + + + + + + + + + + + + + <_DotNetHostDirectory>$(NetCoreRoot) + <_DotNetHostFileName>dotnet + <_DotNetHostFileName Condition="$([MSBuild]::IsOSPlatform(`Windows`))">dotnet.exe + + + + + + + + + + + + + + + + + + + + + + 5 + 0 + + + + + + + + <_ILLinkSuppressions Include="$(MSBuildThisFileDirectory)6.0_suppressions.xml" /> + + + + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --link-attributes "@(_ILLinkSuppressions->'%(Identity)', '" --link-attributes "')" + + + + + copyused + partial + full + + + <_TrimmerDefaultAction Condition="$([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '7.0'))">$(TrimmerDefaultAction) + <_TrimmerDefaultAction Condition="'$(_TrimmerDefaultAction)' == '' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '6.0'))">$(TrimMode) + <_TrimmerDefaultAction Condition="'$(_TrimmerDefaultAction)' == '' And $([MSBuild]::VersionEquals($(TargetFrameworkVersion), '6.0'))">copy + + + + $(TreatWarningsAsErrors) + <_ExtraTrimmerArgs>--skip-unresolved true $(_ExtraTrimmerArgs) + true + + + + + + $(NoWarn);IL2008 + $(NoWarn);IL2009 + + $(NoWarn);IL2037 + + $(NoWarn);IL2009;IL2012 + + + + + <_ExtraTrimmerArgs>--enable-serialization-discovery $(_ExtraTrimmerArgs) + + + + + + true + false + + + + <_TrimmerUnreachableBodies>false + + + + + + true + + + + + + + + + + + + + + + + + + + + + + copy + + + + + + + <__SingleWarnIntermediateAssembly Include="@(ManagedAssemblyToLink)" /> + <__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" /> + + <_SingleWarnIntermediateAssembly Include="@(ManagedAssemblyToLink)" /> + <_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" /> + + <_SingleWarnIntermediateAssembly> + false + + + + + + + + false + + + + + <_TrimmerFeatureSettings Include="@(RuntimeHostConfigurationOption)" Condition="'%(RuntimeHostConfigurationOption.Trim)' == 'true'" /> + + + + + + + + + + + + + + + + <__PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(ManagedAssemblyToLink->'%(RelativeDir)%(Filename).pdb')" /> + <_PDBToLink Include="@(ResolvedFileToPublish)" Exclude="@(__PDBToLink)" /> + + + + <_LinkedResolvedFileToPublishCandidate Include="@(ManagedAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + <_LinkedResolvedFileToPublishCandidate Include="@(_PDBToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + + + + \ No newline at end of file diff --git a/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs b/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs new file mode 100644 index 00000000000..2bc77920e40 --- /dev/null +++ b/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs @@ -0,0 +1,202 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using ILLink.Shared; +using Mono.Cecil; + +namespace Mono.Linker.Steps +{ + public class RootAssemblyInput : BaseStep + { + readonly string fileName; + readonly AssemblyRootMode rootMode; + + public RootAssemblyInput (string fileName, AssemblyRootMode rootMode) + { + this.fileName = fileName; + this.rootMode = rootMode; + } + + protected override void Process () + { + AssemblyDefinition? assembly = LoadAssemblyFile (); + if (assembly == null) + return; + + var di = new DependencyInfo (DependencyKind.RootAssembly, assembly); + var origin = new MessageOrigin (assembly); + + AssemblyAction action = Context.Annotations.GetAction (assembly); + switch (action) { + case AssemblyAction.Copy: + Annotations.Mark (assembly.MainModule, di, origin); + // Mark Step will take care of marking whole assembly + return; + case AssemblyAction.CopyUsed: + case AssemblyAction.Link: + break; + default: + Context.LogError (null, DiagnosticId.RootAssemblyCannotUseAction, assembly.Name.ToString (), action.ToString ()); + return; + } + + switch (rootMode) { + case AssemblyRootMode.EntryPoint: + var ep = assembly.MainModule.EntryPoint; + if (ep == null) { + Context.LogError (null, DiagnosticId.RootAssemblyDoesNotHaveEntryPoint, assembly.Name.ToString ()); + return; + } + + Annotations.Mark (ep.DeclaringType, di, origin); + Annotations.AddPreservedMethod (ep.DeclaringType, ep); + break; + case AssemblyRootMode.VisibleMembers: + var preserve_visible = TypePreserveMembers.Visible; + if (MarkInternalsVisibleTo (assembly)) + preserve_visible |= TypePreserveMembers.Internal; + + MarkAndPreserve (assembly, preserve_visible); + break; + + case AssemblyRootMode.Library: + var preserve_library = TypePreserveMembers.Visible | TypePreserveMembers.Library; + if (MarkInternalsVisibleTo (assembly)) + preserve_library |= TypePreserveMembers.Internal; + + MarkAndPreserve (assembly, preserve_library); + + // Assembly root mode wins over any enabled optimization which + // could conflict with library rooting behaviour + Context.Optimizations.Disable ( + CodeOptimizations.Sealer | + CodeOptimizations.UnusedTypeChecks | + CodeOptimizations.UnreachableBodies | + CodeOptimizations.UnusedInterfaces | + CodeOptimizations.RemoveDescriptors | + CodeOptimizations.RemoveLinkAttributes | + CodeOptimizations.RemoveSubstitutions | + CodeOptimizations.RemoveDynamicDependencyAttribute | + CodeOptimizations.OptimizeTypeHierarchyAnnotations, assembly.Name.Name); + + // Enable EventSource special handling + Context.DisableEventSourceSpecialHandling = false; + + // No metadata trimming + Context.MetadataTrimming = MetadataTrimming.None; + break; + case AssemblyRootMode.AllMembers: + Context.Annotations.SetAction (assembly, AssemblyAction.Copy); + return; + } + } + + AssemblyDefinition? LoadAssemblyFile () + { + AssemblyDefinition? assembly; + + if (File.Exists (fileName)) { + assembly = Context.Resolver.GetAssembly (fileName); + AssemblyDefinition? loaded = Context.GetLoadedAssembly (assembly.Name.Name); + + // The same assembly could be already loaded if there are multiple inputs pointing to same file + if (loaded != null) + return loaded; + + Context.Resolver.CacheAssembly (assembly); + return assembly; + } + + // + // Quirks mode for netcore to support passing ambiguous assembly name + // + assembly = Context.TryResolve (fileName); + if (assembly == null) + Context.LogError (null, DiagnosticId.RootAssemblyCouldNotBeFound, fileName); + + return assembly; + } + + void MarkAndPreserve (AssemblyDefinition assembly, TypePreserveMembers preserve) + { + var module = assembly.MainModule; + if (module.HasExportedTypes) + foreach (var type in module.ExportedTypes) + MarkAndPreserve (assembly, type, preserve); + + foreach (var type in module.Types) + MarkAndPreserve (type, preserve); + } + + void MarkAndPreserve (TypeDefinition type, TypePreserveMembers preserve) + { + TypePreserveMembers preserve_anything = preserve; + if ((preserve & TypePreserveMembers.Visible) != 0 && !IsTypeVisible (type)) + preserve_anything &= ~TypePreserveMembers.Visible; + + if ((preserve & TypePreserveMembers.Internal) != 0 && IsTypePrivate (type)) + preserve_anything &= ~TypePreserveMembers.Internal; + + // Keep all interfaces and interface members in library mode + if ((preserve & TypePreserveMembers.Library) != 0 && type.IsInterface) { + Annotations.Mark (type, new DependencyInfo (DependencyKind.RootAssembly, type.Module.Assembly), new MessageOrigin (type.Module.Assembly)); + Annotations.SetPreserve (type, TypePreserve.All); + } + + switch (preserve_anything) { + case 0: + return; + case TypePreserveMembers.Library: + // + // In library mode private type can have members kept for serialization if + // the type is referenced + // + preserve = preserve_anything; + Annotations.SetMembersPreserve (type, preserve); + break; + default: + Annotations.Mark (type, new DependencyInfo (DependencyKind.RootAssembly, type.Module.Assembly), new MessageOrigin (type.Module.Assembly)); + Annotations.SetMembersPreserve (type, preserve); + break; + } + + if (!type.HasNestedTypes) + return; + + foreach (TypeDefinition nested in type.NestedTypes) + MarkAndPreserve (nested, preserve); + } + + void MarkAndPreserve (AssemblyDefinition assembly, ExportedType type, TypePreserveMembers preserve) + { + var di = new DependencyInfo (DependencyKind.RootAssembly, assembly); + var origin = new MessageOrigin (assembly); + Context.Annotations.Mark (type, di, origin); + Context.Annotations.Mark (assembly.MainModule, di, origin); + Annotations.SetMembersPreserve (type, preserve); + } + + static bool IsTypeVisible (TypeDefinition type) + { + return type.IsPublic || type.IsNestedPublic || type.IsNestedFamily || type.IsNestedFamilyOrAssembly; + } + + static bool IsTypePrivate (TypeDefinition type) + { + return type.IsNestedPrivate; + } + + bool MarkInternalsVisibleTo (AssemblyDefinition assembly) + { + foreach (CustomAttribute attribute in assembly.CustomAttributes) { + if (attribute.Constructor.DeclaringType.IsTypeOf ("System.Runtime.CompilerServices", "InternalsVisibleToAttribute")) { + Context.Annotations.Mark (attribute, new DependencyInfo (DependencyKind.RootAssembly, assembly)); + return true; + } + } + + return false; + } + } +} diff --git a/src/tools/illink/src/linker/Linker/AssemblyRootMode.cs b/src/tools/illink/src/linker/Linker/AssemblyRootMode.cs new file mode 100644 index 00000000000..c9a3d98a604 --- /dev/null +++ b/src/tools/illink/src/linker/Linker/AssemblyRootMode.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Mono.Linker +{ + public enum AssemblyRootMode + { + AllMembers = 0, + EntryPoint, + VisibleMembers, + Library + } +} diff --git a/src/tools/illink/src/linker/Linker/Driver.cs b/src/tools/illink/src/linker/Linker/Driver.cs new file mode 100644 index 00000000000..7b9e10bdda6 --- /dev/null +++ b/src/tools/illink/src/linker/Linker/Driver.cs @@ -0,0 +1,1422 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// +// Driver.cs +// +// Author: +// Jb Evain (jbevain@gmail.com) +// +// (C) 2006 Jb Evain +// +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using ILLink.Shared; +using Mono.Cecil; +using Mono.Linker.Steps; + +namespace Mono.Linker +{ + + public partial class Driver : IDisposable + { + const string resolvers = "-a|-x"; + const string _linker = "IL Linker"; + + public static int Main (string[] args) + { + LinkerEventSource.Log.LinkerStart (string.Join ("; ", args)); + if (args.Length == 0) { + Console.Error.WriteLine ("No parameters specified"); + LinkerEventSource.Log.LinkerStop (); + return 1; + } + + if (!ProcessResponseFile (args, out var arguments)) { + LinkerEventSource.Log.LinkerStop (); + return 1; + } + + try { + using (Driver driver = new Driver (arguments)) { + return driver.Run (); + } + } catch { + Console.Error.WriteLine ("Fatal error in {0}", _linker); + throw; + } finally { + LinkerEventSource.Log.LinkerStop (); + } + } + + readonly Queue arguments; + bool _needAddBypassNGenStep; + LinkContext? context; + protected LinkContext Context { + get { + Debug.Assert (context != null); + return context; + } + } + + public Driver (Queue arguments) + { + this.arguments = arguments; + } + + public static bool ProcessResponseFile (string[] args, out Queue result) + { + result = new Queue (); + foreach (string arg in args) { + if (arg.StartsWith ("@")) { + try { + string responseFileName = arg.Substring (1); + using (var responseFileText = new StreamReader (responseFileName)) + ParseResponseFile (responseFileText, result); + } catch (Exception e) when (e is IOException or ObjectDisposedException) { + Console.Error.WriteLine ("Cannot read response file due to '{0}'", e.Message); + return false; + } + } else { + result.Enqueue (arg); + } + } + + return true; + } + + public static void ParseResponseFile (TextReader reader, Queue result) + { + int cur; + while ((cur = reader.Read ()) >= 0) { + // skip whitespace + while (char.IsWhiteSpace ((char) cur)) { + if ((cur = reader.Read ()) < 0) + break; + } + if (cur < 0) // EOF + break; + + StringBuilder argBuilder = new StringBuilder (); + bool inquote = false; + + // build up an argument one character at a time + while (true) { + bool copyChar = true; + int numBackslash = 0; + + // count backslashes + while (cur == '\\') { + numBackslash++; + if ((cur = reader.Read ()) < 0) + break; + } + if (cur == '"') { + if ((numBackslash % 2) == 0) { + if (inquote && (reader.Peek () == '"')) { + // handle "" escape sequence in a quote + cur = reader.Read (); + } else { + // unescaped " begins/ends a quote + copyChar = false; + inquote = !inquote; + } + } + // treat backslashes before " as escapes + numBackslash /= 2; + } + if (numBackslash > 0) + argBuilder.Append (new String ('\\', numBackslash)); + if (cur < 0 || (!inquote && char.IsWhiteSpace ((char) cur))) + break; + if (copyChar) + argBuilder.Append ((char) cur); + cur = reader.Read (); + } + result.Enqueue (argBuilder.ToString ()); + } + } + + void ErrorMissingArgument (string optionName) + { + Context.LogError (null, DiagnosticId.MissingArgumentForCommanLineOptionName, optionName); + } + + public enum DependenciesFileFormat { Xml, Dgml }; + + // Perform setup of the LinkContext and parse the arguments. + // Return values: + // 0 => successfully set up context with all arguments + // 1 => argument processing stopped early without errors + // -1 => error setting up context + protected int SetupContext (ILogger? customLogger = null) + { + Pipeline p = GetStandardPipeline (); + context = GetDefaultContext (p, customLogger); + + var body_substituter_steps = new Stack (); + var xml_custom_attribute_steps = new Stack (); + var custom_steps = new List (); + var set_optimizations = new List<(CodeOptimizations, string?, bool)> (); + bool dumpDependencies = false; + string? dependenciesFileName = null; + context.StripSecurity = true; + bool new_mvid_used = false; + bool deterministic_used = false; + bool keepCompilersResources = false; + MetadataTrimming metadataTrimming = MetadataTrimming.Any; + DependenciesFileFormat fileType = DependenciesFileFormat.Xml; + + List inputs = CreateDefaultResolvers (); + + while (arguments.Count > 0) { + string token = arguments.Dequeue (); + if (token.Length < 2) { + context.LogError (null, DiagnosticId.UnrecognizedCommandLineOption, token); + return -1; + } + + // + // Handling of --value like options + // + if (token[0] == '-' && token[1] == '-') { + switch (token) { + case "--help": + Usage (); + return 1; + + case "--skip-unresolved": + if (!GetBoolParam (token, l => context.IgnoreUnresolved = l)) + return -1; + + continue; + + case "--verbose": + context.LogMessages = true; + continue; + + case "--dependencies-file": + if (!GetStringParam (token, out dependenciesFileName)) + return -1; + + continue; + + case "--dump-dependencies": + dumpDependencies = true; + continue; + + case "--dependencies-file-format": + if (!GetStringParam (token, out var dependenciesFileFormat)) + return -1; + + if (!Enum.TryParse (dependenciesFileFormat, ignoreCase: true, out fileType)) { + context.LogError (null, DiagnosticId.InvalidDependenciesFileFormat); + return -1; + } + continue; + + case "--reduced-tracing": + if (!GetBoolParam (token, l => context.EnableReducedTracing = l)) + return -1; + + continue; + + case "--used-attrs-only": + if (!GetBoolParam (token, l => context.KeepUsedAttributeTypesOnly = l)) + return -1; + + continue; + + case "--strip-security": + if (!GetBoolParam (token, l => context.StripSecurity = l)) + return -1; + + continue; + + case "--strip-descriptors": + if (!GetBoolParam (token, l => set_optimizations.Add ((CodeOptimizations.RemoveDescriptors, null, l)))) + return -1; + + continue; + + case "--strip-substitutions": + if (!GetBoolParam (token, l => set_optimizations.Add ((CodeOptimizations.RemoveSubstitutions, null, l)))) + return -1; + + continue; + + case "--strip-link-attributes": + if (!GetBoolParam (token, l => set_optimizations.Add ((CodeOptimizations.RemoveLinkAttributes, null, l)))) + return -1; + + continue; + + case "--substitutions": + if (arguments.Count < 1) { + ErrorMissingArgument (token); + return -1; + } + + if (!GetStringParam (token, out string? substitutionFile)) + return -1; + + body_substituter_steps.Push (substitutionFile); + + continue; + case "--explicit-reflection": + if (!GetBoolParam (token, l => context.AddReflectionAnnotations = l)) + return -1; + + continue; + + case "--action": { + if (!GetStringParam (token, out string? actionString)) + return -1; + + AssemblyAction? action = ParseAssemblyAction (actionString); + if (action == null) + return -1; + + string? assemblyName = GetNextStringValue (); + if (assemblyName == null) { + context.DefaultAction = action.Value; + continue; + } + + if (!IsValidAssemblyName (assemblyName)) { + context.LogError (null, DiagnosticId.InvalidAssemblyName, assemblyName); + return -1; + } + + context.RegisterAssemblyAction (assemblyName, action.Value); + continue; + } + case "--trim-mode": { + if (!GetStringParam (token, out string? actionString)) + return -1; + + AssemblyAction? action = ParseAssemblyAction (actionString); + if (action == null) + return -1; + + context.TrimAction = action.Value; + continue; + } + case "--custom-step": + if (!GetStringParam (token, out string? custom_step)) + return -1; + + custom_steps.Add (custom_step); + + continue; + + case "--custom-data": + if (arguments.Count < 1) { + ErrorMissingArgument (token); + return -1; + } + + var arg = arguments.Dequeue (); + string[] values = arg.Split ('='); + if (values?.Length != 2) { + context.LogError (null, DiagnosticId.CustomDataFormatIsInvalid); + return -1; + } + + context.SetCustomData (values[0], values[1]); + continue; + + case "--keep-compilers-resources": + if (!GetBoolParam (token, l => keepCompilersResources = l)) + return -1; + + continue; + + case "--keep-dep-attributes": + if (!GetBoolParam (token, l => set_optimizations.Add ((CodeOptimizations.RemoveDynamicDependencyAttribute, null, !l)))) + return -1; + + continue; + + case "--keep-metadata": { + if (!GetStringParam (token, out string? mname)) + return -1; + + if (!TryGetMetadataTrimming (mname, out var type)) + return -1; + + metadataTrimming &= ~type; + continue; + } + + case "--enable-serialization-discovery": + if (!GetBoolParam (token, l => context.EnableSerializationDiscovery = l)) + return -1; + + continue; + + case "--disable-operator-discovery": + if (!GetBoolParam (token, l => context.DisableOperatorDiscovery = l)) + return -1; + + continue; + + case "--ignore-descriptors": + if (!GetBoolParam (token, l => context.IgnoreDescriptors = l)) + return -1; + + continue; + + case "--ignore-substitutions": + if (!GetBoolParam (token, l => context.IgnoreSubstitutions = l)) + return -1; + + continue; + + case "--ignore-link-attributes": + if (!GetBoolParam (token, l => context.IgnoreLinkAttributes = l)) + return -1; + + continue; + + case "--disable-opt": { + if (!GetStringParam (token, out string? optName)) + return -1; + + if (!GetOptimizationName (optName, out var opt)) + return -1; + + string? assemblyName = GetNextStringValue (); + set_optimizations.Add ((opt, assemblyName, false)); + + continue; + } + case "--enable-opt": { + if (!GetStringParam (token, out string? optName)) + return -1; + + if (!GetOptimizationName (optName, out var opt)) + return -1; + + string? assemblyName = GetNextStringValue (); + set_optimizations.Add ((opt, assemblyName, true)); + + continue; + } + + case "--feature": { + if (!GetStringParam (token, out string? featureName)) + return -1; + + if (!GetBoolParam (token, value => { + context.SetFeatureValue (featureName, value); + })) + return -1; + + continue; + } + case "--new-mvid": + // + // This is not same as --deterministic which calculates MVID + // from stable assembly content. This option creates a new random + // mvid or uses mvid of the source assembly. + // + if (!GetBoolParam (token, l => { + if (!l) + p.RemoveStep (typeof (RegenerateGuidStep)); + })) + return -1; + + new_mvid_used = true; + continue; + + case "--deterministic": + if (!GetBoolParam (token, l => context.DeterministicOutput = l)) + return -1; + + deterministic_used = true; + continue; + + case "--output-assemblylist": + if (!GetStringParam (token, out string? assemblyListFile)) + return -1; + + context.AssemblyListFile = assemblyListFile; + + continue; + + case "--output-pinvokes": + if (!GetStringParam (token, out string? pinvokesListFile)) + return -1; + + context.PInvokesListFile = pinvokesListFile; + + continue; + + case "--link-attributes": + if (arguments.Count < 1) { + ErrorMissingArgument (token); + return -1; + } + + if (!GetStringParam (token, out string? fileList)) + return -1; + + foreach (string file in GetFiles (fileList)) + xml_custom_attribute_steps.Push (file); + + continue; + + case "--generate-warning-suppressions": + if (!GetStringParam (token, out string? generateWarningSuppressionsArgument)) + return -1; + + if (!GetWarningSuppressionWriterFileOutputKind (generateWarningSuppressionsArgument, out var fileOutputKind)) { + context.LogError (null, DiagnosticId.InvalidGenerateWarningSuppressionsValue, generateWarningSuppressionsArgument); + return -1; + } + + context.WarningSuppressionWriter = new WarningSuppressionWriter (context, fileOutputKind); + continue; + + case "--notrimwarn": + context.NoTrimWarn = true; + continue; + + case "--nowarn": + if (!GetStringParam (token, out string? noWarnArgument)) + return -1; + + context.NoWarn.UnionWith (ProcessWarningCodes (noWarnArgument)); + continue; + + case "--warnaserror": + case "--warnaserror+": + var warningList = GetNextStringValue (); + if (!string.IsNullOrEmpty (warningList)) { + foreach (var warning in ProcessWarningCodes (warningList)) + context.WarnAsError[warning] = true; + + } else { + context.GeneralWarnAsError = true; + context.WarnAsError.Clear (); + } + + continue; + + case "--warnaserror-": + warningList = GetNextStringValue (); + if (!string.IsNullOrEmpty (warningList)) { + foreach (var warning in ProcessWarningCodes (warningList)) + context.WarnAsError[warning] = false; + + } else { + context.GeneralWarnAsError = false; + context.WarnAsError.Clear (); + } + + continue; + + case "--warn": + if (!GetStringParam (token, out string? warnVersionArgument)) + return -1; + + if (!GetWarnVersion (warnVersionArgument, out WarnVersion version)) + return -1; + + context.WarnVersion = version; + + continue; + + case "--singlewarn": + case "--singlewarn+": { + string? assemblyName = GetNextStringValue (); + if (assemblyName != null) { + if (!IsValidAssemblyName (assemblyName)) { + context.LogError (null, DiagnosticId.InvalidAssemblyName, assemblyName); + return -1; + } + + context.SingleWarn[assemblyName] = true; + } else { + context.GeneralSingleWarn = true; + context.SingleWarn.Clear (); + } + + continue; + } + + case "--singlewarn-": { + string? assemblyName = GetNextStringValue (); + if (assemblyName != null) { + if (!IsValidAssemblyName (assemblyName)) { + context.LogError (null, DiagnosticId.InvalidAssemblyName, assemblyName); + return -1; + } + + context.SingleWarn[assemblyName] = false; + } else { + context.GeneralSingleWarn = false; + context.SingleWarn.Clear (); + } + + continue; + } + + case "--version": + Version (); + return 1; + + case "--about": + About (); + return 1; + } + } + + if (token[0] == '-' || token[1] == '/') { + + switch (token.Substring (1)) { + case "d": + if (!GetStringParam (token, out string? directory)) + return -1; + + DirectoryInfo info = new DirectoryInfo (directory); + context.Resolver.AddSearchDirectory (info.FullName); + + continue; + case "o": + case "out": + if (!GetStringParam (token, out string? outputDirectory)) + return -1; + + context.OutputDirectory = outputDirectory; + + continue; + case "x": { + if (!GetStringParam (token, out string? xmlFile)) + return -1; + + if (!File.Exists (xmlFile)) { + context.LogError (null, DiagnosticId.XmlDescriptorCouldNotBeFound, xmlFile); + return -1; + } + + inputs.Add (new ResolveFromXmlStep (File.OpenRead (xmlFile), xmlFile)); + continue; + } + case "a": { + if (!GetStringParam (token, out string? assemblyFile)) + return -1; + + if (!File.Exists (assemblyFile) && assemblyFile.EndsWith (".dll", StringComparison.InvariantCultureIgnoreCase)) { + context.LogError (null, DiagnosticId.RootAssemblyCouldNotBeFound, assemblyFile); + return -1; + } + + AssemblyRootMode rmode = AssemblyRootMode.AllMembers; + var rootMode = GetNextStringValue (); + if (rootMode != null) { + var parsed_rmode = ParseAssemblyRootMode (rootMode); + if (parsed_rmode is null) + return -1; + + rmode = parsed_rmode.Value; + } + + inputs.Add (new RootAssemblyInput (assemblyFile, rmode)); + continue; + } + case "b": + if (!GetBoolParam (token, l => context.LinkSymbols = l)) + return -1; + + continue; + case "g": + if (!GetBoolParam (token, l => context.DeterministicOutput = !l)) + return -1; + + continue; + case "z": + if (!GetBoolParam (token, l => context.IgnoreDescriptors = !l)) + return -1; + + continue; + case "?": + case "h": + case "help": + Usage (); + return 1; + + case "reference": + if (!GetStringParam (token, out string? reference)) + return -1; + + context.Resolver.AddReferenceAssembly (reference); + + continue; + } + } + + context.LogError (null, DiagnosticId.UnrecognizedCommandLineOption, token); + return -1; + } + + if (inputs.Count == 0) { + context.LogError (null, DiagnosticId.NoFilesToLinkSpecified, resolvers); + return -1; + } + + if (new_mvid_used && deterministic_used) { + context.LogError (null, DiagnosticId.NewMvidAndDeterministicCannotBeUsedAtSameTime); + return -1; + } + + context.MetadataTrimming = metadataTrimming; + + // Default to deterministic output + if (!new_mvid_used && !deterministic_used) { + context.DeterministicOutput = true; + } + if (dumpDependencies) { + switch (fileType) { + case DependenciesFileFormat.Xml: + AddXmlDependencyRecorder (context, dependenciesFileName); + break; + case DependenciesFileFormat.Dgml: + AddDgmlDependencyRecorder (context, dependenciesFileName); + break; + default: + context.LogError (null, DiagnosticId.InvalidDependenciesFileFormat); + break; + } + } + + + if (set_optimizations.Count > 0) { + foreach (var (opt, assemblyName, enable) in set_optimizations) { + if (enable) + context.Optimizations.Enable (opt, assemblyName); + else + context.Optimizations.Disable (opt, assemblyName); + } + } + + // + // Modify the default pipeline + // + + for (int i = inputs.Count; i != 0; --i) + p.PrependStep (inputs[i - 1]); + + foreach (var file in xml_custom_attribute_steps) + AddLinkAttributesStep (p, file); + + foreach (var file in body_substituter_steps) + AddBodySubstituterStep (p, file); + + if (context.DeterministicOutput) + p.RemoveStep (typeof (RegenerateGuidStep)); + + if (context.AddReflectionAnnotations) + p.AddStepAfter (typeof (MarkStep), new ReflectionBlockedStep ()); + + if (_needAddBypassNGenStep) + p.AddStepAfter (typeof (SweepStep), new AddBypassNGenStep ()); + + if (keepCompilersResources) { + p.RemoveStep (typeof (RemoveResourcesStep)); + } + + p.AddStepBefore (typeof (OutputStep), new SealerStep ()); + + // + // Pipeline setup with all steps enabled + // + // RootAssemblyInputStep or ResolveFromXmlStep [at least one of them] + // LinkAttributesStep [optional, possibly many] + // BodySubstituterStep [optional] + // MarkStep + // ReflectionBlockedStep [optional] + // RemoveResourcesStep [optional] + // ProcessWarningsStep + // OutputWarningSuppressions + // SweepStep + // AddBypassNGenStep [optional] + // CodeRewriterStep + // CleanStep + // RegenerateGuidStep [optional] + // SealerStep + // OutputStep + + if (context.EnableSerializationDiscovery) + p.MarkHandlers.Add (new DiscoverSerializationHandler ()); + + if (!context.DisableOperatorDiscovery) + p.MarkHandlers.Add (new DiscoverOperatorsHandler ()); + + foreach (string custom_step in custom_steps) { + if (!AddCustomStep (p, custom_step)) + return -1; + } + + return 0; + } + + // Returns the exit code of the process. 0 indicates success. + // Known non-recoverable errors (LinkerFatalErrorException) set the exit code + // to the error code. + // May propagate exceptions, which will result in the process getting an + // exit code determined by dotnet. + public int Run (ILogger? customLogger = null) + { + int setupStatus = SetupContext (customLogger); + if (setupStatus > 0) + return 0; + if (setupStatus < 0) + return 1; + + Pipeline p = Context.Pipeline; + PreProcessPipeline (p); + + try { + p.Process (Context); + } catch (Exception e) when (LogFatalError (e)) { + // Unreachable + throw; + } + + Context.FlushCachedWarnings (); + Context.Tracer.Finish (); + return Context.ErrorsCount > 0 ? 1 : 0; + } + + /// + /// This method is called in the exception filter for unexpected exceptions. + /// Prints error messages and returns false to avoid catching in the exception filter. + /// + bool LogFatalError (Exception e) + { + switch (e) { + case LinkerFatalErrorException lex: + Context.LogMessage (lex.MessageContainer); + Debug.Assert (lex.MessageContainer.Category == MessageCategory.Error); + Debug.Assert (lex.MessageContainer.Code != null); + Debug.Assert (lex.MessageContainer.Code.Value != 0); + break; + case ResolutionException re: + Context.LogError (null, DiagnosticId.FailedToResolveMetadataElement, re.Message); + break; + default: + Context.LogError (null, DiagnosticId.LinkerUnexpectedError); + break; + } + return false; + } + + partial void PreProcessPipeline (Pipeline pipeline); + + private static IEnumerable ProcessWarningCodes (string value) + { + string Unquote (string arg) + { + if (arg.Length > 1 && arg[0] == '"' && arg[arg.Length - 1] == '"') + return arg.Substring (1, arg.Length - 2); + + return arg; + } + + value = Unquote (value); + string[] values = value.Split (new char[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string v in values) { + var id = v.Trim (); + if (!id.StartsWith ("IL", StringComparison.Ordinal) || !ushort.TryParse (id.AsSpan (2), out ushort code)) + continue; + + yield return code; + } + } + + Assembly? GetCustomAssembly (string arg) + { + if (Path.IsPathRooted (arg)) { + var assemblyPath = Path.GetFullPath (arg); + if (File.Exists (assemblyPath)) { + // The CLR will return the already-loaded assembly if the same path is requested multiple times + // (or even if a different path specifies the "same" assembly, based on the MVID). + + // Ignore warning, since we're just enabling analyzer for dogfooding +#pragma warning disable IL2026 + return AssemblyLoadContext.Default.LoadFromAssemblyPath (assemblyPath); +#pragma warning restore IL2026 + } + Context.LogError (null, DiagnosticId.AssemblyInCustomStepOptionCouldNotBeFound, arg); + } else + Context.LogError (null, DiagnosticId.AssemblyPathInCustomStepMustBeFullyQualified, arg); + + return null; + } + + protected virtual void AddResolveFromXmlStep (Pipeline pipeline, string file) + { + pipeline.PrependStep (new ResolveFromXmlStep (File.OpenRead (file), file)); + } + + protected virtual void AddLinkAttributesStep (Pipeline pipeline, string file) + { + pipeline.AddStepBefore (typeof (MarkStep), new LinkAttributesStep (File.OpenRead (file), file)); + } + + static void AddBodySubstituterStep (Pipeline pipeline, string file) + { + pipeline.AddStepBefore (typeof (MarkStep), new BodySubstituterStep (File.OpenRead (file), file)); + } + + protected virtual void AddXmlDependencyRecorder (LinkContext context, string? fileName) + { + context.Tracer.AddRecorder (new XmlDependencyRecorder (context, fileName)); + } + + protected virtual void AddDgmlDependencyRecorder (LinkContext context, string? fileName) + { + context.Tracer.AddRecorder (new DgmlDependencyRecorder (context, fileName)); + } + + protected bool AddMarkHandler (Pipeline pipeline, string arg) + { + if (!TryGetCustomAssembly (ref arg, out Assembly? custom_assembly)) + return false; + + var step = ResolveStep (arg, custom_assembly); + if (step == null) + return false; + + pipeline.AppendMarkHandler (step); + return true; + } + + bool TryGetCustomAssembly (ref string arg, [NotNullWhen (true)] out Assembly? assembly) + { + assembly = null; + int pos = arg.IndexOf (","); + if (pos == -1) + return false; + + assembly = GetCustomAssembly (arg.Substring (pos + 1)); + if (assembly == null) + return false; + + arg = arg.Substring (0, pos); + return true; + } + + protected bool AddCustomStep (Pipeline pipeline, string arg) + { + if (!TryGetCustomAssembly (ref arg, out Assembly? custom_assembly)) + return false; + + string customStepName; + string? targetName = null; + bool before = false; + if (!arg.Contains (':')) { + customStepName = arg; + } else { + string[] parts = arg.Split (':'); + if (parts.Length != 2) { + Context.LogError (null, DiagnosticId.InvalidArgForCustomStep, arg); + return false; + } + customStepName = parts[1]; + + if (!parts[0].StartsWith ("-") && !parts[0].StartsWith ("+")) { + Context.LogError (null, DiagnosticId.ExpectedSignToControlNewStepInsertion); + return false; + } + + before = parts[0][0] == '-'; + targetName = parts[0].Substring (1); + } + + var stepType = ResolveStepType (customStepName, custom_assembly); + if (stepType == null) + return false; + + if (typeof (IStep).IsAssignableFrom (stepType)) { + + var customStep = (IStep?) Activator.CreateInstance (stepType) ?? throw new InvalidOperationException (); + if (targetName == null) { + pipeline.AppendStep (customStep); + return true; + } + + IStep? target = FindStep (pipeline, targetName); + if (target == null) { + Context.LogError (null, DiagnosticId.PipelineStepCouldNotBeFound, targetName); + return false; + } + + if (before) + pipeline.AddStepBefore (target, customStep); + else + pipeline.AddStepAfter (target, customStep); + + return true; + } + + if (typeof (IMarkHandler).IsAssignableFrom (stepType)) { + + var customStep = (IMarkHandler?) Activator.CreateInstance (stepType) ?? throw new InvalidOperationException (); + if (targetName == null) { + pipeline.AppendMarkHandler (customStep); + return true; + } + + IMarkHandler? target = FindMarkHandler (pipeline, targetName); + if (target == null) { + Context.LogError (null, DiagnosticId.PipelineStepCouldNotBeFound, targetName); + return false; + } + + if (before) + pipeline.AddMarkHandlerBefore (target, customStep); + else + pipeline.AddMarkHandlerAfter (target, customStep); + + return true; + } + + Context.LogError (null, DiagnosticId.CustomStepTypeIsIncompatibleWithLinkerVersion, stepType.ToString ()); + return false; + } + + protected virtual IStep? FindStep (Pipeline pipeline, string name) + { + foreach (IStep step in pipeline.GetSteps ()) { + Type t = step.GetType (); + if (t.Name == name) + return step; + } + + return null; + } + + static IMarkHandler? FindMarkHandler (Pipeline pipeline, string name) + { + foreach (IMarkHandler step in pipeline.MarkHandlers) { + Type t = step.GetType (); + if (t.Name == name) + return step; + } + + return null; + } + + Type? ResolveStepType (string type, Assembly assembly) + { + // Ignore warning, since we're just enabling analyzer for dogfooding +#pragma warning disable IL2026 + Type? step = assembly != null ? assembly.GetType (type) : Type.GetType (type, false); +#pragma warning restore IL2026 + + if (step == null) { + Context.LogError (null, DiagnosticId.CustomStepTypeCouldNotBeFound, type); + return null; + } + + return step; + } + + TStep? ResolveStep (string type, Assembly assembly) where TStep : class + { + // Ignore warning, since we're just enabling analyzer for dogfooding +#pragma warning disable IL2026 + Type? step = assembly != null ? assembly.GetType (type) : Type.GetType (type, false); +#pragma warning restore IL2026 + + if (step == null) { + Context.LogError (null, DiagnosticId.CustomStepTypeCouldNotBeFound, type); + return null; + } + + if (!typeof (TStep).IsAssignableFrom (step)) { + Context.LogError (null, DiagnosticId.CustomStepTypeIsIncompatibleWithLinkerVersion, type); + return null; + } + + return (TStep?) Activator.CreateInstance (step); + } + + static string[] GetFiles (string param) + { + if (param.Length < 1 || param[0] != '@') + return new string[] { param }; + + string file = param.Substring (1); + return ReadLines (file); + } + + static string[] ReadLines (string file) + { + var lines = new List (); + using (StreamReader reader = new StreamReader (file)) { + string? line; + while ((line = reader.ReadLine ()) != null) + lines.Add (line); + } + return lines.ToArray (); + } + + AssemblyAction? ParseAssemblyAction (string s) + { + switch (s.ToLowerInvariant ()) { + case "copy": + return AssemblyAction.Copy; + case "copyused": + return AssemblyAction.CopyUsed; + case "link": + return AssemblyAction.Link; + case "skip": + return AssemblyAction.Skip; + + case "addbypassngen": + _needAddBypassNGenStep = true; + return AssemblyAction.AddBypassNGen; + case "addbypassngenused": + _needAddBypassNGenStep = true; + return AssemblyAction.AddBypassNGenUsed; + } + + Context.LogError (null, DiagnosticId.InvalidAssemblyAction, s); + return null; + } + + AssemblyRootMode? ParseAssemblyRootMode (string s) + { + switch (s.ToLowerInvariant ()) { + case "all": + return AssemblyRootMode.AllMembers; + case "visible": + return AssemblyRootMode.VisibleMembers; + case "entrypoint": + return AssemblyRootMode.EntryPoint; + case "library": + return AssemblyRootMode.Library; + } + + Context.LogError (null, DiagnosticId.InvalidAssemblyRootMode, s); + return null; + } + + bool GetWarnVersion (string text, out WarnVersion version) + { + if (int.TryParse (text, out int versionNum)) { + version = (WarnVersion) versionNum; + if (version >= WarnVersion.ILLink0 && version <= WarnVersion.Latest) + return true; + } + + Context.LogError (null, DiagnosticId.InvalidWarningVersion, text); + version = 0; + return false; + } + + protected bool GetOptimizationName (string text, out CodeOptimizations optimization) + { + switch (text.ToLowerInvariant ()) { + case "beforefieldinit": + optimization = CodeOptimizations.BeforeFieldInit; + return true; + case "overrideremoval": + optimization = CodeOptimizations.OverrideRemoval; + return true; + case "unreachablebodies": + optimization = CodeOptimizations.UnreachableBodies; + return true; + case "unusedinterfaces": + optimization = CodeOptimizations.UnusedInterfaces; + return true; + case "unusedtypechecks": + optimization = CodeOptimizations.UnusedTypeChecks; + return true; + case "ipconstprop": + optimization = CodeOptimizations.IPConstantPropagation; + return true; + case "sealer": + optimization = CodeOptimizations.Sealer; + return true; + } + + Context.LogError (null, DiagnosticId.InvalidOptimizationValue, text); + optimization = 0; + return false; + } + + bool TryGetMetadataTrimming (string text, out MetadataTrimming metadataTrimming) + { + switch (text.ToLowerInvariant ()) { + case "all": + metadataTrimming = MetadataTrimming.Any; + return true; + case "none": + metadataTrimming = MetadataTrimming.None; + return true; + case "parametername": + metadataTrimming = MetadataTrimming.ParameterName; + return true; + } + + Context.LogError (null, DiagnosticId.InvalidMetadataOption, text); + metadataTrimming = 0; + return false; + } + + protected static bool GetWarningSuppressionWriterFileOutputKind (string text, out WarningSuppressionWriter.FileOutputKind fileOutputKind) + { + switch (text.ToLowerInvariant ()) { + case "cs": + fileOutputKind = WarningSuppressionWriter.FileOutputKind.CSharp; + return true; + + case "xml": + fileOutputKind = WarningSuppressionWriter.FileOutputKind.Xml; + return true; + + default: + fileOutputKind = WarningSuppressionWriter.FileOutputKind.CSharp; + return false; + } + } + + bool GetBoolParam (string token, Action action) + { + if (arguments.Count == 0) { + action (true); + return true; + } + + var arg = arguments.Peek (); + if (bool.TryParse (arg.ToLowerInvariant (), out bool value)) { + arguments.Dequeue (); + action (value); + return true; + } + + if (arg.StartsWith ("-") || arg.StartsWith ("/")) { + action (true); + return true; + } + + Context.LogError (null, DiagnosticId.InvalidArgumentForTokenOption, token); + return false; + } + + bool GetStringParam (string token, [NotNullWhen (true)] out string? value) + { + value = null; + if (arguments.Count < 1) { + ErrorMissingArgument (token); + return false; + } + + var arg = arguments.Dequeue (); + if (!string.IsNullOrEmpty (arg)) { + value = arg; + return true; + } + + ErrorMissingArgument (token); + return false; + } + + string? GetNextStringValue () + { + if (arguments.Count < 1) + return null; + + var arg = arguments.Peek (); + if (arg.StartsWith ("-") || arg.StartsWith ("/")) + return null; + + arguments.Dequeue (); + return arg; + } + + protected virtual LinkContext GetDefaultContext (Pipeline pipeline, ILogger? logger) + { + return new LinkContext (pipeline, logger ?? new ConsoleLogger (), "output") { + TrimAction = AssemblyAction.Link, + DefaultAction = AssemblyAction.Link, + }; + } + + protected virtual List CreateDefaultResolvers () + { + return new List (); + } + + static bool IsValidAssemblyName (string value) + { + return !string.IsNullOrEmpty (value); + } + + static void Usage () + { + Console.WriteLine (_linker); + + Console.WriteLine ($"illink [options] {resolvers}"); + Console.WriteLine (" -a FILE [MODE] Assembly file used as root assembly with optional MODE value to alter default root mode"); + Console.WriteLine (" Mode can be one of the following values"); + Console.WriteLine (" all: Keep all members in root assembly"); + Console.WriteLine (" default: Use entry point for applications and all members for libraries"); + Console.WriteLine (" entrypoint: Use assembly entry point as only root in the assembly"); + Console.WriteLine (" library: All assembly members and data needed for secondary trimming are retained"); + Console.WriteLine (" visible: Keep all members and types visible outside of the assembly"); + Console.WriteLine (" -x FILE XML descriptor file with members to be kept"); + + Console.WriteLine (); + Console.WriteLine ("Options"); + Console.WriteLine (" -d PATH Specify additional directory to search in for assembly references"); + Console.WriteLine (" -reference FILE Specify additional file location used to resolve assembly references"); + Console.WriteLine (" -b Update debug symbols for all modified files. Defaults to false"); + Console.WriteLine (" -out PATH Specify the output directory. Defaults to 'output'"); + Console.WriteLine (" -h Lists all {0} options", _linker); + Console.WriteLine (" @FILE Read response file for more options"); + + Console.WriteLine (); + Console.WriteLine ("Actions"); + Console.WriteLine (" --trim-mode ACTION Sets action for assemblies annotated with IsTrimmable attribute. Defaults to 'link'"); + Console.WriteLine (" copy: Analyze whole assembly and save it to the output"); + Console.WriteLine (" copyused: Same as copy but only for assemblies which are needed"); + Console.WriteLine (" link: Remove any unused IL or metadata and optimizes the assembly"); + Console.WriteLine (" skip: Do not process the assembly"); + Console.WriteLine (" addbypassngen: Add BypassNGenAttribute to unused methods"); + Console.WriteLine (" addbypassngenused: Same as addbypassngen but unused assemblies are removed"); + Console.WriteLine (" --action ACTION Sets action for assemblies that have no IsTrimmable attribute. Defaults to 'link'"); + Console.WriteLine (" --action ACTION ASM Overrides the default action for specific assembly name"); + + Console.WriteLine (); + Console.WriteLine ("Advanced Options"); + Console.WriteLine (" --about About the {0}", _linker); + Console.WriteLine (" --custom-step CFG Add a custom step to the existing pipeline"); + Console.WriteLine (" Step can use one of following configurations"); + Console.WriteLine (" TYPE,PATH_TO_ASSEMBLY: Add user defined type as last step to the pipeline"); + Console.WriteLine (" -NAME:TYPE,PATH_TO_ASSEMBLY: Inserts step type before existing step with name"); + Console.WriteLine (" +NAME:TYPE,PATH_TO_ASSEMBLY: Add step type after existing step"); + Console.WriteLine (" --custom-data KEY=VALUE Populates context data set with user specified key-value pair"); + Console.WriteLine (" --deterministic Produce a deterministic output for modified assemblies"); + Console.WriteLine (" --ignore-descriptors Skips reading embedded descriptors (short -z). Defaults to false"); + Console.WriteLine (" --skip-unresolved Ignore unresolved types, methods, and assemblies. Defaults to false"); + Console.WriteLine (" --output-pinvokes PATH Output a JSON file with all modules and entry points of the P/Invokes found"); + Console.WriteLine (" --verbose Log messages indicating progress and warnings"); + Console.WriteLine (" --nowarn WARN Disable specific warning messages"); + Console.WriteLine (" --warn VERSION Only print out warnings with version <= VERSION. Defaults to '9999'"); + Console.WriteLine (" VERSION is an integer in the range 0-9999."); + Console.WriteLine (" --warnaserror[+|-] Report all warnings as errors"); + Console.WriteLine (" --warnaserror[+|-] WARN Report specific warnings as errors"); + Console.WriteLine (" --singlewarn[+|-] Show at most one analysis warning per assembly"); + Console.WriteLine (" --singlewarn[+|-] ASM Show at most one analysis warning for a specific assembly"); + Console.WriteLine (" --version Print the version number of the {0}", _linker); + + Console.WriteLine (); + Console.WriteLine ("Trimming"); + Console.WriteLine (" --disable-opt NAME [ASM] Disable one of the default optimizations globaly or for a specific assembly name"); + Console.WriteLine (" beforefieldinit: Unused static fields are removed if there is no static ctor"); + Console.WriteLine (" ipconstprop: Interprocedural constant propagation on return values"); + Console.WriteLine (" overrideremoval: Overrides of virtual methods on types that are never instantiated are removed"); + Console.WriteLine (" unreachablebodies: Instance methods that are marked but not executed are converted to throws"); + Console.WriteLine (" unusedinterfaces: Removes interface types from declaration when not used"); + Console.WriteLine (" unusedtypechecks: Inlines never successful type checks"); + Console.WriteLine (" --enable-opt NAME [ASM] Enable one of the additional optimizations globaly or for a specific assembly name"); + Console.WriteLine (" sealer: Any method or type which does not have override is marked as sealed"); + Console.WriteLine (" --explicit-reflection Adds to members never used through reflection DisablePrivateReflection attribute. Defaults to false"); + Console.WriteLine (" --feature FEATURE VALUE Apply any optimizations defined when this feature setting is a constant known at link time"); + Console.WriteLine (" --keep-compilers-resources Keep assembly resources used for F# compilation resources. Defaults to false"); + Console.WriteLine (" --keep-dep-attributes Keep attributes used for manual dependency tracking. Defaults to false"); + Console.WriteLine (" --keep-metadata NAME Keep metadata which would otherwise be removed if not used"); + Console.WriteLine (" all: Metadata for any member are all kept"); + Console.WriteLine (" parametername: All parameter names are kept"); + Console.WriteLine (" --new-mvid Generate a new guid for each linked assembly (short -g). Defaults to true"); + Console.WriteLine (" --strip-descriptors Remove XML descriptor resources for linked assemblies. Defaults to true"); + Console.WriteLine (" --strip-security Remove metadata and code related to Code Access Security. Defaults to true"); + Console.WriteLine (" --substitutions FILE Configuration file with field or methods substitution rules"); + Console.WriteLine (" --ignore-substitutions Skips reading embedded substitutions. Defaults to false"); + Console.WriteLine (" --strip-substitutions Remove XML substitution resources for linked assemblies. Defaults to true"); + Console.WriteLine (" --used-attrs-only Attribute usage is removed if the attribute type is not used. Defaults to false"); + Console.WriteLine (" --link-attributes FILE Supplementary custom attribute definitions for attributes controlling the linker behavior."); + Console.WriteLine (" --ignore-link-attributes Skips reading embedded attributes. Defaults to false"); + Console.WriteLine (" --strip-link-attributes Remove XML link attributes resources for linked assemblies. Defaults to true"); + + Console.WriteLine (); + Console.WriteLine ("Analyzer"); + Console.WriteLine (" --dependencies-file FILE Specify the dependencies output. Defaults to 'output/linker-dependencies.xml'"); + Console.WriteLine (" if 'xml' is file format, 'output/linker-dependencies.dgml if 'dgml' is file format"); + Console.WriteLine (" --dump-dependencies Dump dependencies for the linker analyzer tool"); + Console.WriteLine (" --dependencies-file-format FORMAT Specify output file type. Defaults to 'xml'"); + Console.WriteLine (" xml: outputs an .xml file"); + Console.WriteLine (" dgml: outputs a .dgml file"); + Console.WriteLine (" --reduced-tracing Reduces dependency output related to assemblies that will not be modified"); + Console.WriteLine (""); + } + + static void Version () + { + Console.WriteLine ("{0} Version {1}", + _linker, + System.Reflection.Assembly.GetExecutingAssembly ().GetName ().Version); + } + + static void About () + { + Console.WriteLine ("For more information, visit the project Web site"); + Console.WriteLine (" https://github.com/dotnet/linker"); + } + + static Pipeline GetStandardPipeline () + { + Pipeline p = new Pipeline (); + p.AppendStep (new ProcessReferencesStep ()); + p.AppendStep (new MarkStep ()); + p.AppendStep (new RemoveResourcesStep ()); + p.AppendStep (new ValidateVirtualMethodAnnotationsStep ()); + p.AppendStep (new ProcessWarningsStep ()); + p.AppendStep (new OutputWarningSuppressions ()); + p.AppendStep (new SweepStep ()); + p.AppendStep (new CheckSuppressionsDispatcher ()); + p.AppendStep (new CodeRewriterStep ()); + p.AppendStep (new CleanStep ()); + p.AppendStep (new RegenerateGuidStep ()); + p.AppendStep (new OutputStep ()); + return p; + } + + public void Dispose () + { + context?.Dispose (); + } + } +} diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Inheritance.Interfaces.BaseProvidesInterfaceEdgeCaseTests.g.cs b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Inheritance.Interfaces.BaseProvidesInterfaceEdgeCaseTests.g.cs new file mode 100644 index 00000000000..5aaf8776a26 --- /dev/null +++ b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Inheritance.Interfaces.BaseProvidesInterfaceEdgeCaseTests.g.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace ILLink.RoslynAnalyzer.Tests.Inheritance.Interfaces +{ + public sealed partial class BaseProvidesInterfaceEdgeCaseTests : LinkerTestBase + { + + protected override string TestSuiteName => "Inheritance.Interfaces.BaseProvidesInterfaceEdgeCase"; + + [Fact] + public Task BaseProvidesInterfaceMethodCircularReference () + { + return RunTest (allowMissingWarnings: true); + } + + } +} \ No newline at end of file diff --git a/src/tools/illink/test/ILLink.Tasks.Tests/Mock.cs b/src/tools/illink/test/ILLink.Tasks.Tests/Mock.cs new file mode 100644 index 00000000000..979d62b1448 --- /dev/null +++ b/src/tools/illink/test/ILLink.Tasks.Tests/Mock.cs @@ -0,0 +1,228 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Build.Framework; +using Mono.Linker; +using Mono.Linker.Steps; + +namespace ILLink.Tasks.Tests +{ + public class MockTask : ILLink + { + + public List<(MessageImportance Importance, string Line)> Messages { get; } = new List<(MessageImportance Importance, string Line)> (); + + public MockTask () + { + // Ensure that [Required] members are non-null + AssemblyPaths = Array.Empty (); + RootAssemblyNames = Array.Empty (); + ILLinkPath = Path.Combine (Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location), "illink.dll"); + } + + public MockDriver CreateDriver () + { + using (var responseFileText = new StringReader (GenerateResponseFileCommands ())) { + var arguments = new Queue (); + Driver.ParseResponseFile (responseFileText, arguments); + return new MockDriver (arguments); + } + } + + public static string[] OptimizationNames { + get { + var field = typeof (ILLink).GetField ("_optimizationNames", BindingFlags.NonPublic | BindingFlags.Static); + return (string[]) field.GetValue (null); + } + } + + public void SetOptimization (string optimization, bool enabled) + { + var property = typeof (ILLink).GetProperty (optimization); + property.GetSetMethod ().Invoke (this, new object[] { enabled }); + } + + static readonly string[] nonOptimizationBooleanProperties = new string[] { + "DumpDependencies", + "RemoveSymbols", + "TreatWarningsAsErrors", + "SingleWarn" + }; + + public static IEnumerable GetOptimizationPropertyNames () + { + foreach (var property in typeof (ILLink).GetProperties (BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)) { + if (property.PropertyType != typeof (bool)) + continue; + if (nonOptimizationBooleanProperties.Contains (property.Name)) + continue; + yield return property.Name; + } + } + + protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance) => Messages.Add ((messageImportance, singleLine)); + } + + public class MockBuildEngine : IBuildEngine + { + public void LogErrorEvent (BuildErrorEventArgs e) { } + public void LogWarningEvent (BuildWarningEventArgs e) { } + public void LogMessageEvent (BuildMessageEventArgs e) { } + public void LogCustomEvent (CustomBuildEventArgs e) { } + public bool BuildProjectFile (string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => false; + public bool ContinueOnError => false; + public int LineNumberOfTaskNode => 0; + public int ColumnNumberOfTaskNode => 0; + public string ProjectFileOfTaskNode => null; + } + + public class MockDriver : Driver + { + public class CustomLogger : Mono.Linker.ILogger + { + public List Messages = new List (); + + public void LogMessage (MessageContainer message) + { + Messages.Add (message); + } + } + + public MockDriver (Queue arguments) : base (arguments) + { + // Always set up the context early on. + Logger = new CustomLogger (); + SetupContext (Logger); + } + + public new LinkContext Context => base.Context; + + public CustomLogger Logger { get; private set; } + + public IEnumerable GetRootAssemblies () + { + foreach (var step in Context.Pipeline.GetSteps ()) { + if (!(step is RootAssemblyInput)) + continue; + + var assemblyName = (string) typeof (RootAssemblyInput).GetField ("fileName", BindingFlags.NonPublic | BindingFlags.Instance).GetValue (step); + if (assemblyName == null) + continue; + + yield return assemblyName; + } + } + + public IEnumerable GetRootDescriptors () + { + foreach (var step in Context.Pipeline.GetSteps ()) { + if (!(step is ResolveFromXmlStep)) + continue; + + var descriptor = (string) typeof (ResolveFromXmlStep).GetField ("_xmlDocumentLocation", BindingFlags.NonPublic | BindingFlags.Instance).GetValue (step); + + yield return descriptor; + } + } + + public IEnumerable GetReferenceAssemblies () + { + return (IEnumerable) typeof (AssemblyResolver).GetField ("_references", BindingFlags.NonPublic | BindingFlags.Instance).GetValue (Context.Resolver); + } + + protected override void AddResolveFromXmlStep (Pipeline pipeline, string file) + { + // Don't try to load an xml file - just pretend it exists. + pipeline.PrependStep (new ResolveFromXmlStep (documentStream: null, file)); + } + + protected override void AddXmlDependencyRecorder (LinkContext context, string file) + { + // Don't try to open the output file for writing - just pretend it exists. + Context.Tracer.AddRecorder (MockXmlDependencyRecorder.Singleton); + } + + protected override void AddDgmlDependencyRecorder (LinkContext context, string file) + { + // Don't try to open the output file for writing - just pretend it exists. + Context.Tracer.AddRecorder (MockDgmlDependencyRecorder.Singleton); + } + + public IEnumerable GetDependencyRecorders () + { + return (IEnumerable) typeof (Tracer).GetField ("recorders", BindingFlags.NonPublic | BindingFlags.Instance).GetValue (Context.Tracer); + } + + public new bool GetOptimizationName (string optimization, out CodeOptimizations codeOptimizations) + { + return base.GetOptimizationName (optimization, out codeOptimizations); + } + + public CodeOptimizations GetDefaultOptimizations () + { + return Context.Optimizations.Global; + } + + public Dictionary GetCustomData () + { + var field = typeof (LinkContext).GetField ("_parameters", BindingFlags.NonPublic | BindingFlags.Instance); + return (Dictionary) field.GetValue (Context); + } + + protected override List CreateDefaultResolvers () + { + return new List () { + new RootAssemblyInput (null, AssemblyRootMode.EntryPoint) + }; + } + } + + public class MockXmlDependencyRecorder : IDependencyRecorder + { + public static MockXmlDependencyRecorder Singleton { get; } = new MockXmlDependencyRecorder (); + public void RecordDependency (object source, object arget, bool marked) { } + public void RecordDependency (object target, in DependencyInfo reason, bool marked) { } + public void FinishRecording () { } + } + + public class MockDgmlDependencyRecorder : IDependencyRecorder + { + public static MockXmlDependencyRecorder Singleton { get; } = new MockXmlDependencyRecorder (); + public void RecordDependency (object source, object arget, bool marked) { } + public void RecordDependency (object target, in DependencyInfo reason, bool marked) { } + public void FinishRecording () { } + } + + public class MockCustomStep : IStep + { + public void Process (LinkContext context) { } + } + + public class MockCustomStep2 : MockCustomStep { } + + public class MockCustomStep3 : MockCustomStep { } + + public class MockCustomStep4 : MockCustomStep { } + + public class MockCustomStep5 : MockCustomStep { } + + public class MockCustomStep6 : MockCustomStep { } + + public class MockMarkHandler : IMarkHandler + { + public void Initialize (LinkContext context, MarkContext markContext) { } + } + + public class MockMarkHandler2 : MockMarkHandler { } + + public class MockMarkHandler3 : MockMarkHandler { } + + public class MockMarkHandler4 : MockMarkHandler { } + +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Inheritance.Interfaces/StaticInterfaceMethods/StaticInterfaceMethodsInPreservedScope.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Inheritance.Interfaces/StaticInterfaceMethods/StaticInterfaceMethodsInPreservedScope.cs new file mode 100644 index 00000000000..362413c3f97 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Inheritance.Interfaces/StaticInterfaceMethods/StaticInterfaceMethodsInPreservedScope.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Helpers; +using Mono.Linker.Tests.Cases.Expectations.Metadata; +using Mono.Linker.Tests.Cases.Inheritance.Interfaces.StaticInterfaceMethods.Dependencies; + +namespace Mono.Linker.Tests.Cases.Inheritance.Interfaces.StaticInterfaceMethods +{ + [SetupCompileBefore ("library.dll", new[] { "Dependencies/Library.cs" })] + [SetupLinkerAction ("skip", "library")] + public static class StaticInterfaceMethodsInPreservedScope + { + [Kept] + public static void Main () + { + var x = typeof (VirtualInterfaceMethods); + x = typeof (AbstractInterfaceMethods); + x = typeof (IStaticInterfaceWithDefaultImpls); + x = typeof (IStaticAbstractMethods); + } + + [Kept] + [KeptInterface (typeof (IStaticInterfaceWithDefaultImpls))] + public class VirtualInterfaceMethods : IStaticInterfaceWithDefaultImpls + { + [Kept] + static int IStaticInterfaceWithDefaultImpls.Property { + [Kept] + [KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] + get => 1; + [Kept] + [KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] + set => _ = value; + } + + [Kept] + [KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] + static int IStaticInterfaceWithDefaultImpls.Method () => 1; + + // There is a default implementation and the type isn't instantiated, so we don't need this + int IStaticInterfaceWithDefaultImpls.InstanceMethod () => 0; + } + + [Kept] + [KeptInterface (typeof (IStaticAbstractMethods))] + public class AbstractInterfaceMethods : IStaticAbstractMethods + { + [Kept] + static int IStaticAbstractMethods.Property { + [Kept] + [KeptOverride (typeof (IStaticAbstractMethods))] + get => 1; [Kept] + [KeptOverride (typeof (IStaticAbstractMethods))] + set => _ = value; + } + + [Kept] + [KeptOverride (typeof (IStaticAbstractMethods))] + static int IStaticAbstractMethods.Method () => 1; + + [Kept] + [KeptOverride (typeof (IStaticAbstractMethods))] + int IStaticAbstractMethods.InstanceMethod () => 0; + } + } +} + diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Inheritance.Interfaces/StaticInterfaceMethods/StaticVirtualInterfaceMethodsInPreservedScope.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Inheritance.Interfaces/StaticInterfaceMethods/StaticVirtualInterfaceMethodsInPreservedScope.cs new file mode 100644 index 00000000000..26a5a2cc066 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Inheritance.Interfaces/StaticInterfaceMethods/StaticVirtualInterfaceMethodsInPreservedScope.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Helpers; +using Mono.Linker.Tests.Cases.Expectations.Metadata; +using Mono.Linker.Tests.Cases.Inheritance.Interfaces.StaticInterfaceMethods.Dependencies; + +namespace Mono.Linker.Tests.Cases.Inheritance.Interfaces.StaticInterfaceMethods +{ + [SetupCompileBefore ("library.dll", new[] { "Dependencies/Library.cs" })] + [SetupLinkerAction ("skip", "library")] + public static class StaticVirtualInterfaceMethodsInPreservedScope + { + [Kept] + public static void Main () + { + NotRelevantToVariantCasting.Keep (); + var t = typeof (RelevantToVariantCasting); + MarkInterfaceMethods (); + var x = new InstantiatedClass (); + } + + [Kept] + static void MarkInterfaceMethods () where T : IStaticInterfaceWithDefaultImpls + { + T.Property = T.Property + 1; + T.Method (); + CallInstanceMethod (null); + + [Kept] + void CallInstanceMethod (IStaticInterfaceWithDefaultImpls x) + { + x.InstanceMethod (); + } + } + + [Kept] + [KeptInterface (typeof (IStaticInterfaceWithDefaultImpls))] + public class RelevantToVariantCasting : IStaticInterfaceWithDefaultImpls + { + [Kept] + static int IStaticInterfaceWithDefaultImpls.Property { [Kept][KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] get => 1; [Kept][KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] set => _ = value; } + [Kept] + [KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] + static int IStaticInterfaceWithDefaultImpls.Method () => 1; + int IStaticInterfaceWithDefaultImpls.InstanceMethod () => 0; + } + + [Kept] + [KeptInterface (typeof (IStaticInterfaceWithDefaultImpls))] + public class UsedAsTypeArgument : IStaticInterfaceWithDefaultImpls + { + [Kept] + static int IStaticInterfaceWithDefaultImpls.Property { [Kept][KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] get => 1; [Kept][KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] set => _ = value; } + [Kept] + [KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] + static int IStaticInterfaceWithDefaultImpls.Method () => 1; + int IStaticInterfaceWithDefaultImpls.InstanceMethod () => 0; + } + + [Kept] + public class NotRelevantToVariantCasting : IStaticInterfaceWithDefaultImpls + { + [Kept] + public static void Keep () { } + static int IStaticInterfaceWithDefaultImpls.Property { get => 1; set => _ = value; } + static int IStaticInterfaceWithDefaultImpls.Method () => 1; + int IStaticInterfaceWithDefaultImpls.InstanceMethod () => 0; + } + [Kept] + [KeptMember (".ctor()")] + [KeptInterface (typeof (IStaticInterfaceWithDefaultImpls))] + public class InstantiatedClass : IStaticInterfaceWithDefaultImpls + { + [Kept] + static int IStaticInterfaceWithDefaultImpls.Property { + [Kept] + [KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] + get => 1; + [Kept] + [KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] + set => _ = value; + } + [Kept] + [KeptOverride (typeof (IStaticInterfaceWithDefaultImpls))] + static int IStaticInterfaceWithDefaultImpls.Method () => 1; + [Kept] + int IStaticInterfaceWithDefaultImpls.InstanceMethod () => 0; + } + } +} + diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Libraries/DefaultLibraryLinkBehavior.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Libraries/DefaultLibraryLinkBehavior.cs new file mode 100644 index 00000000000..fd0c5578162 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Libraries/DefaultLibraryLinkBehavior.cs @@ -0,0 +1,29 @@ +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.Cases.Libraries +{ + [SetupCompileAsLibrary] + [SetupLinkerArgument ("-a", "test.dll")] + [Kept] + [KeptMember (".ctor()")] + public class DefaultLibraryLinkBehavior + { + // Kept because by default libraries their action set to copy + [Kept] + public static void Main () + { + // Main is needed for the test collector to find and treat as a test + } + + [Kept] + public void UnusedPublicMethod () + { + } + + [Kept] + private void UnusedPrivateMethod () + { + } + } +} \ No newline at end of file diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/LinkerArgumentBuilder.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/LinkerArgumentBuilder.cs new file mode 100644 index 00000000000..ae0673e9903 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/LinkerArgumentBuilder.cs @@ -0,0 +1,251 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Mono.Linker.Tests.Extensions; + +namespace Mono.Linker.Tests.TestCasesRunner +{ + public class LinkerArgumentBuilder + { + private readonly List _arguments = new List (); + private readonly TestCaseMetadataProvider _metadataProvider; + + public LinkerArgumentBuilder (TestCaseMetadataProvider metadataProvider) + { + _metadataProvider = metadataProvider; + } + + public virtual void AddSearchDirectory (NPath directory) + { + Append ("-d"); + Append (directory.ToString ()); + } + + public virtual void AddReference (NPath path) + { + Append ("-reference"); + Append (path.ToString ()); + } + + public virtual void AddOutputDirectory (NPath directory) + { + Append ("-o"); + Append (directory.ToString ()); + } + + public virtual void AddLinkXmlFile (string file) + { + Append ("-x"); + Append (file); + } + + public virtual void AddResponseFile (NPath path) + { + Append ($"@{path}"); + } + + public virtual void AddTrimMode (string value) + { + Append ("--trim-mode"); + Append (value); + } + + public virtual void AddDefaultAction (string value) + { + Append ("--action"); + Append (value); + } + + public virtual void LinkFromAssembly (string fileName) + { + Append ("-a"); + Append (fileName); + Append ("entrypoint"); + } + + public virtual void LinkFromPublicAndFamily (string fileName) + { +#if NETCOREAPP + Append ("-a"); + Append (fileName); + Append ("visible"); +#else + Append ("-r"); + Append (fileName); +#endif + } + + public virtual void IgnoreDescriptors (bool value) + { + Append ("--ignore-descriptors"); + Append (value ? "true" : "false"); + } + + public virtual void IgnoreSubstitutions (bool value) + { + Append ("--ignore-substitutions"); + Append (value ? "true" : "false"); + } + + public virtual void IgnoreLinkAttributes (bool value) + { + Append ("--ignore-link-attributes"); + Append (value ? "true" : "false"); + } + + public virtual void AddIl8n (string value) + { + Append ("-l"); + Append (value); + } + + public virtual void AddLinkSymbols (string value) + { + Append ("-b"); + Append (value); + } + + public virtual void AddKeepDebugMembers (string value) + { + Append ("-v"); + Append (value); + } + + public virtual void AddAssemblyAction (string action, string assembly) + { + Append ("--action"); + Append (action); + Append (assembly); + } + + public virtual void AddSkipUnresolved (bool skipUnresolved) + { + if (skipUnresolved) { + Append ("--skip-unresolved"); + Append ("true"); + } + } + + public virtual void AddStripDescriptors (bool stripDescriptors) + { + if (!stripDescriptors) { + Append ("--strip-descriptors"); + Append ("false"); + } + } + + public virtual void AddStripSubstitutions (bool stripSubstitutions) + { + if (!stripSubstitutions) { + Append ("--strip-substitutions"); + Append ("false"); + } + } + + public virtual void AddStripLinkAttributes (bool stripLinkAttributes) + { + if (!stripLinkAttributes) { + Append ("--strip-link-attributes"); + Append ("false"); + } + } + + public virtual void AddSubstitutions (string file) + { + Append ("--substitutions"); + Append (file); + } + + public virtual void AddLinkAttributes (string file) + { + Append ("--link-attributes"); + Append (file); + } + + public string[] ToArgs () + { + return _arguments.ToArray (); + } + + protected void Append (string arg) + { + _arguments.Add (arg); + } + + public virtual void AddAdditionalArgument (string flag, string[] values) + { + Append (flag); + if (values != null) { + foreach (var val in values) + Append (val); + } + } + + public virtual void ProcessTestInputAssembly (NPath inputAssemblyPath) + { + if (_metadataProvider.LinkPublicAndFamily ()) + LinkFromPublicAndFamily (inputAssemblyPath.ToString ()); + else + LinkFromAssembly (inputAssemblyPath.ToString ()); + } + + public virtual void ProcessOptions (TestCaseLinkerOptions options) + { + if (options.TrimMode != null) + AddTrimMode (options.TrimMode); + + if (options.DefaultAssembliesAction != null) + AddDefaultAction (options.DefaultAssembliesAction); + + if (options.AssembliesAction != null) { + foreach (var entry in options.AssembliesAction) + AddAssemblyAction (entry.Key, entry.Value); + } + + // Honoring descriptors causes a ton of stuff to be preserved. That's good for normal use cases, but for + // our test cases that pollutes the results + IgnoreDescriptors (options.IgnoreDescriptors); + + IgnoreSubstitutions (options.IgnoreSubstitutions); + + IgnoreLinkAttributes (options.IgnoreLinkAttributes); + +#if !NETCOREAPP + if (!string.IsNullOrEmpty (options.Il8n)) + AddIl8n (options.Il8n); +#endif + + if (!string.IsNullOrEmpty (options.LinkSymbols)) + AddLinkSymbols (options.LinkSymbols); + + if (!string.IsNullOrEmpty (options.KeepDebugMembers)) + AddKeepDebugMembers (options.KeepDebugMembers); + + AddSkipUnresolved (options.SkipUnresolved); + + AddStripDescriptors (options.StripDescriptors); + + AddStripSubstitutions (options.StripSubstitutions); + + AddStripLinkAttributes (options.StripLinkAttributes); + + foreach (var descriptor in options.Descriptors) + AddLinkXmlFile (descriptor); + + foreach (var substitutions in options.Substitutions) + AddSubstitutions (substitutions); + + foreach (var attributeDefinition in options.LinkAttributes) + AddLinkAttributes (attributeDefinition); + + // A list of expensive optimizations which should not run by default + AddAdditionalArgument ("--disable-opt", new[] { "ipconstprop" }); + + // Unity uses different argument format and needs to be able to translate to their format. In order to make that easier + // we keep the information in flag + values format for as long as we can so that this information doesn't have to be parsed out of a single string + foreach (var additionalArgument in options.AdditionalArguments) + AddAdditionalArgument (additionalArgument.Key, additionalArgument.Value); + } + } +} \ No newline at end of file