320 lines
12 KiB
C#
320 lines
12 KiB
C#
// -----------------------------------------------------------------------------
|
|
// file="GenerateCli"
|
|
// Project: StgSharp
|
|
// Copyright (c) Nitload.
|
|
// SPDX-License-Identifier: MIT
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using StgSharp.GenerateGL.Generation;
|
|
using StgSharp.GenerateGL.Registry;
|
|
|
|
namespace StgSharp.GenerateGL
|
|
{
|
|
/// <summary>
|
|
/// Owns the command-line boundary of the future code generation pipeline.
|
|
/// </summary>
|
|
internal static class GenerateCli
|
|
{
|
|
|
|
private const int QueryErrorExitCode = 1;
|
|
private const int SuccessExitCode = 0;
|
|
private const int UsageErrorExitCode = 2;
|
|
private const string GeneratedFileMarker = "// <auto-generated>";
|
|
|
|
public static int Generate(
|
|
GlRegistryProjection projection,
|
|
IReadOnlyList<string> arguments,
|
|
out GeneratedFileSet? generatedFiles
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(projection);
|
|
ArgumentNullException.ThrowIfNull(arguments);
|
|
generatedFiles = null;
|
|
|
|
if (arguments.Count == 1 && arguments[0] is "--help" or "-h")
|
|
{
|
|
WriteHelp();
|
|
return SuccessExitCode;
|
|
}
|
|
|
|
if (arguments.Count == 0 || arguments.Count > 2 ||
|
|
!TryReadTarget(arguments[0], out GlGenerationTarget target))
|
|
{
|
|
Console.Error.WriteLine(
|
|
"error: generate requires api, enum, or all and accepts one optional output directory.");
|
|
Console.Error.WriteLine("Run 'generate --help' for command information.");
|
|
return UsageErrorExitCode;
|
|
}
|
|
|
|
string? outputDirectory = arguments.Count == 2 ? arguments[1] : null;
|
|
string? resolvedOutputDirectory = null;
|
|
try
|
|
{
|
|
generatedFiles = OpenGlGenerator.Generate(projection, target);
|
|
if (outputDirectory is not null)
|
|
{
|
|
resolvedOutputDirectory = WriteGeneratedFiles(
|
|
generatedFiles,
|
|
outputDirectory);
|
|
}
|
|
}
|
|
catch (InvalidDataException exception)
|
|
{
|
|
Console.Error.WriteLine($"error: {exception.Message}");
|
|
return QueryErrorExitCode;
|
|
}
|
|
catch (InvalidOperationException exception)
|
|
{
|
|
Console.Error.WriteLine($"error: {exception.Message}");
|
|
return QueryErrorExitCode;
|
|
}
|
|
catch (UnauthorizedAccessException exception)
|
|
{
|
|
Console.Error.WriteLine($"error: {exception.Message}");
|
|
return QueryErrorExitCode;
|
|
}
|
|
catch (IOException exception)
|
|
{
|
|
Console.Error.WriteLine($"error: {exception.Message}");
|
|
return QueryErrorExitCode;
|
|
}
|
|
catch (ArgumentException exception)
|
|
{
|
|
Console.Error.WriteLine($"error: {exception.Message}");
|
|
return QueryErrorExitCode;
|
|
}
|
|
catch (NotSupportedException exception)
|
|
{
|
|
Console.Error.WriteLine($"error: {exception.Message}");
|
|
return QueryErrorExitCode;
|
|
}
|
|
|
|
Console.WriteLine(
|
|
$"Generated {generatedFiles.Files.Count} file(s) in memory.");
|
|
Console.WriteLine("Use 'list files' and 'preview <filename>' to inspect them.");
|
|
Console.WriteLine(
|
|
resolvedOutputDirectory is null
|
|
? "No files were written to disk."
|
|
: $"Wrote {generatedFiles.Files.Count} file(s) to " +
|
|
$"'{resolvedOutputDirectory}'.");
|
|
return SuccessExitCode;
|
|
}
|
|
|
|
private static bool TryReadTarget(
|
|
string value,
|
|
out GlGenerationTarget target
|
|
)
|
|
{
|
|
target = value.ToUpperInvariant() switch
|
|
{
|
|
"API" => GlGenerationTarget.Api,
|
|
"ENUM" or "ENUMS" => GlGenerationTarget.Enum,
|
|
"ALL" => GlGenerationTarget.All,
|
|
_ => GlGenerationTarget.None,
|
|
};
|
|
return target != GlGenerationTarget.None;
|
|
}
|
|
|
|
private static string WriteGeneratedFiles(
|
|
GeneratedFileSet files,
|
|
string outputDirectory
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(files);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(outputDirectory);
|
|
|
|
string directory = Path.TrimEndingDirectorySeparator(
|
|
Path.GetFullPath(outputDirectory));
|
|
if (!Directory.Exists(directory))
|
|
{
|
|
throw new DirectoryNotFoundException(
|
|
$"Output directory '{directory}' does not exist.");
|
|
}
|
|
|
|
StringComparison pathComparison = OperatingSystem.IsWindows()
|
|
? StringComparison.OrdinalIgnoreCase
|
|
: StringComparison.Ordinal;
|
|
List<StagedGeneratedFile> stagedFiles = new List<StagedGeneratedFile>(
|
|
files.Files.Count);
|
|
try
|
|
{
|
|
foreach (GeneratedFile file in files.Files)
|
|
{
|
|
string fileName = Path.GetFileName(file.Name);
|
|
if (!string.Equals(fileName, file.Name, StringComparison.Ordinal) ||
|
|
string.IsNullOrWhiteSpace(fileName))
|
|
{
|
|
throw new InvalidDataException(
|
|
$"Generated file name '{file.Name}' is not a plain file name.");
|
|
}
|
|
|
|
string targetPath = Path.GetFullPath(Path.Combine(directory, fileName));
|
|
if (!string.Equals(
|
|
Path.GetDirectoryName(targetPath),
|
|
directory,
|
|
pathComparison))
|
|
{
|
|
throw new InvalidDataException(
|
|
$"Generated file '{file.Name}' escapes the output directory.");
|
|
}
|
|
|
|
EnsureTargetCanBeReplaced(targetPath);
|
|
string temporaryPath = Path.Combine(
|
|
directory,
|
|
$".{fileName}.{Guid.NewGuid():N}.tmp");
|
|
using (FileStream stream = new FileStream(
|
|
temporaryPath,
|
|
FileMode.CreateNew,
|
|
FileAccess.Write,
|
|
FileShare.None))
|
|
using (StreamWriter writer = new StreamWriter(
|
|
stream,
|
|
new UTF8Encoding(false)))
|
|
{
|
|
writer.Write(file.Content);
|
|
}
|
|
|
|
stagedFiles.Add(new StagedGeneratedFile(temporaryPath, targetPath));
|
|
}
|
|
|
|
foreach (StagedGeneratedFile file in stagedFiles)
|
|
{
|
|
File.Move(file.TemporaryPath, file.TargetPath, true);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
foreach (StagedGeneratedFile file in stagedFiles)
|
|
{
|
|
if (File.Exists(file.TemporaryPath))
|
|
{
|
|
File.Delete(file.TemporaryPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
return directory;
|
|
}
|
|
|
|
private static void EnsureTargetCanBeReplaced(
|
|
string targetPath
|
|
)
|
|
{
|
|
if (!File.Exists(targetPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string? firstLine = File.ReadLines(targetPath).FirstOrDefault();
|
|
if (!string.Equals(firstLine, GeneratedFileMarker, StringComparison.Ordinal))
|
|
{
|
|
throw new IOException(
|
|
$"Refusing to overwrite non-generated file '{targetPath}'.");
|
|
}
|
|
}
|
|
|
|
public static int ListFiles(
|
|
GeneratedFileSet files,
|
|
IReadOnlyList<string> arguments
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(files);
|
|
ArgumentNullException.ThrowIfNull(arguments);
|
|
if (arguments.Count != 0)
|
|
{
|
|
Console.Error.WriteLine("error: list files does not accept arguments.");
|
|
return UsageErrorExitCode;
|
|
}
|
|
|
|
if (files.Files.Count == 0)
|
|
{
|
|
Console.Error.WriteLine("No generated files are held in this session.");
|
|
Console.Error.WriteLine("Run 'generate api', 'generate enum', or 'generate all' first.");
|
|
return QueryErrorExitCode;
|
|
}
|
|
|
|
Console.WriteLine("Generated files held in the current session");
|
|
for (int index = 0; index < files.Files.Count; index++)
|
|
{
|
|
GeneratedFile file = files.Files[index];
|
|
Console.WriteLine(
|
|
$" [{index + 1}] {file.Name} ({file.Content.Length} characters)");
|
|
}
|
|
|
|
return SuccessExitCode;
|
|
}
|
|
|
|
public static int Preview(
|
|
GeneratedFileSet files,
|
|
IReadOnlyList<string> arguments
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(files);
|
|
ArgumentNullException.ThrowIfNull(arguments);
|
|
if (arguments.Count != 1)
|
|
{
|
|
Console.Error.WriteLine("error: preview requires one generated file name.");
|
|
return UsageErrorExitCode;
|
|
}
|
|
|
|
if (!files.TryGetFile(arguments[0], out GeneratedFile file))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"No generated file named '{arguments[0]}' is held in this session.");
|
|
if (files.Files.Count == 0)
|
|
{
|
|
Console.Error.WriteLine(
|
|
"Run 'generate api', 'generate enum', or 'generate all' first.");
|
|
}
|
|
else
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"Available files: {string.Join(", ", files.Files.Select(value => value.Name))}");
|
|
}
|
|
|
|
return QueryErrorExitCode;
|
|
}
|
|
|
|
Console.Write(file.Content);
|
|
return SuccessExitCode;
|
|
}
|
|
|
|
public static void WriteHelp()
|
|
{
|
|
Console.WriteLine("Generate C# bindings from the loaded OpenGL registry.");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Usage:");
|
|
Console.WriteLine(" generate api [output-directory]");
|
|
Console.WriteLine(" generate enum [output-directory]");
|
|
Console.WriteLine(" generate all [output-directory]");
|
|
Console.WriteLine(" generate --help");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Targets:");
|
|
Console.WriteLine(" api Generate function tables and managed forwarding APIs.");
|
|
Console.WriteLine(" enum Generate glConst and strongly typed enum groups.");
|
|
Console.WriteLine(" all Generate both API and enum files.");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Output:");
|
|
Console.WriteLine(" Without a directory, output is retained in memory only.");
|
|
Console.WriteLine(" API output references enum output; use all for a clean directory.");
|
|
Console.WriteLine(" The output directory must already exist and be writable.");
|
|
Console.WriteLine(" Existing files are replaced only when marked as generated.");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Inspection:");
|
|
Console.WriteLine(" list files");
|
|
Console.WriteLine(" preview <filename>");
|
|
}
|
|
|
|
private sealed record StagedGeneratedFile(
|
|
string TemporaryPath,
|
|
string TargetPath
|
|
);
|
|
|
|
}
|
|
}
|