// ----------------------------------------------------------------------------- // 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 > 1) { Console.Error.WriteLine("error: generate accepts at most one output directory."); Console.Error.WriteLine("Run 'generate --help' for command information."); return UsageErrorExitCode; } string? outputDirectory = arguments.Count == 1 ? arguments[0] : null; string? resolvedOutputDirectory = null; try { generatedFiles = OpenGlGenerator.Generate(projection); 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 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' 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' 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"); Console.WriteLine(" generate "); Console.WriteLine(" generate --help"); Console.WriteLine(); Console.WriteLine("Output:"); Console.WriteLine(" Without a directory, output is retained in memory only."); 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 ); } }