Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/windows-packaging-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,25 @@ jobs:
-RepoRoot "${{ github.workspace }}" `
-AppVersion $appVersion
$uvExe = Join-Path $out "tools/uv/uv.exe"
$pythonExe = Join-Path $out "tools/python/python.exe"
$nodeExe = Join-Path $out "tools/node/node.exe"
$chromeExe = Get-ChildItem -Path (Join-Path $out "tools/chrome") -Recurse -Filter "chrome.exe" -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "chrome-win" } |
Select-Object -First 1
if (-not (Test-Path $uvExe)) {
throw "uv executable not found in staging: $uvExe"
}
if (-not (Test-Path $pythonExe)) {
throw "python executable not found in staging: $pythonExe"
}
if (-not (Test-Path $nodeExe)) {
throw "node executable not found in staging: $nodeExe"
}
if (-not $chromeExe) {
throw "chrome executable not found in staging under tools/chrome"
}
$uvVersion = (& $uvExe --version).Trim()
$pythonVersion = (& $pythonExe --version).Trim()
$nodeVersion = (& $nodeExe --version).Trim()
$chromeVersion = (Get-Item -LiteralPath $chromeExe.FullName).VersionInfo.ProductVersion
if ([string]::IsNullOrWhiteSpace($chromeVersion)) {
Expand All @@ -71,13 +76,18 @@ jobs:
}
Write-Host "[runtime] pinned uv version: $($manifest.uv.version)"
Write-Host "[runtime] bundled uv version: $uvVersion"
Write-Host "[runtime] pinned python version: $($manifest.python.version)"
Write-Host "[runtime] bundled python version: $pythonVersion"
Write-Host "[runtime] pinned node version: $($manifest.nodejs.version)"
Write-Host "[runtime] bundled node version: $nodeVersion"
Write-Host "[runtime] pinned chrome version: $($manifest.chrome_for_testing.version)"
Write-Host "[runtime] bundled chrome version: $chromeVersion"
if ($uvVersion -notmatch ("^uv\s+" + [regex]::Escape($manifest.uv.version) + "(\s|$)")) {
throw "Bundled uv version does not match pinned version in manifest"
}
if ($pythonVersion -ne ("Python " + $manifest.python.version)) {
throw "Bundled python version does not match pinned version in manifest"
}
if ($nodeVersion -ne ("v" + $manifest.nodejs.version)) {
throw "Bundled node version does not match pinned version in manifest"
}
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/windows-packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,25 @@ jobs:
$manifest = Get-Content -Path $manifestPath -Raw -Encoding utf8 | ConvertFrom-Json
& "${{ github.workspace }}/packaging/windows/build-installer.ps1" -OutputDir $out -RepoRoot "${{ github.workspace }}"
$uvExe = Join-Path $out "tools/uv/uv.exe"
$pythonExe = Join-Path $out "tools/python/python.exe"
$nodeExe = Join-Path $out "tools/node/node.exe"
$chromeExe = Get-ChildItem -Path (Join-Path $out "tools/chrome") -Recurse -Filter "chrome.exe" -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "chrome-win" } |
Select-Object -First 1
if (-not (Test-Path $uvExe)) {
throw "uv executable not found in staging: $uvExe"
}
if (-not (Test-Path $pythonExe)) {
throw "python executable not found in staging: $pythonExe"
}
if (-not (Test-Path $nodeExe)) {
throw "node executable not found in staging: $nodeExe"
}
if (-not $chromeExe) {
throw "chrome executable not found in staging under tools/chrome"
}
$uvVersion = (& $uvExe --version).Trim()
$pythonVersion = (& $pythonExe --version).Trim()
$nodeVersion = (& $nodeExe --version).Trim()
$chromeVersion = (Get-Item -LiteralPath $chromeExe.FullName).VersionInfo.ProductVersion
if ([string]::IsNullOrWhiteSpace($chromeVersion)) {
Expand All @@ -66,13 +71,18 @@ jobs:
}
Write-Host "[runtime] pinned uv version: $($manifest.uv.version)"
Write-Host "[runtime] bundled uv version: $uvVersion"
Write-Host "[runtime] pinned python version: $($manifest.python.version)"
Write-Host "[runtime] bundled python version: $pythonVersion"
Write-Host "[runtime] pinned node version: $($manifest.nodejs.version)"
Write-Host "[runtime] bundled node version: $nodeVersion"
Write-Host "[runtime] pinned chrome version: $($manifest.chrome_for_testing.version)"
Write-Host "[runtime] bundled chrome version: $chromeVersion"
if ($uvVersion -notmatch ("^uv\s+" + [regex]::Escape($manifest.uv.version) + "(\s|$)")) {
throw "Bundled uv version does not match pinned version in manifest"
}
if ($pythonVersion -ne ("Python " + $manifest.python.version)) {
throw "Bundled python version does not match pinned version in manifest"
}
if ($nodeVersion -ne ("v" + $manifest.nodejs.version)) {
throw "Bundled node version does not match pinned version in manifest"
}
Expand Down
31 changes: 31 additions & 0 deletions packaging/windows/bootstrap-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ function Add-UserPathEntryIfMissing {
}
}

function Add-ProcessPathEntryIfMissing {
param([string]$Entry)

if ([string]::IsNullOrWhiteSpace($Entry)) { return }

$processPath = $env:Path
if ([string]::IsNullOrWhiteSpace($processPath)) {
$env:Path = $Entry
return
}

$existing = $processPath -split ';' | Where-Object { ($_.TrimEnd('\','/')).ToLower() -eq $Entry.TrimEnd('\','/').ToLower() }
if (-not $existing) {
$env:Path = "$Entry;$processPath"
}
}

function Resolve-ChromeExecutablePath {
param([string]$BrowserRoot)

Expand Down Expand Up @@ -107,6 +124,20 @@ else {
Write-Host "[flocks-bootstrap] warning: bundled node not found at $bundledNode" -ForegroundColor Yellow
}

$bundledPythonDir = Join-Path $InstallRoot "tools\python"
$bundledPython = Join-Path $bundledPythonDir "python.exe"
if (Test-Path $bundledPython) {
Add-ProcessPathEntryIfMissing -Entry $bundledPythonDir
$env:FLOCKS_BUNDLED_PYTHON = $bundledPython
$env:UV_PYTHON = $bundledPython
$env:UV_PYTHON_DOWNLOADS = "never"
$env:UV_NO_MANAGED_PYTHON = "1"
Write-Host "[flocks-bootstrap] configured bundled Python runtime: $bundledPython"
}
else {
Write-Host "[flocks-bootstrap] warning: bundled Python runtime not found at $bundledPython" -ForegroundColor Yellow
}

# 2) Expose bundled Chrome for Testing under ~/.flocks/browser so install.ps1's
# Resolve-ChromeForTestingPath finds it and skips the real download.
# Prefer a directory junction (fast, no disk duplication) and fall back to copy.
Expand Down
58 changes: 58 additions & 0 deletions packaging/windows/build-staging.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,35 @@ function Get-OrDownloadFileFromCandidates {
throw "Failed to download $Label"
}

function Expand-TarGzArchive {
param(
[Parameter(Mandatory = $true)][string]$ArchivePath,
[Parameter(Mandatory = $true)][string]$DestinationPath
)

$tarExe = Get-Command tar.exe -ErrorAction SilentlyContinue
if (-not $tarExe) {
throw "tar.exe is required to extract $ArchivePath"
}

Remove-PathWithRetry -Path $DestinationPath
New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null

& $tarExe.Source -xzf $ArchivePath -C $DestinationPath
if ($LASTEXITCODE -ne 0) {
throw "tar.exe failed to extract $ArchivePath with exit code $LASTEXITCODE"
}
$global:LASTEXITCODE = 0
}

Write-Host "[build-staging] RepoRoot: $RepoRoot"
Write-Host "[build-staging] OutputDir: $OutputDir"

$manifest = Read-Manifest -Path $ManifestPath
$uvVersion = $manifest.uv.version
$pythonVersion = $manifest.python.version
$pythonStandaloneRelease = $manifest.python.python_build_standalone_release
$pythonArchiveName = $manifest.python.windows_archive_name
$nodeVersion = $manifest.nodejs.version
$nodeSuffix = $manifest.nodejs.windows_zip_suffix
$cacheRoot = Resolve-CacheRoot -RepoRoot $RepoRoot -CacheRootOverride $CacheRoot
Expand All @@ -170,11 +194,13 @@ Write-Host "[build-staging] CacheRoot: $cacheRoot"
Ensure-EmptyDir -Path $OutputDir

$toolsUv = Join-Path $OutputDir "tools\uv"
$toolsPython = Join-Path $OutputDir "tools\python"
$toolsNode = Join-Path $OutputDir "tools\node"
$toolsChrome = Join-Path $OutputDir "tools\chrome"
$flocksDest = Join-Path $OutputDir "flocks"

New-Item -ItemType Directory -Path $toolsUv -Force | Out-Null
New-Item -ItemType Directory -Path $toolsPython -Force | Out-Null
New-Item -ItemType Directory -Path $toolsNode -Force | Out-Null
New-Item -ItemType Directory -Path $toolsChrome -Force | Out-Null

Expand All @@ -185,6 +211,38 @@ $uvZip = Join-Path $cacheRoot "downloads\uv-$uvVersion-$uvZipName"
Get-OrDownloadFile -Url $uvUrl -CachePath $uvZip -Label "uv $uvVersion"
Expand-Archive -Path $uvZip -DestinationPath $toolsUv -Force

# Python runtime (python-build-standalone install-only archive)
$pythonArchiveNameEscaped = [Uri]::EscapeDataString($pythonArchiveName)
$pythonMirrorBase = $env:FLOCKS_PYTHON_STANDALONE_MIRROR_BASE_URL
$pythonUrls = @()
if (-not [string]::IsNullOrWhiteSpace($pythonMirrorBase)) {
$mirrorBase = $pythonMirrorBase.TrimEnd('/')
$pythonUrls += "$mirrorBase/$pythonStandaloneRelease/$pythonArchiveNameEscaped"
Write-Host "[build-staging] Added python-build-standalone mirror candidate from FLOCKS_PYTHON_STANDALONE_MIRROR_BASE_URL"
}
$pythonUrls += "https://github.com/astral-sh/python-build-standalone/releases/download/$pythonStandaloneRelease/$pythonArchiveNameEscaped"
$pythonArchive = Join-Path $cacheRoot "downloads\python-$pythonVersion-$pythonStandaloneRelease-$pythonArchiveName"
Get-OrDownloadFileFromCandidates -Urls $pythonUrls -CachePath $pythonArchive -Label "Python $pythonVersion"

$pythonExtract = Join-Path $env:TEMP "python-extract-$pythonVersion-$pythonStandaloneRelease"
Expand-TarGzArchive -ArchivePath $pythonArchive -DestinationPath $pythonExtract
$pythonExe = Get-ChildItem -Path $pythonExtract -Recurse -Filter "python.exe" -File -ErrorAction SilentlyContinue |
Where-Object { $_.DirectoryName -notmatch '\\DLLs($|\\)' } |
Select-Object -First 1
if (-not $pythonExe) {
throw "python.exe not found after extracting bundled Python runtime"
}
$pythonSource = $pythonExe.Directory.FullName
robocopy $pythonSource $toolsPython /E /NFL /NDL /NJH /NJS /nc /ns /np | Out-Null
if ($LASTEXITCODE -ge 8) {
throw "robocopy failed while copying bundled Python with exit code $LASTEXITCODE"
}
$global:LASTEXITCODE = 0
Remove-PathWithRetry -Path $pythonExtract
if (-not (Test-Path (Join-Path $toolsPython "python.exe"))) {
throw "Bundled Python runtime missing python.exe under tools\python after extraction"
}

# Node.js official zip (portable)
$nodeZipName = "node-v$nodeVersion-$nodeSuffix.zip"
$nodeUrl = "https://nodejs.org/dist/v$nodeVersion/$nodeZipName"
Expand Down
5 changes: 5 additions & 0 deletions packaging/windows/versions.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
"uv": {
"version": "0.9.15"
},
"python": {
"version": "3.12.12",
"python_build_standalone_release": "20251202",
"windows_archive_name": "cpython-3.12.12+20251202-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
},
"nodejs": {
"version": "24.14.0",
"windows_zip_suffix": "win-x64"
Expand Down
11 changes: 11 additions & 0 deletions tests/packaging/test_windows_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ def test_windows_bundled_uv_supports_python_downloads_json_url() -> None:
manifest = json.loads(WINDOWS_MANIFEST.read_text(encoding="utf-8"))

assert _parse_version(manifest["uv"]["version"]) >= (0, 7, 3)


def test_windows_manifest_pins_bundled_python_runtime() -> None:
manifest = json.loads(WINDOWS_MANIFEST.read_text(encoding="utf-8"))

python = manifest["python"]
assert _parse_version(python["version"]) >= (3, 12, 0)
assert python["python_build_standalone_release"].isdigit()
assert python["windows_archive_name"].endswith(".tar.gz")
assert "install_only" in python["windows_archive_name"]
assert "windows-msvc" in python["windows_archive_name"]
16 changes: 16 additions & 0 deletions tests/scripts/test_browser_runtime_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,33 @@ def test_powershell_bootstrap_wires_bundled_toolchain() -> None:
"""packaging/windows/bootstrap-windows.ps1 is the single place that bridges the bundled layout to install.ps1."""
script = (PACKAGING_WINDOWS_DIR / "bootstrap-windows.ps1").read_text(encoding="utf-8-sig")

assert "Add-ProcessPathEntryIfMissing" in script
assert "Resolve-ChromeExecutablePath" in script
assert "FLOCKS_SKIP_ADMIN_CHECK" in script
assert "FLOCKS_BROWSER_EXECUTABLE_OVERRIDE" in script
assert "FLOCKS_BUNDLED_PYTHON" in script
assert "UV_PYTHON" in script
assert "UV_PYTHON_DOWNLOADS" in script
assert "UV_NO_MANAGED_PYTHON" in script
assert "tools\\uv" in script
assert "tools\\python" in script
assert "tools\\node" in script
assert "tools\\chrome" in script
assert ".flocks\\browser" in script
assert "mklink /J" in script
assert 'scripts\\install_zh.ps1' in script


def test_build_staging_bundles_python_runtime() -> None:
script = (PACKAGING_WINDOWS_DIR / "build-staging.ps1").read_text(encoding="utf-8-sig")

assert "tools\\python" in script
assert "python-build-standalone" in script
assert "python.exe" in script
assert "tar.exe" in script
assert "FLOCKS_PYTHON_STANDALONE_MIRROR_BASE_URL" in script


def test_inno_setup_points_to_packaging_bootstrap() -> None:
"""flocks-setup.iss must invoke the bootstrap from its new packaging location."""
iss = (PACKAGING_WINDOWS_DIR / "flocks-setup.iss").read_text(encoding="utf-8")
Expand Down
Loading