Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
25149ea
Unify response-file empty-line handling across all 5 generators
Sergio0694 Jun 8, 2026
6efa4ab
Phase 1: extract shared CLI infrastructure into WinRT.Generator.Cli
Sergio0694 Jun 9, 2026
5435ebd
Phase 2: extract shared error contract foundation
Sergio0694 Jun 9, 2026
4f58720
Phase 3: introduce IGeneratorErrorFactory shared error contract
Sergio0694 Jun 9, 2026
a6a873d
Phase 4: extract response-file parsing and formatting via reflection
Sergio0694 Jun 9, 2026
0bd144a
Phase 5: extract shared debug-repro leaf helpers
Sergio0694 Jun 9, 2026
7179aac
Phase 6: extract shared Run entry-point preamble into GeneratorHost.P…
Sergio0694 Jun 9, 2026
848cd1f
Rename WinRT.Generator.Cli to WinRT.Generator.Core
Sergio0694 Jun 9, 2026
0533632
Rename GeneratorCli namespace to Generator
Sergio0694 Jun 9, 2026
cece6be
Use <inheritdoc/> for generator args docs
Sergio0694 Jun 9, 2026
0c0b5d4
Remove ParseFromResponseFile/FormatToResponseFile wrappers from args …
Sergio0694 Jun 9, 2026
ec962ed
Always quote the phase name in unhandled generator messages
Sergio0694 Jun 9, 2026
a5a1ff3
Delete dead SignatureComparerExtensions copies in Impl + Projection
Sergio0694 Jun 9, 2026
c7e6242
Add debug-repro orchestration helpers to DebugReproPacker
Sergio0694 Jun 9, 2026
a8917ec
Move RuntimeContextExtensions.LoadModule to Core
Sergio0694 Jun 9, 2026
10ba861
Add GeneratorPhaseRunner to wrap per-phase try/catch + log
Sergio0694 Jun 9, 2026
785a032
Centralize IGeneratorErrorFactory message strings in WellKnownGenerat…
Sergio0694 Jun 9, 2026
e044e48
Consolidate MvidGenerator + IncrementalHashExtensions in Core
Sergio0694 Jun 9, 2026
a9322b0
Consolidate WellKnownPublicKeys + WellKnownPublicKeyTokens in Core
Sergio0694 Jun 9, 2026
d93c8c7
Make GeneratorPhaseRunner generic in TArgs and capture the args itself
Sergio0694 Jun 9, 2026
adcf776
Simplify response-file parsing and messages
Sergio0694 Jun 9, 2026
79f3f18
Standardize generator naming and runner calls
Sergio0694 Jun 9, 2026
d124065
Move InternalsVisibleTo declarations from AssemblyInfo.cs to .csproj
Sergio0694 Jun 9, 2026
7a598dc
Remove redundant global/System qualifiers
Sergio0694 Jun 9, 2026
afef579
Use primary constructor for GeneratorPhaseRunner
Sergio0694 Jun 9, 2026
6276d75
Auto-call ThrowIfCancellationRequested after each RunPhase body
Sergio0694 Jun 9, 2026
6a21afd
Rename file to GeneratorPhaseRunner{TArgs}.cs
Sergio0694 Jun 9, 2026
c5e0359
Add CodeQL annotations & clarify IID comments
Sergio0694 Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System;

namespace WindowsRuntime.ImplGenerator.Attributes;
namespace WindowsRuntime.Generator.Attributes;

/// <summary>
/// An attribute indicating the name of a given command line argument.
Expand All @@ -16,4 +16,4 @@ internal sealed class CommandLineArgumentNameAttribute(string name) : Attribute
/// Gets the command line argument name.
/// </summary>
public string Name { get; } = name;
}
}
218 changes: 218 additions & 0 deletions src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Leaf helpers shared across the CsWinRT CLI generators for packaging and unpacking debug repros.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal static class DebugReproPacker
{
/// <summary>
/// Generates a hashed filename by appending a Shake128 hash of the original file path.
/// </summary>
/// <param name="filePath">The original file path.</param>
/// <returns>The hashed filename in the form <c>{name}_{HEX}{ext}</c>.</returns>
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)}";
}

/// <summary>
/// Copies all specified files to a target folder using hashed file names, and returns the list of updated names.
/// </summary>
/// <param name="filePaths">The input file paths.</param>
/// <param name="destinationDirectory">The target directory to copy the files to.</param>
/// <param name="originalPaths">A dictionary to store the original paths of the copied files (keyed by hashed file name).</param>
/// <param name="token">A cancellation token to monitor for cancellation requests.</param>
/// <returns>The list of updated hashed filenames, in the same order as <paramref name="filePaths"/>.</returns>
public static List<string> CopyHashedFilesToDirectory(
string[] filePaths,
string destinationDirectory,
Dictionary<string, string> originalPaths,
CancellationToken token)
{
List<string> 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;
}

/// <summary>
/// Copies a single specified file to a target folder using a hashed file name.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
/// <param name="filePath">The input file path, or <see langword="null"/> to skip.</param>
/// <param name="destinationDirectory">The target directory to copy the file to.</param>
/// <param name="originalPaths">A dictionary to store the original path of the copied file (keyed by hashed file name).</param>
/// <param name="token">A cancellation token to monitor for cancellation requests.</param>
/// <returns>The hashed filename, or <see langword="null"/> if <paramref name="filePath"/> was <see langword="null"/>.</returns>
[return: NotNullIfNotNull(nameof(filePath))]
public static string? CopyHashedFileToDirectory(
string? filePath,
string destinationDirectory,
Dictionary<string, string> 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;
}

/// <summary>
/// Serializes an input path map to a target directory as a JSON file.
/// </summary>
/// <param name="pathMap">The input path map (hashed file name → original file path).</param>
/// <param name="destinationDirectory">The target directory.</param>
/// <param name="fileName">The name to use for the file with the serialized path map.</param>
public static void CopyPathMapToDirectory(
Dictionary<string, string> 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);
}

/// <summary>
/// Extracts an input path map from a .zip archive entry.
/// </summary>
/// <param name="pathMapEntry">The input path map entry.</param>
/// <returns>The deserialized path map (hashed file name → original file path).</returns>
/// <remarks>
/// The <paramref name="pathMapEntry"/> value is expected to have the content produced by calls to <see cref="CopyPathMapToDirectory"/>.
/// </remarks>
public static Dictionary<string, string> 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)!;
}

/// <summary>
/// Prepares the staging directory and target archive path for a debug repro save operation.
/// </summary>
/// <typeparam name="TError">The per-tool error factory used to throw if <paramref name="debugReproDirectory"/> does not exist.</typeparam>
/// <param name="debugReproDirectory">The user-provided directory where the resulting <c>.zip</c> archive will be written. Must already exist.</param>
/// <param name="toolName">The CLI tool name (e.g. <c>"cswinrtimplgen"</c>), used as the prefix of the staging directory.</param>
/// <param name="archiveFileName">The file name of the resulting <c>.zip</c> archive (e.g. <c>"impl-debug-repro.zip"</c>).</param>
/// <returns>A pair containing the freshly-created staging directory and the absolute path of the target archive.</returns>
/// <exception cref="Exception">Thrown via <typeparamref name="TError"/> if <paramref name="debugReproDirectory"/> does not exist.</exception>
public static (string TempDirectory, string ZipPath) BeginSave<TError>(
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);
}

/// <summary>
/// Finalizes a debug repro save by zipping the staging directory into the target archive and deleting the staging directory.
/// </summary>
/// <param name="tempDirectory">The staging directory previously returned by <see cref="BeginSave{TError}(string, string, string)"/>.</param>
/// <param name="zipPath">The absolute path of the target <c>.zip</c> archive, previously returned by <see cref="BeginSave{TError}(string, string, string)"/>.</param>
/// <remarks>
/// If a file already exists at <paramref name="zipPath"/>, it is deleted before the new archive is created.
/// </remarks>
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);
}

/// <summary>
/// Creates a freshly-named temporary directory for unpacking a debug repro <c>.zip</c> archive.
/// </summary>
/// <param name="toolName">The CLI tool name (e.g. <c>"cswinrtimplgen"</c>), used as the prefix of the directory.</param>
/// <returns>The absolute path of the created directory.</returns>
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@
// Licensed under the MIT License.

using System;
using WindowsRuntime.ImplGenerator.Errors;

namespace WindowsRuntime.ImplGenerator;
namespace WindowsRuntime.Generator.Errors;

/// <summary>
/// Extensions for interop exceptions.
/// Shared extensions for CsWinRT CLI generator exceptions.
/// </summary>
internal static class ImplExceptionExtensions
internal static class GeneratorExceptionExtensions
{
extension(Exception exception)
{
/// <summary>
/// Gets a value indicating whether an exception is well known (and should therefore not be caught).
/// </summary>
public bool IsWellKnown => exception is OperationCanceledException or WellKnownImplException;
public bool IsWellKnown => exception is OperationCanceledException or WellKnownGeneratorException;
}
}
}
36 changes: 36 additions & 0 deletions src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace WindowsRuntime.Generator.Errors;

/// <summary>
/// Routes shared logical errors through the per-tool well-known exception factory.
/// </summary>
/// <remarks>
/// 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 <see cref="WellKnownGeneratorException"/> subtype.
/// </remarks>
internal interface IGeneratorErrorFactory
{
/// <summary>Some exception was thrown when trying to read the response file.</summary>
static abstract Exception ResponseFileReadError(Exception exception);

/// <summary>Failed to parse an argument from the response file.</summary>
static abstract Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception);

/// <summary>The input response file is malformed.</summary>
static abstract Exception MalformedResponseFile();

/// <summary>The debug repro directory does not exist.</summary>
static abstract Exception DebugReproDirectoryDoesNotExist(string path);

/// <summary>The debug repro contains a file entry that has no mapping.</summary>
static abstract Exception DebugReproMissingFileEntryMapping(string path);

/// <summary>The debug repro contains a file entry that was not recognized.</summary>
static abstract Exception DebugReproUnrecognizedFileEntry(string path);
}
41 changes: 41 additions & 0 deletions src/WinRT.Generator.Core/Errors/UnhandledGeneratorException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace WindowsRuntime.Generator.Errors;

/// <summary>
/// An unhandled exception for a CsWinRT CLI generator.
/// </summary>
/// <remarks>
/// Each per-tool unhandled exception inherits from this type and provides its
/// <see cref="ErrorPrefix"/> and <see cref="GeneratorName"/> so that the standardized
/// <see cref="ToString"/> message remains tool-specific.
/// </remarks>
/// <param name="phase">The phase that failed.</param>
/// <param name="exception">The inner exception.</param>
internal abstract class UnhandledGeneratorException(string phase, Exception exception)
: Exception(null, exception)
{
/// <summary>
/// Gets the error prefix for the per-tool exception ID (e.g. <c>"CSWINRTIMPLGEN"</c>).
/// </summary>
protected abstract string ErrorPrefix { get; }

/// <summary>
/// Gets the name of the generator used in the standard message.
/// </summary>
protected abstract string GeneratorName { get; }

/// <inheritdoc/>
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.""";
}
}

35 changes: 35 additions & 0 deletions src/WinRT.Generator.Core/Errors/WellKnownGeneratorException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace WindowsRuntime.Generator.Errors;

/// <summary>
/// A well-known exception for a CsWinRT CLI generator.
/// </summary>
/// <remarks>
/// Each per-tool well-known exception inherits from this type and adds a marker
/// (e.g. <c>WellKnownImplException</c>) so the concrete runtime type remains tool-specific.
/// The base class provides the shared <see cref="Id"/> field and <see cref="ToString"/> logic,
/// which matches the simple per-tool format <c>error {Id}: {Message}[ Inner exception: ...]</c>.
/// </remarks>
/// <param name="id">The id of the exception (e.g. <c>"CSWINRTIMPLGEN0001"</c>).</param>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The inner exception, if any.</param>
internal abstract class WellKnownGeneratorException(string id, string message, Exception? innerException)
: Exception(message, innerException)
{
/// <summary>
/// Gets the id of the exception (e.g. <c>"CSWINRTIMPLGEN0001"</c>).
/// </summary>
public string Id { get; } = id;

/// <inheritdoc/>
public override string ToString()
{
return InnerException is not null
? $"""error {Id}: {Message} Inner exception: '{InnerException.GetType().Name}': '{InnerException.Message}'."""
: $"""error {Id}: {Message}""";
}
}
Loading
Loading