Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-12 08:18:47

0001 #!/bin/bash
0002 set -u
0003 set -e
0004 set -o pipefail
0005 
0006 SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
0007 
0008 export SPACK_COLOR=always
0009 
0010 function start_section() {
0011     local section_name="$1"
0012     if [ -n "${GITHUB_ACTIONS:-}" ]; then
0013         echo "::group::${section_name}"
0014     else
0015         echo "${section_name}"
0016     fi
0017 }
0018 
0019 function end_section() {
0020     if [ -n "${GITHUB_ACTIONS:-}" ]; then
0021         echo "::endgroup::"
0022     fi
0023 }
0024 
0025 # Signatures in a command's output that indicate a *transient* failure (network
0026 # / registry hiccup) that a retry can plausibly fix. Anything that does not
0027 # match one of these is treated as a genuine failure and is NOT retried, so we
0028 # don't burn CI time looping on an error a retry cannot resolve (compile error,
0029 # disk full, bad spec, ...).
0030 #
0031 # NOTE on "no binary available": spack does *not* surface a throttled/failed
0032 # GHCR fetch as a network error. Under `--use-buildcache only` it swallows the
0033 # fetch failure and reports the spec as simply having no binary, which is by far
0034 # the most common transient failure we see. So those signatures are treated as
0035 # transient and retried. The cost of being wrong is bounded (a genuinely missing
0036 # binary just burns the retry budget before failing), whereas not retrying turns
0037 # every GHCR hiccup into a red build.
0038 TRANSIENT_ERROR_PATTERNS=(
0039   # spack buildcache misses caused by upstream fetch/rate-limit failures
0040   "No binary for"
0041   "no binary available"
0042   "NoBinaryFoundError"
0043   "cannot install.*cache-only"
0044   "Failed to install.*due to.*FetchError"
0045   "Failed to fetch"
0046   "fetch failed"
0047   "FetchError"
0048   "curl.*(error|failed|timed out|Could not resolve)"
0049   "Connection reset"
0050   "Connection refused"
0051   "Connection timed out"
0052   "Could not resolve host"
0053   "Temporary failure in name resolution"
0054   "Network is unreachable"
0055   "Read timed out"
0056   "read timeout"
0057   "timed out"
0058   "[Tt]imeout"
0059   "TLS handshake"
0060   "EOF occurred"
0061   "Server disconnected"
0062   "Remote end closed connection"
0063   "toomanyrequests"
0064   "Too Many Requests"
0065   "rate limit"
0066   # GitHub sheds anonymous load with "GitHub is temporarily limiting some
0067   # unauthenticated downloads to protect the stability of the platform. Please
0068   # retry later or authenticate." github_auth.sh keeps us off that quota, but
0069   # the message is explicitly a "try again" and must not be a hard failure.
0070   "temporarily limiting"
0071   "unauthenticated download"
0072   "Please retry later"
0073   "HTTP Error 429"
0074   "HTTP Error 5[0-9][0-9]"
0075   "50[0-9] (Internal Server Error|Bad Gateway|Service Unavailable|Gateway Time-out)"
0076   "unable to access.*github.com"
0077   "RPC failed"
0078   "early EOF"
0079 )
0080 
0081 # Run "$@", streaming its combined output while also capturing it. On failure,
0082 # retry with exponential backoff *only* when the captured output matches a known
0083 # transient-error signature; otherwise return the command's exit status
0084 # immediately. Output is streamed live (via tee) so long-running commands don't
0085 # trip CI "no output" watchdogs.
0086 #
0087 # Backoff is 20s, 40s, 80s, 160s, 320s (+/- jitter) by default: ~10 min of total
0088 # wait across 6 attempts. GHCR rate-limit windows outlast a short backoff, so a
0089 # tight schedule just burns all attempts inside the same bad window and still
0090 # fails. Jitter keeps the many parallel CI jobs from retrying in lockstep and
0091 # re-triggering the limit together.
0092 function retry_transient() {
0093   local max_attempts=${DEP_RETRY_MAX_ATTEMPTS:-6}
0094   local delay=${DEP_RETRY_BASE_DELAY:-20}
0095   local attempt=1
0096   local log status pat matched jitter sleep_for
0097   log=$(mktemp)
0098   while true; do
0099     echo "attempt ${attempt}/${max_attempts}: $*"
0100     status=0
0101     # pipefail (set above) makes PIPESTATUS[0] the command's own exit status.
0102     "$@" 2>&1 | tee "${log}" || status=${PIPESTATUS[0]}
0103     if [ "${status}" -eq 0 ]; then
0104       rm -f "${log}"
0105       return 0
0106     fi
0107 
0108     matched=""
0109     for pat in "${TRANSIENT_ERROR_PATTERNS[@]}"; do
0110       if grep -qiE -- "${pat}" "${log}"; then
0111         matched="${pat}"
0112         break
0113       fi
0114     done
0115     rm -f "${log}"
0116 
0117     if [ -z "${matched}" ]; then
0118       echo "Command failed (exit ${status}) with no transient-error signature; not retrying"
0119       return "${status}"
0120     fi
0121     if [ "${attempt}" -ge "${max_attempts}" ]; then
0122       echo "Command still failing after ${max_attempts} attempts (last transient signature: '${matched}')"
0123       return "${status}"
0124     fi
0125     # +/-25% jitter so parallel jobs don't retry in lockstep.
0126     jitter=$(( (RANDOM % (delay / 2 + 1)) - delay / 4 ))
0127     sleep_for=$(( delay + jitter ))
0128     [ "${sleep_for}" -lt 1 ] && sleep_for=1
0129     echo "Transient failure detected (matched '${matched}'); retrying in ${sleep_for}s"
0130     sleep "${sleep_for}"
0131     attempt=$((attempt + 1))
0132     delay=$((delay * 2))
0133   done
0134 }
0135 
0136 # Parse command line arguments
0137 while getopts "c:t:d:e:s:F:fh" opt; do
0138   case ${opt} in
0139     c )
0140       compiler=$OPTARG
0141       ;;
0142     F )
0143       flavor=$OPTARG
0144       ;;
0145     t )
0146       tag=$OPTARG
0147       ;;
0148     d )
0149       destination=$OPTARG
0150       ;;
0151     e )
0152       env_file=$OPTARG
0153       ;;
0154     s )
0155       cxx_std=$OPTARG
0156       ;;
0157     f )
0158       full_install=true
0159       ;;
0160     h )
0161       echo "Usage: $0 [-c compiler] [-t tag] [-d destination] -e env_file [-h]"
0162       echo "Options:"
0163       echo "  -c <compiler>    Specify compiler (defaults to CXX env var)"
0164       echo "  -t <tag>         Specify dependency tag (defaults to DEPENDENCY_TAG env var)"
0165       echo "  -d <destination> Specify install destination (defaults based on CI environment)"
0166       echo "  -e <env_file>    Specify environment file to output environments to"
0167       echo "  -s <cxx_std>     C++ standard for lockfile selection (e.g. 20, 23). Defaults to CXXSTD env var or 20."
0168       echo "  -F <flavor>      Accelerator flavor (e.g. cuda13, rocm7). Defaults to FLAVOR env var or the host stack."
0169       echo "  -f               Full dependency installation. Includes Geant4 datasets and Python packages."
0170       echo "  -h               Show this help message"
0171       exit 0
0172       ;;
0173     \? )
0174       echo "Invalid option: -$OPTARG" 1>&2
0175       exit 1
0176       ;;
0177     : )
0178       echo "Option -$OPTARG requires an argument" 1>&2
0179       exit 1
0180       ;;
0181   esac
0182 done
0183 
0184 script_start=$(date +%s.%N)
0185 
0186 # Helper to print elapsed time since previous checkpoint
0187 checkpoint() {
0188     local label=$1
0189     local now
0190     now=$(date +%s.%N)
0191     local elapsed
0192     elapsed=$(echo "$now - ${last_time:-$script_start}" | bc)
0193     printf "[%s] %.3f s\n" "$label" "$elapsed"
0194     last_time=$now
0195 }
0196 
0197 # Set defaults if not specified
0198 if [ -z "${compiler:-}" ]; then
0199   compiler="${CXX:-default}"
0200 fi
0201 
0202 if [ -z "${tag:-}" ]; then
0203   tag="${DEPENDENCY_TAG:-}"
0204   if [ -z "${tag:-}" ]; then
0205     echo "No tag specified via -t or DEPENDENCY_TAG environment variable"
0206     exit 1
0207   fi
0208 fi
0209 
0210 if [ -z "${destination:-}" ]; then
0211   if [ -n "${GITHUB_ACTIONS:-}" ]; then
0212     destination="${GITHUB_WORKSPACE}/dependencies"
0213   elif [ -n "${GITLAB_CI:-}" ]; then
0214     destination="${CI_PROJECT_DIR}/dependencies"
0215   else
0216     echo "No destination specified via -d and not running in CI"
0217     exit 1
0218   fi
0219 fi
0220 
0221 if [ -z "${env_file:-}" ]; then
0222   echo "No environment file specified via -e"
0223   exit 1
0224 fi
0225 
0226 if [ -z "${cxx_std:-}" ]; then
0227   cxx_std="${CXXSTD:-20}"
0228 fi
0229 
0230 # `host` is the plain CPU stack, published with no flavor token: pass nothing.
0231 if [ -z "${flavor:-}" ]; then
0232   flavor="${FLAVOR:-}"
0233 fi
0234 if [ "${flavor}" == "host" ]; then
0235   flavor=""
0236 fi
0237 
0238 checkpoint "Create environment file $(realpath "$env_file")"
0239 echo "" > "$env_file"
0240 export env_file
0241 
0242 function set_env {
0243   key="$1"
0244   value="$2"
0245 
0246   echo "=> ${key}=${value}"
0247 
0248   echo "export ${key}=${value}" >> "$env_file"
0249 }
0250 
0251 
0252 
0253 checkpoint "Starting setup script"
0254 
0255 mkdir -p "${destination}"
0256 # Spack resolves a relative view root against the environment directory, not
0257 # the cwd, so a relative -d would materialize the view under
0258 # ${destination}/env/${destination}/view once the root below is written into
0259 # the manifest. Pin it here so everything derived from it is absolute.
0260 destination="$(cd "${destination}" && pwd)"
0261 
0262 echo "Install tag: $tag"
0263 echo "Install destination: $destination"
0264 
0265 if [ -n "${GITLAB_CI:-}" ]; then
0266     _spack_folder=${CI_PROJECT_DIR}/spack
0267 else
0268     _spack_folder=${PWD}/spack
0269 fi
0270 
0271 start_section "Authenticate github.com fetches"
0272 # Before anything reaches github.com: setup_spack.sh clones spack itself, and
0273 # the builtin package repo is fetched right after it.
0274 "${SCRIPT_DIR}/github_auth.sh"
0275 end_section
0276 
0277 start_section "Install spack if not already installed"
0278 if ! command -v spack &> /dev/null; then
0279   "${SCRIPT_DIR}/setup_spack.sh" "${_spack_folder}"
0280   source "${_spack_folder}/share/spack/setup-env.sh"
0281 fi
0282 checkpoint "Spack install complete"
0283 
0284 _spack_repo_version=${SPACK_REPO_VERSION:-develop}
0285 _spack_repo_directory="$(realpath "$(spack location --repo builtin)/../../../")"
0286 
0287 echo "Ensure builtin repo is synced to commit ${_spack_repo_version}"
0288 
0289 git config --global --add safe.directory "${_spack_repo_directory}"
0290 retry_transient spack repo update builtin --commit "${_spack_repo_version}"
0291 checkpoint "Spack repository updated"
0292 
0293 end_section
0294 
0295 if [ -n "${GITLAB_CI:-}" ]; then
0296   # Use the project spack config for GitLab CI so we can cache it
0297   mkdir -p ${CI_PROJECT_DIR}/.spack
0298   ln -s ${CI_PROJECT_DIR}/.spack ${HOME}/.spack
0299 fi
0300 
0301 
0302 
0303 if [ -n "${CI:-}" ]; then
0304   start_section "Add buildcache mirror"
0305   mirror_name="acts-spack-buildcache"
0306   mirror_url="oci://ghcr.io/acts-project/spack-buildcache"
0307   if [ -n "${GITLAB_CI:-}" ]; then
0308   # Use CERN mirror for non-Github Actions
0309     mirror_url="oci://registry.cern.ch/ghcr.io/acts-project/spack-buildcache"
0310   fi
0311 
0312   # Check if this buildcache is already configured
0313   if ! spack mirror list | grep -q ${mirror_name}; then
0314     echo "Adding buildcache ${mirror_name}"
0315     spack mirror add ${mirror_name} ${mirror_url} --unsigned
0316   fi
0317   # Authenticate GHCR reads to avoid anonymous rate limits (which spack
0318   # misclassifies as "no binary available"). Idempotent on cached spack installs.
0319   # GITHUB_TOKEN must be in env when `spack install` later fetches from the mirror.
0320   if [ -n "${GITHUB_TOKEN:-}" ] && [[ "${mirror_url}" == oci://ghcr.io/* ]]; then
0321     echo "Setting GHCR credentials on ${mirror_name}"
0322     spack mirror set \
0323       --oci-username "${GITHUB_ACTOR:-x-access-token}" \
0324       --oci-password-variable GITHUB_TOKEN \
0325       "${mirror_name}"
0326   fi
0327   # Verify the mirror config (password-variable stores only the env var name,
0328   # not the secret value, so this is safe to print).
0329   spack mirror list
0330   spack config get mirrors
0331   end_section
0332 
0333   start_section "Add ACTS package repository"
0334   if ! spack repo list | grep -q "acts"; then
0335     echo "Adding ACTS package repository from ci-dependencies"
0336     retry_transient spack repo add https://github.com/acts-project/ci-dependencies.git --path spack_repo/acts
0337   fi
0338   echo "Updating ACTS package repository to tag ${tag}"
0339   retry_transient spack repo update acts --tag "${tag}"
0340   end_section
0341 
0342   start_section "Locate OpenGL"
0343   "${SCRIPT_DIR}/opengl.sh"
0344   checkpoint "OpenGL location complete"
0345   end_section
0346 fi
0347 
0348 start_section "Get spack lock file"
0349 arch=$(spack arch --family)
0350 
0351 env_dir="${destination}/env"
0352 view_dir="${destination}/view"
0353 venv_dir="${destination}/venv"
0354 mkdir -p ${env_dir}
0355 
0356 lock_file_path="${destination}/spack.lock"
0357 cmd=(
0358     "${SCRIPT_DIR}/select_lockfile.py"
0359     "--tag" "${tag}"
0360     "--arch" "${arch}"
0361     "--cxx" "${cxx_std}"
0362     "--output" "${lock_file_path}"
0363 )
0364 
0365 if [ "${compiler}" != "default" ]; then
0366     cmd+=("--compiler-binary" "${compiler}")
0367 fi
0368 
0369 if [ -n "${flavor}" ]; then
0370     cmd+=("--flavor" "${flavor}")
0371 fi
0372 
0373 "${cmd[@]}"
0374 
0375 checkpoint "Lock file prepared"
0376 
0377 end_section
0378 
0379 
0380 
0381 start_section "Create spack environment"
0382 spack env create -d "${env_dir}" "${lock_file_path}" --with-view "$view_dir"
0383 # ci-dependencies' own spack.yaml excludes libiconv from its view (see its
0384 # commit a025501f) because the view's GNU libiconv exports libiconv*, not
0385 # iconv*, and on macOS DYLD_LIBRARY_PATH outranks a binary's absolute install
0386 # name -- so it hijacks cmake/ctest/cpack/ccmake, which are built against
0387 # /usr/lib/libiconv, away from it. That exclude lives in the manifest, not
0388 # the lockfile, so creating the env straight from spack.lock above loses it
0389 # regardless of DEPENDENCY_TAG. Re-apply it to the locally generated
0390 # manifest before the view gets populated below, rather than unsetting
0391 # DYLD_LIBRARY_PATH for every command: dd4hep's own plugin lookup reads that
0392 # variable directly (not through dlopen's OS-level resolution), so it still
0393 # needs the view on it.
0394 #
0395 # The colon-path form (`config add view:default:exclude:[libiconv]`) can't
0396 # do this: view's schema default is a bare bool, and config add errors
0397 # trying to assign into that regardless of view's current form. `-f <file>`
0398 # merges a real YAML document instead and does the right thing (verified:
0399 # installing a spec with this in place drops it from the view while leaving
0400 # it installed). `-f -` silently no-ops rather than reading stdin, so this
0401 # needs a real file. `root` has to be repeated here: the view descriptor
0402 # schema marks it required, so a document carrying only `exclude` is
0403 # rejected outright.
0404 view_exclude_config="$(mktemp)"
0405 cat > "$view_exclude_config" <<YAML
0406 view:
0407   default:
0408     root: ${view_dir}
0409     exclude:
0410     - libiconv
0411 YAML
0412 spack -e "${env_dir}" config add -f "$view_exclude_config"
0413 rm -f "$view_exclude_config"
0414 checkpoint "Spack environment created"
0415 spack -e "${env_dir}" spec -l
0416 checkpoint "Spack spec complete"
0417 spack -e "${env_dir}" find
0418 checkpoint "Spack find complete"
0419 end_section
0420 
0421 start_section "Install spack packages"
0422 # Retry to absorb transient GHCR fetch failures (rate limits, network hiccups)
0423 # when pulling binaries from the buildcache. These usually surface as spack
0424 # reporting "no binary available" rather than as a network error, so that
0425 # signature is retried too (see TRANSIENT_ERROR_PATTERNS). Install is
0426 # idempotent: already-installed specs are skipped on subsequent attempts, so
0427 # each retry only re-attempts what is still missing and is therefore cheap.
0428 retry_transient spack -e "${env_dir}" install --fail-fast --use-buildcache only --concurrent-packages 10
0429 checkpoint "Spack install complete"
0430 end_section
0431 
0432 start_section "Patch up Geant4 data directory"
0433 if [ "${full_install:-false}" == "true" ]; then
0434   if ! which uv &> /dev/null ; then
0435     echo "uv not found, installing uv"
0436     curl -LsSf https://astral.sh/uv/install.sh | sh
0437     UV_EXE="/root/.local/bin/uv"
0438     checkpoint "uv installation complete"
0439   else
0440     UV_EXE=$(which uv)
0441   fi
0442   $UV_EXE run "$SCRIPT_DIR/download_geant4_datasets.py" -j8 --config "${view_dir}/bin/geant4-config"
0443   checkpoint "Geant4 datasets download complete"
0444 fi
0445 geant4_dir=$(spack -e "${env_dir}" location -i geant4)
0446 # Prepare the folder for G4 data, and symlink it to where G4 will look for it.
0447 # `data` itself, not just its parent: geant4.sh does `cd .../share/Geant4/data`
0448 # and only the full_install path above creates it. Without it the ln below has
0449 # no existing directory to resolve onto and writes a symlink at the target's own
0450 # path -- a self-reference that makes every later cd fail with ELOOP.
0451 mkdir -p "${geant4_dir}/share/Geant4/data"
0452 [ -e "${view_dir}/share/Geant4/data" ] ||
0453   ln -s "${geant4_dir}/share/Geant4/data" "${view_dir}/share/Geant4/data"
0454 end_section
0455 
0456 start_section "Prepare python environment"
0457 "${view_dir}/bin/python3" -m venv --system-site-packages "$venv_dir"
0458 # NOTE: pip, not uv, on purpose. The venv is deliberately --system-site-packages
0459 # so that the packages the spack view already provides (numpy and everything
0460 # built against it) are reused rather than replaced. pip honours that and skips
0461 # them; uv ignores system site-packages entirely and installs its own PyPI wheel
0462 # over the top, which silently swaps out the spack-built stack.
0463 retry_transient "${venv_dir}/bin/python3" -m pip install pyyaml jinja2
0464 if [ "${full_install:-false}" == "true" ]; then
0465   retry_transient "${venv_dir}/bin/python3" -m pip install -r "${SCRIPT_DIR}/../../Python/Examples/tests/requirements.txt"
0466   retry_transient "${venv_dir}/bin/python3" -m pip install histcmp==0.10.0 matplotlib
0467   retry_transient "${venv_dir}/bin/python3" -m pip install pytest-md-report
0468 fi
0469 checkpoint "Python environment prepared"
0470 end_section
0471 
0472 start_section "Set environment variables"
0473 set_env PATH "${venv_dir}/bin:${view_dir}/bin/:${PATH}"
0474 # lib64 carries CUDA's own libraries (e.g. cusparse): the view merges
0475 # packages' lib64/ trees there rather than into lib/, and prebuilt binaries
0476 # that dlopen them at runtime (rather than being linked with a baked RPATH)
0477 # need it on the search path too.
0478 set_env LD_LIBRARY_PATH "${venv_dir}/lib:${view_dir}/lib:${view_dir}/lib64:${view_dir}/lib/root"
0479 set_env DYLD_LIBRARY_PATH "${venv_dir}/lib:${view_dir}/lib:${view_dir}/lib64:${view_dir}/lib/root"
0480 # CCCL's CMake configs (Thrust, CUB, libcudacxx) sit under
0481 # targets/<arch>-linux/lib/cmake, which CMake does not search below a prefix,
0482 # so find_package(Thrust) misses them. The glob is a no-op without a flavor.
0483 cmake_prefix_path="${venv_dir}:${view_dir}"
0484 for cuda_cmake_dir in "${view_dir}"/targets/*/lib/cmake; do
0485   if [ -d "${cuda_cmake_dir}" ]; then
0486     cmake_prefix_path="${cmake_prefix_path}:${cuda_cmake_dir}"
0487   fi
0488 done
0489 set_env CMAKE_PREFIX_PATH "${cmake_prefix_path}"
0490 set_env ROOT_SETUP_SCRIPT "${view_dir}/bin/thisroot.sh"
0491 set_env ROOT_INCLUDE_PATH "${view_dir}/include"
0492 # cleanup setup-python mess
0493 set_env PKG_CONFIG_PATH ""
0494 set_env pythonLocation ""
0495 set_env Python_ROOT_DIR ""
0496 set_env Python2_ROOT_DIR ""
0497 set_env Python3_ROOT_DIR ""
0498 end_section
0499 
0500 checkpoint "Setup script complete"