diff --git a/src/WinRT.Impl.Generator/Attributes/CommandLineArgumentNameAttribute.cs b/src/WinRT.Generator.Core/Attributes/CommandLineArgumentNameAttribute.cs similarity index 91% rename from src/WinRT.Impl.Generator/Attributes/CommandLineArgumentNameAttribute.cs rename to src/WinRT.Generator.Core/Attributes/CommandLineArgumentNameAttribute.cs index 95d0ace705..ca36c6346f 100644 --- a/src/WinRT.Impl.Generator/Attributes/CommandLineArgumentNameAttribute.cs +++ b/src/WinRT.Generator.Core/Attributes/CommandLineArgumentNameAttribute.cs @@ -3,7 +3,7 @@ using System; -namespace WindowsRuntime.ImplGenerator.Attributes; +namespace WindowsRuntime.Generator.Attributes; /// /// An attribute indicating the name of a given command line argument. @@ -16,4 +16,4 @@ internal sealed class CommandLineArgumentNameAttribute(string name) : Attribute /// Gets the command line argument name. /// public string Name { get; } = name; -} \ No newline at end of file +} diff --git a/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs b/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs new file mode 100644 index 0000000000..ba0da678e9 --- /dev/null +++ b/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading; +using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.Helpers; + +namespace WindowsRuntime.Generator.DebugRepro; + +/// +/// Leaf helpers shared across the CsWinRT CLI generators for packaging and unpacking debug repros. +/// +/// +/// Each generator still owns the high-level orchestration (which input categories exist, which subfolders are +/// used, how the response file is re-stitched). This type only owns identical helpers that can be reused. +/// +internal static class DebugReproPacker +{ + /// + /// Generates a hashed filename by appending a Shake128 hash of the original file path. + /// + /// The original file path. + /// The hashed filename in the form {name}_{HEX}{ext}. + public static string GetHashedFileName(string filePath) + { + string fileName = Path.GetFileName(Path.Normalize(filePath)); + byte[] utf8Data = Encoding.UTF8.GetBytes(filePath); + byte[] hashData = Shake128.HashData(utf8Data, outputLength: 16); + string hash = Convert.ToHexString(hashData); + + return $"{Path.GetFileNameWithoutExtension(fileName)}_{hash}{Path.GetExtension(fileName)}"; + } + + /// + /// Copies all specified files to a target folder using hashed file names, and returns the list of updated names. + /// + /// The input file paths. + /// The target directory to copy the files to. + /// A dictionary to store the original paths of the copied files (keyed by hashed file name). + /// A cancellation token to monitor for cancellation requests. + /// The list of updated hashed filenames, in the same order as . + public static List CopyHashedFilesToDirectory( + string[] filePaths, + string destinationDirectory, + Dictionary originalPaths, + CancellationToken token) + { + List updatedFileNames = []; + + foreach (string filePath in filePaths) + { + token.ThrowIfCancellationRequested(); + + string hashedName = GetHashedFileName(filePath); + string destinationPath = Path.Combine(destinationDirectory, hashedName); + + File.Copy(filePath, destinationPath, overwrite: true); + + updatedFileNames.Add(hashedName); + originalPaths.Add(hashedName, filePath); + } + + return updatedFileNames; + } + + /// + /// Copies a single specified file to a target folder using a hashed file name. + /// + /// + /// This is the simple variant used by the impl, projection, projection-ref, and WinMD generators. + /// The interop generator keeps its own variant locally (which adds reserved-DLL dedupe and throws + /// on path mismatches against the shared reference set). + /// + /// The input file path, or to skip. + /// The target directory to copy the file to. + /// A dictionary to store the original path of the copied file (keyed by hashed file name). + /// A cancellation token to monitor for cancellation requests. + /// The hashed filename, or if was . + [return: NotNullIfNotNull(nameof(filePath))] + public static string? CopyHashedFileToDirectory( + string? filePath, + string destinationDirectory, + Dictionary originalPaths, + CancellationToken token) + { + if (filePath is null) + { + return null; + } + + string hashedName = GetHashedFileName(filePath); + string destinationPath = Path.Combine(destinationDirectory, hashedName); + + File.Copy(filePath, destinationPath, overwrite: true); + + token.ThrowIfCancellationRequested(); + + originalPaths.Add(hashedName, filePath); + + return hashedName; + } + + /// + /// Serializes an input path map to a target directory as a JSON file. + /// + /// The input path map (hashed file name → original file path). + /// The target directory. + /// The name to use for the file with the serialized path map. + public static void CopyPathMapToDirectory( + Dictionary pathMap, + string destinationDirectory, + string fileName) + { + // Create the .json file with the input path map + string jsonFilePath = Path.Combine(destinationDirectory, fileName); + + using Stream jsonStream = File.Create(jsonFilePath); + + // Serialize the path map to the target file + JsonSerializer.Serialize(jsonStream, pathMap, GeneratorJsonSerializerContext.Default.DictionaryStringString); + } + + /// + /// Extracts an input path map from a .zip archive entry. + /// + /// The input path map entry. + /// The deserialized path map (hashed file name → original file path). + /// + /// The value is expected to have the content produced by calls to . + /// + public static Dictionary ExtractPathMap(ZipArchiveEntry pathMapEntry) + { + using Stream stream = pathMapEntry.Open(); + + // Load the mapping with all the original file paths for the included files + return JsonSerializer.Deserialize(stream, GeneratorJsonSerializerContext.Default.DictionaryStringString)!; + } + + /// + /// Prepares the staging directory and target archive path for a debug repro save operation. + /// + /// The per-tool error factory used to throw if does not exist. + /// The user-provided directory where the resulting .zip archive will be written. Must already exist. + /// The CLI tool name (e.g. "cswinrtimplgen"), used as the prefix of the staging directory. + /// The file name of the resulting .zip archive (e.g. "impl-debug-repro.zip"). + /// A pair containing the freshly-created staging directory and the absolute path of the target archive. + /// Thrown via if does not exist. + public static (string TempDirectory, string ZipPath) BeginSave( + string debugReproDirectory, + string toolName, + string archiveFileName) + where TError : IGeneratorErrorFactory + { + // The target folder must exist + if (!Directory.Exists(debugReproDirectory)) + { + throw TError.DebugReproDirectoryDoesNotExist(debugReproDirectory); + } + + // Path for the ZIP archive + string zipPath = Path.Combine(debugReproDirectory, archiveFileName); + + // Create a temporary directory to stage files for the ZIP + string tempFolderName = $"{toolName}-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; + string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); + + _ = Directory.CreateDirectory(tempDirectory); + + return (tempDirectory, zipPath); + } + + /// + /// Finalizes a debug repro save by zipping the staging directory into the target archive and deleting the staging directory. + /// + /// The staging directory previously returned by . + /// The absolute path of the target .zip archive, previously returned by . + /// + /// If a file already exists at , it is deleted before the new archive is created. + /// + public static void FinalizeSave(string tempDirectory, string zipPath) + { + // Delete the previous file, if it exists + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + // Create the actual .zip file in the target directory + ZipFile.CreateFromDirectory(tempDirectory, zipPath); + + // Clean up the temporary directory + Directory.Delete(tempDirectory, recursive: true); + } + + /// + /// Creates a freshly-named temporary directory for unpacking a debug repro .zip archive. + /// + /// The CLI tool name (e.g. "cswinrtimplgen"), used as the prefix of the directory. + /// The absolute path of the created directory. + public static string CreateUnpackTempDirectory(string toolName) + { + // Create a temporary directory to extract the files from the debug repro + string tempFolderName = $"{toolName}-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; + string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); + + _ = Directory.CreateDirectory(tempDirectory); + + return tempDirectory; + } +} diff --git a/src/WinRT.Impl.Generator/Extensions/ImplExceptionExtensions.cs b/src/WinRT.Generator.Core/Errors/GeneratorExceptionExtensions.cs similarity index 65% rename from src/WinRT.Impl.Generator/Extensions/ImplExceptionExtensions.cs rename to src/WinRT.Generator.Core/Errors/GeneratorExceptionExtensions.cs index 9bf6d91b12..ab7cc82890 100644 --- a/src/WinRT.Impl.Generator/Extensions/ImplExceptionExtensions.cs +++ b/src/WinRT.Generator.Core/Errors/GeneratorExceptionExtensions.cs @@ -2,20 +2,19 @@ // Licensed under the MIT License. using System; -using WindowsRuntime.ImplGenerator.Errors; -namespace WindowsRuntime.ImplGenerator; +namespace WindowsRuntime.Generator.Errors; /// -/// Extensions for interop exceptions. +/// Shared extensions for CsWinRT CLI generator exceptions. /// -internal static class ImplExceptionExtensions +internal static class GeneratorExceptionExtensions { extension(Exception exception) { /// /// Gets a value indicating whether an exception is well known (and should therefore not be caught). /// - public bool IsWellKnown => exception is OperationCanceledException or WellKnownImplException; + public bool IsWellKnown => exception is OperationCanceledException or WellKnownGeneratorException; } -} \ No newline at end of file +} diff --git a/src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs b/src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs new file mode 100644 index 0000000000..8906ab8c63 --- /dev/null +++ b/src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.Generator.Errors; + +/// +/// Routes shared logical errors through the per-tool well-known exception factory. +/// +/// +/// Shared infrastructure (response-file parsing, debug-repro packing, etc.) is generic over an +/// implementation of this interface to preserve per-tool exception identity exactly: each factory +/// continues to assign its own numeric error IDs, format its own messages (including embedded tool names), +/// and construct its own concrete subtype. +/// +internal interface IGeneratorErrorFactory +{ + /// Some exception was thrown when trying to read the response file. + static abstract Exception ResponseFileReadError(Exception exception); + + /// Failed to parse an argument from the response file. + static abstract Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception); + + /// The input response file is malformed. + static abstract Exception MalformedResponseFile(); + + /// The debug repro directory does not exist. + static abstract Exception DebugReproDirectoryDoesNotExist(string path); + + /// The debug repro contains a file entry that has no mapping. + static abstract Exception DebugReproMissingFileEntryMapping(string path); + + /// The debug repro contains a file entry that was not recognized. + static abstract Exception DebugReproUnrecognizedFileEntry(string path); +} diff --git a/src/WinRT.Generator.Core/Errors/UnhandledGeneratorException.cs b/src/WinRT.Generator.Core/Errors/UnhandledGeneratorException.cs new file mode 100644 index 0000000000..3b51d8ad6b --- /dev/null +++ b/src/WinRT.Generator.Core/Errors/UnhandledGeneratorException.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.Generator.Errors; + +/// +/// An unhandled exception for a CsWinRT CLI generator. +/// +/// +/// Each per-tool unhandled exception inherits from this type and provides its +/// and so that the standardized +/// message remains tool-specific. +/// +/// The phase that failed. +/// The inner exception. +internal abstract class UnhandledGeneratorException(string phase, Exception exception) + : Exception(null, exception) +{ + /// + /// Gets the error prefix for the per-tool exception ID (e.g. "CSWINRTIMPLGEN"). + /// + protected abstract string ErrorPrefix { get; } + + /// + /// Gets the name of the generator used in the standard message. + /// + protected abstract string GeneratorName { get; } + + /// + public override string ToString() + { + return + $"""error {ErrorPrefix}9999: The CsWinRT {GeneratorName} generator failed with an unhandled exception """ + + $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the '{phase}' phase. This might be due to an invalid """ + + $"""configuration in the current project, but the generator should still correctly identify that and fail gracefully. Please open an """ + + $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; + } +} + diff --git a/src/WinRT.Generator.Core/Errors/WellKnownGeneratorException.cs b/src/WinRT.Generator.Core/Errors/WellKnownGeneratorException.cs new file mode 100644 index 0000000000..d65c0e9016 --- /dev/null +++ b/src/WinRT.Generator.Core/Errors/WellKnownGeneratorException.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.Generator.Errors; + +/// +/// A well-known exception for a CsWinRT CLI generator. +/// +/// +/// Each per-tool well-known exception inherits from this type and adds a marker +/// (e.g. WellKnownImplException) so the concrete runtime type remains tool-specific. +/// The base class provides the shared field and logic, +/// which matches the simple per-tool format error {Id}: {Message}[ Inner exception: ...]. +/// +/// The id of the exception (e.g. "CSWINRTIMPLGEN0001"). +/// The exception message. +/// The inner exception, if any. +internal abstract class WellKnownGeneratorException(string id, string message, Exception? innerException) + : Exception(message, innerException) +{ + /// + /// Gets the id of the exception (e.g. "CSWINRTIMPLGEN0001"). + /// + public string Id { get; } = id; + + /// + public override string ToString() + { + return InnerException is not null + ? $"""error {Id}: {Message} Inner exception: '{InnerException.GetType().Name}': '{InnerException.Message}'.""" + : $"""error {Id}: {Message}"""; + } +} diff --git a/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs b/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs new file mode 100644 index 0000000000..03e5bee363 --- /dev/null +++ b/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.Generator.Errors; + +/// +/// Shared message templates for the well-known logical errors defined by . +/// +/// +/// Each per-tool WellKnown*Exceptions factory uses these helpers to format its own +/// instance of every method, ensuring that the message +/// text stays identical across all generators while the per-tool error ID prefix (e.g. +/// CSWINRTIMPLGEN) and concrete exception type are still chosen per-tool. +/// +internal static class WellKnownGeneratorMessages +{ + /// + public const string ResponseFileReadError = "Failed to read the response file (e.g. it may be missing or not accessible)."; + + /// + /// The name of the response-file argument that failed to parse. + public static string ResponseFileArgumentParsingError(string argumentName) + { + return $"Failed to parse argument '{argumentName}' from response file."; + } + + /// + public const string MalformedResponseFile = "The response file is malformed and contains invalid content."; + + /// + /// The directory path that does not exist. + public static string DebugReproDirectoryDoesNotExist(string path) + { + return $"The debug repro directory '{path}' does not exist."; + } + + /// + /// The debug-repro file entry path that has no mapping. + public static string DebugReproMissingFileEntryMapping(string path) + { + return $"The debug repro file entry with path '{path}' is missing its assembly path mapping."; + } + + /// + /// The debug-repro file entry path that was not recognized. + public static string DebugReproUnrecognizedFileEntry(string path) + { + return $"The debug repro file entry with path '{path}' was not recognized."; + } +} diff --git a/src/WinRT.Interop.Generator/Extensions/FileExtensions.cs b/src/WinRT.Generator.Core/Extensions/FileExtensions.cs similarity index 95% rename from src/WinRT.Interop.Generator/Extensions/FileExtensions.cs rename to src/WinRT.Generator.Core/Extensions/FileExtensions.cs index 46869482ef..02ac39d6fc 100644 --- a/src/WinRT.Interop.Generator/Extensions/FileExtensions.cs +++ b/src/WinRT.Generator.Core/Extensions/FileExtensions.cs @@ -5,7 +5,7 @@ using System.IO; using System.Text; -namespace WindowsRuntime.InteropGenerator; +namespace WindowsRuntime.Generator; /// /// Extensions for . @@ -35,4 +35,4 @@ public static string[] ReadAllLines(Stream stream) return [.. lines]; } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Extensions/IncrementalHashExtensions.cs b/src/WinRT.Generator.Core/Extensions/IncrementalHashExtensions.cs similarity index 95% rename from src/WinRT.Interop.Generator/Extensions/IncrementalHashExtensions.cs rename to src/WinRT.Generator.Core/Extensions/IncrementalHashExtensions.cs index ed75d20673..1db570f31a 100644 --- a/src/WinRT.Interop.Generator/Extensions/IncrementalHashExtensions.cs +++ b/src/WinRT.Generator.Core/Extensions/IncrementalHashExtensions.cs @@ -5,7 +5,7 @@ using System.IO; using System.Security.Cryptography; -namespace WindowsRuntime.InteropGenerator; +namespace WindowsRuntime.Generator.Extensions; /// /// Extensions for the type. @@ -34,4 +34,4 @@ public void AppendData(Stream stream) ArrayPool.Shared.Return(buffer); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Extensions/PathExtensions.cs b/src/WinRT.Generator.Core/Extensions/PathExtensions.cs similarity index 73% rename from src/WinRT.Interop.Generator/Extensions/PathExtensions.cs rename to src/WinRT.Generator.Core/Extensions/PathExtensions.cs index fac234101c..fcb37d6130 100644 --- a/src/WinRT.Interop.Generator/Extensions/PathExtensions.cs +++ b/src/WinRT.Generator.Core/Extensions/PathExtensions.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; -namespace WindowsRuntime.InteropGenerator; +namespace WindowsRuntime.Generator; /// /// Extensions for the type. @@ -23,8 +23,9 @@ internal static class PathExtensions public static string? Normalize(string? path) { // If on Windows, no normalization is needed. Paths in debug repros will use this format. - // Note: 'cswinrtinteropgen' is only meant to be used on Windows (because CsWinRT itself is - // only supported on Windows), but this allows debugging repros on other platforms too. + // Note: the CsWinRT generators are only meant to be used on Windows (because CsWinRT + // itself is only supported on Windows), but this allows debugging repros on other + // platforms too. if (OperatingSystem.IsWindows()) { return path; @@ -35,21 +36,6 @@ internal static class PathExtensions return path?.Replace('\\', '/'); } - /// - public static ReadOnlySpan Normalize(ReadOnlySpan path) - { - if (OperatingSystem.IsWindows()) - { - return path; - } - - char[] buffer = new char[path.Length]; - - path.Replace(buffer, '\\', '/'); - - return buffer; - } - /// /// Checks whether a given path represents a file or folder contained within a folder with a given name. /// @@ -61,4 +47,4 @@ public static bool IsWithinDirectoryName(ReadOnlySpan path, ReadOnlySpan /// Extensions for the type. diff --git a/src/WinRT.Generator.Core/GeneratorHost.cs b/src/WinRT.Generator.Core/GeneratorHost.cs new file mode 100644 index 0000000000..27df49e2d1 --- /dev/null +++ b/src/WinRT.Generator.Core/GeneratorHost.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading; +using WindowsRuntime.Generator.Errors; + +namespace WindowsRuntime.Generator; + +/// +/// Shared Run entry-point scaffold for the CsWinRT CLI generators. +/// +internal static class GeneratorHost +{ + /// + /// Runs the shared unpack, parse, save preamble for a CsWinRT CLI generator and returns a + /// ready to drive the remaining per-tool phases. + /// + /// The per-tool args record (must implement ). + /// The input file path (response file or debug-repro .zip). + /// The tool name used in messages (e.g. "cswinrtimplgen"). + /// Extracts a debug-repro .zip to a temp folder and returns the rewritten response file path. + /// Parses a response file at the given path into . + /// Saves a debug repro of the current invocation (called only when is set and we are not already replaying). + /// Wraps an unexpected exception into the per-tool Unhandled*Exception with the given phase name. + /// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). + /// The token for the operation. + /// + /// A pre-bound to the parsed args, + /// and for use by subsequent phases. + /// + public static GeneratorPhaseRunner CreateRunner( + string inputFilePath, + string toolName, + Func unpackDebugRepro, + Func parseFromResponseFile, + Action saveDebugRepro, + Func wrapUnhandled, + Action log, + CancellationToken token) + where TArgs : IGeneratorArgs + { + string responseFilePath = inputFilePath; + bool isUsingDebugRepro = false; + + // Load the debug repro to investigate with, if we have one + try + { + // If the input file path is a .zip, treat it as a debug repro and unpack + // it. Any other extension is treated as a regular response file. + if (Path.GetExtension(Path.Normalize(inputFilePath)) == ".zip") + { + log($"Unpacking input '{toolName}' debug repro"); + + isUsingDebugRepro = true; + + // If we unpacked a debug repro, we'll also replace the input file + // path with the extracted response file from the input repro. + responseFilePath = unpackDebugRepro(inputFilePath, token); + } + } + catch (Exception e) when (!e.IsWellKnown) + { + throw wrapUnhandled("unpack-debug-repro", e); + } + + token.ThrowIfCancellationRequested(); + + TArgs args; + + // Parse the actual arguments from the response file + try + { + args = parseFromResponseFile(responseFilePath, token); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw wrapUnhandled("parsing", e); + } + + args.Token.ThrowIfCancellationRequested(); + + // Save a debug repro, if needed + try + { + // If no debug repro directory was provided, we have nothing to do. + // This is fully expected, it just means no debug repro is needed. + // We also skip this if we're currently processing an input debug + // repro, as there would be no point in creating a new one from that. + if (args.DebugReproDirectory is not null && !isUsingDebugRepro) + { + log($"Saving '{toolName}' debug repro"); + + saveDebugRepro(args); + } + } + catch (Exception e) when (!e.IsWellKnown) + { + throw wrapUnhandled("save-debug-repro", e); + } + + args.Token.ThrowIfCancellationRequested(); + + return new(args, wrapUnhandled, log); + } +} diff --git a/src/WinRT.Generator.Core/GeneratorPhaseRunner{TArgs}.cs b/src/WinRT.Generator.Core/GeneratorPhaseRunner{TArgs}.cs new file mode 100644 index 0000000000..58b3cf1299 --- /dev/null +++ b/src/WinRT.Generator.Core/GeneratorPhaseRunner{TArgs}.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.Generator.Errors; + +#pragma warning disable CS1573 + +namespace WindowsRuntime.Generator; + +/// +/// Runs the body of a single generator phase, wrapping unexpected exceptions in the per-tool +/// Unhandled*Exception and optionally logging a progress message before the body runs. +/// +/// The per-tool args record (must implement ). +/// The parsed per-tool args, forwarded to every body. +/// Wraps an unexpected exception into the per-tool Unhandled*Exception with the given phase name. +/// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). +internal readonly struct GeneratorPhaseRunner( + TArgs args, + Func wrapUnhandled, + Action log) + where TArgs : IGeneratorArgs +{ + /// + /// Gets the parsed per-tool args, forwarded to every body. + /// + public TArgs Args => args; + + /// + /// Runs , wrapping any unexpected exception in the per-tool + /// Unhandled*Exception with as the phase tag. + /// + /// The phase name used by the per-tool Unhandled*Exception. + /// The body of the phase to run. The captured is forwarded as its argument. + public void RunPhase(string phaseName, Action body) + { + try + { + body(Args); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw wrapUnhandled(phaseName, e); + } + + args.Token.ThrowIfCancellationRequested(); + } + + /// + /// The progress message to log before the body runs. + public void RunPhase(string phaseName, string logMessage, Action body) + { + try + { + log(logMessage); + + body(Args); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw wrapUnhandled(phaseName, e); + } + + args.Token.ThrowIfCancellationRequested(); + } + + /// + /// The value returned by . + public T RunPhase(string phaseName, Func body) + { + T result; + + try + { + result = body(Args); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw wrapUnhandled(phaseName, e); + } + + args.Token.ThrowIfCancellationRequested(); + + return result; + } + + /// + /// The progress message to log before the body runs. + /// The value returned by . + public T RunPhase(string phaseName, string logMessage, Func body) + { + T result; + + try + { + log(logMessage); + + result = body(Args); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw wrapUnhandled(phaseName, e); + } + + args.Token.ThrowIfCancellationRequested(); + + return result; + } +} + diff --git a/src/WinRT.Impl.Generator/Helpers/ImplGeneratorJsonSerializerContext.cs b/src/WinRT.Generator.Core/Helpers/GeneratorJsonSerializerContext.cs similarity index 56% rename from src/WinRT.Impl.Generator/Helpers/ImplGeneratorJsonSerializerContext.cs rename to src/WinRT.Generator.Core/Helpers/GeneratorJsonSerializerContext.cs index bdff83c9f8..88ea3b6d25 100644 --- a/src/WinRT.Impl.Generator/Helpers/ImplGeneratorJsonSerializerContext.cs +++ b/src/WinRT.Generator.Core/Helpers/GeneratorJsonSerializerContext.cs @@ -4,11 +4,11 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace WindowsRuntime.ImplGenerator.Helpers; +namespace WindowsRuntime.Generator.Helpers; /// -/// A for types used in the impl generator. +/// A for types used across the CsWinRT CLI generators. /// [JsonSerializable(typeof(Dictionary))] [JsonSourceGenerationOptions(WriteIndented = true)] -internal sealed partial class ImplGeneratorJsonSerializerContext : JsonSerializerContext; +internal sealed partial class GeneratorJsonSerializerContext : JsonSerializerContext; diff --git a/src/WinRT.Generator.Core/Helpers/MvidGenerator.cs b/src/WinRT.Generator.Core/Helpers/MvidGenerator.cs new file mode 100644 index 0000000000..b3f32689b8 --- /dev/null +++ b/src/WinRT.Generator.Core/Helpers/MvidGenerator.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using WindowsRuntime.Generator.Extensions; + +namespace WindowsRuntime.Generator.Helpers; + +/// +/// A generator for MVIDs for .NET modules. +/// +internal static class MvidGenerator +{ + /// + /// Generates a deterministic MVID based on two input IIDs. + /// + /// The first IID to combine. + /// The second IID to combine. + /// The resulting MVID. + public static Guid CreateMvid(Guid left, Guid right) + { + Span input = stackalloc byte[32]; + + // Write the two IIDs in sequence + _ = left.TryWriteBytes(input, bigEndian: true, out _); + _ = right.TryWriteBytes(input[16..], bigEndian: true, out _); + + // CodeQL [SM02196] We'll fill the entire buffer during hashing (see below) + Span hash = stackalloc byte[SHA1.HashSizeInBytes]; + + // Hash the two IIDs together (the order matters). + // CodeQL [SM02196] This hash is only used as MVID for the assembly, not for authentication. + _ = SHA1.HashData(input, hash); + + // Create the final MVID from the first 16 bytes of the hash + return new(hash[..16]); + } + + /// + /// Generates a deterministic MVID based on a set of input assemblies. + /// + /// The input paths of all assemblies being processed. + /// The resulting MVID. + public static Guid CreateMvid(params IEnumerable assemblyPaths) + { + // CodeQL [SM02196] This hash is only used as MVID for the assembly, not for authentication. + using IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA1); + + // Process all input assemblies to compute the MVID + foreach (string assemblyPath in assemblyPaths.Order()) + { + using FileStream stream = File.OpenRead(assemblyPath); + + hasher.AppendData(stream); + } + + // CodeQL [SM02196] We'll fill the entire buffer during hashing (see below) + Span hash = stackalloc byte[SHA1.HashSizeInBytes]; + + // Write the final combined hash. + _ = hasher.GetCurrentHash(hash); + + // Create the final MVID from the first 16 bytes of the hash + return new(hash[..16]); + } +} diff --git a/src/WinRT.Generator.Core/IGeneratorArgs.cs b/src/WinRT.Generator.Core/IGeneratorArgs.cs new file mode 100644 index 0000000000..f6bf0ed5c8 --- /dev/null +++ b/src/WinRT.Generator.Core/IGeneratorArgs.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; + +namespace WindowsRuntime.Generator; + +/// +/// Common surface implemented by every per-tool arguments record. +/// +internal interface IGeneratorArgs +{ + /// + /// Gets the cancellation token for the generator invocation. + /// + CancellationToken Token { get; } + + /// + /// Gets the directory where the debug repro should be written, if requested. + /// + string? DebugReproDirectory { get; } +} diff --git a/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs b/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs new file mode 100644 index 0000000000..fa90f69710 --- /dev/null +++ b/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using WindowsRuntime.Generator.Attributes; + +namespace WindowsRuntime.Generator.Parsing; + +/// +/// Helper to format argument objects into response files. +/// +internal static class ResponseFileBuilder +{ + /// + /// Formats as a response file string. + /// + /// The strongly-typed args record. + /// The args instance to format. + /// The formatted response file text. + public static string Format<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TArgs>(TArgs args) + where TArgs : class + { + StringBuilder builder = new(); + + foreach (PropertyInfo property in typeof(TArgs).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + CommandLineArgumentNameAttribute? cliAttribute = property.GetCustomAttribute(); + + if (cliAttribute is null) + { + continue; + } + + object? value = property.GetValue(args); + + // 'null' round-trips as "missing" (the parser will apply the property's default) + if (value is null) + { + continue; + } + + // Empty arrays round-trip as "missing" too: the parser defaults 'string[]' properties to + // an empty array. Emitting "name" for an empty array would also fail the parser's + // 'IndexOf(\' \') == -1 -> MalformedResponseFile' check (after a trailing-space trim). + if (value is ICollection { Count: 0 }) + { + continue; + } + + // Optional booleans default to 'false' on parse, so emitting "name false" would be + // redundant. Required booleans always emit (both 'true' and 'false') so the parser + // sees them and doesn't reject them as missing-required. + bool isRequired = property.GetCustomAttribute() is not null; + + if (value is false && !isRequired) + { + continue; + } + + _ = builder.Append(cliAttribute.Name); + _ = builder.Append(' '); + _ = builder.AppendLine(FormatValue(value)); + } + + return builder.ToString(); + } + + /// + /// Formats a single value for inclusion in a response file line. + /// + /// The non- value to format. + /// The formatted string representation. + private static string FormatValue(object value) + { + if (value is string s) + { + return s; + } + + // String values are formatted as comma-separated lists, without brackets or spaces + if (value is string[] array) + { + return string.Join(',', array); + } + + // All other primitive values use invariant culture, matching the parser's + // invariant 'Convert.ChangeType' so the round-trip is deterministic. + return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; + } +} diff --git a/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs b/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs new file mode 100644 index 0000000000..25a2bc7a6b --- /dev/null +++ b/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using WindowsRuntime.Generator.Attributes; +using WindowsRuntime.Generator.Errors; + +namespace WindowsRuntime.Generator.Parsing; + +/// +/// Helper to parse response files into argument objects. +/// +internal static class ResponseFileParser +{ + /// + /// Parses an instance of from a response file at the given path. + /// + /// + /// The path may be prefixed with @ (matching MSBuild's default escaping + /// for ToolTask response files), which is stripped before reading the file. + /// + /// The strongly-typed args record. Must have a public parameterless constructor surface (only public properties are inspected via reflection). + /// The per-tool well-known exception factory used to route parsing errors. + /// The path to the response file (optionally prefixed with @). + /// The cancellation token for the operation. + /// The populated instance. + public static TArgs Parse<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TArgs, TErr>( + string path, + CancellationToken token) + where TArgs : class + where TErr : IGeneratorErrorFactory + { + // If the path is a response file, it will start with the '@' character. + // This matches the default escaping 'ToolTask' uses for response files. + if (path is ['@', .. string escapedPath]) + { + path = escapedPath; + } + + string[] lines; + + // Read all lines in the response file (each line contains a single command line argument) + try + { + lines = File.ReadAllLines(path); + } + catch (Exception e) + { + throw TErr.ResponseFileReadError(e); + } + + Dictionary argsMap = BuildArgsMap(lines); + + return Populate(argsMap, token); + } + + /// + /// Parses an instance of from a response file read from a stream. + /// + /// The strongly-typed args record. + /// The per-tool well-known exception factory used to route parsing errors. + /// The stream containing the response file content. + /// The cancellation token for the operation. + /// The populated instance. + public static TArgs Parse<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TArgs, TErr>( + Stream stream, + CancellationToken token) + where TArgs : class + where TErr : IGeneratorErrorFactory + { + string[] lines = File.ReadAllLines(stream); + + Dictionary argsMap = BuildArgsMap(lines); + + return Populate(argsMap, token); + } + + /// + /// Builds the name-to-value map from the lines of a response file. + /// + /// The per-tool well-known exception factory used to route parsing errors. + /// The lines read from the response file. + /// The resulting map. + private static Dictionary BuildArgsMap(string[] lines) + where TErr : IGeneratorErrorFactory + { + Dictionary argsMap = []; + + foreach (string line in lines) + { + string trimmedLine = line.Trim(); + + // Skip empty lines (the MSBuild ToolTask may emit blank lines) + if (trimmedLine.Length == 0) + { + continue; + } + + // Each line has the command line argument name followed by a space, and then the + // argument value. If there are no spaces on any given line, the file is malformed. + int indexOfSpace = trimmedLine.IndexOf(' '); + + if (indexOfSpace == -1) + { + throw TErr.MalformedResponseFile(); + } + + // Now we can parse the actual command line argument name and value + string argumentName = trimmedLine.AsSpan()[..indexOfSpace].ToString(); + string argumentValue = trimmedLine.AsSpan()[(indexOfSpace + 1)..].TrimEnd().ToString(); + + // We should never have duplicate commands + if (!argsMap.TryAdd(argumentName, argumentValue)) + { + throw TErr.MalformedResponseFile(); + } + } + + return argsMap; + } + + /// + /// Populates an arguments object with the provided parsed values. + /// + /// The strongly-typed args record. + /// The per-tool well-known exception factory used to route parsing errors. + /// The pre-built name-to-value map. + /// The cancellation token, assigned to any -typed property on . + /// The populated instance. + private static TArgs Populate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TArgs, TErr>( + Dictionary argsMap, + CancellationToken token) + where TArgs : class + where TErr : IGeneratorErrorFactory + { + // We bypass any constructor (and the runtime's required-member enforcement) by using + // 'GetUninitializedObject'. All properties are then populated via reflection from the + // response file values, with explicit defaults applied for non-required properties. + TArgs instance = (TArgs)RuntimeHelpers.GetUninitializedObject(typeof(TArgs)); + + foreach (PropertyInfo property in typeof(TArgs).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + Type propertyType = property.PropertyType; + + // The cancellation token property is always assigned from the parser's argument, + // not from the response file (it has no '[CommandLineArgumentName]' annotation). + if (propertyType == typeof(CancellationToken)) + { + property.SetValue(instance, token); + + continue; + } + + // Properties without the CLI attribute are not part of the response file contract + CommandLineArgumentNameAttribute? cliAttribute = property.GetCustomAttribute(); + + if (cliAttribute is null) + { + continue; + } + + bool isRequired = property.GetCustomAttribute() is not null; + bool hasValue = argsMap.TryGetValue(cliAttribute.Name, out string? rawValue); + + if (!hasValue) + { + ApplyDefault(instance, property, propertyType, isRequired); + + continue; + } + + // For required values, a parse failure throws (preserving the existing behavior of + // 'GetBooleanArgument' / 'GetInt32Argument' / 'GetStringArrayArgument'). For optional + // values, a parse failure silently falls back to the default (matching the existing + // 'GetOptionalBoolArgument' / 'GetOptionalStringArrayArgument' behavior). + if (TryConvert(rawValue!, propertyType, out object? converted)) + { + property.SetValue(instance, converted); + } + else if (isRequired) + { + throw TErr.ResponseFileArgumentParsingError(property.Name, null); + } + else + { + ApplyDefault(instance, property, propertyType, isRequired: false); + } + } + + return instance; + } + + /// + /// Applies the default value for a property when the response file does not provide one. + /// + /// The per-tool well-known exception factory used to route parsing errors. + /// The args instance being populated. + /// The property being set. + /// The property's type. + /// Whether the property has . + private static void ApplyDefault( + object instance, + PropertyInfo property, + Type propertyType, + bool isRequired) + where TErr : IGeneratorErrorFactory + { + if (isRequired) + { + throw TErr.ResponseFileArgumentParsingError(property.Name, null); + } + + // '[DefaultValue("…")]' takes precedence: it lets per-tool args express initializer-style + // defaults that 'GetUninitializedObject' would otherwise skip (initializers aren't needed). + DefaultValueAttribute? defaultValueAttribute = property.GetCustomAttribute(); + + if (defaultValueAttribute is not null) + { + property.SetValue(instance, defaultValueAttribute.Value); + + return; + } + + // 'string[]' properties default to an empty array (matching 'GetOptionalStringArrayArgument'). + if (propertyType == typeof(string[])) + { + property.SetValue(instance, Array.Empty()); + + return; + } + } + + /// + /// Converts a raw string from the response file into the target property type. + /// + /// The raw string value from the response file. + /// The destination property type. + /// The converted value on success. + /// on success; if the value cannot be parsed for . + private static bool TryConvert(string rawValue, Type targetType, out object? converted) + { + if (targetType == typeof(string)) + { + converted = rawValue; + + return true; + } + + if (targetType == typeof(string[])) + { + converted = rawValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + return true; + } + + // For primitives ('bool', 'int', etc.) we rely on the standard 'Convert.ChangeType', which + // is AOT-safe for the built-in primitives and uses invariant culture for deterministic parsing. + try + { + converted = Convert.ChangeType(rawValue, targetType, CultureInfo.InvariantCulture); + + return true; + } + catch (Exception e) when (e is FormatException or InvalidCastException or OverflowException) + { + converted = null; + + return false; + } + } +} diff --git a/src/WinRT.Interop.Generator/References/WellKnownPublicKeyTokens.cs b/src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs similarity index 62% rename from src/WinRT.Interop.Generator/References/WellKnownPublicKeyTokens.cs rename to src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs index a9f34cc3da..0942d8ec00 100644 --- a/src/WinRT.Interop.Generator/References/WellKnownPublicKeyTokens.cs +++ b/src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs @@ -1,35 +1,40 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace WindowsRuntime.InteropGenerator.References; +namespace WindowsRuntime.Generator.References; /// -/// Well known public key tokens. +/// Well-known public key tokens for assemblies used by the CsWinRT generators. /// internal static class WellKnownPublicKeyTokens { /// - /// The public key data for System.Memory.dll. + /// The public key token for mscorlib. + /// + public static readonly byte[] MSCorLib = [0xB7, 0x7A, 0x5C, 0x56, 0x19, 0x34, 0xE0, 0x89]; + + /// + /// The public key token for System.Memory.dll. /// public static readonly byte[] SystemMemory = [0xCC, 0x7B, 0x13, 0xFF, 0xCD, 0x2D, 0xDD, 0x51]; /// - /// The public key data for System.ObjectModel.dll. + /// The public key token for System.ObjectModel.dll. /// public static readonly byte[] SystemObjectModel = [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; /// - /// The public key data for System.Runtime.InteropServices.dll. + /// The public key token for System.Runtime.InteropServices.dll. /// public static readonly byte[] SystemRuntimeInteropServices = [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; /// - /// The public key data for System.Numerics.Vectors.dll. + /// The public key token for System.Numerics.Vectors.dll. /// public static readonly byte[] SystemNumericsVectors = [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; /// - /// The public key data for System.Threading.dll. + /// The public key token for System.Threading.dll. /// public static readonly byte[] SystemThreading = [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; @@ -37,4 +42,4 @@ internal static class WellKnownPublicKeyTokens /// The public key token for CsWinRT assemblies (31bf3856ad364e35). /// public static readonly byte[] CsWinRT = [0x31, 0xBF, 0x38, 0x56, 0xAD, 0x36, 0x4E, 0x35]; -} \ No newline at end of file +} diff --git a/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs b/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs new file mode 100644 index 0000000000..1c755b9d73 --- /dev/null +++ b/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.Generator.References; + +/// +/// Well-known public keys for assemblies used by the CsWinRT generators. +/// +internal static class WellKnownPublicKeys +{ + /// + /// The public key used by the precompiled Windows SDK projection assemblies (WinRT.Sdk.Projection.dll, + /// WinRT.Sdk.Xaml.Projection.dll) and the merged projection assembly (WinRT.Projection.dll) that + /// the projection generator produces at app build time. + /// + public static readonly byte[] WindowsSdkProjection = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; + + /// + /// The public key for the real CsWinRT 3.0 runtime release assemblies (e.g. WinRT.Runtime.dll). + /// + public static readonly byte[] CsWinRT = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x65, 0x32, 0x43, 0xC2, 0xEC, 0x7B, 0x83, 0x00, 0x92, 0x93, 0xB0, 0xE7, 0x98, 0x19, 0x3A, 0x8B, 0xB1, 0x03, 0x90, 0xB5, 0xA1, 0x2A, 0x3F, 0x2D, 0x5C, 0x58, 0x28, 0x2D, 0x71, 0x29, 0xDA, 0xD2, 0xD0, 0xC0, 0x12, 0x75, 0x53, 0x29, 0xC1, 0xA4, 0x51, 0x73, 0xE1, 0xAC, 0x9B, 0x8B, 0x4C, 0x0A, 0x4E, 0x07, 0x82, 0x30, 0xBD, 0xD5, 0xE8, 0xCE, 0x39, 0x32, 0x08, 0x3B, 0x09, 0x89, 0x2D, 0x82, 0x8E, 0x4B, 0x18, 0xAF, 0x00, 0xCE, 0x74, 0x6B, 0xCC, 0x99, 0x4D, 0xAD, 0x06, 0xAC, 0xEC, 0x3E, 0x69, 0x79, 0x3E, 0x75, 0xF8, 0xD2, 0xCC, 0x8E, 0x77, 0xF4, 0x46, 0x68, 0x55, 0x0C, 0xA9, 0xB8, 0x3D, 0xD6, 0x48, 0x2D, 0xA9, 0xAA, 0x4D, 0x2E, 0x8B, 0xCF, 0x2E, 0x17, 0x93, 0xCF, 0x84, 0xC4, 0x95, 0x34, 0x6B, 0x1B, 0x99, 0xEB, 0x99, 0x76, 0x7F, 0xB4, 0x12, 0x46, 0x67, 0x2E, 0x7C, 0xF0]; +} diff --git a/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj b/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj new file mode 100644 index 0000000000..5e0d083a0e --- /dev/null +++ b/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj @@ -0,0 +1,44 @@ + + + net10.0 + 14.0 + enable + true + true + true + + + C#/WinRT Generator core shared infrastructure v$(VersionString) + C#/WinRT Generator core shared infrastructure v$(VersionString) + Copyright (c) Microsoft Corporation. All rights reserved. + $(AssemblyVersionNumber) + $(VersionNumber) + + + true + + + WindowsRuntime.Generator + + + true + true + latest + latest-all + true + strict + true + + + + + + + + + + + + + + diff --git a/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs b/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs index a5c3aa5ded..64817c827c 100644 --- a/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs +++ b/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs @@ -126,7 +126,7 @@ protected override string GenerateFullPathToTool() string? effectiveArchitecture = CsWinRTToolsArchitecture; // Special case for when 'AnyCPU' is specified (mostly for testing scenarios). - if (effectiveArchitecture?.Equals("AnyCPU", System.StringComparison.OrdinalIgnoreCase) is true) + if (effectiveArchitecture?.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase) is true) { return Path.Combine(CsWinRTToolsDirectory!, ToolName); } diff --git a/src/WinRT.Impl.Generator/Errors/UnhandledImplException.cs b/src/WinRT.Impl.Generator/Errors/UnhandledImplException.cs index cd391f8b66..af5926b763 100644 --- a/src/WinRT.Impl.Generator/Errors/UnhandledImplException.cs +++ b/src/WinRT.Impl.Generator/Errors/UnhandledImplException.cs @@ -2,37 +2,20 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ImplGenerator.Errors; /// /// An unhandled exception for the impl generator. /// -internal sealed class UnhandledImplException : Exception +/// +internal sealed class UnhandledImplException(string phase, Exception exception) + : UnhandledGeneratorException(phase, exception) { - /// - /// The phase that failed. - /// - private readonly string _phase; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The phase that failed. - /// The inner exception. - public UnhandledImplException(string phase, Exception exception) - : base(null, exception) - { - _phase = phase; - } + /// + protected override string ErrorPrefix => WellKnownImplExceptions.ErrorPrefix; /// - public override string ToString() - { - return - $"""error {WellKnownImplExceptions.ErrorPrefix}9999: The CsWinRT impl generator failed with an unhandled exception """ + - $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the {_phase} phase. This might be due to an invalid """ + - $"""configuration in the current project, but the generator should still correctly identify that and fail gracefully. Please open an """ + - $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; - } -} \ No newline at end of file + protected override string GeneratorName => "impl"; +} diff --git a/src/WinRT.Impl.Generator/Errors/WellKnownImplException.cs b/src/WinRT.Impl.Generator/Errors/WellKnownImplException.cs index 7bd5e646e0..7a2be1f116 100644 --- a/src/WinRT.Impl.Generator/Errors/WellKnownImplException.cs +++ b/src/WinRT.Impl.Generator/Errors/WellKnownImplException.cs @@ -2,36 +2,13 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ImplGenerator.Errors; /// -/// A well known exceptions for the interop generator. +/// A well known exception for the impl generator. /// -internal sealed class WellKnownImplException : Exception -{ - /// - /// Creates a new instance with the specified parameters. - /// - /// The id of the exception. - /// The exception message. - /// The inner exception. - public WellKnownImplException(string id, string message, Exception? innerException) - : base(message, innerException) - { - Id = id; - } - - /// - /// Gets the id of the exception. - /// - public string Id { get; } - - /// - public override string ToString() - { - return InnerException is not null - ? $"""error {Id}: {Message} Inner exception: '{InnerException.GetType().Name}': '{InnerException.Message}'.""" - : $"""error {Id}: {Message}"""; - } -} \ No newline at end of file +/// +internal sealed class WellKnownImplException(string id, string message, Exception? innerException) + : WellKnownGeneratorException(id, message, innerException); diff --git a/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs b/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs index 7b669deed5..ab965be045 100644 --- a/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs +++ b/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ImplGenerator.Errors; /// -/// Well known exceptions for the interop generator. +/// Well known exceptions for the impl generator. /// -internal static class WellKnownImplExceptions +internal sealed class WellKnownImplExceptions : IGeneratorErrorFactory { /// /// The prefix for all errors produced by this tool. @@ -16,27 +17,28 @@ internal static class WellKnownImplExceptions public const string ErrorPrefix = "CSWINRTIMPLGEN"; /// - /// Some exception was thrown when trying to read the response file. + /// Prevents external instantiation; this type is only used to dispatch through . /// + private WellKnownImplExceptions() + { + } + + /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, "Failed to read the response file to run 'cswinrtimplgen'.", exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError, exception); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(2, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(2, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } - /// - /// The input response file is malformed. - /// + /// public static Exception MalformedResponseFile() { - return Exception(3, "The response file is malformed and contains invalid content."); + return Exception(3, WellKnownGeneratorMessages.MalformedResponseFile); } /// @@ -95,28 +97,22 @@ public static Exception SignDllError(Exception exception) return Exception(10, "Failed to sign the impl .dll on disk.", exception); } - /// - /// The debug repro directory does not exist. - /// + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { - return Exception(11, $"The debug repro directory '{path}' does not exist."); + return Exception(11, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static Exception DebugReproMissingFileEntryMapping(string path) { - return Exception(12, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(12, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static Exception DebugReproUnrecognizedFileEntry(string path) { - return Exception(13, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(13, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.Impl.Generator/Extensions/RuntimeContextExtensions.cs b/src/WinRT.Impl.Generator/Extensions/RuntimeContextExtensions.cs deleted file mode 100644 index 55b50ba06f..0000000000 --- a/src/WinRT.Impl.Generator/Extensions/RuntimeContextExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using AsmResolver.DotNet; -using AsmResolver.PE; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.ImplGenerator; - -/// -/// Extensions for the type. -/// -internal static class RuntimeContextExtensions -{ - extension(RuntimeContext signature) - { - /// - /// Loads a .NET module into the context from the provided input image. - /// - /// The image containing the .NET metadata. - /// The module. - /// Occurs when the image does not contain a valid .NET metadata directory. - public ModuleDefinition LoadModule(PEImage peImage) - { - AssemblyDefinition assemblyDefinition = signature.LoadAssembly(peImage); - - // Every valid .NET assembly will always have exactly one module. In practice, we should - // never encounter an assembly with zero or more than one module, but we can still check - // and ensure that this is the case, just to guard against malformed .NET assemblies too. - if (assemblyDefinition.Modules is not [ModuleDefinition moduleDefinition]) - { - throw new BadImageFormatException(); - } - - return moduleDefinition; - } - } -} \ No newline at end of file diff --git a/src/WinRT.Impl.Generator/Extensions/SignatureComparerExtensions.cs b/src/WinRT.Impl.Generator/Extensions/SignatureComparerExtensions.cs deleted file mode 100644 index c1aa401f6a..0000000000 --- a/src/WinRT.Impl.Generator/Extensions/SignatureComparerExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ImplGenerator; - -/// -/// Extensions for . -/// -internal static class SignatureComparerExtensions -{ -#pragma warning disable IDE0052 // TODO: remove this once Roslyn bug is fixed - /// - /// Backing field for . - /// - private static readonly SignatureComparer IgnoreVersion = new(SignatureComparisonFlags.VersionAgnostic); -#pragma warning restore IDE0052 - - extension(SignatureComparer comparer) - { - /// - /// An immutable default instance of , with . - /// - public static SignatureComparer IgnoreVersion => IgnoreVersion; - - /// - /// Creates an instance for a pair of values. - /// - /// The resulting instance. - public IEqualityComparer<(TypeSignature, TypeSignature)> MakeValueTupleComparer() - { - return new SignatureValueTupleComparer(comparer); - } - } -} - -/// -/// An for a pair of values. -/// -file sealed class SignatureValueTupleComparer : IEqualityComparer<(TypeSignature, TypeSignature)> -{ - /// - /// The wrapped instance used for comparison. - /// - private readonly SignatureComparer _comparer; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The instance to wrap. - public SignatureValueTupleComparer(SignatureComparer comparer) - { - _comparer = comparer; - } - - /// - public bool Equals((TypeSignature, TypeSignature) x, (TypeSignature, TypeSignature) y) - { - return _comparer.Equals(x.Item1, y.Item1) && _comparer.Equals(x.Item2, y.Item2); - } - - /// - public int GetHashCode((TypeSignature, TypeSignature) obj) - { - return HashCode.Combine(_comparer.GetHashCode(obj.Item1), _comparer.GetHashCode(obj.Item2)); - } -} \ No newline at end of file diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.DebugRepro.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.DebugRepro.cs index eac02ae9f4..dea139fcf1 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.DebugRepro.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.DebugRepro.cs @@ -1,19 +1,17 @@ + // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; using System.Threading; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.DebugRepro; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.ImplGenerator.Errors; -using WindowsRuntime.ImplGenerator.Helpers; -using WindowsRuntime.InteropGenerator; #pragma warning disable IDE0008 @@ -35,11 +33,7 @@ internal static partial class ImplGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtimplgen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtimplgen"); token.ThrowIfCancellationRequested(); @@ -57,13 +51,13 @@ private static string UnpackDebugRepro(string path, CancellationToken token) // Parse the debug repro .rsp file using (Stream stream = responseFileEntry.Open()) { - args = ImplGeneratorArgs.ParseFromResponseFile(stream, token); + args = ResponseFileParser.Parse(stream, token); } token.ThrowIfCancellationRequested(); // Load the mappings with all the original file paths for reference .dll-s - Dictionary originalReferenceDllPaths = ExtractPathMap(originalReferenceDllPathsEntry); + Dictionary originalReferenceDllPaths = DebugReproPacker.ExtractPathMap(originalReferenceDllPathsEntry); token.ThrowIfCancellationRequested(); @@ -118,7 +112,7 @@ private static string UnpackDebugRepro(string path, CancellationToken token) token.ThrowIfCancellationRequested(); // Prepare the .rsp file with all updated arguments - string rspText = new ImplGeneratorArgs + string rspText = ResponseFileBuilder.Format(new ImplGeneratorArgs { ReferenceAssemblyPaths = [.. referencePaths], OutputAssemblyPath = outputAssemblyPath!, @@ -127,7 +121,7 @@ private static string UnpackDebugRepro(string path, CancellationToken token) AssemblyOriginatorKeyFile = args.AssemblyOriginatorKeyFile, DebugReproDirectory = null, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtimplgen.rsp"); @@ -150,21 +144,13 @@ private static void SaveDebugRepro(ImplGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownImplExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtimplgen", + archiveFileName: "impl-debug-repro.zip"); - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "impl-debug-repro.zip"); - - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtimplgen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string referenceDirectory = Path.Combine(tempDirectory, "reference"); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(referenceDirectory); // Map with all the original paths @@ -172,17 +158,17 @@ private static void SaveDebugRepro(ImplGeneratorArgs args) // Add all reference paths with hashed names to the reference subdirectory under the // temporary directory, and store them with the updated names in a list to use to build the .rsp file. - List updatedReferenceDllNames = CopyHashedFilesToDirectory(args.ReferenceAssemblyPaths, referenceDirectory, originalReferenceDllPaths, args.Token); + List updatedReferenceDllNames = DebugReproPacker.CopyHashedFilesToDirectory(args.ReferenceAssemblyPaths, referenceDirectory, originalReferenceDllPaths, args.Token); args.Token.ThrowIfCancellationRequested(); // Hash and copy the output assembly - string outputAssemblyHashedName = CopyHashedFileToDirectory(args.OutputAssemblyPath, tempDirectory, originalReferenceDllPaths, args.Token); + string outputAssemblyHashedName = DebugReproPacker.CopyHashedFileToDirectory(args.OutputAssemblyPath, tempDirectory, originalReferenceDllPaths, args.Token); args.Token.ThrowIfCancellationRequested(); // Prepare the .rsp file with all updated arguments - string rspText = new ImplGeneratorArgs + string rspText = ResponseFileBuilder.Format(new ImplGeneratorArgs { ReferenceAssemblyPaths = [.. updatedReferenceDllNames], OutputAssemblyPath = outputAssemblyHashedName, @@ -191,7 +177,7 @@ private static void SaveDebugRepro(ImplGeneratorArgs args) AssemblyOriginatorKeyFile = args.AssemblyOriginatorKeyFile, DebugReproDirectory = args.DebugReproDirectory, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtimplgen.rsp"); @@ -201,134 +187,10 @@ private static void SaveDebugRepro(ImplGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); // Create the .json file with the reference path map - CopyPathMapToDirectory(originalReferenceDllPaths, tempDirectory, ReferencePathMapFileName); + DebugReproPacker.CopyPathMapToDirectory(originalReferenceDllPaths, tempDirectory, ReferencePathMapFileName); args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); - } - - /// - /// Generates a hashed filename by appending a hash of the original filename. - /// - /// The original file path. - /// The hashed filename. - private static string GetHashedFileName(string filePath) - { - string fileName = Path.GetFileName(Path.Normalize(filePath)); - byte[] utf8Data = Encoding.UTF8.GetBytes(filePath); - byte[] hashData = Shake128.HashData(utf8Data, outputLength: 16); - string hash = Convert.ToHexString(hashData); - - return $"{Path.GetFileNameWithoutExtension(fileName)}_{hash}{Path.GetExtension(fileName)}"; - } - - /// - /// Copies all specified assemblies to a target folder, and returns the list of updated hashed filenames. - /// - /// The input assembly paths. - /// The target directory to copy the assemblies to. - /// A dictionary to store the original paths of the copied assemblies. - /// A cancellation token to monitor for cancellation requests. - /// The list of updated hashed filenames. - private static List CopyHashedFilesToDirectory( - string[] assemblyPaths, - string destinationDirectory, - Dictionary originalPaths, - CancellationToken token) - { - List updatedDllNames = []; - - foreach (string assemblyPath in assemblyPaths) - { - token.ThrowIfCancellationRequested(); - - string hashedName = GetHashedFileName(assemblyPath); - string destinationPath = Path.Combine(destinationDirectory, hashedName); - - File.Copy(assemblyPath, destinationPath, overwrite: true); - - updatedDllNames.Add(hashedName); - originalPaths.Add(hashedName, assemblyPath); - } - - return updatedDllNames; - } - - /// - /// Copies a specified assembly to a target folder. - /// - /// The input assembly paths. - /// The target directory to copy the assembly to. - /// A dictionary to store the original paths of the copied assemblies. - /// A cancellation token to monitor for cancellation requests. - /// The hashed filename. - [return: NotNullIfNotNull(nameof(assemblyPath))] - private static string? CopyHashedFileToDirectory( - string? assemblyPath, - string destinationDirectory, - Dictionary originalPaths, - CancellationToken token) - { - if (assemblyPath is null) - { - return null; - } - - string hashedName = GetHashedFileName(assemblyPath); - string destinationPath = Path.Combine(destinationDirectory, hashedName); - - File.Copy(assemblyPath, destinationPath, overwrite: true); - - token.ThrowIfCancellationRequested(); - - originalPaths.Add(hashedName, assemblyPath); - - return hashedName; - } - - /// - /// Copies an input path map to a target directory, as a serialized JSON file. - /// - /// The input path map. - /// The target directory to copy the assemblies to. - /// The name to use for the file with the serialized path map. - private static void CopyPathMapToDirectory( - Dictionary pathMap, - string destinationDirectory, - string fileName) - { - // Create the .json file with the input path map - string jsonFilePath = Path.Combine(destinationDirectory, fileName); - - using Stream jsonStream = File.Create(jsonFilePath); - - // Serialize the path map to the target file - JsonSerializer.Serialize(jsonStream, pathMap, ImplGeneratorJsonSerializerContext.Default.DictionaryStringString); - } - - /// - /// Extracts an input path from a .zip archive entry. - /// - /// The input path map entry. - /// - /// The value is expected to have the content produced by calls to . - /// - private static Dictionary ExtractPathMap(ZipArchiveEntry pathMapEntry) - { - using Stream stream = pathMapEntry.Open(); - - // Load the mapping with all the original file paths for the included .dll-s - return JsonSerializer.Deserialize(stream, ImplGeneratorJsonSerializerContext.Default.DictionaryStringString)!; + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } } diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs index 0f9854159b..331c19a05d 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs @@ -17,9 +17,13 @@ using AsmResolver.PE; using AsmResolver.PE.DotNet.StrongName; using ConsoleAppFramework; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.Extensions; +using WindowsRuntime.Generator.Helpers; +using WindowsRuntime.Generator.Parsing; +using WindowsRuntime.Generator.References; using WindowsRuntime.ImplGenerator.Errors; -using WindowsRuntime.ImplGenerator.References; -using WindowsRuntime.InteropGenerator; namespace WindowsRuntime.ImplGenerator.Generation; @@ -66,143 +70,53 @@ internal static partial class ImplGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - string responseFilePath = inputFilePath; - bool isUsingDebugRepro = false; - - // Load the debug repro to investigate with, if we have one - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - if (Path.GetExtension(Path.Normalize(inputFilePath)) == ".zip") - { - ConsoleApp.Log("Unpacking input 'cswinrtimplgen' debug repro"); - - isUsingDebugRepro = true; - - // If we unpacked a debug repro, we'll also replace the input file - // path with the extracted response file from the input repro. - responseFilePath = UnpackDebugRepro(inputFilePath, token); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("unpack-debug-repro", e); - } - - token.ThrowIfCancellationRequested(); - - ImplGeneratorArgs args; - - // Parse the actual arguments from the response file - try - { - args = ImplGeneratorArgs.ParseFromResponseFile(responseFilePath, token); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("parsing", e); - } - - args.Token.ThrowIfCancellationRequested(); - - // Save a debug repro, if needed - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - // We also skip this if we're currently processing an input debug - // repro, as there would be no point in creating a new one from that. - if (args.DebugReproDirectory is not null && !isUsingDebugRepro) - { - ConsoleApp.Log("Saving 'cswinrtimplgen' debug repro"); - - SaveDebugRepro(args); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("save-debug-repro", e); - } - - args.Token.ThrowIfCancellationRequested(); - - RuntimeContext runtimeContext; - ModuleDefinition outputModule; + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + inputFilePath: inputFilePath, + toolName: "cswinrtimplgen", + unpackDebugRepro: UnpackDebugRepro, + parseFromResponseFile: ResponseFileParser.Parse, + saveDebugRepro: SaveDebugRepro, + wrapUnhandled: static (phase, e) => new UnhandledImplException(phase, e), + log: ConsoleApp.Log, + token: token); // Initialize the assembly resolver and load the output module - try - { - LoadOutputModule(args, out runtimeContext, out outputModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("loading", e); - } - - args.Token.ThrowIfCancellationRequested(); - - ModuleDefinition implModule; + (RuntimeContext runtimeContext, ModuleDefinition outputModule) = runner.RunPhase( + phaseName: "loading", + body: LoadOutputModule); // Define the impl module to emit - try - { - implModule = DefineImplModule(runtimeContext, outputModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("loading", e); - } - - args.Token.ThrowIfCancellationRequested(); + ModuleDefinition implModule = runner.RunPhase( + phaseName: "loading", + body: _ => DefineImplModule(runtimeContext, outputModule)); // Emit all necessary IL code in the impl module - try + runner.RunPhase(phaseName: "generation", body: _ => { EmitAssemblyAttributes(outputModule, implModule); EmitTypeForwards(outputModule, implModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("generation", e); - } - - args.Token.ThrowIfCancellationRequested(); + }); // Write the module to disk with all the generated contents - try - { - WriteImplModuleToDisk(args, outputModule, implModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("emit", e); - } + runner.RunPhase( + phaseName: "emit", + body: args => WriteImplModuleToDisk(args, outputModule, implModule)); // Signs the module on disk, if needed - try - { - SignImplModuleOnDisk(args, outputModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("sign", e); - } + runner.RunPhase( + phaseName: "sign", + body: args => SignImplModuleOnDisk(args, outputModule)); // Notify the user that generation was successful - ConsoleApp.Log($"Impl code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, implModule.Name!)}"); + ConsoleApp.Log($"Impl code generated -> {Path.Combine(runner.Args.GeneratedAssemblyDirectory, implModule.Name!)}"); } /// /// Loads the output assembly being produced. /// /// The arguments for this invocation. - /// The instance in use. - /// The loaded for the output assembly. - private static void LoadOutputModule( - ImplGeneratorArgs args, - out RuntimeContext runtimeContext, - out ModuleDefinition outputModule) + /// The instance in use and the loaded for the output assembly. + private static (RuntimeContext RuntimeContext, ModuleDefinition OutputModule) LoadOutputModule(ImplGeneratorArgs args) { PEImage outputAssemblyImage; @@ -227,12 +141,12 @@ private static void LoadOutputModule( PathAssemblyResolver assemblyResolver = new(args.ReferenceAssemblyPaths); // Initialize the runtime context (this will be reused to allow caching) - runtimeContext = new RuntimeContext(targetRuntime, assemblyResolver); + RuntimeContext runtimeContext = new(targetRuntime, assemblyResolver); // Try to load the .dll at the current path try { - outputModule = runtimeContext.LoadModule(outputAssemblyImage); + return (runtimeContext, runtimeContext.LoadModule(outputAssemblyImage)); } catch (Exception e) { @@ -325,7 +239,7 @@ private static void EmitTypeForwards(ModuleDefinition inputModule, ModuleDefinit // will always have a version number equal or higher than this, so it will load correctly. AssemblyReference sdkProjectionAssembly = new("WinRT.Sdk.Projection"u8, new Version(0, 0, 0, 0)) { - PublicKeyOrToken = ImplValues.PublicKeyData, + PublicKeyOrToken = WellKnownPublicKeys.WindowsSdkProjection, HasPublicKey = true }; @@ -333,7 +247,7 @@ private static void EmitTypeForwards(ModuleDefinition inputModule, ModuleDefinit // This is only used when the option to use Windows UI Xaml projections is enabled. AssemblyReference sdkXamlProjectionAssembly = new("WinRT.Sdk.Xaml.Projection"u8, new Version(0, 0, 0, 0)) { - PublicKeyOrToken = ImplValues.PublicKeyData, + PublicKeyOrToken = WellKnownPublicKeys.WindowsSdkProjection, HasPublicKey = true }; @@ -341,7 +255,7 @@ private static void EmitTypeForwards(ModuleDefinition inputModule, ModuleDefinit // Unlike the implementation .dll for the Windows SDK however, this .dll is created on the fly. AssemblyReference projectionAssembly = new("WinRT.Projection"u8, new Version(0, 0, 0, 0)) { - PublicKeyOrToken = ImplValues.PublicKeyData, + PublicKeyOrToken = WellKnownPublicKeys.WindowsSdkProjection, HasPublicKey = true }; diff --git a/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.Formatting.cs b/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.Formatting.cs deleted file mode 100644 index 22cf0b5835..0000000000 --- a/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.Formatting.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text; - -namespace WindowsRuntime.ImplGenerator.Generation; - -/// -internal partial class ImplGeneratorArgs -{ - /// - /// Formats the current instance into a response file text. - /// - /// The resulting response file text. - public string FormatToResponseFile() - { - StringBuilder builder = new(); - - _ = builder.Append(GetCommandLineArgumentName(nameof(ReferenceAssemblyPaths))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', ReferenceAssemblyPaths)); - - _ = builder.Append(GetCommandLineArgumentName(nameof(OutputAssemblyPath))); - _ = builder.Append(' '); - _ = builder.AppendLine(OutputAssemblyPath); - - _ = builder.Append(GetCommandLineArgumentName(nameof(GeneratedAssemblyDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(GeneratedAssemblyDirectory); - - _ = builder.Append(GetCommandLineArgumentName(nameof(TreatWarningsAsErrors))); - _ = builder.Append(' '); - _ = builder.AppendLine(TreatWarningsAsErrors.ToString()); - - if (AssemblyOriginatorKeyFile is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(AssemblyOriginatorKeyFile))); - _ = builder.Append(' '); - _ = builder.AppendLine(AssemblyOriginatorKeyFile); - } - - if (DebugReproDirectory is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(DebugReproDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(DebugReproDirectory); - } - - return builder.ToString(); - } -} diff --git a/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.Parsing.cs b/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.Parsing.cs deleted file mode 100644 index 27fd5e5115..0000000000 --- a/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.Parsing.cs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using WindowsRuntime.ImplGenerator.Attributes; -using WindowsRuntime.ImplGenerator.Errors; -using WindowsRuntime.InteropGenerator; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.ImplGenerator.Generation; - -/// -internal partial class ImplGeneratorArgs -{ - /// - /// Parses an instance from a target response file. - /// - /// The path to the response file. - /// The token for the operation. - /// The resulting instance. - public static ImplGeneratorArgs ParseFromResponseFile(string path, CancellationToken token) - { - // If the path is a response file, it will start with the '@' character. - // This matches the default escaping 'ToolTask' uses for response files. - if (path is ['@', .. string escapedPath]) - { - path = escapedPath; - } - - string[] lines; - - // Read all lines in the response file (each line contains a single command line argument) - try - { - lines = File.ReadAllLines(path); - } - catch (Exception e) - { - throw WellKnownImplExceptions.ResponseFileReadError(e); - } - - return ParseFromResponseFile(lines, token); - } - - /// - /// Parses an instance from a target response file. - /// - /// The stream to the response file. - /// The token for the operation. - /// The resulting instance. - public static ImplGeneratorArgs ParseFromResponseFile(Stream stream, CancellationToken token) - { - string[] responseArgs = File.ReadAllLines(stream); - - return ParseFromResponseFile(responseArgs, token); - } - - /// - /// Parses an instance from a target response file. - /// - /// The lines read from the response file. - /// The token for the operation. - /// The resulting instance. - private static ImplGeneratorArgs ParseFromResponseFile(string[] lines, CancellationToken token) - { - Dictionary argsMap = []; - - // Build a map with all the commands and their values - foreach (string line in lines) - { - string trimmedLine = line.Trim(); - - // Each line has the command line argument name followed by a space, and then the - // argument value. If there are no spaces on any given line, the file is malformed. - int indexOfSpace = trimmedLine.IndexOf(' '); - - if (indexOfSpace == -1) - { - throw WellKnownImplExceptions.MalformedResponseFile(); - } - - // Now we can parse the actual command line argument name and value - string argumentName = trimmedLine.AsSpan()[..indexOfSpace].ToString(); - string argumentValue = trimmedLine.AsSpan()[(indexOfSpace + 1)..].TrimEnd().ToString(); - - // We should never have duplicate commands - if (!argsMap.TryAdd(argumentName, argumentValue)) - { - throw WellKnownImplExceptions.MalformedResponseFile(); - } - } - - // Parse all commands to create the managed arguments to use - return new() - { - ReferenceAssemblyPaths = GetStringArrayArgument(argsMap, nameof(ReferenceAssemblyPaths)), - OutputAssemblyPath = GetStringArgument(argsMap, nameof(OutputAssemblyPath)), - GeneratedAssemblyDirectory = GetStringArgument(argsMap, nameof(GeneratedAssemblyDirectory)), - TreatWarningsAsErrors = GetBooleanArgument(argsMap, nameof(TreatWarningsAsErrors)), - AssemblyOriginatorKeyFile = GetNullableStringArgument(argsMap, nameof(AssemblyOriginatorKeyFile)), - DebugReproDirectory = GetNullableStringArgument(argsMap, nameof(DebugReproDirectory)), - Token = token - }; - } - - /// - /// Gets the command line argument name for a property. - /// - /// The target property name. - /// The command line argument name for . - public static string GetCommandLineArgumentName(string propertyName) - { - try - { - return typeof(ImplGeneratorArgs).GetProperty(propertyName)!.GetCustomAttribute()!.Name; - } - catch (Exception e) - { - throw WellKnownImplExceptions.ResponseFileArgumentParsingError(propertyName, e); - } - } - - /// - /// Parses a array argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string[] GetStringArrayArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - } - - throw WellKnownImplExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string GetStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - throw WellKnownImplExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a nullable (optional) argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string? GetNullableStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - return null; - } - - /// - /// Parses a argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static bool GetBooleanArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - if (bool.TryParse(argumentValue, out bool parsedValue)) - { - return parsedValue; - } - } - - throw WellKnownImplExceptions.ResponseFileArgumentParsingError(propertyName); - } -} \ No newline at end of file diff --git a/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.cs b/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.cs index be772b2086..d85ec1dd2e 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGeneratorArgs.cs @@ -2,14 +2,15 @@ // Licensed under the MIT License. using System.Threading; -using WindowsRuntime.ImplGenerator.Attributes; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Attributes; namespace WindowsRuntime.ImplGenerator.Generation; /// /// Input parameters for . /// -internal sealed partial class ImplGeneratorArgs +internal sealed class ImplGeneratorArgs : IGeneratorArgs { /// Gets the input .dll paths. [CommandLineArgumentName("--reference-assembly-paths")] @@ -31,10 +32,11 @@ internal sealed partial class ImplGeneratorArgs [CommandLineArgumentName("--assembly-originator-key-file")] public string? AssemblyOriginatorKeyFile { get; init; } - /// Gets the token for the operation. + /// public required CancellationToken Token { get; init; } - /// Gets the directory to use to place the debug repro, if requested. + /// [CommandLineArgumentName("--debug-repro-directory")] public string? DebugReproDirectory { get; init; } -} \ No newline at end of file +} + diff --git a/src/WinRT.Impl.Generator/Helpers/MvidGenerator.cs b/src/WinRT.Impl.Generator/Helpers/MvidGenerator.cs deleted file mode 100644 index 4a87706e0c..0000000000 --- a/src/WinRT.Impl.Generator/Helpers/MvidGenerator.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Security.Cryptography; - -namespace WindowsRuntime.ImplGenerator; - -/// -/// A generator for MVIDs for .NET modules. -/// -internal static class MvidGenerator -{ - /// - /// Generates a deterministic MVID based on two input IIDs. - /// - /// The first IID to combine. - /// The second IID to combine. - /// The resulting MVID. - public static Guid CreateMvid(Guid left, Guid right) - { - Span input = stackalloc byte[32]; - - // Write the two IIDs in sequence - _ = left.TryWriteBytes(input, bigEndian: true, out _); - _ = right.TryWriteBytes(input[16..], bigEndian: true, out _); - - Span hash = stackalloc byte[SHA1.HashSizeInBytes]; - - // Hash the two IIDs together (the order matters) - _ = SHA1.HashData(input, hash); - - // Create the final MVID from the first 16 bytes of the hash - return new(hash[..16]); - } -} diff --git a/src/WinRT.Impl.Generator/References/ImplValues.cs b/src/WinRT.Impl.Generator/References/ImplValues.cs deleted file mode 100644 index e29b8828de..0000000000 --- a/src/WinRT.Impl.Generator/References/ImplValues.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace WindowsRuntime.ImplGenerator.References; - -/// -/// Well known impl values (constants). -/// -internal static class ImplValues -{ - /// - /// The public key for CsWinRT assemblies. - /// - public const string PublicKey = "0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9"; - - /// - /// The public key data for CsWinRT assemblies. - /// - public static readonly byte[] PublicKeyData = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; -} \ No newline at end of file diff --git a/src/WinRT.Impl.Generator/WinRT.Impl.Generator.csproj b/src/WinRT.Impl.Generator/WinRT.Impl.Generator.csproj index e37bc9a077..c34119d260 100644 --- a/src/WinRT.Impl.Generator/WinRT.Impl.Generator.csproj +++ b/src/WinRT.Impl.Generator/WinRT.Impl.Generator.csproj @@ -37,6 +37,11 @@ true win-$(BuildToolArch) true + + + $(NoWarn);IDE0028 @@ -51,8 +56,7 @@ - - + diff --git a/src/WinRT.Internal/WinRT.Internal.csproj b/src/WinRT.Internal/WinRT.Internal.csproj index b8f65df35d..cf114c6f0e 100644 --- a/src/WinRT.Internal/WinRT.Internal.csproj +++ b/src/WinRT.Internal/WinRT.Internal.csproj @@ -135,8 +135,9 @@ <_CsWinRTInternalWinMDInputAssemblyPath>@(IntermediateAssembly->'%(FullPath)') diff --git a/src/WinRT.Interop.Generator/Attributes/CommandLineArgumentNameAttribute.cs b/src/WinRT.Interop.Generator/Attributes/CommandLineArgumentNameAttribute.cs deleted file mode 100644 index 140760c163..0000000000 --- a/src/WinRT.Interop.Generator/Attributes/CommandLineArgumentNameAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace WindowsRuntime.InteropGenerator.Attributes; - -/// -/// An attribute indicating the name of a given command line argument. -/// -/// The command line argument name. -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -internal sealed class CommandLineArgumentNameAttribute(string name) : Attribute -{ - /// - /// Gets the command line argument name. - /// - public string Name { get; } = name; -} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Builders/IgnoresAccessChecksToBuilder.cs b/src/WinRT.Interop.Generator/Builders/IgnoresAccessChecksToBuilder.cs index d00bffcd9b..59c480334e 100644 --- a/src/WinRT.Interop.Generator/Builders/IgnoresAccessChecksToBuilder.cs +++ b/src/WinRT.Interop.Generator/Builders/IgnoresAccessChecksToBuilder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using AsmResolver.DotNet; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Factories; using WindowsRuntime.InteropGenerator.References; diff --git a/src/WinRT.Interop.Generator/Builders/WindowsRuntimeTypeHierarchyBuilder.cs b/src/WinRT.Interop.Generator/Builders/WindowsRuntimeTypeHierarchyBuilder.cs index 26b86be38f..aabdfe8ed1 100644 --- a/src/WinRT.Interop.Generator/Builders/WindowsRuntimeTypeHierarchyBuilder.cs +++ b/src/WinRT.Interop.Generator/Builders/WindowsRuntimeTypeHierarchyBuilder.cs @@ -15,6 +15,7 @@ using AsmResolver.PE.DotNet.Metadata.Tables; using CommunityToolkit.HighPerformance; using CommunityToolkit.HighPerformance.Buffers; +using WindowsRuntime.Generator.Errors; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.References; using static AsmResolver.PE.DotNet.Cil.CilOpCodes; diff --git a/src/WinRT.Interop.Generator/Errors/UnhandledInteropException.cs b/src/WinRT.Interop.Generator/Errors/UnhandledInteropException.cs index b279abc9aa..972d473b14 100644 --- a/src/WinRT.Interop.Generator/Errors/UnhandledInteropException.cs +++ b/src/WinRT.Interop.Generator/Errors/UnhandledInteropException.cs @@ -2,37 +2,20 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.InteropGenerator.Errors; /// /// An unhandled exception for the interop generator. /// -internal sealed class UnhandledInteropException : Exception +/// +internal sealed class UnhandledInteropException(string phase, Exception exception) + : UnhandledGeneratorException(phase, exception) { - /// - /// The phase that failed. - /// - private readonly string _phase; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The phase that failed. - /// The inner exception. - public UnhandledInteropException(string phase, Exception exception) - : base(null, exception) - { - _phase = phase; - } + /// + protected override string ErrorPrefix => WellKnownInteropExceptions.ErrorPrefix; /// - public override string ToString() - { - return - $"""error {WellKnownInteropExceptions.ErrorPrefix}9999: The CsWinRT interop generator failed with an unhandled exception """ + - $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the '{_phase}' phase. This might be due to an invalid """ + - $"""configuration in the current project, but the generator should still correctly identify that and fail gracefully. Please open an """ + - $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; - } -} \ No newline at end of file + protected override string GeneratorName => "interop"; +} diff --git a/src/WinRT.Interop.Generator/Errors/WellKnownInteropException.cs b/src/WinRT.Interop.Generator/Errors/WellKnownInteropException.cs index 2d7ea67300..d5676957f8 100644 --- a/src/WinRT.Interop.Generator/Errors/WellKnownInteropException.cs +++ b/src/WinRT.Interop.Generator/Errors/WellKnownInteropException.cs @@ -6,36 +6,27 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; using System.Text; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.InteropGenerator.Errors; /// /// A well-known exception for the interop generator. /// -internal sealed class WellKnownInteropException : Exception +/// +/// The interop generator extends the shared base with an +/// optional outer exception that can be attached via when re-throwing, +/// plus a richer that surfaces both the inner and outer context when present. +/// +/// +internal sealed class WellKnownInteropException(string id, string message, Exception? innerException) + : WellKnownGeneratorException(id, message, innerException) { /// /// The outer exception to include in the output, if available. /// private WellKnownInteropException? _outerException; - /// - /// Creates a new instance with the specified parameters. - /// - /// The id of the exception. - /// The exception message. - /// The inner exception. - public WellKnownInteropException(string id, string message, Exception? innerException) - : base(message, innerException) - { - Id = id; - } - - /// - /// Gets the id of the exception. - /// - public string Id { get; } - /// public override string ToString() { @@ -95,4 +86,4 @@ public void ThrowOrAttach(Exception exception) // been used as the inner exception by callers, when constructing this instance. throw this; } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs index cb47d56868..6df46e720a 100644 --- a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs +++ b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs @@ -8,6 +8,7 @@ using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; +using WindowsRuntime.Generator.Errors; using WindowsRuntime.InteropGenerator.Fixups; namespace WindowsRuntime.InteropGenerator.Errors; @@ -15,13 +16,54 @@ namespace WindowsRuntime.InteropGenerator.Errors; /// /// Well known exceptions for the interop generator. /// -internal static class WellKnownInteropExceptions +internal sealed class WellKnownInteropExceptions : IGeneratorErrorFactory { /// /// The prefix for all errors produced by this tool. /// public const string ErrorPrefix = "CSWINRTINTEROPGEN"; + /// + /// Prevents external instantiation; this type is only used to dispatch through . + /// + private WellKnownInteropExceptions() + { + } + + // Explicit IGeneratorErrorFactory implementations: the public static methods for these six + // shared logical errors return 'WellKnownInteropException' (so callers can chain '.ThrowOrAttach(...)'), + // but the interface contract returns 'Exception'. C# 'static abstract' interface methods don't + // support return-type covariance, so we forward through these tiny explicit shims. + static Exception IGeneratorErrorFactory.ResponseFileReadError(Exception exception) + { + return ResponseFileReadError(exception); + } + + static Exception IGeneratorErrorFactory.ResponseFileArgumentParsingError(string argumentName, Exception? exception) + { + return ResponseFileArgumentParsingError(argumentName, exception); + } + + static Exception IGeneratorErrorFactory.MalformedResponseFile() + { + return MalformedResponseFile(); + } + + static Exception IGeneratorErrorFactory.DebugReproDirectoryDoesNotExist(string path) + { + return DebugReproDirectoryDoesNotExist(path); + } + + static Exception IGeneratorErrorFactory.DebugReproMissingFileEntryMapping(string path) + { + return DebugReproMissingFileEntryMapping(path); + } + + static Exception IGeneratorErrorFactory.DebugReproUnrecognizedFileEntry(string path) + { + return DebugReproUnrecognizedFileEntry(path); + } + /// /// A runtime class name is too long. /// @@ -242,36 +284,28 @@ public static WellKnownInteropException IDictionary2TypeCodeGenerationError(Type return Exception(27, $"Failed to generate marshalling code for 'IDictionary' type '{dictionaryType}'.", exception); } - /// - /// Some exception was thrown when trying to read the response file. - /// + /// public static WellKnownInteropException ResponseFileReadError(Exception exception) { - return Exception(28, "Failed to read the response file to run 'cswinrtinteropgen'.", exception); + return Exception(28, WellKnownGeneratorMessages.ResponseFileReadError, exception); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static WellKnownInteropException ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(29, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(29, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } - /// - /// The input response file is malformed. - /// + /// public static WellKnownInteropException MalformedResponseFile() { - return Exception(30, "The response file is malformed and contains invalid content."); + return Exception(30, WellKnownGeneratorMessages.MalformedResponseFile); } - /// - /// The debug repro directory does not exist. - /// + /// public static WellKnownInteropException DebugReproDirectoryDoesNotExist(string path) { - return Exception(31, $"The debug repro directory '{path}' does not exist."); + return Exception(31, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } /// @@ -746,20 +780,16 @@ public static WellKnownInteropException ReservedDllOriginalPathMismatchFromDebug return Exception(88, $"The reserved '{dllName}' assembly has a mismatching path with the item supplied via '$(ReferencePath)': the debug repro cannot be generated."); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static WellKnownInteropException DebugReproMissingFileEntryMapping(string path) { - return Exception(89, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(89, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static WellKnownInteropException DebugReproUnrecognizedFileEntry(string path) { - return Exception(90, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(90, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.Interop.Generator/Extensions/InteropExceptionExtensions.cs b/src/WinRT.Interop.Generator/Extensions/InteropExceptionExtensions.cs deleted file mode 100644 index 28bba1fec4..0000000000 --- a/src/WinRT.Interop.Generator/Extensions/InteropExceptionExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using WindowsRuntime.InteropGenerator.Errors; - -namespace WindowsRuntime.InteropGenerator; - -/// -/// Extensions for interop exceptions. -/// -internal static class InteropExceptionExtensions -{ - extension(Exception exception) - { - /// - /// Gets a value indicating whether an exception is well known (and should therefore not be caught). - /// - public bool IsWellKnown => exception is OperationCanceledException or WellKnownInteropException; - } -} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Extensions/RuntimeContextExtensions.cs b/src/WinRT.Interop.Generator/Extensions/RuntimeContextExtensions.cs deleted file mode 100644 index 05b9c3833f..0000000000 --- a/src/WinRT.Interop.Generator/Extensions/RuntimeContextExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using AsmResolver.DotNet; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.InteropGenerator; - -/// -/// Extensions for the type. -/// -internal static class RuntimeContextExtensions -{ - extension(RuntimeContext signature) - { - /// - /// Loads a .NET module into the context from the provided input file. - /// - /// The file path to the input executable to load. - /// The module. - /// Occurs when the image does not contain a valid .NET metadata directory. - public ModuleDefinition LoadModule(string filePath) - { - AssemblyDefinition assemblyDefinition = signature.LoadAssembly(filePath); - - // Every valid .NET assembly will always have exactly one module. In practice, we should - // never encounter an assembly with zero or more than one module, but we can still check - // and ensure that this is the case, just to guard against malformed .NET assemblies too. - if (assemblyDefinition.Modules is not [ModuleDefinition moduleDefinition]) - { - throw new BadImageFormatException(); - } - - return moduleDefinition; - } - } -} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.DebugRepro.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.DebugRepro.cs index 22396c11b1..f39a75b95b 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.DebugRepro.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.DebugRepro.cs @@ -7,12 +7,11 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; using System.Threading; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.DebugRepro; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.InteropGenerator.Errors; -using WindowsRuntime.InteropGenerator.Helpers; #pragma warning disable IDE0008 @@ -39,11 +38,7 @@ internal partial class InteropGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtinteropgen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtinteropgen"); token.ThrowIfCancellationRequested(); @@ -62,14 +57,14 @@ private static string UnpackDebugRepro(string path, CancellationToken token) // Parse the debug repro .rsp file using (Stream stream = responseFileEntry.Open()) { - args = InteropGeneratorArgs.ParseFromResponseFile(stream, token); + args = ResponseFileParser.Parse(stream, token); } token.ThrowIfCancellationRequested(); // Load the mappings with all the original file paths for both reference and implementation .dll-s - Dictionary originalReferenceDllPaths = ExtractPathMap(originalReferenceDllPathsEntry); - Dictionary originalImplementationDllPaths = ExtractPathMap(originalImplementationDllPathsEntry); + Dictionary originalReferenceDllPaths = DebugReproPacker.ExtractPathMap(originalReferenceDllPathsEntry); + Dictionary originalImplementationDllPaths = DebugReproPacker.ExtractPathMap(originalImplementationDllPathsEntry); token.ThrowIfCancellationRequested(); @@ -165,7 +160,7 @@ private static string UnpackDebugRepro(string path, CancellationToken token) token.ThrowIfCancellationRequested(); // Prepare the .rsp file with all updated arguments - string rspText = new InteropGeneratorArgs + string rspText = ResponseFileBuilder.Format(new InteropGeneratorArgs { ReferenceAssemblyPaths = [.. referencePaths], ImplementationAssemblyPaths = [.. implementationPaths], @@ -183,7 +178,7 @@ private static string UnpackDebugRepro(string path, CancellationToken token) MaxDegreesOfParallelism = args.MaxDegreesOfParallelism, DebugReproDirectory = null, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtinteropgen.rsp"); @@ -206,22 +201,14 @@ private static void SaveDebugRepro(InteropGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownInteropExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } - - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "interop-debug-repro.zip"); + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtinteropgen", + archiveFileName: "interop-debug-repro.zip"); - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtinteropgen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string referenceDirectory = Path.Combine(tempDirectory, "reference"); string implementationDirectory = Path.Combine(tempDirectory, "implementation"); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(referenceDirectory); _ = Directory.CreateDirectory(implementationDirectory); @@ -231,8 +218,8 @@ private static void SaveDebugRepro(InteropGeneratorArgs args) // Add all reference and implementation paths with hashed names to the respective subdirectories under the // temporary directory, and store them with the updated names in a list to use to build the .rsp file. - List updatedReferenceDllNames = CopyHashedFilesToDirectory(args.ReferenceAssemblyPaths, referenceDirectory, originalReferenceDllPaths, args.Token); - List updatedImplementationDllNames = CopyHashedFilesToDirectory(args.ImplementationAssemblyPaths, implementationDirectory, originalImplementationDllPaths, args.Token); + List updatedReferenceDllNames = DebugReproPacker.CopyHashedFilesToDirectory(args.ReferenceAssemblyPaths, referenceDirectory, originalReferenceDllPaths, args.Token); + List updatedImplementationDllNames = DebugReproPacker.CopyHashedFilesToDirectory(args.ImplementationAssemblyPaths, implementationDirectory, originalImplementationDllPaths, args.Token); args.Token.ThrowIfCancellationRequested(); @@ -246,7 +233,7 @@ private static void SaveDebugRepro(InteropGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); // Prepare the .rsp file with all updated arguments - string rspText = new InteropGeneratorArgs + string rspText = ResponseFileBuilder.Format(new InteropGeneratorArgs { ReferenceAssemblyPaths = [.. updatedReferenceDllNames], ImplementationAssemblyPaths = [.. updatedImplementationDllNames], @@ -264,7 +251,7 @@ private static void SaveDebugRepro(InteropGeneratorArgs args) MaxDegreesOfParallelism = args.MaxDegreesOfParallelism, DebugReproDirectory = args.DebugReproDirectory, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtinteropgen.rsp"); @@ -274,102 +261,54 @@ private static void SaveDebugRepro(InteropGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); // Create the .json file with the reference path map - CopyPathMapToDirectory(originalReferenceDllPaths, tempDirectory, ReferencePathMapFileName); + DebugReproPacker.CopyPathMapToDirectory(originalReferenceDllPaths, tempDirectory, ReferencePathMapFileName); args.Token.ThrowIfCancellationRequested(); // Do the same for the implementation path map - CopyPathMapToDirectory(originalImplementationDllPaths, tempDirectory, ImplementationPathMapFileName); + DebugReproPacker.CopyPathMapToDirectory(originalImplementationDllPaths, tempDirectory, ImplementationPathMapFileName); args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); - } - - /// - /// Generates a hashed filename by appending a hash of the original filename. - /// - /// The original file path. - /// The hashed filename. - private static string GetHashedFileName(string filePath) - { - string fileName = Path.GetFileName(Path.Normalize(filePath)); - byte[] utf8Data = Encoding.UTF8.GetBytes(filePath); - byte[] hashData = Shake128.HashData(utf8Data, outputLength: 16); - string hash = Convert.ToHexString(hashData); - - return $"{Path.GetFileNameWithoutExtension(fileName)}_{hash}{Path.GetExtension(fileName)}"; - } - - /// - /// Copies all specified assemblies to a target folder, and returns the list of updated hashed filenames. - /// - /// The input assembly paths. - /// The target directory to copy the assemblies to. - /// A dictionary to store the original paths of the copied assemblies. - /// A cancellation token to monitor for cancellation requests. - /// The list of updated hashed filenames. - private static List CopyHashedFilesToDirectory( - string[] assemblyPaths, - string destinationDirectory, - Dictionary originalPaths, - CancellationToken token) - { - List updatedDllNames = []; - - foreach (string assemblyPath in assemblyPaths) - { - token.ThrowIfCancellationRequested(); - - string hashedName = GetHashedFileName(assemblyPath); - string destinationPath = Path.Combine(destinationDirectory, hashedName); - - File.Copy(assemblyPath, destinationPath, overwrite: true); - - updatedDllNames.Add(hashedName); - originalPaths.Add(hashedName, assemblyPath); - } - - return updatedDllNames; + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } /// - /// Copies a specified assembly to a target folder. + /// Copies a single specified file to a target folder, with reserved-DLL dedupe. /// - /// The input assembly paths. - /// The target directory to copy the assembly to. - /// A dictionary to store the original paths of the copied assemblies. + /// + /// The interop generator keeps a local variant (instead of using the shared + /// ) + /// to handle the private implementation-detail assemblies (WinRT.Sdk.Projection.dll, WinRT.Sdk.Xaml.Projection.dll, + /// WinRT.Projection.dll, WinRT.Component.dll) that are passed both via the reference set and + /// explicitly via dedicated properties. When the same path is already in the map (under the same hashed name), + /// the copy is skipped; if the hashed name collides but the original paths differ, that's a real bug and we + /// throw . + /// + /// The input file path, or to skip. + /// The target directory to copy the file to. + /// A dictionary to store the original path of the copied file (keyed by hashed file name). /// A cancellation token to monitor for cancellation requests. - /// The hashed filename. - [return: NotNullIfNotNull(nameof(assemblyPath))] + /// The hashed filename, or if was . + [return: NotNullIfNotNull(nameof(filePath))] private static string? CopyHashedFileToDirectory( - string? assemblyPath, + string? filePath, string destinationDirectory, Dictionary originalPaths, CancellationToken token) { - if (assemblyPath is null) + if (filePath is null) { return null; } - string hashedName = GetHashedFileName(assemblyPath); + string hashedName = DebugReproPacker.GetHashedFileName(filePath); // Special case for private implementation detail assemblies (e.g. 'WinRT.Projection.dll') that are // both passed via the reference set, but also explicitly as separate properties. In that case, we // expect that those should already be in the original paths at this point. So we validate that // the path actually matches, and simply do nothing if that's the case, as this is intended. - if (originalPaths.TryGetValue(hashedName, out string? originalPath) && originalPath == assemblyPath) + if (originalPaths.TryGetValue(hashedName, out string? originalPath) && originalPath == filePath) { return hashedName; } @@ -378,7 +317,7 @@ private static List CopyHashedFilesToDirectory( // different path than the one provided to the reference set, which should never happen (it's invalid). if (originalPaths.ContainsKey(hashedName)) { - string fileName = Path.GetFileName(Path.Normalize(assemblyPath)); + string fileName = Path.GetFileName(Path.Normalize(filePath)); throw WellKnownInteropExceptions.ReservedDllOriginalPathMismatchFromDebugRepro(fileName); } @@ -387,47 +326,12 @@ private static List CopyHashedFilesToDirectory( // After validating that the file is unique and should be copied, we can safely do that. We move // this operation to ensure we don't accidentally end up with duplicated .dll-s in the debug repro. - File.Copy(assemblyPath, destinationPath, overwrite: true); + File.Copy(filePath, destinationPath, overwrite: true); token.ThrowIfCancellationRequested(); - originalPaths.Add(hashedName, assemblyPath); + originalPaths.Add(hashedName, filePath); return hashedName; } - - /// - /// Copies an input path map to a target directory, as a serialized JSON file. - /// - /// The input path map. - /// The target directory to copy the assemblies to. - /// The name to use for the file with the serialized path map. - private static void CopyPathMapToDirectory( - Dictionary pathMap, - string destinationDirectory, - string fileName) - { - // Create the .json file with the input path map - string jsonFilePath = Path.Combine(destinationDirectory, fileName); - - using Stream jsonStream = File.Create(jsonFilePath); - - // Serialize the path map to the target file - JsonSerializer.Serialize(jsonStream, pathMap, InteropGeneratorJsonSerializerContext.Default.DictionaryStringString); - } - - /// - /// Extracts an input path from a .zip archive entry. - /// - /// The input path map entry. - /// - /// The value is expected to have the content produced by calls to . - /// - private static Dictionary ExtractPathMap(ZipArchiveEntry pathMapEntry) - { - using Stream stream = pathMapEntry.Open(); - - // Load the mapping with all the original file paths for the included .dll-s - return JsonSerializer.Deserialize(stream, InteropGeneratorJsonSerializerContext.Default.DictionaryStringString)!; - } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs index 8e05a5797c..42760ba3c2 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs @@ -10,6 +10,9 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE; +using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.Extensions; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.Discovery; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Models; @@ -555,7 +558,7 @@ private static InteropReferences CreateDiscoveryInteropReferences(ModuleDefiniti AssemblyReference windowsRuntimeAssembly = new("WinRT.Runtime"u8, windowsRuntimeVersion) { // Set the public keys, as it's needed to ensure references compare as equals as expected - PublicKeyOrToken = InteropValues.CsWinRTPublicKeyData, + PublicKeyOrToken = WellKnownPublicKeys.CsWinRT, HasPublicKey = true }; diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs index 6817914a14..012efd323f 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs @@ -8,10 +8,12 @@ using System.Linq; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.Helpers; using WindowsRuntime.InteropGenerator.Builders; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Fixups; -using WindowsRuntime.InteropGenerator.Helpers; using WindowsRuntime.InteropGenerator.Models; using WindowsRuntime.InteropGenerator.References; using WindowsRuntime.InteropGenerator.Rewriters; diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs index f330be463d..7ec2f9c60c 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; using System.Threading; using ConsoleAppFramework; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.References; @@ -22,99 +23,29 @@ internal static partial class InteropGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - string responseFilePath = inputFilePath; - bool isUsingDebugRepro = false; - - // Load the debug repro to investigate with, if we have one - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - if (Path.GetExtension(Path.Normalize(inputFilePath)) == ".zip") - { - ConsoleApp.Log("Unpacking input 'cswinrtinteropgen' debug repro"); - - isUsingDebugRepro = true; - - // If we unpacked a debug repro, we'll also replace the input file - // path with the extracted response file from the input repro. - responseFilePath = UnpackDebugRepro(inputFilePath, token); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledInteropException("unpack-debug-repro", e); - } - - token.ThrowIfCancellationRequested(); - - InteropGeneratorArgs args; - - // Parse the actual arguments from the response file - try - { - args = InteropGeneratorArgs.ParseFromResponseFile(responseFilePath, token); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledInteropException("parsing", e); - } - - args.Token.ThrowIfCancellationRequested(); - - // Save a debug repro, if needed - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - // We also skip this if we're currently processing an input debug - // repro, as there would be no point in creating a new one from that. - if (args.DebugReproDirectory is not null && !isUsingDebugRepro) - { - ConsoleApp.Log("Saving 'cswinrtinteropgen' debug repro"); - - SaveDebugRepro(args); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledInteropException("save-debug-repro", e); - } - - args.Token.ThrowIfCancellationRequested(); - - InteropGeneratorDiscoveryState discoveryState; - - // Wrap the actual logic, to ensure that we're only ever throwing an exception that will result - // in either graceful cancellation, or a well formatted error message. The 'ConsoleApp' code is - // taking care of passing the exception 'ToString()' result to the output buffer, so we want all - // exceptions that can reach that path to have our custom formatting implementation there. - try - { - ConsoleApp.Log($"Processing {args.ReferenceAssemblyPaths.Length + args.ImplementationAssemblyPaths.Length + 1} module(s)"); - - discoveryState = Discover(args); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledInteropException("discovery", e); - } - - args.Token.ThrowIfCancellationRequested(); - - // Same thing for the emit phase - try - { - ConsoleApp.Log("Generating interop code"); - - Emit(args, discoveryState); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledInteropException("emit", e); - } + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + inputFilePath: inputFilePath, + toolName: "cswinrtinteropgen", + unpackDebugRepro: UnpackDebugRepro, + parseFromResponseFile: ResponseFileParser.Parse, + saveDebugRepro: SaveDebugRepro, + wrapUnhandled: static (phase, e) => new UnhandledInteropException(phase, e), + log: ConsoleApp.Log, + token: token); + + // Discover the types to process + InteropGeneratorDiscoveryState discoveryState = runner.RunPhase( + phaseName: "discovery", + logMessage: $"Processing {runner.Args.ReferenceAssemblyPaths.Length + runner.Args.ImplementationAssemblyPaths.Length + 1} module(s)", + body: Discover); + + // Emit the resulting interop assembly + runner.RunPhase( + phaseName: "emit", + logMessage: "Generating interop code", + body: args => Emit(args, discoveryState)); // Notify the user that generation was successful - ConsoleApp.Log($"Interop code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, InteropNames.WindowsRuntimeInteropDllName)}"); + ConsoleApp.Log($"Interop code generated -> {Path.Combine(runner.Args.GeneratedAssemblyDirectory, InteropNames.WindowsRuntimeInteropDllName)}"); } } \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.Formatting.cs b/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.Formatting.cs deleted file mode 100644 index 5fced287dc..0000000000 --- a/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.Formatting.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text; - -namespace WindowsRuntime.InteropGenerator.Generation; - -/// -internal partial class InteropGeneratorArgs -{ - /// - /// Formats the current instance into a response file text. - /// - /// The resulting response file text. - public string FormatToResponseFile() - { - StringBuilder builder = new(); - - _ = builder.Append(GetCommandLineArgumentName(nameof(ReferenceAssemblyPaths))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', ReferenceAssemblyPaths)); - - _ = builder.Append(GetCommandLineArgumentName(nameof(ImplementationAssemblyPaths))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', ImplementationAssemblyPaths)); - - _ = builder.Append(GetCommandLineArgumentName(nameof(OutputAssemblyPath))); - _ = builder.Append(' '); - _ = builder.AppendLine(OutputAssemblyPath); - - _ = builder.Append(GetCommandLineArgumentName(nameof(WinRTSdkProjectionAssemblyPath))); - _ = builder.Append(' '); - _ = builder.AppendLine(WinRTSdkProjectionAssemblyPath); - - if (WinRTSdkXamlProjectionAssemblyPath is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(WinRTSdkXamlProjectionAssemblyPath))); - _ = builder.Append(' '); - _ = builder.AppendLine(WinRTSdkXamlProjectionAssemblyPath); - } - - if (WinRTProjectionAssemblyPath is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(WinRTProjectionAssemblyPath))); - _ = builder.Append(' '); - _ = builder.AppendLine(WinRTProjectionAssemblyPath); - } - - if (WinRTComponentAssemblyPath is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(WinRTComponentAssemblyPath))); - _ = builder.Append(' '); - _ = builder.AppendLine(WinRTComponentAssemblyPath); - } - - _ = builder.Append(GetCommandLineArgumentName(nameof(GeneratedAssemblyDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(GeneratedAssemblyDirectory); - - _ = builder.Append(GetCommandLineArgumentName(nameof(UseWindowsUIXamlProjections))); - _ = builder.Append(' '); - _ = builder.AppendLine(UseWindowsUIXamlProjections.ToString()); - - _ = builder.Append(GetCommandLineArgumentName(nameof(ValidateWinRTRuntimeAssemblyVersion))); - _ = builder.Append(' '); - _ = builder.AppendLine(ValidateWinRTRuntimeAssemblyVersion.ToString()); - - _ = builder.Append(GetCommandLineArgumentName(nameof(ValidateWinRTRuntimeDllVersion2References))); - _ = builder.Append(' '); - _ = builder.AppendLine(ValidateWinRTRuntimeDllVersion2References.ToString()); - - _ = builder.Append(GetCommandLineArgumentName(nameof(EnableIncrementalGeneration))); - _ = builder.Append(' '); - _ = builder.AppendLine(EnableIncrementalGeneration.ToString()); - - _ = builder.Append(GetCommandLineArgumentName(nameof(TreatWarningsAsErrors))); - _ = builder.Append(' '); - _ = builder.AppendLine(TreatWarningsAsErrors.ToString()); - - _ = builder.Append(GetCommandLineArgumentName(nameof(MaxDegreesOfParallelism))); - _ = builder.Append(' '); - _ = builder.AppendLine(MaxDegreesOfParallelism.ToString()); - - if (DebugReproDirectory is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(DebugReproDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(DebugReproDirectory); - } - - return builder.ToString(); - } -} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.Parsing.cs b/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.Parsing.cs deleted file mode 100644 index 89e4559ac4..0000000000 --- a/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.Parsing.cs +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using WindowsRuntime.InteropGenerator.Attributes; -using WindowsRuntime.InteropGenerator.Errors; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.InteropGenerator.Generation; - -/// -internal partial class InteropGeneratorArgs -{ - /// - /// Parses an instance from a target response file. - /// - /// The path to the response file. - /// The token for the operation. - /// The resulting instance. - public static InteropGeneratorArgs ParseFromResponseFile(string path, CancellationToken token) - { - // If the path is a response file, it will start with the '@' character. - // This matches the default escaping 'ToolTask' uses for response files. - if (path is ['@', .. string escapedPath]) - { - path = escapedPath; - } - - string[] lines; - - // Read all lines in the response file (each line contains a single command line argument) - try - { - lines = File.ReadAllLines(path); - } - catch (Exception e) - { - throw WellKnownInteropExceptions.ResponseFileReadError(e); - } - - return ParseFromResponseFile(lines, token); - } - - /// - /// Parses an instance from a target response file. - /// - /// The stream to the response file. - /// The token for the operation. - /// The resulting instance. - public static InteropGeneratorArgs ParseFromResponseFile(Stream stream, CancellationToken token) - { - string[] responseArgs = File.ReadAllLines(stream); - - return ParseFromResponseFile(responseArgs, token); - } - - /// - /// Parses an instance from a target response file. - /// - /// The lines read from the response file. - /// The token for the operation. - /// The resulting instance. - private static InteropGeneratorArgs ParseFromResponseFile(string[] lines, CancellationToken token) - { - Dictionary argsMap = []; - - // Build a map with all the commands and their values - foreach (string line in lines) - { - string trimmedLine = line.Trim(); - - // Each line has the command line argument name followed by a space, and then the - // argument value. If there are no spaces on any given line, the file is malformed. - int indexOfSpace = trimmedLine.IndexOf(' '); - - if (indexOfSpace == -1) - { - throw WellKnownInteropExceptions.MalformedResponseFile(); - } - - // Now we can parse the actual command line argument name and value - string argumentName = trimmedLine.AsSpan()[..indexOfSpace].ToString(); - string argumentValue = trimmedLine.AsSpan()[(indexOfSpace + 1)..].TrimEnd().ToString(); - - // We should never have duplicate commands - if (!argsMap.TryAdd(argumentName, argumentValue)) - { - throw WellKnownInteropExceptions.MalformedResponseFile(); - } - } - - // Parse all commands to create the managed arguments to use - return new() - { - ReferenceAssemblyPaths = GetStringArrayArgument(argsMap, nameof(ReferenceAssemblyPaths)), - ImplementationAssemblyPaths = GetStringArrayArgument(argsMap, nameof(ImplementationAssemblyPaths)), - OutputAssemblyPath = GetStringArgument(argsMap, nameof(OutputAssemblyPath)), - WinRTSdkProjectionAssemblyPath = GetStringArgument(argsMap, nameof(WinRTSdkProjectionAssemblyPath)), - WinRTSdkXamlProjectionAssemblyPath = GetNullableStringArgument(argsMap, nameof(WinRTSdkXamlProjectionAssemblyPath)), - WinRTProjectionAssemblyPath = GetNullableStringArgument(argsMap, nameof(WinRTProjectionAssemblyPath)), - WinRTComponentAssemblyPath = GetNullableStringArgument(argsMap, nameof(WinRTComponentAssemblyPath)), - GeneratedAssemblyDirectory = GetStringArgument(argsMap, nameof(GeneratedAssemblyDirectory)), - UseWindowsUIXamlProjections = GetBooleanArgument(argsMap, nameof(UseWindowsUIXamlProjections)), - ValidateWinRTRuntimeAssemblyVersion = GetBooleanArgument(argsMap, nameof(ValidateWinRTRuntimeAssemblyVersion)), - ValidateWinRTRuntimeDllVersion2References = GetBooleanArgument(argsMap, nameof(ValidateWinRTRuntimeDllVersion2References)), - EnableIncrementalGeneration = GetBooleanArgument(argsMap, nameof(EnableIncrementalGeneration)), - TreatWarningsAsErrors = GetBooleanArgument(argsMap, nameof(TreatWarningsAsErrors)), - MaxDegreesOfParallelism = GetInt32Argument(argsMap, nameof(MaxDegreesOfParallelism)), - DebugReproDirectory = GetNullableStringArgument(argsMap, nameof(DebugReproDirectory)), - Token = token - }; - } - - /// - /// Gets the command line argument name for a property. - /// - /// The target property name. - /// The command line argument name for . - public static string GetCommandLineArgumentName(string propertyName) - { - try - { - return typeof(InteropGeneratorArgs).GetProperty(propertyName)!.GetCustomAttribute()!.Name; - } - catch (Exception e) - { - throw WellKnownInteropExceptions.ResponseFileArgumentParsingError(propertyName, e); - } - } - - /// - /// Parses a array argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string[] GetStringArrayArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - } - - throw WellKnownInteropExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string GetStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - throw WellKnownInteropExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a nullable (optional) argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string? GetNullableStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - return null; - } - - /// - /// Parses an argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static int GetInt32Argument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - if (int.TryParse(argumentValue, out int parsedValue)) - { - return parsedValue; - } - } - - throw WellKnownInteropExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static bool GetBooleanArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - if (bool.TryParse(argumentValue, out bool parsedValue)) - { - return parsedValue; - } - } - - throw WellKnownInteropExceptions.ResponseFileArgumentParsingError(propertyName); - } -} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.cs b/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.cs index 0384a4f8cd..baf3434fed 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.cs @@ -2,14 +2,15 @@ // Licensed under the MIT License. using System.Threading; -using WindowsRuntime.InteropGenerator.Attributes; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Attributes; namespace WindowsRuntime.InteropGenerator.Generation; /// /// Input parameters for . /// -internal sealed partial class InteropGeneratorArgs +internal sealed class InteropGeneratorArgs : IGeneratorArgs { /// Gets the input reference .dll paths. [CommandLineArgumentName("--reference-assembly-paths")] @@ -67,10 +68,11 @@ internal sealed partial class InteropGeneratorArgs [CommandLineArgumentName("--max-degrees-of-parallelism")] public required int MaxDegreesOfParallelism { get; init; } - /// Gets the token for the operation. + /// public required CancellationToken Token { get; init; } - /// Gets the directory to use to place the debug repro, if requested. + /// [CommandLineArgumentName("--debug-repro-directory")] public string? DebugReproDirectory { get; init; } -} \ No newline at end of file +} + diff --git a/src/WinRT.Interop.Generator/Helpers/InteropGeneratorJsonSerializerContext.cs b/src/WinRT.Interop.Generator/Helpers/InteropGeneratorJsonSerializerContext.cs deleted file mode 100644 index 2546cf4cf9..0000000000 --- a/src/WinRT.Interop.Generator/Helpers/InteropGeneratorJsonSerializerContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace WindowsRuntime.InteropGenerator.Helpers; - -/// -/// A for types used in the interop generator. -/// -[JsonSerializable(typeof(Dictionary))] -[JsonSourceGenerationOptions(WriteIndented = true)] -internal sealed partial class InteropGeneratorJsonSerializerContext : JsonSerializerContext; \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Helpers/MvidGenerator.cs b/src/WinRT.Interop.Generator/Helpers/MvidGenerator.cs deleted file mode 100644 index d738973660..0000000000 --- a/src/WinRT.Interop.Generator/Helpers/MvidGenerator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; - -namespace WindowsRuntime.InteropGenerator.Helpers; - -/// -/// A generator for MVIDs for .NET modules. -/// -internal static class MvidGenerator -{ - /// - /// Generates a deterministic MVID based on a set of input assemblies. - /// - /// The input paths of all assemblies being processed. - /// The resulting MVID. - public static Guid CreateMvid(params IEnumerable assemblyPaths) - { - using IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA1); - - // Process all input assemblies to compute the MVID - foreach (string assemblyPath in assemblyPaths.Order()) - { - using FileStream stream = File.OpenRead(assemblyPath); - - hasher.AppendData(stream); - } - - Span hash = stackalloc byte[SHA1.HashSizeInBytes]; - - // Write the final combined hash - _ = hasher.GetCurrentHash(hash); - - // Create the final MVID from the first 16 bytes of the hash - return new(hash[..16]); - } -} diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index fb8e570927..77c1e2ed9d 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -4,6 +4,7 @@ using System; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.Factories; #pragma warning disable IDE0032 diff --git a/src/WinRT.Interop.Generator/References/InteropValues.cs b/src/WinRT.Interop.Generator/References/InteropValues.cs deleted file mode 100644 index a74b9748d5..0000000000 --- a/src/WinRT.Interop.Generator/References/InteropValues.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace WindowsRuntime.InteropGenerator.References; - -/// -/// Well known interop values (constants). -/// -internal static class InteropValues -{ - /// - /// The public key for CsWinRT assemblies. - /// - public const string CsWinRTPublicKey = "0024000004800000940000000602000000240000525341310004000001000100653243C2EC7B83009293B0E798193A8BB10390B5A12A3F2D5C58282D7129DAD2D0C012755329C1A45173E1AC9B8B4C0A4E078230BDD5E8CE3932083B09892D828E4B18AF00CE746BCC994DAD06ACEC3E69793E75F8D2CC8E77F44668550CA9B83DD6482DA9AA4D2E8BCF2E1793CF84C495346B1B99EB99767FB41246672E7CF0"; - - /// - /// The public key for Windows SDK projection assemblies. - /// - public const string WindowsSdkProjectionPublicKey = "0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9"; - - /// - /// The public key data for CsWinRT assemblies. - /// - public static readonly byte[] CsWinRTPublicKeyData = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x65, 0x32, 0x43, 0xC2, 0xEC, 0x7B, 0x83, 0x00, 0x92, 0x93, 0xB0, 0xE7, 0x98, 0x19, 0x3A, 0x8B, 0xB1, 0x03, 0x90, 0xB5, 0xA1, 0x2A, 0x3F, 0x2D, 0x5C, 0x58, 0x28, 0x2D, 0x71, 0x29, 0xDA, 0xD2, 0xD0, 0xC0, 0x12, 0x75, 0x53, 0x29, 0xC1, 0xA4, 0x51, 0x73, 0xE1, 0xAC, 0x9B, 0x8B, 0x4C, 0x0A, 0x4E, 0x07, 0x82, 0x30, 0xBD, 0xD5, 0xE8, 0xCE, 0x39, 0x32, 0x08, 0x3B, 0x09, 0x89, 0x2D, 0x82, 0x8E, 0x4B, 0x18, 0xAF, 0x00, 0xCE, 0x74, 0x6B, 0xCC, 0x99, 0x4D, 0xAD, 0x06, 0xAC, 0xEC, 0x3E, 0x69, 0x79, 0x3E, 0x75, 0xF8, 0xD2, 0xCC, 0x8E, 0x77, 0xF4, 0x46, 0x68, 0x55, 0x0C, 0xA9, 0xB8, 0x3D, 0xD6, 0x48, 0x2D, 0xA9, 0xAA, 0x4D, 0x2E, 0x8B, 0xCF, 0x2E, 0x17, 0x93, 0xCF, 0x84, 0xC4, 0x95, 0x34, 0x6B, 0x1B, 0x99, 0xEB, 0x99, 0x76, 0x7F, 0xB4, 0x12, 0x46, 0x67, 0x2E, 0x7C, 0xF0]; - - /// - /// The public key data for Windows SDK projection assemblies. - /// - public static readonly byte[] WindowsSdkProjectionPublicKeyData = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; -} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs index 4d56b5bdfd..fafda0e0fb 100644 --- a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs +++ b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs @@ -65,7 +65,7 @@ internal static class WellKnownInterfaceIIDs /// Whether to use Windows.UI.Xaml projections. /// The instance to use. /// The for the get_IID_... method for . - /// + /// /// /// The types handled by this method should be kept in sync with /// and diff --git a/src/WinRT.Interop.Generator/WinRT.Interop.Generator.csproj b/src/WinRT.Interop.Generator/WinRT.Interop.Generator.csproj index 062188b98b..f3a7ff2e70 100644 --- a/src/WinRT.Interop.Generator/WinRT.Interop.Generator.csproj +++ b/src/WinRT.Interop.Generator/WinRT.Interop.Generator.csproj @@ -37,6 +37,13 @@ true win-$(BuildToolArch) true + + + $(NoWarn);IDE0028;IDE0370 @@ -76,6 +83,10 @@ false + + + + diff --git a/src/WinRT.Projection.Generator/Attributes/CommandLineArgumentNameAttribute.cs b/src/WinRT.Projection.Generator/Attributes/CommandLineArgumentNameAttribute.cs deleted file mode 100644 index 044139902a..0000000000 --- a/src/WinRT.Projection.Generator/Attributes/CommandLineArgumentNameAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace WindowsRuntime.ProjectionGenerator.Attributes; - -/// -/// An attribute indicating the name of a given command line argument. -/// -/// The command line argument name. -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -internal sealed class CommandLineArgumentNameAttribute(string name) : Attribute -{ - /// - /// Gets the command line argument name. - /// - public string Name { get; } = name; -} diff --git a/src/WinRT.Projection.Generator/Errors/UnhandledProjectionGeneratorException.cs b/src/WinRT.Projection.Generator/Errors/UnhandledProjectionGeneratorException.cs index de0df58c60..da7ea08ff4 100644 --- a/src/WinRT.Projection.Generator/Errors/UnhandledProjectionGeneratorException.cs +++ b/src/WinRT.Projection.Generator/Errors/UnhandledProjectionGeneratorException.cs @@ -2,37 +2,21 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ProjectionGenerator.Errors; /// /// An unhandled exception for the projection generator. /// -internal sealed class UnhandledProjectionGeneratorException : Exception +/// +internal sealed class UnhandledProjectionGeneratorException(string phase, Exception exception) + : UnhandledGeneratorException(phase, exception) { - /// - /// The phase that failed. - /// - private readonly string _phase; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The phase that failed. - /// The inner exception. - public UnhandledProjectionGeneratorException(string phase, Exception exception) - : base(null, exception) - { - _phase = phase; - } + /// + protected override string ErrorPrefix => WellKnownProjectionGeneratorExceptions.ErrorPrefix; /// - public override string ToString() - { - return - $"""error {WellKnownProjectionGeneratorExceptions.ErrorPrefix}9999: The CsWinRT projection generator failed with an unhandled exception """ + - $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the {_phase} phase. This might be due to an invalid """ + - $"""configuration in the current project, but the generator should still correctly identify that and fail gracefully. Please open an """ + - $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; - } + protected override string GeneratorName => "projection"; } + diff --git a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorException.cs b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorException.cs index 3b9bc0b690..64d32601ac 100644 --- a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorException.cs +++ b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorException.cs @@ -2,36 +2,13 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ProjectionGenerator.Errors; /// -/// A well known exceptions for the projection generator. +/// A well known exception for the projection generator. /// -internal sealed class WellKnownProjectionGeneratorException : Exception -{ - /// - /// Creates a new instance with the specified parameters. - /// - /// The id of the exception. - /// The exception message. - /// The inner exception. - public WellKnownProjectionGeneratorException(string id, string message, Exception? innerException) - : base(message, innerException) - { - Id = id; - } - - /// - /// Gets the id of the exception. - /// - public string Id { get; } - - /// - public override string ToString() - { - return InnerException is not null - ? $"""error {Id}: {Message} Inner exception: '{InnerException.GetType().Name}': '{InnerException.Message}'.""" - : $"""error {Id}: {Message}"""; - } -} +/// +internal sealed class WellKnownProjectionGeneratorException(string id, string message, Exception? innerException) + : WellKnownGeneratorException(id, message, innerException); diff --git a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs index 9358b975c1..4ac475f319 100644 --- a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs +++ b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs @@ -5,13 +5,14 @@ using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ProjectionGenerator.Errors; /// /// Well known exceptions for the projection generator. /// -internal static class WellKnownProjectionGeneratorExceptions +internal sealed class WellKnownProjectionGeneratorExceptions : IGeneratorErrorFactory { /// /// The prefix for all errors produced by this tool. @@ -19,27 +20,28 @@ internal static class WellKnownProjectionGeneratorExceptions public const string ErrorPrefix = "CSWINRTPROJECTIONGEN"; /// - /// Some exception was thrown when trying to read the response file. + /// Prevents external instantiation; this type is only used to dispatch through . /// + private WellKnownProjectionGeneratorExceptions() + { + } + + /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, "Failed to read the response file to run 'cswinrtprojectiongen'.", exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError, exception); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(2, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(2, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } - /// - /// The input response file is malformed. - /// + /// public static Exception MalformedResponseFile() { - return Exception(3, "The response file is malformed and contains invalid content."); + return Exception(3, WellKnownGeneratorMessages.MalformedResponseFile); } /// @@ -92,28 +94,22 @@ public static Exception CsWinRTProcessError(int exitCode, Exception exception) return Exception(8, $"The projection writer failed during source generation (exit code {exitCode}).", exception); } - /// - /// The debug repro directory does not exist. - /// + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { - return Exception(9, $"The debug repro directory '{path}' does not exist."); + return Exception(9, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static Exception DebugReproMissingFileEntryMapping(string path) { - return Exception(10, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(10, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static Exception DebugReproUnrecognizedFileEntry(string path) { - return Exception(11, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(11, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.Projection.Generator/Extensions/ProjectionGeneratorExceptionExtensions.cs b/src/WinRT.Projection.Generator/Extensions/ProjectionGeneratorExceptionExtensions.cs deleted file mode 100644 index 143693992b..0000000000 --- a/src/WinRT.Projection.Generator/Extensions/ProjectionGeneratorExceptionExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using WindowsRuntime.ProjectionGenerator.Errors; - -namespace WindowsRuntime.ProjectionGenerator; - -/// -/// Extensions for projection generator exceptions. -/// -internal static class ProjectionGeneratorExceptionExtensions -{ - extension(Exception exception) - { - /// - /// Gets a value indicating whether an exception is well known (and should therefore not be caught). - /// - public bool IsWellKnown => exception is OperationCanceledException or WellKnownProjectionGeneratorException; - } -} diff --git a/src/WinRT.Projection.Generator/Extensions/SignatureComparerExtensions.cs b/src/WinRT.Projection.Generator/Extensions/SignatureComparerExtensions.cs deleted file mode 100644 index 82670098d4..0000000000 --- a/src/WinRT.Projection.Generator/Extensions/SignatureComparerExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ProjectionGenerator; - -/// -/// Extensions for . -/// -internal static class SignatureComparerExtensions -{ -#pragma warning disable IDE0052 // TODO: remove this once Roslyn bug is fixed - /// - /// Backing field for . - /// - private static readonly SignatureComparer IgnoreVersion = new(SignatureComparisonFlags.VersionAgnostic); -#pragma warning restore IDE0052 - - extension(SignatureComparer comparer) - { - /// - /// An immutable default instance of , with . - /// - public static SignatureComparer IgnoreVersion => IgnoreVersion; - - /// - /// Creates an instance for a pair of values. - /// - /// The resulting instance. - public IEqualityComparer<(TypeSignature, TypeSignature)> MakeValueTupleComparer() - { - return new SignatureValueTupleComparer(comparer); - } - } -} - -/// -/// An for a pair of values. -/// -file sealed class SignatureValueTupleComparer : IEqualityComparer<(TypeSignature, TypeSignature)> -{ - /// - /// The wrapped instance used for comparison. - /// - private readonly SignatureComparer _comparer; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The instance to wrap. - public SignatureValueTupleComparer(SignatureComparer comparer) - { - _comparer = comparer; - } - - /// - public bool Equals((TypeSignature, TypeSignature) x, (TypeSignature, TypeSignature) y) - { - return _comparer.Equals(x.Item1, y.Item1) && _comparer.Equals(x.Item2, y.Item2); - } - - /// - public int GetHashCode((TypeSignature, TypeSignature) obj) - { - return HashCode.Combine(_comparer.GetHashCode(obj.Item1), _comparer.GetHashCode(obj.Item2)); - } -} diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.DebugRepro.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.DebugRepro.cs index 949fbc53bb..1e784160d3 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.DebugRepro.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.DebugRepro.cs @@ -6,13 +6,11 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; using System.Threading; -using WindowsRuntime.InteropGenerator; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.DebugRepro; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.ProjectionGenerator.Errors; -using WindowsRuntime.ProjectionGenerator.Helpers; using WindowsRuntime.ProjectionWriter.Helpers; #pragma warning disable IDE0008 @@ -62,11 +60,7 @@ internal static partial class ProjectionGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtprojectiongen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtprojectiongen"); token.ThrowIfCancellationRequested(); @@ -94,15 +88,15 @@ .. archive.Entries.Where(entry => // Parse the debug repro .rsp file using (Stream stream = responseFileEntry.Open()) { - args = ProjectionGeneratorArgs.ParseFromResponseFile(stream, token); + args = ResponseFileParser.Parse(stream, token); } token.ThrowIfCancellationRequested(); // Load the mappings with all the original file paths - Dictionary originalReferencePaths = ExtractPathMap(originalReferencePathsEntry); - Dictionary originalWinMDPaths = ExtractPathMap(originalWinMDPathsEntry); - Dictionary originalWindowsMetadataPaths = ExtractPathMap(originalWindowsMetadataPathsEntry); + Dictionary originalReferencePaths = DebugReproPacker.ExtractPathMap(originalReferencePathsEntry); + Dictionary originalWinMDPaths = DebugReproPacker.ExtractPathMap(originalWinMDPathsEntry); + Dictionary originalWindowsMetadataPaths = DebugReproPacker.ExtractPathMap(originalWindowsMetadataPathsEntry); token.ThrowIfCancellationRequested(); @@ -175,7 +169,7 @@ .. archive.Entries.Where(entry => // Prepare the .rsp file with all updated arguments. The 'WindowsMetadata' value points at the // bundled folder, which the writer scans recursively to pick up all the .winmd files it contains. - string rspText = new ProjectionGeneratorArgs + string rspText = ResponseFileBuilder.Format(new ProjectionGeneratorArgs { ReferenceAssemblyPaths = [.. referencePaths], GeneratedAssemblyDirectory = tempDirectory, @@ -188,7 +182,7 @@ .. archive.Entries.Where(entry => MaxDegreesOfParallelism = args.MaxDegreesOfParallelism, DebugReproDirectory = null, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtprojectiongen.rsp"); @@ -211,23 +205,15 @@ private static void SaveDebugRepro(ProjectionGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownProjectionGeneratorExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtprojectiongen", + archiveFileName: "projection-debug-repro.zip"); - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "projection-debug-repro.zip"); - - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtprojectiongen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string referenceDirectory = Path.Combine(tempDirectory, ReferenceSubfolder); string winmdDirectory = Path.Combine(tempDirectory, WinMDSubfolder); string windowsMetadataDirectory = Path.Combine(tempDirectory, WindowsMetadataSubfolder); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(referenceDirectory); _ = Directory.CreateDirectory(winmdDirectory); _ = Directory.CreateDirectory(windowsMetadataDirectory); @@ -239,8 +225,8 @@ private static void SaveDebugRepro(ProjectionGeneratorArgs args) // Add all reference and .winmd paths with hashed names to the respective subdirectories under the // temporary directory, and store them with the updated names in a list to use to build the .rsp file. - List updatedReferenceNames = CopyHashedFilesToDirectory(args.ReferenceAssemblyPaths, referenceDirectory, originalReferencePaths, args.Token); - List updatedWinMDNames = CopyHashedFilesToDirectory(args.WinMDPaths, winmdDirectory, originalWinMDPaths, args.Token); + List updatedReferenceNames = DebugReproPacker.CopyHashedFilesToDirectory(args.ReferenceAssemblyPaths, referenceDirectory, originalReferencePaths, args.Token); + List updatedWinMDNames = DebugReproPacker.CopyHashedFilesToDirectory(args.WinMDPaths, winmdDirectory, originalWinMDPaths, args.Token); args.Token.ThrowIfCancellationRequested(); @@ -267,14 +253,14 @@ private static void SaveDebugRepro(ProjectionGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); // Bundle the expanded Windows metadata files into the windows-metadata subdirectory - _ = CopyHashedFilesToDirectory([.. expandedWindowsMetadataPaths], windowsMetadataDirectory, originalWindowsMetadataPaths, args.Token); + _ = DebugReproPacker.CopyHashedFilesToDirectory([.. expandedWindowsMetadataPaths], windowsMetadataDirectory, originalWindowsMetadataPaths, args.Token); args.Token.ThrowIfCancellationRequested(); // Prepare the .rsp file with all updated arguments. The 'WindowsMetadata' value is just the // subfolder name (relative path); the replay run resolves it to an absolute path inside its // own temporary unpack directory, since the original 'DebugReproDirectory' may not exist there. - string rspText = new ProjectionGeneratorArgs + string rspText = ResponseFileBuilder.Format(new ProjectionGeneratorArgs { ReferenceAssemblyPaths = [.. updatedReferenceNames], GeneratedAssemblyDirectory = args.GeneratedAssemblyDirectory, @@ -287,7 +273,7 @@ private static void SaveDebugRepro(ProjectionGeneratorArgs args) MaxDegreesOfParallelism = args.MaxDegreesOfParallelism, DebugReproDirectory = args.DebugReproDirectory, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtprojectiongen.rsp"); @@ -297,104 +283,12 @@ private static void SaveDebugRepro(ProjectionGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); // Create the .json files with the path maps for each category - CopyPathMapToDirectory(originalReferencePaths, tempDirectory, ReferencePathMapFileName); - CopyPathMapToDirectory(originalWinMDPaths, tempDirectory, WinMDPathMapFileName); - CopyPathMapToDirectory(originalWindowsMetadataPaths, tempDirectory, WindowsMetadataPathMapFileName); + DebugReproPacker.CopyPathMapToDirectory(originalReferencePaths, tempDirectory, ReferencePathMapFileName); + DebugReproPacker.CopyPathMapToDirectory(originalWinMDPaths, tempDirectory, WinMDPathMapFileName); + DebugReproPacker.CopyPathMapToDirectory(originalWindowsMetadataPaths, tempDirectory, WindowsMetadataPathMapFileName); args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); - } - - /// - /// Generates a hashed filename by appending a hash of the original filename. - /// - /// The original file path. - /// The hashed filename. - private static string GetHashedFileName(string filePath) - { - string fileName = Path.GetFileName(Path.Normalize(filePath)); - byte[] utf8Data = Encoding.UTF8.GetBytes(filePath); - byte[] hashData = Shake128.HashData(utf8Data, outputLength: 16); - string hash = Convert.ToHexString(hashData); - - return $"{Path.GetFileNameWithoutExtension(fileName)}_{hash}{Path.GetExtension(fileName)}"; - } - - /// - /// Copies all specified files to a target folder, and returns the list of updated hashed filenames. - /// - /// The input file paths. - /// The target directory to copy the files to. - /// A dictionary to store the original paths of the copied files. - /// A cancellation token to monitor for cancellation requests. - /// The list of updated hashed filenames. - private static List CopyHashedFilesToDirectory( - string[] filePaths, - string destinationDirectory, - Dictionary originalPaths, - CancellationToken token) - { - List updatedNames = []; - - foreach (string filePath in filePaths) - { - token.ThrowIfCancellationRequested(); - - string hashedName = GetHashedFileName(filePath); - string destinationPath = Path.Combine(destinationDirectory, hashedName); - - File.Copy(filePath, destinationPath, overwrite: true); - - updatedNames.Add(hashedName); - originalPaths.Add(hashedName, filePath); - } - - return updatedNames; - } - - /// - /// Copies an input path map to a target directory, as a serialized JSON file. - /// - /// The input path map. - /// The target directory to copy the assemblies to. - /// The name to use for the file with the serialized path map. - private static void CopyPathMapToDirectory( - Dictionary pathMap, - string destinationDirectory, - string fileName) - { - // Create the .json file with the input path map - string jsonFilePath = Path.Combine(destinationDirectory, fileName); - - using Stream jsonStream = File.Create(jsonFilePath); - - // Serialize the path map to the target file - JsonSerializer.Serialize(jsonStream, pathMap, ProjectionGeneratorJsonSerializerContext.Default.DictionaryStringString); - } - - /// - /// Extracts an input path from a .zip archive entry. - /// - /// The input path map entry. - /// - /// The value is expected to have the content produced by calls to . - /// - private static Dictionary ExtractPathMap(ZipArchiveEntry pathMapEntry) - { - using Stream stream = pathMapEntry.Open(); - - // Load the mapping with all the original file paths for the included assemblies - return JsonSerializer.Deserialize(stream, ProjectionGeneratorJsonSerializerContext.Default.DictionaryStringString)!; + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } } diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Emit.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Emit.cs index 2743360763..506a93f8f8 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Emit.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Emit.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.IO; +using System.Runtime.InteropServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; +using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.References; using WindowsRuntime.ProjectionGenerator.Errors; namespace WindowsRuntime.ProjectionGenerator.Generation; @@ -16,11 +18,6 @@ namespace WindowsRuntime.ProjectionGenerator.Generation; /// internal partial class ProjectionGenerator { - /// - /// The public key for CsWinRT assemblies, used for delay signing projection DLLs. - /// - private static readonly ImmutableArray CsWinRTPublicKey = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; - /// /// Runs the emit logic for the generator. /// @@ -67,7 +64,7 @@ private static void Emit(ProjectionGeneratorArgs args, ProjectionGeneratorProces allowUnsafe: true, optimizationLevel: OptimizationLevel.Release, deterministic: true, - cryptoPublicKey: CsWinRTPublicKey, + cryptoPublicKey: ImmutableCollectionsMarshal.AsImmutableArray(WellKnownPublicKeys.WindowsSdkProjection), delaySign: true, generalDiagnosticOption: ReportDiagnostic.Info)); } diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs index bd576152ac..58e6fba9ee 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs @@ -7,6 +7,7 @@ using System.Linq; using AsmResolver; using AsmResolver.DotNet; +using WindowsRuntime.Generator.Errors; using WindowsRuntime.ProjectionGenerator.Errors; using WindowsRuntime.ProjectionWriter; using WindowsRuntime.ProjectionWriter.Helpers; @@ -66,7 +67,7 @@ private static void GenerateSources(ProjectionGeneratorProcessingState processin // Invoke the projection writer in-process via its public C# API. try { - global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run(processingState.WriterOptions); + ProjectionWriter.ProjectionWriter.Run(processingState.WriterOptions); } catch (Exception e) when (!e.IsWellKnown) { diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs index 019a2077eb..7bb3a9ffb1 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; using System.Threading; using ConsoleAppFramework; -using WindowsRuntime.InteropGenerator; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.ProjectionGenerator.Errors; namespace WindowsRuntime.ProjectionGenerator.Generation; @@ -22,90 +22,28 @@ internal static partial class ProjectionGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - string responseFilePath = inputFilePath; - bool isUsingDebugRepro = false; - - // Load the debug repro to investigate with, if we have one - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - if (Path.GetExtension(Path.Normalize(inputFilePath)) == ".zip") - { - ConsoleApp.Log("Unpacking input 'cswinrtprojectiongen' debug repro"); - - isUsingDebugRepro = true; - - // If we unpacked a debug repro, we'll also replace the input file - // path with the extracted response file from the input repro. - responseFilePath = UnpackDebugRepro(inputFilePath, token); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("unpack-debug-repro", e); - } - - token.ThrowIfCancellationRequested(); - - ProjectionGeneratorArgs args; - - // Parse the actual arguments from the response file - try - { - args = ProjectionGeneratorArgs.ParseFromResponseFile(responseFilePath, token); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("parsing", e); - } - - args.Token.ThrowIfCancellationRequested(); - - // Save a debug repro, if needed - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - // We also skip this if we're currently processing an input debug - // repro, as there would be no point in creating a new one from that. - if (args.DebugReproDirectory is not null && !isUsingDebugRepro) - { - ConsoleApp.Log("Saving 'cswinrtprojectiongen' debug repro"); - - SaveDebugRepro(args); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("save-debug-repro", e); - } - - args.Token.ThrowIfCancellationRequested(); - - ProjectionGeneratorProcessingState processingState; - - // Process all .winmd references - try - { - // Show the appropriate message to inform users of what this generator is doing, - // based on the input arguments. If we don't have precompiled projections, this - // tool might run up to 3 times during builds, so this helps make things clearer. - ConsoleApp.Log(args switch + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + inputFilePath: inputFilePath, + toolName: "cswinrtprojectiongen", + unpackDebugRepro: UnpackDebugRepro, + parseFromResponseFile: ResponseFileParser.Parse, + saveDebugRepro: SaveDebugRepro, + wrapUnhandled: static (phase, e) => new UnhandledProjectionGeneratorException(phase, e), + log: ConsoleApp.Log, + token: token); + + // Process all .winmd references. Show the appropriate message to inform users of what this + // generator is doing, based on the input arguments. If we don't have precompiled projections, + // this tool might run up to 3 times during builds, so this helps make things clearer. + ProjectionGeneratorProcessingState processingState = runner.RunPhase( + phaseName: "processing", + logMessage: runner.Args switch { { WindowsSdkOnly: true, WindowsUIXamlProjection: false } => "Processing Windows SDK .winmd references", { WindowsSdkOnly: true, WindowsUIXamlProjection: true } => "Processing 'Windows.UI.Xaml' .winmd references", - _ => $"Processing {args.WinMDPaths.Length} .winmd reference(s)" - }); - - processingState = ProcessReferences(args); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("processing", e); - } - - args.Token.ThrowIfCancellationRequested(); + _ => $"Processing {runner.Args.WinMDPaths.Length} .winmd reference(s)" + }, + body: ProcessReferences); // If no types were found to project (e.g., component mode with no component references), // skip the source generation and emit phases entirely (no .dll will be produced at all). @@ -115,32 +53,18 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) } // Invoke the projection writer (in-process) to generate the projection sources - try - { - ConsoleApp.Log("Generating projection code"); - - GenerateSources(processingState); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("source-generation", e); - } - - args.Token.ThrowIfCancellationRequested(); + runner.RunPhase( + phaseName: "source-generation", + logMessage: "Generating projection code", + body: _ => GenerateSources(processingState)); // Invoke Roslyn to compile the generated sources into 'WinRT.Projection.dll' - try - { - ConsoleApp.Log("Compiling projection code"); - - Emit(args, processingState); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("emit", e); - } + runner.RunPhase( + phaseName: "emit", + logMessage: "Compiling projection code", + body: args => Emit(args, processingState)); // Notify the user that generation was successful - ConsoleApp.Log($"Projection code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, args.AssemblyName)}.dll"); + ConsoleApp.Log($"Projection code generated -> {Path.Combine(runner.Args.GeneratedAssemblyDirectory, runner.Args.AssemblyName)}.dll"); } } \ No newline at end of file diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Formatting.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Formatting.cs deleted file mode 100644 index 1010ac57ef..0000000000 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Formatting.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text; - -namespace WindowsRuntime.ProjectionGenerator.Generation; - -/// -internal partial class ProjectionGeneratorArgs -{ - /// - /// Formats the current instance into a response file text. - /// - /// The resulting response file text. - public string FormatToResponseFile() - { - StringBuilder builder = new(); - - _ = builder.Append(GetCommandLineArgumentName(nameof(ReferenceAssemblyPaths))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', ReferenceAssemblyPaths)); - - _ = builder.Append(GetCommandLineArgumentName(nameof(GeneratedAssemblyDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(GeneratedAssemblyDirectory); - - _ = builder.Append(GetCommandLineArgumentName(nameof(WinMDPaths))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', WinMDPaths)); - - _ = builder.Append(GetCommandLineArgumentName(nameof(TargetFramework))); - _ = builder.Append(' '); - _ = builder.AppendLine(TargetFramework); - - _ = builder.Append(GetCommandLineArgumentName(nameof(WindowsMetadata))); - _ = builder.Append(' '); - _ = builder.AppendLine(WindowsMetadata); - - _ = builder.Append(GetCommandLineArgumentName(nameof(AssemblyName))); - _ = builder.Append(' '); - _ = builder.AppendLine(AssemblyName); - - if (WindowsSdkOnly) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(WindowsSdkOnly))); - _ = builder.Append(' '); - _ = builder.AppendLine(WindowsSdkOnly.ToString()); - } - - if (WindowsUIXamlProjection) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(WindowsUIXamlProjection))); - _ = builder.Append(' '); - _ = builder.AppendLine(WindowsUIXamlProjection.ToString()); - } - - _ = builder.Append(GetCommandLineArgumentName(nameof(MaxDegreesOfParallelism))); - _ = builder.Append(' '); - _ = builder.AppendLine(MaxDegreesOfParallelism.ToString()); - - if (DebugReproDirectory is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(DebugReproDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(DebugReproDirectory); - } - - return builder.ToString(); - } -} diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Parsing.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Parsing.cs deleted file mode 100644 index 2f5011d5e9..0000000000 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Parsing.cs +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using WindowsRuntime.InteropGenerator; -using WindowsRuntime.ProjectionGenerator.Attributes; -using WindowsRuntime.ProjectionGenerator.Errors; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.ProjectionGenerator.Generation; - -/// -internal partial class ProjectionGeneratorArgs -{ - /// - /// Parses an instance from a target response file. - /// - /// The path to the response file. - /// The token for the operation. - /// The resulting instance. - public static ProjectionGeneratorArgs ParseFromResponseFile(string path, CancellationToken token) - { - // If the path is a response file, it will start with the '@' character. - // This matches the default escaping 'ToolTask' uses for response files. - if (path is ['@', .. string escapedPath]) - { - path = escapedPath; - } - - string[] responseArgs; - - // Read all lines in the response file (each line contains a single command line argument) - try - { - responseArgs = File.ReadAllLines(path); - } - catch (Exception e) - { - throw WellKnownProjectionGeneratorExceptions.ResponseFileReadError(e); - } - - return ParseFromResponseFile(responseArgs, token); - } - - /// - /// Parses an instance from a target response file. - /// - /// The stream to the response file. - /// The token for the operation. - /// The resulting instance. - public static ProjectionGeneratorArgs ParseFromResponseFile(Stream stream, CancellationToken token) - { - string[] responseArgs = File.ReadAllLines(stream); - - return ParseFromResponseFile(responseArgs, token); - } - - /// - /// Parses an instance from the lines of a response file. - /// - /// The lines read from the response file. - /// The token for the operation. - /// The resulting instance. - private static ProjectionGeneratorArgs ParseFromResponseFile(string[] lines, CancellationToken token) - { - Dictionary argsMap = []; - - // Build a map with all the commands and their values - foreach (string line in lines) - { - string trimmedLine = line.Trim(); - - // Skip empty lines (the MSBuild ToolTask may emit blank lines). - if (trimmedLine.Length == 0) - { - continue; - } - - // Each line has the command line argument name followed by a space, and then the - // argument value. If there are no spaces on any given line, the file is malformed. - int indexOfSpace = trimmedLine.IndexOf(' '); - - if (indexOfSpace == -1) - { - throw WellKnownProjectionGeneratorExceptions.MalformedResponseFile(); - } - - // Now we can parse the actual command line argument name and value - string argumentName = trimmedLine.AsSpan()[..indexOfSpace].ToString(); - string argumentValue = trimmedLine.AsSpan()[(indexOfSpace + 1)..].TrimEnd().ToString(); - - // We should never have duplicate commands - if (!argsMap.TryAdd(argumentName, argumentValue)) - { - throw WellKnownProjectionGeneratorExceptions.MalformedResponseFile(); - } - } - - // Parse all commands to create the managed arguments to use - return new() - { - ReferenceAssemblyPaths = GetStringArrayArgument(argsMap, nameof(ReferenceAssemblyPaths)), - GeneratedAssemblyDirectory = GetStringArgument(argsMap, nameof(GeneratedAssemblyDirectory)), - WinMDPaths = GetStringArrayArgument(argsMap, nameof(WinMDPaths)), - TargetFramework = GetStringArgument(argsMap, nameof(TargetFramework)), - WindowsMetadata = GetStringArgument(argsMap, nameof(WindowsMetadata)), - AssemblyName = GetOptionalStringArgument(argsMap, nameof(AssemblyName), "WinRT.Projection"), - WindowsSdkOnly = GetOptionalBoolArgument(argsMap, nameof(WindowsSdkOnly)), - WindowsUIXamlProjection = GetOptionalBoolArgument(argsMap, nameof(WindowsUIXamlProjection)), - MaxDegreesOfParallelism = GetInt32Argument(argsMap, nameof(MaxDegreesOfParallelism)), - DebugReproDirectory = GetNullableStringArgument(argsMap, nameof(DebugReproDirectory)), - Token = token - }; - } - - /// - /// Gets the command line argument name for a property. - /// - /// The target property name. - /// The command line argument name for . - public static string GetCommandLineArgumentName(string propertyName) - { - try - { - return typeof(ProjectionGeneratorArgs).GetProperty(propertyName)!.GetCustomAttribute()!.Name; - } - catch (Exception e) - { - throw WellKnownProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName, e); - } - } - - /// - /// Parses a array argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string[] GetStringArrayArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - } - - throw WellKnownProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string GetStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - throw WellKnownProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses an optional argument, returning a default value if not present. - /// - /// The input map with raw arguments. - /// The target property name. - /// The default value to return if the argument is not present. - /// The resulting argument, or if not found. - private static string GetOptionalStringArgument(Dictionary argsMap, string propertyName, string defaultValue) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - return defaultValue; - } - - /// - /// Parses a nullable (optional) argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument, or if not present. - private static string? GetNullableStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - return null; - } - - /// - /// Parses an optional argument, returning false if not present. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument, or false if not found. - private static bool GetOptionalBoolArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return bool.TryParse(argumentValue, out bool result) && result; - } - - return false; - } - - /// - /// Parses an argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static int GetInt32Argument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - if (int.TryParse(argumentValue, out int parsedValue)) - { - return parsedValue; - } - } - - throw WellKnownProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName); - } -} diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.cs index 054ddea620..e01fb8d7fa 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.cs @@ -1,15 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.ComponentModel; using System.Threading; -using WindowsRuntime.ProjectionGenerator.Attributes; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Attributes; namespace WindowsRuntime.ProjectionGenerator.Generation; /// /// Input parameters for . /// -internal sealed partial class ProjectionGeneratorArgs +internal sealed class ProjectionGeneratorArgs : IGeneratorArgs { /// Gets the input .dll paths. [CommandLineArgumentName("--reference-assembly-paths")] @@ -33,6 +35,7 @@ internal sealed partial class ProjectionGeneratorArgs /// Gets the output assembly name. Defaults to 'WinRT.Projection'. [CommandLineArgumentName("--assembly-name")] + [DefaultValue("WinRT.Projection")] public string AssemblyName { get; init; } = "WinRT.Projection"; /// @@ -54,10 +57,11 @@ internal sealed partial class ProjectionGeneratorArgs [CommandLineArgumentName("--max-degrees-of-parallelism")] public required int MaxDegreesOfParallelism { get; init; } - /// Gets the token for the operation. + /// public required CancellationToken Token { get; init; } - /// Gets the directory to use to place the debug repro, if requested. + /// [CommandLineArgumentName("--debug-repro-directory")] public string? DebugReproDirectory { get; init; } -} \ No newline at end of file +} + diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs index fd1ede400c..b4c09a58bb 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs @@ -10,7 +10,7 @@ namespace WindowsRuntime.ProjectionGenerator.Generation; /// /// The path to the folder where sources will be generated. /// The reference assembly paths excluding projection assemblies. -/// The options to pass to . +/// The options to pass to . /// Whether any types were found to project. internal sealed class ProjectionGeneratorProcessingState( string sourcesFolder, @@ -29,7 +29,7 @@ internal sealed class ProjectionGeneratorProcessingState( public string[] ReferencesWithoutProjections { get; } = referencesWithoutProjections; /// - /// Gets the options used to invoke . + /// Gets the options used to invoke . /// public ProjectionWriterOptions WriterOptions { get; } = writerOptions; diff --git a/src/WinRT.Projection.Generator/Helpers/ProjectionGeneratorJsonSerializerContext.cs b/src/WinRT.Projection.Generator/Helpers/ProjectionGeneratorJsonSerializerContext.cs deleted file mode 100644 index a8c089657e..0000000000 --- a/src/WinRT.Projection.Generator/Helpers/ProjectionGeneratorJsonSerializerContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace WindowsRuntime.ProjectionGenerator.Helpers; - -/// -/// A for types used in the projection generator. -/// -[JsonSerializable(typeof(Dictionary))] -[JsonSourceGenerationOptions(WriteIndented = true)] -internal sealed partial class ProjectionGeneratorJsonSerializerContext : JsonSerializerContext; diff --git a/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj b/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj index a9f22755d4..0068c222ce 100644 --- a/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj +++ b/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj @@ -54,11 +54,7 @@ - - - - - + diff --git a/src/WinRT.Projection.Ref.Generator/Attributes/CommandLineArgumentNameAttribute.cs b/src/WinRT.Projection.Ref.Generator/Attributes/CommandLineArgumentNameAttribute.cs deleted file mode 100644 index d6f296dfcc..0000000000 --- a/src/WinRT.Projection.Ref.Generator/Attributes/CommandLineArgumentNameAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace WindowsRuntime.ReferenceProjectionGenerator.Attributes; - -/// -/// An attribute indicating the name of a given command line argument. -/// -/// The command line argument name. -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -internal sealed class CommandLineArgumentNameAttribute(string name) : Attribute -{ - /// - /// Gets the command line argument name. - /// - public string Name { get; } = name; -} diff --git a/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs b/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs index 731e2a129b..1bf7f1f132 100644 --- a/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs +++ b/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs @@ -2,37 +2,21 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ReferenceProjectionGenerator.Errors; /// /// An unhandled exception for the reference projection generator. /// -internal sealed class UnhandledReferenceProjectionGeneratorException : Exception +/// +internal sealed class UnhandledReferenceProjectionGeneratorException(string phase, Exception exception) + : UnhandledGeneratorException(phase, exception) { - /// - /// The phase that failed. - /// - private readonly string _phase; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The phase that failed. - /// The inner exception. - public UnhandledReferenceProjectionGeneratorException(string phase, Exception exception) - : base(null, exception) - { - _phase = phase; - } + /// + protected override string ErrorPrefix => WellKnownReferenceProjectionGeneratorExceptions.ErrorPrefix; /// - public override string ToString() - { - return - $"""error {WellKnownReferenceProjectionGeneratorExceptions.ErrorPrefix}9999: The CsWinRT reference projection generator failed with an unhandled exception """ + - $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the {_phase} phase. This might be due to an invalid """ + - $"""configuration in the current project, but the generator should still correctly identify that and fail gracefully. Please open an """ + - $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; - } + protected override string GeneratorName => "reference projection"; } + diff --git a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorException.cs b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorException.cs index 72f93c8799..8f66cfa80f 100644 --- a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorException.cs +++ b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorException.cs @@ -2,36 +2,13 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ReferenceProjectionGenerator.Errors; /// /// A well known exception for the reference projection generator. /// -internal sealed class WellKnownReferenceProjectionGeneratorException : Exception -{ - /// - /// Creates a new instance with the specified parameters. - /// - /// The id of the exception. - /// The exception message. - /// The inner exception. - public WellKnownReferenceProjectionGeneratorException(string id, string message, Exception? innerException) - : base(message, innerException) - { - Id = id; - } - - /// - /// Gets the id of the exception. - /// - public string Id { get; } - - /// - public override string ToString() - { - return InnerException is not null - ? $"""error {Id}: {Message} Inner exception: '{InnerException.GetType().Name}': '{InnerException.Message}'.""" - : $"""error {Id}: {Message}"""; - } -} +/// +internal sealed class WellKnownReferenceProjectionGeneratorException(string id, string message, Exception? innerException) + : WellKnownGeneratorException(id, message, innerException); diff --git a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs index 0d9dff9399..aa3d8014d6 100644 --- a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs +++ b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.ReferenceProjectionGenerator.Errors; /// /// Well known exceptions for the reference projection generator. /// -internal static class WellKnownReferenceProjectionGeneratorExceptions +internal sealed class WellKnownReferenceProjectionGeneratorExceptions : IGeneratorErrorFactory { /// /// The prefix for all errors produced by this tool. @@ -16,27 +17,28 @@ internal static class WellKnownReferenceProjectionGeneratorExceptions public const string ErrorPrefix = "CSWINRTPROJECTIONREFGEN"; /// - /// Some exception was thrown when trying to read the response file. + /// Prevents external instantiation; this type is only used to dispatch through . /// + private WellKnownReferenceProjectionGeneratorExceptions() + { + } + + /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, "Failed to read the response file to run 'cswinrtprojectionrefgen'.", exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError, exception); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(2, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(2, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } - /// - /// The input response file is malformed. - /// + /// public static Exception MalformedResponseFile() { - return Exception(3, "The response file is malformed and contains invalid content."); + return Exception(3, WellKnownGeneratorMessages.MalformedResponseFile); } /// @@ -55,28 +57,22 @@ public static Exception CsWinRTProcessError(Exception exception) return Exception(5, "The projection writer failed during source generation.", exception); } - /// - /// The debug repro directory does not exist. - /// + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { - return Exception(6, $"The debug repro directory '{path}' does not exist."); + return Exception(6, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static Exception DebugReproMissingFileEntryMapping(string path) { - return Exception(7, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(7, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static Exception DebugReproUnrecognizedFileEntry(string path) { - return Exception(8, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(8, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.Projection.Ref.Generator/Extensions/ReferenceProjectionGeneratorExceptionExtensions.cs b/src/WinRT.Projection.Ref.Generator/Extensions/ReferenceProjectionGeneratorExceptionExtensions.cs deleted file mode 100644 index a0a981c6c3..0000000000 --- a/src/WinRT.Projection.Ref.Generator/Extensions/ReferenceProjectionGeneratorExceptionExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using WindowsRuntime.ReferenceProjectionGenerator.Errors; - -namespace WindowsRuntime.ReferenceProjectionGenerator; - -/// -/// Extensions for reference projection generator exceptions. -/// -internal static class ReferenceProjectionGeneratorExceptionExtensions -{ - extension(Exception exception) - { - /// - /// Gets a value indicating whether an exception is well known (and should therefore not be caught). - /// - public bool IsWellKnown => exception is OperationCanceledException or WellKnownReferenceProjectionGeneratorException; - } -} diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.DebugRepro.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.DebugRepro.cs index d3d507f95d..1c08ac14e1 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.DebugRepro.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.DebugRepro.cs @@ -6,14 +6,12 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; using System.Threading; -using WindowsRuntime.InteropGenerator; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.DebugRepro; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.ProjectionWriter.Helpers; using WindowsRuntime.ReferenceProjectionGenerator.Errors; -using WindowsRuntime.ReferenceProjectionGenerator.Helpers; #pragma warning disable IDE0008 @@ -35,11 +33,7 @@ internal static partial class ReferenceProjectionGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtprojectionrefgen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtprojectionrefgen"); token.ThrowIfCancellationRequested(); @@ -57,13 +51,13 @@ private static string UnpackDebugRepro(string path, CancellationToken token) // Parse the debug repro .rsp file using (Stream stream = responseFileEntry.Open()) { - args = ReferenceProjectionGeneratorArgs.ParseFromResponseFile(stream, token); + args = ResponseFileParser.Parse(stream, token); } token.ThrowIfCancellationRequested(); // Load the mappings with all the original file paths for the input .winmd files - Dictionary originalInputPaths = ExtractPathMap(originalInputPathsEntry); + Dictionary originalInputPaths = DebugReproPacker.ExtractPathMap(originalInputPathsEntry); token.ThrowIfCancellationRequested(); @@ -110,7 +104,7 @@ private static string UnpackDebugRepro(string path, CancellationToken token) token.ThrowIfCancellationRequested(); // Prepare the .rsp file with all updated arguments - string rspText = new ReferenceProjectionGeneratorArgs + string rspText = ResponseFileBuilder.Format(new ReferenceProjectionGeneratorArgs { InputPaths = [.. inputPaths], OutputDirectory = tempDirectory, @@ -125,7 +119,7 @@ private static string UnpackDebugRepro(string path, CancellationToken token) ReferenceProjection = args.ReferenceProjection, DebugReproDirectory = null, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtprojectionrefgen.rsp"); @@ -148,21 +142,13 @@ private static void SaveDebugRepro(ReferenceProjectionGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownReferenceProjectionGeneratorExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtprojectionrefgen", + archiveFileName: "ref-projection-debug-repro.zip"); - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "ref-projection-debug-repro.zip"); - - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtprojectionrefgen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string inputDirectory = Path.Combine(tempDirectory, "input"); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(inputDirectory); // Expand all input paths (which may be file paths, directories to recursively scan, or @@ -183,12 +169,12 @@ private static void SaveDebugRepro(ReferenceProjectionGeneratorArgs args) // Add all input paths with hashed names to the input subdirectory under the temporary // directory, and store them with the updated names in a list to use to build the .rsp file. - List updatedInputNames = CopyHashedFilesToDirectory([.. expandedInputPaths], inputDirectory, originalInputPaths, args.Token); + List updatedInputNames = DebugReproPacker.CopyHashedFilesToDirectory([.. expandedInputPaths], inputDirectory, originalInputPaths, args.Token); args.Token.ThrowIfCancellationRequested(); // Prepare the .rsp file with all updated arguments - string rspText = new ReferenceProjectionGeneratorArgs + string rspText = ResponseFileBuilder.Format(new ReferenceProjectionGeneratorArgs { InputPaths = [.. updatedInputNames], OutputDirectory = args.OutputDirectory, @@ -203,7 +189,7 @@ private static void SaveDebugRepro(ReferenceProjectionGeneratorArgs args) ReferenceProjection = args.ReferenceProjection, DebugReproDirectory = args.DebugReproDirectory, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtprojectionrefgen.rsp"); @@ -213,102 +199,10 @@ private static void SaveDebugRepro(ReferenceProjectionGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); // Create the .json file with the input path map - CopyPathMapToDirectory(originalInputPaths, tempDirectory, InputPathMapFileName); + DebugReproPacker.CopyPathMapToDirectory(originalInputPaths, tempDirectory, InputPathMapFileName); args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); - } - - /// - /// Generates a hashed filename by appending a hash of the original filename. - /// - /// The original file path. - /// The hashed filename. - private static string GetHashedFileName(string filePath) - { - string fileName = Path.GetFileName(Path.Normalize(filePath)); - byte[] utf8Data = Encoding.UTF8.GetBytes(filePath); - byte[] hashData = Shake128.HashData(utf8Data, outputLength: 16); - string hash = Convert.ToHexString(hashData); - - return $"{Path.GetFileNameWithoutExtension(fileName)}_{hash}{Path.GetExtension(fileName)}"; - } - - /// - /// Copies all specified files to a target folder, and returns the list of updated hashed filenames. - /// - /// The input file paths. - /// The target directory to copy the files to. - /// A dictionary to store the original paths of the copied files. - /// A cancellation token to monitor for cancellation requests. - /// The list of updated hashed filenames. - private static List CopyHashedFilesToDirectory( - string[] filePaths, - string destinationDirectory, - Dictionary originalPaths, - CancellationToken token) - { - List updatedFileNames = []; - - foreach (string filePath in filePaths) - { - token.ThrowIfCancellationRequested(); - - string hashedName = GetHashedFileName(filePath); - string destinationPath = Path.Combine(destinationDirectory, hashedName); - - File.Copy(filePath, destinationPath, overwrite: true); - - updatedFileNames.Add(hashedName); - originalPaths.Add(hashedName, filePath); - } - - return updatedFileNames; - } - - /// - /// Copies an input path map to a target directory, as a serialized JSON file. - /// - /// The input path map. - /// The target directory to copy the assemblies to. - /// The name to use for the file with the serialized path map. - private static void CopyPathMapToDirectory( - Dictionary pathMap, - string destinationDirectory, - string fileName) - { - // Create the .json file with the input path map - string jsonFilePath = Path.Combine(destinationDirectory, fileName); - - using Stream jsonStream = File.Create(jsonFilePath); - - // Serialize the path map to the target file - JsonSerializer.Serialize(jsonStream, pathMap, ReferenceProjectionGeneratorJsonSerializerContext.Default.DictionaryStringString); - } - - /// - /// Extracts an input path from a .zip archive entry. - /// - /// The input path map entry. - /// - /// The value is expected to have the content produced by calls to . - /// - private static Dictionary ExtractPathMap(ZipArchiveEntry pathMapEntry) - { - using Stream stream = pathMapEntry.Open(); - - // Load the mapping with all the original file paths for the included files - return JsonSerializer.Deserialize(stream, ReferenceProjectionGeneratorJsonSerializerContext.Default.DictionaryStringString)!; + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } } diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs index 5156adf288..a242cbcd8e 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -6,7 +6,9 @@ using System.IO; using System.Threading; using ConsoleAppFramework; -using WindowsRuntime.InteropGenerator; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.ProjectionWriter; using WindowsRuntime.ProjectionWriter.Helpers; using WindowsRuntime.ReferenceProjectionGenerator.Errors; @@ -27,93 +29,35 @@ internal static partial class ReferenceProjectionGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - string responseFilePath = inputFilePath; - bool isUsingDebugRepro = false; - - // Load the debug repro to investigate with, if we have one - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - if (Path.GetExtension(Path.Normalize(inputFilePath)) == ".zip") - { - ConsoleApp.Log("Unpacking input 'cswinrtprojectionrefgen' debug repro"); - - isUsingDebugRepro = true; - - // If we unpacked a debug repro, we'll also replace the input file - // path with the extracted response file from the input repro. - responseFilePath = UnpackDebugRepro(inputFilePath, token); - } - } - catch (Exception e) when (!e.IsWellKnown) + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + inputFilePath: inputFilePath, + toolName: "cswinrtprojectionrefgen", + unpackDebugRepro: UnpackDebugRepro, + parseFromResponseFile: ResponseFileParser.Parse, + saveDebugRepro: SaveDebugRepro, + wrapUnhandled: static (phase, e) => new UnhandledReferenceProjectionGeneratorException(phase, e), + log: ConsoleApp.Log, + token: token); + + // Validate the target framework. CsWinRT 3.0 requires .NET 10 or later + if (!string.IsNullOrEmpty(runner.Args.TargetFramework) && !runner.Args.TargetFramework.StartsWith("net10.0", StringComparison.Ordinal)) { - throw new UnhandledReferenceProjectionGeneratorException("unpack-debug-repro", e); - } - - token.ThrowIfCancellationRequested(); - - ReferenceProjectionGeneratorArgs args; - - // Parse the actual arguments from the response file - try - { - args = ReferenceProjectionGeneratorArgs.ParseFromResponseFile(responseFilePath, token); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledReferenceProjectionGeneratorException("parsing", e); - } - - args.Token.ThrowIfCancellationRequested(); - - // Save a debug repro, if needed - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - // We also skip this if we're currently processing an input debug - // repro, as there would be no point in creating a new one from that. - if (args.DebugReproDirectory is not null && !isUsingDebugRepro) - { - ConsoleApp.Log("Saving 'cswinrtprojectionrefgen' debug repro"); - - SaveDebugRepro(args); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledReferenceProjectionGeneratorException("save-debug-repro", e); - } - - args.Token.ThrowIfCancellationRequested(); - - // Validate the target framework. CsWinRT 3.0 requires .NET 10 or later. - if (!string.IsNullOrEmpty(args.TargetFramework) && !args.TargetFramework.StartsWith("net10.0", StringComparison.Ordinal)) - { - throw WellKnownReferenceProjectionGeneratorExceptions.UnsupportedTargetFramework(args.TargetFramework); + throw WellKnownReferenceProjectionGeneratorExceptions.UnsupportedTargetFramework(runner.Args.TargetFramework); } // Build the writer options from the parsed arguments - ProjectionWriterOptions options; - - try - { - options = BuildWriterOptions(args); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledReferenceProjectionGeneratorException("processing", e); - } - - args.Token.ThrowIfCancellationRequested(); + ProjectionWriterOptions options = runner.RunPhase( + phaseName: "processing", + body: BuildWriterOptions); - // Invoke the projection writer (in-process) to generate the projection sources + // Invoke the projection writer (in-process) to generate the projection sources. We can't + // route this through the shared 'runner.RunPhase' helper because we wrap the exception + // into a well-known 'CsWinRTProcessError' rather than the per-tool 'Unhandled' factory. try { ConsoleApp.Log($"Generating reference projection sources -> {options.OutputFolder}"); - global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run(options); + ProjectionWriter.ProjectionWriter.Run(options); } catch (Exception e) when (!e.IsWellKnown) { diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Formatting.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Formatting.cs deleted file mode 100644 index f61338f8ef..0000000000 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Formatting.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text; - -namespace WindowsRuntime.ReferenceProjectionGenerator.Generation; - -/// -internal partial class ReferenceProjectionGeneratorArgs -{ - /// - /// Formats the current instance into a response file text. - /// - /// The resulting response file text. - public string FormatToResponseFile() - { - StringBuilder builder = new(); - - _ = builder.Append(GetCommandLineArgumentName(nameof(InputPaths))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', InputPaths)); - - _ = builder.Append(GetCommandLineArgumentName(nameof(OutputDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(OutputDirectory); - - _ = builder.Append(GetCommandLineArgumentName(nameof(TargetFramework))); - _ = builder.Append(' '); - _ = builder.AppendLine(TargetFramework); - - if (IncludeNamespaces.Length > 0) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(IncludeNamespaces))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', IncludeNamespaces)); - } - - if (ExcludeNamespaces.Length > 0) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(ExcludeNamespaces))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', ExcludeNamespaces)); - } - - if (AdditionExcludeNamespaces.Length > 0) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(AdditionExcludeNamespaces))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', AdditionExcludeNamespaces)); - } - - if (Verbose) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(Verbose))); - _ = builder.Append(' '); - _ = builder.AppendLine(Verbose.ToString()); - } - - if (Component) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(Component))); - _ = builder.Append(' '); - _ = builder.AppendLine(Component.ToString()); - } - - if (PublicExclusiveTo) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(PublicExclusiveTo))); - _ = builder.Append(' '); - _ = builder.AppendLine(PublicExclusiveTo.ToString()); - } - - if (IdicExclusiveTo) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(IdicExclusiveTo))); - _ = builder.Append(' '); - _ = builder.AppendLine(IdicExclusiveTo.ToString()); - } - - if (ReferenceProjection) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(ReferenceProjection))); - _ = builder.Append(' '); - _ = builder.AppendLine(ReferenceProjection.ToString()); - } - - if (DebugReproDirectory is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(DebugReproDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(DebugReproDirectory); - } - - return builder.ToString(); - } -} diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Parsing.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Parsing.cs deleted file mode 100644 index c3eebe8810..0000000000 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.Parsing.cs +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using WindowsRuntime.InteropGenerator; -using WindowsRuntime.ReferenceProjectionGenerator.Attributes; -using WindowsRuntime.ReferenceProjectionGenerator.Errors; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.ReferenceProjectionGenerator.Generation; - -/// -internal partial class ReferenceProjectionGeneratorArgs -{ - /// - /// Parses an instance from a target response file. - /// - /// The path to the response file. - /// The token for the operation. - /// The resulting instance. - public static ReferenceProjectionGeneratorArgs ParseFromResponseFile(string path, CancellationToken token) - { - // If the path is a response file, it will start with the '@' character. - // This matches the default escaping 'ToolTask' uses for response files. - if (path is ['@', .. string escapedPath]) - { - path = escapedPath; - } - - string[] responseArgs; - - // Read all lines in the response file (each line contains a single command line argument) - try - { - responseArgs = File.ReadAllLines(path); - } - catch (Exception e) - { - throw WellKnownReferenceProjectionGeneratorExceptions.ResponseFileReadError(e); - } - - return ParseFromResponseFile(responseArgs, token); - } - - /// - /// Parses an instance from a target response file. - /// - /// The stream to the response file. - /// The token for the operation. - /// The resulting instance. - public static ReferenceProjectionGeneratorArgs ParseFromResponseFile(Stream stream, CancellationToken token) - { - string[] responseArgs = File.ReadAllLines(stream); - - return ParseFromResponseFile(responseArgs, token); - } - - /// - /// Parses an instance from the lines of a response file. - /// - /// The lines read from the response file. - /// The token for the operation. - /// The resulting instance. - private static ReferenceProjectionGeneratorArgs ParseFromResponseFile(string[] lines, CancellationToken token) - { - Dictionary argsMap = []; - - // Build a map with all the commands and their values - foreach (string line in lines) - { - string trimmedLine = line.Trim(); - - // Skip empty lines (the MSBuild ToolTask may emit blank lines). - if (trimmedLine.Length == 0) - { - continue; - } - - // Each line has the command line argument name followed by a space, and then the - // argument value. If there are no spaces on any given line, the file is malformed. - int indexOfSpace = trimmedLine.IndexOf(' '); - - if (indexOfSpace == -1) - { - throw WellKnownReferenceProjectionGeneratorExceptions.MalformedResponseFile(); - } - - // Now we can parse the actual command line argument name and value - string argumentName = trimmedLine.AsSpan()[..indexOfSpace].ToString(); - string argumentValue = trimmedLine.AsSpan()[(indexOfSpace + 1)..].TrimEnd().ToString(); - - // We should never have duplicate commands - if (!argsMap.TryAdd(argumentName, argumentValue)) - { - throw WellKnownReferenceProjectionGeneratorExceptions.MalformedResponseFile(); - } - } - - // Parse all commands to create the managed arguments to use - return new() - { - InputPaths = GetStringArrayArgument(argsMap, nameof(InputPaths)), - OutputDirectory = GetStringArgument(argsMap, nameof(OutputDirectory)), - TargetFramework = GetStringArgument(argsMap, nameof(TargetFramework)), - IncludeNamespaces = GetOptionalStringArrayArgument(argsMap, nameof(IncludeNamespaces)), - ExcludeNamespaces = GetOptionalStringArrayArgument(argsMap, nameof(ExcludeNamespaces)), - AdditionExcludeNamespaces = GetOptionalStringArrayArgument(argsMap, nameof(AdditionExcludeNamespaces)), - Verbose = GetOptionalBoolArgument(argsMap, nameof(Verbose)), - Component = GetOptionalBoolArgument(argsMap, nameof(Component)), - PublicExclusiveTo = GetOptionalBoolArgument(argsMap, nameof(PublicExclusiveTo)), - IdicExclusiveTo = GetOptionalBoolArgument(argsMap, nameof(IdicExclusiveTo)), - ReferenceProjection = GetOptionalBoolArgument(argsMap, nameof(ReferenceProjection)), - DebugReproDirectory = GetNullableStringArgument(argsMap, nameof(DebugReproDirectory)), - Token = token - }; - } - - /// - /// Gets the command line argument name for a property. - /// - /// The target property name. - /// The command line argument name for . - public static string GetCommandLineArgumentName(string propertyName) - { - try - { - return typeof(ReferenceProjectionGeneratorArgs).GetProperty(propertyName)!.GetCustomAttribute()!.Name; - } - catch (Exception e) - { - throw WellKnownReferenceProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName, e); - } - } - - /// - /// Parses a required array argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string[] GetStringArrayArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - } - - throw WellKnownReferenceProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses an optional array argument, returning an empty array if not present. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument, or an empty array if not found. - private static string[] GetOptionalStringArrayArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - } - - return []; - } - - /// - /// Parses a required argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string GetStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - throw WellKnownReferenceProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a nullable (optional) argument. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument. - private static string? GetNullableStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - return null; - } - - /// - /// Parses an optional argument, returning false if not present. - /// - /// The input map with raw arguments. - /// The target property name. - /// The resulting argument, or false if not found. - private static bool GetOptionalBoolArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return bool.TryParse(argumentValue, out bool result) && result; - } - - return false; - } -} diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.cs index 53f6dedea8..508ed77844 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGeneratorArgs.cs @@ -2,14 +2,15 @@ // Licensed under the MIT License. using System.Threading; -using WindowsRuntime.ReferenceProjectionGenerator.Attributes; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Attributes; namespace WindowsRuntime.ReferenceProjectionGenerator.Generation; /// /// Input parameters for . /// -internal sealed partial class ReferenceProjectionGeneratorArgs +internal sealed class ReferenceProjectionGeneratorArgs : IGeneratorArgs { /// Gets the input .winmd paths (files, directories to recursively scan, or special /// tokens like "local", "sdk", "sdk+", or a version like "10.0.26100.0"). @@ -56,10 +57,12 @@ internal sealed partial class ReferenceProjectionGeneratorArgs [CommandLineArgumentName("--reference-projection")] public bool ReferenceProjection { get; init; } - /// Gets the token for the operation. + /// public required CancellationToken Token { get; init; } - /// Gets the directory to use to place the debug repro, if requested. + /// [CommandLineArgumentName("--debug-repro-directory")] public string? DebugReproDirectory { get; init; } } + + diff --git a/src/WinRT.Projection.Ref.Generator/Helpers/ReferenceProjectionGeneratorJsonSerializerContext.cs b/src/WinRT.Projection.Ref.Generator/Helpers/ReferenceProjectionGeneratorJsonSerializerContext.cs deleted file mode 100644 index 768cd91ab4..0000000000 --- a/src/WinRT.Projection.Ref.Generator/Helpers/ReferenceProjectionGeneratorJsonSerializerContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace WindowsRuntime.ReferenceProjectionGenerator.Helpers; - -/// -/// A for types used in the reference projection generator. -/// -[JsonSerializable(typeof(Dictionary))] -[JsonSourceGenerationOptions(WriteIndented = true)] -internal sealed partial class ReferenceProjectionGeneratorJsonSerializerContext : JsonSerializerContext; diff --git a/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj b/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj index ad2bd77ed0..6f68a999b9 100644 --- a/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj +++ b/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj @@ -51,11 +51,7 @@ - - - - - + diff --git a/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs b/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs index 06a163f49d..b86a3c1360 100644 --- a/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs +++ b/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs @@ -1106,7 +1106,7 @@ public static ArgumentException GetInvalidSeekOriginException(string? paramName) extension(UnauthorizedAccessException) { /// - /// Throws an indicating that the internal buffer of a cannot be accessed. + /// Throws an indicating that the internal buffer of a cannot be accessed. /// /// Always thrown. [DoesNotReturn] diff --git a/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs b/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs deleted file mode 100644 index 78ff10de64..0000000000 --- a/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; - -namespace WindowsRuntime.WinMDGenerator.Attributes; - -/// -/// An attribute indicating the name of a given command line argument. -/// -/// The command line argument name. -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] -internal sealed class CommandLineArgumentNameAttribute(string name) : Attribute -{ - /// - /// Gets the command line argument name. - /// - public string Name { get; } = name; -} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs b/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs index 96e3700bd3..9002ee8c21 100644 --- a/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs +++ b/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs @@ -2,37 +2,20 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.WinMDGenerator.Errors; /// /// An unhandled exception for the WinMD generator. /// -internal sealed class UnhandledWinMDException : Exception +/// +internal sealed class UnhandledWinMDException(string phase, Exception exception) + : UnhandledGeneratorException(phase, exception) { - /// - /// The phase that failed. - /// - private readonly string _phase; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The phase that failed. - /// The inner exception. - public UnhandledWinMDException(string phase, Exception exception) - : base(null, exception) - { - _phase = phase; - } + /// + protected override string ErrorPrefix => WellKnownWinMDExceptions.ErrorPrefix; /// - public override string ToString() - { - return - $"""error {WellKnownWinMDExceptions.ErrorPrefix}9999: The CsWinRT WinMD generator failed with an unhandled exception """ + - $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the {_phase} phase. This might be due to an invalid """ + - $"""configuration in the current project, but the generator should still correctly identify that and fail gracefully. Please open an """ + - $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; - } -} \ No newline at end of file + protected override string GeneratorName => "WinMD"; +} diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDException.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDException.cs index 16ba08edcc..2ccce2e233 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDException.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDException.cs @@ -2,36 +2,13 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.WinMDGenerator.Errors; /// /// A well known exception for the WinMD generator. /// -internal sealed class WellKnownWinMDException : Exception -{ - /// - /// Creates a new instance with the specified parameters. - /// - /// The id of the exception. - /// The exception message. - /// The inner exception. - public WellKnownWinMDException(string id, string message, Exception? innerException) - : base(message, innerException) - { - Id = id; - } - - /// - /// Gets the id of the exception. - /// - public string Id { get; } - - /// - public override string ToString() - { - return InnerException is not null - ? $"""error {Id}: {Message} Inner exception: '{InnerException.GetType().Name}': '{InnerException.Message}'.""" - : $"""error {Id}: {Message}"""; - } -} \ No newline at end of file +/// +internal sealed class WellKnownWinMDException(string id, string message, Exception? innerException) + : WellKnownGeneratorException(id, message, innerException); diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs index 83766d01cb..d958c0c7a9 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using System; +using WindowsRuntime.Generator.Errors; namespace WindowsRuntime.WinMDGenerator.Errors; /// /// Well-known exceptions for the WinMD generator. /// -internal static class WellKnownWinMDExceptions +internal sealed class WellKnownWinMDExceptions : IGeneratorErrorFactory { /// /// The prefix for all errors produced by this tool. @@ -16,27 +17,28 @@ internal static class WellKnownWinMDExceptions public const string ErrorPrefix = "CSWINRTWINMDGEN"; /// - /// Some exception was thrown when trying to read the response file. + /// Prevents external instantiation; this type is only used to dispatch through . /// + private WellKnownWinMDExceptions() + { + } + + /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, "Failed to read the response file to run 'cswinrtwinmdgen'.", exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError, exception); } - /// - /// The input response file is malformed. - /// + /// public static Exception MalformedResponseFile() { - return Exception(2, "The response file is malformed and contains invalid content."); + return Exception(2, WellKnownGeneratorMessages.MalformedResponseFile); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(3, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(3, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } /// @@ -71,28 +73,22 @@ public static Exception InputAssemblyRuntimeVersionNotFound(string path) return Exception(7, $"Failed to probe the .NET runtime version from the input assembly '{path}'."); } - /// - /// The debug repro directory does not exist. - /// + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { - return Exception(8, $"The debug repro directory '{path}' does not exist."); + return Exception(8, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static Exception DebugReproMissingFileEntryMapping(string path) { - return Exception(9, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(9, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static Exception DebugReproUnrecognizedFileEntry(string path) { - return Exception(10, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(10, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs b/src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs deleted file mode 100644 index c376c49110..0000000000 --- a/src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using WindowsRuntime.WinMDGenerator.Errors; - -namespace WindowsRuntime.WinMDGenerator; - -/// -/// Extensions for WinMD generator exceptions. -/// -internal static class WinMDExceptionExtensions -{ - extension(Exception exception) - { - /// - /// Gets a value indicating whether an exception is well known (and should therefore not be caught). - /// - public bool IsWellKnown => exception is OperationCanceledException or WellKnownWinMDException; - } -} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.DebugRepro.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.DebugRepro.cs index d7e1814d1a..4f2ea5783e 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.DebugRepro.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.DebugRepro.cs @@ -3,17 +3,14 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; using System.Threading; -using WindowsRuntime.InteropGenerator; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.DebugRepro; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.WinMDGenerator.Errors; -using WindowsRuntime.WinMDGenerator.Helpers; #pragma warning disable IDE0008 @@ -35,11 +32,7 @@ internal static partial class WinMDGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtwinmdgen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtwinmdgen"); token.ThrowIfCancellationRequested(); @@ -65,13 +58,13 @@ .. archive.Entries.Where(entry => // Parse the debug repro .rsp file using (Stream stream = responseFileEntry.Open()) { - args = WinMDGeneratorArgs.ParseFromResponseFile(stream, token); + args = ResponseFileParser.Parse(stream, token); } token.ThrowIfCancellationRequested(); // Load the mappings with all the original file paths for reference assemblies - Dictionary originalReferencePaths = ExtractPathMap(originalReferencePathsEntry); + Dictionary originalReferencePaths = DebugReproPacker.ExtractPathMap(originalReferencePathsEntry); token.ThrowIfCancellationRequested(); @@ -130,7 +123,7 @@ .. archive.Entries.Where(entry => string outputWinmdPath = Path.Combine(tempDirectory, originalOutputName); // Prepare the .rsp file with all updated arguments - string rspText = new WinMDGeneratorArgs + string rspText = ResponseFileBuilder.Format(new WinMDGeneratorArgs { InputAssemblyPath = inputAssemblyPath!, ReferenceAssemblyPaths = [.. referencePaths], @@ -139,7 +132,7 @@ .. archive.Entries.Where(entry => UseWindowsUIXamlProjections = args.UseWindowsUIXamlProjections, DebugReproDirectory = null, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtwinmdgen.rsp"); @@ -162,21 +155,13 @@ private static void SaveDebugRepro(WinMDGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownWinMDExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } - - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "winmd-debug-repro.zip"); + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtwinmdgen", + archiveFileName: "winmd-debug-repro.zip"); - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtwinmdgen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string referenceDirectory = Path.Combine(tempDirectory, "reference"); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(referenceDirectory); // Map with all the original paths @@ -184,17 +169,17 @@ private static void SaveDebugRepro(WinMDGeneratorArgs args) // Add all reference paths with hashed names to the reference subdirectory under the // temporary directory, and store them with the updated names in a list to use to build the .rsp file. - List updatedReferenceNames = CopyHashedFilesToDirectory(args.ReferenceAssemblyPaths, referenceDirectory, originalReferencePaths, args.Token); + List updatedReferenceNames = DebugReproPacker.CopyHashedFilesToDirectory(args.ReferenceAssemblyPaths, referenceDirectory, originalReferencePaths, args.Token); args.Token.ThrowIfCancellationRequested(); // Hash and copy the input assembly to the top level temporary directory - string inputAssemblyHashedName = CopyHashedFileToDirectory(args.InputAssemblyPath, tempDirectory, originalReferencePaths, args.Token); + string inputAssemblyHashedName = DebugReproPacker.CopyHashedFileToDirectory(args.InputAssemblyPath, tempDirectory, originalReferencePaths, args.Token); args.Token.ThrowIfCancellationRequested(); // Prepare the .rsp file with all updated arguments - string rspText = new WinMDGeneratorArgs + string rspText = ResponseFileBuilder.Format(new WinMDGeneratorArgs { InputAssemblyPath = inputAssemblyHashedName, ReferenceAssemblyPaths = [.. updatedReferenceNames], @@ -203,7 +188,7 @@ private static void SaveDebugRepro(WinMDGeneratorArgs args) UseWindowsUIXamlProjections = args.UseWindowsUIXamlProjections, DebugReproDirectory = args.DebugReproDirectory, Token = CancellationToken.None - }.FormatToResponseFile(); + }); // Create the actual .rsp file string rspFilePath = Path.Combine(tempDirectory, "cswinrtwinmdgen.rsp"); @@ -213,134 +198,10 @@ private static void SaveDebugRepro(WinMDGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); // Create the .json file with the reference path map - CopyPathMapToDirectory(originalReferencePaths, tempDirectory, ReferencePathMapFileName); + DebugReproPacker.CopyPathMapToDirectory(originalReferencePaths, tempDirectory, ReferencePathMapFileName); args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); - } - - /// - /// Generates a hashed filename by appending a hash of the original filename. - /// - /// The original file path. - /// The hashed filename. - private static string GetHashedFileName(string filePath) - { - string fileName = Path.GetFileName(Path.Normalize(filePath)); - byte[] utf8Data = Encoding.UTF8.GetBytes(filePath); - byte[] hashData = Shake128.HashData(utf8Data, outputLength: 16); - string hash = Convert.ToHexString(hashData); - - return $"{Path.GetFileNameWithoutExtension(fileName)}_{hash}{Path.GetExtension(fileName)}"; - } - - /// - /// Copies all specified assemblies to a target folder, and returns the list of updated hashed filenames. - /// - /// The input assembly paths. - /// The target directory to copy the assemblies to. - /// A dictionary to store the original paths of the copied assemblies. - /// A cancellation token to monitor for cancellation requests. - /// The list of updated hashed filenames. - private static List CopyHashedFilesToDirectory( - string[] assemblyPaths, - string destinationDirectory, - Dictionary originalPaths, - CancellationToken token) - { - List updatedNames = []; - - foreach (string assemblyPath in assemblyPaths) - { - token.ThrowIfCancellationRequested(); - - string hashedName = GetHashedFileName(assemblyPath); - string destinationPath = Path.Combine(destinationDirectory, hashedName); - - File.Copy(assemblyPath, destinationPath, overwrite: true); - - updatedNames.Add(hashedName); - originalPaths.Add(hashedName, assemblyPath); - } - - return updatedNames; - } - - /// - /// Copies a specified assembly to a target folder. - /// - /// The input assembly path. - /// The target directory to copy the assembly to. - /// A dictionary to store the original paths of the copied assemblies. - /// A cancellation token to monitor for cancellation requests. - /// The hashed filename. - [return: NotNullIfNotNull(nameof(assemblyPath))] - private static string? CopyHashedFileToDirectory( - string? assemblyPath, - string destinationDirectory, - Dictionary originalPaths, - CancellationToken token) - { - if (assemblyPath is null) - { - return null; - } - - string hashedName = GetHashedFileName(assemblyPath); - string destinationPath = Path.Combine(destinationDirectory, hashedName); - - File.Copy(assemblyPath, destinationPath, overwrite: true); - - token.ThrowIfCancellationRequested(); - - originalPaths.Add(hashedName, assemblyPath); - - return hashedName; - } - - /// - /// Copies an input path map to a target directory, as a serialized JSON file. - /// - /// The input path map. - /// The target directory to copy the assemblies to. - /// The name to use for the file with the serialized path map. - private static void CopyPathMapToDirectory( - Dictionary pathMap, - string destinationDirectory, - string fileName) - { - // Create the .json file with the input path map - string jsonFilePath = Path.Combine(destinationDirectory, fileName); - - using Stream jsonStream = File.Create(jsonFilePath); - - // Serialize the path map to the target file - JsonSerializer.Serialize(jsonStream, pathMap, WinMDGeneratorJsonSerializerContext.Default.DictionaryStringString); - } - - /// - /// Extracts an input path from a .zip archive entry. - /// - /// The input path map entry. - /// - /// The value is expected to have the content produced by calls to . - /// - private static Dictionary ExtractPathMap(ZipArchiveEntry pathMapEntry) - { - using Stream stream = pathMapEntry.Open(); - - // Load the mapping with all the original file paths for the included assemblies - return JsonSerializer.Deserialize(stream, WinMDGeneratorJsonSerializerContext.Default.DictionaryStringString)!; + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } } diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs index ea927ce18b..86ab36b8ec 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs @@ -6,6 +6,7 @@ using System.Linq; using AsmResolver.DotNet; using AsmResolver.PE; +using WindowsRuntime.Generator.Extensions; using WindowsRuntime.WinMDGenerator.Discovery; using WindowsRuntime.WinMDGenerator.Errors; diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs index 875770bde0..1f71951433 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.IO; using System.Threading; using ConsoleAppFramework; -using WindowsRuntime.InteropGenerator; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Parsing; using WindowsRuntime.WinMDGenerator.Errors; namespace WindowsRuntime.WinMDGenerator.Generation; @@ -24,7 +23,7 @@ namespace WindowsRuntime.WinMDGenerator.Generation; /// The generation process runs in three phases: /// /// -/// Parse: Read arguments from the response file via . +/// Parse: Read arguments from the response file via . /// Discover: Load the input assembly and discover public types via . /// Generate: Transform discovered types and write the WinMD file via . /// @@ -38,95 +37,28 @@ internal static partial class WinMDGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - string responseFilePath = inputFilePath; - bool isUsingDebugRepro = false; - - // Load the debug repro to investigate with, if we have one - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - if (Path.GetExtension(Path.Normalize(inputFilePath)) == ".zip") - { - ConsoleApp.Log("Unpacking input 'cswinrtwinmdgen' debug repro"); - - isUsingDebugRepro = true; - - // If we unpacked a debug repro, we'll also replace the input file - // path with the extracted response file from the input repro. - responseFilePath = UnpackDebugRepro(inputFilePath, token); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledWinMDException("unpack-debug-repro", e); - } - - token.ThrowIfCancellationRequested(); - - WinMDGeneratorArgs args; - - // Parse the arguments from the response file - try - { - args = WinMDGeneratorArgs.ParseFromResponseFile(responseFilePath, token); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledWinMDException("parsing", e); - } - - token.ThrowIfCancellationRequested(); - - // Save a debug repro, if needed - try - { - // If no debug repro directory was provided, we have nothing to do. - // This is fully expected, it just means no debug repro is needed. - // We also skip this if we're currently processing an input debug - // repro, as there would be no point in creating a new one from that. - if (args.DebugReproDirectory is not null && !isUsingDebugRepro) - { - ConsoleApp.Log("Saving 'cswinrtwinmdgen' debug repro"); - - SaveDebugRepro(args); - } - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledWinMDException("save-debug-repro", e); - } - - args.Token.ThrowIfCancellationRequested(); + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + inputFilePath: inputFilePath, + toolName: "cswinrtwinmdgen", + unpackDebugRepro: UnpackDebugRepro, + parseFromResponseFile: ResponseFileParser.Parse, + saveDebugRepro: SaveDebugRepro, + wrapUnhandled: static (phase, e) => new UnhandledWinMDException(phase, e), + log: ConsoleApp.Log, + token: token); // Discover the types to process - WinMDGeneratorDiscoveryState discoveryState; - - try - { - ConsoleApp.Log($"Processing assembly: '{System.IO.Path.GetFileName(args.InputAssemblyPath)}'"); - - discoveryState = Discover(args); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledWinMDException("discovery", e); - } - - token.ThrowIfCancellationRequested(); + WinMDGeneratorDiscoveryState discoveryState = runner.RunPhase( + phaseName: "discovery", + logMessage: $"Processing assembly: '{System.IO.Path.GetFileName(runner.Args.InputAssemblyPath)}'", + body: Discover); // Generate and write the .winmd file - try - { - ConsoleApp.Log($"Defining {discoveryState.PublicTypes.Count} authored type(s)"); - - Generate(args, discoveryState); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledWinMDException("generation", e); - } + runner.RunPhase( + phaseName: "generation", + logMessage: $"Defining {discoveryState.PublicTypes.Count} authored type(s)", + body: args => Generate(args, discoveryState)); - ConsoleApp.Log($"Windows Runtime assembly (.winmd) generated -> {args.OutputWinmdPath}"); + ConsoleApp.Log($"Windows Runtime assembly (.winmd) generated -> {runner.Args.OutputWinmdPath}"); } } \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Formatting.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Formatting.cs deleted file mode 100644 index ca487eec3e..0000000000 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Formatting.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text; - -namespace WindowsRuntime.WinMDGenerator.Generation; - -/// -internal partial class WinMDGeneratorArgs -{ - /// - /// Formats the current instance into a response file text. - /// - /// The resulting response file text. - public string FormatToResponseFile() - { - StringBuilder builder = new(); - - _ = builder.Append(GetCommandLineArgumentName(nameof(InputAssemblyPath))); - _ = builder.Append(' '); - _ = builder.AppendLine(InputAssemblyPath); - - _ = builder.Append(GetCommandLineArgumentName(nameof(ReferenceAssemblyPaths))); - _ = builder.Append(' '); - _ = builder.AppendLine(string.Join(',', ReferenceAssemblyPaths)); - - _ = builder.Append(GetCommandLineArgumentName(nameof(OutputWinmdPath))); - _ = builder.Append(' '); - _ = builder.AppendLine(OutputWinmdPath); - - _ = builder.Append(GetCommandLineArgumentName(nameof(AssemblyVersion))); - _ = builder.Append(' '); - _ = builder.AppendLine(AssemblyVersion); - - _ = builder.Append(GetCommandLineArgumentName(nameof(UseWindowsUIXamlProjections))); - _ = builder.Append(' '); - _ = builder.AppendLine(UseWindowsUIXamlProjections.ToString()); - - if (DebugReproDirectory is not null) - { - _ = builder.Append(GetCommandLineArgumentName(nameof(DebugReproDirectory))); - _ = builder.Append(' '); - _ = builder.AppendLine(DebugReproDirectory); - } - - return builder.ToString(); - } -} diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs deleted file mode 100644 index 351029ab5a..0000000000 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using WindowsRuntime.InteropGenerator; -using WindowsRuntime.WinMDGenerator.Attributes; -using WindowsRuntime.WinMDGenerator.Errors; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.WinMDGenerator.Generation; - -/// -internal partial class WinMDGeneratorArgs -{ - /// - /// Parses a instance from a target response file. - /// - /// - /// The path may be prefixed with @ (matching the default escaping ToolTask uses - /// for response files), which is stripped before reading the file. - /// - /// The path to the response file (optionally prefixed with @). - /// The cancellation token for the operation. - /// The resulting instance. - public static WinMDGeneratorArgs ParseFromResponseFile(string path, CancellationToken token) - { - // If the path is a response file, it will start with the '@' character. - // This matches the default escaping 'ToolTask' uses for response files. - if (path is ['@', .. string escapedPath]) - { - path = escapedPath; - } - - string[] lines; - - // Read all lines in the response file (each line contains a single command line argument) - try - { - lines = File.ReadAllLines(path); - } - catch (Exception e) - { - throw WellKnownWinMDExceptions.ResponseFileReadError(e); - } - - return ParseFromResponseFile(lines, token); - } - - /// - /// Parses a instance from a target response file. - /// - /// The stream to the response file. - /// The token for the operation. - /// The resulting instance. - public static WinMDGeneratorArgs ParseFromResponseFile(Stream stream, CancellationToken token) - { - string[] responseArgs = File.ReadAllLines(stream); - - return ParseFromResponseFile(responseArgs, token); - } - - /// - /// Parses a instance from the lines of a response file. - /// - /// - /// Each line in the response file contains a single command-line argument in the form - /// --argument-name value. Lines are parsed into a key-value map, and each property - /// of is populated using its . - /// Duplicate argument names cause a malformed response file error. - /// - /// The lines read from the response file. - /// The cancellation token for the operation. - /// The resulting instance. - private static WinMDGeneratorArgs ParseFromResponseFile(string[] lines, CancellationToken token) - { - Dictionary argsMap = []; - - // Build a map with all the commands and their values - foreach (string line in lines) - { - string trimmedLine = line.Trim(); - - if (string.IsNullOrEmpty(trimmedLine)) - { - continue; - } - - // Each line has the command line argument name followed by a space, and then the - // argument value. If there are no spaces on any given line, the file is malformed. - int indexOfSpace = trimmedLine.IndexOf(' '); - - if (indexOfSpace == -1) - { - throw WellKnownWinMDExceptions.MalformedResponseFile(); - } - - // Now we can parse the actual command line argument name and value - string argumentName = trimmedLine.AsSpan()[..indexOfSpace].ToString(); - string argumentValue = trimmedLine.AsSpan()[(indexOfSpace + 1)..].TrimEnd().ToString(); - - // We should never have duplicate commands - if (!argsMap.TryAdd(argumentName, argumentValue)) - { - throw WellKnownWinMDExceptions.MalformedResponseFile(); - } - } - - // Parse all commands to create the managed arguments to use - return new() - { - InputAssemblyPath = GetStringArgument(argsMap, nameof(InputAssemblyPath)), - ReferenceAssemblyPaths = GetStringArrayArgument(argsMap, nameof(ReferenceAssemblyPaths)), - OutputWinmdPath = GetStringArgument(argsMap, nameof(OutputWinmdPath)), - AssemblyVersion = GetStringArgument(argsMap, nameof(AssemblyVersion)), - UseWindowsUIXamlProjections = GetBooleanArgument(argsMap, nameof(UseWindowsUIXamlProjections)), - DebugReproDirectory = GetNullableStringArgument(argsMap, nameof(DebugReproDirectory)), - Token = token - }; - } - - /// - /// Gets the command-line argument name for a property by reading its - /// . - /// - /// The target property name on . - /// The command-line argument name (e.g., "--input-assembly-path"). - public static string GetCommandLineArgumentName(string propertyName) - { - try - { - return typeof(WinMDGeneratorArgs).GetProperty(propertyName)!.GetCustomAttribute()!.Name; - } - catch (Exception e) - { - throw WellKnownWinMDExceptions.ResponseFileArgumentParsingError(propertyName, e); - } - } - - /// - /// Parses a comma-separated array argument from the arguments map. - /// - /// The parsed arguments map. - /// The property name to look up. - /// An array of trimmed, non-empty string values. - private static string[] GetStringArrayArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - } - - throw WellKnownWinMDExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a argument from the arguments map. - /// - /// The parsed arguments map. - /// The property name to look up. - /// The string value of the argument. - private static string GetStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - throw WellKnownWinMDExceptions.ResponseFileArgumentParsingError(propertyName); - } - - /// - /// Parses a nullable (optional) argument. - /// - /// The parsed arguments map. - /// The property name to look up. - /// The resulting argument, or if not present. - private static string? GetNullableStringArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - return argumentValue; - } - - return null; - } - - /// - /// Parses a argument from the arguments map. - /// - /// The parsed arguments map. - /// The property name to look up. - /// The parsed boolean value. - private static bool GetBooleanArgument(Dictionary argsMap, string propertyName) - { - if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) - { - if (bool.TryParse(argumentValue, out bool parsedValue)) - { - return parsedValue; - } - } - - throw WellKnownWinMDExceptions.ResponseFileArgumentParsingError(propertyName); - } -} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs index c686f116f0..07dcf7b211 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs @@ -2,14 +2,15 @@ // Licensed under the MIT License. using System.Threading; -using WindowsRuntime.WinMDGenerator.Attributes; +using WindowsRuntime.Generator; +using WindowsRuntime.Generator.Attributes; namespace WindowsRuntime.WinMDGenerator.Generation; /// /// Input parameters for . /// -internal sealed partial class WinMDGeneratorArgs +internal sealed class WinMDGeneratorArgs : IGeneratorArgs { /// Gets the path to the compiled input assembly (.dll) to analyze. [CommandLineArgumentName("--input-assembly-path")] @@ -31,10 +32,11 @@ internal sealed partial class WinMDGeneratorArgs [CommandLineArgumentName("--use-windows-ui-xaml-projections")] public required bool UseWindowsUIXamlProjections { get; init; } - /// Gets the token for the operation. + /// public required CancellationToken Token { get; init; } - /// Gets the directory to use to place the debug repro, if requested. + /// [CommandLineArgumentName("--debug-repro-directory")] public string? DebugReproDirectory { get; init; } -} \ No newline at end of file +} + diff --git a/src/WinRT.WinMD.Generator/Helpers/WinMDGeneratorJsonSerializerContext.cs b/src/WinRT.WinMD.Generator/Helpers/WinMDGeneratorJsonSerializerContext.cs deleted file mode 100644 index a6a1422219..0000000000 --- a/src/WinRT.WinMD.Generator/Helpers/WinMDGeneratorJsonSerializerContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace WindowsRuntime.WinMDGenerator.Helpers; - -/// -/// A for types used in the WinMD generator. -/// -[JsonSerializable(typeof(Dictionary))] -[JsonSourceGenerationOptions(WriteIndented = true)] -internal sealed partial class WinMDGeneratorJsonSerializerContext : JsonSerializerContext; diff --git a/src/WinRT.WinMD.Generator/References/WellKnownPublicKeyTokens.cs b/src/WinRT.WinMD.Generator/References/WellKnownPublicKeyTokens.cs deleted file mode 100644 index 77897462dc..0000000000 --- a/src/WinRT.WinMD.Generator/References/WellKnownPublicKeyTokens.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace WindowsRuntime.WinMDGenerator.References; - -/// -/// Well known public key tokens. -/// -internal static class WellKnownPublicKeyTokens -{ - /// - /// The public key data for mscorlib. - /// - public static readonly byte[] MSCorLib = [0xB7, 0x7A, 0x5C, 0x56, 0x19, 0x34, 0xE0, 0x89]; -} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj b/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj index 0d13c5831c..b2bb0f6dff 100644 --- a/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj +++ b/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj @@ -56,8 +56,7 @@ - - + diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs index 9cafa77b75..764cc80001 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs @@ -66,7 +66,7 @@ private void AddGuidAttributeFromName(TypeDefinition outputType, string name) Span hash = stackalloc byte[SHA1.HashSizeInBytes]; // Hash the data (we know we'll always fully fill the buffer). - // CodeQL [SM02196] Windows Runtime uses UUID v5 SHA1 to generate Guids for parameterized types. + // CodeQL [SM02196] Windows Runtime uses UUID v5 SHA1 to generate IIDs for parameterized types. _ = SHA1.HashData(span[..writtenNumberOfBytes], hash); // Return the rented array to the pool, if we have one diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs index 4412429c39..07a492aa10 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs @@ -6,9 +6,9 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.References; using WindowsRuntime.WinMDGenerator.Models; -using WindowsRuntime.WinMDGenerator.References; using AssemblyAttributes = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyAttributes; #pragma warning disable IDE0072 diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs index bd6ee8f52e..e7af3b363f 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using AsmResolver.DotNet; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.References; using WindowsRuntime.WinMDGenerator.Errors; using WindowsRuntime.WinMDGenerator.Helpers; using WindowsRuntime.WinMDGenerator.Models; -using WindowsRuntime.WinMDGenerator.References; using AssemblyAttributes = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyAttributes; #pragma warning disable IDE0046 diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index e62de46e37..8d8143675a 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -416,6 +416,7 @@ +