Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-11 09:31:28

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