Skip to content

Commit 0003287

Browse files
authored
Add 'RunCsWinRTGenerator' task and target (#49417)
2 parents 8219b58 + 960f2c6 commit 0003287

File tree

2 files changed

+436
-0
lines changed

2 files changed

+436
-0
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Runtime.InteropServices;
9+
using System.Text;
10+
using Microsoft.Build.Framework;
11+
using Microsoft.Build.Utilities;
12+
13+
namespace Microsoft.NET.Build.Tasks;
14+
15+
/// <summary>
16+
/// The custom MSBuild task that invokes the 'cswinrtgen' tool.
17+
/// </summary>
18+
public sealed class RunCsWinRTGenerator : ToolTask
19+
{
20+
/// <summary>
21+
/// Gets or sets the paths to assembly files that are reference assemblies, representing
22+
/// the entire surface area for compilation. These assemblies are the full set of assemblies
23+
/// that will contribute to the interop .dll being generated.
24+
/// </summary>
25+
[Required]
26+
public ITaskItem[]? ReferenceAssemblyPaths { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets the path to the output assembly that was produced by the build (for the current project).
30+
/// </summary>
31+
/// <remarks>
32+
/// This property is an array, but it should only ever receive a single item.
33+
/// </remarks>
34+
[Required]
35+
public ITaskItem[]? OutputAssemblyPath { get; set; }
36+
37+
/// <summary>
38+
/// Gets or sets the directory where the generated interop assembly will be placed.
39+
/// </summary>
40+
[Required]
41+
public string? InteropAssemblyDirectory { get; set; }
42+
43+
/// <summary>
44+
/// Gets or sets the directory where the debug repro will be produced.
45+
/// </summary>
46+
/// <remarks>If not set, no debug repro will be produced.</remarks>
47+
public string? DebugReproDirectory { get; set; }
48+
49+
/// <summary>
50+
/// Gets or sets the tools directory where the 'cswinrtgen' tool is located.
51+
/// </summary>
52+
[Required]
53+
public string? CsWinRTToolsDirectory { get; set; }
54+
55+
/// <summary>
56+
/// Gets or sets the architecture of 'cswinrtgen' to use.
57+
/// </summary>
58+
/// <remarks>
59+
/// If not set, the architecture will be determined based on the current process architecture.
60+
/// </remarks>
61+
public string? CsWinRTToolsArchitecture { get; set; }
62+
63+
/// <summary>
64+
/// Gets or sets whether to use <c>Windows.UI.Xaml</c> projections.
65+
/// </summary>
66+
/// <remarks>If not set, it will default to <see langword="false"/> (i.e. using <c>Microsoft.UI.Xaml</c> projections).</remarks>
67+
public bool UseWindowsUIXamlProjections { get; set; } = false;
68+
69+
/// <summary>
70+
/// Gets whether to validate the assembly version of <c>WinRT.Runtime.dll</c>, to ensure it matches the generator.
71+
/// </summary>
72+
public bool ValidateWinRTRuntimeAssemblyVersion { get; set; } = true;
73+
74+
/// <summary>
75+
/// Gets whether to validate that any references to <c>WinRT.Runtime.dll</c> version 2 are present across any assemblies.
76+
/// </summary>
77+
public bool ValidateWinRTRuntimeDllVersion2References { get; set; } = true;
78+
79+
/// <summary>
80+
/// Gets whether to enable incremental generation (i.e. with a cache file on disk saving the full set of types to generate).
81+
/// </summary>
82+
public bool EnableIncrementalGeneration { get; set; } = true;
83+
84+
/// <summary>
85+
/// Gets whether to treat warnings coming from 'cswinrtgen' as errors (regardless of the global 'TreatWarningsAsErrors' setting).
86+
/// </summary>
87+
public bool TreatWarningsAsErrors { get; set; } = false;
88+
89+
/// <summary>
90+
/// Gets or sets the maximum number of parallel tasks to use for execution.
91+
/// </summary>
92+
/// <remarks>If not set, the default will match the number of available processor cores.</remarks>
93+
public int MaxDegreesOfParallelism { get; set; } = -1;
94+
95+
/// <summary>
96+
/// Gets or sets additional arguments to pass to the tool.
97+
/// </summary>
98+
public ITaskItem[]? AdditionalArguments { get; set; }
99+
100+
/// <inheritdoc/>
101+
protected override string ToolName => "cswinrtgen.exe";
102+
103+
/// <summary>
104+
/// Gets the effective item spec for the output assembly.
105+
/// </summary>
106+
private string EffectiveOutputAssemblyItemSpec => OutputAssemblyPath![0].ItemSpec;
107+
108+
/// <inheritdoc/>
109+
#if NET10_0_OR_GREATER
110+
[MemberNotNullWhen(true, nameof(ReferenceAssemblyPaths))]
111+
[MemberNotNullWhen(true, nameof(OutputAssemblyPath))]
112+
[MemberNotNullWhen(true, nameof(InteropAssemblyDirectory))]
113+
[MemberNotNullWhen(true, nameof(CsWinRTToolsDirectory))]
114+
#endif
115+
protected override bool ValidateParameters()
116+
{
117+
if (!base.ValidateParameters())
118+
{
119+
return false;
120+
}
121+
122+
if (ReferenceAssemblyPaths is not { Length: > 0 })
123+
{
124+
Log.LogWarning("Invalid 'ReferenceAssemblyPaths' input(s).");
125+
126+
return false;
127+
}
128+
129+
if (OutputAssemblyPath is not { Length: 1 })
130+
{
131+
Log.LogWarning("Invalid 'OutputAssemblyPath' input.");
132+
133+
return false;
134+
}
135+
136+
if (InteropAssemblyDirectory is null || !Directory.Exists(InteropAssemblyDirectory))
137+
{
138+
Log.LogWarning("Generated assembly directory '{0}' is invalid or does not exist.", InteropAssemblyDirectory);
139+
140+
return false;
141+
}
142+
143+
if (DebugReproDirectory is not null && !Directory.Exists(DebugReproDirectory))
144+
{
145+
Log.LogWarning("Debug repro directory '{0}' is invalid or does not exist.", DebugReproDirectory);
146+
147+
return false;
148+
}
149+
150+
if (CsWinRTToolsDirectory is null || !Directory.Exists(CsWinRTToolsDirectory))
151+
{
152+
Log.LogWarning("Tools directory '{0}' is invalid or does not exist.", CsWinRTToolsDirectory);
153+
154+
return false;
155+
}
156+
157+
if (CsWinRTToolsArchitecture is not null &&
158+
!CsWinRTToolsArchitecture.Equals("x86", StringComparison.OrdinalIgnoreCase) &&
159+
!CsWinRTToolsArchitecture.Equals("x64", StringComparison.OrdinalIgnoreCase) &&
160+
!CsWinRTToolsArchitecture.Equals("arm64", StringComparison.OrdinalIgnoreCase) &&
161+
!CsWinRTToolsArchitecture.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase))
162+
{
163+
Log.LogWarning("Tools architecture '{0}' is invalid (it must be 'x86', 'x64', 'arm64', or 'AnyCPU').", CsWinRTToolsArchitecture);
164+
165+
return false;
166+
}
167+
168+
// 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
169+
// 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.
170+
// See: https://learn.microsoft.com/dotnet/api/system.threading.tasks.paralleloptions.maxdegreeofparallelism#system-threading-tasks-paralleloptions-maxdegreeofparallelism.
171+
if (MaxDegreesOfParallelism is not (-1 or > 0))
172+
{
173+
Log.LogWarning("Invalid 'MaxDegreesOfParallelism' value. It must be '-1' or greater than '0' (but was '{0}').", MaxDegreesOfParallelism);
174+
175+
return false;
176+
}
177+
178+
return true;
179+
}
180+
181+
/// <inheritdoc/>
182+
[SuppressMessage("Style", "IDE0072", Justification = "We always use 'x86' as a fallback for all other CPU architectures.")]
183+
protected override string GenerateFullPathToTool()
184+
{
185+
string? effectiveArchitecture = CsWinRTToolsArchitecture;
186+
187+
// Special case for when 'AnyCPU' is specified (mostly for testing scenarios).
188+
// We just reuse the exact input directory and assume the architecture matches.
189+
// This makes it easy to run the task against a local build of 'cswinrtgen'.
190+
if (effectiveArchitecture?.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase) is true)
191+
{
192+
return Path.Combine(CsWinRTToolsDirectory!, ToolName);
193+
}
194+
195+
// If the architecture is not specified, determine it based on the current process architecture
196+
effectiveArchitecture ??= RuntimeInformation.ProcessArchitecture switch
197+
{
198+
Architecture.X64 => "x64",
199+
Architecture.Arm64 => "arm64",
200+
_ => "x86"
201+
};
202+
203+
// The tool is inside an architecture-specific subfolder, as it's a native binary
204+
string architectureDirectory = $"win-{effectiveArchitecture}";
205+
206+
return Path.Combine(CsWinRTToolsDirectory!, architectureDirectory, ToolName);
207+
}
208+
209+
/// <inheritdoc/>
210+
protected override string GenerateResponseFileCommands()
211+
{
212+
StringBuilder args = new();
213+
214+
IEnumerable<string> referenceAssemblyPaths = ReferenceAssemblyPaths!.Select(static path => path.ItemSpec);
215+
string referenceAssemblyPathsArg = string.Join(",", referenceAssemblyPaths);
216+
217+
AppendResponseFileCommand(args, "--reference-assembly-paths", referenceAssemblyPathsArg);
218+
AppendResponseFileCommand(args, "--output-assembly-path", EffectiveOutputAssemblyItemSpec);
219+
AppendResponseFileCommand(args, "--generated-assembly-directory", InteropAssemblyDirectory!);
220+
AppendResponseFileOptionalCommand(args, "--debug-repro-directory", DebugReproDirectory);
221+
AppendResponseFileCommand(args, "--use-windows-ui-xaml-projections", UseWindowsUIXamlProjections.ToString());
222+
AppendResponseFileCommand(args, "--validate-winrt-runtime-assembly-version", ValidateWinRTRuntimeAssemblyVersion.ToString());
223+
AppendResponseFileCommand(args, "--validate-winrt-runtime-dll-version-2-references", ValidateWinRTRuntimeDllVersion2References.ToString());
224+
AppendResponseFileCommand(args, "--enable-incremental-generation", EnableIncrementalGeneration.ToString());
225+
AppendResponseFileCommand(args, "--treat-warnings-as-errors", TreatWarningsAsErrors.ToString());
226+
AppendResponseFileCommand(args, "--max-degrees-of-parallelism", MaxDegreesOfParallelism.ToString());
227+
228+
// Add any additional arguments that are not statically known
229+
foreach (ITaskItem additionalArgument in AdditionalArguments ?? [])
230+
{
231+
_ = args.AppendLine(additionalArgument.ItemSpec);
232+
}
233+
234+
return args.ToString();
235+
}
236+
237+
/// <summary>
238+
/// Appends a command line argument to the response file arguments, with the right format.
239+
/// </summary>
240+
/// <param name="args">The command line arguments being built.</param>
241+
/// <param name="commandName">The command name to append.</param>
242+
/// <param name="commandValue">The command value to append.</param>
243+
private static void AppendResponseFileCommand(StringBuilder args, string commandName, string commandValue)
244+
{
245+
_ = args.Append($"{commandName} ").AppendLine(commandValue);
246+
}
247+
248+
/// <summary>
249+
/// Appends an optional command line argument to the response file arguments, with the right format.
250+
/// </summary>
251+
/// <param name="args">The command line arguments being built.</param>
252+
/// <param name="commandName">The command name to append.</param>
253+
/// <param name="commandValue">The optional command value to append.</param>
254+
/// <remarks>This method will not append the command if <paramref name="commandValue"/> is <see langword="null"/>.</remarks>
255+
private static void AppendResponseFileOptionalCommand(StringBuilder args, string commandName, string? commandValue)
256+
{
257+
if (commandValue is not null)
258+
{
259+
AppendResponseFileCommand(args, commandName, commandValue);
260+
}
261+
}
262+
}

0 commit comments

Comments
 (0)