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 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";