blob: 5ab5178ed696c3b7717c1d1690b84c5381c86185 [file]
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
$rootdir = Get-Location
# The affected-tests tier (see scripts/affected_tests.py) passes its selection
# as a file rather than a variable so the list is not bounded by CI variable
# limits. The file holds "NONE" or one target per line; the full-suite form is
# "buildtests" plus the targets it does not aggregate, and so takes the same
# path as any other list.
#
# Read first, before vcvarsall, the ccache provisioning and the CMake configure:
# a diff that reaches no test at all selects "NONE", and such a job should cost
# a checkout rather than a full configure. Declining to schedule it at all
# would have to go through `rules:`, which GitLab evaluates when the pipeline is
# created -- before select:tests has produced this file -- so exiting early is
# the cheapest answer available within one pipeline.
$requested = @()
if (${EIGEN_CI_BUILD_TARGET_FILE}) {
$target_file = ${EIGEN_CI_BUILD_TARGET_FILE}
if (-Not [System.IO.Path]::IsPathRooted($target_file)) {
$target_file = Join-Path ${rootdir} $target_file
}
# Fail loudly rather than falling through to the default target: a missing
# selection would otherwise silently build the entire test suite.
if (-Not (Test-Path $target_file)) {
Write-Error ("EIGEN_CI_BUILD_TARGET_FILE=${EIGEN_CI_BUILD_TARGET_FILE} does not exist. " +
"The select:tests artifact is missing; refusing to guess a build target.")
Exit 1
}
$requested = @(Get-Content $target_file | ForEach-Object { $_.Trim() } |
Where-Object { $_ } | Sort-Object -Unique)
if ($requested -contains "NONE") {
Write-Host "No tests are affected by this merge request; nothing to build."
Exit 0
}
}
# Sweep MSVC intermediate files (_CL_*) that killed jobs left in the shared
# %TEMP%, which jobs predating the redirect below still use. The cutoff
# exceeds the 10 h job timeout, so a running job's files are never touched.
$stale_cl = @(Get-ChildItem -Path $env:TEMP -Filter "_CL_*" -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddHours(-12) })
if ($stale_cl.Count -gt 0) {
$stale_bytes = ($stale_cl | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
$stale_mb = [math]::Round(($stale_bytes / 1MB), 1)
Write-Host ("Sweeping {0} stale _CL_* file(s) ({1} MB) from {2}" -f $stale_cl.Count, $stale_mb, $env:TEMP)
$stale_cl | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue
}
# Redirect MSVC compiler intermediate files into a job-scoped directory (.temp).
# Normal completion removes .temp in the finally block below; after a timeout,
# cancellation, or runner crash, the next job on the same build slot reclaims
# it via GitLab Runner's default git clean -ffdx.
$job_temp = Join-Path $rootdir ".temp"
if (-Not (Test-Path $job_temp)) {
New-Item -ItemType Directory -Force -Path $job_temp | Out-Null
}
$env:TEMP = $job_temp
$env:TMP = $job_temp
# Find Visual Studio installation directory. -products * includes Build
# Tools installs, which vswhere's default product filter skips; without it a
# Build Tools-only runner gets an empty path here, vcvarsall never runs, and
# the configure step fails with "No CMAKE_CXX_COMPILER could be found".
$VS_INSTALL_DIR = &"${Env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath
# Run VCVarsAll.bat initialization script and extract environment variables.
# http://allen-mack.blogspot.com/2008/03/replace-visual-studio-command-prompt.html
cmd.exe /c "`"${VS_INSTALL_DIR}\VC\Auxiliary\Build\vcvarsall.bat`" $EIGEN_CI_MSVC_ARCH -vcvars_ver=$EIGEN_CI_MSVC_VER & set" |
foreach {
if ($_ -match "^([^=]+)=(.*)$") {
set-item -force -LiteralPath "ENV:\$($Matches[1])" -value "$($Matches[2])"
}
}
# Create and enter build directory.
if (-Not (Test-Path ${EIGEN_CI_BUILDDIR})) {
mkdir $EIGEN_CI_BUILDDIR
}
cd $EIGEN_CI_BUILDDIR
# We need to split EIGEN_CI_ADDITIONAL_ARGS, otherwise they are interpreted
# as a single argument. Split by space, unless double-quoted.
$split_args = [regex]::Split(${EIGEN_CI_ADDITIONAL_ARGS}, ' (?=(?:[^"]|"[^"]*")*$)' )
# Compiler cache launcher selection:
# Prefer sccache (Mozilla Shared Compilation Cache) with native Google Cloud Storage
# remote backend (gs://eigen-gitlab-ci-cache). If sccache or GCS authentication is
# unavailable, fall back to ccache with local disk / GitLab runner cache.
#
# Trust is established at every use, not at download time: runner-installed ccache
# must report at least the version required for MSVC support (>= 4.8), and restored
# or freshly extracted binaries must match the pinned SHA-256 — the GitLab cache is
# writable job output, so restoring it does not authenticate it. Re-verifying per use
# also ensures version bumps invalidate stale cached copies.
$sccache_exe = ""
$ccache_exe = ""
$compiler_launcher = ""
$launchers = @()
if ("${EIGEN_CI_CCACHE}" -eq "on") {
. (Join-Path ${rootdir} "ci/scripts/install_compiler_cache.ps1")
if ("${env:EIGEN_CI_SCCACHE}" -ne "off") {
$sccache_exe = Install-Sccache
}
$ccache_exe = Install-Ccache
# 1. Try starting sccache server if available
if ($sccache_exe) {
$env:SCCACHE_DIR = if ($env:SCCACHE_DIR) { $env:SCCACHE_DIR } elseif ($env:EIGEN_CI_SCCACHE_DIR) { $env:EIGEN_CI_SCCACHE_DIR } else { Join-Path $env:CI_PROJECT_DIR ".sccache" }
$env:SCCACHE_CACHE_SIZE = if ($env:SCCACHE_CACHE_SIZE) { $env:SCCACHE_CACHE_SIZE } elseif ($env:EIGEN_CI_SCCACHE_CACHE_SIZE) { $env:EIGEN_CI_SCCACHE_CACHE_SIZE } else { "4G" }
# Rewrite paths relative to rootdir for cross-runner / cross-directory cache hits.
$env:SCCACHE_BASEDIRS = $rootdir
# Isolate daemon port per job: CI_CONCURRENT_ID is only unique within one
# runner registration, and the Windows host runs multiple registrations.
$job_id = if ($env:CI_JOB_ID) { [int64]$env:CI_JOB_ID } else { 0 }
$env:SCCACHE_SERVER_PORT = [string](4226 + ($job_id % 10000))
# Keep the server for the whole job; see build.linux.script.sh.
$env:SCCACHE_IDLE_TIMEOUT = "0"
$cred_server_job = $null
if ($env:EIGEN_GCS_CACHE_TOKEN_RW -or $env:EIGEN_GCS_CACHE_TOKEN_RO) {
$env:SCCACHE_GCS_BUCKET = if ($env:EIGEN_CI_SCCACHE_GCS_BUCKET) { $env:EIGEN_CI_SCCACHE_GCS_BUCKET } else { "eigen-gitlab-ci-cache" }
$env:SCCACHE_MULTILEVEL_CHAIN = "disk,gcs"
if ($env:EIGEN_GCS_CACHE_TOKEN_RW) {
$env:SCCACHE_GCS_RW_MODE = "READ_WRITE"
} else {
$env:SCCACHE_GCS_RW_MODE = "READ_ONLY"
}
$url_secret = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | % {[char]$_})
$cred_server_job = Start-Job -ScriptBlock {
param($path_secret)
$tok = if ($env:EIGEN_GCS_CACHE_TOKEN_RW) { $env:EIGEN_GCS_CACHE_TOKEN_RW } else { $env:EIGEN_GCS_CACHE_TOKEN_RO }
$listener = New-Object System.Net.HttpListener
$tcp = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
$tcp.Start()
$p = $tcp.Server.LocalEndPoint.Port
$tcp.Stop()
$listener.Prefixes.Add("http://127.0.0.1:$p/")
$listener.Start()
Write-Output "PORT:$p"
while ($listener.IsListening) {
$ctx = $listener.GetContext()
$resp = $ctx.Response
if ($ctx.Request.RawUrl -ne "/token/$path_secret") {
$resp.StatusCode = 403
$resp.Close()
continue
}
$resp.ContentType = "application/json"
$body = [System.Text.Encoding]::UTF8.GetBytes("{`"accessToken`":`"$tok`"}")
$resp.ContentLength64 = $body.Length
$resp.OutputStream.Write($body, 0, $body.Length)
$resp.Close()
}
} -ArgumentList $url_secret
# Wait up to 2 seconds for local credential server to be ready
$cred_port = $null
for ($i = 0; $i -lt 40; $i++) {
$out = Receive-Job $cred_server_job -ErrorAction SilentlyContinue
if ($out) {
foreach ($line in $out) {
if ($line -match "^PORT:(\d+)$") {
$cred_port = $Matches[1]
break
}
}
}
if ($cred_port) {
try {
$res = Invoke-WebRequest -Uri "http://127.0.0.1:$cred_port/token/$url_secret" -UseBasicParsing -TimeoutSec 1 -ErrorAction SilentlyContinue
if ($res -and $res.StatusCode -eq 200) { break }
} catch {}
}
Start-Sleep -Milliseconds 50
}
if ($cred_port) {
$env:SCCACHE_GCS_CREDENTIALS_URL = "http://127.0.0.1:$cred_port/token/$url_secret"
} else {
Write-Warning "Local credential server failed to bind or respond; skipping GCS remote cache."
Remove-Item Env:SCCACHE_GCS_BUCKET -ErrorAction SilentlyContinue
Remove-Item Env:SCCACHE_GCS_RW_MODE -ErrorAction SilentlyContinue
Remove-Item Env:SCCACHE_MULTILEVEL_CHAIN -ErrorAction SilentlyContinue
if ($cred_server_job) {
Stop-Job $cred_server_job -ErrorAction SilentlyContinue
Remove-Job $cred_server_job -Force -ErrorAction SilentlyContinue
$cred_server_job = $null
}
}
}
& $sccache_exe --start-server | Out-Null
if ($LASTEXITCODE -eq 0) {
$compiler_launcher = "sccache"
& $sccache_exe --zero-stats | Out-Null
$sccache_cmake = $sccache_exe -replace '\\', '/'
$launchers = "-DCMAKE_C_COMPILER_LAUNCHER=${sccache_cmake}",
"-DCMAKE_CXX_COMPILER_LAUNCHER=${sccache_cmake}"
} else {
Write-Warning "sccache server failed to start (check GCS credentials/network); falling back to ccache."
if ($cred_server_job) {
Stop-Job $cred_server_job -ErrorAction SilentlyContinue
Remove-Job $cred_server_job -ErrorAction SilentlyContinue
}
}
}
# 2. Fall back to ccache if sccache is unavailable or failed to start
if ((-not $compiler_launcher) -and $ccache_exe) {
$compiler_launcher = "ccache"
# EIGEN_CI_CCACHE_* provide the YAML fallback defaults. A runner may explicitly
# set standard CCACHE_* variables (e.g. to a persistent host directory) in
# config.toml without being overridden by the YAML template.
if (-not $env:CCACHE_DIR -and $env:EIGEN_CI_CCACHE_DIR) {
$env:CCACHE_DIR = $env:EIGEN_CI_CCACHE_DIR
}
if (-not $env:CCACHE_MAXSIZE -and $env:EIGEN_CI_CCACHE_MAXSIZE) {
$env:CCACHE_MAXSIZE = $env:EIGEN_CI_CCACHE_MAXSIZE
}
if (-not $env:CCACHE_BASEDIR -and $env:EIGEN_CI_CCACHE_BASEDIR) {
$env:CCACHE_BASEDIR = $env:EIGEN_CI_CCACHE_BASEDIR
}
if (-not $env:CCACHE_COMPRESSLEVEL -and $env:EIGEN_CI_CCACHE_COMPRESSLEVEL) {
$env:CCACHE_COMPRESSLEVEL = $env:EIGEN_CI_CCACHE_COMPRESSLEVEL
}
# Forward slashes: CMake treats the launcher as a path-valued cache entry.
$ccache_cmake = $ccache_exe -replace '\\', '/'
$launchers = "-DCMAKE_C_COMPILER_LAUNCHER=${ccache_cmake}",
"-DCMAKE_CXX_COMPILER_LAUNCHER=${ccache_cmake}"
# Log stats per job via CCACHE_STATSLOG rather than global --zero-stats /
# --show-stats so concurrent jobs sharing a host CCACHE_DIR do not reset or
# mix each other's counters.
$env:CCACHE_STATSLOG = Join-Path (Get-Location) "ccache-stats.log"
if (Test-Path $env:CCACHE_STATSLOG) {
Remove-Item $env:CCACHE_STATSLOG -Force
}
}
}
$success = 0
$show_stats = $false
try {
# Configure build.
cmake -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel `
-DEIGEN_TEST_CUSTOM_CXX_FLAGS="${EIGEN_CI_TEST_CUSTOM_CXX_FLAGS}" `
${launchers} ${split_args} "${rootdir}"
# Targets that this configuration did not register (optional dependencies such
# as CHOLMOD or CUDA) are dropped from the selection read above: ninja aborts on
# an unknown target, and this is the first point that knows what CMake actually
# configured.
$selected_targets = @()
if (${EIGEN_CI_BUILD_TARGET_FILE}) {
$configured = @{}
try {
foreach ($line in (ninja -t targets all 2>$null)) {
if ($line -match '^([A-Za-z_0-9]+): phony$') { $configured[$Matches[1]] = $true }
}
} catch {
Write-Warning "Enumerating configured targets failed: $_"
}
# An empty query means ninja is unusable, not that nothing is configured.
# Without this the intersection below would be empty and the job would
# trivially "succeed" having built nothing.
if ($configured.Count -eq 0) {
Write-Error "Could not enumerate configured targets via 'ninja -t targets'."
Exit 1
}
$selected_targets = @($requested | Where-Object { $configured.ContainsKey($_) })
$unconfigured = @($requested | Where-Object { -Not $configured.ContainsKey($_) })
Write-Host ("Affected selection: {0} of {1} requested targets are configured here." -f
$selected_targets.Count, $requested.Count)
if ($unconfigured.Count -gt 0) {
Write-Host ("Not configured in this build: " + ($unconfigured -join " "))
}
if ($selected_targets.Count -eq 0) {
Write-Host "None of the affected tests exist in this configuration; nothing to build."
Exit 0
}
}
# Built as an array, not a string: PowerShell hands a single string containing
# spaces to a native command as one argument, which cmake rejects as an unknown
# target name. An affected selection is always a list, and the SME-style
# multi-target EIGEN_CI_BUILD_TARGET is one too.
$target = @()
if ($selected_targets.Count -gt 0) {
$target = @("--target") + $selected_targets
} elseif (${EIGEN_CI_BUILD_TARGET}) {
$target = @("--target") + @(${EIGEN_CI_BUILD_TARGET} -split '\s+' | Where-Object { $_ })
}
$show_stats = $true
# Windows builds sometimes fail due heap errors. In that case, try
# building the rest, then try to build again with a single thread.
cmake --build . ${target} -- -k0 || cmake --build . ${target} -- -k0 -j1
$success = $LASTEXITCODE
} finally {
# Hit/miss summary and daemon teardown. Runs on early exits and failures too:
# stopping the sccache server releases its Windows file lock on
# .sccache-bin\sccache.exe before the next job's git clean runs.
if ($compiler_launcher -eq "sccache" -and $sccache_exe) {
if ($show_stats) {
& $sccache_exe --show-stats
}
& $sccache_exe --stop-server 2>$null | Out-Null
if ($cred_server_job) {
Stop-Job $cred_server_job -ErrorAction SilentlyContinue
Remove-Job $cred_server_job -Force -ErrorAction SilentlyContinue
}
# Incompatible cache formats: purge unused .ccache\ before cache upload so the
# uploaded cache archive only holds .sccache\ and stays strictly below the 5 GB runner cap.
$old_ccache = Join-Path $rootdir ".ccache"
if (Test-Path $old_ccache) { Remove-Item -Recurse -Force $old_ccache }
} elseif ($compiler_launcher -eq "ccache" -and $ccache_exe) {
if ($show_stats) {
& $ccache_exe --show-log-stats
}
if (Test-Path $env:CCACHE_STATSLOG) {
Remove-Item $env:CCACHE_STATSLOG -Force
}
# Incompatible cache formats: purge unused .sccache\ before cache upload.
$old_sccache = Join-Path $rootdir ".sccache"
if (Test-Path $old_sccache) { Remove-Item -Recurse -Force $old_sccache }
}
# Return to root directory.
cd ${rootdir}
# Reclaim job-local temporary directory on normal exit; killed or timed-out
# jobs are cleaned up by the next job's git clean -ffdx.
if (Test-Path $job_temp) {
Remove-Item -Recurse -Force $job_temp -ErrorAction SilentlyContinue
}
}
# Explicitly propagate exit code to indicate pass/failure of build command.
if($success -ne 0) { Exit $success }