#!/bin/bash # SPDX-FileCopyrightText: The Eigen Authors # SPDX-License-Identifier: MPL-2.0 set -x rootdir=`pwd` mkdir -p ${EIGEN_CI_BUILDDIR} cd ${EIGEN_CI_BUILDDIR} # Compile through ccache when the job enables it and the image provides it. # The GitLab cache holds ${CCACHE_DIR}, keyed on content and compiler, so it # still hits after the fresh per-job clone re-stamps every source mtime # (which makes any cached ninja state rebuild from scratch). launchers="" if [[ "${EIGEN_CI_CCACHE}" == "on" ]] && command -v ccache >/dev/null 2>&1; then launchers="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" ccache --zero-stats fi cmake -G Ninja \ -DCMAKE_CXX_COMPILER=${EIGEN_CI_CXX_COMPILER} \ -DCMAKE_C_COMPILER=${EIGEN_CI_C_COMPILER} \ -DCMAKE_CXX_COMPILER_TARGET=${EIGEN_CI_CXX_COMPILER_TARGET} \ ${launchers} \ ${EIGEN_CI_ADDITIONAL_ARGS} ${rootdir} # 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. # Targets that this configuration did not register (optional dependencies such # as CHOLMOD, CUDA or SYCL) are dropped here: ninja aborts on an unknown target, # and this is the first point that knows what CMake actually configured. selected_targets="" if [[ -n "${EIGEN_CI_BUILD_TARGET_FILE}" ]]; then target_file="${EIGEN_CI_BUILD_TARGET_FILE}" [[ "${target_file}" = /* ]] || target_file="${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 [[ ! -f "${target_file}" ]]; then echo "EIGEN_CI_BUILD_TARGET_FILE=${EIGEN_CI_BUILD_TARGET_FILE} does not exist." >&2 echo "The select:tests artifact is missing; refusing to guess a build target." >&2 exit 1 fi requested=$(cat "${target_file}") if [[ "${requested}" == "NONE" ]]; then echo "No tests are affected by this merge request; nothing to build." cd ${rootdir} set +x return 0 2>/dev/null || exit 0 else { set +x; } 2>/dev/null # The runner sources this script under `set -eo pipefail`, so every command # substitution below has to end in a success status: a failed ninja, or a # grep that legitimately counts zero lines, would otherwise abandon the job # before the checks that are meant to report it. configured=$(ninja -t targets all 2>/dev/null | sed -n 's/^\([A-Za-z_0-9]*\): phony$/\1/p' | sort -u || true) # 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 [[ -z "${configured}" ]]; then echo "Could not enumerate configured targets via 'ninja -t targets'." >&2 exit 1 fi requested_targets=$(echo "${requested}" | sort -u) selected_targets=$(comm -12 <(echo "${requested_targets}") <(echo "${configured}")) unconfigured=$(comm -23 <(echo "${requested_targets}") <(echo "${configured}")) nrequested=$(echo "${requested_targets}" | grep -c . || true) nselected=$(echo "${selected_targets}" | grep -c . || true) echo "Affected selection: ${nselected} of ${nrequested} requested targets are configured here." if [[ -n "${unconfigured}" ]]; then echo "Not configured in this build: $(echo "${unconfigured}" | tr '\n' ' ')" fi set -x if [[ -z "${selected_targets}" ]]; then echo "None of the affected tests exist in this configuration; nothing to build." cd ${rootdir} set +x return 0 2>/dev/null || exit 0 fi EIGEN_CI_BUILD_TARGET=$(echo "${selected_targets}" | tr '\n' ' ') fi fi target="" if [[ ${EIGEN_CI_BUILD_TARGET} ]]; then target="--target ${EIGEN_CI_BUILD_TARGET}" fi # Builds (particularly gcc) sometimes get killed, potentially when running # out of resources. In that case, keep trying to build the remaining # targets (k0), then retry with reduced parallelism to minimize resource use. # EIGEN_CI_BUILD_JOBS can be set to limit parallelism for memory-hungry # compilers (e.g. NVHPC). When unset, leave -j off so ninja picks its # own default (NPROC + 2). njobs is still needed for batch-size sizing. njobs=${EIGEN_CI_BUILD_JOBS:-${NPROC}} jobs="" if [[ -n "${EIGEN_CI_BUILD_JOBS}" ]]; then jobs="-j${EIGEN_CI_BUILD_JOBS}" fi # Fallback parallelism for retry builds after a failure (default: 2). fallback_jobs="-j${EIGEN_CI_FALLBACK_JOBS:-2}" # For phony meta-targets (e.g. buildtests), shuffle the dependency list and # build in batches so that memory-hungry compilations (like bdcsvd with # nvc++) are spread out instead of all running at once. Ninja ignores the # command-line target order and schedules by its dependency graph, so we # must feed it batches to actually influence scheduling. # # Every batch is a barrier, so the batch has to hold enough work to keep all # cores busy until the slowest target in it finishes. Per-target compile times # are heavily right-skewed (median around 10s, worst case several minutes), so a # batch only a small multiple of njobs drains almost immediately and then runs # down to a single straggler. Measured runner occupancy on the 2026-08-02 # nightly, at the old depth of max(2 * njobs, 48): # # 32 vCPU, batch 64 depth 1.9x 39% busy # 8 vCPU, batch 48 depth 4.8x 66% busy # 4 vCPU, batch 48 depth 8.0x 78% busy # # Peak concurrency, and hence peak memory, is set by -j and is unchanged by the # batch size; the batch only controls which targets may co-run. Override with # EIGEN_CI_BUILD_BATCH_SIZE for fine-grained control. default_batch=$((njobs * 8)) default_batch=$((default_batch > 96 ? default_batch : 96)) batch_size=${EIGEN_CI_BUILD_BATCH_SIZE:-${default_batch}} shuffled=false # An affected-tests selection names CMake targets, and most of those are # aggregates: "buildtests", and the parent of every split test (bdcsvd depends # on bdcsvd_1..bdcsvd_41). The batch loop can only spread apart the targets it # is handed, so an unexpanded parent puts a whole test family in one batch and # lets all of its parts co-run. expand_to_leaves resolves them first. # # CMake emits every aggregate as a phony edge, so descending through phony # edges alone reaches the compiles and links without walking into object files: # buildtests -> test/bdcsvd_1, bdcsvd -> test/bdcsvd -> test/bdcsvd_1, and a # plain executable such as bug1213 -> test/bug1213, which is already a link # edge and stays as it is. expand_to_leaves() { local scratch depth scratch=$(mktemp -d) printf '%s\n' ${1} | awk 'NF' | sort -u > "${scratch}/frontier" : > "${scratch}/leaves" # Every level costs one ninja invocation over the whole frontier (about 0.1s # for the full suite). Three levels are enough for the graph above; the cap # only bounds an unexpected cycle. for depth in 1 2 3 4 5 6 7 8; do [[ -s "${scratch}/frontier" ]] || break # A query that cannot be answered is not a reason to drop targets: leave # the frontier unexpanded and let ninja resolve it during the build. ninja -t query $(cat "${scratch}/frontier") > "${scratch}/query" 2>/dev/null || break awk ' function flush() { if (name == "") return if (rule == "phony" && ninputs > 0) { for (i = 1; i <= ninputs; i++) print "N", inputs[i] } else { print "L", name } name = ""; rule = ""; ninputs = 0; section = "" } /^[^ ]/ { flush(); name = $0; sub(/:$/, "", name); next } /^ input:/ { rule = $2; section = "input"; next } /^ outputs:/ { section = ""; next } section == "input" && /^ / { inputs[++ninputs] = $1 } END { flush() } ' "${scratch}/query" > "${scratch}/classified" sed -n 's/^L //p' "${scratch}/classified" >> "${scratch}/leaves" sed -n 's/^N //p' "${scratch}/classified" | sort -u > "${scratch}/next" mv "${scratch}/next" "${scratch}/frontier" done # Whatever is still unexpanded -- a failed query or a chain past the cap -- # is built as named. Empty after a normal walk. cat "${scratch}/frontier" >> "${scratch}/leaves" sort -u "${scratch}/leaves" rm -rf "${scratch}" } deps="" if [[ -n "${selected_targets}" ]] && command -v ninja >/dev/null 2>&1; then { set +x; } 2>/dev/null deps=$(expand_to_leaves "${selected_targets}") # The meta-target path below quotes EIGEN_CI_BUILD_TARGET into `ninja -t query`, # so a space-separated list (the SME cross-build's product_* targets) would go in # as one bogus name and make the query fail, aborting the job before any build # runs. Skip batching for a list and let the plain `cmake --build --target t1 t2 # ...` at the end build it directly; it handles multiple targets. elif [[ -n "${EIGEN_CI_BUILD_TARGET}" && "${EIGEN_CI_BUILD_TARGET}" != *[[:space:]]* ]] && command -v ninja >/dev/null 2>&1; then # Suppress xtrace while extracting and shuffling the target list # to avoid dumping ~1200 lines to the CI log. { set +x; } 2>/dev/null deps=$(ninja -t query "${EIGEN_CI_BUILD_TARGET}" 2>/dev/null \ | awk '/^ input:/{found=1; next} /^ outputs:/{found=0} found && /^ /{print $1}') # CMake custom targets like BuildOfficial have an intermediate phony # (e.g. test/BuildOfficial) that holds the real dependencies. If we # got exactly one dep, resolve it one more level. if [[ $(echo "$deps" | wc -l) -eq 1 ]] && [[ -n "$deps" ]]; then inner=$(ninja -t query "$deps" 2>/dev/null \ | awk '/^ input:/{found=1; next} /^ outputs:/{found=0} found && /^ /{print $1}') if [[ -n "$inner" ]]; then deps="$inner" fi fi fi if [[ -n "${deps}" ]]; then # Deterministic shuffle: hash each target name and sort by hash. # Stable across runs (helps ninja's .ninja_log and build caches), # portable (no shuf dependency), and spreads same-family targets apart. # Uses Knuth's multiplicative hash (golden-ratio prime 2654435761) for # good avalanche — similar names like bdcsvd_1..bdcsvd_51 get widely # dispersed instead of clustering together. shuffled_deps=$(echo "$deps" | awk ' BEGIN { for(i=0;i<128;i++) ord[sprintf("%c",i)]=i } { h=0 for(i=1;i<=length($0);i++) h=(h+ord[substr($0,i,1)])*2654435761%2147483647 printf "%010d %s\n",h,$0 }' | sort | sed 's/^[^ ]* //') if [[ -n "$shuffled_deps" ]]; then ndeps=$(echo "$shuffled_deps" | wc -l) echo "Building ${ndeps} targets in batches of ${batch_size} (njobs=${njobs})" shuffled=true # Build in batches: ninja parallelises within each batch, but batches # run sequentially so memory-hungry targets from different families # don't pile up simultaneously. Track failures so we can report the # right exit code at the end. # Note: xtrace stays off to avoid dumping the full target list. # Use process substitution so the while loop runs in the current # shell and build_failed propagates. batch_num=0 build_failed=false while IFS= read -r batch; do batch_num=$((batch_num + 1)) echo "=== Batch ${batch_num} ===" ninja -k0 ${jobs} ${batch} || ninja -k0 ${fallback_jobs} ${batch} || build_failed=true done < <(echo "$shuffled_deps" | xargs -n "${batch_size}") if [[ "$build_failed" == "true" ]]; then echo "Some batches failed." # The cache is pushed even on failure (cache:when: always), so the # stats still describe what the next attempt can reuse. if [[ -n "${launchers}" ]]; then ccache --show-stats fi exit 1 fi fi set -x fi if [[ "$shuffled" != "true" ]]; then cmake --build . ${target} -- -k0 ${jobs} || cmake --build . ${target} -- -k0 ${fallback_jobs} fi # Hit/miss summary for judging what the cache pays for on this job. if [[ -n "${launchers}" ]]; then ccache --show-stats fi cd ${rootdir} set +x