// -----------------------------------------------------------------------------
// 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
{
///
/// Owns the command-line boundary of the future code generation pipeline.
///
internal static class GenerateCli
{
private const int QueryErrorExitCode = 1;
private const int SuccessExitCode = 0;
private const int UsageErrorExitCode = 2;
private const string GeneratedFileMarker = "// ";
public static int Generate(
GlRegistryProjection projection,
IReadOnlyList 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 ' 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 stagedFiles = new List(
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 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 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 ");
}
private sealed record StagedGeneratedFile(
string TemporaryPath,
string TargetPath
);
}
}