File: LinkTask.cs
Web Access
Project: src\src\tools\illink\src\ILLink.Tasks\ILLink.Tasks.csproj (ILLink.Tasks)
// 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
	{
		/// <summary>
		///   Paths to the assembly files that should be considered as
		///   input to the ILLink.
		///   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 ILLink produces granular warnings for this assembly.
		///   Maps to '-reference', and possibly '--action', '--enable-opt', '--disable-opt', '--verbose'
		/// </summary>
		[Required]
		public ITaskItem[] AssemblyPaths { get; set; }
 
		/// <summary>
		///    Paths to assembly files that are reference assemblies,
		///    representing the surface area for compilation.
		///    Maps to '-reference', with action set to 'skip' via '--action'.
		/// </summary>
		public ITaskItem[] ReferenceAssemblyPaths { get; set; }
 
		/// <summary>
		///   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.
		/// </summary>
		[Required]
		public ITaskItem[] RootAssemblyNames { get; set; }
 
		/// <summary>
		///   The directory in which to place linked assemblies.
		///    Maps to '-out'.
		/// </summary>
		[Required]
		public ITaskItem OutputDirectory { get; set; }
 
		/// <summary>
		/// The subset of warnings that have to be turned off.
		/// Maps to '--nowarn'.
		/// </summary>
		public string NoWarn { get; set; }
 
		/// <summary>
		/// The warning version to use.
		/// Maps to '--warn'.
		/// </summary>
		public string Warn { get; set; }
 
		/// <summary>
		/// Treat all warnings as errors.
		/// Maps to '--warnaserror' if true, '--warnaserror-' if false.
		/// </summary>
		public bool TreatWarningsAsErrors { set => _treatWarningsAsErrors = value; }
		bool? _treatWarningsAsErrors;
 
		/// <summary>
		/// Produce at most one trim analysis warning per assembly.
		/// Maps to '--singlewarn' if true, '--singlewarn-' if false.
		/// </summary>
		public bool SingleWarn { set => _singleWarn = value; }
		bool? _singleWarn;
 
		/// <summary>
		/// The list of warnings to report as errors.
		/// Maps to '--warnaserror LIST-OF-WARNINGS'.
		/// </summary>
		public string WarningsAsErrors { get; set; }
 
		/// <summary>
		/// The list of warnings to report as usual.
		/// Maps to '--warnaserror- LIST-OF-WARNINGS'.
		/// </summary>
		public string WarningsNotAsErrors { get; set; }
 
		/// <summary>
		///   A list of XML root descriptor files specifying ILLink
		///   roots at a granular level. See the dotnet/linker
		///   documentation for details about the format.
		///   Maps to '-x'.
		/// </summary>
		public ITaskItem[] RootDescriptorFiles { get; set; }
 
		/// <summary>
		///   Boolean specifying whether to enable beforefieldinit optimization globally.
		///   Maps to '--enable-opt beforefieldinit' or '--disable-opt beforefieldinit'.
		/// </summary>
		public bool BeforeFieldInit { set => _beforeFieldInit = value; }
		bool? _beforeFieldInit;
 
		/// <summary>
		///   Boolean specifying whether to enable overrideremoval optimization globally.
		///   Maps to '--enable-opt overrideremoval' or '--disable-opt overrideremoval'.
		/// </summary>
		public bool OverrideRemoval { set => _overrideRemoval = value; }
		bool? _overrideRemoval;
 
		/// <summary>
		///   Boolean specifying whether to enable unreachablebodies optimization globally.
		///   Maps to '--enable-opt unreachablebodies' or '--disable-opt unreachablebodies'.
		/// </summary>
		public bool UnreachableBodies { set => _unreachableBodies = value; }
		bool? _unreachableBodies;
 
		/// <summary>
		///   Boolean specifying whether to enable unusedinterfaces optimization globally.
		///   Maps to '--enable-opt unusedinterfaces' or '--disable-opt unusedinterfaces'.
		/// </summary>
		public bool UnusedInterfaces { set => _unusedInterfaces = value; }
		bool? _unusedInterfaces;
 
		/// <summary>
		///   Boolean specifying whether to enable ipconstprop optimization globally.
		///   Maps to '--enable-opt ipconstprop' or '--disable-opt ipconstprop'.
		/// </summary>
		public bool IPConstProp { set => _iPConstProp = value; }
		bool? _iPConstProp;
 
		/// <summary>
		///   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'.
		/// </summary>
		public ITaskItem[] FeatureSettings { get; set; }
 
		/// <summary>
		///   Boolean specifying whether to enable sealer optimization globally.
		///   Maps to '--enable-opt sealer' or '--disable-opt sealer'.
		/// </summary>
		public bool Sealer { set => _sealer = value; }
		bool? _sealer;
 
		static readonly string[] _optimizationNames = new string[] {
			"BeforeFieldInit",
			"OverrideRemoval",
			"UnreachableBodies",
			"UnusedInterfaces",
			"IPConstProp",
			"Sealer"
		};
 
		/// <summary>
		///   Custom data key-value pairs to pass to ILLink.
		///   The name of the item is the key, and the required "Value"
		///   metadata is the value. Maps to '--custom-data key=value'.
		/// </summary>
		public ITaskItem[] CustomData { get; set; }
 
		/// <summary>
		///   Extra arguments to pass to illink, delimited by spaces.
		/// </summary>
		public string ExtraArgs { get; set; }
 
		/// <summary>
		///   Make illink dump dependencies file for ILLink analyzer tool.
		///   Maps to '--dump-dependencies'.
		/// </summary>
		public bool DumpDependencies { get; set; }
 
		/// <summary>
		///   Make illink dump dependencies to the specified file type.
		///   Maps to '--dependencies-file-format'.
		/// </summary>
		public string DependenciesFileFormat { get; set; }
 
		/// <summary>
		///   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.)
		/// </summary>
		public bool RemoveSymbols { set => _removeSymbols = value; }
		bool? _removeSymbols;
 
		/// <summary>
		///   Preserve original path to debug symbols from each assembly's debug header.
		///   Maps to '--preserve-symbol-paths' if true.
		///   Default if not specified is to write out the full path to the pdb in the debug header.
		/// </summary>
		public bool PreserveSymbolPaths { get; set; }
 
		/// <summary>
		///   Sets the default action for trimmable assemblies.
		///   Maps to '--trim-mode'
		/// </summary>
		public string TrimMode { get; set; }
 
		/// <summary>
		///   Sets the default action for assemblies which have not opted into trimming.
		///   Maps to '--action'
		/// </summary>
		public string DefaultAction { get; set; }
 
		/// <summary>
		///   A list of custom steps to insert into the ILLink 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 ILLink step. The custom step will be inserted before it.
		///   AfterStep: The name of a ILLink 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'.
		/// </summary>
		public ITaskItem[] CustomSteps { get; set; }
 
		/// <summary>
		///   A list selected metadata which should not be trimmed. It maps to 'keep-metadata' option
		/// </summary>
		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;
 
#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file
				var taskDirectory = Path.GetDirectoryName (Assembly.GetExecutingAssembly ().Location);
#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file
				// IL Linker always runs on .NET Core, even when using desktop MSBuild to host ILLink.Tasks.
				_illinkPath = Path.Combine (Path.GetDirectoryName (taskDirectory), "net9.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<string> assemblyNames = new HashSet<string> (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 (!bool.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 (!bool.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 (PreserveSymbolPaths)
				args.AppendLine ("--preserve-symbol-paths");
 
			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 ();
		}
	}
}