From 20f4b6a7b840a5ef0f853c2e5de35f17bf13440c Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 6 Aug 2026 08:13:49 +0300 Subject: [PATCH] refactor: separate package materialization from activation --- Launcher.Next.Infrastructure/LocalPackageStore.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Launcher.Next.Infrastructure/LocalPackageStore.cs b/Launcher.Next.Infrastructure/LocalPackageStore.cs index 538feba..0ab5e08 100644 --- a/Launcher.Next.Infrastructure/LocalPackageStore.cs +++ b/Launcher.Next.Infrastructure/LocalPackageStore.cs @@ -5,6 +5,7 @@ namespace Launcher.Next.Infrastructure; public sealed class Sha256FileHasher : IFileHasher { public async Task Sha256Async(string path, CancellationToken cancellationToken) { await using var s = File.OpenRead(path); return Convert.ToHexString(await SHA256.HashDataAsync(s, cancellationToken)); } } +public sealed record ImmutableLocalPackage(string PackageId, string Version, string LocalPackageRoot); public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalPackageStore, IPackageActivator, IRollbackManager, IPackageSynchronizer, ILastKnownGoodPackageResolver, IPackageRequirementCacheBinder { private sealed record Pointer(string Current, string? Previous); @@ -46,13 +47,19 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP return previous is null ? null : new(requirement.PackageId, pointer.Previous, previous, true, "PREVIOUS_VERIFIED_AFTER_CURRENT_REJECTED"); } public async Task SynchronizeAsync(PackageManifest manifest, string sourceRoot, CancellationToken ct, IProgress? progress = null) + { + var materialized = await MaterializeAsync(manifest, sourceRoot, ct, progress); + return await ActivateAsync(materialized.PackageId, materialized.Version, ct); + } + /// Copies and commits an immutable local version; never mutates activation pointers. + public async Task MaterializeAsync(PackageManifest manifest, string sourceRoot, CancellationToken ct, IProgress? progress = null) { if (!new PackageManifestValidator().Validate(manifest, manifest.ProductYear, manifest.Architecture).IsValid) throw new InvalidOperationException("MANIFEST_INVALID"); if (manifest.Files.Any(x => string.Equals(x.Path, StoredManifestFileName, StringComparison.OrdinalIgnoreCase))) throw new InvalidOperationException("MANIFEST_RESERVED_FILE"); var staging = Path.Combine(root, "Launcher", "staging", manifest.PackageId + "-" + manifest.Version + "-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(staging); try { var i = 0; foreach (var f in manifest.Files) { ct.ThrowIfCancellationRequested(); var source = Path.Combine(sourceRoot, f.Path); var dest = PackageFilePath(staging, f.Path); Directory.CreateDirectory(Path.GetDirectoryName(dest)!); File.Copy(source, dest, true); if (!string.Equals(await hasher.Sha256Async(dest, ct), f.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("HASH_MISMATCH"); progress?.Report(new("copy", ++i, manifest.Files.Count)); } await File.WriteAllTextAsync(Path.Combine(staging, StoredManifestFileName), JsonSerializer.Serialize(manifest), ct); - var versionDir = VersionDirectory(manifest.PackageId, manifest.Version); Directory.CreateDirectory(Path.GetDirectoryName(versionDir)!); if (Directory.Exists(versionDir)) Directory.Delete(staging, true); else Directory.Move(staging, versionDir); return await ActivateAsync(manifest.PackageId, manifest.Version, ct); } + var versionDir = VersionDirectory(manifest.PackageId, manifest.Version); Directory.CreateDirectory(Path.GetDirectoryName(versionDir)!); if (Directory.Exists(versionDir)) Directory.Delete(staging, true); else Directory.Move(staging, versionDir); return new(manifest.PackageId, manifest.Version, versionDir); } catch { if (Directory.Exists(staging)) Directory.Delete(staging, true); throw; } } private async Task VerifyVersionAsync(PackageRequirement requirement, string version, CancellationToken ct)