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;