Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 75 additions & 8 deletions src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Text;
using System.Text.Json;
using System.Threading;
using WindowsRuntime.Generator.Errors;
using WindowsRuntime.Generator.Helpers;

namespace WindowsRuntime.Generator.DebugRepro;
Expand All @@ -18,14 +19,8 @@ namespace WindowsRuntime.Generator.DebugRepro;
/// Leaf helpers shared across the CsWinRT CLI generators for packaging and unpacking debug repros.
/// </summary>
/// <remarks>
/// Each generator's <c>SaveDebugRepro</c>/<c>UnpackDebugRepro</c> 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 the small, identical-across-tools building blocks:
/// <list type="bullet">
/// <item>Hashing the original file path into a stable, collision-free file name.</item>
/// <item>Copying files (or a single file) into a destination directory using the hashed names.</item>
/// <item>Serializing / deserializing the "hashed name → original path" mapping as JSON inside the repro archive.</item>
/// </list>
/// 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
{
Expand Down Expand Up @@ -148,4 +143,76 @@ public static Dictionary<string, string> ExtractPathMap(ZipArchiveEntry pathMapE
// 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;
}
}
14 changes: 3 additions & 11 deletions src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,10 @@ namespace WindowsRuntime.Generator.Errors;
/// Routes shared logical errors through the per-tool well-known exception factory.
/// </summary>
/// <remarks>
/// <para>
/// Shared infrastructure (response-file parsing, debug-repro packing, etc.) is generic over an
/// implementation of this interface and reaches the per-tool factory through its <c>static abstract</c>
/// members. This preserves 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="Errors.WellKnownGeneratorException"/> subtype.
/// </para>
/// <para>
/// Implementations must be sealed (not <c>static</c>) so they can participate in the
/// <c>static abstract</c> interface contract. The implementing type is not meant to be instantiated;
/// it is used as a type parameter to dispatch the factory call.
/// </para>
/// 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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace WindowsRuntime.Generator.Errors;
/// </summary>
/// <remarks>
/// Each per-tool unhandled exception inherits from this type and provides its
/// <see cref="ErrorPrefix"/> and <see cref="GeneratorDescription"/> so that the standardized
/// <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>
Expand All @@ -24,16 +24,15 @@ internal abstract class UnhandledGeneratorException(string phase, Exception exce
protected abstract string ErrorPrefix { get; }

/// <summary>
/// Gets the description of the generator used in the standard message
/// (e.g. <c>"impl generator"</c>, <c>"interop generator"</c>, <c>"WinMD generator"</c>).
/// Gets the name of the generator used in the standard message.
/// </summary>
protected abstract string GeneratorDescription { get; }
protected abstract string GeneratorName { get; }

/// <inheritdoc/>
public override string ToString()
{
return
$"""error {ErrorPrefix}9999: The CsWinRT {GeneratorDescription} failed with an unhandled exception """ +
$"""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.""";
Expand Down
50 changes: 50 additions & 0 deletions src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace WindowsRuntime.Generator.Errors;

/// <summary>
/// Shared message templates for the well-known logical errors defined by <see cref="IGeneratorErrorFactory"/>.
/// </summary>
/// <remarks>
/// Each per-tool <c>WellKnown*Exceptions</c> factory uses these helpers to format its own
/// instance of every <see cref="IGeneratorErrorFactory"/> method, ensuring that the message
/// text stays identical across all generators while the per-tool error ID prefix (e.g.
/// <c>CSWINRTIMPLGEN</c>) and concrete exception type are still chosen per-tool.
/// </remarks>
internal static class WellKnownGeneratorMessages
{
/// <see cref="IGeneratorErrorFactory.ResponseFileReadError"/>
public const string ResponseFileReadError = "Failed to read the response file (e.g. it may be missing or not accessible).";

/// <see cref="IGeneratorErrorFactory.ResponseFileArgumentParsingError"/>
/// <param name="argumentName">The name of the response-file argument that failed to parse.</param>
public static string ResponseFileArgumentParsingError(string argumentName)
{
return $"Failed to parse argument '{argumentName}' from response file.";
}

/// <see cref="IGeneratorErrorFactory.MalformedResponseFile"/>
public const string MalformedResponseFile = "The response file is malformed and contains invalid content.";

/// <see cref="IGeneratorErrorFactory.DebugReproDirectoryDoesNotExist"/>
/// <param name="path">The directory path that does not exist.</param>
public static string DebugReproDirectoryDoesNotExist(string path)
{
return $"The debug repro directory '{path}' does not exist.";
}

/// <see cref="IGeneratorErrorFactory.DebugReproMissingFileEntryMapping"/>
/// <param name="path">The debug-repro file entry path that has no mapping.</param>
public static string DebugReproMissingFileEntryMapping(string path)
{
return $"The debug repro file entry with path '{path}' is missing its assembly path mapping.";
}

/// <see cref="IGeneratorErrorFactory.DebugReproUnrecognizedFileEntry"/>
/// <param name="path">The debug-repro file entry path that was not recognized.</param>
public static string DebugReproUnrecognizedFileEntry(string path)
{
return $"The debug repro file entry with path '{path}' was not recognized.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.IO;
using System.Security.Cryptography;

namespace WindowsRuntime.InteropGenerator;
namespace WindowsRuntime.Generator.Extensions;

/// <summary>
/// Extensions for the <see cref="IncrementalHash"/> type.
Expand Down Expand Up @@ -34,4 +34,4 @@ public void AppendData(Stream stream)
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
}
15 changes: 0 additions & 15 deletions src/WinRT.Generator.Core/Extensions/PathExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,6 @@ internal static class PathExtensions
return path?.Replace('\\', '/');
}

/// <inheritdoc cref="Normalize(string?)"/>
public static ReadOnlySpan<char> Normalize(ReadOnlySpan<char> path)
{
if (OperatingSystem.IsWindows())
{
return path;
}

char[] buffer = new char[path.Length];

path.Replace(buffer, '\\', '/');

return buffer;
}

/// <summary>
/// Checks whether a given path represents a file or folder contained within a folder with a given name.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

#pragma warning disable IDE0046

namespace WindowsRuntime.WinMDGenerator;
namespace WindowsRuntime.Generator.Extensions;

/// <summary>
/// Extensions for the <see cref="RuntimeContext"/> type.
Expand Down
24 changes: 8 additions & 16 deletions src/WinRT.Generator.Core/GeneratorHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,11 @@ namespace WindowsRuntime.Generator;
/// <summary>
/// Shared <c>Run</c> entry-point scaffold for the CsWinRT CLI generators.
/// </summary>
/// <remarks>
/// Each generator's <c>Run</c> method historically opened with an identical preamble:
/// <list type="number">
/// <item>If the input file path looks like a <c>.zip</c>, unpack a debug repro and re-route to the extracted <c>.rsp</c>.</item>
/// <item>Parse the response file into a per-tool args record.</item>
/// <item>If <c>DebugReproDirectory</c> is set and we are not already replaying, save a debug repro of the current invocation.</item>
/// </list>
/// <see cref="Prepare{TArgs}"/> encapsulates that preamble. Each generator's <c>Run</c> now starts with a
/// single call to it; the per-tool unpack / save / parse logic is supplied via delegates so behavior
/// stays identical (same log messages, same exception phases, same per-tool unhandled exception type).
/// </remarks>
internal static class GeneratorHost
{
/// <summary>
/// Runs the shared unpack → parse → save preamble for a CsWinRT CLI generator.
/// Runs the shared unpack, parse, save preamble for a CsWinRT CLI generator and returns a
/// <see cref="GeneratorPhaseRunner{TArgs}"/> ready to drive the remaining per-tool phases.
/// </summary>
/// <typeparam name="TArgs">The per-tool args record (must implement <see cref="IGeneratorArgs"/>).</typeparam>
/// <param name="inputFilePath">The input file path (response file or debug-repro <c>.zip</c>).</param>
Expand All @@ -36,8 +26,11 @@ internal static class GeneratorHost
/// <param name="wrapUnhandled">Wraps an unexpected exception into the per-tool <c>Unhandled*Exception</c> with the given phase name.</param>
/// <param name="log">Logs a progress message to the user (typically <c>ConsoleApp.Log</c> from ConsoleAppFramework).</param>
/// <param name="token">The token for the operation.</param>
/// <returns>The parsed <typeparamref name="TArgs"/> instance.</returns>
public static TArgs Prepare<TArgs>(
/// <returns>
/// A <see cref="GeneratorPhaseRunner{TArgs}"/> pre-bound to the parsed args, <paramref name="wrapUnhandled"/>
/// and <paramref name="log"/> for use by subsequent phases.
/// </returns>
public static GeneratorPhaseRunner<TArgs> CreateRunner<TArgs>(
string inputFilePath,
string toolName,
Func<string, CancellationToken, string> unpackDebugRepro,
Expand Down Expand Up @@ -109,7 +102,6 @@ public static TArgs Prepare<TArgs>(

args.Token.ThrowIfCancellationRequested();

return args;
return new(args, wrapUnhandled, log);
}
}

Loading
Loading