-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add 'RunCsWinRTGenerator' task and target #49417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
// 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; | ||
|
||
/// <summary> | ||
/// The custom MSBuild task that invokes the 'cswinrtgen' tool. | ||
/// </summary> | ||
public sealed class RunCsWinRTGenerator : ToolTask | ||
{ | ||
/// <summary> | ||
/// The name of the generated interop assembly. | ||
/// </summary> | ||
private const string InteropAssemblyName = "WinRT.Interop.dll"; | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
[Required] | ||
public ITaskItem[]? ReferenceAssemblyPaths { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the path to the output assembly that was produced by the build (for the current project). | ||
/// </summary> | ||
/// <remarks> | ||
/// This property is an array, but it should only ever receive a single item. | ||
/// </remarks> | ||
[Required] | ||
public ITaskItem[]? OutputAssemblyPath { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the directory where the generated interop assembly will be placed. | ||
/// </summary> | ||
/// <remarks>If not set, the same directory as <see cref="OutputAssemblyPath"/> will be used.</remarks> | ||
public string? InteropAssemblyDirectory { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the directory where the debug repro will be produced. | ||
/// </summary> | ||
/// <remarks>If not set, no debug repro will be produced.</remarks> | ||
public string? DebugReproDirectory { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the tools directory where the 'cswinrtgen' tool is located. | ||
/// </summary> | ||
[Required] | ||
public string? CsWinRTToolsDirectory { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets whether to use <c>Windows.UI.Xaml</c> projections. | ||
/// </summary> | ||
/// <remarks>If not set, it will default to <see langword="false"/> (i.e. using <c>Microsoft.UI.Xaml</c> projections).</remarks> | ||
public bool UseWindowsUIXamlProjections { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Gets whether to validate the assembly version of <c>WinRT.Runtime.dll</c>, to ensure it matches the generator. | ||
/// </summary> | ||
public bool ValidateWinRTRuntimeAssemblyVersion { get; init; } = true; | ||
|
||
/// <summary> | ||
/// Gets whether to treat warnings coming from 'cswinrtgen' as errors (regardless of the global 'TreatWarningsAsErrors' setting). | ||
/// </summary> | ||
public bool TreatWarningsAsErrors { get; init; } = false; | ||
|
||
/// <summary> | ||
/// Gets or sets the maximum number of parallel tasks to use for execution. | ||
/// </summary> | ||
/// <remarks>If not set, the default will match the number of available processor cores.</remarks> | ||
public int MaxDegreesOfParallelism { get; set; } = -1; | ||
|
||
/// <summary> | ||
/// Gets or sets additional arguments to pass to the tool. | ||
/// </summary> | ||
public ITaskItem[]? AdditionalArguments { get; set; } | ||
|
||
/// <summary> | ||
/// Gets the resulting generated interop .dll item. | ||
/// </summary> | ||
[Output] | ||
public ITaskItem? InteropAssemblyPath { get; private set; } | ||
|
||
/// <inheritdoc/> | ||
protected override string ToolName => "cswinrtgen.exe"; | ||
|
||
/// <summary> | ||
/// Gets the effective item spec for the output assembly. | ||
/// </summary> | ||
private string EffectiveOutputAssemblyItemSpec => OutputAssemblyPath![0].ItemSpec; | ||
|
||
/// <summary> | ||
/// Gets the effective directory where the generated interop assembly will be placed. | ||
/// </summary> | ||
private string EffectiveGeneratedAssemblyDirectory => InteropAssemblyDirectory ?? Path.GetDirectoryName(EffectiveOutputAssemblyItemSpec)!; | ||
|
||
/// <summary> | ||
/// Gets the effective path of the produced interop assembly. | ||
/// </summary> | ||
private string EffectiveGeneratedAssemblyPath => Path.Combine(EffectiveGeneratedAssemblyDirectory, InteropAssemblyName); | ||
|
||
/// <inheritdoc/> | ||
public override bool Execute() | ||
{ | ||
// If the tool execution fails, we will not have a generated interop .dll path | ||
if (!base.Execute()) | ||
{ | ||
InteropAssemblyPath = null; | ||
|
||
return false; | ||
} | ||
|
||
// Return the generated interop assembly path as an output item | ||
InteropAssemblyPath = new TaskItem(EffectiveGeneratedAssemblyPath); | ||
|
||
return true; | ||
} | ||
|
||
/// <inheritdoc/> | ||
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 not null && !Directory.Exists(InteropAssemblyDirectory)) | ||
{ | ||
Log.LogWarning("Generated assembly directory '{0}' does not exist.", InteropAssemblyDirectory); | ||
|
||
return false; | ||
} | ||
|
||
if (DebugReproDirectory is not null && !Directory.Exists(DebugReproDirectory)) | ||
{ | ||
Log.LogWarning("Debug repro directory '{0}' does not exist.", DebugReproDirectory); | ||
|
||
return false; | ||
} | ||
|
||
if (CsWinRTToolsDirectory is null || !Directory.Exists(CsWinRTToolsDirectory)) | ||
{ | ||
Log.LogWarning("Tools directory '{0}' does not exist.", CsWinRTToolsDirectory); | ||
|
||
return false; | ||
} | ||
|
||
if (MaxDegreesOfParallelism is 0 or < -1) | ||
{ | ||
Log.LogWarning("Invalid 'MaxDegreesOfParallelism' value. It must be '-1' or greater than '0' (but was '{0}').", MaxDegreesOfParallelism); | ||
|
||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/// <inheritdoc/> | ||
[SuppressMessage("Style", "IDE0072", Justification = "We always use 'x86' as a fallback for all other CPU architectures.")] | ||
protected override string GenerateFullPathToTool() | ||
{ | ||
// The tool is inside an architecture-specific subfolder, as it's a native binary | ||
string architectureDirectory = RuntimeInformation.ProcessArchitecture switch | ||
{ | ||
Architecture.X64 => "win-x64", | ||
Architecture.Arm64 => "win-arm64", | ||
_ => "win-x86" | ||
}; | ||
|
||
return Path.Combine(CsWinRTToolsDirectory!, architectureDirectory, ToolName); | ||
} | ||
|
||
/// <inheritdoc/> | ||
protected override string GenerateResponseFileCommands() | ||
{ | ||
StringBuilder args = new(); | ||
|
||
IEnumerable<string> 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", EffectiveGeneratedAssemblyDirectory); | ||
|
||
// The debug repro directory is optional, and might not be set | ||
if (DebugReproDirectory is not null) | ||
{ | ||
AppendResponseFileCommand(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, "--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(); | ||
} | ||
|
||
/// <summary> | ||
/// Appends a command line argument to the response file arguments, with the right format. | ||
/// </summary> | ||
/// <param name="args">The command line arguments being built.</param> | ||
/// <param name="commandName">The command name to append.</param> | ||
/// <param name="commandValue">The command value to append.</param> | ||
private static void AppendResponseFileCommand(StringBuilder args, string commandName, string commandValue) | ||
{ | ||
_ = args.Append($"{commandName} ").AppendLine(commandValue); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -167,4 +167,73 @@ Copyright (c) .NET Foundation. All rights reserved. | |
and '$(UseUwp)' == 'true' "> | ||
<NetSdkError ResourceName="WindowsSDKXamlInvalidTfm" /> | ||
</Target> | ||
|
||
<!-- | ||
============================================================ | ||
_RunCsWinRTGenerator | ||
|
||
Runs 'cswinrtgen' to produce the 'WinRT.Interop.dll' assembly. | ||
============================================================ | ||
--> | ||
<UsingTask Condition="'$(CsWinRTGeneratorTasksOverriden)' != 'true'" TaskName="RunCsWinRTGenerator" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" /> | ||
<Target | ||
Name="_RunCsWinRTGenerator" | ||
DependsOnTargets="CoreCompile;$(GetTargetPathDependsOn);$(GetTargetPathWithTargetPlatformMonikerDependsOn)" | ||
BeforeTargets="GetTargetPath;GetTargetPathWithTargetPlatformMoniker;GenerateBuildDependencyFile;GeneratePublishDependencyFile" | ||
Inputs="@(ReferencePath);@(IntermediateAssembly)" | ||
Condition="'$(CsWinRTGenerateInteropAssembly)' == 'true'"> | ||
|
||
<!-- Default property values for 'cswinrtgen' --> | ||
<PropertyGroup> | ||
<CsWinRTToolsDirectory Condition="'$(CsWinRTToolsDirectory)' == ''"></CsWinRTToolsDirectory> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we need to get the path to the root directory of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's a packageref to this package (implicit or explicit), there may be a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tried in a blank app targeting |
||
<CsWinRTGeneratorValidateWinRTRuntimeAssemblyVersion Condition="'$(ValidateWinRTRuntimeAssemblyVersion)' == ''">true</CsWinRTGeneratorValidateWinRTRuntimeAssemblyVersion> | ||
<CsWinRTGeneratorTreatWarningsAsErrors Condition="'$(CsWinRTGeneratorTreatWarningsAsErrors)' == ''">false</CsWinRTGeneratorTreatWarningsAsErrors> | ||
<CsWinRTGeneratorMaxDegreesOfParallelism Condition="'$(CsWinRTGeneratorMaxDegreesOfParallelism)' == ''">-1</CsWinRTGeneratorMaxDegreesOfParallelism> | ||
<CsWinRTGeneratorStandardOutputImportance Condition="'$(CsWinRTGeneratorStandardOutputImportance)' == ''">High</CsWinRTGeneratorStandardOutputImportance> | ||
<CsWinRTGeneratorStandardErrorImportance Condition="'$(CsWinRTGeneratorStandardErrorImportance)' == ''">High</CsWinRTGeneratorStandardErrorImportance> | ||
<CsWinRTGeneratorLogStandardErrorAsError Condition="'$(CsWinRTGeneratorLogStandardErrorAsError)' == ''">true</CsWinRTGeneratorLogStandardErrorAsError> | ||
</PropertyGroup> | ||
|
||
<!-- Invoke 'cswinrtgen' --> | ||
<CsWinRTGenerator | ||
ReferenceAssemblyPaths="@(ReferencePath)" | ||
OutputAssemblyPath="@(IntermediateAssembly)" | ||
InteropAssemblyDirectory="$(CsWinRTGeneratorInteropAssemblyDirectory)" | ||
DebugReproDirectory="$(CsWinRTGeneratorDebugReproDirectory)" | ||
CsWinRTToolsDirectory="$(CsWinRTToolsDirectory)" | ||
UseWindowsUIXamlProjections="$(CsWinRTUseWindowsUIXamlProjections)" | ||
ValidateWinRTRuntimeAssemblyVersion="$(CsWinRTGeneratorValidateWinRTRuntimeAssemblyVersion)" | ||
TreatWarningsAsErrors="$(CsWinRTGeneratorTreatWarningsAsErrors)" | ||
MaxDegreesOfParallelism="$(CsWinRTGeneratorMaxDegreesOfParallelism)" | ||
AdditionalArguments="@(CsWinRTGeneratorAdditionalArgument)" | ||
StandardOutputImportance="$(CsWinRTGeneratorStandardOutputImportance)" | ||
StandardErrorImportance="$(CsWinRTGeneratorStandardErrorImportance)" | ||
LogStandardErrorAsError="$(CsWinRTGeneratorLogStandardErrorAsError)"> | ||
<Output ItemName="CsWinRTGeneratorInteropAssemblyPath" TaskParameter="InteropAssemblyPath" /> | ||
</CsWinRTGenerator> | ||
|
||
<!-- Set the metadata for the interop .dll item --> | ||
<ItemGroup> | ||
<CsWinRTGeneratorInteropAssemblyPath Condition="'%(Filename)%(Extension)' == 'WinRT.Interop.dll'"> | ||
<AssemblyName>WinRT.Interop</AssemblyName> | ||
<TargetFrameworkIdentifier>.NETCoreApp</TargetFrameworkIdentifier> | ||
<ReferenceOutputAssembly>true</ReferenceOutputAssembly> | ||
<IncludeRuntimeDependency>true</IncludeRuntimeDependency> | ||
<Private>true</Private> | ||
<MSBuildSourceTargetName>InvokeCsWinRTGenerator</MSBuildSourceTargetName> | ||
<Platforms>AnyCPU</Platforms> | ||
<CopyLocal>true</CopyLocal> | ||
</CsWinRTGeneratorInteropAssemblyPath> | ||
</ItemGroup> | ||
|
||
<!-- Add the interop .dll to the list of reference paths --> | ||
<ItemGroup> | ||
<ReferencePath Include="@(CsWinRTGeneratorInteropAssemblyPath)" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need two things here:
I'm not sure how to do both. I could do it by adding the item to both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will need to look into answers for both of these, I'll try to do that tomorrow. |
||
</ItemGroup> | ||
|
||
<!-- Append to 'FileWrites' so the interop .dll will be removed on clean --> | ||
<ItemGroup> | ||
<FileWrites Include="@(CsWinRTGeneratorInteropAssemblyPath)"/> | ||
</ItemGroup> | ||
</Target> | ||
</Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, this target would only run if something changed. As in, if there's been any code changes or if any transitive dependency changed. I'm assuming the
Csc
task that's invoked inCoreCompile
would already be doing some kind of check like that, to skip work if eg. you just F5 again without changing anything. Is there a way for us to perform the same check (or get that result out ofCoreCompile
) so we can also skip this target if not needed?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This concept is known as incrementality. The short form is that if you know your inputs, and your inputs are files whose timestamps can be tracked, then you can set
Inputs=..all that stuff..
on yourTarget
and the MSBuild engine will transparently track them and only call your target if any of them are out of date. Similarly, if you know your Outputs, you can provide those to the engine as well and up-to-date checking will take place.This means that many complex Targets often have a 'compute the inputs and outputs for Target XXXXX' Target that runs immediately before them, so that you can compute the correct set of inputs/outputs for incrementality purposes.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! I added the
Inputs
property. I have a couple more questions though:CsWinRTGeneratorInteropAssemblyPath
item that the target produces as output. Is there a way to configure that returned item as theOutputs
item for the wrapping task?Thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the first point, we usually try to have those paths be able to be input into the Task to help with this kind of incrementality.
For the second, this is a gap in the MSBuild Incrementality model - it only supports files today. To get around this, most people have a 'generate property cache file' Target that they have run immediately before their real Target and they add this generated file to their Inputs. You can see an example of this pattern here.