mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-04-30 09:38:38 -05:00
- Quarto 1.9.27: Linux (.deb), Windows (direct download; Scoop Extras has 1.8.27) - R 4.5.2: Linux (CRAN jammy-cran40), Windows (Scoop main/r) - Baremetal: quarto-actions/setup for both Linux and Windows - Remove ggrepel version pin (R 4.5.x supports ggrepel 0.9.7) - Update docs: BUILD.md, CONTAINER_BUILDS.md, docker READMEs
1469 lines
60 KiB
YAML
1469 lines
60 KiB
YAML
name: '📚 Book · 🔨 Build (Baremetal)'
|
|
|
|
# Concurrency disabled - allow unlimited parallel builds
|
|
|
|
# This workflow builds a Quarto project and uploads artifacts
|
|
# It handles both Windows and Linux environments with extensive caching for better performance
|
|
# Note: This workflow does NOT deploy - all deployment is handled by publish-live workflow
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
# Platform selection checkboxes
|
|
build_linux:
|
|
description: '🐧 Build on Linux'
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
build_windows:
|
|
description: '🪟 Build on Windows'
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
# Format selection checkboxes
|
|
build_html:
|
|
description: '📄 Build HTML format'
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
build_pdf:
|
|
description: '📑 Build PDF format'
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
build_epub:
|
|
description: '📚 Build EPUB format'
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
# Build configuration
|
|
build_target:
|
|
description: '📦 Build target (vol1, vol2)'
|
|
required: false
|
|
type: choice
|
|
default: 'vol1'
|
|
options:
|
|
- vol1
|
|
- vol2
|
|
target:
|
|
description: 'Target branch (dev/main)'
|
|
required: false
|
|
type: choice
|
|
default: 'dev'
|
|
options:
|
|
- dev
|
|
- main
|
|
# Advanced options
|
|
quarto-version:
|
|
description: 'Version of Quarto to use'
|
|
required: false
|
|
type: string
|
|
default: '1.9.27'
|
|
r-version:
|
|
description: 'Version of R to use'
|
|
required: false
|
|
type: string
|
|
default: '4.5.2'
|
|
quarto-log-level:
|
|
description: 'Quarto log level'
|
|
required: false
|
|
type: choice
|
|
default: 'INFO'
|
|
options:
|
|
- INFO
|
|
- DEBUG
|
|
artifact_name:
|
|
description: 'Custom artifact name (optional)'
|
|
required: false
|
|
type: string
|
|
default: ''
|
|
workflow_call:
|
|
inputs:
|
|
# Platform selection
|
|
build_linux:
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
description: 'Build on Linux'
|
|
build_windows:
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
description: 'Build on Windows'
|
|
# Format selection
|
|
build_html:
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
description: 'Build HTML format'
|
|
build_pdf:
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
description: 'Build PDF format'
|
|
build_epub:
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
description: 'Build EPUB format'
|
|
# Build configuration
|
|
build_target:
|
|
required: false
|
|
type: string
|
|
default: 'vol1'
|
|
description: 'Build target (vol1/vol2)'
|
|
target:
|
|
required: false
|
|
type: string
|
|
default: 'dev'
|
|
description: 'Target branch (dev/main)'
|
|
# Configuration options
|
|
quarto-version:
|
|
required: false
|
|
type: string
|
|
default: '1.9.27'
|
|
description: 'Version of Quarto to use'
|
|
r-version:
|
|
required: false
|
|
type: string
|
|
default: '4.5.2'
|
|
description: 'Version of R to use'
|
|
artifact_name:
|
|
required: false
|
|
type: string
|
|
default: ''
|
|
description: 'Custom artifact name (optional)'
|
|
|
|
outputs:
|
|
build_success:
|
|
description: "Whether all builds completed successfully"
|
|
value: ${{ jobs.collect-outputs.outputs.build_success }}
|
|
linux_html_vol1_artifact:
|
|
description: "Linux HTML artifact name (Volume I)"
|
|
value: ${{ jobs.collect-outputs.outputs.linux_html_vol1_artifact }}
|
|
linux_pdf_vol1_artifact:
|
|
description: "Linux PDF artifact name (Volume I)"
|
|
value: ${{ jobs.collect-outputs.outputs.linux_pdf_vol1_artifact }}
|
|
linux_epub_vol1_artifact:
|
|
description: "Linux EPUB artifact name (Volume I)"
|
|
value: ${{ jobs.collect-outputs.outputs.linux_epub_vol1_artifact }}
|
|
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 }}
|
|
linux_html_vol2_artifact:
|
|
description: "Linux HTML artifact name (Volume II)"
|
|
value: ${{ jobs.collect-outputs.outputs.linux_html_vol2_artifact }}
|
|
linux_pdf_vol2_artifact:
|
|
description: "Linux PDF artifact name (Volume II)"
|
|
value: ${{ jobs.collect-outputs.outputs.linux_pdf_vol2_artifact }}
|
|
linux_epub_vol2_artifact:
|
|
description: "Linux EPUB artifact name (Volume II)"
|
|
value: ${{ jobs.collect-outputs.outputs.linux_epub_vol2_artifact }}
|
|
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: write
|
|
pages: write
|
|
|
|
# =============================================================================
|
|
# PATH CONFIGURATION - Uses GitHub Repository Variables (Settings > Variables)
|
|
# =============================================================================
|
|
# MLSysBook content lives under book/ to accommodate TinyTorch at root
|
|
# Use ${{ vars.BOOK_ROOT }}, ${{ vars.BOOK_QUARTO }}, etc. in workflow steps
|
|
# Variables: BOOK_ROOT, BOOK_DOCKER, BOOK_TOOLS, BOOK_QUARTO, BOOK_DEPS
|
|
|
|
jobs:
|
|
build:
|
|
name: '${{ matrix.os_emoji }} Build ${{ matrix.os_name }} (${{ matrix.format_emoji }} ${{ matrix.format }})'
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# Linux Volume I builds
|
|
- os: ubuntu-latest
|
|
os_name: Linux
|
|
os_emoji: '🐧'
|
|
format: HTML
|
|
format_emoji: '📄'
|
|
volume: vol1
|
|
config: _quarto-html-vol1.yml
|
|
render_target: html
|
|
output_dir: _build/html-vol1
|
|
enabled: ${{ inputs.build_linux && inputs.build_html }}
|
|
- os: ubuntu-latest
|
|
os_name: Linux
|
|
os_emoji: '🐧'
|
|
format: PDF
|
|
format_emoji: '📑'
|
|
volume: vol1
|
|
config: _quarto-pdf-vol1.yml
|
|
render_target: titlepage-pdf
|
|
output_dir: _build/pdf-vol1
|
|
enabled: ${{ inputs.build_linux && inputs.build_pdf }}
|
|
- os: ubuntu-latest
|
|
os_name: Linux
|
|
os_emoji: '🐧'
|
|
format: EPUB
|
|
format_emoji: '📚'
|
|
volume: vol1
|
|
config: _quarto-epub-vol1.yml
|
|
render_target: epub
|
|
output_dir: _build/epub-vol1
|
|
enabled: ${{ inputs.build_linux && inputs.build_epub }}
|
|
# Linux Volume II builds
|
|
- os: ubuntu-latest
|
|
os_name: Linux
|
|
os_emoji: '🐧'
|
|
format: HTML
|
|
format_emoji: '📄'
|
|
volume: vol2
|
|
config: _quarto-html-vol2.yml
|
|
render_target: html
|
|
output_dir: _build/html-vol2
|
|
enabled: ${{ inputs.build_linux && inputs.build_html }}
|
|
- os: ubuntu-latest
|
|
os_name: Linux
|
|
os_emoji: '🐧'
|
|
format: PDF
|
|
format_emoji: '📑'
|
|
volume: vol2
|
|
config: _quarto-pdf-vol2.yml
|
|
render_target: titlepage-pdf
|
|
output_dir: _build/pdf-vol2
|
|
enabled: ${{ inputs.build_linux && inputs.build_pdf }}
|
|
- os: ubuntu-latest
|
|
os_name: Linux
|
|
os_emoji: '🐧'
|
|
format: EPUB
|
|
format_emoji: '📚'
|
|
volume: vol2
|
|
config: _quarto-epub-vol2.yml
|
|
render_target: epub
|
|
output_dir: _build/epub-vol2
|
|
enabled: ${{ inputs.build_linux && inputs.build_epub }}
|
|
# Windows Volume I builds
|
|
- os: windows-latest
|
|
os_name: Windows
|
|
os_emoji: '🪟'
|
|
format: HTML
|
|
format_emoji: '📄'
|
|
volume: vol1
|
|
config: _quarto-html-vol1.yml
|
|
render_target: html
|
|
output_dir: _build/html-vol1
|
|
enabled: ${{ inputs.build_windows && inputs.build_html }}
|
|
- os: windows-latest
|
|
os_name: Windows
|
|
os_emoji: '🪟'
|
|
format: PDF
|
|
format_emoji: '📑'
|
|
volume: vol1
|
|
config: _quarto-pdf-vol1.yml
|
|
render_target: titlepage-pdf
|
|
output_dir: _build/pdf-vol1
|
|
enabled: ${{ inputs.build_windows && inputs.build_pdf }}
|
|
- os: windows-latest
|
|
os_name: Windows
|
|
os_emoji: '🪟'
|
|
format: EPUB
|
|
format_emoji: '📚'
|
|
volume: vol1
|
|
config: _quarto-epub-vol1.yml
|
|
render_target: epub
|
|
output_dir: _build/epub-vol1
|
|
enabled: ${{ inputs.build_windows && inputs.build_epub }}
|
|
# Windows Volume II builds
|
|
- os: windows-latest
|
|
os_name: Windows
|
|
os_emoji: '🪟'
|
|
format: HTML
|
|
format_emoji: '📄'
|
|
volume: vol2
|
|
config: _quarto-html-vol2.yml
|
|
render_target: html
|
|
output_dir: _build/html-vol2
|
|
enabled: ${{ inputs.build_windows && inputs.build_html }}
|
|
- os: windows-latest
|
|
os_name: Windows
|
|
os_emoji: '🪟'
|
|
format: PDF
|
|
format_emoji: '📑'
|
|
volume: vol2
|
|
config: _quarto-pdf-vol2.yml
|
|
render_target: titlepage-pdf
|
|
output_dir: _build/pdf-vol2
|
|
enabled: ${{ inputs.build_windows && inputs.build_pdf }}
|
|
- os: windows-latest
|
|
os_name: Windows
|
|
os_emoji: '🪟'
|
|
format: EPUB
|
|
format_emoji: '📚'
|
|
volume: vol2
|
|
config: _quarto-epub-vol2.yml
|
|
render_target: epub
|
|
output_dir: _build/epub-vol2
|
|
enabled: ${{ inputs.build_windows && inputs.build_epub }}
|
|
|
|
timeout-minutes: 120 # ⏰ Set job timeout to 2 hours (7200 seconds)
|
|
|
|
outputs:
|
|
os_name: ${{ matrix.os_name }}
|
|
format: ${{ matrix.format }}
|
|
artifact_name: ${{ steps.build-declaration.outputs.artifact_name }}
|
|
status: ${{ job.status }}
|
|
output_dir: ${{ steps.build.outputs.output_dir }}
|
|
|
|
env:
|
|
R_LIBS_USER: ${{ github.workspace }}/.r-lib
|
|
QUARTO_LOG_LEVEL: ${{ inputs.quarto-log-level || 'INFO' }}
|
|
# UTF-8 encoding for proper emoji display
|
|
PYTHONIOENCODING: utf-8
|
|
LANG: en_US.UTF-8
|
|
LC_ALL: en_US.UTF-8
|
|
|
|
steps:
|
|
- name: 🛑 Skip build
|
|
if: "!matrix.enabled"
|
|
run: echo "Build skipped because matrix.enabled is false"
|
|
|
|
- name: 📥 Checkout repository
|
|
if: matrix.enabled
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
# === WINDOWS: Install Scoop and Chocolatey (Package Managers) ===
|
|
- name: 📦 Install Scoop Package Manager (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING SCOOP PACKAGE MANAGER ==="
|
|
Write-Output "This matches the Windows container setup (Phase 5)"
|
|
|
|
if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) {
|
|
Write-Output "🔤 Setting UTF-8 encoding..."
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
Write-Output "✅ UTF-8 encoding set"
|
|
|
|
Write-Output "🔐 Setting execution policy..."
|
|
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
|
|
Write-Output "✅ Execution policy set"
|
|
|
|
Write-Output "📦 Installing Scoop package manager..."
|
|
Write-Output "📥 Downloading Scoop install script..."
|
|
Invoke-WebRequest -useb get.scoop.sh -outfile 'install-scoop.ps1'
|
|
Write-Output "📥 Scoop install script downloaded"
|
|
Write-Output "📦 Running Scoop installer..."
|
|
& .\install-scoop.ps1 -RunAsAdmin
|
|
Write-Output "✅ Scoop installed"
|
|
} else {
|
|
Write-Output "✅ Scoop already installed"
|
|
}
|
|
|
|
# Add Scoop to PATH for subsequent steps
|
|
$scoopShims = Join-Path (Resolve-Path ~).Path 'scoop\shims'
|
|
Write-Output "📁 Scoop shims path: $scoopShims"
|
|
Write-Output "🔗 Adding Scoop shims to PATH..."
|
|
echo "$scoopShims" | Out-File -Append -Encoding UTF8 $env:GITHUB_PATH
|
|
|
|
# Also add to current session PATH
|
|
$env:PATH = "$scoopShims;$env:PATH"
|
|
|
|
# Verify scoop is now available
|
|
Write-Output "🔍 Verifying Scoop installation..."
|
|
scoop --version
|
|
Write-Output "✅ Scoop verified in current session"
|
|
|
|
# Add Git (required for buckets)
|
|
Write-Output "📦 Installing Git..."
|
|
scoop install git
|
|
Write-Output "✅ Git installed"
|
|
|
|
# Add r-bucket for R packages
|
|
Write-Output "📦 Adding r-bucket..."
|
|
scoop bucket add r-bucket https://github.com/cderv/r-bucket.git
|
|
Write-Output "✅ r-bucket added"
|
|
|
|
# Add extras bucket
|
|
Write-Output "📦 Adding extras bucket..."
|
|
scoop bucket add extras
|
|
Write-Output "✅ extras bucket added"
|
|
|
|
Write-Output "✅ Scoop setup complete"
|
|
|
|
- name: 📦 Install Chocolatey Package Manager (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING CHOCOLATEY PACKAGE MANAGER ==="
|
|
Write-Output "This matches the Windows container setup (Phase 2)"
|
|
|
|
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
|
|
Write-Output "📦 Installing Chocolatey..."
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
iex ((New-Object Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
|
Write-Output "✅ Chocolatey installed"
|
|
|
|
# Refresh environment variables for current session
|
|
$env:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
|
|
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
|
|
refreshenv
|
|
} else {
|
|
Write-Output "✅ Chocolatey already installed"
|
|
}
|
|
|
|
choco --version
|
|
Write-Output "✅ Chocolatey setup complete"
|
|
|
|
# === WINDOWS: Install Tools via Scoop (same as container) ===
|
|
- name: 🐍 Install Python via Scoop (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING PYTHON VIA SCOOP ==="
|
|
Write-Output "This matches the Windows container setup (Phase 9)"
|
|
Write-Output "Installing Python 3.13 (3.14 not yet supported by pydantic-core)"
|
|
|
|
# Add versions bucket for specific version installs
|
|
Write-Output "📦 Adding versions bucket..."
|
|
scoop bucket add versions
|
|
Write-Output "✅ Versions bucket added"
|
|
|
|
# List available Python versions
|
|
Write-Output "📋 Available Python packages:"
|
|
scoop search python | Select-String "python"
|
|
|
|
# Install Python 3.13 from versions bucket
|
|
Write-Output "📦 Installing Python 3.13 from versions bucket..."
|
|
scoop install versions/python313
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "❌ Failed to install versions/python313"
|
|
Write-Output "⚠️ Trying alternative: python3.13 or python from main..."
|
|
|
|
# Try python3.13 as alternative name
|
|
scoop install python3.13 -ErrorAction SilentlyContinue
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Output "⚠️ python3.13 not found, trying main/python..."
|
|
scoop install main/python
|
|
}
|
|
}
|
|
|
|
Write-Output "✅ Python install command completed"
|
|
|
|
# Check which Python app was installed
|
|
Write-Output "🔍 Checking installed Python apps..."
|
|
scoop list | Select-String "python"
|
|
|
|
# Try to get Python path - try different app names
|
|
$pythonPath = $null
|
|
$appNames = @('python313', 'python3.13', 'python')
|
|
foreach ($appName in $appNames) {
|
|
$pythonPath = scoop prefix $appName 2>$null
|
|
if ($pythonPath) {
|
|
Write-Output "✅ Found Python as '$appName'"
|
|
break
|
|
}
|
|
}
|
|
|
|
if (-not $pythonPath) {
|
|
Write-Error "❌ Could not find Python installation via Scoop"
|
|
Write-Error "Checked app names: python313, python3.13, python"
|
|
scoop list
|
|
exit 1
|
|
}
|
|
|
|
Write-Output "📁 Python location: $pythonPath"
|
|
|
|
# Add to GITHUB_PATH for subsequent steps
|
|
Write-Output "🔗 Adding Python to GITHUB_PATH for future steps..."
|
|
echo "$pythonPath" | Out-File -Append -Encoding UTF8 $env:GITHUB_PATH
|
|
echo "$pythonPath\Scripts" | Out-File -Append -Encoding UTF8 $env:GITHUB_PATH
|
|
Write-Output "✅ Python added to GITHUB_PATH"
|
|
|
|
# Also add to current session
|
|
$env:PATH = "$pythonPath;$pythonPath\Scripts;$env:PATH"
|
|
|
|
$pythonVersion = python --version
|
|
Write-Output "📊 Python version: $pythonVersion"
|
|
|
|
# Verify we got Python 3.13
|
|
if ($pythonVersion -notmatch "3\.13") {
|
|
Write-Error "❌ Expected Python 3.13, got: $pythonVersion"
|
|
exit 1
|
|
}
|
|
|
|
Write-Output "✅ Python 3.13 installation verified and added to PATH"
|
|
|
|
- name: 📊 Install R via Scoop (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING R VIA SCOOP ==="
|
|
Write-Output "This matches the Windows container setup (Phase 12)"
|
|
|
|
Write-Output "📦 Installing R ${{ inputs.r-version }} from main bucket..."
|
|
scoop install main/r@${{ inputs.r-version }}
|
|
Write-Output "✅ R installed"
|
|
|
|
Write-Output "📊 Verifying R installation..."
|
|
R.exe --version
|
|
Write-Output "✅ R installation complete"
|
|
|
|
- name: 🎨 Install Inkscape via Scoop (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING INKSCAPE VIA SCOOP ==="
|
|
Write-Output "This matches the Windows container setup (Phase 8)"
|
|
|
|
Write-Output "📦 Installing Inkscape..."
|
|
scoop install inkscape
|
|
Write-Output "✅ Inkscape installed"
|
|
|
|
Write-Output "📊 Inkscape version:"
|
|
inkscape --version
|
|
Write-Output "✅ Inkscape installation complete"
|
|
|
|
- name: 📦 Install Ghostscript via Scoop (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING GHOSTSCRIPT VIA SCOOP ==="
|
|
Write-Output "This matches the Windows container setup (Phase 7)"
|
|
|
|
Write-Output "📦 Installing Ghostscript..."
|
|
scoop install main/ghostscript
|
|
Write-Output "✅ Ghostscript installed"
|
|
|
|
Write-Output "📊 Ghostscript version:"
|
|
gs --version
|
|
Write-Output "✅ Ghostscript installation complete"
|
|
|
|
- name: 📦 Install Visual C++ Redistributable (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING VISUAL C++ REDISTRIBUTABLE ==="
|
|
Write-Output "This matches the Windows container setup (Phase 11)"
|
|
Write-Output "Required for Quarto DLL dependencies on Windows"
|
|
|
|
choco install vcredist-all -y
|
|
Write-Output "✅ Visual C++ Redistributable installed"
|
|
|
|
# === LINUX: Setup Python (GitHub Action) ===
|
|
- name: 🐍 Set up Python (Linux)
|
|
if: matrix.enabled && runner.os == 'Linux'
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: '3.13'
|
|
|
|
# === Setup Quarto (Linux + Windows via quarto-actions) ===
|
|
- name: 📦 Setup Quarto
|
|
if: matrix.enabled
|
|
uses: quarto-dev/quarto-actions/setup@v2
|
|
with:
|
|
version: ${{ inputs.quarto-version }}
|
|
|
|
# === CROSS-PLATFORM: Verification ===
|
|
- name: 📋 Quarto Setup Info
|
|
if: matrix.enabled
|
|
shell: bash
|
|
run: |
|
|
echo "🔄 Checking Quarto installation..."
|
|
quarto check
|
|
echo "📊 Quarto version info:"
|
|
quarto --version
|
|
echo "📍 Quarto installation location:"
|
|
which quarto || where.exe quarto
|
|
|
|
- name: 💾 Cache Python packages
|
|
if: matrix.enabled
|
|
uses: actions/cache@v5
|
|
id: cache-python-packages
|
|
with:
|
|
path: |
|
|
~/.cache/pip
|
|
~\AppData\Local\pip\Cache
|
|
key: python-pkgs-${{ runner.os }}-${{ hashFiles(format('{0}/requirements.txt', vars.BOOK_DEPS)) }}
|
|
restore-keys: |
|
|
python-pkgs-${{ runner.os }}-
|
|
|
|
# Install Ghostscript before Python package verification
|
|
- name: 📦 Install Ghostscript (Linux)
|
|
if: matrix.enabled && runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y ghostscript
|
|
|
|
- name: 📦 Install Python dependencies (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING PYTHON DEPENDENCIES ==="
|
|
Write-Output "Using Scoop-installed Python (should be 3.13.x)"
|
|
|
|
# Try to find Scoop's Python with different app names
|
|
$scoopPython = $null
|
|
$appNames = @('python313', 'python3.13', 'python')
|
|
foreach ($appName in $appNames) {
|
|
$path = scoop prefix $appName 2>$null
|
|
if ($path) {
|
|
$scoopPython = $path
|
|
Write-Output "✅ Found Scoop Python as '$appName'"
|
|
break
|
|
}
|
|
}
|
|
|
|
if (-not $scoopPython) {
|
|
Write-Error "❌ Could not find Scoop Python installation"
|
|
Write-Error "GITHUB_PATH was not set correctly in previous step"
|
|
Write-Error "Current Python being used:"
|
|
python --version
|
|
(Get-Command python).Source
|
|
exit 1
|
|
}
|
|
|
|
Write-Output "📁 Scoop Python location: $scoopPython"
|
|
|
|
# Prepend to PATH and verify immediately
|
|
$env:PATH = "$scoopPython;$scoopPython\Scripts;$env:PATH"
|
|
|
|
# Verify we're using the right Python
|
|
$pythonExe = (Get-Command python).Source
|
|
Write-Output "📍 Python executable being used: $pythonExe"
|
|
|
|
$pythonVersion = python --version
|
|
Write-Output "📊 Python version: $pythonVersion"
|
|
|
|
# Fail if not Python 3.13
|
|
if ($pythonVersion -notmatch "3\.13") {
|
|
Write-Error "❌ Wrong Python. Expected 3.13 from Scoop, got: $pythonVersion"
|
|
Write-Error "Python location: $pythonExe"
|
|
Write-Error "This means PATH is not configured correctly"
|
|
exit 1
|
|
}
|
|
Write-Output "✅ Confirmed using Scoop Python 3.13"
|
|
|
|
Write-Output "📦 Upgrading pip..."
|
|
python -m pip install --upgrade pip
|
|
|
|
Write-Output "📦 Installing Python packages from requirements.txt..."
|
|
python -m pip install -r ${{ vars.BOOK_DEPS }}/requirements.txt
|
|
Write-Output "✅ Python dependencies installed"
|
|
|
|
- name: 📦 Install Python dependencies (Linux)
|
|
if: matrix.enabled && runner.os == 'Linux'
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
python -m pip install -r ${{ vars.BOOK_DEPS }}/requirements.txt
|
|
|
|
# Cache Linux system packages without hardcoded paths
|
|
- name: 💾 Cache APT packages
|
|
if: matrix.enabled && runner.os == 'Linux'
|
|
uses: actions/cache@v5
|
|
id: cache-apt
|
|
with:
|
|
path: ~/.apt-cache
|
|
key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/*.yml') }}
|
|
restore-keys: |
|
|
apt-${{ runner.os }}-
|
|
|
|
- name: 🛠️ Install Linux Dependencies
|
|
if: matrix.enabled && runner.os == 'Linux' && steps.cache-apt.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
echo "🔄 Installing Linux dependencies..."
|
|
echo "📦 Creating APT cache directory"
|
|
mkdir -p ~/.apt-cache
|
|
|
|
echo "📦 Updating package lists"
|
|
sudo apt-get update
|
|
|
|
echo "📦 Installing required system libraries"
|
|
sudo apt-get -o dir::cache::archives="$HOME/.apt-cache" install -y \
|
|
fonts-dejavu \
|
|
fonts-freefont-ttf \
|
|
gdk-pixbuf2.0-bin \
|
|
libcairo2 \
|
|
libfontconfig1 \
|
|
libfontconfig1-dev \
|
|
libfreetype6 \
|
|
libfreetype6-dev \
|
|
libpango-1.0-0 \
|
|
libpangocairo-1.0-0 \
|
|
libpangoft2-1.0-0 \
|
|
libxml2-dev \
|
|
libcurl4-openssl-dev \
|
|
libjpeg-dev \
|
|
libtiff5-dev \
|
|
libpng-dev \
|
|
libharfbuzz-dev \
|
|
libfribidi-dev \
|
|
librsvg2-dev \
|
|
libgdal-dev \
|
|
libudunits2-dev
|
|
|
|
echo "✅ Linux dependencies installed"
|
|
|
|
- name: 🎨 Install Inkscape and font dependencies (Linux)
|
|
if: matrix.enabled && runner.os == 'Linux'
|
|
run: |
|
|
# First remove any existing Inkscape
|
|
sudo apt-get remove -y inkscape || true
|
|
# Install Inkscape from PPA for more reliable version
|
|
echo "📦 Installing Inkscape from PPA..."
|
|
sudo add-apt-repository ppa:inkscape.dev/stable -y
|
|
sudo apt-get update
|
|
sudo apt-get install -y inkscape
|
|
|
|
# Install font dependencies
|
|
echo "📦 Installing font dependencies..."
|
|
sudo apt-get install -y \
|
|
fonts-freefont-ttf \
|
|
fonts-liberation \
|
|
fontconfig
|
|
|
|
# Update font cache after installing Inkscape and fonts
|
|
echo "🧹 Updating font cache..."
|
|
sudo fc-cache -fv
|
|
|
|
# Verify Inkscape installation
|
|
echo "📊 Inkscape version:"
|
|
inkscape --version
|
|
|
|
# Test SVG to PDF conversion with the new Inkscape
|
|
echo "🧪 Testing Inkscape SVG to PDF conversion..."
|
|
echo '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><circle cx="50" cy="50" r="40" fill="red"/></svg>' > test.svg
|
|
inkscape --export-type=pdf --export-filename=test.pdf test.svg
|
|
|
|
# Verify if the PDF was created
|
|
if [ -f test.pdf ]; then
|
|
echo "✅ Inkscape SVG to PDF conversion successful!"
|
|
ls -lh test.pdf
|
|
else
|
|
echo "❌ Inkscape SVG to PDF conversion failed."
|
|
echo "🔍 Checking Inkscape installation..."
|
|
dpkg -l | grep inkscape
|
|
which inkscape
|
|
ldd $(which inkscape) | grep "not found" || echo "All dependencies resolved"
|
|
fi
|
|
|
|
# Install TeX Live packages - ALWAYS install for consistency
|
|
# Required for TikZ diagrams in HTML (PDF->SVG), PDF output, and potential future needs
|
|
|
|
# === WINDOWS: Install TeX Live via Chocolatey (same as container) ===
|
|
- name: 📦 Install TeX Live via Chocolatey (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== INSTALLING TEX LIVE VIA CHOCOLATEY ==="
|
|
Write-Output "This matches the Windows container setup (Phase 4)"
|
|
|
|
Write-Output "📦 Installing TeX Live via Chocolatey (pinned to 2025.20251008.0)..."
|
|
choco install texlive --version=2025.20251008.0 -y
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "❌ TeX Live installation failed"
|
|
exit 1
|
|
}
|
|
Write-Output "✅ TeX Live installed via Chocolatey"
|
|
|
|
Write-Output "🔍 Finding TeX Live installation directory..."
|
|
$texRoot = Join-Path $env:SystemDrive 'texlive'
|
|
Write-Output "📁 TeX Live root: $texRoot"
|
|
|
|
Write-Output "🔍 Looking for year-based directories..."
|
|
$texYearDir = Get-ChildItem $texRoot -Directory |
|
|
Where-Object { $_.Name -match '^\d{4}$' } |
|
|
Sort-Object Name -Descending |
|
|
Select-Object -First 1
|
|
Write-Output "📁 Found year directory: $($texYearDir.FullName)"
|
|
|
|
$texLiveBin = Join-Path $texYearDir.FullName 'bin\windows'
|
|
Write-Output "📁 TeX Live bin directory: $texLiveBin"
|
|
|
|
Write-Output "🔧 Adding TeX Live to PATH..."
|
|
echo "$texLiveBin" | Out-File -Append -Encoding UTF8 $env:GITHUB_PATH
|
|
$env:PATH = "$texLiveBin;$env:PATH"
|
|
Write-Output "✅ PATH updated with: $texLiveBin"
|
|
|
|
Write-Output "📋 Reading collections from tl_packages..."
|
|
if (Test-Path '${{ vars.BOOK_DEPS }}/tl_packages') {
|
|
$collections = Get-Content '${{ vars.BOOK_DEPS }}/tl_packages' |
|
|
Where-Object { $_.Trim() -ne '' -and -not $_.Trim().StartsWith('#') }
|
|
Write-Output "📦 Found $($collections.Count) collections to install"
|
|
Write-Output "📋 Collections:"
|
|
$collections | ForEach-Object { Write-Output " - $_" }
|
|
|
|
Write-Output "🔄 Installing collections..."
|
|
$i = 1
|
|
foreach ($collection in $collections) {
|
|
Write-Output "📦 [$i/$($collections.Count)] Installing $collection..."
|
|
& "$texLiveBin\tlmgr.bat" install $collection
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Output "✅ $collection installed successfully"
|
|
} else {
|
|
Write-Output "⚠️ Failed to install $collection, continuing..."
|
|
}
|
|
$i++
|
|
}
|
|
Write-Output "✅ Collection installation complete"
|
|
} else {
|
|
Write-Output "⚠️ No tl_packages file found, skipping collection installation"
|
|
}
|
|
|
|
Write-Output "🔄 Updating tlmgr..."
|
|
& "$texLiveBin\tlmgr.bat" update --self --all
|
|
Write-Output "✅ tlmgr updated"
|
|
|
|
Write-Output "🔍 Verifying lualatex installation..."
|
|
& "$texLiveBin\lualatex.exe" --version
|
|
Write-Output "✅ TeX Live installation verified"
|
|
|
|
# === LINUX: Install TeX Live via GitHub Action ===
|
|
- name: 📦 Install TeX Live packages (Linux)
|
|
if: matrix.enabled && runner.os == 'Linux'
|
|
uses: zauguin/install-texlive@v4
|
|
with:
|
|
package_file: ${{ vars.BOOK_DEPS }}/tl_packages
|
|
texlive_version: 2025
|
|
cache_version: 1
|
|
|
|
- name: 🔍 Verify TeX Live Installation
|
|
if: matrix.enabled
|
|
shell: bash
|
|
run: |
|
|
echo "🔄 Verifying TeX Live installation (installed for all builds)..."
|
|
echo "📊 Current format: ${{ matrix.format }}"
|
|
echo "📊 Philosophy: All builds get same environment, only build targets differ"
|
|
|
|
# Check LaTeX engines
|
|
echo "📊 Checking LaTeX engines:"
|
|
which lualatex || echo "❌ lualatex not found"
|
|
which pdflatex || echo "❌ pdflatex not found"
|
|
lualatex --version | head -2 || echo "❌ lualatex version failed"
|
|
|
|
# Check if required packages are available
|
|
echo "📊 Checking core LaTeX and TikZ packages:"
|
|
kpsewhich pgf.sty && echo "✅ PGF package found" || echo "❌ PGF package missing"
|
|
kpsewhich pgfplots.sty && echo "✅ PGFPlots package found" || echo "❌ PGFPlots package missing"
|
|
kpsewhich xcolor.sty && echo "✅ XColor package found" || echo "❌ XColor package missing"
|
|
kpsewhich amsmath.sty && echo "✅ AMSMath package found" || echo "❌ AMSMath package missing"
|
|
kpsewhich standalone.cls && echo "✅ Standalone class found" || echo "❌ Standalone class missing"
|
|
|
|
echo "📊 Checking font packages:"
|
|
kpsewhich phvr7t.tfm && echo "✅ Helvetica font found" || echo "❌ Helvetica font missing"
|
|
kpsewhich t1phv.fd && echo "✅ Helvetica font descriptor found" || echo "❌ Helvetica font descriptor missing"
|
|
|
|
# Test TikZ compilation
|
|
echo "🧪 Testing TikZ compilation..."
|
|
cat > test_tikz.tex << 'EOF'
|
|
\documentclass{standalone}
|
|
\usepackage{tikz}
|
|
\usepackage{pgfplots}
|
|
\usepackage{amsmath}
|
|
\usepackage{amssymb}
|
|
\usepackage{xcolor}
|
|
\usepackage[T1]{fontenc}
|
|
\usetikzlibrary{positioning}
|
|
\usetikzlibrary{calc}
|
|
\begin{document}
|
|
\begin{tikzpicture}[font=\small\usefont{T1}{phv}{m}{n}]
|
|
\node[draw, fill=blue!20] at (0,0) {TikZ Test};
|
|
\node[draw, fill=red!20] at (2,0) {Success};
|
|
\draw[->] (0.8,0) -- (1.2,0);
|
|
\end{tikzpicture}
|
|
\end{document}
|
|
EOF
|
|
|
|
if lualatex -interaction=nonstopmode test_tikz.tex; then
|
|
echo "✅ TikZ compilation successful"
|
|
ls -la test_tikz.pdf
|
|
else
|
|
echo "❌ TikZ compilation failed"
|
|
cat test_tikz.log | tail -20 || echo "No log file found"
|
|
fi
|
|
|
|
rm -f test_tikz.*
|
|
|
|
# === LINUX: Setup R via GitHub Action ===
|
|
- name: 📊 Setup R (Linux)
|
|
if: matrix.enabled && runner.os == 'Linux'
|
|
uses: r-lib/actions/setup-r@v2
|
|
with:
|
|
r-version: ${{ inputs.r-version }}
|
|
use-public-rspm: true
|
|
|
|
# === CROSS-PLATFORM: R Setup Info ===
|
|
- name: 📋 R Setup Info (Windows)
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "🔄 R Version Information:"
|
|
R.exe --version
|
|
Write-Output "📁 R home:"
|
|
Rscript -e 'cat(R.home(), "\n")'
|
|
Write-Output "📊 R library paths:"
|
|
Rscript -e '.libPaths()'
|
|
|
|
- name: 📋 R Setup Info (Linux)
|
|
if: matrix.enabled && runner.os == 'Linux'
|
|
shell: bash
|
|
run: |
|
|
echo "🔄 R Version Information:"
|
|
R --version | head -3
|
|
echo "📁 R home:"
|
|
Rscript -e 'cat(R.home(), "\n")'
|
|
echo "📊 R library paths:"
|
|
Rscript -e '.libPaths()'
|
|
|
|
# Cache R packages using standard paths
|
|
# Cache version bumped to force reinstall when adding packages (e.g. ggrepel for hw_acceleration.qmd)
|
|
- name: 💾 Cache R packages
|
|
if: matrix.enabled
|
|
uses: actions/cache@v5
|
|
id: cache-r-packages
|
|
with:
|
|
path: |
|
|
${{ env.R_LIBS_USER }}
|
|
key: r-pkgs-${{ runner.os }}-${{ inputs.r-version }}-v2-${{ hashFiles(format('{0}/install_packages.R', vars.BOOK_DEPS), '**/*.qmd') }}
|
|
restore-keys: |
|
|
r-pkgs-${{ runner.os }}-${{ inputs.r-version }}-v2-
|
|
|
|
- name: 📦 Install R packages
|
|
if: matrix.enabled && steps.cache-r-packages.outputs.cache-hit != 'true'
|
|
shell: Rscript {0}
|
|
run: |
|
|
# This matches the Windows container setup (Phase 13)
|
|
# Set options for better package installation
|
|
options(repos = c(CRAN = "https://cran.rstudio.com"))
|
|
|
|
cat("=== INSTALLING R PACKAGES ===\n")
|
|
cat("This matches the Windows container setup\n\n")
|
|
|
|
cat("🔄 Setting up R environment...\n")
|
|
cat(paste("R library path:", Sys.getenv("R_LIBS_USER"), "\n"))
|
|
|
|
# Create and set library path
|
|
lib_path <- Sys.getenv("R_LIBS_USER")
|
|
dir.create(lib_path, showWarnings = FALSE, recursive = TRUE)
|
|
.libPaths(lib_path)
|
|
|
|
# Install remotes first
|
|
cat("📦 Installing remotes package...\n")
|
|
install.packages("remotes")
|
|
|
|
# Install packages from install_packages.R
|
|
if (file.exists("${{ vars.BOOK_DEPS }}/install_packages.R")) {
|
|
cat("📦 Installing packages from ${{ vars.BOOK_DEPS }}/install_packages.R...\n")
|
|
source("${{ vars.BOOK_DEPS }}/install_packages.R")
|
|
} else {
|
|
cat("⚠️ No ${{ vars.BOOK_DEPS }}/install_packages.R found, installing common packages\n")
|
|
pkgs <- c("rmarkdown", "knitr", "tidyverse", "ggplot2", "bookdown")
|
|
cat(paste("📦 Installing packages:", paste(pkgs, collapse=", "), "\n"))
|
|
install.packages(pkgs)
|
|
}
|
|
|
|
# Verify critical packages (same as container does inline)
|
|
cat("\n🔍 Verifying R package installation...\n")
|
|
for (p in c("rmarkdown", "knitr", "ggrepel")) {
|
|
if (!require(p, character.only=TRUE, quietly=TRUE)) {
|
|
stop(paste("missing:", p))
|
|
}
|
|
}
|
|
|
|
cat("✅ R package installation complete\n")
|
|
cat("📊 Installed packages:\n")
|
|
ip <- installed.packages()[, "Package"]
|
|
print(head(ip, 10))
|
|
cat(paste("Total packages installed:", length(ip), "\n"))
|
|
|
|
# === WINDOWS: Comprehensive Verification (matches container FINAL CHECKS) ===
|
|
- name: 🔍 Comprehensive Windows Environment Verification
|
|
if: matrix.enabled && runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Output "=== FINAL VERIFICATION WITH ENHANCED DIAGNOSTICS ==="
|
|
Write-Output "This matches the Windows container FINAL CHECKS phase"
|
|
Write-Output ""
|
|
|
|
Write-Output "🔍 SYSTEM DIAGNOSTICS:"
|
|
Write-Output "----------------------"
|
|
Write-Output "PATH environment variable:"
|
|
Write-Output $env:PATH
|
|
Write-Output ""
|
|
|
|
Write-Output "Visual C++ Redistributable check:"
|
|
Get-ChildItem 'C:\Windows\System32' -Filter 'msvcp*.dll' -ErrorAction SilentlyContinue | Select-Object Name, Length, LastWriteTime
|
|
Write-Output ""
|
|
|
|
Write-Output "📊 TOOL VERIFICATION:"
|
|
Write-Output "---------------------"
|
|
|
|
Write-Output "Checking Quarto..."
|
|
try {
|
|
quarto --version
|
|
Write-Output "✅ Quarto version check: PASSED"
|
|
Write-Output "Running Quarto check for comprehensive validation..."
|
|
& quarto check 2>&1 | Write-Output
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Output "✅ Quarto check: PASSED"
|
|
} else {
|
|
Write-Output "⚠️ Quarto check: ISSUES DETECTED"
|
|
Write-Output "Exit code: $LASTEXITCODE"
|
|
}
|
|
} catch {
|
|
Write-Output "❌ Quarto verification failed:"
|
|
Write-Output $_.Exception.Message
|
|
}
|
|
|
|
Write-Output "Checking Python..."
|
|
python --version
|
|
Write-Output "✅ Python verified"
|
|
|
|
Write-Output "Checking R..."
|
|
R.exe --version
|
|
Write-Output "✅ R verified"
|
|
|
|
Write-Output "Checking LaTeX..."
|
|
lualatex --version
|
|
Write-Output "✅ LaTeX verified"
|
|
|
|
Write-Output "Checking Ghostscript..."
|
|
gs --version
|
|
Write-Output "✅ Ghostscript verified"
|
|
|
|
Write-Output "Checking Inkscape..."
|
|
inkscape --version
|
|
Write-Output "✅ Inkscape verified"
|
|
|
|
Write-Output ""
|
|
Write-Output "🎯 VERIFICATION STATUS:"
|
|
Write-Output "------------------------"
|
|
Write-Output "✅ All tools verified successfully"
|
|
Write-Output "Ready to proceed with build"
|
|
|
|
- name: 🔨 Build ${{ matrix.format }}
|
|
if: matrix.enabled
|
|
id: build
|
|
shell: bash
|
|
run: |
|
|
OUTPUT_DIR="${{ matrix.output_dir }}"
|
|
echo "output_dir=${OUTPUT_DIR}" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "🚀 Setting up ${{ matrix.format }} configuration..."
|
|
echo "🔍 DEBUG: Current directory: $(pwd)"
|
|
echo "🔍 DEBUG: Listing repository root:"
|
|
ls -la
|
|
echo "🔍 DEBUG: Checking ${{ vars.BOOK_QUARTO }} exists:"
|
|
ls -la ${{ vars.BOOK_QUARTO }}/ || echo "${{ vars.BOOK_QUARTO }} not found!"
|
|
cd ${{ vars.BOOK_QUARTO }}
|
|
echo "🔍 DEBUG: Now in $(pwd)"
|
|
echo "🔍 DEBUG: Listing current directory:"
|
|
ls -la
|
|
echo "🔍 DEBUG: Checking config directory:"
|
|
ls -la config/ || echo "config directory not found!"
|
|
rm -f _quarto.yml
|
|
cp config/${{ matrix.config }} _quarto.yml
|
|
echo "✅ Configuration set to ${{ matrix.format }}"
|
|
|
|
echo "🔨 Building ${{ matrix.format }}..."
|
|
# Update status to building (with timeout, failures are non-fatal)
|
|
curl --max-time 10 --retry 2 --retry-delay 2 -X POST \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-H "Accept: application/vnd.github.v3+json" \
|
|
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}" \
|
|
-d "{
|
|
\"state\": \"pending\",
|
|
\"description\": \"Building ${{ matrix.format }} content (${{ matrix.os_name }})\",
|
|
\"context\": \"ci/quarto-build-${{ matrix.os_name }}-${{ matrix.format }}\"
|
|
}" || echo "⚠️ Failed to update commit status to pending (non-fatal)"
|
|
|
|
quarto render --to ${{ matrix.render_target }} --output-dir "../${OUTPUT_DIR}"
|
|
echo "✅ ${{ matrix.format }} build completed"
|
|
|
|
- name: 📋 Check Quarto Build Output
|
|
if: matrix.enabled
|
|
shell: bash
|
|
run: |
|
|
echo "🔄 Checking Quarto build output for ${{ matrix.format }}..."
|
|
echo "🔍 DEBUG: Current directory: $(pwd)"
|
|
cd ${{ vars.BOOK_ROOT }}
|
|
echo "🔍 DEBUG: Changed to: $(pwd)"
|
|
if [ -d "${{ steps.build.outputs.output_dir }}" ]; then
|
|
echo "✅ ${{ steps.build.outputs.output_dir }} directory exists"
|
|
echo "📊 Files in ${{ steps.build.outputs.output_dir }} directory:"
|
|
ls -la ${{ steps.build.outputs.output_dir }} | head -n 20
|
|
echo "📊 Total files in ${{ steps.build.outputs.output_dir }}:"
|
|
find ${{ steps.build.outputs.output_dir }} -type f | wc -l
|
|
else
|
|
echo "❌ ${{ steps.build.outputs.output_dir }} directory not found!"
|
|
exit 1
|
|
fi
|
|
|
|
- name: 📉 Compress PDF (Linux)
|
|
if: matrix.enabled && matrix.format == 'PDF' && runner.os == 'Linux'
|
|
working-directory: ${{ vars.BOOK_ROOT }}/${{ steps.build.outputs.output_dir }}
|
|
run: |
|
|
if [ -f "Machine-Learning-Systems.pdf" ]; then
|
|
echo "📉 Compressing PDF with professional compression tool..."
|
|
python3 ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_pdf.py \
|
|
--input "Machine-Learning-Systems.pdf" \
|
|
--output "compressed.pdf" \
|
|
--quality minimal \
|
|
--verbose
|
|
mv compressed.pdf Machine-Learning-Systems.pdf
|
|
echo "✅ PDF compression completed"
|
|
else
|
|
echo "⚠️ PDF file not found for compression"
|
|
fi
|
|
|
|
- name: 📉 Compress PDF (Windows)
|
|
if: matrix.enabled && matrix.format == 'PDF' && runner.os == 'Windows'
|
|
shell: pwsh
|
|
working-directory: ${{ vars.BOOK_ROOT }}/${{ steps.build.outputs.output_dir }}
|
|
run: |
|
|
# Set UTF-8 encoding for proper emoji display
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
$input = "Machine-Learning-Systems.pdf"
|
|
$output = "compressed.pdf"
|
|
|
|
if (!(Test-Path $input)) {
|
|
Write-Warning "⚠️ Input PDF not found! Skipping compression..."
|
|
exit 0 # Non-zero exit would fail the workflow
|
|
}
|
|
|
|
Write-Output "📉 Compressing PDF with professional compression tool..."
|
|
|
|
python ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_pdf.py --input $input --output $output --quality minimal --verbose
|
|
|
|
if (Test-Path $output) {
|
|
Write-Output "✅ PDF compression completed"
|
|
Move-Item -Force $output $input
|
|
} else {
|
|
Write-Warning "⚠️ Compression failed but continuing"
|
|
}
|
|
|
|
- name: 📚 Compress EPUB (Linux)
|
|
if: matrix.enabled && matrix.format == 'EPUB' && runner.os == 'Linux'
|
|
working-directory: ${{ vars.BOOK_ROOT }}/${{ steps.build.outputs.output_dir }}
|
|
run: |
|
|
if [ -f "Machine-Learning-Systems.epub" ]; then
|
|
echo "📚 Compressing EPUB with optimized compression tool..."
|
|
echo "🔍 DEBUG: GITHUB_WORKSPACE=${{ github.workspace }}"
|
|
echo "🔍 DEBUG: PWD=$(pwd)"
|
|
echo "🔍 DEBUG: Script path: ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_epub.py"
|
|
echo "🔍 DEBUG: Verifying Python and Pillow installation:"
|
|
python3 --version
|
|
python3 -c "import sys; print('Python path:', sys.executable)"
|
|
if ! python3 -c "import PIL; print('✅ Pillow version: ' + PIL.__version__)" 2>/dev/null; then
|
|
echo "⚠️ Pillow not found or incompatible, installing Pillow>=10.0.0..."
|
|
python3 -m pip install 'Pillow>=10.0.0' --quiet
|
|
python3 -c "import PIL; print('✅ Pillow version (installed): ' + PIL.__version__)"
|
|
fi
|
|
echo "🔍 DEBUG: Checking quarto directory structure:"
|
|
ls -la "${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/" || echo "❌ ${{ vars.BOOK_QUARTO }}/ not found"
|
|
echo "🔍 DEBUG: Checking for publish directory:"
|
|
ls -la "${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/" || echo "❌ ${{ vars.BOOK_QUARTO }}/publish/ not found"
|
|
echo "🔍 DEBUG: Repository root contents:"
|
|
ls -la "${{ github.workspace }}/" | head -10
|
|
python3 ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_epub.py \
|
|
--input "Machine-Learning-Systems.epub" \
|
|
--output "compressed.epub" \
|
|
--verbose
|
|
mv compressed.epub Machine-Learning-Systems.epub
|
|
echo "✅ EPUB compression completed (using optimized defaults: quality=50, max-size=1000px)"
|
|
else
|
|
echo "⚠️ EPUB file not found for compression"
|
|
fi
|
|
|
|
- name: 📚 Compress EPUB (Windows)
|
|
if: matrix.enabled && matrix.format == 'EPUB' && runner.os == 'Windows'
|
|
shell: pwsh
|
|
working-directory: ${{ vars.BOOK_ROOT }}/${{ steps.build.outputs.output_dir }}
|
|
run: |
|
|
# Set UTF-8 encoding for proper emoji display
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
$input = "Machine-Learning-Systems.epub"
|
|
$output = "compressed.epub"
|
|
|
|
if (!(Test-Path $input)) {
|
|
Write-Warning "⚠️ Input EPUB not found! Skipping compression..."
|
|
exit 0 # Non-zero exit would fail the workflow
|
|
}
|
|
|
|
Write-Output "📚 Compressing EPUB with optimized compression tool..."
|
|
Write-Output "🔍 DEBUG: Verifying Python and Pillow installation:"
|
|
python --version
|
|
python -c "import sys; print('Python path:', sys.executable)"
|
|
try {
|
|
python -c "import PIL; print('✅ Pillow version: ' + PIL.__version__)"
|
|
} catch {
|
|
Write-Output "⚠️ Pillow not found or incompatible, installing Pillow>=10.0.0..."
|
|
python -m pip install 'Pillow>=10.0.0' --quiet
|
|
python -c "import PIL; print('✅ Pillow version (installed): ' + PIL.__version__)"
|
|
}
|
|
|
|
python ${{ github.workspace }}/${{ vars.BOOK_QUARTO }}/publish/compress_epub.py --input $input --output $output --verbose
|
|
|
|
if (Test-Path $output) {
|
|
Write-Output "✅ EPUB compression completed (using optimized defaults: quality=50, max-size=1000px)"
|
|
Move-Item -Force $output $input
|
|
} else {
|
|
Write-Warning "⚠️ Compression failed but continuing"
|
|
}
|
|
|
|
- name: 📋 Build Declaration
|
|
if: matrix.enabled
|
|
id: build-declaration
|
|
shell: bash
|
|
run: |
|
|
if [ -n "${{ inputs.artifact_name }}" ]; then
|
|
ARTIFACT_NAME_RAW="${{ inputs.artifact_name }}-${{ matrix.os_name }}-${{ matrix.format }}-${{ matrix.volume }}"
|
|
else
|
|
FORMAT_LC=$(echo "${{ matrix.format }}" | tr '[:upper:]' '[:lower:]')
|
|
OS_LC=$(echo "${{ matrix.os_name }}" | tr '[:upper:]' '[:lower:]')
|
|
ARTIFACT_NAME_RAW="${{ inputs.target }}-${FORMAT_LC}-${{ matrix.volume }}-${OS_LC}"
|
|
fi
|
|
echo "📦 Build Declaration: Successfully created artifact '$ARTIFACT_NAME_RAW'"
|
|
echo "artifact_name=$ARTIFACT_NAME_RAW" >> $GITHUB_OUTPUT
|
|
|
|
- name: 📤 Upload build artifacts
|
|
if: matrix.enabled
|
|
uses: actions/upload-artifact@v6
|
|
with:
|
|
name: ${{ steps.build-declaration.outputs.artifact_name }}
|
|
path: ${{ steps.build.outputs.output_dir }}
|
|
|
|
- name: 📝 Collect Build Logs and System Info (Universal)
|
|
if: matrix.enabled && always() # Always run, even if previous steps failed
|
|
shell: pwsh
|
|
run: |
|
|
# Set UTF-8 encoding for proper emoji display
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
Write-Output "🔄 Collecting comprehensive build logs and system information..."
|
|
|
|
# Create logs directory structure
|
|
New-Item -Type Directory -Path "logs", "logs/system-info", "logs/build-output" -Force | Out-Null
|
|
|
|
# === COLLECT ALL BUILD LOGS ===
|
|
Write-Output "📄 Searching for build logs in all directories..."
|
|
|
|
# Find and copy all .log files from common locations
|
|
$logPaths = @("quarto", "build", "_book", ".", "_site")
|
|
foreach ($path in $logPaths) {
|
|
if (Test-Path $path) {
|
|
Write-Output "🔍 Searching in: $path"
|
|
Get-ChildItem -Path $path -Recurse -Include "*.log" -ErrorAction SilentlyContinue | ForEach-Object {
|
|
$relativePath = $_.FullName.Replace($PWD.Path, "").TrimStart("/\")
|
|
$sanitizedName = $relativePath -replace "[/\\:]", "_"
|
|
Write-Output "📄 Found log: $relativePath → logs/$sanitizedName"
|
|
Copy-Item $_.FullName "logs/$sanitizedName" -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
# Look for any output files that might contain build info
|
|
Get-ChildItem -Recurse -Include "*.out", "*.aux", "*.fls", "*.fdb_latexmk" -ErrorAction SilentlyContinue | ForEach-Object {
|
|
$relativePath = $_.FullName.Replace($PWD.Path, "").TrimStart("/\")
|
|
$sanitizedName = $relativePath -replace "[/\\:]", "_"
|
|
Copy-Item $_.FullName "logs/build-output/$sanitizedName" -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# === SYSTEM ENVIRONMENT ===
|
|
Write-Output "🖥️ Collecting system environment..."
|
|
@"
|
|
=== Build Environment Information ===
|
|
Date: $(Get-Date)
|
|
GitHub Workflow: ${{ github.workflow }}
|
|
GitHub Run ID: ${{ github.run_id }}
|
|
Runner OS: ${{ runner.os }}
|
|
Build Format: ${{ matrix.format }}
|
|
PowerShell Version: $($PSVersionTable.PSVersion)
|
|
Working Directory: $PWD
|
|
|
|
"@ | Out-File "logs/system-info/environment.log" -Encoding UTF8
|
|
|
|
# === QUARTO INFORMATION ===
|
|
Write-Output "📚 Collecting Quarto information..."
|
|
@"
|
|
=== Quarto Information ===
|
|
"@ | Out-File "logs/system-info/quarto-info.log" -Encoding UTF8
|
|
|
|
if (Get-Command quarto -ErrorAction SilentlyContinue) {
|
|
& quarto --version 2>&1 | Out-File "logs/system-info/quarto-info.log" -Append -Encoding UTF8
|
|
"--- Quarto Check ---" | Out-File "logs/system-info/quarto-info.log" -Append -Encoding UTF8
|
|
& quarto check 2>&1 | Out-File "logs/system-info/quarto-info.log" -Append -Encoding UTF8
|
|
} else {
|
|
"Quarto not found" | Out-File "logs/system-info/quarto-info.log" -Append -Encoding UTF8
|
|
}
|
|
|
|
# === SUMMARY ===
|
|
Write-Output "📊 Build log collection summary:"
|
|
Write-Output "📁 Log files collected:"
|
|
Get-ChildItem -Path "logs" -Recurse -File | ForEach-Object {
|
|
$size = if ($_.Length -gt 1MB) { "{0:N2} MB" -f ($_.Length / 1MB) }
|
|
elseif ($_.Length -gt 1KB) { "{0:N2} KB" -f ($_.Length / 1KB) }
|
|
else { "$($_.Length) bytes" }
|
|
Write-Output " 📄 $($_.FullName.Replace($PWD.Path, '.')) ($size)"
|
|
}
|
|
|
|
- name: 📤 Upload Build Logs (Always)
|
|
if: matrix.enabled && always() # Upload logs even if build fails
|
|
uses: actions/upload-artifact@v6
|
|
with:
|
|
name: build-logs-${{ matrix.os_name }}-${{ matrix.format }}-${{ github.run_id }}
|
|
path: logs/
|
|
if-no-files-found: warn
|
|
|
|
- name: 📋 Build Summary
|
|
if: matrix.enabled && always()
|
|
shell: pwsh
|
|
run: |
|
|
# Set UTF-8 encoding for proper emoji display
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
# TeX Live is now always installed for consistent environment across all builds
|
|
$texLiveStatus = "✅ Always installed (zauguin/install-texlive@v4)"
|
|
|
|
@"
|
|
## 📊 Build Status Summary for ${{ matrix.os_name }} ${{ matrix.format }}
|
|
**Status: ${{ job.status }}**
|
|
🎯 Target: ${{ inputs.target }}
|
|
📚 Quarto Version: ${{ inputs.quarto-version }}
|
|
🔬 R Version: ${{ inputs.r-version }}
|
|
🧩 Cache Status:
|
|
- TeX Live: $texLiveStatus
|
|
- R Packages: ${{ steps.cache-r-packages.outputs.cache-hit == 'true' && '✅ Hit' || '❌ Miss' }}
|
|
- Python Packages: ${{ steps.cache-python-packages.outputs.cache-hit == 'true' && '✅ Hit' || '❌ Miss' }}
|
|
📝 Debug Artifacts:
|
|
- Build logs available as artifact: build-logs-${{ matrix.os_name }}-${{ matrix.format }}-${{ github.run_id }}
|
|
⏰ Completed at: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
|
"@ | Add-Content $env:GITHUB_STEP_SUMMARY
|
|
|
|
- name: 🎯 Set Final Build Status
|
|
if: matrix.enabled && always()
|
|
continue-on-error: true # Don't fail the workflow if status update fails
|
|
shell: bash
|
|
run: |
|
|
echo "🎯 Setting final build status..."
|
|
|
|
# Determine the final status based on job status
|
|
if [ "${{ job.status }}" = "success" ]; then
|
|
STATE="success"
|
|
DESCRIPTION="Build completed successfully (${{ matrix.os_name }}, ${{ matrix.format }})"
|
|
else
|
|
STATE="failure"
|
|
DESCRIPTION="Build failed (${{ matrix.os_name }}, ${{ matrix.format }})"
|
|
fi
|
|
|
|
echo "📊 Final status: $STATE"
|
|
echo "📝 Description: $DESCRIPTION"
|
|
|
|
# Set the final commit status with timeout and retry logic
|
|
echo "🔄 Attempting to update commit status..."
|
|
|
|
# Try with shorter timeout to fail fast if network is unreachable
|
|
if curl --max-time 10 --retry 2 --retry-delay 2 -X POST \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-H "Accept: application/vnd.github.v3+json" \
|
|
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}" \
|
|
-d "{
|
|
\"state\": \"$STATE\",
|
|
\"description\": \"$DESCRIPTION\",
|
|
\"context\": \"ci/quarto-build-${{ matrix.os_name }}-${{ matrix.format }}\"
|
|
}"; then
|
|
echo "✅ Commit status updated successfully"
|
|
else
|
|
echo "⚠️ Failed to update commit status (network timeout or connection issue)"
|
|
echo "This is non-fatal and the build status is still recorded in the workflow"
|
|
fi
|
|
|
|
collect-outputs:
|
|
name: '📊 Collect Outputs'
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
if: always()
|
|
outputs:
|
|
build_success: ${{ steps.collect.outputs.build_success }}
|
|
linux_html_vol1_artifact: ${{ steps.collect.outputs.linux_html_vol1_artifact }}
|
|
linux_pdf_vol1_artifact: ${{ steps.collect.outputs.linux_pdf_vol1_artifact }}
|
|
linux_epub_vol1_artifact: ${{ steps.collect.outputs.linux_epub_vol1_artifact }}
|
|
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 }}
|
|
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_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 results
|
|
id: collect
|
|
run: |
|
|
# Determine overall build success
|
|
if [[ "${{ needs.build.result }}" == "success" || "${{ needs.build.result }}" == "skipped" ]]; then
|
|
echo "build_success=true" >> $GITHUB_OUTPUT
|
|
BUILD_SUCCESS_MSG="✅ Success"
|
|
else
|
|
echo "build_success=false" >> $GITHUB_OUTPUT
|
|
BUILD_SUCCESS_MSG="❌ Failure"
|
|
fi
|
|
|
|
TARGET="${{ inputs.target }}"
|
|
if [[ "${{ inputs.build_linux && inputs.build_html }}" == "true" ]]; then
|
|
echo "linux_html_vol1_artifact=${TARGET}-html-vol1-linux" >> $GITHUB_OUTPUT
|
|
echo "linux_html_vol2_artifact=${TARGET}-html-vol2-linux" >> $GITHUB_OUTPUT
|
|
fi
|
|
if [[ "${{ inputs.build_linux && inputs.build_pdf }}" == "true" ]]; then
|
|
echo "linux_pdf_vol1_artifact=${TARGET}-pdf-vol1-linux" >> $GITHUB_OUTPUT
|
|
echo "linux_pdf_vol2_artifact=${TARGET}-pdf-vol2-linux" >> $GITHUB_OUTPUT
|
|
fi
|
|
if [[ "${{ inputs.build_linux && inputs.build_epub }}" == "true" ]]; then
|
|
echo "linux_epub_vol1_artifact=${TARGET}-epub-vol1-linux" >> $GITHUB_OUTPUT
|
|
echo "linux_epub_vol2_artifact=${TARGET}-epub-vol2-linux" >> $GITHUB_OUTPUT
|
|
fi
|
|
if [[ "${{ inputs.build_windows && inputs.build_html }}" == "true" ]]; then
|
|
echo "windows_html_vol1_artifact=${TARGET}-html-vol1-windows" >> $GITHUB_OUTPUT
|
|
echo "windows_html_vol2_artifact=${TARGET}-html-vol2-windows" >> $GITHUB_OUTPUT
|
|
fi
|
|
if [[ "${{ inputs.build_windows && inputs.build_pdf }}" == "true" ]]; then
|
|
echo "windows_pdf_vol1_artifact=${TARGET}-pdf-vol1-windows" >> $GITHUB_OUTPUT
|
|
echo "windows_pdf_vol2_artifact=${TARGET}-pdf-vol2-windows" >> $GITHUB_OUTPUT
|
|
fi
|
|
if [[ "${{ inputs.build_windows && inputs.build_epub }}" == "true" ]]; then
|
|
echo "windows_epub_vol1_artifact=${TARGET}-epub-vol1-windows" >> $GITHUB_OUTPUT
|
|
echo "windows_epub_vol2_artifact=${TARGET}-epub-vol2-windows" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
echo "✅ Results collected - Status: $BUILD_SUCCESS_MSG"
|