diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/RunCsWinRTGenerator.cs b/src/Tasks/Microsoft.NET.Build.Tasks/RunCsWinRTGenerator.cs new file mode 100644 index 000000000000..3c28ee32aa74 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks/RunCsWinRTGenerator.cs @@ -0,0 +1,262 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Build.Tasks; + +/// +/// The custom MSBuild task that invokes the 'cswinrtgen' tool. +/// +public sealed class RunCsWinRTGenerator : ToolTask +{ + /// + /// Gets or sets the paths to assembly files that are reference assemblies, representing + /// the entire surface area for compilation. These assemblies are the full set of assemblies + /// that will contribute to the interop .dll being generated. + /// + [Required] + public ITaskItem[]? ReferenceAssemblyPaths { get; set; } + + /// + /// Gets or sets the path to the output assembly that was produced by the build (for the current project). + /// + /// + /// This property is an array, but it should only ever receive a single item. + /// + [Required] + public ITaskItem[]? OutputAssemblyPath { get; set; } + + /// + /// Gets or sets the directory where the generated interop assembly will be placed. + /// + [Required] + public string? InteropAssemblyDirectory { get; set; } + + /// + /// Gets or sets the directory where the debug repro will be produced. + /// + /// If not set, no debug repro will be produced. + public string? DebugReproDirectory { get; set; } + + /// + /// Gets or sets the tools directory where the 'cswinrtgen' tool is located. + /// + [Required] + public string? CsWinRTToolsDirectory { get; set; } + + /// + /// Gets or sets the architecture of 'cswinrtgen' to use. + /// + /// + /// If not set, the architecture will be determined based on the current process architecture. + /// + public string? CsWinRTToolsArchitecture { get; set; } + + /// + /// Gets or sets whether to use Windows.UI.Xaml projections. + /// + /// If not set, it will default to (i.e. using Microsoft.UI.Xaml projections). + public bool UseWindowsUIXamlProjections { get; set; } = false; + + /// + /// Gets whether to validate the assembly version of WinRT.Runtime.dll, to ensure it matches the generator. + /// + public bool ValidateWinRTRuntimeAssemblyVersion { get; set; } = true; + + /// + /// Gets whether to validate that any references to WinRT.Runtime.dll version 2 are present across any assemblies. + /// + public bool ValidateWinRTRuntimeDllVersion2References { get; set; } = true; + + /// + /// Gets whether to enable incremental generation (i.e. with a cache file on disk saving the full set of types to generate). + /// + public bool EnableIncrementalGeneration { get; set; } = true; + + /// + /// Gets whether to treat warnings coming from 'cswinrtgen' as errors (regardless of the global 'TreatWarningsAsErrors' setting). + /// + public bool TreatWarningsAsErrors { get; set; } = false; + + /// + /// Gets or sets the maximum number of parallel tasks to use for execution. + /// + /// If not set, the default will match the number of available processor cores. + public int MaxDegreesOfParallelism { get; set; } = -1; + + /// + /// Gets or sets additional arguments to pass to the tool. + /// + public ITaskItem[]? AdditionalArguments { get; set; } + + /// + protected override string ToolName => "cswinrtgen.exe"; + + /// + /// Gets the effective item spec for the output assembly. + /// + private string EffectiveOutputAssemblyItemSpec => OutputAssemblyPath![0].ItemSpec; + + /// +#if NET10_0_OR_GREATER + [MemberNotNullWhen(true, nameof(ReferenceAssemblyPaths))] + [MemberNotNullWhen(true, nameof(OutputAssemblyPath))] + [MemberNotNullWhen(true, nameof(InteropAssemblyDirectory))] + [MemberNotNullWhen(true, nameof(CsWinRTToolsDirectory))] +#endif + protected override bool ValidateParameters() + { + if (!base.ValidateParameters()) + { + return false; + } + + if (ReferenceAssemblyPaths is not { Length: > 0 }) + { + Log.LogWarning("Invalid 'ReferenceAssemblyPaths' input(s)."); + + return false; + } + + if (OutputAssemblyPath is not { Length: 1 }) + { + Log.LogWarning("Invalid 'OutputAssemblyPath' input."); + + return false; + } + + if (InteropAssemblyDirectory is null || !Directory.Exists(InteropAssemblyDirectory)) + { + Log.LogWarning("Generated assembly directory '{0}' is invalid or does not exist.", InteropAssemblyDirectory); + + return false; + } + + if (DebugReproDirectory is not null && !Directory.Exists(DebugReproDirectory)) + { + Log.LogWarning("Debug repro directory '{0}' is invalid or does not exist.", DebugReproDirectory); + + return false; + } + + if (CsWinRTToolsDirectory is null || !Directory.Exists(CsWinRTToolsDirectory)) + { + Log.LogWarning("Tools directory '{0}' is invalid or does not exist.", CsWinRTToolsDirectory); + + return false; + } + + if (CsWinRTToolsArchitecture is not null && + !CsWinRTToolsArchitecture.Equals("x86", StringComparison.OrdinalIgnoreCase) && + !CsWinRTToolsArchitecture.Equals("x64", StringComparison.OrdinalIgnoreCase) && + !CsWinRTToolsArchitecture.Equals("arm64", StringComparison.OrdinalIgnoreCase) && + !CsWinRTToolsArchitecture.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase)) + { + Log.LogWarning("Tools architecture '{0}' is invalid (it must be 'x86', 'x64', 'arm64', or 'AnyCPU').", CsWinRTToolsArchitecture); + + return false; + } + + // The degrees of parallelism matches the semantics of the 'MaxDegreesOfParallelism' property of 'Parallel.For'. That is, it must either be exactly '-1', which is a special + // value meaning "use as many parallel threads as the runtime deems appropriate", or it must be set to a positive integer, to explicitly control the number of threads. + // See: https://learn.microsoft.com/dotnet/api/system.threading.tasks.paralleloptions.maxdegreeofparallelism#system-threading-tasks-paralleloptions-maxdegreeofparallelism. + if (MaxDegreesOfParallelism is not (-1 or > 0)) + { + Log.LogWarning("Invalid 'MaxDegreesOfParallelism' value. It must be '-1' or greater than '0' (but was '{0}').", MaxDegreesOfParallelism); + + return false; + } + + return true; + } + + /// + [SuppressMessage("Style", "IDE0072", Justification = "We always use 'x86' as a fallback for all other CPU architectures.")] + protected override string GenerateFullPathToTool() + { + string? effectiveArchitecture = CsWinRTToolsArchitecture; + + // Special case for when 'AnyCPU' is specified (mostly for testing scenarios). + // We just reuse the exact input directory and assume the architecture matches. + // This makes it easy to run the task against a local build of 'cswinrtgen'. + if (effectiveArchitecture?.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase) is true) + { + return Path.Combine(CsWinRTToolsDirectory!, ToolName); + } + + // If the architecture is not specified, determine it based on the current process architecture + effectiveArchitecture ??= RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "x64", + Architecture.Arm64 => "arm64", + _ => "x86" + }; + + // The tool is inside an architecture-specific subfolder, as it's a native binary + string architectureDirectory = $"win-{effectiveArchitecture}"; + + return Path.Combine(CsWinRTToolsDirectory!, architectureDirectory, ToolName); + } + + /// + protected override string GenerateResponseFileCommands() + { + StringBuilder args = new(); + + IEnumerable referenceAssemblyPaths = ReferenceAssemblyPaths!.Select(static path => path.ItemSpec); + string referenceAssemblyPathsArg = string.Join(",", referenceAssemblyPaths); + + AppendResponseFileCommand(args, "--reference-assembly-paths", referenceAssemblyPathsArg); + AppendResponseFileCommand(args, "--output-assembly-path", EffectiveOutputAssemblyItemSpec); + AppendResponseFileCommand(args, "--generated-assembly-directory", InteropAssemblyDirectory!); + AppendResponseFileOptionalCommand(args, "--debug-repro-directory", DebugReproDirectory); + AppendResponseFileCommand(args, "--use-windows-ui-xaml-projections", UseWindowsUIXamlProjections.ToString()); + AppendResponseFileCommand(args, "--validate-winrt-runtime-assembly-version", ValidateWinRTRuntimeAssemblyVersion.ToString()); + AppendResponseFileCommand(args, "--validate-winrt-runtime-dll-version-2-references", ValidateWinRTRuntimeDllVersion2References.ToString()); + AppendResponseFileCommand(args, "--enable-incremental-generation", EnableIncrementalGeneration.ToString()); + AppendResponseFileCommand(args, "--treat-warnings-as-errors", TreatWarningsAsErrors.ToString()); + AppendResponseFileCommand(args, "--max-degrees-of-parallelism", MaxDegreesOfParallelism.ToString()); + + // Add any additional arguments that are not statically known + foreach (ITaskItem additionalArgument in AdditionalArguments ?? []) + { + _ = args.AppendLine(additionalArgument.ItemSpec); + } + + return args.ToString(); + } + + /// + /// Appends a command line argument to the response file arguments, with the right format. + /// + /// The command line arguments being built. + /// The command name to append. + /// The command value to append. + private static void AppendResponseFileCommand(StringBuilder args, string commandName, string commandValue) + { + _ = args.Append($"{commandName} ").AppendLine(commandValue); + } + + /// + /// Appends an optional command line argument to the response file arguments, with the right format. + /// + /// The command line arguments being built. + /// The command name to append. + /// The optional command value to append. + /// This method will not append the command if is . + private static void AppendResponseFileOptionalCommand(StringBuilder args, string commandName, string? commandValue) + { + if (commandValue is not null) + { + AppendResponseFileCommand(args, commandName, commandValue); + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Windows.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Windows.targets index 644d0faa4658..85c07b37e009 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Windows.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Windows.targets @@ -167,4 +167,178 @@ Copyright (c) .NET Foundation. All rights reserved. and '$(UseUwp)' == 'true' "> + + + + + + true + false + + + $(IntermediateOutputPath) + true + true + true + false + -1 + High + High + true + + + <_CsWinRTGeneratorInteropAssemblyName>WinRT.Interop + <_CsWinRTGeneratorInteropAssemblyFileName>$(_CsWinRTGeneratorInteropAssemblyName).dll + <_CsWinRTGeneratorInteropAssemblyPath>$([MSBuild]::NormalizePath('$(CsWinRTGeneratorInteropAssemblyDirectory)', '$(_CsWinRTGeneratorInteropAssemblyFileName)')) + + + <_RunCsWinRTGeneratorPropertyInputsCachePath Condition="'$(_RunCsWinRTGeneratorPropertyInputsCachePath)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).cswinrtgen.cache + <_RunCsWinRTGeneratorPropertyInputsCachePath>$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(_RunCsWinRTGeneratorPropertyInputsCachePath)')) + + + + + + + + <_WinRTRuntimeDllReferencePath + Include="@(ReferencePath)" + Condition="'%(Filename)%(Extension)' == 'WinRT.Runtime.dll'" /> + + + + + <_WinRTRuntimeDllDirectory>$([System.IO.Path]::GetDirectoryName('%(_WinRTRuntimeDllReferencePath.FullPath)')) + <_CsWinRTOrTargetingPackLibDirectory>$([System.IO.Path]::GetDirectoryName('$(_WinRTRuntimeDllDirectory)')) + <_CsWinRTOrTargetingPackRootDirectory>$([System.IO.Path]::GetDirectoryName('$(_CsWinRTOrTargetingPackLibDirectory)')) + <_CsWinRTOrTargetingPackToolsDirectory>$([System.IO.Path]::Combine('$(_CsWinRTOrTargetingPackRootDirectory)', 'tools')) + + $(CsWinRTToolsDirectory) + $(_CsWinRTOrTargetingPackToolsDirectory) + + + + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTEffectiveToolsDirectory)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTToolsArchitecture)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorInteropAssemblyDirectory)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorDebugReproDirectory)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTUseWindowsUIXamlProjections)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorValidateWinRTRuntimeAssemblyVersion)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorValidateWinRTRuntimeDllVersion2References)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorEnableIncrementalGeneration)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorTreatWarningsAsErrors)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorMaxDegreesOfParallelism)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="@(CsWinRTGeneratorAdditionalArgument)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorStandardOutputImportance)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorStandardErrorImportance)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorLogStandardErrorAsError)" /> + + + + + + + + + + + + + + + + + + + + $(_CsWinRTGeneratorInteropAssemblyName) + .NETCoreApp + true + true + true + _RunCsWinRTGenerator + AnyCPU + + + + + + + + + + + <_SourceItemsToCopyToOutputDirectory + Include="@(CsWinRTGeneratorInteropAssemblyPath)" + TargetPath="$(_CsWinRTGeneratorInteropAssemblyFileName)" /> + + + <_SourceItemsToCopyToPublishDirectory + Include="@(CsWinRTGeneratorInteropAssemblyPath)" + TargetPath="$(_CsWinRTGeneratorInteropAssemblyFileName)" /> + + + + + + +