From 549ed746c8ac93b55beace5a5118bf28918b1828 Mon Sep 17 00:00:00 2001 From: Zaldaryon <273555259+Zaldaryon@users.noreply.github.com> Date: Wed, 16 Sep 2026 07:49:01 -0300 Subject: [PATCH] fix(installer): resolve Windows source script paths --- .../BuildLayerTests.cs | 33 ++++++ .../SourceAcquisitionTests.cs | 109 ++++++++++++++++++ Optimum.Bootstrap.Core/Build/RepoRoot.cs | 9 +- .../Build/ScriptBuildDriver.cs | 11 +- .../Build/SourceAcquisition.cs | 18 ++- 5 files changed, 167 insertions(+), 13 deletions(-) diff --git a/Optimum.Bootstrap.Core.Tests/BuildLayerTests.cs b/Optimum.Bootstrap.Core.Tests/BuildLayerTests.cs index 2f52c213..8333c227 100644 --- a/Optimum.Bootstrap.Core.Tests/BuildLayerTests.cs +++ b/Optimum.Bootstrap.Core.Tests/BuildLayerTests.cs @@ -201,3 +201,36 @@ public async Task AnUnspawnableStepExecutableFailsTheBuildInsteadOfHangingTheWiz } } } + +public class ScriptBuildDriverWindowsCommandTests +{ + [Fact] + public void BootstrapCommandUsesAnAbsoluteScriptPathFromTheSourceRoot() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + string repo = Path.Combine(Path.GetTempPath(), "optimum-source"); + BuildRequest request = new(repo, Path.Combine(Path.GetTempPath(), "optimum-output")); + + var command = new ScriptBuildDriver(probe).BootstrapCommand(request); + + Assert.Equal("-File", command.Args[0]); + Assert.Equal(Path.GetFullPath(Path.Combine(repo, "scripts", "bootstrap.ps1")), command.Args[1]); + Assert.True(Path.IsPathFullyQualified(command.Args[1])); + } + + [Fact] + public void PackageCommandUsesAnAbsoluteScriptPathFromTheSourceRoot() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + string repo = Path.Combine(Path.GetTempPath(), "optimum-source"); + string output = Path.Combine(Path.GetTempPath(), "optimum-output"); + BuildRequest request = new(repo, output); + + var command = new ScriptBuildDriver(probe).PackageCommand(request); + + Assert.Equal("-File", command.Args[0]); + Assert.Equal(Path.GetFullPath(Path.Combine(repo, "scripts", "package.ps1")), command.Args[1]); + Assert.True(Path.IsPathFullyQualified(command.Args[1])); + Assert.Equal(output, command.Args[3]); + } +} diff --git a/Optimum.Bootstrap.Core.Tests/SourceAcquisitionTests.cs b/Optimum.Bootstrap.Core.Tests/SourceAcquisitionTests.cs index 105366a9..6bcf935e 100644 --- a/Optimum.Bootstrap.Core.Tests/SourceAcquisitionTests.cs +++ b/Optimum.Bootstrap.Core.Tests/SourceAcquisitionTests.cs @@ -7,6 +7,46 @@ namespace Optimum.Bootstrap.Core.Tests; public class SourceCacheTests { + [Fact] + public void WindowsCacheWithoutScriptsIsNotUsable() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + probe.AddFile("/cache/forks.json"); + + Assert.False(SourceCache.IsUsableCheckout(probe, "/cache")); + } + + [Fact] + public void WindowsCacheWithOnlyUnixBootstrapScriptIsNotUsable() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + probe.AddFile("/cache/forks.json"); + probe.AddFile("/cache/scripts/bootstrap.sh"); + + Assert.False(SourceCache.IsUsableCheckout(probe, "/cache")); + } + + [Fact] + public void WindowsCacheMissingPackagingScriptIsNotUsable() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + probe.AddFile("/cache/forks.json"); + probe.AddFile("/cache/scripts/bootstrap.ps1"); + + Assert.False(SourceCache.IsUsableCheckout(probe, "/cache")); + } + + [Fact] + public void WindowsCacheWithBothRequiredScriptsIsUsable() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + probe.AddFile("/cache/forks.json"); + probe.AddFile("/cache/scripts/bootstrap.ps1"); + probe.AddFile("/cache/scripts/package.ps1"); + + Assert.True(SourceCache.IsUsableCheckout(probe, "/cache")); + } + [Theory] [InlineData("0.3.14", "v0.3.14")] [InlineData("1.0.0", "v1.0.0")] @@ -75,6 +115,40 @@ public void CloneArgumentsOmitTheBranchWhenThereIsNoTag() } } +public class RepoRootTests +{ + [Fact] + public void WindowsDiscoveryRejectsAUnixOnlyCheckout() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + probe.AddFile("/repo/forks.json"); + probe.AddFile("/repo/scripts/bootstrap.sh"); + + Assert.Null(RepoRoot.Discover(probe, "/repo")); + } + + [Fact] + public void WindowsDiscoveryReturnsTheCheckoutWithBothPowerShellScripts() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + probe.AddFile("/repo/forks.json"); + probe.AddFile("/repo/scripts/bootstrap.ps1"); + probe.AddFile("/repo/scripts/package.ps1"); + + Assert.Equal("/repo", RepoRoot.Discover(probe, "/repo")); + } + + [Fact] + public void UnixDiscoveryKeepsItsExistingBootstrapShellContract() + { + var probe = new FakeSystemProbe { Os = OsKind.Linux }; + probe.AddFile("/repo/forks.json"); + probe.AddFile("/repo/scripts/bootstrap.sh"); + + Assert.Equal("/repo", RepoRoot.Discover(probe, "/repo")); + } +} + public class GitSourceProviderTests { [Fact] @@ -118,6 +192,41 @@ public async Task ReusesACachedCheckoutWithoutTouchingGit() Assert.Equal(cached, result.RepoRoot); } + [Fact] + public async Task DoesNotReuseAnIncompleteWindowsCheckout() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + probe.Environment["LOCALAPPDATA"] = "/cache"; + string cached = "/cache/optimum/src-v0.3.14"; + probe.AddFile($"{cached}/forks.json"); + probe.AddFile($"{cached}/scripts/bootstrap.sh"); + // No Git on PATH: an incomplete cache must be rejected, not reused. + + var result = await new GitSourceProvider(probe) + .EnsureAsync(new SourceRequest("0.3.14"), NullBuildObserver.Instance, CancellationToken.None); + + Assert.False(result.Ok); + Assert.Equal(FailureReason.SourceUnavailable, result.Reason); + } + + [Fact] + public async Task ReusesACompleteWindowsCheckoutFromTheSourceCache() + { + var probe = new FakeSystemProbe { Os = OsKind.Windows }; + probe.Environment["LOCALAPPDATA"] = "/cache"; + string cached = "/cache/optimum/src-v0.3.14"; + probe.AddFile($"{cached}/forks.json"); + probe.AddFile($"{cached}/scripts/bootstrap.ps1"); + probe.AddFile($"{cached}/scripts/package.ps1"); + // No Git on PATH: proves the complete cached source is the one used. + + var result = await new GitSourceProvider(probe) + .EnsureAsync(new SourceRequest("0.3.14"), NullBuildObserver.Instance, CancellationToken.None); + + Assert.True(result.Ok); + Assert.Equal(cached, result.RepoRoot); + } + [Fact] public async Task FailsWithSourceUnavailableWhenGitIsMissing() { diff --git a/Optimum.Bootstrap.Core/Build/RepoRoot.cs b/Optimum.Bootstrap.Core/Build/RepoRoot.cs index 47cbeb57..fc94e6d2 100644 --- a/Optimum.Bootstrap.Core/Build/RepoRoot.cs +++ b/Optimum.Bootstrap.Core/Build/RepoRoot.cs @@ -4,9 +4,9 @@ namespace Optimum.Bootstrap.Core.Build; /// /// Finds the Optimum checkout the engine has to drive: the nearest directory at -/// or above a starting point that holds forks.json next to -/// scripts/bootstrap.sh. Both front ends need this because the build -/// pipeline is still the shell scripts (INSTALLER-PLAN.md section 2). +/// or above a starting point that holds the manifest and the scripts required +/// by the probed platform. Both front ends need this because the build pipeline +/// is still the platform scripts (INSTALLER-PLAN.md section 2). /// public static class RepoRoot { @@ -18,8 +18,7 @@ public static class RepoRoot for (string? dir = start; dir is not null; dir = Path.GetDirectoryName(dir)) { - if (probe.FileExists(Path.Combine(dir, "forks.json")) - && probe.FileExists(Path.Combine(dir, "scripts", "bootstrap.sh"))) + if (SourceCache.IsUsableCheckout(probe, dir)) return dir; } diff --git a/Optimum.Bootstrap.Core/Build/ScriptBuildDriver.cs b/Optimum.Bootstrap.Core/Build/ScriptBuildDriver.cs index d9447d4c..56be4571 100644 --- a/Optimum.Bootstrap.Core/Build/ScriptBuildDriver.cs +++ b/Optimum.Bootstrap.Core/Build/ScriptBuildDriver.cs @@ -111,11 +111,11 @@ public async Task RunAsync( } } - private (string Exe, IReadOnlyList Args) BootstrapCommand(BuildRequest request) + internal (string Exe, IReadOnlyList Args) BootstrapCommand(BuildRequest request) { if (probe.Os == OsKind.Windows) { - List win = ["-File", "scripts/bootstrap.ps1"]; + List win = ["-File", ScriptPath(request.RepoRoot, "bootstrap.ps1")]; if (request.ClientArchive is not null) win.AddRange(["-ClientArchive", request.ClientArchive]); if (request.Version is not null) @@ -131,13 +131,13 @@ public async Task RunAsync( return ("bash", unix); } - private (string Exe, IReadOnlyList Args) PackageCommand(BuildRequest request) + internal (string Exe, IReadOnlyList Args) PackageCommand(BuildRequest request) { string output = request.OutputDirectory; switch (probe.Os) { case OsKind.Windows: - List win = ["-File", "scripts/package.ps1", "-OutputDir", output]; + List win = ["-File", ScriptPath(request.RepoRoot, "package.ps1"), "-OutputDir", output]; if (request.ClientArchive is not null) win.AddRange(["-ClientArchive", request.ClientArchive]); return (PwshExecutable(), win); case OsKind.MacOs: @@ -154,6 +154,9 @@ public async Task RunAsync( } } + private static string ScriptPath(string repoRoot, string scriptName) => + Path.GetFullPath(Path.Combine(repoRoot, "scripts", scriptName)); + private string DotnetExecutable() => DotnetSdkProbe.Find(probe) ?? "dotnet"; /// diff --git a/Optimum.Bootstrap.Core/Build/SourceAcquisition.cs b/Optimum.Bootstrap.Core/Build/SourceAcquisition.cs index b93ca7c1..6b134217 100644 --- a/Optimum.Bootstrap.Core/Build/SourceAcquisition.cs +++ b/Optimum.Bootstrap.Core/Build/SourceAcquisition.cs @@ -87,10 +87,20 @@ internal static string SanitizeVersion(string version) return v.Length > 1 && v[0] == 'v' && char.IsDigit(v[1]) ? v : null; } - /// True when a directory holds the two files the pipeline needs. - public static bool IsUsableCheckout(ISystemProbe probe, string directory) => - probe.FileExists(Path.Combine(directory, "forks.json")) - && probe.FileExists(Path.Combine(directory, "scripts", "bootstrap.sh")); + /// + /// True when a directory holds the manifest and the scripts required by the + /// platform's build and packaging pipeline. + /// + public static bool IsUsableCheckout(ISystemProbe probe, string directory) + { + if (!probe.FileExists(Path.Combine(directory, "forks.json"))) + return false; + + return probe.Os == OsKind.Windows + ? probe.FileExists(Path.Combine(directory, "scripts", "bootstrap.ps1")) + && probe.FileExists(Path.Combine(directory, "scripts", "package.ps1")) + : probe.FileExists(Path.Combine(directory, "scripts", "bootstrap.sh")); + } internal static IReadOnlyList CloneArguments(string? tagRef, string targetDirectory) {