← All source files

V18 / 8d896b8a86e5

VersionCatalog.cs

Interfaces/Gui/LilithGui/VersionCatalog.cs

Unchanged from the preceding version
CS
using System.Diagnostics;
using System.IO.Compression;
using System.Xml.Linq;

namespace Lilith.Gui;

public sealed record GuiVersion(int Major,string Label,string Version,string Revision,bool WorkingTree);
internal sealed class VersionCatalog(string repo)
{
    internal async Task<GuiVersion[]> ListAsync()
    {
        var versions = new List<GuiVersion>();
        string refs = await Run("git",["for-each-ref","--format=%(refname:short)","refs/heads/","refs/tags/"],repo);
        foreach (string branch in refs.Split('\n',StringSplitOptions.RemoveEmptyEntries|StringSplitOptions.TrimEntries))
        {
            try
            {
                string xml = await Run("git",["show",branch+":Version.props"],repo);
                string version = ReadVersion(xml); int major = int.Parse(version.Split('.')[0]);
                string commit = (await Run("git",["rev-parse",branch+"^{commit}"],repo)).Trim();
                versions.Add(new(major,"V"+major,version,commit,false));
            }
            catch (InvalidOperationException) { /* Not every release branch is an application release. */ }
        }
        string current = ReadVersion(await File.ReadAllTextAsync(Path.Combine(repo,"Version.props")));
        int currentMajor = int.Parse(current.Split('.')[0]);
        versions.RemoveAll(v=>v.Major==currentMajor);
        versions.Add(new(currentMajor,"V"+currentMajor,current,"working-tree",true));
        return versions.GroupBy(v=>v.Major).Select(g=>g.OrderBy(v=>v.WorkingTree).ThenBy(v=>Numeric(v.Version)).ThenBy(v=>!v.Version.Contains('-')).ThenBy(v=>Prerelease(v.Version)).Last()).OrderBy(v=>v.Major).ToArray();
    }
    private static Version Numeric(string version)=>Version.Parse(version.Split('-')[0]);
    private static int Prerelease(string version)=>int.TryParse(version.Split('.').Last(),out int number)?number:0;
    internal static string ReadVersion(string xml)
    {
        var elements = XDocument.Parse(xml).Descendants();
        string prefix = elements.First(e=>e.Name.LocalName=="VersionPrefix").Value;
        string suffix = elements.FirstOrDefault(e=>e.Name.LocalName=="VersionSuffix")?.Value??"";
        return prefix+(suffix.Length==0?"":"-"+suffix);
    }
    internal async Task<string> PrepareAsync(GuiVersion version,Action<string> progress)
    {
        string source = repo;
        if (!version.WorkingTree)
        {
            string cache = Path.Combine(repo,"Admin","Build","GuiBackend");
            string marker = Path.Combine(cache,"revision.txt");
            if (!File.Exists(marker)||File.ReadAllText(marker)!=version.Revision)
            {
                string resolved = Path.GetFullPath(cache);
                if (!resolved.StartsWith(Path.GetFullPath(Path.Combine(repo,"Admin","Build"))+Path.DirectorySeparatorChar,StringComparison.OrdinalIgnoreCase)) throw new IOException("Invalid backend cache root.");
                for(string? part=resolved;part is not null&&part.StartsWith(Path.GetFullPath(repo),StringComparison.OrdinalIgnoreCase);part=Path.GetDirectoryName(part))
                    if(Directory.Exists(part)&&(File.GetAttributes(part)&FileAttributes.ReparsePoint)!=0)throw new IOException("Linked cache paths cannot be replaced.");
                if (Directory.Exists(cache)) Directory.Delete(cache,true);
                Directory.CreateDirectory(cache); progress("Exporting pinned "+version.Label+" source");
                var start = new ProcessStartInfo("git") { WorkingDirectory=repo,RedirectStandardOutput=true,RedirectStandardError=true,UseShellExecute=false,CreateNoWindow=true };
                start.ArgumentList.Add("-c");start.ArgumentList.Add("safe.directory="+repo.Replace('\\','/'));
                foreach(string arg in new[]{"archive","--format=zip",version.Revision,"Lilith","Interfaces/ConsoleApp","Version.props","Directory.Build.props","Directory.Build.targets"})start.ArgumentList.Add(arg);
                using var process = Process.Start(start)??throw new IOException("Git did not start.");
                using var zip = new MemoryStream(); var errorTask=process.StandardError.ReadToEndAsync();
                await process.StandardOutput.BaseStream.CopyToAsync(zip); await process.WaitForExitAsync();
                if(process.ExitCode!=0)throw new IOException(await errorTask);
                zip.Position=0; using var archive = new ZipArchive(zip,ZipArchiveMode.Read);
                foreach(var entry in archive.Entries)
                {
                    string path=Path.GetFullPath(Path.Combine(cache,entry.FullName));
                    if(!path.StartsWith(resolved+Path.DirectorySeparatorChar,StringComparison.OrdinalIgnoreCase)||((entry.ExternalAttributes>>16)&0xF000)==0xA000)throw new IOException("Invalid archive path/link.");
                    if(entry.FullName.EndsWith('/')){Directory.CreateDirectory(path);continue;}
                    Directory.CreateDirectory(Path.GetDirectoryName(path)!); entry.ExtractToFile(path);
                }
                await File.WriteAllTextAsync(marker,version.Revision);
            }
            source=cache;
        }
        progress("Building "+version.Label+" backend from "+(version.WorkingTree?"working tree":version.Revision[..8]));
        await Run("dotnet",["build","Interfaces/ConsoleApp/LilithConsole/LilithConsole.csproj","-c","Release","-p:NuGetAudit=false"],source);
        string output=Path.Combine(source,"Admin","Build","bin","Interfaces","ConsoleApp","LilithConsole","LilithConsole","Release","net10.0");
        if(!File.Exists(Path.Combine(output,"Agent.dll")))throw new IOException("Pinned backend build did not produce Agent.dll.");
        return output;
    }
    internal static async Task<string> Run(string executable,IEnumerable<string> args,string directory)
    {
        var start=new ProcessStartInfo(executable){WorkingDirectory=directory,RedirectStandardOutput=true,RedirectStandardError=true,UseShellExecute=false,CreateNoWindow=true};
        if(executable=="git"){start.ArgumentList.Add("-c");start.ArgumentList.Add("safe.directory="+directory.Replace('\\','/'));}
        foreach(string arg in args)start.ArgumentList.Add(arg);
        using var process=Process.Start(start)??throw new IOException("Could not start "+executable);
        var stdout=process.StandardOutput.ReadToEndAsync();var stderr=process.StandardError.ReadToEndAsync();
        await process.WaitForExitAsync();string text=await stdout;
        if(process.ExitCode!=0)throw new InvalidOperationException(executable+" failed: "+text+await stderr);
        return text;
    }
}