diff --git a/.gitignore b/.gitignore index f74f096..738eda3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ .pytest_cache/ .venv/ .uv-cache/ +/.build/ # Sensitive/raw acquisition data belongs in ThePirateRay-PrivateStash. /raw/ diff --git a/docs/research/rayforge-windows-gui-preflight.md b/docs/research/rayforge-windows-gui-preflight.md index 5cf0407..f6a4f7d 100644 --- a/docs/research/rayforge-windows-gui-preflight.md +++ b/docs/research/rayforge-windows-gui-preflight.md @@ -37,9 +37,22 @@ toolchain is a separate, reviewable action: upstream's setup includes MSYS2, GTK, compiler/build packages and network downloads, so it should not be an implicit side effect of a workflow test. +## On-demand source and build policy + +The public repository records the upstream URL and exact commit in +`upstreams/rayforge.json`. `scripts/prepare_rayforge_build.ps1` fetches that +revision into the ignored `.build/upstreams/` cache only when explicitly +invoked. Its default action prepares source only; it neither installs the +Windows toolchain nor starts a build. + +Windows dependencies and bundle creation require separate +`-InstallWindowsDependencies` and `-BuildWindowsBundle` switches. This keeps a +multi-gigabyte MSYS2/UCRT64 toolchain out of normal clone, test and documentation +workflows and makes a CI-produced bundle the preferred interaction-test input. + ## Next action -Provide an already reviewed Rayforge Windows build/runtime, or explicitly -approve installation of the pinned checkout's MSYS2/UCRT64 dependencies. Then -rerun the preflight and perform the packaged no-device interaction checklist -from `rayforge-graphical-workflow-spike.md`. +Produce or obtain an artifact for the pinned revision through the explicit +on-demand build path, then rerun the preflight and perform the packaged +no-device interaction checklist from `rayforge-graphical-workflow-spike.md`. +Do not make the Windows build toolchain an implicit project dependency. diff --git a/scripts/prepare_rayforge_build.ps1 b/scripts/prepare_rayforge_build.ps1 new file mode 100644 index 0000000..28a07db --- /dev/null +++ b/scripts/prepare_rayforge_build.ps1 @@ -0,0 +1,94 @@ +[CmdletBinding(SupportsShouldProcess)] +param( + [string]$CacheRoot, + [switch]$InstallWindowsDependencies, + [switch]$BuildWindowsBundle +) + +$ErrorActionPreference = "Stop" + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$manifestPath = Join-Path $repositoryRoot "upstreams\rayforge.json" +$manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json + +if (-not $CacheRoot) { + $CacheRoot = Join-Path $repositoryRoot ".build\upstreams" +} + +$shortCommit = $manifest.commit.Substring(0, 12) +$checkout = Join-Path $CacheRoot "rayforge-$shortCommit" + +if (-not (Test-Path -LiteralPath $checkout)) { + if (-not $PSCmdlet.ShouldProcess($checkout, "Clone pinned Rayforge source")) { + return + } + + New-Item -ItemType Directory -Force -Path $CacheRoot | Out-Null + & git clone --filter=blob:none --no-checkout $manifest.repository $checkout + if ($LASTEXITCODE -ne 0) { + throw "Rayforge clone failed with exit code $LASTEXITCODE" + } +} + +if (-not (Test-Path -LiteralPath (Join-Path $checkout ".git"))) { + throw "Existing cache path is not a Git checkout: $checkout" +} + +$currentCommit = (& git -C $checkout rev-parse HEAD 2>$null) +if ($LASTEXITCODE -ne 0 -or $currentCommit -ne $manifest.commit) { + if (-not $PSCmdlet.ShouldProcess($checkout, "Fetch and check out pinned commit $($manifest.commit)")) { + return + } + + & git -C $checkout fetch --depth 1 origin $manifest.commit + if ($LASTEXITCODE -ne 0) { + throw "Rayforge fetch failed with exit code $LASTEXITCODE" + } + & git -C $checkout checkout --detach $manifest.commit + if ($LASTEXITCODE -ne 0) { + throw "Rayforge checkout failed with exit code $LASTEXITCODE" + } +} + +$verifiedCommit = (& git -C $checkout rev-parse HEAD).Trim() +if ($verifiedCommit -ne $manifest.commit) { + throw "Rayforge pin mismatch: expected $($manifest.commit), got $verifiedCommit" +} + +Write-Output "Rayforge source ready at $checkout" +Write-Output "Verified commit: $verifiedCommit" + +if (-not $InstallWindowsDependencies -and -not $BuildWindowsBundle) { + Write-Output "No dependencies were installed and no build was started." + return +} + +$bash = "C:\msys64\usr\bin\bash.exe" +if (-not (Test-Path -LiteralPath $bash)) { + throw "MSYS2 is required for the upstream Windows build. Install it explicitly before using build flags." +} + +$msysCheckout = (& $bash -lc "cygpath -u '$checkout'").Trim() + +if ($InstallWindowsDependencies) { + if ($PSCmdlet.ShouldProcess("C:\msys64", "Install Rayforge Windows build dependencies")) { + & $bash -lc "cd '$msysCheckout' && bash scripts/win/win_setup.sh" + if ($LASTEXITCODE -ne 0) { + throw "Rayforge Windows dependency setup failed with exit code $LASTEXITCODE" + } + } +} + +if ($BuildWindowsBundle) { + $environmentFile = Join-Path $checkout ".msys2_env" + if (-not (Test-Path -LiteralPath $environmentFile)) { + throw "Windows dependencies are not prepared. Re-run with -InstallWindowsDependencies." + } + + if ($PSCmdlet.ShouldProcess($checkout, "Build pinned Rayforge Windows bundle")) { + & $bash -lc "cd '$msysCheckout' && bash scripts/win/win_build.sh 'pirateray-$shortCommit'" + if ($LASTEXITCODE -ne 0) { + throw "Rayforge Windows build failed with exit code $LASTEXITCODE" + } + } +} diff --git a/tests/test_upstream_build_reference.py b/tests/test_upstream_build_reference.py new file mode 100644 index 0000000..9b89d37 --- /dev/null +++ b/tests/test_upstream_build_reference.py @@ -0,0 +1,35 @@ +import json +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "upstreams" / "rayforge.json" +PREPARE = ROOT / "scripts" / "prepare_rayforge_build.ps1" +PIN = "c28918c87f8273dd1d244f5a057addc192807fa5" + + +class UpstreamBuildReferenceTests(unittest.TestCase): + def test_rayforge_reference_is_exact_and_https(self): + data = json.loads(MANIFEST.read_text(encoding="utf-8")) + self.assertEqual(data["commit"], PIN) + self.assertEqual( + data["repository"], "https://github.com/barebaric/rayforge.git" + ) + + def test_prepare_script_is_on_demand_and_uses_ignored_cache(self): + script = PREPARE.read_text(encoding="utf-8") + self.assertIn('".build\\upstreams"', script) + self.assertIn("SupportsShouldProcess", script) + self.assertIn("InstallWindowsDependencies", script) + self.assertIn("BuildWindowsBundle", script) + self.assertIn("No dependencies were installed", script) + + def test_prepare_script_has_no_hardware_path(self): + script = PREPARE.read_text(encoding="utf-8").lower() + for forbidden in ("pyusb", "usb.core", "9588", "9899", "frame", "mark"): + self.assertNotIn(forbidden, script) + + +if __name__ == "__main__": + unittest.main() diff --git a/upstreams/rayforge.json b/upstreams/rayforge.json new file mode 100644 index 0000000..1639690 --- /dev/null +++ b/upstreams/rayforge.json @@ -0,0 +1,7 @@ +{ + "name": "Rayforge", + "repository": "https://github.com/barebaric/rayforge.git", + "commit": "c28918c87f8273dd1d244f5a057addc192807fa5", + "license": "MIT", + "purpose": "Pinned host application for offline reuse and JCZ addon validation" +}