From 8b18c0f438937e0f1a1e12d598dcba13a15766c3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 17:59:04 -0700 Subject: [PATCH 01/10] Add GenerateCsWinRTStubExes MSBuild task Add a new MSBuild task that generates one or more stub .exe files by invoking MSVC (cl.exe). Each item in the StubExes input describes one stub to generate, with optional metadata for OutputType, Win32Manifest, AppContainer, and Platform. This replaces the inline MSBuild logic from the _CsWinRTGenerateStubExe target with a proper C# task that supports generating multiple stubs in a single build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GenerateCsWinRTStubExes.cs | 666 ++++++++++++++++++ 1 file changed, 666 insertions(+) create mode 100644 src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs new file mode 100644 index 0000000000..6012350c38 --- /dev/null +++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs @@ -0,0 +1,666 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Build.Tasks; + +/// +/// An MSBuild task that generates one or more stub .exe files by invoking MSVC (cl.exe). +/// +/// +/// +/// A "stub .exe" is a small native executable whose only job is to call into a method +/// exported from a companion .dll produced by the Native AOT toolchain. This is needed +/// when the project is compiled as a shared library (i.e. NativeLib=Shared), and the +/// application still needs a standard .exe entry point. +/// +/// +/// Each item in describes one stub to generate. The item identity +/// (Include) is used as the output binary name (i.e. {Identity}.exe), and +/// optional metadata controls per-stub behavior: +/// +/// +/// +/// Metadata +/// Description +/// +/// +/// OutputType +/// +/// The output type for the stub, controlling the PE subsystem. +/// WinExe produces a /SUBSYSTEM:WINDOWS binary; +/// any other value (including Exe) produces /SUBSYSTEM:CONSOLE. +/// Defaults to the project's OutputType (see ). +/// +/// +/// +/// Win32Manifest +/// +/// Path to a Win32 manifest file to embed into the stub .exe. +/// If empty, the stub is produced with /MANIFEST:NO. +/// Defaults to the project's Win32Manifest (see ). +/// +/// +/// +/// AppContainer +/// +/// Whether to set the /APPCONTAINER linker flag, for UWP apps. +/// Defaults to . +/// +/// +/// +/// Platform +/// +/// The target platform (x64, x86, or arm64). +/// Defaults to . +/// +/// +/// +/// +public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task +{ + /// + /// Gets or sets the collection of stub executables to generate. + /// + /// + /// + /// The item identity (Include) is the stub name (used as the output .exe filename). + /// Supported metadata: OutputType, Win32Manifest, AppContainer, Platform. + /// + /// + [Required] + public ITaskItem[]? StubExes { get; set; } + + /// + /// Gets or sets the path to the .c source file used for building the stub executable. + /// + [Required] + public string? SourceFilePath { get; set; } + + /// + /// Gets or sets the path to the .lib file produced by the Native AOT toolchain, + /// which the linker needs to resolve the managed entry point import. + /// + [Required] + public string? NativeLibraryPath { get; set; } + + /// + /// Gets or sets the intermediate output directory where generated files are placed. + /// + [Required] + public string? IntermediateOutputDirectory { get; set; } + + /// + /// Gets or sets the destination directory for the compiled stub .exe files + /// (typically the native output directory alongside the AOT-compiled .dll). + /// + [Required] + public string? DestinationDirectory { get; set; } + + /// + /// Gets or sets the path to the MSVC compiler (cl.exe). + /// + /// + /// When is , this can be just "cl" + /// (resolved from the environment PATH). Otherwise, it should be the full path to cl.exe + /// from the Native AOT tooling's _CppToolsDirectory. + /// + [Required] + public string? ClExePath { get; set; } + + /// + /// Gets or sets whether environmental tools are being used (i.e. building from a VS Developer Command Prompt). + /// + /// + /// When , the task does not need to manually resolve MSVC and Windows SDK include/lib + /// paths, because they are already available on the PATH and in environment variables. + /// + public bool UseEnvironmentalTools { get; set; } + + /// + /// Gets or sets the path to the MSVC include directory. + /// + /// Only used when is . + public string? MsvcIncludePath { get; set; } + + /// + /// Gets or sets the path to the MSVC lib directory (architecture-specific). + /// + /// Only used when is . + public string? MsvcLibPath { get; set; } + + /// + /// Gets or sets the path to the Windows SDK UCRT include directory. + /// + /// Only used when is . + public string? WindowsSdkUcrtIncludePath { get; set; } + + /// + /// Gets or sets the path to the Windows SDK UM include directory. + /// + /// Only used when is . + public string? WindowsSdkUmIncludePath { get; set; } + + /// + /// Gets or sets the path to the Windows SDK UCRT lib directory (architecture-specific). + /// + /// Only used when is . + public string? WindowsSdkUcrtLibPath { get; set; } + + /// + /// Gets or sets the path to the Windows SDK UM lib directory (architecture-specific). + /// + /// Only used when is . + public string? WindowsSdkUmLibPath { get; set; } + + /// + /// Gets or sets additional directories to prepend to the PATH when invoking cl.exe. + /// + /// + /// This is used when not using environmental tools and the Windows SDK build tools directory + /// (containing mt.exe and rc.exe) is not already on the PATH. + /// + public string? AdditionalPath { get; set; } + + /// + /// Gets or sets whether the build configuration is Debug. + /// + /// + /// This controls whether the debug or release variants of the C runtime libraries are linked. + /// + public bool IsDebugConfiguration { get; set; } + + /// + /// Gets or sets whether Control Flow Guard (CFG) is enabled. + /// + /// Maps to the ControlFlowGuard MSBuild property (value "Guard"). + public bool ControlFlowGuard { get; set; } + + /// + /// Gets or sets the CET compatibility mode. + /// + /// + /// Maps to the CETCompat MSBuild property. + /// + /// Empty or non-"false": CET shadow stack is enabled (for x64). + /// "false": CET shadow stack is disabled. + /// + /// + public string? CETCompat { get; set; } + + /// + /// Gets or sets the default output type for stubs that don't specify one. + /// + /// Falls back to the project's OutputType. + public string? DefaultOutputType { get; set; } + + /// + /// Gets or sets the default Win32 manifest path for stubs that don't specify one. + /// + public string? DefaultWin32Manifest { get; set; } + + /// + /// Gets or sets the default AppContainer value for stubs that don't specify one. + /// + public bool DefaultAppContainer { get; set; } + + /// + /// Gets or sets the default platform for stubs that don't specify one. + /// + public string? DefaultPlatform { get; set; } + + /// + /// Gets or sets whether to include generated stub .exe files in the publish output. + /// + /// Maps to CopyBuildOutputToPublishDirectory. + public bool CopyBuildOutputToPublishDirectory { get; set; } = true; + + /// + /// Returns the list of generated stub .exe items to be included in ResolvedFileToPublish. + /// + /// + /// Each output item has RelativePath and CopyToPublishDirectory metadata set, + /// so the calling target can merge them directly into ResolvedFileToPublish. + /// + [Output] + public ITaskItem[]? GeneratedStubExes { get; set; } + + /// + public override bool Execute() + { + if (StubExes is not { Length: > 0 }) + { + Log.LogError("No stub executables were specified."); + + return false; + } + + if (string.IsNullOrEmpty(SourceFilePath) || !File.Exists(SourceFilePath)) + { + Log.LogError("The stub .exe source file '{0}' does not exist.", SourceFilePath); + + return false; + } + + if (string.IsNullOrEmpty(NativeLibraryPath) || !File.Exists(NativeLibraryPath)) + { + Log.LogError("The native library '{0}' does not exist.", NativeLibraryPath); + + return false; + } + + if (string.IsNullOrEmpty(IntermediateOutputDirectory)) + { + Log.LogError("The intermediate output directory was not specified."); + + return false; + } + + if (string.IsNullOrEmpty(DestinationDirectory)) + { + Log.LogError("The destination directory was not specified."); + + return false; + } + + if (string.IsNullOrEmpty(ClExePath)) + { + Log.LogError( + "Failed to find 'cl.exe', which is needed to compile stub executables. " + + "Try setting 'CsWinRTUseEnvironmentalTools' and building from a Visual Studio Developer Command Prompt (or PowerShell) session."); + + return false; + } + + // Validate MSVC/SDK paths when not using environmental tools + if (!UseEnvironmentalTools) + { + if (string.IsNullOrEmpty(MsvcIncludePath) || !Directory.Exists(MsvcIncludePath) || + string.IsNullOrEmpty(WindowsSdkUcrtIncludePath) || !Directory.Exists(WindowsSdkUcrtIncludePath)) + { + Log.LogError( + "Failed to find the paths for the include folders to pass to MSVC, which are needed to compile stub executables. " + + "Try setting 'CsWinRTUseEnvironmentalTools' and building from a Visual Studio Developer Command Prompt (or PowerShell) session."); + + return false; + } + } + + List generatedItems = new(); + + foreach (ITaskItem stubExe in StubExes) + { + if (!GenerateStubExe(stubExe, generatedItems)) + { + return false; + } + } + + GeneratedStubExes = generatedItems.ToArray(); + + return true; + } + + /// + /// Generates a single stub .exe for the given item. + /// + /// The describing the stub to generate. + /// The list to add generated output items to. + /// if the stub was generated successfully; otherwise, . + private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) + { + string stubName = stubExe.ItemSpec; + + // Resolve per-stub metadata, falling back to defaults + string outputType = GetMetadataOrDefault(stubExe, "OutputType", DefaultOutputType ?? "Exe"); + string win32Manifest = GetMetadataOrDefault(stubExe, "Win32Manifest", DefaultWin32Manifest ?? ""); + bool appContainer = GetBooleanMetadataOrDefault(stubExe, "AppContainer", DefaultAppContainer); + string platform = GetMetadataOrDefault(stubExe, "Platform", DefaultPlatform ?? ""); + + // Validate the platform + if (!UseEnvironmentalTools && + !string.Equals(platform, "arm64", StringComparison.OrdinalIgnoreCase) && + !string.Equals(platform, "x64", StringComparison.OrdinalIgnoreCase) && + !string.Equals(platform, "x86", StringComparison.OrdinalIgnoreCase)) + { + Log.LogError( + "Invalid platform '{0}' for stub '{1}'. Make sure to set 'Platform' to either 'arm64', 'x64', or 'x86'. " + + "Alternatively, try setting 'CsWinRTUseEnvironmentalTools' and building from a Visual Studio Developer Command Prompt (or PowerShell) session.", + platform, + stubName); + + return false; + } + + // Prepare paths for the intermediate working directory for this stub + string stubIntermediateDir = Path.Combine(IntermediateOutputDirectory!, stubName); + string sourceFileName = stubName + ".c"; + string sourceFilePath = Path.Combine(stubIntermediateDir, sourceFileName); + string binaryFileName = stubName + ".exe"; + string binaryOutputFilePath = Path.Combine(stubIntermediateDir, binaryFileName); + string binaryDestinationFilePath = Path.Combine(DestinationDirectory!, binaryFileName); + + Log.LogMessage(MessageImportance.Normal, "Generating stub .exe '{0}'", stubName); + + // Ensure the intermediate directory exists + Directory.CreateDirectory(stubIntermediateDir); + + // Copy the .c source template into the working directory with the stub-specific name + File.Copy(SourceFilePath!, sourceFilePath, overwrite: true); + + // Delete any previously-generated broken .exe in the destination (see https://github.com/dotnet/runtime/issues/111313) + if (File.Exists(binaryDestinationFilePath)) + { + File.Delete(binaryDestinationFilePath); + } + + // Build the MSVC command-line arguments + string arguments = BuildMsvcArguments( + sourceFilePath: sourceFilePath, + platform: platform, + outputType: outputType, + win32Manifest: win32Manifest, + appContainer: appContainer); + + // Invoke cl.exe + if (!InvokeCompiler(arguments, stubIntermediateDir, stubName)) + { + return false; + } + + // Copy the resulting executable to the native output directory + if (!File.Exists(binaryOutputFilePath)) + { + Log.LogError("The stub .exe '{0}' was not produced by the compiler.", binaryOutputFilePath); + + return false; + } + + File.Copy(binaryOutputFilePath, binaryDestinationFilePath, overwrite: true); + + // If publishing, add to the output items + if (CopyBuildOutputToPublishDirectory) + { + TaskItem outputItem = new(binaryDestinationFilePath); + + outputItem.SetMetadata("RelativePath", binaryFileName); + outputItem.SetMetadata("CopyToPublishDirectory", "PreserveNewest"); + + generatedItems.Add(outputItem); + } + + return true; + } + + /// + /// Builds the full MSVC command-line arguments string for compiling a single stub .exe. + /// + /// The path to the .c source file. + /// The target platform. + /// The output type (WinExe or Exe). + /// The optional Win32 manifest path. + /// Whether to enable /APPCONTAINER. + /// The formatted arguments string. + private string BuildMsvcArguments( + string sourceFilePath, + string platform, + string outputType, + string win32Manifest, + bool appContainer) + { + StringBuilder args = new(); + + // Hide the copyright banner (https://learn.microsoft.com/cpp/build/reference/nologo-suppress-startup-banner-c-cpp) + args.Append("/nologo "); + + // If not using environmental tools, pass the paths to the required include folders + if (!UseEnvironmentalTools) + { + AppendQuoted(args, "/I", MsvcIncludePath!); + AppendQuoted(args, "/I", WindowsSdkUcrtIncludePath!); + AppendQuoted(args, "/I", WindowsSdkUmIncludePath!); + } + + // Configure the C runtime library linking behavior: + // - Statically link the MSVC-specific runtime (VCRUNTIME140), which is small. + // - Dynamically link UCRT (the OS-provided C runtime), matching Native AOT behavior. + // We start with /MT[d] for static linking, then override the UCRT portion below. + // See: https://learn.microsoft.com/cpp/build/reference/md-mt-ld-use-run-time-library + args.Append(IsDebugConfiguration ? "/MTd " : "/MT "); + + // Optimize for speed (https://learn.microsoft.com/cpp/build/reference/o1-o2-minimize-size-maximize-speed) + args.Append("/O2 "); + + // Source file and native library + AppendQuotedPath(args, sourceFilePath); + AppendQuotedPath(args, NativeLibraryPath!); + + // If not using environmental tools, pass the paths to the required lib folders + if (!UseEnvironmentalTools) + { + AppendQuotedWildcard(args, MsvcLibPath!); + AppendQuotedWildcard(args, WindowsSdkUcrtLibPath!); + AppendQuotedWildcard(args, WindowsSdkUmLibPath!); + } + + // Start linker options (https://learn.microsoft.com/cpp/build/reference/compiler-command-line-syntax) + args.Append("/link "); + + // Hide the copyright banner for the linker + args.Append("/NOLOGO "); + + // Embed a manifest if specified, otherwise suppress manifest generation. + // See: https://learn.microsoft.com/cpp/build/reference/manifest-create-side-by-side-assembly-manifest + if (!string.IsNullOrEmpty(win32Manifest)) + { + args.AppendFormat("/MANIFEST:EMBED /MANIFESTINPUT:{0} ", win32Manifest); + } + else + { + args.Append("/MANIFEST:NO "); + } + + // Switch UCRT from static to dynamic linking to reduce binary size (~98 KB → ~20 KB). + // See: https://learn.microsoft.com/cpp/build/reference/defaultlib-specify-default-library + // See: https://learn.microsoft.com/cpp/build/reference/nodefaultlib-ignore-libraries + if (IsDebugConfiguration) + { + args.Append("/NODEFAULTLIB:libucrtd.lib /DEFAULTLIB:ucrtd.lib "); + } + else + { + args.Append("/NODEFAULTLIB:libucrt.lib /DEFAULTLIB:ucrt.lib "); + } + + // Skip incremental linking (https://learn.microsoft.com/cpp/build/reference/incremental-link-incrementally) + args.Append("/INCREMENTAL:NO "); + + // Enable COMDAT folding and unreferenced code removal (https://learn.microsoft.com/cpp/build/reference/opt-optimizations) + args.Append("/OPT:ICF /OPT:REF "); + + // Set the AppContainer bit for UWP apps (https://learn.microsoft.com/cpp/build/reference/appcontainer-windows-store-app) + if (appContainer) + { + args.Append("/APPCONTAINER "); + } + + // Set the subsystem type (https://learn.microsoft.com/cpp/build/reference/subsystem-specify-subsystem) + args.Append(string.Equals(outputType, "WinExe", StringComparison.OrdinalIgnoreCase) + ? "/SUBSYSTEM:WINDOWS " + : "/SUBSYSTEM:CONSOLE "); + + // Always use 'wmainCRTStartup' as the entry point, matching Native AOT behavior. + // This allows using 'wmain' for all application types, including WinExe. + // See: https://learn.microsoft.com/cpp/build/reference/entry-entry-point-symbol + args.Append("/ENTRY:wmainCRTStartup "); + + // Enable CFG if requested (https://learn.microsoft.com/cpp/build/reference/guard-enable-control-flow-guard) + if (ControlFlowGuard) + { + args.Append("/guard:cf "); + } + + // Configure CET shadow stack compatibility (https://learn.microsoft.com/cpp/build/reference/cetcompat) + bool cetEnabled = !string.Equals(CETCompat, "false", StringComparison.OrdinalIgnoreCase); + bool isX64 = string.Equals(platform, "x64", StringComparison.OrdinalIgnoreCase); + + if (isX64) + { + args.Append(cetEnabled ? "/CETCOMPAT " : "/CETCOMPAT:NO "); + } + + // Enable EH continuation metadata if CET is enabled and CFG is active, matching Native AOT. + // See: https://learn.microsoft.com/cpp/build/reference/guard-enable-eh-continuation-metadata + if (cetEnabled && isX64 && ControlFlowGuard) + { + args.Append("/guard:ehcont "); + } + + return args.ToString().TrimEnd(); + } + + /// + /// Invokes cl.exe with the given arguments. + /// + /// The command-line arguments. + /// The working directory for the process. + /// The name of the stub being compiled (for diagnostics). + /// if the compiler exited successfully; otherwise, . + private bool InvokeCompiler(string arguments, string workingDirectory, string stubName) + { + ProcessStartInfo startInfo = new() + { + FileName = ClExePath!, + Arguments = arguments, + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + // Add extra directories to the PATH if needed (e.g. for mt.exe and rc.exe) + if (!string.IsNullOrEmpty(AdditionalPath)) + { + string currentPath = startInfo.EnvironmentVariables.ContainsKey("PATH") + ? startInfo.EnvironmentVariables["PATH"]! + : ""; + + startInfo.EnvironmentVariables["PATH"] = currentPath + ";" + AdditionalPath; + } + + try + { + using Process? process = Process.Start(startInfo); + + if (process is null) + { + Log.LogError("Failed to start cl.exe for stub '{0}'.", stubName); + + return false; + } + + string stdout = process.StandardOutput.ReadToEnd(); + string stderr = process.StandardError.ReadToEnd(); + + process.WaitForExit(); + + // Log compiler output + if (!string.IsNullOrWhiteSpace(stdout)) + { + Log.LogMessage(MessageImportance.Normal, stdout.TrimEnd()); + } + + if (!string.IsNullOrWhiteSpace(stderr)) + { + Log.LogWarning("{0}", stderr.TrimEnd()); + } + + if (process.ExitCode != 0) + { + Log.LogError("cl.exe failed for stub '{0}' with exit code {1}.", stubName, process.ExitCode); + + return false; + } + + return true; + } + catch (Exception e) + { + Log.LogError("Failed to invoke cl.exe for stub '{0}': {1}", stubName, e.Message); + + return false; + } + } + + /// + /// Gets a metadata value from an item, falling back to a default if the metadata is empty. + /// + /// The task item. + /// The metadata name. + /// The default value if metadata is empty. + /// The effective metadata value. + private static string GetMetadataOrDefault(ITaskItem item, string metadataName, string defaultValue) + { + string value = item.GetMetadata(metadataName); + + return string.IsNullOrEmpty(value) ? defaultValue : value; + } + + /// + /// Gets a boolean metadata value from an item, falling back to a default if the metadata is empty. + /// + /// The task item. + /// The metadata name. + /// The default value if metadata is empty. + /// The effective boolean value. + private static bool GetBooleanMetadataOrDefault(ITaskItem item, string metadataName, bool defaultValue) + { + string value = item.GetMetadata(metadataName); + + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + + return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Appends a quoted path argument to the string builder (e.g. "C:\path\file.c" ). + /// + /// The string builder. + /// The path to quote. + private static void AppendQuotedPath(StringBuilder args, string path) + { + args.Append('"').Append(path).Append("\" "); + } + + /// + /// Appends a quoted argument with a flag prefix (e.g. /I "C:\include" ). + /// + /// The string builder. + /// The compiler flag. + /// The path to quote. + private static void AppendQuoted(StringBuilder args, string flag, string path) + { + args.Append(flag).Append(" \"").Append(path).Append("\" "); + } + + /// + /// Appends a quoted wildcard lib path (e.g. "C:\lib\*.lib" ). + /// + /// The string builder. + /// The library directory path. + private static void AppendQuotedWildcard(StringBuilder args, string libDir) + { + args.Append('"').Append(Path.Combine(libDir, "*.lib")).Append("\" "); + } +} From 7a645774ffbbf3b35e79ad0c7584d309a356e551 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 18:06:06 -0700 Subject: [PATCH 02/10] Update _CsWinRTGenerateStubExe target to use GenerateCsWinRTStubExes task Refactor the _CsWinRTGenerateStubExe target to delegate all MSVC compilation logic to the new GenerateCsWinRTStubExes MSBuild task. The target now supports multiple stub .exe generation through the CsWinRTStubExe item group. Each item's identity is the output binary name, with optional metadata for OutputType, Win32Manifest, AppContainer, and Platform. When no items are defined, the target adds a default item named after the assembly, preserving the original single-stub behavior. The target still handles resolving all environment-specific paths (MSVC toolchain, Windows SDK, cl.exe location) and passes them to the task, which handles the actual compilation and output management. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nuget/Microsoft.Windows.CsWinRT.targets | 251 ++++++++---------------- 1 file changed, 87 insertions(+), 164 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 8ae76bd4f6..218a0c633b 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -362,11 +362,43 @@ $(CsWinRTInternalProjection) + - - + + <_StubExeFolderName>StubExe <_StubExeGeneratedFilesDir Condition="'$(_StubExeGeneratedFilesDir)' == '' AND '$(GeneratedFilesDir)' != ''">$(GeneratedFilesDir)\$(_StubExeFolderName)\ <_StubExeGeneratedFilesDir Condition="'$(_StubExeGeneratedFilesDir)' == ''">$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', 'Generated Files', '$(_StubExeFolderName)')) - + <_StubExeNativeLibraryPath>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(NativeOutputPath), $(AssemblyName).lib)) - - - <_StubExeSourceFileName>$(AssemblyName).c - <_StubExeSourceFilePath>$([System.IO.Path]::Combine($(_StubExeGeneratedFilesDir), $(_StubExeSourceFileName))) - + <_StubExeOriginalSourceFilePath>$([System.IO.Path]::Combine($(MSBuildThisFileDirectory), 'sources', 'StubExe.c')) - - - <_StubExeBinaryFileName>$(AssemblyName).exe - <_StubExeBinaryOutputFilePath>$([System.IO.Path]::Combine($(_StubExeGeneratedFilesDir), $(_StubExeBinaryFileName))) - <_StubExeBinaryDestinationFilePath>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(NativeOutputPath), $(_StubExeBinaryFileName))) - + + <_StubExeDestinationDir>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(NativeOutputPath))) + + <_StubExePlatform Condition="'$(_StubExePlatform)' == '' AND '$(Platform)' != '' AND '$(Platform)' != 'AnyCPU'">$(Platform) <_StubExePlatform Condition="'$(_StubExePlatform)' == '' AND '$(PlatformTarget)' != ''">$(PlatformTarget) + <_StubExeWin32ManifestPath Condition="'$(Win32Manifest)' != ''">$([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(Win32Manifest))) - - - - - - - - + + + + <_ClExeFilePath Condition="'$(_ClExeFilePath)' == '' AND '$(CsWinRTUseEnvironmentalTools)' == 'true'">cl - <_ClExeFilePath Condition="'$(_ClExeFilePath)' == '' AND '$(_CppToolsDirectory)' != ''">"$([System.IO.Path]::Combine($(_CppToolsDirectory), 'cl.exe'))" + <_ClExeFilePath Condition="'$(_ClExeFilePath)' == '' AND '$(_CppToolsDirectory)' != ''">$([System.IO.Path]::Combine($(_CppToolsDirectory), 'cl.exe')) - - - - + <_MsvcCurrentVersionInstallDirectory>$([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine($(_CppToolsDirectory), '..', '..', '..')))) <_MsvcIncludePath>$([System.IO.Path]::Combine($(_MsvcCurrentVersionInstallDirectory), 'include')) <_MsvcLibPath>$([System.IO.Path]::Combine($(_MsvcCurrentVersionInstallDirectory), 'lib', '$(_StubExePlatform)')) - <_MsvcLibWildcardPath>$([System.IO.Path]::Combine($(_MsvcLibPath), '*.lib')) <_WindowsSdk10InstallDirectory Condition="'$(_WindowsSdk10InstallDirectory)' == '' AND '$(WindowsSdkPath)' != ''">$(WindowsSdkPath) @@ -445,141 +465,44 @@ $(CsWinRTInternalProjection) <_WindowsSdkLibPath Condition="'$(EffectiveTargetPlatformVersion)' != ''">$([System.IO.Path]::Combine($(_WindowsSdk10InstallDirectory), 'Lib', '$(EffectiveTargetPlatformVersion)')) <_WindowsSdk-ucrt-LibPath>$([System.IO.Path]::Combine($(_WindowsSdkLibPath), 'ucrt', '$(_StubExePlatform)')) <_WindowsSdk-um-LibPath>$([System.IO.Path]::Combine($(_WindowsSdkLibPath), 'um', '$(_StubExePlatform)')) - <_WindowsSdk-ucrt-LibWildcardPath>$([System.IO.Path]::Combine($(_WindowsSdk-ucrt-LibPath), '*.lib')) - <_WindowsSdk-um-LibWildcardPath>$([System.IO.Path]::Combine($(_WindowsSdk-um-LibPath), '*.lib')) - - - - - - - - - - - - - <_StubExeMsvcArgs Condition="'$(CsWinRTUseEnvironmentalTools)' != 'true'" Include='"$(_MsvcLibWildcardPath)"' /> - <_StubExeMsvcArgs Condition="'$(CsWinRTUseEnvironmentalTools)' != 'true'" Include='"$(_WindowsSdk-ucrt-LibWildcardPath)"' /> - <_StubExeMsvcArgs Condition="'$(CsWinRTUseEnvironmentalTools)' != 'true'" Include='"$(_WindowsSdk-um-LibWildcardPath)"' /> - - - <_StubExeMsvcArgs Include="/nologo" /> - - - <_StubExeMsvcArgs Condition="'$(CsWinRTUseEnvironmentalTools)' != 'true'" Include='/I "$(_MsvcIncludePath)"' /> - <_StubExeMsvcArgs Condition="'$(CsWinRTUseEnvironmentalTools)' != 'true'" Include='/I "$(_WindowsSdk-ucrt-IncludePath)"' /> - <_StubExeMsvcArgs Condition="'$(CsWinRTUseEnvironmentalTools)' != 'true'" Include='/I "$(_WindowsSdk-um-IncludePath)"' /> - - - <_StubExeMsvcArgs Condition="'$(Configuration)' == 'Debug'" Include="/MTd" /> - <_StubExeMsvcArgs Condition="'$(Configuration)' != 'Debug'" Include="/MT" /> - - - <_StubExeMsvcArgs Include="/O2" /> - - - <_StubExeMsvcArgs Include="/link" /> - - - <_StubExeMsvcArgs Include="/NOLOGO" /> - - - <_StubExeMsvcArgs Include="/MANIFEST:NO" Condition="'$(_StubExeWin32ManifestPath)'==''" /> - <_StubExeMsvcArgs Include="/MANIFEST:EMBED /MANIFESTINPUT:$(_StubExeWin32ManifestPath)" Condition="'$(_StubExeWin32ManifestPath)'!=''" /> - - - <_StubExeMsvcArgs Condition="'$(Configuration)' == 'Debug'" Include="/NODEFAULTLIB:libucrtd.lib" /> - <_StubExeMsvcArgs Condition="'$(Configuration)' != 'Debug'" Include="/NODEFAULTLIB:libucrt.lib" /> - <_StubExeMsvcArgs Condition="'$(Configuration)' == 'Debug'" Include="/DEFAULTLIB:ucrtd.lib" /> - <_StubExeMsvcArgs Condition="'$(Configuration)' != 'Debug'" Include="/DEFAULTLIB:ucrt.lib" /> - - - <_StubExeMsvcArgs Include="/INCREMENTAL:NO" /> - - - <_StubExeMsvcArgs Include="/OPT:ICF" /> - <_StubExeMsvcArgs Include="/OPT:REF" /> - - - <_StubExeMsvcArgs Condition="'$(UseUwpTools)' == 'true'" Include="/APPCONTAINER" /> - - - <_StubExeMsvcArgs Condition="'$(OutputType)' == 'WinExe'" Include="/SUBSYSTEM:WINDOWS" /> - <_StubExeMsvcArgs Condition="'$(OutputType)' != 'WinExe'" Include="/SUBSYSTEM:CONSOLE" /> - - - <_StubExeMsvcArgs Include="/ENTRY:wmainCRTStartup" /> - - - <_StubExeMsvcArgs Condition="'$(ControlFlowGuard)' == 'Guard'" Include="/guard:cf" /> - - <_StubExeMsvcArgs Condition="'$(CETCompat)' != 'false' and '$(_StubExePlatform)' == 'x64'" Include="/CETCOMPAT" /> - <_StubExeMsvcArgs Condition="'$(CETCompat)' == 'false' and '$(_StubExePlatform)' == 'x64'" Include="/CETCOMPAT:NO" /> - - - <_StubExeMsvcArgs Condition="'$(CETCompat)' != 'false' and '$(_StubExePlatform)' == 'x64' and '$(ControlFlowGuard)' == 'Guard'" Include="/guard:ehcont"/> - - - - - <_StubExeMsvcArgsText>@(_StubExeMsvcArgs, ' ') + + <_StubExeAdditionalPath Condition="'$(WindowsSDKBuildToolsBinVersionedArchFolder)' != ''">$(PATH);$(WindowsSDKBuildToolsBinVersionedArchFolder) - - - - - - <_StubExeAdditionalPath>$(PATH);$(WindowsSDKBuildToolsBinVersionedArchFolder) - <_StubExeEnvVars>PATH=$(_StubExeAdditionalPath.Replace(';','%3B')) - - - - - - - - + + - + + + + + + - - $(_StubExeBinaryFileName) - PreserveNewest - + From e1cfe958924ab903c99dfb3bfa8878831acaa2bc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 18:40:29 -0700 Subject: [PATCH 03/10] Support per-stub SourceText and SourceFile metadata Add two new optional metadata properties to CsWinRTStubExe items: - SourceText: inline C source code written directly to a .c file and compiled. Takes highest precedence. - SourceFile: path to a custom .c source file, copied and compiled. Takes precedence over the default source template. When neither is specified, the default CsWinRT source template (StubExe.c from the NuGet package) is used, preserving backward compatibility. The SourceFilePath task parameter is no longer required, allowing all items to provide their own source without a default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nuget/Microsoft.Windows.CsWinRT.targets | 7 ++ .../GenerateCsWinRTStubExes.cs | 77 ++++++++++++++++--- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 218a0c633b..7247eda4e1 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -390,11 +390,18 @@ $(CsWinRTInternalProjection) Defaults to $(UseUwpTools). Platform - Target platform (arm64, x64, x86). Defaults to the resolved platform from $(Platform) / $(PlatformTarget). + SourceText - Inline C source code for the stub. When set, this text is + written to a .c file and compiled. Takes precedence over + SourceFile and the default source template. + SourceFile - Path to a custom .c source file for the stub. When set, it + is copied and compiled. Takes precedence over the default + source template, but is overridden by SourceText. Example: + ============================================================ --> diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs index 6012350c38..4cd046e96c 100644 --- a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs +++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs @@ -62,6 +62,23 @@ namespace Microsoft.NET.Build.Tasks; /// Defaults to . /// /// +/// +/// SourceText +/// +/// Inline C source code to use for this stub. When set, the text is written +/// to a .c file in the intermediate directory and compiled. Takes +/// precedence over SourceFile and the default . +/// +/// +/// +/// SourceFile +/// +/// Path to a custom .c source file to use for this stub. When set, +/// the file is copied to the intermediate directory and compiled. Takes +/// precedence over the default , but is +/// overridden by SourceText if both are specified. +/// +/// /// /// public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task @@ -72,16 +89,20 @@ public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task /// /// /// The item identity (Include) is the stub name (used as the output .exe filename). - /// Supported metadata: OutputType, Win32Manifest, AppContainer, Platform. + /// Supported metadata: OutputType, Win32Manifest, AppContainer, Platform, + /// SourceText, SourceFile. /// /// [Required] public ITaskItem[]? StubExes { get; set; } /// - /// Gets or sets the path to the .c source file used for building the stub executable. + /// Gets or sets the path to the default .c source file used for building stub executables. /// - [Required] + /// + /// This is used as a fallback when a stub item does not specify SourceText or SourceFile + /// metadata. If all items provide their own source, this property can be left empty. + /// public string? SourceFilePath { get; set; } /// @@ -242,13 +263,6 @@ public override bool Execute() return false; } - if (string.IsNullOrEmpty(SourceFilePath) || !File.Exists(SourceFilePath)) - { - Log.LogError("The stub .exe source file '{0}' does not exist.", SourceFilePath); - - return false; - } - if (string.IsNullOrEmpty(NativeLibraryPath) || !File.Exists(NativeLibraryPath)) { Log.LogError("The native library '{0}' does not exist.", NativeLibraryPath); @@ -323,6 +337,8 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) string win32Manifest = GetMetadataOrDefault(stubExe, "Win32Manifest", DefaultWin32Manifest ?? ""); bool appContainer = GetBooleanMetadataOrDefault(stubExe, "AppContainer", DefaultAppContainer); string platform = GetMetadataOrDefault(stubExe, "Platform", DefaultPlatform ?? ""); + string sourceText = stubExe.GetMetadata("SourceText"); + string sourceFile = stubExe.GetMetadata("SourceFile"); // Validate the platform if (!UseEnvironmentalTools && @@ -352,8 +368,45 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) // Ensure the intermediate directory exists Directory.CreateDirectory(stubIntermediateDir); - // Copy the .c source template into the working directory with the stub-specific name - File.Copy(SourceFilePath!, sourceFilePath, overwrite: true); + // Resolve the source for this stub: + // 1. SourceText metadata: write inline text to the .c file + // 2. SourceFile metadata: copy the specified file + // 3. Default SourceFilePath: copy the default template from the NuGet package + if (!string.IsNullOrEmpty(sourceText)) + { + File.WriteAllText(sourceFilePath, sourceText); + } + else if (!string.IsNullOrEmpty(sourceFile)) + { + if (!File.Exists(sourceFile)) + { + Log.LogError("The source file '{0}' specified for stub '{1}' does not exist.", sourceFile, stubName); + + return false; + } + + File.Copy(sourceFile, sourceFilePath, overwrite: true); + } + else if (!string.IsNullOrEmpty(SourceFilePath)) + { + if (!File.Exists(SourceFilePath)) + { + Log.LogError("The default stub .exe source file '{0}' does not exist.", SourceFilePath); + + return false; + } + + File.Copy(SourceFilePath, sourceFilePath, overwrite: true); + } + else + { + Log.LogError( + "No source was specified for stub '{0}'. Set 'SourceText' or 'SourceFile' metadata on the item, " + + "or provide a default source file via the 'SourceFilePath' task parameter.", + stubName); + + return false; + } // Delete any previously-generated broken .exe in the destination (see https://github.com/dotnet/runtime/issues/111313) if (File.Exists(binaryDestinationFilePath)) From 31991f6cf79999dda7be709515ff5c3791c6530f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 18:44:28 -0700 Subject: [PATCH 04/10] Rename SourceFilePath to DefaultSourceFilePath Rename the task parameter from SourceFilePath to DefaultSourceFilePath for clarity, since it serves as the fallback when no per-item SourceText or SourceFile metadata is specified. Update the corresponding MSBuild target property reference to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nuget/Microsoft.Windows.CsWinRT.targets | 2 +- .../GenerateCsWinRTStubExes.cs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 7247eda4e1..812e11b705 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -483,7 +483,7 @@ $(CsWinRTInternalProjection) /// Inline C source code to use for this stub. When set, the text is written /// to a .c file in the intermediate directory and compiled. Takes -/// precedence over SourceFile and the default . +/// precedence over SourceFile and the default . /// /// /// @@ -75,7 +75,7 @@ namespace Microsoft.NET.Build.Tasks; /// /// Path to a custom .c source file to use for this stub. When set, /// the file is copied to the intermediate directory and compiled. Takes -/// precedence over the default , but is +/// precedence over the default , but is /// overridden by SourceText if both are specified. /// /// @@ -103,7 +103,7 @@ public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task /// This is used as a fallback when a stub item does not specify SourceText or SourceFile /// metadata. If all items provide their own source, this property can be left empty. /// - public string? SourceFilePath { get; set; } + public string? DefaultSourceFilePath { get; set; } /// /// Gets or sets the path to the .lib file produced by the Native AOT toolchain, @@ -371,7 +371,7 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) // Resolve the source for this stub: // 1. SourceText metadata: write inline text to the .c file // 2. SourceFile metadata: copy the specified file - // 3. Default SourceFilePath: copy the default template from the NuGet package + // 3. Default DefaultSourceFilePath: copy the default template from the NuGet package if (!string.IsNullOrEmpty(sourceText)) { File.WriteAllText(sourceFilePath, sourceText); @@ -387,22 +387,22 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) File.Copy(sourceFile, sourceFilePath, overwrite: true); } - else if (!string.IsNullOrEmpty(SourceFilePath)) + else if (!string.IsNullOrEmpty(DefaultSourceFilePath)) { - if (!File.Exists(SourceFilePath)) + if (!File.Exists(DefaultSourceFilePath)) { - Log.LogError("The default stub .exe source file '{0}' does not exist.", SourceFilePath); + Log.LogError("The default stub .exe source file '{0}' does not exist.", DefaultSourceFilePath); return false; } - File.Copy(SourceFilePath, sourceFilePath, overwrite: true); + File.Copy(DefaultSourceFilePath, sourceFilePath, overwrite: true); } else { Log.LogError( "No source was specified for stub '{0}'. Set 'SourceText' or 'SourceFile' metadata on the item, " + - "or provide a default source file via the 'SourceFilePath' task parameter.", + "or provide a default source file via the 'DefaultSourceFilePath' task parameter.", stubName); return false; From 396bd5f4a3924d5de9cf0259ea4adab7c95579c5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 18:45:39 -0700 Subject: [PATCH 05/10] Change CETCompat task parameter from string to bool Replace the string-based CETCompat property (which compared against the literal 'false') with a proper bool defaulting to true. The MSBuild target now evaluates the condition and passes a boolean value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nuget/Microsoft.Windows.CsWinRT.targets | 2 +- .../GenerateCsWinRTStubExes.cs | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 812e11b705..a93be25a34 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -498,7 +498,7 @@ $(CsWinRTInternalProjection) AdditionalPath="$(_StubExeAdditionalPath)" IsDebugConfiguration="$([MSBuild]::ValueOrDefault('$(Configuration)', '') == 'Debug')" ControlFlowGuard="$([MSBuild]::ValueOrDefault('$(ControlFlowGuard)', '') == 'Guard')" - CETCompat="$(CETCompat)" + CETCompat="$([MSBuild]::ValueOrDefault('$(CETCompat)', '') != 'false')" DefaultOutputType="$(OutputType)" DefaultWin32Manifest="$(_StubExeWin32ManifestPath)" DefaultAppContainer="$(UseUwpTools)" diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs index cfe106fe0c..5a13da14f5 100644 --- a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs +++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs @@ -205,16 +205,13 @@ public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task public bool ControlFlowGuard { get; set; } /// - /// Gets or sets the CET compatibility mode. + /// Gets or sets whether CET shadow stack compatibility is enabled. /// /// - /// Maps to the CETCompat MSBuild property. - /// - /// Empty or non-"false": CET shadow stack is enabled (for x64). - /// "false": CET shadow stack is disabled. - /// + /// When (the default), the /CETCOMPAT linker flag is set for x64 targets. + /// Maps to the CETCompat MSBuild property. /// - public string? CETCompat { get; set; } + public bool CETCompat { get; set; } = true; /// /// Gets or sets the default output type for stubs that don't specify one. @@ -561,7 +558,7 @@ private string BuildMsvcArguments( } // Configure CET shadow stack compatibility (https://learn.microsoft.com/cpp/build/reference/cetcompat) - bool cetEnabled = !string.Equals(CETCompat, "false", StringComparison.OrdinalIgnoreCase); + bool cetEnabled = CETCompat; bool isX64 = string.Equals(platform, "x64", StringComparison.OrdinalIgnoreCase); if (isX64) From 0986aeb9bebbf4c630694b97e4e375501f33c7fe Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 19:02:18 -0700 Subject: [PATCH 06/10] Extract validation into separate methods Split Execute into three phases: ValidateParameters (task-level checks), ValidateStubExeItem (per-item checks for platform, source resolution), and GenerateStubExe (compilation only, assumes valid input). This makes GenerateStubExe simpler and ensures all validation errors are reported before any compilation begins. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GenerateCsWinRTStubExes.cs | 123 ++++++++++++------ 1 file changed, 85 insertions(+), 38 deletions(-) diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs index 5a13da14f5..8dc5c43c7a 100644 --- a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs +++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs @@ -252,6 +252,32 @@ public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task /// public override bool Execute() + { + if (!ValidateParameters()) + { + return false; + } + + List generatedItems = new(); + + foreach (ITaskItem stubExe in StubExes!) + { + if (!GenerateStubExe(stubExe, generatedItems)) + { + return false; + } + } + + GeneratedStubExes = generatedItems.ToArray(); + + return true; + } + + /// + /// Validates all task parameters and input items upfront, before any compilation begins. + /// + /// if all parameters and items are valid; otherwise, . + private bool ValidateParameters() { if (StubExes is not { Length: > 0 }) { @@ -304,40 +330,30 @@ public override bool Execute() } } - List generatedItems = new(); - + // Validate each input item foreach (ITaskItem stubExe in StubExes) { - if (!GenerateStubExe(stubExe, generatedItems)) + if (!ValidateStubExeItem(stubExe)) { return false; } } - GeneratedStubExes = generatedItems.ToArray(); - return true; } /// - /// Generates a single stub .exe for the given item. + /// Validates a single describing a stub to generate. /// - /// The describing the stub to generate. - /// The list to add generated output items to. - /// if the stub was generated successfully; otherwise, . - private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) + /// The item to validate. + /// if the item is valid; otherwise, . + private bool ValidateStubExeItem(ITaskItem stubExe) { string stubName = stubExe.ItemSpec; - // Resolve per-stub metadata, falling back to defaults - string outputType = GetMetadataOrDefault(stubExe, "OutputType", DefaultOutputType ?? "Exe"); - string win32Manifest = GetMetadataOrDefault(stubExe, "Win32Manifest", DefaultWin32Manifest ?? ""); - bool appContainer = GetBooleanMetadataOrDefault(stubExe, "AppContainer", DefaultAppContainer); + // Validate the platform string platform = GetMetadataOrDefault(stubExe, "Platform", DefaultPlatform ?? ""); - string sourceText = stubExe.GetMetadata("SourceText"); - string sourceFile = stubExe.GetMetadata("SourceFile"); - // Validate the platform if (!UseEnvironmentalTools && !string.Equals(platform, "arm64", StringComparison.OrdinalIgnoreCase) && !string.Equals(platform, "x64", StringComparison.OrdinalIgnoreCase) && @@ -352,26 +368,13 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) return false; } - // Prepare paths for the intermediate working directory for this stub - string stubIntermediateDir = Path.Combine(IntermediateOutputDirectory!, stubName); - string sourceFileName = stubName + ".c"; - string sourceFilePath = Path.Combine(stubIntermediateDir, sourceFileName); - string binaryFileName = stubName + ".exe"; - string binaryOutputFilePath = Path.Combine(stubIntermediateDir, binaryFileName); - string binaryDestinationFilePath = Path.Combine(DestinationDirectory!, binaryFileName); - - Log.LogMessage(MessageImportance.Normal, "Generating stub .exe '{0}'", stubName); - - // Ensure the intermediate directory exists - Directory.CreateDirectory(stubIntermediateDir); + // Validate the source: one of SourceText, SourceFile, or DefaultSourceFilePath must be available + string sourceText = stubExe.GetMetadata("SourceText"); + string sourceFile = stubExe.GetMetadata("SourceFile"); - // Resolve the source for this stub: - // 1. SourceText metadata: write inline text to the .c file - // 2. SourceFile metadata: copy the specified file - // 3. Default DefaultSourceFilePath: copy the default template from the NuGet package if (!string.IsNullOrEmpty(sourceText)) { - File.WriteAllText(sourceFilePath, sourceText); + // Inline source text is always valid } else if (!string.IsNullOrEmpty(sourceFile)) { @@ -381,8 +384,6 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) return false; } - - File.Copy(sourceFile, sourceFilePath, overwrite: true); } else if (!string.IsNullOrEmpty(DefaultSourceFilePath)) { @@ -392,8 +393,6 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) return false; } - - File.Copy(DefaultSourceFilePath, sourceFilePath, overwrite: true); } else { @@ -405,6 +404,54 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) return false; } + return true; + } + + /// + /// Generates a single stub .exe for the given item. + /// + /// The describing the stub to generate (must be pre-validated). + /// The list to add generated output items to. + /// if the stub was generated successfully; otherwise, . + private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) + { + string stubName = stubExe.ItemSpec; + + // Resolve per-stub metadata, falling back to defaults + string outputType = GetMetadataOrDefault(stubExe, "OutputType", DefaultOutputType ?? "Exe"); + string win32Manifest = GetMetadataOrDefault(stubExe, "Win32Manifest", DefaultWin32Manifest ?? ""); + bool appContainer = GetBooleanMetadataOrDefault(stubExe, "AppContainer", DefaultAppContainer); + string platform = GetMetadataOrDefault(stubExe, "Platform", DefaultPlatform ?? ""); + string sourceText = stubExe.GetMetadata("SourceText"); + string sourceFile = stubExe.GetMetadata("SourceFile"); + + // Prepare paths for the intermediate working directory for this stub + string stubIntermediateDir = Path.Combine(IntermediateOutputDirectory!, stubName); + string sourceFileName = stubName + ".c"; + string sourceFilePath = Path.Combine(stubIntermediateDir, sourceFileName); + string binaryFileName = stubName + ".exe"; + string binaryOutputFilePath = Path.Combine(stubIntermediateDir, binaryFileName); + string binaryDestinationFilePath = Path.Combine(DestinationDirectory!, binaryFileName); + + Log.LogMessage(MessageImportance.Normal, "Generating stub .exe '{0}'", stubName); + + // Ensure the intermediate directory exists + Directory.CreateDirectory(stubIntermediateDir); + + // Write the source for this stub (priority: SourceText > SourceFile > DefaultSourceFilePath) + if (!string.IsNullOrEmpty(sourceText)) + { + File.WriteAllText(sourceFilePath, sourceText); + } + else if (!string.IsNullOrEmpty(sourceFile)) + { + File.Copy(sourceFile, sourceFilePath, overwrite: true); + } + else + { + File.Copy(DefaultSourceFilePath!, sourceFilePath, overwrite: true); + } + // Delete any previously-generated broken .exe in the destination (see https://github.com/dotnet/runtime/issues/111313) if (File.Exists(binaryDestinationFilePath)) { From b958312e18840fa7944093f17e84f2e6c9b1a23d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 19:05:11 -0700 Subject: [PATCH 07/10] Remove unused using directives Remove unused using directives from GenerateCsWinRTStubExes.cs and RunCsWinRTInteropGenerator.cs to clean up imports and avoid compiler/IDE warnings. No functional changes; purely a readability/cleanup tweak. --- src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs | 3 --- src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs index 8dc5c43c7a..3520066150 100644 --- a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs +++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs @@ -1,10 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; diff --git a/src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs b/src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs index df3cda15c3..0f5a6af30f 100644 --- a/src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs +++ b/src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs @@ -1,10 +1,7 @@ // 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; From e782d3adf10b7e6250022676b593e17e7a71b336 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 19:11:46 -0700 Subject: [PATCH 08/10] Remove unused using directives Remove unused using directives from GenerateCsWinRTStubExes.cs and RunCsWinRTInteropGenerator.cs to clean up imports and avoid compiler/IDE warnings. No functional changes; purely a readability/cleanup tweak. --- src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs index 3520066150..235e6ed737 100644 --- a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs +++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs @@ -255,7 +255,7 @@ public override bool Execute() return false; } - List generatedItems = new(); + List generatedItems = []; foreach (ITaskItem stubExe in StubExes!) { @@ -265,7 +265,7 @@ public override bool Execute() } } - GeneratedStubExes = generatedItems.ToArray(); + GeneratedStubExes = [.. generatedItems]; return true; } From 232d70ee5005bcf29441ad98d66b2be636b91679 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Mar 2026 20:02:14 -0700 Subject: [PATCH 09/10] Address PR review feedback - Extend MSVC/SDK path validation to cover all include and lib paths (WindowsSdkUmIncludePath, MsvcLibPath, WindowsSdkUcrtLibPath, WindowsSdkUmLibPath), not just the two that were checked before - Quote the manifest path in /MANIFESTINPUT to handle paths with spaces - Fix AdditionalPath to only pass the extra directory, not the full PATH (the task already appends to the existing PATH) - Validate stub names are simple file names (no path separators, no '..' traversal, no invalid filename chars) to prevent path traversal - Resolve Win32Manifest paths to full paths so they work correctly regardless of the cl.exe working directory - Wrap file operations in GenerateStubExe in try/catch for clear error messages, and ensure DestinationDirectory exists - Use async event-based reads (BeginOutputReadLine/BeginErrorReadLine) for stdout/stderr to avoid potential deadlocks from full pipe buffers - Fix AdditionalPath doc to say 'append' and clarify callers should not include the current PATH - Make Platform a task-level setting (DefaultPlatform) instead of per-item metadata, since the MSVC/SDK lib paths are resolved once for the whole task and per-item Platform would not actually switch them Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nuget/Microsoft.Windows.CsWinRT.targets | 4 +- .../GenerateCsWinRTStubExes.cs | 148 ++++++++++++------ 2 files changed, 101 insertions(+), 51 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index a93be25a34..1e780fa8fa 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -388,8 +388,6 @@ $(CsWinRTInternalProjection) generation is suppressed. Defaults to the project's $(Win32Manifest). AppContainer - 'true' to set /APPCONTAINER for UWP apps. Defaults to $(UseUwpTools). - Platform - Target platform (arm64, x64, x86). Defaults to the resolved - platform from $(Platform) / $(PlatformTarget). SourceText - Inline C source code for the stub. When set, this text is written to a .c file and compiled. Takes precedence over SourceFile and the default source template. @@ -474,7 +472,7 @@ $(CsWinRTInternalProjection) <_WindowsSdk-um-LibPath>$([System.IO.Path]::Combine($(_WindowsSdkLibPath), 'um', '$(_StubExePlatform)')) - <_StubExeAdditionalPath Condition="'$(WindowsSDKBuildToolsBinVersionedArchFolder)' != ''">$(PATH);$(WindowsSDKBuildToolsBinVersionedArchFolder) + <_StubExeAdditionalPath Condition="'$(WindowsSDKBuildToolsBinVersionedArchFolder)' != ''">$(WindowsSDKBuildToolsBinVersionedArchFolder) diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs index 235e6ed737..c5f947e8b0 100644 --- a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs +++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs @@ -53,13 +53,6 @@ namespace Microsoft.NET.Build.Tasks; /// /// /// -/// Platform -/// -/// The target platform (x64, x86, or arm64). -/// Defaults to . -/// -/// -/// /// SourceText /// /// Inline C source code to use for this stub. When set, the text is written @@ -86,7 +79,7 @@ public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task /// /// /// The item identity (Include) is the stub name (used as the output .exe filename). - /// Supported metadata: OutputType, Win32Manifest, AppContainer, Platform, + /// Supported metadata: OutputType, Win32Manifest, AppContainer, /// SourceText, SourceFile. /// /// @@ -179,11 +172,13 @@ public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task public string? WindowsSdkUmLibPath { get; set; } /// - /// Gets or sets additional directories to prepend to the PATH when invoking cl.exe. + /// Gets or sets an additional PATH fragment to append when invoking cl.exe. /// /// /// This is used when not using environmental tools and the Windows SDK build tools directory - /// (containing mt.exe and rc.exe) is not already on the PATH. + /// (containing mt.exe and rc.exe) is not already on the PATH. The value is + /// appended to the existing PATH and may contain one or more semicolon-separated directories. + /// Callers should not include the current PATH (for example, via $(PATH)) in this value. /// public string? AdditionalPath { get; set; } @@ -227,8 +222,12 @@ public sealed class GenerateCsWinRTStubExes : Microsoft.Build.Utilities.Task public bool DefaultAppContainer { get; set; } /// - /// Gets or sets the default platform for stubs that don't specify one. + /// Gets or sets the target platform for all stubs. /// + /// + /// The platform controls architecture-specific linker flags such as /CETCOMPAT. + /// Valid values are arm64, x64, and x86. + /// public string? DefaultPlatform { get; set; } /// @@ -317,14 +316,33 @@ private bool ValidateParameters() if (!UseEnvironmentalTools) { if (string.IsNullOrEmpty(MsvcIncludePath) || !Directory.Exists(MsvcIncludePath) || - string.IsNullOrEmpty(WindowsSdkUcrtIncludePath) || !Directory.Exists(WindowsSdkUcrtIncludePath)) + string.IsNullOrEmpty(WindowsSdkUcrtIncludePath) || !Directory.Exists(WindowsSdkUcrtIncludePath) || + string.IsNullOrEmpty(WindowsSdkUmIncludePath) || !Directory.Exists(WindowsSdkUmIncludePath) || + string.IsNullOrEmpty(MsvcLibPath) || !Directory.Exists(MsvcLibPath) || + string.IsNullOrEmpty(WindowsSdkUcrtLibPath) || !Directory.Exists(WindowsSdkUcrtLibPath) || + string.IsNullOrEmpty(WindowsSdkUmLibPath) || !Directory.Exists(WindowsSdkUmLibPath)) { Log.LogError( - "Failed to find the paths for the include folders to pass to MSVC, which are needed to compile stub executables. " + + "Failed to find the paths for the include and library folders to pass to MSVC, which are needed to compile stub executables. " + "Try setting 'CsWinRTUseEnvironmentalTools' and building from a Visual Studio Developer Command Prompt (or PowerShell) session."); return false; } + + // Validate the platform when not using environmental tools + string platform = DefaultPlatform ?? ""; + + if (!string.Equals(platform, "arm64", StringComparison.OrdinalIgnoreCase) && + !string.Equals(platform, "x64", StringComparison.OrdinalIgnoreCase) && + !string.Equals(platform, "x86", StringComparison.OrdinalIgnoreCase)) + { + Log.LogError( + "Invalid platform '{0}'. Make sure to set 'Platform' to either 'arm64', 'x64', or 'x86'. " + + "Alternatively, try setting 'CsWinRTUseEnvironmentalTools' and building from a Visual Studio Developer Command Prompt (or PowerShell) session.", + platform); + + return false; + } } // Validate each input item @@ -348,18 +366,13 @@ private bool ValidateStubExeItem(ITaskItem stubExe) { string stubName = stubExe.ItemSpec; - // Validate the platform - string platform = GetMetadataOrDefault(stubExe, "Platform", DefaultPlatform ?? ""); - - if (!UseEnvironmentalTools && - !string.Equals(platform, "arm64", StringComparison.OrdinalIgnoreCase) && - !string.Equals(platform, "x64", StringComparison.OrdinalIgnoreCase) && - !string.Equals(platform, "x86", StringComparison.OrdinalIgnoreCase)) + // Validate that the stub name is a simple file name (no path separators, no '..' traversal, no invalid chars) + if (string.IsNullOrEmpty(stubName) || + stubName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || + stubName.Contains("..")) { Log.LogError( - "Invalid platform '{0}' for stub '{1}'. Make sure to set 'Platform' to either 'arm64', 'x64', or 'x86'. " + - "Alternatively, try setting 'CsWinRTUseEnvironmentalTools' and building from a Visual Studio Developer Command Prompt (or PowerShell) session.", - platform, + "Invalid stub name '{0}'. The stub name must be a simple file name without path separators or invalid characters.", stubName); return false; @@ -418,10 +431,16 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) string outputType = GetMetadataOrDefault(stubExe, "OutputType", DefaultOutputType ?? "Exe"); string win32Manifest = GetMetadataOrDefault(stubExe, "Win32Manifest", DefaultWin32Manifest ?? ""); bool appContainer = GetBooleanMetadataOrDefault(stubExe, "AppContainer", DefaultAppContainer); - string platform = GetMetadataOrDefault(stubExe, "Platform", DefaultPlatform ?? ""); + string platform = DefaultPlatform ?? ""; string sourceText = stubExe.GetMetadata("SourceText"); string sourceFile = stubExe.GetMetadata("SourceFile"); + // Resolve manifest path to full path so it works regardless of working directory + if (!string.IsNullOrEmpty(win32Manifest)) + { + win32Manifest = Path.GetFullPath(win32Manifest); + } + // Prepare paths for the intermediate working directory for this stub string stubIntermediateDir = Path.Combine(IntermediateOutputDirectory!, stubName); string sourceFileName = stubName + ".c"; @@ -432,27 +451,37 @@ private bool GenerateStubExe(ITaskItem stubExe, List generatedItems) Log.LogMessage(MessageImportance.Normal, "Generating stub .exe '{0}'", stubName); - // Ensure the intermediate directory exists - Directory.CreateDirectory(stubIntermediateDir); - - // Write the source for this stub (priority: SourceText > SourceFile > DefaultSourceFilePath) - if (!string.IsNullOrEmpty(sourceText)) - { - File.WriteAllText(sourceFilePath, sourceText); - } - else if (!string.IsNullOrEmpty(sourceFile)) + try { - File.Copy(sourceFile, sourceFilePath, overwrite: true); + // Ensure the intermediate and destination directories exist + Directory.CreateDirectory(stubIntermediateDir); + Directory.CreateDirectory(DestinationDirectory!); + + // Write the source for this stub (priority: SourceText > SourceFile > DefaultSourceFilePath) + if (!string.IsNullOrEmpty(sourceText)) + { + File.WriteAllText(sourceFilePath, sourceText); + } + else if (!string.IsNullOrEmpty(sourceFile)) + { + File.Copy(sourceFile, sourceFilePath, overwrite: true); + } + else + { + File.Copy(DefaultSourceFilePath!, sourceFilePath, overwrite: true); + } + + // Delete any previously-generated broken .exe in the destination (see https://github.com/dotnet/runtime/issues/111313) + if (File.Exists(binaryDestinationFilePath)) + { + File.Delete(binaryDestinationFilePath); + } } - else + catch (Exception ex) { - File.Copy(DefaultSourceFilePath!, sourceFilePath, overwrite: true); - } + Log.LogError("Failed to prepare files for stub .exe '{0}': {1}", stubName, ex.Message); - // Delete any previously-generated broken .exe in the destination (see https://github.com/dotnet/runtime/issues/111313) - if (File.Exists(binaryDestinationFilePath)) - { - File.Delete(binaryDestinationFilePath); + return false; } // Build the MSVC command-line arguments @@ -554,7 +583,7 @@ private string BuildMsvcArguments( // See: https://learn.microsoft.com/cpp/build/reference/manifest-create-side-by-side-assembly-manifest if (!string.IsNullOrEmpty(win32Manifest)) { - args.AppendFormat("/MANIFEST:EMBED /MANIFESTINPUT:{0} ", win32Manifest); + args.Append("/MANIFEST:EMBED /MANIFESTINPUT:\"").Append(win32Manifest).Append("\" "); } else { @@ -661,20 +690,43 @@ private bool InvokeCompiler(string arguments, string workingDirectory, string st return false; } - string stdout = process.StandardOutput.ReadToEnd(); - string stderr = process.StandardError.ReadToEnd(); + // Read stdout and stderr concurrently to avoid deadlocks from full pipe buffers. + // See: https://learn.microsoft.com/dotnet/api/system.diagnostics.process.standardoutput#remarks + StringBuilder stdoutBuilder = new(); + StringBuilder stderrBuilder = new(); + + process.OutputDataReceived += (_, e) => + { + if (e.Data is not null) + { + stdoutBuilder.AppendLine(e.Data); + } + }; + process.ErrorDataReceived += (_, e) => + { + if (e.Data is not null) + { + stderrBuilder.AppendLine(e.Data); + } + }; + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); process.WaitForExit(); + string stdout = stdoutBuilder.ToString().TrimEnd(); + string stderr = stderrBuilder.ToString().TrimEnd(); + // Log compiler output - if (!string.IsNullOrWhiteSpace(stdout)) + if (stdout.Length > 0) { - Log.LogMessage(MessageImportance.Normal, stdout.TrimEnd()); + Log.LogMessage(MessageImportance.Normal, stdout); } - if (!string.IsNullOrWhiteSpace(stderr)) + if (stderr.Length > 0) { - Log.LogWarning("{0}", stderr.TrimEnd()); + Log.LogWarning("{0}", stderr); } if (process.ExitCode != 0) From 1133c3335897f2ec7c576ad172ef46918d99a3ad Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 26 Mar 2026 10:20:56 -0700 Subject: [PATCH 10/10] Document process stdio handling Add inline comments in GenerateCsWinRTStubExes.cs to clarify handling of stdout/stderr and the sequence of starting asynchronous reads (BeginOutputReadLine/BeginErrorReadLine) before waiting for process exit. Improves code readability by documenting intent around receiving stdio lines and blocking until completion. --- src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs index c5f947e8b0..ff72c915c4 100644 --- a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs +++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs @@ -695,6 +695,7 @@ private bool InvokeCompiler(string arguments, string workingDirectory, string st StringBuilder stdoutBuilder = new(); StringBuilder stderrBuilder = new(); + // Handle receiving stdio lines process.OutputDataReceived += (_, e) => { if (e.Data is not null) @@ -703,6 +704,7 @@ private bool InvokeCompiler(string arguments, string workingDirectory, string st } }; + // Handle receiving stderr lines process.ErrorDataReceived += (_, e) => { if (e.Data is not null) @@ -711,6 +713,7 @@ private bool InvokeCompiler(string arguments, string workingDirectory, string st } }; + // Start reading asynchronously and then block until the process completes process.BeginOutputReadLine(); process.BeginErrorReadLine(); process.WaitForExit();