diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets
index 8ae76bd4f6..1e780fa8fa 100644
--- a/nuget/Microsoft.Windows.CsWinRT.targets
+++ b/nuget/Microsoft.Windows.CsWinRT.targets
@@ -362,11 +362,48 @@ $(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 +470,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)' != ''">$(WindowsSDKBuildToolsBinVersionedArchFolder)
-
-
-
-
-
- <_StubExeAdditionalPath>$(PATH);$(WindowsSDKBuildToolsBinVersionedArchFolder)
- <_StubExeEnvVars>PATH=$(_StubExeAdditionalPath.Replace(';','%3B'))
-
-
-
-
-
-
-
-
+
+
-
+
+
+
+
+
+
-
- $(_StubExeBinaryFileName)
- PreserveNewest
-
+
diff --git a/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs
new file mode 100644
index 0000000000..ff72c915c4
--- /dev/null
+++ b/src/WinRT.Generator.Tasks/GenerateCsWinRTStubExes.cs
@@ -0,0 +1,815 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+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 .
+///
+///
+/// -
+/// 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
+{
+ ///
+ /// 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,
+ /// SourceText, SourceFile.
+ ///
+ ///
+ [Required]
+ public ITaskItem[]? StubExes { get; set; }
+
+ ///
+ /// Gets or sets the path to the default .c source file used for building stub executables.
+ ///
+ ///
+ /// 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? DefaultSourceFilePath { 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 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. 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; }
+
+ ///
+ /// 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 whether CET shadow stack compatibility is enabled.
+ ///
+ ///
+ /// When (the default), the /CETCOMPAT linker flag is set for x64 targets.
+ /// Maps to the CETCompat MSBuild property.
+ ///
+ public bool CETCompat { get; set; } = true;
+
+ ///
+ /// 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 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; }
+
+ ///
+ /// 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 (!ValidateParameters())
+ {
+ return false;
+ }
+
+ List generatedItems = [];
+
+ foreach (ITaskItem stubExe in StubExes!)
+ {
+ if (!GenerateStubExe(stubExe, generatedItems))
+ {
+ return false;
+ }
+ }
+
+ GeneratedStubExes = [.. generatedItems];
+
+ 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 })
+ {
+ Log.LogError("No stub executables were specified.");
+
+ 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) ||
+ 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 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
+ foreach (ITaskItem stubExe in StubExes)
+ {
+ if (!ValidateStubExeItem(stubExe))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Validates a single describing a stub to generate.
+ ///
+ /// The item to validate.
+ /// if the item is valid; otherwise, .
+ private bool ValidateStubExeItem(ITaskItem stubExe)
+ {
+ string stubName = stubExe.ItemSpec;
+
+ // 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 stub name '{0}'. The stub name must be a simple file name without path separators or invalid characters.",
+ stubName);
+
+ return false;
+ }
+
+ // Validate the source: one of SourceText, SourceFile, or DefaultSourceFilePath must be available
+ string sourceText = stubExe.GetMetadata("SourceText");
+ string sourceFile = stubExe.GetMetadata("SourceFile");
+
+ if (!string.IsNullOrEmpty(sourceText))
+ {
+ // Inline source text is always valid
+ }
+ 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;
+ }
+ }
+ else if (!string.IsNullOrEmpty(DefaultSourceFilePath))
+ {
+ if (!File.Exists(DefaultSourceFilePath))
+ {
+ Log.LogError("The default stub .exe source file '{0}' does not exist.", DefaultSourceFilePath);
+
+ return false;
+ }
+ }
+ 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 'DefaultSourceFilePath' task parameter.",
+ stubName);
+
+ 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 = 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";
+ 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);
+
+ try
+ {
+ // 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);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.LogError("Failed to prepare files for stub .exe '{0}': {1}", stubName, ex.Message);
+
+ return false;
+ }
+
+ // 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.Append("/MANIFEST:EMBED /MANIFESTINPUT:\"").Append(win32Manifest).Append("\" ");
+ }
+ 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 = CETCompat;
+ 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;
+ }
+
+ // 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();
+
+ // Handle receiving stdio lines
+ process.OutputDataReceived += (_, e) =>
+ {
+ if (e.Data is not null)
+ {
+ stdoutBuilder.AppendLine(e.Data);
+ }
+ };
+
+ // Handle receiving stderr lines
+ process.ErrorDataReceived += (_, e) =>
+ {
+ if (e.Data is not null)
+ {
+ stderrBuilder.AppendLine(e.Data);
+ }
+ };
+
+ // Start reading asynchronously and then block until the process completes
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ process.WaitForExit();
+
+ string stdout = stdoutBuilder.ToString().TrimEnd();
+ string stderr = stderrBuilder.ToString().TrimEnd();
+
+ // Log compiler output
+ if (stdout.Length > 0)
+ {
+ Log.LogMessage(MessageImportance.Normal, stdout);
+ }
+
+ if (stderr.Length > 0)
+ {
+ Log.LogWarning("{0}", stderr);
+ }
+
+ 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("\" ");
+ }
+}
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;