mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-03-11 17:49:25 -05:00
ci: restore Windows build infrastructure
Restore Windows container build support that was accidentally removed
in commits a76aab467..a90c8803f. This restores:
- Windows Docker infrastructure (book/docker/windows/)
- Windows container build workflow (infra-container-windows.yml)
- Windows matrix entries in book-build-container.yml
- Windows health check support in infra-health-check.yml
- Windows build flags in book-validate-dev.yml and book-publish-live.yml
Restored from pre-removal state at f85e319d6.
This commit is contained in:
238
.github/workflows/book-build-container.yml
vendored
238
.github/workflows/book-build-container.yml
vendored
@@ -130,6 +130,26 @@ on:
|
||||
linux_epub_vol2_artifact:
|
||||
description: "Linux EPUB artifact name (Volume II)"
|
||||
value: ${{ jobs.collect-outputs.outputs.linux_epub_vol2_artifact }}
|
||||
# Windows Volume I artifacts
|
||||
windows_html_vol1_artifact:
|
||||
description: "Windows HTML artifact name (Volume I)"
|
||||
value: ${{ jobs.collect-outputs.outputs.windows_html_vol1_artifact }}
|
||||
windows_pdf_vol1_artifact:
|
||||
description: "Windows PDF artifact name (Volume I)"
|
||||
value: ${{ jobs.collect-outputs.outputs.windows_pdf_vol1_artifact }}
|
||||
windows_epub_vol1_artifact:
|
||||
description: "Windows EPUB artifact name (Volume I)"
|
||||
value: ${{ jobs.collect-outputs.outputs.windows_epub_vol1_artifact }}
|
||||
# Windows Volume II artifacts
|
||||
windows_html_vol2_artifact:
|
||||
description: "Windows HTML artifact name (Volume II)"
|
||||
value: ${{ jobs.collect-outputs.outputs.windows_html_vol2_artifact }}
|
||||
windows_pdf_vol2_artifact:
|
||||
description: "Windows PDF artifact name (Volume II)"
|
||||
value: ${{ jobs.collect-outputs.outputs.windows_pdf_vol2_artifact }}
|
||||
windows_epub_vol2_artifact:
|
||||
description: "Windows EPUB artifact name (Volume II)"
|
||||
value: ${{ jobs.collect-outputs.outputs.windows_epub_vol2_artifact }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -341,6 +361,7 @@ jobs:
|
||||
run: |
|
||||
echo "🎯 Target branch: ${{ inputs.target }}"
|
||||
echo "🐧 Build Linux: ${{ inputs.build_linux }}"
|
||||
echo "🪟 Build Windows: ${{ inputs.build_windows }}"
|
||||
echo "📄 Build HTML: ${{ inputs.build_html }}"
|
||||
echo "📑 Build PDF: ${{ inputs.build_pdf }}"
|
||||
echo "📚 Build EPUB: ${{ inputs.build_epub }}"
|
||||
@@ -348,6 +369,46 @@ jobs:
|
||||
echo "🏷️ Container tag: ${{ inputs.container_tag }}"
|
||||
echo "✅ Current matrix job enabled: ${{ matrix.enabled }}"
|
||||
|
||||
- name: 🔑 Log in to GitHub Container Registry
|
||||
if: matrix.platform == 'windows' && matrix.enabled
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ inputs.container_registry }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐳 Ensure Docker daemon (Windows)
|
||||
if: matrix.platform == 'windows' && matrix.enabled
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$dockerService = Get-Service -Name docker -ErrorAction Stop
|
||||
|
||||
if ($dockerService.Status -ne 'Running') {
|
||||
Write-Host "Starting Docker service..."
|
||||
Start-Service -Name docker
|
||||
} else {
|
||||
Write-Host "Docker service already running."
|
||||
}
|
||||
|
||||
$maxAttempts = 12
|
||||
for ($i = 1; $i -le $maxAttempts; $i++) {
|
||||
try {
|
||||
docker version | Out-Null
|
||||
Write-Host "Docker daemon is ready."
|
||||
break
|
||||
} catch {
|
||||
if ($i -eq $maxAttempts) {
|
||||
throw "Docker daemon failed to start after waiting."
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
}
|
||||
|
||||
- name: 🐳 Pull Docker Image
|
||||
if: matrix.platform == 'windows' && matrix.enabled
|
||||
run: docker pull ${{ env.CONTAINER_IMAGE }}
|
||||
|
||||
- name: 🧪 Preflight toolchain (Linux)
|
||||
if: matrix.platform == 'linux' && matrix.enabled
|
||||
working-directory: ${{ vars.BOOK_QUARTO }}
|
||||
@@ -393,6 +454,84 @@ jobs:
|
||||
|
||||
echo "✅ Linux toolchain preflight checks passed"
|
||||
|
||||
- name: 🧪 Preflight toolchain (Windows)
|
||||
if: matrix.platform == 'windows' && matrix.enabled
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "🧪 Running Windows toolchain preflight checks..."
|
||||
$preflightScript = @"
|
||||
|
||||
`$ErrorActionPreference = 'Stop'
|
||||
`$PSNativeCommandUseErrorActionPreference = `$true
|
||||
|
||||
function Invoke-Check {
|
||||
param(
|
||||
[string]`$Name,
|
||||
[scriptblock]`$Action
|
||||
)
|
||||
Write-Host \"▶ preflight: `$Name\"
|
||||
try {
|
||||
& `$Action
|
||||
if (-not `$?) { throw 'Command returned non-zero status' }
|
||||
Write-Host \"✅ preflight: `$Name\"
|
||||
} catch {
|
||||
`$errorMessage = `$_.Exception.Message
|
||||
Write-Error (\"❌ preflight failed: `$Name -- {0}\" -f `$errorMessage)
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-Check 'quarto on PATH' { `$resolved = Get-Command quarto -ErrorAction Stop; Write-Host (\" quarto -> {0}\" -f `$resolved.Source) }
|
||||
Invoke-Check 'quarto version' { quarto --version | Select-Object -First 1 }
|
||||
Invoke-Check 'pandoc available' {
|
||||
`$pandocCmd = Get-Command pandoc -ErrorAction SilentlyContinue
|
||||
if (`$pandocCmd) {
|
||||
Write-Host (\" pandoc -> {0}\" -f `$pandocCmd.Source)
|
||||
} else {
|
||||
quarto pandoc --version | Select-Object -First 1 | Out-Null
|
||||
Write-Host \" pandoc -> bundled via quarto\"
|
||||
}
|
||||
}
|
||||
Invoke-Check 'pandoc version' {
|
||||
`$pandocCmd = Get-Command pandoc -ErrorAction SilentlyContinue
|
||||
if (`$pandocCmd) {
|
||||
pandoc --version | Select-Object -First 1
|
||||
} else {
|
||||
quarto pandoc --version | Select-Object -First 1
|
||||
}
|
||||
}
|
||||
Invoke-Check 'python on PATH' { `$resolved = Get-Command python -ErrorAction Stop; Write-Host (\" python -> {0}\" -f `$resolved.Source) }
|
||||
Invoke-Check 'python version' { python --version }
|
||||
Invoke-Check 'python3 on PATH' { `$resolved = Get-Command python3 -ErrorAction Stop; Write-Host (\" python3 -> {0}\" -f `$resolved.Source) }
|
||||
Invoke-Check 'python3 version' { python3 --version }
|
||||
Invoke-Check 'mlsysim import' { python -c 'import mlsysim,sys; print(\"mlsysim:\", mlsysim.__file__); print(\"python:\", sys.executable)' }
|
||||
Invoke-Check 'Rscript on PATH' { `$resolved = Get-Command Rscript -ErrorAction Stop; Write-Host (\" Rscript -> {0}\" -f `$resolved.Source) }
|
||||
Invoke-Check 'Rscript version' { Rscript --version | Select-Object -First 1 }
|
||||
Invoke-Check 'inkscape on PATH' { `$resolved = Get-Command inkscape -ErrorAction Stop; Write-Host (\" inkscape -> {0}\" -f `$resolved.Source) }
|
||||
Invoke-Check 'inkscape version' { inkscape --version | Select-Object -First 1 }
|
||||
|
||||
if ('${{ matrix.format_name }}' -eq 'PDF') {
|
||||
Invoke-Check 'lualatex on PATH' { `$resolved = Get-Command lualatex -ErrorAction Stop; Write-Host (\" lualatex -> {0}\" -f `$resolved.Source) }
|
||||
Invoke-Check 'lualatex version' { lualatex --version | Select-Object -First 1 }
|
||||
Invoke-Check 'ghostscript on PATH' {
|
||||
`$gsCmd = Get-Command gs -ErrorAction SilentlyContinue
|
||||
if (-not `$gsCmd) { `$gsCmd = Get-Command gswin64c -ErrorAction SilentlyContinue }
|
||||
if (-not `$gsCmd) { throw 'Ghostscript not found (expected gs or gswin64c)' }
|
||||
Write-Host (\" ghostscript -> {0}\" -f `$gsCmd.Source)
|
||||
}
|
||||
Invoke-Check 'ghostscript version' {
|
||||
`$gsCmd = Get-Command gs -ErrorAction SilentlyContinue
|
||||
if (-not `$gsCmd) { `$gsCmd = Get-Command gswin64c -ErrorAction SilentlyContinue }
|
||||
& `$gsCmd.Source --version
|
||||
}
|
||||
}
|
||||
|
||||
if ('${{ matrix.format_name }}' -eq 'EPUB') {
|
||||
Invoke-Check 'Pillow import' { python -c 'import PIL; print(\"Pillow:\", PIL.__version__)' }
|
||||
}
|
||||
"@
|
||||
$preflightScript | docker run --rm -e PYTHONPATH=C:\workspace -v "$($PWD.Path):C:\workspace" -w "C:\workspace\book\quarto" ${{ env.CONTAINER_IMAGE }} -i pwsh -NoLogo -Command -
|
||||
Write-Host "✅ Windows toolchain preflight checks passed"
|
||||
|
||||
- name: 🔨 Build ${{ matrix.format_name }} (Linux)
|
||||
if: matrix.platform == 'linux' && matrix.enabled
|
||||
@@ -406,6 +545,19 @@ jobs:
|
||||
quarto render --to ${{ matrix.render_target }} --output-dir "${{ matrix.output_dir }}"
|
||||
echo "✅ ${{ matrix.format_name }} build completed"
|
||||
|
||||
- name: 🔨 Build ${{ matrix.format_name }} (Windows)
|
||||
if: matrix.platform == 'windows' && matrix.enabled
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "🔨 Building ${{ matrix.format_name }} on Windows container..."
|
||||
$buildScript = @"
|
||||
|
||||
if (Test-Path '_quarto.yml') { Remove-Item '_quarto.yml' -Force }
|
||||
Copy-Item 'config\${{ matrix.config }}' '_quarto.yml' -Force
|
||||
quarto render --to ${{ matrix.render_target }} --output-dir '${{ matrix.output_dir }}'
|
||||
"@
|
||||
$buildScript | docker run --rm -e PYTHONPATH=C:\workspace -v "$($PWD.Path):C:\workspace" -w "C:\workspace\book\quarto" ${{ env.CONTAINER_IMAGE }} -i pwsh -NoLogo -Command -
|
||||
Write-Host "✅ ${{ matrix.format_name }} build completed"
|
||||
|
||||
- name: 📉 Compress PDF (Linux)
|
||||
if: matrix.platform == 'linux' && matrix.format_name == 'PDF' && matrix.enabled
|
||||
@@ -442,6 +594,45 @@ jobs:
|
||||
echo "⚠️ PDF file not found for compression"
|
||||
fi
|
||||
|
||||
- name: 📉 Compress PDF (Windows)
|
||||
if: matrix.platform == 'windows' && matrix.format_name == 'PDF' && matrix.enabled
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "🔨 Compressing PDF on Windows container..."
|
||||
$compressPdfScript = @"
|
||||
|
||||
if (Test-Path 'Machine-Learning-Systems.pdf') {
|
||||
Write-Host '📉 Compressing PDF with professional compression tool...'
|
||||
Write-Host '🔍 DEBUG: Current directory:'
|
||||
Get-Location
|
||||
Write-Host '🔍 DEBUG: Windows environment info:'
|
||||
Write-Host ("OS: {0}" -f [System.Environment]::OSVersion.VersionString)
|
||||
Write-Host ("Architecture: {0}" -f [System.Environment]::Is64BitOperatingSystem)
|
||||
Write-Host '🔍 DEBUG: Verifying Python and Pillow installation:'
|
||||
`$py_version = (python --version 2>&1).Trim()
|
||||
Write-Host " 🐍 Python Version: `$py_version"
|
||||
`$py_path = (python -c 'import sys; print(sys.executable)').Trim()
|
||||
Write-Host " 🐍 Python Path: `$py_path"
|
||||
`$pillow_version = (python -c 'import PIL; print(PIL.__version__)').Trim()
|
||||
Write-Host " ✅ Pillow Version: `$pillow_version"
|
||||
Write-Host '🔍 DEBUG: Checking for script at relative path:'
|
||||
if (Test-Path '..\\..\\publish\\compress_pdf.py') {
|
||||
Write-Host '✅ Found script at ..\\..\\publish\\compress_pdf.py'
|
||||
python ..\..\publish\compress_pdf.py --input 'Machine-Learning-Systems.pdf' --output 'compressed.pdf' --quality minimal --verbose
|
||||
} else {
|
||||
Write-Host '🔄 Trying fallback path: C:\workspace\book\quarto\publish\compress_pdf.py'
|
||||
python C:\workspace\book\quarto\publish\compress_pdf.py --input 'Machine-Learning-Systems.pdf' --output 'compressed.pdf' --quality minimal --verbose
|
||||
}
|
||||
if (Test-Path 'compressed.pdf') {
|
||||
Move-Item -Force 'compressed.pdf' 'Machine-Learning-Systems.pdf'
|
||||
Write-Host '✅ PDF compression completed'
|
||||
}
|
||||
} else {
|
||||
Write-Warning '⚠️ Machine-Learning-Systems.pdf not found for compression.'
|
||||
}
|
||||
"@
|
||||
$compressPdfScript | docker run --rm -e PYTHONPATH=C:\workspace -v "$($PWD.Path):C:\workspace" -w "C:\workspace\book\quarto\${{ matrix.output_dir }}" ${{ env.CONTAINER_IMAGE }} -i pwsh -NoLogo -Command -
|
||||
Write-Host "✅ PDF compression completed."
|
||||
|
||||
- name: 📚 Compress EPUB (Linux)
|
||||
if: matrix.platform == 'linux' && matrix.format_name == 'EPUB' && matrix.enabled
|
||||
@@ -480,6 +671,45 @@ jobs:
|
||||
echo "⚠️ EPUB file not found for compression"
|
||||
fi
|
||||
|
||||
- name: 📚 Compress EPUB (Windows)
|
||||
if: matrix.platform == 'windows' && matrix.format_name == 'EPUB' && matrix.enabled
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "🔨 Compressing EPUB on Windows container..."
|
||||
$compressEpubScript = @"
|
||||
|
||||
if (Test-Path 'Machine-Learning-Systems.epub') {
|
||||
Write-Host '📚 Compressing EPUB with optimized compression tool...'
|
||||
Write-Host '🔍 DEBUG: Current directory:'
|
||||
Get-Location
|
||||
Write-Host '🔍 DEBUG: Windows environment info:'
|
||||
Write-Host ("OS: {0}" -f [System.Environment]::OSVersion.VersionString)
|
||||
Write-Host ("Architecture: {0}" -f [System.Environment]::Is64BitOperatingSystem)
|
||||
Write-Host '🔍 DEBUG: Verifying Python and Pillow installation:'
|
||||
`$py_version = (python --version 2>&1).Trim()
|
||||
Write-Host " 🐍 Python Version: `$py_version"
|
||||
`$py_path = (python -c 'import sys; print(sys.executable)').Trim()
|
||||
Write-Host " 🐍 Python Path: `$py_path"
|
||||
`$pillow_version = (python -c 'import PIL; print(PIL.__version__)').Trim()
|
||||
Write-Host " ✅ Pillow Version: `$pillow_version"
|
||||
Write-Host '🔍 DEBUG: Checking for script at relative path:'
|
||||
if (Test-Path '..\\..\\publish\\compress_epub.py') {
|
||||
Write-Host '✅ Found script at ..\\..\\publish\\compress_epub.py'
|
||||
python ..\..\publish\compress_epub.py --input 'Machine-Learning-Systems.epub' --output 'compressed.epub' --verbose
|
||||
} else {
|
||||
Write-Host '🔄 Trying fallback path: C:\workspace\book\quarto\publish\compress_epub.py'
|
||||
python C:\workspace\book\quarto\publish\compress_epub.py --input 'Machine-Learning-Systems.epub' --output 'compressed.epub' --verbose
|
||||
}
|
||||
if (Test-Path 'compressed.epub') {
|
||||
Move-Item -Force 'compressed.epub' 'Machine-Learning-Systems.epub'
|
||||
Write-Host '✅ EPUB compression completed (using optimized defaults: quality=50, max-size=1000px)'
|
||||
}
|
||||
} else {
|
||||
Write-Warning '⚠️ Machine-Learning-Systems.epub not found for compression.'
|
||||
}
|
||||
"@
|
||||
$compressEpubScript | docker run --rm -e PYTHONPATH=C:\workspace -v "$($PWD.Path):C:\workspace" -w "C:\workspace\book\quarto\${{ matrix.output_dir }}" ${{ env.CONTAINER_IMAGE }} -i pwsh -NoLogo -Command -
|
||||
Write-Host "✅ EPUB compression completed."
|
||||
|
||||
- name: 📤 Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -506,6 +736,14 @@ jobs:
|
||||
linux_html_vol2_artifact: ${{ steps.collect.outputs.linux_html_vol2_artifact }}
|
||||
linux_pdf_vol2_artifact: ${{ steps.collect.outputs.linux_pdf_vol2_artifact }}
|
||||
linux_epub_vol2_artifact: ${{ steps.collect.outputs.linux_epub_vol2_artifact }}
|
||||
# Windows Volume I artifacts
|
||||
windows_html_vol1_artifact: ${{ steps.collect.outputs.windows_html_vol1_artifact }}
|
||||
windows_pdf_vol1_artifact: ${{ steps.collect.outputs.windows_pdf_vol1_artifact }}
|
||||
windows_epub_vol1_artifact: ${{ steps.collect.outputs.windows_epub_vol1_artifact }}
|
||||
# Windows Volume II artifacts
|
||||
windows_html_vol2_artifact: ${{ steps.collect.outputs.windows_html_vol2_artifact }}
|
||||
windows_pdf_vol2_artifact: ${{ steps.collect.outputs.windows_pdf_vol2_artifact }}
|
||||
windows_epub_vol2_artifact: ${{ steps.collect.outputs.windows_epub_vol2_artifact }}
|
||||
|
||||
steps:
|
||||
- name: 🔎 Collect matrix job status details
|
||||
|
||||
4
.github/workflows/book-publish-live.yml
vendored
4
.github/workflows/book-publish-live.yml
vendored
@@ -38,6 +38,9 @@ env:
|
||||
# main-html-linux: Contains build/html/ (web version)
|
||||
# main-pdf-linux: Contains build/pdf/Machine-Learning-Systems.pdf
|
||||
# main-epub-linux: Contains build/epub/Machine-Learning-Systems.epub
|
||||
# main-html-windows: Contains build/html/ (web version)
|
||||
# main-pdf-windows: Contains build/pdf/Machine-Learning-Systems.pdf
|
||||
# main-epub-windows: Contains build/epub/Machine-Learning-Systems.epub
|
||||
#
|
||||
# This workflow downloads the artifact manifest first to get the exact names,
|
||||
# then downloads the HTML, PDF, and EPUB artifacts using those names for coordination.
|
||||
@@ -754,6 +757,7 @@ jobs:
|
||||
uses: ./.github/workflows/book-build-container.yml
|
||||
with:
|
||||
build_linux: true # Production builds Linux only for now
|
||||
build_windows: false
|
||||
build_html: true # HTML + PDF + EPUB for production
|
||||
build_pdf: true
|
||||
build_epub: true
|
||||
|
||||
63
.github/workflows/book-validate-dev.yml
vendored
63
.github/workflows/book-validate-dev.yml
vendored
@@ -37,6 +37,15 @@ on:
|
||||
- html
|
||||
- pdf
|
||||
- epub
|
||||
build_os:
|
||||
description: '⚙️ Build OS (both, linux, windows)'
|
||||
required: true
|
||||
default: 'both'
|
||||
type: choice
|
||||
options:
|
||||
- both
|
||||
- linux
|
||||
- windows
|
||||
# Container validation
|
||||
run_health_check:
|
||||
description: '💊 Run container health check'
|
||||
@@ -111,6 +120,7 @@ jobs:
|
||||
uses: ./.github/workflows/infra-health-check.yml # Shared workflow - no book- prefix
|
||||
with:
|
||||
test_linux: true
|
||||
test_windows: true
|
||||
container_registry: 'ghcr.io'
|
||||
container_tag: 'latest'
|
||||
|
||||
@@ -132,6 +142,7 @@ jobs:
|
||||
build_pdf: ${{ steps.config.outputs.build_pdf }}
|
||||
build_epub: ${{ steps.config.outputs.build_epub }}
|
||||
build_linux: ${{ steps.config.outputs.build_linux }}
|
||||
build_windows: ${{ steps.config.outputs.build_windows }}
|
||||
container_registry: ${{ steps.config.outputs.container_registry }}
|
||||
container_tag: ${{ steps.config.outputs.container_tag }}
|
||||
target: ${{ steps.config.outputs.target }}
|
||||
@@ -149,12 +160,14 @@ jobs:
|
||||
BUILD_PDF="true"
|
||||
BUILD_EPUB="true"
|
||||
BUILD_LINUX="true"
|
||||
BUILD_WINDOWS="true"
|
||||
TARGET="dev" # For push events, always target dev
|
||||
echo "📊 Trigger: Automatic (dev branch push)"
|
||||
echo "📊 HTML: $BUILD_HTML (mandatory)"
|
||||
echo "📊 PDF: $BUILD_PDF (mandatory)"
|
||||
echo "📊 EPUB: $BUILD_EPUB (mandatory)"
|
||||
echo "📊 Linux: $BUILD_LINUX (mandatory)"
|
||||
echo "📊 Windows: $BUILD_WINDOWS (enabled by default)"
|
||||
echo "📊 Method: container-only"
|
||||
echo "📊 Target: $TARGET"
|
||||
else
|
||||
@@ -173,12 +186,19 @@ jobs:
|
||||
BUILD_EPUB="true"
|
||||
fi
|
||||
|
||||
BUILD_LINUX="true" # Windows is deprecated
|
||||
BUILD_LINUX="false"
|
||||
if [[ "${{ inputs.build_os }}" == "both" || "${{ inputs.build_os }}" == "linux" ]]; then
|
||||
BUILD_LINUX="true"
|
||||
fi
|
||||
BUILD_WINDOWS="false"
|
||||
if [[ "${{ inputs.build_os }}" == "both" || "${{ inputs.build_os }}" == "windows" ]]; then
|
||||
BUILD_WINDOWS="true"
|
||||
fi
|
||||
|
||||
TARGET="${{ inputs.target }}"
|
||||
|
||||
echo "📊 Formats: ${{ inputs.build_formats }} (HTML: $BUILD_HTML, PDF: $BUILD_PDF)"
|
||||
echo "📊 OS: Linux (Windows deprecated)"
|
||||
echo "📊 OS: ${{ inputs.build_os }} (Linux: $BUILD_LINUX, Windows: $BUILD_WINDOWS)"
|
||||
echo "📊 Method: container-only"
|
||||
echo "📊 Target: $TARGET"
|
||||
fi
|
||||
@@ -191,7 +211,8 @@ jobs:
|
||||
echo " HTML: $BUILD_HTML"
|
||||
echo " PDF: $BUILD_PDF"
|
||||
echo " Linux: $BUILD_LINUX"
|
||||
echo " Method: container"
|
||||
echo " Windows: $BUILD_WINDOWS"
|
||||
echo " Method: container"
|
||||
echo " Registry: $CONTAINER_REGISTRY"
|
||||
echo " Tag: $CONTAINER_TAG"
|
||||
echo " Target: $TARGET"
|
||||
@@ -201,7 +222,8 @@ jobs:
|
||||
echo "build_pdf=$BUILD_PDF" >> $GITHUB_OUTPUT
|
||||
echo "build_epub=$BUILD_EPUB" >> $GITHUB_OUTPUT
|
||||
echo "build_linux=$BUILD_LINUX" >> $GITHUB_OUTPUT
|
||||
echo "container_registry=$CONTAINER_REGISTRY" >> $GITHUB_OUTPUT
|
||||
echo "build_windows=$BUILD_WINDOWS" >> $GITHUB_OUTPUT
|
||||
echo "container_registry=$CONTAINER_REGISTRY" >> $GITHUB_OUTPUT
|
||||
echo "container_tag=$CONTAINER_TAG" >> $GITHUB_OUTPUT
|
||||
echo "target=$TARGET" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -216,6 +238,7 @@ jobs:
|
||||
uses: ./.github/workflows/book-build-container.yml
|
||||
with:
|
||||
build_linux: ${{ needs.build-config.outputs.build_linux == 'true' }}
|
||||
build_windows: ${{ needs.build-config.outputs.build_windows == 'true' }}
|
||||
build_html: ${{ needs.build-config.outputs.build_html == 'true' }}
|
||||
build_pdf: ${{ needs.build-config.outputs.build_pdf == 'true' }}
|
||||
build_epub: ${{ needs.build-config.outputs.build_epub == 'true' }}
|
||||
@@ -232,6 +255,7 @@ jobs:
|
||||
if: always()
|
||||
outputs:
|
||||
linux_success: ${{ steps.collect.outputs.linux_success }}
|
||||
windows_success: ${{ steps.collect.outputs.windows_success }}
|
||||
linux_html_vol1_artifact: ${{ steps.collect.outputs.linux_html_vol1_artifact }}
|
||||
linux_html_vol2_artifact: ${{ steps.collect.outputs.linux_html_vol2_artifact }}
|
||||
linux_pdf_vol1_artifact: ${{ steps.collect.outputs.linux_pdf_vol1_artifact }}
|
||||
@@ -275,10 +299,16 @@ jobs:
|
||||
LINUX_SUCCESS="true"
|
||||
fi
|
||||
|
||||
echo "Final Success Status: Linux -> $LINUX_SUCCESS"
|
||||
WINDOWS_SUCCESS="false"
|
||||
if [[ "${{ needs.build-config.outputs.build_windows }}" == "true" && "$OVERALL_SUCCESS" == "true" ]]; then
|
||||
WINDOWS_SUCCESS="true"
|
||||
fi
|
||||
|
||||
echo "Final Success Status: Linux -> $LINUX_SUCCESS, Windows -> $WINDOWS_SUCCESS"
|
||||
|
||||
# Set outputs
|
||||
echo "linux_success=$LINUX_SUCCESS" >> $GITHUB_OUTPUT
|
||||
echo "windows_success=$WINDOWS_SUCCESS" >> $GITHUB_OUTPUT
|
||||
echo "linux_html_vol1_artifact=$LINUX_HTML_VOL1" >> $GITHUB_OUTPUT
|
||||
echo "linux_html_vol2_artifact=$LINUX_HTML_VOL2" >> $GITHUB_OUTPUT
|
||||
echo "linux_pdf_vol1_artifact=$LINUX_PDF_VOL1" >> $GITHUB_OUTPUT
|
||||
@@ -320,6 +350,16 @@ jobs:
|
||||
echo "⏭️ Linux build: SKIPPED"
|
||||
fi
|
||||
|
||||
if [ "${{ needs.build-config.outputs.build_windows }}" = "true" ]; then
|
||||
if [ "${{ needs.collect-results.outputs.windows_success }}" = "true" ]; then
|
||||
echo "✅ Windows build: PASSED"
|
||||
else
|
||||
echo "❌ Windows build: FAILED"
|
||||
fi
|
||||
else
|
||||
echo "⏭️ Windows build: SKIPPED"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎯 OVERALL STATUS:"
|
||||
echo "-----------------"
|
||||
@@ -327,7 +367,9 @@ jobs:
|
||||
# Determine overall success
|
||||
PRECOMMIT_OK="${{ needs.pre-commit.result == 'success' }}"
|
||||
LINUX_OK="${{ needs.collect-results.outputs.linux_success == 'true' || needs.build-config.outputs.build_linux == 'false' }}"
|
||||
if [ "$PRECOMMIT_OK" = "true" ] && [ "$LINUX_OK" = "true" ]; then
|
||||
WINDOWS_OK="${{ needs.collect-results.outputs.windows_success == 'true' || needs.build-config.outputs.build_windows == 'false' }}"
|
||||
|
||||
if [ "$PRECOMMIT_OK" = "true" ] && [ "$LINUX_OK" = "true" ] && [ "$WINDOWS_OK" = "true" ]; then
|
||||
echo "🟢 VALIDATION: FULLY OPERATIONAL ✅"
|
||||
echo " All enabled checks passed successfully!"
|
||||
echo "🚀 PREVIEW: DEPLOYMENT TRIGGERED ✅"
|
||||
@@ -345,18 +387,19 @@ jobs:
|
||||
echo "## 📊 Build Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Pre-commit**: ${{ needs.pre-commit.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Linux Build**: ${{ needs.collect-results.outputs.linux_success == 'true' && '✅ success' || '❌ failed' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Windows Build**: ${{ needs.collect-results.outputs.windows_success == 'true' && '✅ success' || '❌ failed' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "## 🔧 Build Configuration" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
echo "- **Trigger**: Automatic (dev branch push)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Format**: HTML + PDF (mandatory)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **OS**: Linux (mandatory)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **OS**: Linux + Windows (mandatory)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Method**: ✅ Container builds only" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "- **Trigger**: Manual workflow dispatch" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Formats**: ${{ inputs.build_formats }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **OS**: Linux" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **OS**: ${{ inputs.build_os }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Method**: ✅ Container builds only" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "- **Registry**: ${{ needs.build-config.outputs.container_registry }}" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -376,7 +419,9 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## 🎯 Summary" >> $GITHUB_STEP_SUMMARY
|
||||
LINUX_OK="${{ needs.collect-results.outputs.linux_success == 'true' || needs.build-config.outputs.build_linux == 'false' }}"
|
||||
if [ "${{ needs.pre-commit.result }}" = "success" ] && [ "$LINUX_OK" = "true" ]; then
|
||||
WINDOWS_OK="${{ needs.collect-results.outputs.windows_success == 'true' || needs.build-config.outputs.build_windows == 'false' }}"
|
||||
|
||||
if [ "${{ needs.pre-commit.result }}" = "success" ] && [ "$LINUX_OK" = "true" ] && [ "$WINDOWS_OK" = "true" ]; then
|
||||
echo "✅ **Validation Passed** - All enabled builds successful" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🚀 **Preview Deployment Triggered** - A separate workflow is handling the deployment." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
|
||||
245
.github/workflows/infra-container-windows.yml
vendored
Normal file
245
.github/workflows/infra-container-windows.yml
vendored
Normal file
@@ -0,0 +1,245 @@
|
||||
name: '🔧 Infra · 🐳 Container (Windows)'
|
||||
|
||||
# This workflow builds the Windows Quarto build container
|
||||
# Windows containers are more complex but provide performance benefits
|
||||
|
||||
# Prevent multiple builds running simultaneously
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_rebuild:
|
||||
description: 'Force rebuild even if no changes'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
no_cache:
|
||||
description: 'Disable Docker build cache (fresh build)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
container_registry:
|
||||
description: 'Container registry URL'
|
||||
required: false
|
||||
default: 'ghcr.io'
|
||||
type: string
|
||||
container_name:
|
||||
description: 'Container image name'
|
||||
required: false
|
||||
default: 'quarto-windows'
|
||||
type: string
|
||||
container_tag:
|
||||
description: 'Container tag'
|
||||
required: false
|
||||
default: 'latest'
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
force_rebuild:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
no_cache:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
container_registry:
|
||||
required: false
|
||||
default: 'ghcr.io'
|
||||
type: string
|
||||
container_name:
|
||||
required: false
|
||||
default: 'quarto-windows'
|
||||
type: string
|
||||
container_tag:
|
||||
required: false
|
||||
default: 'latest'
|
||||
type: string
|
||||
|
||||
outputs:
|
||||
build-status:
|
||||
description: "Container build status (success/failure/skipped)"
|
||||
value: ${{ jobs.build.outputs.build-status }}
|
||||
image-name:
|
||||
description: "Full container image name with registry"
|
||||
value: ${{ jobs.build.outputs.image-name }}
|
||||
image-digest:
|
||||
description: "Container image digest (SHA256)"
|
||||
value: ${{ jobs.build.outputs.image-digest }}
|
||||
cache-hit:
|
||||
description: "Whether build used cache (true/false)"
|
||||
value: ${{ jobs.build.outputs.cache-hit }}
|
||||
|
||||
# Re-enable automatic triggers
|
||||
schedule:
|
||||
- cron: '0 2 * * 0' # Weekly rebuild (Sunday at 2am - after Linux container)
|
||||
push:
|
||||
branches: [dev] # Only trigger on dev branch, not main
|
||||
paths:
|
||||
- 'book/tools/dependencies/**'
|
||||
- 'book/docker/windows/**'
|
||||
- '.github/workflows/book-build-windows-container.yml'
|
||||
|
||||
env:
|
||||
# =============================================================================
|
||||
# PATH CONFIGURATION - Uses GitHub Repository Variables (Settings > Variables)
|
||||
# =============================================================================
|
||||
# MLSysBook content lives under book/ to accommodate TinyTorch at root
|
||||
# Use ${{ vars.BOOK_ROOT }}, ${{ vars.BOOK_DOCKER }}, etc. in workflow steps
|
||||
# Variables: BOOK_ROOT, BOOK_DOCKER, BOOK_TOOLS, BOOK_QUARTO, BOOK_DEPS
|
||||
|
||||
# Container Registry Configuration (configurable via inputs)
|
||||
REGISTRY: ${{ (github.event_name == 'workflow_dispatch' && inputs.container_registry) || 'ghcr.io' }}
|
||||
IMAGE_NAME: ${{ github.repository }}/${{ (github.event_name == 'workflow_dispatch' && inputs.container_name) || 'quarto-windows' }}
|
||||
CONTAINER_TAG: ${{ (github.event_name == 'workflow_dispatch' && inputs.container_tag) || 'latest' }}
|
||||
|
||||
# Container Build Configuration
|
||||
# Using vars.BOOK_DOCKER (repository variable) - works in all contexts
|
||||
DOCKERFILE_PATH: ./${{ vars.BOOK_DOCKER }}/windows/Dockerfile
|
||||
CONTEXT_PATH: .
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
if: github.repository_owner == 'harvard-edge'
|
||||
timeout-minutes: 180 # takes about 2 hours to build on Windows
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
outputs:
|
||||
build-status: ${{ steps.build.outputs.build-status }}
|
||||
image-name: ${{ steps.build.outputs.image-name }}
|
||||
image-digest: ${{ steps.build.outputs.image-digest }}
|
||||
cache-hit: ${{ steps.build.outputs.cache-hit }}
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Skip Docker Buildx for Windows containers - use native Docker engine
|
||||
# Buildx doesn't properly support Windows containers on GitHub Actions
|
||||
# - name: 🛠️ Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 🔐 Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🏷️ Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=${{ env.CONTAINER_TAG }}
|
||||
|
||||
- name: 🐳 Build and Push Windows container
|
||||
id: build
|
||||
shell: pwsh
|
||||
# Use native Docker instead of book/docker/build-push-action for Windows containers
|
||||
# Buildx has compatibility issues with Windows containers on GitHub Actions
|
||||
run: |
|
||||
# Extract image name and tag from metadata
|
||||
$IMAGE_TAG = "${{ steps.meta.outputs.tags }}"
|
||||
$NO_CACHE = "${{ github.event_name == 'workflow_dispatch' && inputs.no_cache || false }}"
|
||||
|
||||
Write-Host "🔨 Building Windows container..."
|
||||
Write-Host "📊 Image: $IMAGE_TAG"
|
||||
Write-Host "📊 Context: ${{ env.CONTEXT_PATH }}"
|
||||
Write-Host "📊 Dockerfile: ${{ env.DOCKERFILE_PATH }}"
|
||||
Write-Host "📊 No Cache: $NO_CACHE"
|
||||
|
||||
# Build the container using native Docker
|
||||
$buildArgs = @(
|
||||
"build",
|
||||
"--file", "${{ env.DOCKERFILE_PATH }}",
|
||||
"--tag", $IMAGE_TAG
|
||||
)
|
||||
|
||||
# Add no-cache flag if requested
|
||||
if ($NO_CACHE -eq "true") {
|
||||
$buildArgs += "--no-cache"
|
||||
Write-Host "🚫 Cache disabled - building from scratch"
|
||||
} else {
|
||||
Write-Host "💾 Using Docker cache"
|
||||
}
|
||||
|
||||
# Add labels from metadata
|
||||
$labels = "${{ steps.meta.outputs.labels }}"
|
||||
if ($labels) {
|
||||
$labels -split "`n" | ForEach-Object {
|
||||
if ($_.Trim()) {
|
||||
$buildArgs += "--label", $_.Trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Add context path
|
||||
$buildArgs += "${{ env.CONTEXT_PATH }}"
|
||||
|
||||
Write-Host "🔨 Running: docker $($buildArgs -join ' ')"
|
||||
& docker @buildArgs
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Docker build failed with exit code $LASTEXITCODE"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "✅ Build completed successfully"
|
||||
|
||||
# Push the container
|
||||
Write-Host "📤 Pushing container to registry..."
|
||||
& docker push $IMAGE_TAG
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ Docker push failed with exit code $LASTEXITCODE"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "✅ Push completed successfully"
|
||||
|
||||
# Get image digest for output
|
||||
$DIGEST = & docker inspect --format='{{index .RepoDigests 0}}' $IMAGE_TAG 2>$null
|
||||
if ($DIGEST -match '@(.+)$') {
|
||||
$DIGEST = $matches[1]
|
||||
} else {
|
||||
$DIGEST = "unknown"
|
||||
}
|
||||
|
||||
# Set outputs for build summary
|
||||
"digest=$DIGEST" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"cache-hit=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
|
||||
- name: 📊 Build Summary
|
||||
id: build-summary
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Determine build status
|
||||
if ("${{ steps.build.outcome }}" -eq "success") {
|
||||
$BUILD_STATUS = "success"
|
||||
} else {
|
||||
$BUILD_STATUS = "failure"
|
||||
}
|
||||
|
||||
# Extract build information
|
||||
$IMAGE_NAME = "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.CONTAINER_TAG }}"
|
||||
$IMAGE_DIGEST = "${{ steps.build.outputs.digest }}"
|
||||
$CACHE_HIT = "${{ steps.build.outputs.cache-hit }}"
|
||||
|
||||
"build-status=$BUILD_STATUS" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"image-name=$IMAGE_NAME" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"image-digest=$IMAGE_DIGEST" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
"cache-hit=$CACHE_HIT" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
|
||||
Write-Host "📊 Build Status: $BUILD_STATUS"
|
||||
Write-Host "🐳 Image: $IMAGE_NAME"
|
||||
Write-Host "🔍 Digest: $IMAGE_DIGEST"
|
||||
Write-Host "💾 Cache Hit: $CACHE_HIT"
|
||||
416
.github/workflows/infra-health-check.yml
vendored
416
.github/workflows/infra-health-check.yml
vendored
@@ -66,8 +66,10 @@ env:
|
||||
REGISTRY: ${{ inputs.container_registry || 'ghcr.io' }}
|
||||
CONTAINER_TAG: ${{ inputs.container_tag || 'latest' }}
|
||||
LINUX_CONTAINER_NAME: 'quarto-linux'
|
||||
WINDOWS_CONTAINER_NAME: 'quarto-windows'
|
||||
# Computed full image names
|
||||
LINUX_IMAGE: ${{ inputs.container_registry || 'ghcr.io' }}/${{ github.repository }}/quarto-linux:${{ inputs.container_tag || 'latest' }}
|
||||
WINDOWS_IMAGE: ${{ inputs.container_registry || 'ghcr.io' }}/${{ github.repository }}/quarto-windows:${{ inputs.container_tag || 'latest' }}
|
||||
|
||||
jobs:
|
||||
# Matrix strategy for both container platforms
|
||||
@@ -83,6 +85,10 @@ jobs:
|
||||
platform: linux
|
||||
container_name: quarto-linux
|
||||
shell: bash
|
||||
- os: windows-latest
|
||||
platform: windows
|
||||
container_name: quarto-windows
|
||||
shell: pwsh
|
||||
|
||||
env:
|
||||
CONTAINER_IMAGE: ${{ inputs.container_registry || 'ghcr.io' }}/${{ github.repository }}/${{ matrix.container_name }}:${{ inputs.container_tag || 'latest' }}
|
||||
@@ -93,14 +99,16 @@ jobs:
|
||||
steps:
|
||||
- name: 📥 Checkout repository
|
||||
if: |
|
||||
(matrix.platform == 'linux' && inputs.test_linux != false)
|
||||
(matrix.platform == 'linux' && inputs.test_linux != false) ||
|
||||
(matrix.platform == 'windows' && inputs.test_windows != false)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔑 Log in to GitHub Container Registry
|
||||
if: |
|
||||
(matrix.platform == 'linux' && inputs.test_linux != false)
|
||||
(matrix.platform == 'linux' && inputs.test_linux != false) ||
|
||||
(matrix.platform == 'windows' && inputs.test_windows != false)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
@@ -109,12 +117,14 @@ jobs:
|
||||
|
||||
- name: 🐳 Pull Docker Image
|
||||
if: |
|
||||
(matrix.platform == 'linux' && inputs.test_linux != false)
|
||||
(matrix.platform == 'linux' && inputs.test_linux != false) ||
|
||||
(matrix.platform == 'windows' && inputs.test_windows != false)
|
||||
run: docker pull ${{ env.CONTAINER_IMAGE }}
|
||||
|
||||
- name: 📊 Container Information
|
||||
if: |
|
||||
(matrix.platform == 'linux' && inputs.test_linux != false)
|
||||
(matrix.platform == 'linux' && inputs.test_linux != false) ||
|
||||
(matrix.platform == 'windows' && inputs.test_windows != false)
|
||||
run: |
|
||||
echo "📊 === CONTAINER INFORMATION ==="
|
||||
echo "📋 Platform: ${{ matrix.platform }}"
|
||||
@@ -289,6 +299,239 @@ jobs:
|
||||
|
||||
echo "📋 Linux tool versions saved to linux-tool-versions.log"
|
||||
|
||||
- name: 🪟 Windows Container - Tool Version Check
|
||||
if: matrix.platform == 'windows' && inputs.test_windows != false
|
||||
run: |
|
||||
Write-Output "🪟 === WINDOWS CONTAINER TOOL VERSIONS ==="
|
||||
Write-Output "📋 Testing Windows tools with enhanced path detection using 'scoop which':"
|
||||
Write-Output "=============================================="
|
||||
|
||||
docker run --rm ${{ env.CONTAINER_IMAGE }} pwsh -Command "
|
||||
Write-Output '🔍 === WINDOWS CONTAINER TOOL VERSIONS ==='
|
||||
Write-Output ''
|
||||
|
||||
# Check Scoop environment
|
||||
Write-Output '📦 SCOOP ENVIRONMENT:'
|
||||
Write-Output '--------------------'
|
||||
if (Test-Path 'C:\Users\ContainerAdministrator\scoop') {
|
||||
Write-Output '✅ Scoop directory found'
|
||||
if (Test-Path 'C:\Users\ContainerAdministrator\scoop\shims') {
|
||||
`$shimCount = (Get-ChildItem 'C:\Users\ContainerAdministrator\scoop\shims' -File | Measure-Object).Count
|
||||
Write-Output `"📁 Scoop shims directory: `$shimCount files`"
|
||||
}
|
||||
} else {
|
||||
Write-Output '❌ Scoop directory not found'
|
||||
}
|
||||
Write-Output ''
|
||||
|
||||
# --- Tool Checks ---
|
||||
|
||||
Write-Output '📊 QUARTO:'
|
||||
Write-Output '----------'
|
||||
try {
|
||||
`$quartoPath = (scoop which quarto 2>`$null).Trim()
|
||||
if (`$LASTEXITCODE -ne 0) { throw }
|
||||
Write-Output `"📍 Location: `$quartoPath`"
|
||||
Write-Output '📦 Method: scoop which'
|
||||
`$quartoVersion = & `$quartoPath --version 2>&1
|
||||
Write-Output `"📋 Version: `$quartoVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
try {
|
||||
`$quartoPath = (Get-Command quarto -ErrorAction Stop).Source
|
||||
Write-Output `"📍 Location: `$quartoPath`"
|
||||
Write-Output '📦 Method: Get-Command (PATH)'
|
||||
`$quartoVersion = & `$quartoPath --version 2>&1
|
||||
Write-Output `"📋 Version: `$quartoVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
Write-Output '❌ Status: NOT FOUND'
|
||||
}
|
||||
}
|
||||
Write-Output ''
|
||||
|
||||
Write-Output '📊 PYTHON:'
|
||||
Write-Output '-----------'
|
||||
try {
|
||||
`$pythonPath = (scoop which python 2>`$null).Trim()
|
||||
if (`$LASTEXITCODE -ne 0) { throw }
|
||||
Write-Output `"📍 Location: `$pythonPath`"
|
||||
Write-Output '📦 Method: scoop which'
|
||||
`$pythonVersion = & `$pythonPath --version 2>&1
|
||||
Write-Output `"📋 Version: `$pythonVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
try {
|
||||
`$pythonPath = (Get-Command python -ErrorAction Stop).Source
|
||||
Write-Output `"📍 Location: `$pythonPath`"
|
||||
Write-Output '📦 Method: Get-Command (PATH)'
|
||||
`$pythonVersion = & `$pythonPath --version 2>&1
|
||||
Write-Output `"📋 Version: `$pythonVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
Write-Output '❌ Status: NOT FOUND'
|
||||
}
|
||||
}
|
||||
Write-Output ''
|
||||
|
||||
Write-Output '📊 R:'
|
||||
Write-Output '------'
|
||||
try {
|
||||
`$rCmd = Get-Command R.exe -ErrorAction SilentlyContinue
|
||||
if (-not `$rCmd) { `$rCmd = Get-Command R -ErrorAction Stop }
|
||||
`$rPath = `$rCmd.Source
|
||||
Write-Output `"📍 Location: `$rPath`"
|
||||
Write-Output '📦 Method: Get-Command (PATH)'
|
||||
`$rVersion = & `$rCmd.Name --version 2>&1 | Select-Object -First 1
|
||||
Write-Output `"📋 Version: `$rVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
Write-Output '❌ Status: NOT FOUND'
|
||||
}
|
||||
Write-Output ''
|
||||
|
||||
Write-Output '📊 LUALATEX:'
|
||||
Write-Output '------------'
|
||||
try {
|
||||
`$lualatexPath = (Get-Command lualatex -ErrorAction Stop).Source
|
||||
Write-Output `"📍 Location: `$lualatexPath`"
|
||||
`$lualatexVersion = & `$lualatexPath --version 2>&1 | Select-Object -First 1
|
||||
Write-Output `"📋 Version: `$lualatexVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
Write-Output '❌ Status: NOT FOUND'
|
||||
}
|
||||
Write-Output ''
|
||||
|
||||
Write-Output '📊 GHOSTSCRIPT:'
|
||||
Write-Output '---------------'
|
||||
try {
|
||||
`$gsPath = (scoop which gswin64c 2>`$null).Trim()
|
||||
if (`$LASTEXITCODE -ne 0) { `$gsPath = (scoop which gs 2>`$null).Trim() }
|
||||
if (`$LASTEXITCODE -ne 0) { throw }
|
||||
Write-Output `"📍 Location: `$gsPath`"
|
||||
Write-Output '📦 Method: scoop which'
|
||||
`$gsVersion = & `$gsPath --version 2>&1
|
||||
Write-Output `"📋 Version: `$gsVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
try {
|
||||
`$gsCmd = Get-Command gswin64c -ErrorAction SilentlyContinue
|
||||
if (-not `$gsCmd) { `$gsCmd = Get-Command gs -ErrorAction Stop }
|
||||
`$gsPath = `$gsCmd.Source
|
||||
Write-Output `"📍 Location: `$gsPath`"
|
||||
Write-Output '📦 Method: Get-Command (PATH)'
|
||||
`$gsVersion = & `$gsCmd.Name --version 2>&1
|
||||
Write-Output `"📋 Version: `$gsVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
Write-Output '❌ Status: NOT FOUND'
|
||||
}
|
||||
}
|
||||
Write-Output ''
|
||||
|
||||
Write-Output '📊 INKSCAPE:'
|
||||
Write-Output '------------'
|
||||
try {
|
||||
`$inkscapePath = (scoop which inkscape 2>`$null).Trim()
|
||||
if (`$LASTEXITCODE -ne 0) { throw }
|
||||
Write-Output `"📍 Location: `$inkscapePath`"
|
||||
Write-Output '📦 Method: scoop which'
|
||||
`$inkscapeVersion = & `$inkscapePath --version 2>&1 | Select-Object -First 1
|
||||
Write-Output `"📋 Version: `$inkscapeVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
try {
|
||||
`$inkscapePath = (Get-Command inkscape -ErrorAction Stop).Source
|
||||
Write-Output `"📍 Location: `$inkscapePath`"
|
||||
Write-Output '📦 Method: Get-Command (PATH)'
|
||||
`$inkscapeVersion = & `$inkscapePath --version 2>&1 | Select-Object -First 1
|
||||
Write-Output `"📋 Version: `$inkscapeVersion`"
|
||||
Write-Output '✅ Status: OK'
|
||||
} catch {
|
||||
Write-Output '❌ Status: NOT FOUND'
|
||||
}
|
||||
}
|
||||
Write-Output ''
|
||||
|
||||
Write-Output '🎯 === WINDOWS TOOL CHECK COMPLETE ==='
|
||||
Write-Output ''
|
||||
|
||||
# Summary of all tool statuses
|
||||
Write-Output '📋 WINDOWS TOOL STATUS SUMMARY:'
|
||||
Write-Output '==============================='
|
||||
|
||||
# Check each tool and show status
|
||||
if ((scoop which quarto 2>`$null) -or (Get-Command quarto -ErrorAction SilentlyContinue)) {
|
||||
Write-Output '✅ Quarto: AVAILABLE'
|
||||
} else {
|
||||
Write-Output '❌ Quarto: MISSING'
|
||||
}
|
||||
if ((scoop which python 2>`$null) -or (Get-Command python -ErrorAction SilentlyContinue)) {
|
||||
Write-Output '✅ Python: AVAILABLE'
|
||||
} else {
|
||||
Write-Output '❌ Python: MISSING'
|
||||
}
|
||||
if ((Get-Command R.exe -ErrorAction SilentlyContinue) -or (Get-Command R -ErrorAction SilentlyContinue)) {
|
||||
Write-Output '✅ R: AVAILABLE'
|
||||
} else {
|
||||
Write-Output '❌ R: MISSING'
|
||||
}
|
||||
if (Get-Command lualatex -ErrorAction SilentlyContinue) {
|
||||
Write-Output '✅ LuaLaTeX: AVAILABLE'
|
||||
} else {
|
||||
Write-Output '❌ LuaLaTeX: MISSING'
|
||||
}
|
||||
if ((scoop which gswin64c 2>`$null) -or (scoop which gs 2>`$null) -or (Get-Command gswin64c -ErrorAction SilentlyContinue) -or (Get-Command gs -ErrorAction SilentlyContinue)) {
|
||||
Write-Output '✅ Ghostscript: AVAILABLE'
|
||||
} else {
|
||||
Write-Output '❌ Ghostscript: MISSING'
|
||||
}
|
||||
if ((scoop which inkscape 2>`$null) -or (Get-Command inkscape -ErrorAction SilentlyContinue)) {
|
||||
Write-Output '✅ Inkscape: AVAILABLE'
|
||||
} else {
|
||||
Write-Output '❌ Inkscape: MISSING'
|
||||
}
|
||||
|
||||
Write-Output ''
|
||||
|
||||
# Check if any tools are missing and fail if so
|
||||
`$FailedTools = 0
|
||||
|
||||
if (-not ((scoop which quarto 2>`$null) -or (Get-Command quarto -ErrorAction SilentlyContinue))) { `$FailedTools++ }
|
||||
if (-not ((scoop which python 2>`$null) -or (Get-Command python -ErrorAction SilentlyContinue))) { `$FailedTools++ }
|
||||
if (-not ((Get-Command R.exe -ErrorAction SilentlyContinue) -or (Get-Command R -ErrorAction SilentlyContinue))) { `$FailedTools++ }
|
||||
if (-not (Get-Command lualatex -ErrorAction SilentlyContinue)) { `$FailedTools++ }
|
||||
if (-not ((scoop which gswin64c 2>`$null) -or (scoop which gs 2>`$null) -or (Get-Command gswin64c -ErrorAction SilentlyContinue) -or (Get-Command gs -ErrorAction SilentlyContinue))) { `$FailedTools++ }
|
||||
if (-not ((scoop which inkscape 2>`$null) -or (Get-Command inkscape -ErrorAction SilentlyContinue))) { `$FailedTools++ }
|
||||
|
||||
if (`$FailedTools -eq 0) {
|
||||
Write-Output '🎯 ✅ Windows container tool validation: PASSED'
|
||||
Write-Output 'All essential Quarto build tools are available!'
|
||||
} else {
|
||||
Write-Output '🎯 ❌ Windows container tool validation: FAILED'
|
||||
Write-Output "Missing `$FailedTools essential tools - container not ready!"
|
||||
Write-Output 'This container is NOT ready for Quarto builds.'
|
||||
exit 1
|
||||
}
|
||||
Write-Output ''
|
||||
|
||||
# List all Scoop apps for debugging
|
||||
Write-Output '📦 SCOOP INSTALLED APPS:'
|
||||
Write-Output '------------------------'
|
||||
if (Get-Command scoop -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
`$scoopApps = & scoop list 2>&1
|
||||
Write-Output `$scoopApps
|
||||
} catch {
|
||||
Write-Output 'Failed to list Scoop apps'
|
||||
}
|
||||
} else {
|
||||
Write-Output 'Scoop command not found'
|
||||
}
|
||||
" | Tee-Object -FilePath "windows-tool-versions.log"
|
||||
|
||||
Write-Output "📋 Windows tool versions saved to windows-tool-versions.log"
|
||||
|
||||
- name: 🐧 Linux Container - Quarto Check
|
||||
if: matrix.platform == 'linux' && inputs.test_linux != false
|
||||
@@ -356,6 +599,149 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: 🪟 Windows Container - Quarto Check
|
||||
if: matrix.platform == 'windows' && inputs.test_windows != false
|
||||
continue-on-error: true # Don't fail workflow if quarto check fails
|
||||
run: |
|
||||
Write-Output "🪟 === WINDOWS QUARTO CHECK (COMPREHENSIVE) ==="
|
||||
Write-Output "📋 Running quarto check to validate full installation:"
|
||||
Write-Output "====================================================="
|
||||
|
||||
# 1) Emit the inner script to a file (no escaping headaches)
|
||||
$inner = @'
|
||||
Write-Output "🔍 Running enhanced Quarto diagnostics..."
|
||||
Write-Output "📋 Capturing comprehensive diagnostic information..."
|
||||
Write-Output ""
|
||||
|
||||
Write-Output '--- SYSTEM DIAGNOSTIC INFO START ---'
|
||||
Write-Output '🔧 Environment Variables:'
|
||||
Write-Output ("PATH: {0}" -f $env:PATH)
|
||||
Write-Output ("QUARTO_LOG_LEVEL: {0}" -f $env:QUARTO_LOG_LEVEL)
|
||||
Write-Output ""
|
||||
|
||||
Write-Output '🔧 Visual C++ Redistributable DLLs:'
|
||||
Get-ChildItem 'C:\Windows\System32' -Filter 'msvcp*.dll' -ErrorAction SilentlyContinue |
|
||||
Select-Object Name, Length |
|
||||
Format-Table -AutoSize | Out-String | Write-Output
|
||||
Get-ChildItem 'C:\Windows\System32' -Filter 'vcruntime*.dll' -ErrorAction SilentlyContinue |
|
||||
Select-Object Name, Length |
|
||||
Format-Table -AutoSize | Out-String | Write-Output
|
||||
Write-Output ""
|
||||
|
||||
Write-Output '🔧 Quarto Installation Info:'
|
||||
try {
|
||||
$quartoCmd = Get-Command quarto.exe -ErrorAction Stop
|
||||
Write-Output ("Quarto.exe location: {0}" -f $quartoCmd.Source)
|
||||
Write-Output ("Quarto.exe file size: {0} bytes" -f (Get-Item $quartoCmd.Source).Length)
|
||||
$quartoVersion = & $quartoCmd.Source --version 2>&1
|
||||
Write-Output ("Quarto version: {0}" -f $quartoVersion.Trim())
|
||||
} catch {
|
||||
Write-Output 'quarto.exe not found in PATH'
|
||||
}
|
||||
Write-Output '--- SYSTEM DIAGNOSTIC INFO END ---'
|
||||
Write-Output ""
|
||||
|
||||
# Run quarto check with enhanced error handling and raw output capture
|
||||
$quartoExitCode = 0
|
||||
try {
|
||||
Write-Output '--- QUARTO CHECK OUTPUT START ---'
|
||||
Write-Output '🔍 Setting maximum verbosity for raw output capture...'
|
||||
$env:QUARTO_LOG_LEVEL = 'DEBUG'
|
||||
$env:QUARTO_PRINT_STACK = 'true'
|
||||
|
||||
# Capture raw output with timestamps using a more robust method
|
||||
Write-Output "⏰ Quarto check started at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
|
||||
$stdOutPath = [System.IO.Path]::GetTempFileName()
|
||||
$stdErrPath = [System.IO.Path]::GetTempFileName()
|
||||
|
||||
$quartoExitCode = 0
|
||||
|
||||
try {
|
||||
$process = Start-Process -FilePath "quarto.exe" -ArgumentList "check", "--log-level=debug" -Wait -PassThru -RedirectStandardOutput $stdOutPath -RedirectStandardError $stdErrPath
|
||||
$quartoExitCode = $process.ExitCode
|
||||
} catch {
|
||||
$quartoExitCode = 1 # Indicate failure
|
||||
Write-Output "‼️ Error executing Start-Process: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$rawOutput = Get-Content $stdOutPath -Raw -ErrorAction SilentlyContinue
|
||||
$errorOutput = Get-Content $stdErrPath -Raw -ErrorAction SilentlyContinue
|
||||
|
||||
# Combine stdout and stderr for the log
|
||||
if (![string]::IsNullOrEmpty($errorOutput)) {
|
||||
$rawOutput += "`n--- STDERR ---`n" + $errorOutput
|
||||
}
|
||||
|
||||
Remove-Item $stdOutPath, $stdErrPath -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Output "⏰ Quarto check completed at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
|
||||
# Display raw output line by line with prefixes for clarity
|
||||
Write-Output '📋 RAW QUARTO CHECK OUTPUT:'
|
||||
Write-Output '┌─────────────────────────────────────────────────────────────────────────────┐'
|
||||
if (-not [string]::IsNullOrEmpty($rawOutput)) {
|
||||
foreach ($line in $rawOutput.Split("`n")) {
|
||||
Write-Output "│ $line"
|
||||
}
|
||||
}
|
||||
Write-Output '└─────────────────────────────────────────────────────────────────────────────┘'
|
||||
|
||||
Write-Output '--- QUARTO CHECK OUTPUT END ---'
|
||||
} catch {
|
||||
Write-Output '--- QUARTO CHECK EXCEPTION ---'
|
||||
Write-Output ("Exception Type: {0}" -f $($_.Exception.GetType().FullName))
|
||||
Write-Output ("Exception Message: {0}" -f $($_.Exception.Message))
|
||||
if ($_.Exception.InnerException) {
|
||||
Write-Output ("Inner Exception: {0}" -f $($_.Exception.InnerException.Message))
|
||||
}
|
||||
Write-Output '--- QUARTO CHECK EXCEPTION END ---'
|
||||
$quartoExitCode = 1
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
if ($quartoExitCode -eq 0) {
|
||||
Write-Output '✅ Quarto check: PASSED - All components verified!'
|
||||
$QuartoStatus = 'PASSED'
|
||||
} else {
|
||||
Write-Output '❌ Quarto check: FAILED - Issues detected!'
|
||||
Write-Output '🔍 This indicates potential container configuration issues.'
|
||||
Write-Output '📋 Check the diagnostic information above for details.'
|
||||
Write-Output ("📊 Exit Code: {0}" -f $quartoExitCode)
|
||||
switch ($quartoExitCode) {
|
||||
-1073741515 { Write-Output '🔍 Exit code -1073741515 (0xC0000135): DLL not found - Missing Visual C++ Runtime or dependencies' }
|
||||
1 { Write-Output '🔍 Exit code 1: General error - Check Quarto dependencies' }
|
||||
default { Write-Output ("🔍 Exit code {0}: Unknown error - Check logs above" -f $quartoExitCode) }
|
||||
}
|
||||
$QuartoStatus = 'FAILED'
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output ("📊 QUARTO CHECK SUMMARY: {0}" -f $QuartoStatus)
|
||||
'@
|
||||
|
||||
$scriptPath = Join-Path $PWD 'windows-quarto-check.ps1'
|
||||
Set-Content -Path $scriptPath -Value $inner -Encoding UTF8
|
||||
|
||||
# 2) Run it inside the Windows container and tee the output
|
||||
docker run --rm `
|
||||
-v "${PWD}:C:\work" `
|
||||
${{ env.CONTAINER_IMAGE }} `
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "C:\work\windows-quarto-check.ps1" `
|
||||
| Tee-Object -FilePath "windows-quarto-check.log"
|
||||
|
||||
Write-Output "📋 Windows quarto check results saved to windows-quarto-check.log"
|
||||
|
||||
# Also save just the raw quarto check output to a separate file for easy viewing
|
||||
if (Test-Path "windows-quarto-check.log") {
|
||||
$logContent = Get-Content "windows-quarto-check.log" -Raw
|
||||
if ($logContent -match '📋 RAW QUARTO CHECK OUTPUT:(.*?)--- QUARTO CHECK OUTPUT END ---') {
|
||||
$rawOutput = $matches[1].Trim()
|
||||
$rawOutput | Out-File "windows-quarto-raw-output.txt" -Encoding UTF8
|
||||
Write-Output "📄 Raw Quarto output extracted to windows-quarto-raw-output.txt"
|
||||
}
|
||||
}
|
||||
|
||||
- name: 📊 Linux Container Analysis
|
||||
if: matrix.platform == 'linux' && inputs.test_linux != false
|
||||
run: |
|
||||
@@ -367,16 +753,27 @@ jobs:
|
||||
echo "✅ Tool validation enforced - will FAIL if any essential tool missing"
|
||||
echo "LINUX CONTAINER TESTS COMPLETE"
|
||||
|
||||
- name: 📊 Windows Container Analysis
|
||||
if: matrix.platform == 'windows' && inputs.test_windows != false
|
||||
run: |
|
||||
Write-Output "WINDOWS CONTAINER TEST SUMMARY"
|
||||
Write-Output "✅ Container pulled successfully"
|
||||
Write-Output "✅ Essential tools validated with pass/fail status (Quarto, Python, R, LaTeX, Ghostscript, Inkscape)"
|
||||
Write-Output "✅ Quarto check completed"
|
||||
Write-Output "✅ Container size displayed"
|
||||
Write-Output "✅ Tool validation enforced - will FAIL if any essential tool missing"
|
||||
Write-Output "WINDOWS CONTAINER TESTS COMPLETE"
|
||||
|
||||
- name: 📤 Upload Test Artifacts
|
||||
if: always() && matrix.platform == 'linux' && inputs.test_linux != false
|
||||
if: always() && ((matrix.platform == 'linux' && inputs.test_linux != false) || (matrix.platform == 'windows' && inputs.test_windows != false))
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.platform }}-container-test-results
|
||||
path: |
|
||||
linux-tool-versions.log
|
||||
linux-quarto-check.log
|
||||
linux-quarto-raw-output.txt
|
||||
${{ matrix.platform == 'linux' && 'linux-tool-versions.log' || 'windows-tool-versions.log' }}
|
||||
${{ matrix.platform == 'linux' && 'linux-quarto-check.log' || 'windows-quarto-check.log' }}
|
||||
${{ matrix.platform == 'linux' && 'linux-quarto-raw-output.txt' || 'windows-quarto-raw-output.txt' }}
|
||||
${{ matrix.platform == 'windows' && 'windows-quarto-check.ps1' || '' }}
|
||||
if-no-files-found: warn
|
||||
|
||||
# Final summary job
|
||||
@@ -394,11 +791,14 @@ jobs:
|
||||
|
||||
# Get container sizes
|
||||
LINUX_IMAGE="${{ inputs.container_registry || 'ghcr.io' }}/${{ github.repository }}/quarto-linux:${{ inputs.container_tag || 'latest' }}"
|
||||
WINDOWS_IMAGE="${{ inputs.container_registry || 'ghcr.io' }}/${{ github.repository }}/quarto-windows:${{ inputs.container_tag || 'latest' }}"
|
||||
LINUX_SIZE=$(docker images "$LINUX_IMAGE" --format "{{.Size}}" 2>/dev/null || echo "Not available")
|
||||
WINDOWS_SIZE=$(docker images "$WINDOWS_IMAGE" --format "{{.Size}}" 2>/dev/null || echo "Not available")
|
||||
|
||||
echo "📦 CONTAINER SIZES:"
|
||||
echo "------------------"
|
||||
echo "🐧 Linux: $LINUX_SIZE"
|
||||
echo "🪟 Windows: $WINDOWS_SIZE"
|
||||
echo ""
|
||||
|
||||
echo "🔍 TEST RESULTS:"
|
||||
|
||||
71
book/docker/windows/.dockerignore
Normal file
71
book/docker/windows/.dockerignore
Normal file
@@ -0,0 +1,71 @@
|
||||
# Exclude unnecessary files from Docker build context
|
||||
# This reduces build time and image size
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
_book/
|
||||
_site/
|
||||
*.pdf
|
||||
*.html
|
||||
|
||||
# Git and version control
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
!docker/quarto-build-windows/README.md
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs and temporary files
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Node modules (if any)
|
||||
node_modules/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# R cache
|
||||
.Rhistory
|
||||
.RData
|
||||
|
||||
# Large media files (not needed for build)
|
||||
assets/media/
|
||||
*.mp4
|
||||
*.avi
|
||||
*.mov
|
||||
|
||||
# Test files
|
||||
test-*
|
||||
*.test.*
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# Large data files
|
||||
data/
|
||||
*.csv
|
||||
*.json
|
||||
*.xml
|
||||
|
||||
# Keep only essential files for build
|
||||
# - tools/dependencies/ (needed for package installation)
|
||||
# - book/ (needed for build testing)
|
||||
# - .github/workflows/ (needed for workflow files)
|
||||
456
book/docker/windows/Dockerfile
Normal file
456
book/docker/windows/Dockerfile
Normal file
@@ -0,0 +1,456 @@
|
||||
# escape=`
|
||||
# MLSysBook Windows Quarto Build Container (Windows Server 2022)
|
||||
# - PowerShell 7 via ZIP (no MSI)
|
||||
# - Quarto via Scoop (extras bucket)
|
||||
# - Python 3.13.1 + requirements
|
||||
# - 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
|
||||
|
||||
FROM mcr.microsoft.com/windows/server:ltsc2022
|
||||
ARG TEXLIVE_VERSION=latest
|
||||
ENV TEXLIVE_VERSION=${TEXLIVE_VERSION}
|
||||
|
||||
# Use Windows PowerShell initially
|
||||
SHELL ["powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass", "-Command"]
|
||||
|
||||
# === PATH CONFIGURATION ===
|
||||
# Paths are hardcoded since they're stable and ARG scope causes issues in multi-layer builds
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 0: Base dirs and env (same as quarto-build workflow)
|
||||
# ------------------------------------------------------------
|
||||
ENV R_LIBS_USER="C:/r-lib"
|
||||
ENV QUARTO_LOG_LEVEL="INFO"
|
||||
ENV PYTHONIOENCODING="utf-8"
|
||||
ENV LANG="en_US.UTF-8"
|
||||
ENV LC_ALL="en_US.UTF-8"
|
||||
# Explicitly set TMP/TEMP so the TeX Live Perl installer always has a valid temp dir
|
||||
ENV TMP="C:/temp"
|
||||
ENV TEMP="C:/temp"
|
||||
|
||||
RUN Write-Host '=== STARTING BASE SETUP ===' ; `
|
||||
Write-Host 'Creating base directories...' ; `
|
||||
New-Item -ItemType Directory -Force -Path 'C:\temp' | Out-Null ; `
|
||||
Write-Host '📁 Created C:\temp' ; `
|
||||
New-Item -ItemType Directory -Force -Path 'C:\r-lib' | Out-Null ; `
|
||||
Write-Host '📁 Created C:\r-lib' ; `
|
||||
Write-Host 'Environment variables set:' ; `
|
||||
Write-Host " R_LIBS_USER: $env:R_LIBS_USER" ; `
|
||||
Write-Host " QUARTO_LOG_LEVEL: $env:QUARTO_LOG_LEVEL" ; `
|
||||
Write-Host " PYTHONIOENCODING: $env:PYTHONIOENCODING" ; `
|
||||
Write-Host " LANG: $env:LANG" ; `
|
||||
Write-Host " LC_ALL: $env:LC_ALL" ; `
|
||||
Write-Host '✅ Base setup complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 1: PowerShell 7 (ZIP install, container-safe)
|
||||
# ------------------------------------------------------------
|
||||
RUN Write-Host '=== STARTING POWERSHELL 7 INSTALLATION ===' ; `
|
||||
Write-Host 'Using ZIP install for container compatibility' ; `
|
||||
Write-Host 'Download URL: https://github.com/PowerShell/PowerShell/releases/download/v7.4.1/PowerShell-7.4.1-win-x64.zip' ; `
|
||||
$Url = 'https://github.com/PowerShell/PowerShell/releases/download/v7.4.1/PowerShell-7.4.1-win-x64.zip' ; `
|
||||
$Zip = 'C:\PowerShell-7.4.1.zip' ; `
|
||||
Write-Host "Downloading PowerShell 7 to: $Zip" ; `
|
||||
Invoke-WebRequest -Uri $Url -OutFile $Zip -UseBasicParsing ; `
|
||||
Write-Host '📥 Download completed' ; `
|
||||
Write-Host 'Creating PowerShell directory...' ; `
|
||||
New-Item -ItemType Directory -Force -Path 'C:\Program Files\PowerShell\7' | Out-Null ; `
|
||||
Write-Host '📁 Directory created' ; `
|
||||
Write-Host 'Extracting ZIP file...' ; `
|
||||
Expand-Archive -Path $Zip -DestinationPath 'C:\Program Files\PowerShell\7' -Force ; `
|
||||
Write-Host '📦 Extraction completed' ; `
|
||||
Write-Host 'Cleaning up ZIP file...' ; `
|
||||
Remove-Item $Zip -Force ; `
|
||||
Write-Host '🧹 Cleanup completed' ; `
|
||||
Write-Host 'Adding PowerShell to PATH...' ; `
|
||||
$mach = [Environment]::GetEnvironmentVariable('PATH','Machine') ; `
|
||||
Write-Host "Current PATH: $mach" ; `
|
||||
if ($mach -notmatch [regex]::Escape('C:\Program Files\PowerShell\7')) { `
|
||||
[Environment]::SetEnvironmentVariable('PATH', ('C:\Program Files\PowerShell\7;' + $mach), 'Machine') ; `
|
||||
Write-Host '🔗 PowerShell added to PATH' ; `
|
||||
} else { `
|
||||
Write-Host '⚠️ PowerShell already in PATH' ; `
|
||||
} ; `
|
||||
Write-Host 'Verifying PowerShell installation...' ; `
|
||||
& 'C:\Program Files\PowerShell\7\pwsh.exe' -NoLogo -Command '$PSVersionTable.PSVersion ; Write-Host ''PowerShell 7 installation verified ✅'''
|
||||
|
||||
# Switch to PowerShell 7 for subsequent layers
|
||||
SHELL ["C:\\Program Files\\PowerShell\\7\\pwsh.exe", "-NoLogo", "-ExecutionPolicy", "Bypass", "-Command"]
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 2: Chocolatey (package manager for Windows)
|
||||
# ------------------------------------------------------------
|
||||
RUN Write-Host '=== STARTING CHOCOLATEY INSTALLATION ===' ; `
|
||||
Write-Host 'Installing Chocolatey package manager...' ; `
|
||||
Write-Host 'Setting TLS 1.2 for download...' ; `
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; `
|
||||
Write-Host '🔒 TLS 1.2 enabled' ; `
|
||||
Write-Host 'Downloading and executing Chocolatey install script...' ; `
|
||||
iex ((New-Object Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) ; `
|
||||
Write-Host '📦 Chocolatey install script executed' ; `
|
||||
Write-Host 'Verifying Chocolatey installation...' ; `
|
||||
choco --version ; `
|
||||
Write-Host '🔧 Configuring Chocolatey for CI stability...' ; `
|
||||
choco config set --name commandExecutionTimeoutSeconds --value 14400 ; `
|
||||
choco feature disable --name showDownloadProgress ; `
|
||||
Write-Host '✅ Chocolatey timeout/progress settings applied' ; `
|
||||
Write-Host '✅ Chocolatey installation complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 3: Copy dependency files (same as quarto-build workflow)
|
||||
# ------------------------------------------------------------
|
||||
COPY book/tools/dependencies/requirements.txt C:/temp/requirements.txt
|
||||
COPY book/tools/dependencies/install_packages.R C:/temp/install_packages.R
|
||||
COPY book/tools/dependencies/tl_packages C:/temp/tl_packages
|
||||
COPY book/docker/windows/verify_r_packages.R C:/temp/verify_r_packages.R
|
||||
COPY book/docker/windows/install_texlive.ps1 C:/temp/install_texlive.ps1
|
||||
RUN Write-Host '✅ Dependency file copy complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 4: Install Scoop (Package manager setup)
|
||||
# ------------------------------------------------------------
|
||||
RUN Write-Host '=== STARTING SCOOP INSTALLATION ===' ; `
|
||||
Write-Host 'Setting UTF-8 encoding...' ; `
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 ; `
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8 ; `
|
||||
Write-Host '🔤 UTF-8 encoding set' ; `
|
||||
Write-Host 'Setting execution policy...' ; `
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force ; `
|
||||
Write-Host '🔐 Execution policy set' ; `
|
||||
Write-Host 'Installing Scoop package manager...' ; `
|
||||
Invoke-WebRequest -useb get.scoop.sh -outfile 'install.ps1' ; `
|
||||
Write-Host '📥 Scoop install script downloaded' ; `
|
||||
& .\install.ps1 -RunAsAdmin ; `
|
||||
Write-Host '📦 Scoop installed' ; `
|
||||
Write-Host 'Adding Scoop shims to PATH...' ; `
|
||||
$scoopShims = Join-Path (Resolve-Path ~).Path 'scoop\shims' ; `
|
||||
Write-Host "Scoop shims path: $scoopShims" ; `
|
||||
$mach = [Environment]::GetEnvironmentVariable('PATH','Machine') ; `
|
||||
[Environment]::SetEnvironmentVariable('PATH', ($scoopShims + ';' + $mach), 'Machine') ; `
|
||||
Write-Host '🔗 Added Scoop shims to PATH' ; `
|
||||
Write-Host 'Installing Git (required for buckets)...' ; `
|
||||
scoop install git ; `
|
||||
Write-Host '📦 Git installed' ; `
|
||||
Write-Host 'Adding r-bucket...' ; `
|
||||
scoop bucket add r-bucket https://github.com/cderv/r-bucket.git ; `
|
||||
Write-Host '📦 r-bucket added' ; `
|
||||
Write-Host 'Adding extras bucket...' ; `
|
||||
scoop bucket add extras ; `
|
||||
Write-Host '📦 extras bucket added' ; `
|
||||
Write-Host '✅ Scoop installation completed!'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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)...' ; `
|
||||
scoop install extras/quarto ; `
|
||||
if (-not (Get-Command quarto -ErrorAction SilentlyContinue)) { `
|
||||
Write-Host '❌ Quarto install failed: executable not found in PATH' ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
$qv = quarto --version ; `
|
||||
if ($LASTEXITCODE -ne 0) { `
|
||||
Write-Host "❌ Quarto install failed: version check exited $LASTEXITCODE" ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
Write-Host "📦 Quarto installed: $qv" ; `
|
||||
Write-Host '✅ Quarto installation completed!'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 6: Install Ghostscript (required for PDF generation)
|
||||
# ------------------------------------------------------------
|
||||
RUN Write-Host '=== STARTING GHOSTSCRIPT INSTALLATION ===' ; `
|
||||
Write-Host 'Installing Ghostscript via Scoop...' ; `
|
||||
scoop install main/ghostscript ; `
|
||||
$gsCmd = Get-Command gs -ErrorAction SilentlyContinue ; `
|
||||
if (-not $gsCmd) { `
|
||||
Write-Host '❌ Ghostscript install failed: gs shim not found in PATH' ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
Write-Host "Resolved Ghostscript: $($gsCmd.Source)" ; `
|
||||
Write-Host '📦 Ghostscript installed (runtime check deferred to final verification)' ; `
|
||||
Write-Host '✅ Ghostscript installation complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 7: Install Inkscape and rsvg-convert (required for SVG processing)
|
||||
# ------------------------------------------------------------
|
||||
RUN Write-Host '=== STARTING INKSCAPE INSTALLATION ===' ; `
|
||||
Write-Host 'Installing Inkscape via Scoop...' ; `
|
||||
scoop install inkscape ; `
|
||||
if (-not (Get-Command inkscape -ErrorAction SilentlyContinue)) { `
|
||||
Write-Host '❌ Inkscape install failed: executable not found in PATH' ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
$iv = inkscape --version ; `
|
||||
if ($LASTEXITCODE -ne 0) { `
|
||||
Write-Host "❌ Inkscape install failed: version check exited $LASTEXITCODE" ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
Write-Host "📦 Inkscape installed: $iv" ; `
|
||||
Write-Host '✅ Inkscape installation complete'
|
||||
|
||||
RUN Write-Host '=== STARTING RSVG-CONVERT INSTALLATION ===' ; `
|
||||
Write-Host 'Installing rsvg-convert via Scoop (required by Quarto for SVG-to-PDF)...' ; `
|
||||
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 ; `
|
||||
} ; `
|
||||
$rv = rsvg-convert --version ; `
|
||||
if ($LASTEXITCODE -ne 0) { `
|
||||
Write-Host "❌ rsvg-convert install failed: version check exited $LASTEXITCODE" ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
Write-Host "📦 rsvg-convert installed: $rv" ; `
|
||||
Write-Host '✅ rsvg-convert installation complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 8: Install Python (Medium complexity)
|
||||
# ------------------------------------------------------------
|
||||
RUN Write-Host '=== STARTING PYTHON INSTALLATION ===' ; `
|
||||
Write-Host 'Installing Python via Scoop (same as quarto-build workflow)...' ; `
|
||||
Write-Host 'Installing Python from main bucket...' ; `
|
||||
scoop install main/python ; `
|
||||
if (-not (Get-Command python -ErrorAction SilentlyContinue)) { `
|
||||
Write-Host '❌ Python install failed: executable not found in PATH' ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
$pv = python --version ; `
|
||||
if ($LASTEXITCODE -ne 0) { `
|
||||
Write-Host "❌ Python install failed: version check exited $LASTEXITCODE" ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
$pyExe = (python -c 'import sys; print(sys.executable)').Trim() ; `
|
||||
$pyDir = Split-Path $pyExe -Parent ; `
|
||||
$py3Exe = Join-Path $pyDir 'python3.exe' ; `
|
||||
if (-not (Test-Path $py3Exe)) { `
|
||||
Copy-Item $pyExe $py3Exe -Force ; `
|
||||
} ; `
|
||||
$pv3 = python3 --version ; `
|
||||
if ($LASTEXITCODE -ne 0) { `
|
||||
Write-Host "❌ Python install failed: python3 alias check exited $LASTEXITCODE" ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
Write-Host "📦 Python installed: $pv" ; `
|
||||
Write-Host "📦 Python3 alias verified: $pv3" ; `
|
||||
Write-Host '✅ Python installation complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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 '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 ; `
|
||||
python -m pip install -r C:/temp/requirements.txt ; `
|
||||
Write-Host '✅ Python package installation complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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...' ; `
|
||||
Write-Host 'This is required for Quarto DLL dependencies on Windows' ; `
|
||||
choco install vcredist-all -y ; `
|
||||
Write-Host '📦 Visual C++ Redistributable installed' ; `
|
||||
Write-Host '✅ Visual C++ Redistributable installation complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 11: Install R (Medium complexity)
|
||||
# ------------------------------------------------------------
|
||||
RUN Write-Host '=== STARTING R INSTALLATION ===' ; `
|
||||
Write-Host 'Installing R 4.5.2 via Scoop (main bucket)...' ; `
|
||||
scoop install main/r@4.5.2 ; `
|
||||
if (-not (Get-Command Rscript -ErrorAction SilentlyContinue)) { `
|
||||
Write-Host '❌ R install failed: Rscript not found in PATH' ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
$rv = Rscript --version ; `
|
||||
if ($LASTEXITCODE -ne 0) { `
|
||||
Write-Host "❌ R install failed: version check exited $LASTEXITCODE" ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
Write-Host "📦 R installed: $rv" ; `
|
||||
Write-Host '✅ R installation complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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)...' ; `
|
||||
Write-Host 'Setting up R environment...' ; `
|
||||
Write-Host "R_LIBS_USER: $env:R_LIBS_USER" ; `
|
||||
Write-Host 'Installing R packages...' ; `
|
||||
Rscript -e 'options(repos=c(CRAN=\"https://cran.rstudio.com\"))' ; `
|
||||
Rscript -e 'dir.create(Sys.getenv(\"R_LIBS_USER\"), recursive=TRUE, showWarnings=FALSE)' ; `
|
||||
Rscript -e '.libPaths(Sys.getenv(\"R_LIBS_USER\"))' ; `
|
||||
Rscript -e 'install.packages(\"remotes\")' ; `
|
||||
if (Test-Path 'C:/temp/install_packages.R') { `
|
||||
Write-Host 'Found install_packages.R, sourcing it...' ; `
|
||||
Rscript 'C:/temp/install_packages.R' ; `
|
||||
} else { `
|
||||
Write-Host 'No install_packages.R found, installing basic packages...' ; `
|
||||
Rscript -e 'install.packages(c(\"rmarkdown\",\"knitr\",\"ggplot2\"))' ; `
|
||||
} ; `
|
||||
Rscript -e 'for (p in c(\"rmarkdown\",\"knitr\")) if (!require(p, character.only=TRUE, quietly=TRUE)) stop(\"missing: \", p)' ; `
|
||||
Write-Host '📦 R packages installed' ; `
|
||||
Write-Host 'Verifying 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'
|
||||
|
||||
RUN Write-Host '=== VERIFY TEX LIVE INSTALLATION ===' ; `
|
||||
$cmd = Get-Command lualatex -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { `
|
||||
Write-Host '❌ TeX Live verification failed: lualatex not found in PATH' ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$lvAll = & lualatex --version 2>&1 ; `
|
||||
$lv = $lvAll | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { `
|
||||
Write-Host "❌ TeX Live verification failed: lualatex --version exited $exitCode" ; `
|
||||
exit 1 ; `
|
||||
} ; `
|
||||
Write-Host "📦 LaTeX verified: $lv" ; `
|
||||
Write-Host '✅ TeX Live verification complete'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# PHASE 14: Cleanup and Environment Setup
|
||||
# ------------------------------------------------------------
|
||||
RUN Write-Host '=== STARTING CLEANUP AND ENVIRONMENT SETUP ===' ; `
|
||||
Write-Host 'Cleaning temporary files and setting up environment...' ; `
|
||||
Write-Host 'Removing temporary files...' ; `
|
||||
Remove-Item C:/temp/requirements.txt -ErrorAction SilentlyContinue ; `
|
||||
Write-Host '🗑️ requirements.txt removed' ; `
|
||||
Remove-Item C:/temp/install_packages.R -ErrorAction SilentlyContinue ; `
|
||||
Write-Host '🗑️ install_packages.R removed' ; `
|
||||
Remove-Item C:/temp/verify_r_packages.R -ErrorAction SilentlyContinue ; `
|
||||
Write-Host '🗑️ verify_r_packages.R removed' ; `
|
||||
Remove-Item C:/temp/tl_packages -ErrorAction SilentlyContinue ; `
|
||||
Write-Host '🗑️ tl_packages removed' ; `
|
||||
Remove-Item C:/temp/requirements/ -Recurse -Force -ErrorAction SilentlyContinue ; `
|
||||
Write-Host '🗑️ requirements/ directory removed' ; `
|
||||
Write-Host 'Setting up environment variables for Quarto...' ; `
|
||||
$env:QUARTO_LOG_LEVEL = 'DEBUG' ; `
|
||||
[Environment]::SetEnvironmentVariable('QUARTO_LOG_LEVEL', 'DEBUG', 'Machine') ; `
|
||||
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\\lib;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
|
||||
# ------------------------------------------------------------
|
||||
WORKDIR C:/workspace
|
||||
RUN Write-Host '=== FINAL VERIFICATION ===' ; `
|
||||
Write-Host 'PATH environment variable:' ; `
|
||||
Write-Host $env:PATH ; `
|
||||
Write-Host '' ; `
|
||||
Write-Host 'Tool checks run in separate Docker steps below for isolated reporting'
|
||||
|
||||
RUN Write-Host '=== VERIFY: Quarto ===' ; `
|
||||
$cmd = Get-Command quarto -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { Write-Host "❌ Quarto FAILED: command not found"; exit 1 } ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$out = & quarto --version 2>&1 | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { Write-Host "❌ Quarto FAILED: exited $exitCode"; exit 1 } ; `
|
||||
Write-Host " $out" ; `
|
||||
Write-Host '✅ Quarto verified'
|
||||
|
||||
RUN Write-Host '=== VERIFY: Python ===' ; `
|
||||
$cmd = Get-Command python -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { Write-Host "❌ Python FAILED: command not found"; exit 1 } ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$out = & python --version 2>&1 | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { Write-Host "❌ Python FAILED: exited $exitCode"; exit 1 } ; `
|
||||
Write-Host " $out" ; `
|
||||
Write-Host '✅ Python verified'
|
||||
|
||||
RUN Write-Host '=== VERIFY: Python3 ===' ; `
|
||||
$cmd = Get-Command python3 -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { Write-Host "❌ Python3 FAILED: command not found"; exit 1 } ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$out = & python3 --version 2>&1 | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { Write-Host "❌ Python3 FAILED: exited $exitCode"; exit 1 } ; `
|
||||
Write-Host " $out" ; `
|
||||
Write-Host '✅ Python3 verified'
|
||||
|
||||
RUN Write-Host '=== VERIFY: R ===' ; `
|
||||
$cmd = Get-Command Rscript -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { Write-Host "❌ R FAILED: command not found"; exit 1 } ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$out = & Rscript --version 2>&1 | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { Write-Host "❌ R FAILED: exited $exitCode"; exit 1 } ; `
|
||||
Write-Host " $out" ; `
|
||||
Write-Host '✅ R verified'
|
||||
|
||||
RUN Write-Host '=== VERIFY: LaTeX ===' ; `
|
||||
$cmd = Get-Command lualatex -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { Write-Host "❌ LaTeX FAILED: command not found"; exit 1 } ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$out = & lualatex --version 2>&1 | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { Write-Host "❌ LaTeX FAILED: exited $exitCode"; exit 1 } ; `
|
||||
Write-Host " $out" ; `
|
||||
Write-Host '✅ LaTeX verified'
|
||||
|
||||
RUN Write-Host '=== VERIFY: Ghostscript ===' ; `
|
||||
$cmd = Get-Command gs -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { $cmd = Get-Command gswin64c -ErrorAction SilentlyContinue } ; `
|
||||
if (-not $cmd) { Write-Host "❌ Ghostscript FAILED: gs/gswin64c not found"; exit 1 } ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$out = & $cmd.Source --version 2>&1 | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { Write-Host "❌ Ghostscript FAILED: exited $exitCode"; exit 1 } ; `
|
||||
Write-Host " $out" ; `
|
||||
Write-Host '✅ Ghostscript verified'
|
||||
|
||||
RUN Write-Host '=== VERIFY: Inkscape ===' ; `
|
||||
$cmd = Get-Command inkscape -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { Write-Host "❌ Inkscape FAILED: command not found"; exit 1 } ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$out = & inkscape --version 2>&1 | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { Write-Host "❌ Inkscape FAILED: exited $exitCode"; exit 1 } ; `
|
||||
Write-Host " $out" ; `
|
||||
Write-Host '✅ Inkscape verified'
|
||||
|
||||
RUN Write-Host '=== VERIFY: rsvg-convert ===' ; `
|
||||
$cmd = Get-Command rsvg-convert -ErrorAction SilentlyContinue ; `
|
||||
if (-not $cmd) { Write-Host "❌ rsvg-convert FAILED: command not found"; exit 1 } ; `
|
||||
Write-Host "Resolved: $($cmd.Source)" ; `
|
||||
$out = & rsvg-convert --version 2>&1 | Select-Object -First 1 ; `
|
||||
$exitCode = if ($null -eq $LASTEXITCODE) { if ($?) { 0 } else { 1 } } else { [int]$LASTEXITCODE } ; `
|
||||
if ($exitCode -ne 0) { Write-Host "❌ rsvg-convert FAILED: exited $exitCode"; exit 1 } ; `
|
||||
Write-Host " $out" ; `
|
||||
Write-Host '✅ rsvg-convert verified'
|
||||
179
book/docker/windows/README.md
Normal file
179
book/docker/windows/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Windows Quarto Build Container
|
||||
|
||||
This directory contains the Windows Server 2022 container configuration for building the MLSysBook with Quarto.
|
||||
|
||||
## 🐳 Container Features
|
||||
|
||||
- **Base Image**: Windows Server 2022 LTSC
|
||||
- **PowerShell**: 7.4.1 (ZIP install, container-safe)
|
||||
- **Quarto**: 1.9.27 (Scoop)
|
||||
- **Python**: 3.13.1 + production dependencies
|
||||
- **TeX Live**: 2025 snapshot with required packages
|
||||
- **R**: 4.5.2 + R Markdown packages
|
||||
- **Graphics**: Ghostscript + Inkscape (via Chocolatey)
|
||||
|
||||
## 🔧 Key Fixes Applied
|
||||
|
||||
### 1. PowerShell 7 Path Issues
|
||||
- **Problem**: Using `pwsh` shorthand can fail in containers
|
||||
- **Fix**: Use full path `C:\Program Files\PowerShell\7\pwsh.exe`
|
||||
|
||||
### 2. TeX Live Installation
|
||||
- **Problem**: `Start-Process` without `-NoNewWindow` can hang
|
||||
- **Fix**: Added `-NoNewWindow` flag for container compatibility
|
||||
- **Problem**: Comments in `tl_packages` file
|
||||
- **Fix**: Filter out comment lines when installing packages
|
||||
|
||||
### 3. TikZ Test Document
|
||||
- **Problem**: Complex here-string with backticks
|
||||
- **Fix**: Simplified to standard multi-line string
|
||||
|
||||
### 4. Package Installation
|
||||
- **Problem**: Silent failures in package installation
|
||||
- **Fix**: Added verbose output and better error handling
|
||||
|
||||
## 🚀 Building the Container
|
||||
|
||||
### Prerequisites
|
||||
- Windows Docker Desktop or Windows Server with Docker
|
||||
- At least 8GB RAM available for Docker
|
||||
- 20GB+ free disk space
|
||||
|
||||
## Local Build
|
||||
To build the Windows container locally, run the following command from the repository root:
|
||||
```powershell
|
||||
docker build -f docker/windows/Dockerfile -t mlsysbook-windows .
|
||||
```
|
||||
|
||||
### Testing
|
||||
To test the Dockerfile before building, you can use the provided PowerShell script:
|
||||
```powershell
|
||||
./docker/windows/test_dockerfile.ps1
|
||||
```
|
||||
|
||||
## Workflow
|
||||
The container is built and pushed to the GitHub Container Registry via the [`.github/workflows/build-windows-container.yml`](../../.github/workflows/build-windows-container.yml) workflow.
|
||||
This workflow is triggered manually or on a weekly schedule.
|
||||
|
||||
## Notes
|
||||
- Building the Windows container can take a significant amount of time (often over 2 hours).
|
||||
- The image is large due to the comprehensive set of pre-installed dependencies.
|
||||
|
||||
## 📋 Build Phases
|
||||
|
||||
1. **Base Setup**: Directories, environment variables
|
||||
2. **PowerShell 7**: ZIP installation (container-safe)
|
||||
3. **Chocolatey**: Package manager installation
|
||||
4. **Dependencies**: Copy required files
|
||||
5. **Quarto**: ZIP installation with PATH setup
|
||||
6. **Python**: 3.13.1 + production requirements
|
||||
7. **Graphics**: Ghostscript + Inkscape
|
||||
8. **TeX Live**: 2025 snapshot + packages
|
||||
9. **R**: 4.5.2 + R Markdown packages
|
||||
10. **Cleanup**: Remove temporary files
|
||||
|
||||
## 🔍 Verification Steps
|
||||
|
||||
The container includes comprehensive verification:
|
||||
|
||||
- **PowerShell 7**: Version check
|
||||
- **Quarto**: Version and command availability
|
||||
- **Python**: Version and pip functionality
|
||||
- **TeX Live**: Package verification with `kpsewhich`
|
||||
- **Fonts**: Helvetica font files verification
|
||||
- **TikZ**: Smoke test with PDF generation
|
||||
- **R**: Package installation verification
|
||||
|
||||
## ⚠️ Common Issues & Solutions
|
||||
|
||||
### 1. Build Timeouts
|
||||
- **Cause**: Large downloads (TeX Live, Python packages)
|
||||
- **Solution**: Increased timeout values in Dockerfile
|
||||
|
||||
### 2. PATH Issues
|
||||
- **Cause**: Windows PATH not properly updated
|
||||
- **Solution**: Explicit PATH manipulation with regex escaping
|
||||
|
||||
### 3. Package Installation Failures
|
||||
- **Cause**: Network issues or missing dependencies
|
||||
- **Solution**: Added verbose output and error checking
|
||||
|
||||
### 4. Memory Issues
|
||||
- **Cause**: TeX Live installation requires significant memory
|
||||
- **Solution**: Use `scheme-infraonly` for minimal installation
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Run Container
|
||||
```powershell
|
||||
docker run -it mlsysbook-windows pwsh
|
||||
```
|
||||
|
||||
### Test Quarto
|
||||
```powershell
|
||||
quarto --version
|
||||
quarto check
|
||||
```
|
||||
|
||||
### Test Python
|
||||
```powershell
|
||||
python --version
|
||||
python -c "import nltk; print('NLTK available')"
|
||||
```
|
||||
|
||||
### Test R
|
||||
```powershell
|
||||
R --version
|
||||
Rscript -e "library(rmarkdown); print('R Markdown available')"
|
||||
```
|
||||
|
||||
### Test TeX Live
|
||||
```powershell
|
||||
lualatex --version
|
||||
kpsewhich pgf.sty
|
||||
```
|
||||
|
||||
## 📊 Performance Notes
|
||||
|
||||
- **Build Time**: ~45-60 minutes on typical hardware
|
||||
- **Image Size**: ~8-12GB (includes TeX Live, R, Python)
|
||||
- **Memory Usage**: 4-6GB during build, 2-3GB runtime
|
||||
- **Disk Space**: 15-20GB for build cache
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Build Fails on TeX Live
|
||||
```powershell
|
||||
# Check available memory
|
||||
docker system df
|
||||
docker system prune -f
|
||||
```
|
||||
|
||||
### PowerShell Issues
|
||||
```powershell
|
||||
# Verify PowerShell 7 installation
|
||||
docker run mlsysbook-windows pwsh -Command "Get-Host"
|
||||
```
|
||||
|
||||
### Package Installation Issues
|
||||
```powershell
|
||||
# Check Chocolatey installation
|
||||
docker run mlsysbook-windows choco --version
|
||||
```
|
||||
|
||||
## 📝 Maintenance
|
||||
|
||||
### Updating Dependencies
|
||||
1. Update version numbers in Dockerfile
|
||||
2. Test with validation script
|
||||
3. Rebuild and verify all components
|
||||
|
||||
### Adding New Packages
|
||||
1. Add to appropriate phase in Dockerfile
|
||||
2. Update verification steps
|
||||
3. Test thoroughly
|
||||
|
||||
### Security Updates
|
||||
- Regularly update base image
|
||||
- Monitor for CVE reports
|
||||
- Update package versions as needed
|
||||
152
book/docker/windows/install_texlive.ps1
Normal file
152
book/docker/windows/install_texlive.ps1
Normal file
@@ -0,0 +1,152 @@
|
||||
param(
|
||||
[string]$TexLiveRoot = 'C:\texlive',
|
||||
[string]$TexInstallDir = 'C:\texlive-install'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$mirrors = @(
|
||||
'https://ctan.math.illinois.edu/systems/texlive/tlnet',
|
||||
'https://mirrors.mit.edu/CTAN/systems/texlive/tlnet',
|
||||
'https://mirror.ctan.org/systems/texlive/tlnet'
|
||||
)
|
||||
|
||||
Write-Host '=== STARTING TEX LIVE INSTALLATION ==='
|
||||
Write-Host '📥 Downloading install-tl...'
|
||||
$installTlZip = 'C:\temp\install-tl.zip'
|
||||
Invoke-WebRequest -Uri 'https://mirror.ctan.org/systems/texlive/tlnet/install-tl.zip' `
|
||||
-OutFile $installTlZip -UseBasicParsing
|
||||
Write-Host '📦 Extracting install-tl...'
|
||||
Expand-Archive -Path $installTlZip -DestinationPath $TexInstallDir -Force
|
||||
Remove-Item $installTlZip -Force
|
||||
|
||||
$installTlDir = Get-ChildItem $TexInstallDir -Directory |
|
||||
Where-Object { $_.Name -match '^install-tl' } |
|
||||
Select-Object -First 1
|
||||
Write-Host "📁 install-tl directory: $($installTlDir.FullName)"
|
||||
|
||||
$installed = $false
|
||||
foreach ($mirror in $mirrors) {
|
||||
Write-Host "🔄 Trying mirror: $mirror"
|
||||
$profile = 'C:\temp\texlive.profile'
|
||||
@(
|
||||
"selected_scheme scheme-basic",
|
||||
"TEXDIR $TexLiveRoot",
|
||||
"TEXMFLOCAL $TexLiveRoot/texmf-local",
|
||||
"TEXMFSYSCONFIG $TexLiveRoot/texmf-config",
|
||||
"TEXMFSYSVAR $TexLiveRoot/texmf-var",
|
||||
"instopt_adjustrepo 0"
|
||||
) | Set-Content $profile
|
||||
$env:TEXLIVE_INSTALL_NO_WELCOME = '1'
|
||||
$batPath = Join-Path $installTlDir.FullName 'install-tl-windows.bat'
|
||||
cmd /c """$batPath"" -no-gui -profile ""$profile"" -repository $mirror"
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$installed = $true
|
||||
Write-Host "✅ TeX Live installed from $mirror"
|
||||
break
|
||||
}
|
||||
Write-Host "⚠️ Mirror $mirror failed (exit $LASTEXITCODE), trying next..."
|
||||
}
|
||||
|
||||
Remove-Item $TexInstallDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $installed) {
|
||||
Write-Host '❌ TeX Live installation failed on all mirrors'
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host '🔍 Finding TeX Live bin directory...'
|
||||
Write-Host " TexLiveRoot contents:"
|
||||
Get-ChildItem $TexLiveRoot -Directory | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
# Strategy 1: look for a year-numbered directory (e.g. 2025)
|
||||
$texYearDir = Get-ChildItem $TexLiveRoot -Directory |
|
||||
Where-Object { $_.Name -match '^\d{4}$' } |
|
||||
Sort-Object Name -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($texYearDir) {
|
||||
$texLiveBin = Join-Path $texYearDir.FullName 'bin\windows'
|
||||
} else {
|
||||
# Strategy 2: search recursively for tlmgr.bat
|
||||
Write-Host ' ⚠️ No year directory found, searching recursively for tlmgr.bat...'
|
||||
$tlmgr = Get-ChildItem $TexLiveRoot -Recurse -Filter 'tlmgr.bat' -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
if ($tlmgr) {
|
||||
$texLiveBin = $tlmgr.DirectoryName
|
||||
} else {
|
||||
Write-Host '❌ Cannot find TeX Live bin directory'
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $texLiveBin)) {
|
||||
Write-Host "❌ TeX Live bin directory does not exist: $texLiveBin"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "📁 TeX Live bin: $texLiveBin"
|
||||
|
||||
# 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...'
|
||||
$tlmgrMirror = 'https://ctan.math.illinois.edu/systems/texlive/tlnet'
|
||||
& "$texLiveBin\tlmgr.bat" option repository $tlmgrMirror
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ tlmgr repository: $tlmgrMirror"
|
||||
} else {
|
||||
Write-Host '⚠️ Could not pin tlmgr repository, continuing with defaults'
|
||||
}
|
||||
|
||||
Write-Host '📋 Reading collections from tl_packages...'
|
||||
if (Test-Path 'C:\temp\tl_packages') {
|
||||
$collections = Get-Content 'C:\temp\tl_packages' |
|
||||
Where-Object { $_.Trim() -ne '' -and -not $_.Trim().StartsWith('#') }
|
||||
Write-Host "📦 Found $($collections.Count) collections to install"
|
||||
$i = 1
|
||||
foreach ($collection in $collections) {
|
||||
Write-Host "📦 [$i/$($collections.Count)] Installing $collection..."
|
||||
& "$texLiveBin\tlmgr.bat" install $collection
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "✅ $collection installed successfully"
|
||||
} else {
|
||||
Write-Host "⚠️ Failed to install $collection, continuing..."
|
||||
}
|
||||
$i++
|
||||
}
|
||||
Write-Host '✅ Collection installation complete'
|
||||
} else {
|
||||
Write-Host '⚠️ No tl_packages file found, skipping collection installation'
|
||||
}
|
||||
|
||||
Write-Host '🔄 Updating tlmgr...'
|
||||
& "$texLiveBin\tlmgr.bat" update --self --all
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host '⚠️ tlmgr update returned non-zero, continuing...'
|
||||
} else {
|
||||
Write-Host '✅ tlmgr updated'
|
||||
}
|
||||
|
||||
Write-Host '🔍 Verifying lualatex installation...'
|
||||
$lualatexPath = Join-Path $texLiveBin 'lualatex.exe'
|
||||
if (-not (Test-Path $lualatexPath)) {
|
||||
Write-Host "❌ lualatex.exe not found at: $lualatexPath"
|
||||
Write-Host " Contents of bin dir:"
|
||||
Get-ChildItem $texLiveBin -ErrorAction SilentlyContinue | Select-Object -First 20 | ForEach-Object { Write-Host " $_" }
|
||||
exit 1
|
||||
}
|
||||
& $lualatexPath --version
|
||||
Write-Host '✅ TeX Live installation verified'
|
||||
13
book/docker/windows/verify_r_packages.R
Normal file
13
book/docker/windows/verify_r_packages.R
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env Rscript
|
||||
|
||||
# Verify R package installation
|
||||
source('C:/temp/install_packages.R')
|
||||
|
||||
missing_packages <- required_packages[!sapply(required_packages, requireNamespace, quietly = TRUE)]
|
||||
|
||||
if(length(missing_packages) > 0) {
|
||||
cat('❌ Missing packages:', paste(missing_packages, collapse = ', '), '\n')
|
||||
quit(status = 1)
|
||||
} else {
|
||||
cat('✅ All required R packages installed successfully\n')
|
||||
}
|
||||
Reference in New Issue
Block a user