Files

160 lines
5.7 KiB
C#
Raw Permalink Normal View History

using System.Diagnostics;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
var root = FindRoot();
var manifestPath = Path.Combine(root, "tools", "setup.json");
var cache = Path.Combine(root, "tools", "cache");
var group = Option("--group");
var rid = Option("--rid") ?? HostRid();
var only = Option("--only")?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var force = args.Contains("--force");
var pin = args.Contains("--pin");
var verify = args.Contains("--verify");
var manifest = JsonNode.Parse(File.ReadAllText(manifestPath));
var assets = manifest["assets"].AsArray();
var failures = 0;
var pinned = false;
using var http = new HttpClient { Timeout = TimeSpan.FromHours(1) };
http.DefaultRequestHeaders.UserAgent.ParseAdd("meweb-setup/1.0");
foreach (var asset in assets)
{
var name = (string)asset["name"];
if (group is not null && (string)asset["group"] != group) continue;
var assetRid = (string)asset["rid"];
if (assetRid is not null && assetRid != rid) continue;
if (only is not null && !only.Contains(name)) continue;
var check = Path.Combine(root, (string)asset["check"]);
var present = File.Exists(check) || Directory.Exists(check);
if (verify)
{
Console.WriteLine($"{(present ? "ok " : "MISSING")} {name,-12} {asset["version"],-28} {asset["license"]}");
if (!present) failures++;
continue;
}
if (present && !force)
{
Console.WriteLine($"ok {name} (already present)");
continue;
}
try
{
var url = (string)asset["url"];
var expected = ((string)asset["sha256"] ?? "").ToLowerInvariant();
if (expected.Length == 0 && !pin)
throw new InvalidOperationException("SHA-256 not pinned in the manifest: run once with --pin and review the value written");
Directory.CreateDirectory(cache);
var file = Path.Combine(cache, $"{name}-{asset["version"]}-{Path.GetFileName(new Uri(url).LocalPath)}".Replace(' ', '_'));
if (!File.Exists(file) || force) await Download(http, url, file, name);
var actual = await Hash(file);
if (expected.Length == 0)
{
asset["sha256"] = actual;
pinned = true;
Console.WriteLine($"pin {name} sha256={actual}");
}
else if (actual != expected)
{
File.Delete(file);
throw new InvalidOperationException($"SHA-256 mismatch: expected {expected}, got {actual}");
}
var dest = Path.Combine(root, (string)asset["dest"]);
Directory.CreateDirectory(dest);
switch ((string)asset["kind"])
{
case "file":
File.Copy(file, Path.Combine(dest, Path.GetFileName(new Uri(url).LocalPath)), overwrite: true);
break;
case "zip":
ZipFile.ExtractToDirectory(file, dest, overwriteFiles: true);
break;
case "tar":
Run("tar", "-xf", file, "-C", dest);
break;
default:
throw new InvalidOperationException($"unknown kind: {asset["kind"]}");
}
if (!File.Exists(check) && !Directory.Exists(check))
throw new InvalidOperationException($"{asset["check"]} is missing after extraction");
Console.WriteLine($"done {name}");
}
catch (Exception ex)
{
failures++;
Console.Error.WriteLine($"ERROR {name}: {ex.Message}");
}
}
if (pinned)
File.WriteAllText(manifestPath, manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }) + Environment.NewLine);
return failures == 0 ? 0 : 1;
string Option(string key)
{
var i = Array.IndexOf(args, key);
return i >= 0 && i + 1 < args.Length ? args[i + 1] : null;
}
static string FindRoot()
{
var dir = new DirectoryInfo(Directory.GetCurrentDirectory());
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "MeWeb.slnx"))) dir = dir.Parent;
return dir?.FullName ?? throw new InvalidOperationException("run from inside the repository (MeWeb.slnx not found)");
}
static async Task Download(HttpClient http, string url, string file, string name)
{
var partial = file + ".part";
using var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
var total = response.Content.Headers.ContentLength ?? -1;
await using (var source = await response.Content.ReadAsStreamAsync())
await using (var target = File.Create(partial))
{
var buffer = new byte[1 << 20];
var done = 0L;
var lastReport = Stopwatch.StartNew();
var read = default(int);
while ((read = await source.ReadAsync(buffer)) > 0)
{
await target.WriteAsync(buffer.AsMemory(0, read));
done += read;
if (lastReport.ElapsedMilliseconds < 2000) continue;
lastReport.Restart();
Console.WriteLine(total > 0 ? $"fetch {name}: {done * 100 / total}% ({done >> 20}/{total >> 20} MB)" : $"fetch {name}: {done >> 20} MB");
}
}
File.Move(partial, file, overwrite: true);
}
static async Task<string> Hash(string file)
{
await using var stream = File.OpenRead(file);
return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream));
}
static void Run(string file, params string[] arguments)
{
var info = new ProcessStartInfo(file) { UseShellExecute = false };
foreach (var argument in arguments) info.ArgumentList.Add(argument);
using var process = Process.Start(info) ?? throw new InvalidOperationException($"cannot start {file}");
process.WaitForExit();
if (process.ExitCode != 0) throw new InvalidOperationException($"{file} exited with code {process.ExitCode}");
}
static string HostRid() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win-x64"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx-arm64"
: "linux-x64";