Overhaul Windows container build for PATH and tool reliability

- Add explicit ENV PATH directive (Phase 15) so Docker layers
  inherit tool paths instead of relying on registry writes
- Reorder phases: TeX Live moved last (slowest, fail last)
- Create stable symlink C:\texlive\bin\windows for year-agnostic PATH
- Skip pip self-upgrade to avoid WinError 3 shim lock
- Use gswin64c (correct Scoop binary name) instead of gs
- Add rsvg-convert fallback to Chocolatey if Scoop fails
- Replace fragile verification loop with Test-Tool function
- Relax ErrorActionPreference for Chocolatey TeX Live in baremetal
This commit is contained in:
Vijay Janapa Reddi
2026-03-04 17:16:34 -05:00
parent fc0290df44
commit 12268748b2
3 changed files with 174 additions and 117 deletions

View File

@@ -540,6 +540,28 @@ jobs:
inkscape --version
Write-Output "✅ Inkscape installation complete"
- name: 🧩 Install rsvg-convert (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
run: |
Write-Output "=== INSTALLING RSVG-CONVERT ==="
Write-Output "Required for Quarto PDF SVG conversion when use-rsvg-convert: true"
scoop install rsvg-convert
if (-not (Get-Command rsvg-convert -ErrorAction SilentlyContinue)) {
Write-Output "⚠️ Scoop install failed; trying Chocolatey fallback..."
choco install rsvg-convert -y
}
if (-not (Get-Command rsvg-convert -ErrorAction SilentlyContinue)) {
Write-Error "❌ rsvg-convert installation failed via both Scoop and Chocolatey"
exit 1
}
Write-Output "📊 rsvg-convert version:"
rsvg-convert --version
Write-Output "✅ rsvg-convert installation complete"
- name: 📦 Install Ghostscript via Scoop (Windows)
if: matrix.enabled && runner.os == 'Windows'
shell: pwsh
@@ -552,7 +574,14 @@ jobs:
Write-Output "✅ Ghostscript installed"
Write-Output "📊 Ghostscript version:"
gs --version
if (Get-Command gswin64c -ErrorAction SilentlyContinue) {
gswin64c --version
} elseif (Get-Command gs -ErrorAction SilentlyContinue) {
gs --version
} else {
Write-Error "❌ Ghostscript executable not found (expected gswin64c or gs)"
exit 1
}
Write-Output "✅ Ghostscript installation complete"
- name: 📦 Install Visual C++ Redistributable (Windows)
@@ -776,10 +805,17 @@ jobs:
Write-Output "This matches the Windows container setup (Phase 4)"
Write-Output "📦 Installing TeX Live via Chocolatey (pinned to 2025.20251008.0)..."
choco install texlive --version=2025.20251008.0 -y
# Temporarily relax ErrorActionPreference — Chocolatey's TeX Live
# installer emits non-fatal RemoteException messages that PowerShell's
# strict mode would otherwise terminate on.
$prevPref = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
choco install texlive --version=2025.20251008.0 -y --no-progress
$chocoExit = $LASTEXITCODE
$ErrorActionPreference = $prevPref
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ TeX Live installation failed"
if ($chocoExit -ne 0) {
Write-Error "❌ TeX Live installation failed (exit code: $chocoExit)"
exit 1
}
Write-Output "✅ TeX Live installed via Chocolatey"
@@ -844,6 +880,8 @@ jobs:
package_file: ${{ vars.BOOK_DEPS }}/tl_packages
texlive_version: 2025
cache_version: 1
env:
CTAN_MIRROR: https://ctan.math.illinois.edu/systems/texlive/tlnet
- name: 🔍 Verify TeX Live Installation
if: matrix.enabled
@@ -1016,78 +1054,50 @@ jobs:
Write-Output "📊 TOOL VERIFICATION:"
Write-Output "---------------------"
$failures = @()
function Test-Tool {
param(
[string]$Name,
[string[]]$Candidates,
[string[]]$Args
)
# -- Quarto --
Write-Output "Checking Quarto..."
try {
$qv = & quarto --version 2>&1
if ($LASTEXITCODE -ne 0) { throw "quarto --version exited with $LASTEXITCODE" }
Write-Output " Quarto $qv"
Write-Output "✅ Quarto verified"
} catch {
Write-Output "❌ Quarto verification FAILED: $($_.Exception.Message)"
$failures += "Quarto"
Write-Output "Checking $Name..."
$resolved = $null
$cmdToRun = $null
foreach ($candidate in $Candidates) {
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
if ($cmd) {
$resolved = $cmd
$cmdToRun = $candidate
break
}
}
if (-not $resolved) {
Write-Output "❌ $Name verification FAILED: command not found ($($Candidates -join ', '))"
return $false
}
Write-Output " Resolved: $($resolved.Source)"
$out = & $cmdToRun @Args 2>&1 | Select-Object -First 1
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE }
if ($exitCode -ne 0) {
Write-Output "❌ $Name verification FAILED: $cmdToRun exited with $exitCode"
return $false
}
Write-Output " $out"
Write-Output "✅ $Name verified"
return $true
}
# -- Python --
Write-Output "Checking Python..."
try {
$pv = & python --version 2>&1
if ($LASTEXITCODE -ne 0) { throw "python --version exited with $LASTEXITCODE" }
Write-Output " $pv"
Write-Output "✅ Python verified"
} catch {
Write-Output "❌ Python verification FAILED: $($_.Exception.Message)"
$failures += "Python"
}
# -- R --
Write-Output "Checking R..."
try {
$rv = & Rscript --version 2>&1
if ($LASTEXITCODE -ne 0) { throw "Rscript --version exited with $LASTEXITCODE" }
Write-Output " $rv"
Write-Output "✅ R verified"
} catch {
Write-Output "❌ R verification FAILED: $($_.Exception.Message)"
$failures += "R"
}
# -- LaTeX --
Write-Output "Checking LaTeX..."
try {
$lv = & lualatex --version 2>&1 | Select-Object -First 1
if ($LASTEXITCODE -ne 0) { throw "lualatex --version exited with $LASTEXITCODE" }
Write-Output " $lv"
Write-Output "✅ LaTeX verified"
} catch {
Write-Output "❌ LaTeX verification FAILED: $($_.Exception.Message)"
$failures += "LaTeX"
}
# -- Ghostscript --
Write-Output "Checking Ghostscript..."
try {
$gv = & gs --version 2>&1
if ($LASTEXITCODE -ne 0) { throw "gs --version exited with $LASTEXITCODE" }
Write-Output " Ghostscript $gv"
Write-Output "✅ Ghostscript verified"
} catch {
Write-Output "❌ Ghostscript verification FAILED: $($_.Exception.Message)"
$failures += "Ghostscript"
}
# -- Inkscape --
Write-Output "Checking Inkscape..."
try {
$iv = & inkscape --version 2>&1
if ($LASTEXITCODE -ne 0) { throw "inkscape --version exited with $LASTEXITCODE" }
Write-Output " $iv"
Write-Output "✅ Inkscape verified"
} catch {
Write-Output "❌ Inkscape verification FAILED: $($_.Exception.Message)"
$failures += "Inkscape"
}
if (-not (Test-Tool -Name "Quarto" -Candidates @("quarto") -Args @("--version"))) { $failures += "Quarto" }
if (-not (Test-Tool -Name "Python" -Candidates @("python") -Args @("--version"))) { $failures += "Python" }
if (-not (Test-Tool -Name "R" -Candidates @("Rscript") -Args @("--version"))) { $failures += "R" }
if (-not (Test-Tool -Name "LaTeX" -Candidates @("lualatex") -Args @("--version"))) { $failures += "LaTeX" }
if (-not (Test-Tool -Name "Ghostscript" -Candidates @("gswin64c","gs") -Args @("--version"))) { $failures += "Ghostscript" }
if (-not (Test-Tool -Name "Inkscape" -Candidates @("inkscape") -Args @("--version"))) { $failures += "Inkscape" }
if (-not (Test-Tool -Name "rsvg-convert" -Candidates @("rsvg-convert") -Args @("--version"))) { $failures += "rsvg-convert" }
# -- Final verdict --
Write-Output ""
@@ -1378,7 +1388,7 @@ jobs:
if: matrix.enabled && always() # Upload logs even if build fails
uses: actions/upload-artifact@v6
with:
name: build-logs-${{ matrix.os_name }}-${{ matrix.format }}-${{ github.run_id }}
name: build-logs-${{ matrix.os_name }}-${{ matrix.format }}-${{ matrix.volume }}-${{ github.run_id }}
path: logs/
if-no-files-found: warn

View File

@@ -3,7 +3,7 @@
# - PowerShell 7 via ZIP (no MSI)
# - Quarto via Scoop (extras bucket)
# - Python 3.13.1 + requirements
# - Ghostscript + Inkscape (Chocolatey)
# - Ghostscript + Inkscape (Scoop)
# - TeX Live (latest by default; overridable via TEXLIVE_VERSION build arg) + packages from tl_packages
# - R 4.5.2 + packages via install_packages.R
# - Verifications: versions, kpsewhich font files, TikZ smoke test
@@ -109,15 +109,7 @@ COPY book/docker/windows/install_texlive.ps1 C:/temp/install_texlive.ps1
RUN Write-Host '✅ Dependency file copy complete'
# ------------------------------------------------------------
# PHASE 4: Install TeX Live FIRST (Most complex, fail fast)
# Direct install-tl approach via standalone script:
# - Bypasses Chocolatey's ErrorActionPreference=Stop wrapper
# - Pins mirror in profile, tries 3 mirrors in order
# ------------------------------------------------------------
RUN & 'C:\temp\install_texlive.ps1'
# ------------------------------------------------------------
# PHASE 5: Install Scoop (Package manager setup)
# PHASE 4: Install Scoop (Package manager setup)
# ------------------------------------------------------------
RUN Write-Host '=== STARTING SCOOP INSTALLATION ===' ; `
Write-Host 'Setting UTF-8 encoding...' ; `
@@ -150,7 +142,7 @@ RUN Write-Host '=== STARTING SCOOP INSTALLATION ===' ; `
Write-Host '✅ Scoop installation completed!'
# ------------------------------------------------------------
# PHASE 6: Install Quarto via Scoop (consistent PATH with other tools)
# PHASE 5: Install Quarto via Scoop (consistent PATH with other tools)
# ------------------------------------------------------------
RUN Write-Host '=== STARTING QUARTO INSTALLATION ===' ; `
Write-Host 'Installing Quarto via Scoop (extras bucket)...' ; `
@@ -160,18 +152,18 @@ RUN Write-Host '=== STARTING QUARTO INSTALLATION ===' ; `
Write-Host '✅ Quarto installation completed!'
# ------------------------------------------------------------
# PHASE 7: Install Ghostscript (required for PDF generation)
# PHASE 6: Install Ghostscript (required for PDF generation)
# ------------------------------------------------------------
RUN Write-Host '=== STARTING GHOSTSCRIPT INSTALLATION ===' ; `
Write-Host 'Installing Ghostscript via Scoop...' ; `
scoop install main/ghostscript ; `
Write-Host '📦 Ghostscript installed' ; `
Write-Host 'Verifying Ghostscript installation...' ; `
gs --version ; `
gswin64c --version ; `
Write-Host '✅ Ghostscript installation complete'
# ------------------------------------------------------------
# PHASE 8: Install Inkscape and rsvg-convert (required for SVG processing)
# PHASE 7: Install Inkscape and rsvg-convert (required for SVG processing)
# ------------------------------------------------------------
RUN Write-Host '=== STARTING INKSCAPE INSTALLATION ===' ; `
Write-Host 'Installing Inkscape via Scoop...' ; `
@@ -183,14 +175,22 @@ RUN Write-Host '=== STARTING INKSCAPE INSTALLATION ===' ; `
RUN Write-Host '=== STARTING RSVG-CONVERT INSTALLATION ===' ; `
Write-Host 'Installing rsvg-convert via Scoop (required by Quarto for SVG-to-PDF)...' ; `
scoop install main/rsvg-convert ; `
scoop install rsvg-convert ; `
if (-not (Get-Command rsvg-convert -ErrorAction SilentlyContinue)) { `
Write-Host '⚠️ Scoop install failed or package unavailable from current buckets; trying Chocolatey...' ; `
choco install rsvg-convert -y ; `
} ; `
if (-not (Get-Command rsvg-convert -ErrorAction SilentlyContinue)) { `
Write-Host '❌ rsvg-convert install failed via both Scoop and Chocolatey' ; `
exit 1 ; `
} ; `
Write-Host '📦 rsvg-convert installed' ; `
Write-Host 'Verifying rsvg-convert installation...' ; `
rsvg-convert --version ; `
Write-Host '✅ rsvg-convert installation complete'
# ------------------------------------------------------------
# PHASE 9: Install Python (Medium complexity)
# PHASE 8: Install Python (Medium complexity)
# ------------------------------------------------------------
RUN Write-Host '=== STARTING PYTHON INSTALLATION ===' ; `
Write-Host 'Installing Python via Scoop (same as quarto-build workflow)...' ; `
@@ -202,13 +202,11 @@ RUN Write-Host '=== STARTING PYTHON INSTALLATION ===' ; `
Write-Host '✅ Python installation complete'
# ------------------------------------------------------------
# PHASE 10: Install Python packages (Medium complexity)
# PHASE 9: Install Python packages (Medium complexity)
# ------------------------------------------------------------
RUN Write-Host '=== STARTING PYTHON PACKAGE INSTALLATION ===' ; `
Write-Host 'Installing Python packages from requirements.txt (same as quarto-build workflow)...' ; `
Write-Host 'Upgrading pip...' ; `
python -m pip install --upgrade pip ; `
Write-Host '📦 pip upgraded' ; `
Write-Host 'Using bundled pip (skip upgrade to avoid WinError 3 shim lock)...' ; `
Write-Host 'Installing packages from requirements.txt...' ; `
Write-Host 'Requirements file contents:' ; `
Get-Content C:/temp/requirements.txt | Write-Host ; `
@@ -216,7 +214,7 @@ RUN Write-Host '=== STARTING PYTHON PACKAGE INSTALLATION ===' ; `
Write-Host '✅ Python package installation complete'
# ------------------------------------------------------------
# PHASE 11: Install Visual C++ Redistributable (Required for Quarto DLLs)
# PHASE 10: Install Visual C++ Redistributable (Required for Quarto DLLs)
# ------------------------------------------------------------
RUN Write-Host '=== STARTING VISUAL C++ REDISTRIBUTABLE INSTALLATION ===' ; `
Write-Host 'Installing Microsoft Visual C++ Redistributable...' ; `
@@ -226,7 +224,7 @@ RUN Write-Host '=== STARTING VISUAL C++ REDISTRIBUTABLE INSTALLATION ===' ; `
Write-Host '✅ Visual C++ Redistributable installation complete'
# ------------------------------------------------------------
# PHASE 12: Install R (Medium complexity)
# PHASE 11: Install R (Medium complexity)
# ------------------------------------------------------------
RUN Write-Host '=== STARTING R INSTALLATION ===' ; `
Write-Host 'Installing R 4.5.2 via Scoop (main bucket)...' ; `
@@ -237,7 +235,7 @@ RUN Write-Host '=== STARTING R INSTALLATION ===' ; `
Write-Host '✅ R installation complete'
# ------------------------------------------------------------
# PHASE 13: Install R packages (Medium complexity)
# PHASE 12: Install R packages (Medium complexity)
# ------------------------------------------------------------
RUN Write-Host '=== INSTALLING R PACKAGES ===' ; `
Write-Host 'Installing R packages from install_packages.R (same as quarto-build workflow)...' ; `
@@ -261,6 +259,14 @@ RUN Write-Host '=== INSTALLING R PACKAGES ===' ; `
Rscript C:/temp/verify_r_packages.R ; `
Write-Host '✅ R package installation complete'
# ------------------------------------------------------------
# PHASE 13: Install TeX Live LAST (largest/slowest install)
# Direct install-tl approach via standalone script:
# - Bypasses Chocolatey's ErrorActionPreference=Stop wrapper
# - Pins mirror in profile, tries 3 mirrors in order
# ------------------------------------------------------------
RUN & 'C:\temp\install_texlive.ps1'
# ------------------------------------------------------------
# PHASE 14: Cleanup and Environment Setup
# ------------------------------------------------------------
@@ -283,6 +289,29 @@ RUN Write-Host '=== STARTING CLEANUP AND ENVIRONMENT SETUP ===' ; `
Write-Host '🔧 QUARTO_LOG_LEVEL set to DEBUG' ; `
Write-Host '✅ Cleanup and environment setup complete'
# ------------------------------------------------------------
# PHASE 15: Commit PATH into Docker image metadata
# [Environment]::SetEnvironmentVariable writes to the registry but Docker does
# not re-read HKLM between RUN steps — each new layer inherits the *previous
# layer's* Docker ENV, not registry changes made mid-layer. Using the Docker
# ENV directive here guarantees these paths are committed into the image and
# visible to every subsequent RUN step and to containers at runtime.
# ------------------------------------------------------------
ENV PATH="C:\\Users\\ContainerAdministrator\\scoop\\shims;\
C:\\Users\\ContainerAdministrator\\scoop\\apps\\python\\current\\Scripts;\
C:\\Users\\ContainerAdministrator\\scoop\\apps\\python\\current;\
C:\\Users\\ContainerAdministrator\\scoop\\apps\\ghostscript\\current\\bin;\
C:\\Users\\ContainerAdministrator\\scoop\\apps\\r\\current\\bin;\
C:\\Users\\ContainerAdministrator\\scoop\\apps\\git\\current\\cmd;\
C:\\texlive\\bin\\windows;\
C:\\Program Files\\PowerShell\\7;\
C:\\ProgramData\\chocolatey\\bin;\
C:\\Windows\\system32;\
C:\\Windows;\
C:\\Windows\\System32\\Wbem;\
C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;\
C:\\Windows\\System32\\OpenSSH\\"
# ------------------------------------------------------------
# FINAL CHECKS: Comprehensive verification with diagnostics
# ------------------------------------------------------------
@@ -294,27 +323,32 @@ RUN Write-Host '=== FINAL VERIFICATION ===' ; `
Write-Host '📊 TOOL VERIFICATION:' ; `
Write-Host '---------------------' ; `
$failures = @() ; `
$tools = @( `
@{ Name = 'Quarto'; Cmd = 'quarto'; Args = '--version' }, `
@{ Name = 'Python'; Cmd = 'python'; Args = '--version' }, `
@{ Name = 'R'; Cmd = 'Rscript'; Args = '--version' }, `
@{ Name = 'LaTeX'; Cmd = 'lualatex'; Args = '--version' }, `
@{ Name = 'Ghostscript'; Cmd = 'gs'; Args = '--version' }, `
@{ Name = 'Inkscape'; Cmd = 'inkscape'; Args = '--version' }, `
@{ Name = 'rsvg-convert'; Cmd = 'rsvg-convert'; Args = '--version' } `
) ; `
foreach ($tool in $tools) { `
Write-Host "Checking $($tool.Name)..." ; `
try { `
$out = & $tool.Cmd $tool.Args 2>&1 | Select-Object -First 1 ; `
if ($LASTEXITCODE -ne 0) { throw ('{0} exited with {1}' -f $tool.Cmd, $LASTEXITCODE) } ; `
Write-Host " $out" ; `
Write-Host "$($tool.Name) verified" ; `
} catch { `
Write-Host "$($tool.Name) FAILED: $($_.Exception.Message)" ; `
$failures += $tool.Name ; `
function Test-Tool { `
param([string]$Name, [string]$Cmd, [string[]]$Args, [switch]$IgnoreExitCode) `
Write-Host "Checking $Name..." ; `
$resolved = Get-Command $Cmd -ErrorAction SilentlyContinue ; `
if (-not $resolved) { `
Write-Host "$Name FAILED: '$Cmd' not found in PATH" ; `
return $false ; `
} ; `
Write-Host " Resolved: $($resolved.Source)" ; `
$out = & $Cmd @Args 2>&1 | Select-Object -First 1 ; `
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
if (-not $IgnoreExitCode -and $exitCode -ne 0) { `
Write-Host "$Name FAILED: exited $exitCode" ; `
return $false ; `
} ; `
Write-Host " $out" ; `
Write-Host "$Name verified" ; `
return $true ; `
} ; `
if (-not (Test-Tool 'Quarto' 'quarto' '--version')) { $failures += 'Quarto' } ; `
if (-not (Test-Tool 'Python' 'python' '--version')) { $failures += 'Python' } ; `
if (-not (Test-Tool 'R' 'Rscript' '--version')) { $failures += 'R' } ; `
if (-not (Test-Tool 'LaTeX' 'lualatex' '--version' -IgnoreExitCode)) { $failures += 'LaTeX' } ; `
if (-not (Test-Tool 'Ghostscript' 'gswin64c' '--version')) { $failures += 'Ghostscript' } ; `
if (-not (Test-Tool 'Inkscape' 'inkscape' '--version')) { $failures += 'Inkscape' } ; `
if (-not (Test-Tool 'rsvg-convert' 'rsvg-convert' '--version')) { $failures += 'rsvg-convert' } ; `
Write-Host '' ; `
Write-Host '🎯 FINAL STATUS:' ; `
Write-Host '----------------' ; `

View File

@@ -86,7 +86,20 @@ if (-not (Test-Path $texLiveBin)) {
}
Write-Host "📁 TeX Live bin: $texLiveBin"
[Environment]::SetEnvironmentVariable('PATH', ($texLiveBin + ';' + [Environment]::GetEnvironmentVariable('PATH', 'Machine')), 'Machine')
# Create a stable symlink so ENV PATH in Docker can use C:\texlive\bin\windows
$stableBin = Join-Path $TexLiveRoot 'bin\windows'
if ($texLiveBin -ne $stableBin -and -not (Test-Path $stableBin)) {
Write-Host "🔗 Creating stable path: $stableBin -> $texLiveBin"
New-Item -ItemType Directory -Force -Path (Join-Path $TexLiveRoot 'bin') | Out-Null
cmd /c mklink /J "$stableBin" "$texLiveBin"
if (Test-Path $stableBin) {
Write-Host "✅ Stable symlink created"
} else {
Write-Host "⚠️ Symlink failed, using discovered path directly"
}
}
[Environment]::SetEnvironmentVariable('PATH', ($stableBin + ';' + [Environment]::GetEnvironmentVariable('PATH', 'Machine')), 'Machine')
Write-Host '✅ PATH updated'
Write-Host '🔧 Pinning tlmgr repository to stable mirror...'