V16 / 6df58cd3ed86
VersionCatalog.cs
Interfaces/Gui/LilithGui/VersionCatalog.cs
Changed in this versionCS
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;
}
}