diff --git a/.github/workflows/docker_build_tpls.yml b/.github/workflows/docker_build_tpls.yml index 76b930fb..c5abfea8 100644 --- a/.github/workflows/docker_build_tpls.yml +++ b/.github/workflows/docker_build_tpls.yml @@ -175,7 +175,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true lfs: true diff --git a/.uberenv_config.json b/.uberenv_config.json index 27503c20..797fdaa4 100644 --- a/.uberenv_config.json +++ b/.uberenv_config.json @@ -10,5 +10,6 @@ "spack_commit_note": "v1.2.2 (Jul 20th 2026)", "spack_packages_commit": "9bb5e217dd9f9233991e0b4ceb407849588a0fa4", "spack_packages_note": "Aug 20th 2026", -"spack_setup_clingo": 0 +"spack_setup_clingo": 0, +"spack_allow_compiler_mixing": true } diff --git a/scripts/setupLC-TPL-uberenv.bash b/scripts/setupLC-TPL-uberenv.bash index 06fbdd4d..663a70bb 100755 --- a/scripts/setupLC-TPL-uberenv.bash +++ b/scripts/setupLC-TPL-uberenv.bash @@ -88,15 +88,15 @@ function launch_jobs() { # Note: The max time allowed on the debug queue is 1h. If we need more, switch to pbatch case "$machine" in dane) - ALLOC_CMD="srun -N 1 --exclusive -p pdebug -t 60 -A chemmech" -# "${UBERENV_HELPER}" "$INSTALL_DIR" dane gcc-12 "+docs %%gcc-12 ${COMMON}" "${ALLOC_CMD}" "$@" & -# "${UBERENV_HELPER}" "$INSTALL_DIR" dane gcc-13 "+docs %%gcc-13 ${COMMON}" "${ALLOC_CMD}" "$@" & + ALLOC_CMD="srun -N 1 --exclusive -t 60 -A vortex" + "${UBERENV_HELPER}" "$INSTALL_DIR" dane gcc-12 "+docs %%gcc-12 ${COMMON}" "${ALLOC_CMD}" "$@" & + "${UBERENV_HELPER}" "$INSTALL_DIR" dane gcc-13 "+docs %%gcc-13 ${COMMON}" "${ALLOC_CMD}" "$@" & "${UBERENV_HELPER}" "$INSTALL_DIR" dane llvm-14 "+docs %%clang-14 ${COMMON}" "${ALLOC_CMD}" "$@" & -# "${UBERENV_HELPER}" "$INSTALL_DIR" dane llvm-19 "+docs %%clang-19 ${COMMON}" "${ALLOC_CMD}" "$@" & + "${UBERENV_HELPER}" "$INSTALL_DIR" dane llvm-19 "+docs %%clang-19 ${COMMON}" "${ALLOC_CMD}" "$@" & ;; matrix) - ALLOC_CMD="srun -N 1 --exclusive -p pdebug -t 60 -A guests" + ALLOC_CMD="srun -N 1 -G 1 -p pdebug -t 60 -A vortex" "${UBERENV_HELPER}" "$INSTALL_DIR" matrix gcc-12-cuda-12.6 "+cuda ~uncrustify cuda_arch=90 %%gcc-12 ^cuda@12.6.0+allow-unsupported-compilers ${COMMON}" "${ALLOC_CMD}" "$@" & "${UBERENV_HELPER}" "$INSTALL_DIR" matrix gcc-13-cuda-12.9 "+cuda ~uncrustify cuda_arch=90 %%gcc-13 ^cuda@12.9.1+allow-unsupported-compilers ${COMMON}" "${ALLOC_CMD}" "$@" & "${UBERENV_HELPER}" "$INSTALL_DIR" matrix llvm-14-cuda-12.6 "+cuda ~uncrustify cuda_arch=90 %%clang-14 ^cuda@12.6.0+allow-unsupported-compilers ${COMMON}" "${ALLOC_CMD}" "$@" & @@ -104,7 +104,7 @@ function launch_jobs() { ;; tuo|tuolumne) - ALLOC_CMD="salloc -N 1 --exclusive -p pdebug -t 60 -A chemmech" + ALLOC_CMD="salloc -N 1 --exclusive -p pdebug -t 60 -A vortex" "${UBERENV_HELPER}" "$INSTALL_DIR" tuolumne cce-20-rocm-6.4.3 "+rocm~pygeosx~trilinos~petsc~docs amdgpu_target=gfx942 %%cce-20 ${COMMON}" "${ALLOC_CMD}" "$@" & "${UBERENV_HELPER}" "$INSTALL_DIR" tuolumne llvm-amdgpu-6.4.3 "+rocm~pygeosx~trilinos~petsc~docs amdgpu_target=gfx942 %%llvm-amdgpu_6_4_3 ${COMMON}" "${ALLOC_CMD}" "$@" & ;; diff --git a/scripts/setupMacOS-TPL-deps.bash b/scripts/setupMacOS-TPL-deps.bash new file mode 100755 index 00000000..cf2e3fd6 --- /dev/null +++ b/scripts/setupMacOS-TPL-deps.bash @@ -0,0 +1,798 @@ +#!/bin/bash + +# Validate and, when necessary, install the Homebrew dependencies used by the +# macOS TPL configuration. Homebrew must already exist. This script +# intentionally never updates or upgrades Homebrew. + +set -euo pipefail + +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P) +DEFAULT_MANIFEST="${SCRIPT_DIR}/spack_configs/macOS/homebrew-manifest.json" + +MANIFEST=${DEFAULT_MANIFEST} +CHECK_ONLY=false +SPACK_CONFIG_OUT= +SPACK_CONFIG_TEMPLATE=${GEOS_TPL_SPACK_CONFIG_TEMPLATE:-${SCRIPT_DIR}/spack_configs/macOS/spack.yaml} + +# These overrides allow the shell tests to inject deterministic stand-ins. In a +# normal shell, Homebrew is found through PATH or its standard install paths. +BREW_BIN=${GEOS_TPL_BREW_BIN:-} +PLUTIL_BIN=${GEOS_TPL_PLUTIL_BIN:-/usr/bin/plutil} +UNAME_BIN=${GEOS_TPL_UNAME_BIN:-/usr/bin/uname} +SW_VERS_BIN=${GEOS_TPL_SW_VERS_BIN:-/usr/bin/sw_vers} +XCRUN_BIN=${GEOS_TPL_XCRUN_BIN:-/usr/bin/xcrun} +CLANG_BIN=${GEOS_TPL_CLANG_BIN:-/usr/bin/clang} + +WORK_DIR= +ERROR_COUNT=0 +QUALIFICATION_BREW_PREFIX= +QUALIFICATION_CMAKE_VERSION= +QUALIFICATION_OPENMPI_VERSION= +QUALIFICATION_MPICH_VERSION=5.0.1 +QUALIFICATION_PERL_VERSION= +HOST_BREW_PREFIX= +HOST_MACOS_VERSION= +HOST_SDK_VERSION= +HOST_CLANG_VERSION= +HOST_CLANG_BUILD= +HOST_CMAKE_VERSION= +HOST_PERL_VERSION= +MPI_PROVIDER= +MPI_VERSION= + +declare -a FORMULA_NAMES=() +declare -a FORMULA_VERSIONS=() +declare -a FORMULA_VERSION_POLICIES=() +declare -a FORMULA_MIN_VERSIONS=() +declare -a FORMULA_PREFIXES=() +declare -a FORMULA_SHA256S=() +declare -a MISSING_FORMULAS=() + +usage() +{ + cat <<'EOF' +Usage: scripts/setupMacOS-TPL-deps.bash [options] + +Validate the Homebrew dependency set recorded in the checked-in manifest. Exact +pins and minimum-version policies are applied as declared. The manifest's host +and platform versions describe the qualification host; they are not exact +host-version gates. By default, missing formulas are installed only after the +complete preflight succeeds. Exact-pinned formula drift is never upgraded or +downgraded. + +Options: + --check-only Validate without installing anything. + --manifest PATH Use an alternate manifest (primarily for testing). + --spack-config-out PATH + Write a host-specific Spack environment file after + validation. The output contains the Homebrew prefix + and MPI provider; Uberenv discovers Apple Clang. + -h, --help Show this help text. +EOF +} + +die() +{ + echo "ERROR: $*" >&2 + exit 1 +} + +record_error() +{ + echo "ERROR: $*" >&2 + ERROR_COUNT=$((ERROR_COUNT + 1)) +} + +cleanup() +{ + if [[ -n "${WORK_DIR}" && -d "${WORK_DIR}" ]]; then + case "${WORK_DIR}" in + "${TMPDIR:-/tmp}"/geos-macos-tpl-deps.*) + rm -rf -- "${WORK_DIR}" + ;; + esac + fi +} + +is_nonnegative_integer() +{ + case "$1" in + ''|*[!0-9]*) return 1 ;; + *) return 0 ;; + esac +} + +is_sha256() +{ + [[ ${#1} -eq 64 ]] || return 1 + case "$1" in + *[!0-9a-f]*) return 1 ;; + *) return 0 ;; + esac +} + +version_at_least() +{ + awk -v actual="$1" -v minimum="$2" ' + BEGIN { + actual_count = split(actual, actual_parts, "[.]") + minimum_count = split(minimum, minimum_parts, "[.]") + count = actual_count > minimum_count ? actual_count : minimum_count + for (i = 1; i <= count; ++i) { + actual_part = (i <= actual_count) ? actual_parts[i] + 0 : 0 + minimum_part = (i <= minimum_count) ? minimum_parts[i] + 0 : 0 + if (actual_part > minimum_part) exit 0 + if (actual_part < minimum_part) exit 1 + } + exit 0 + } + ' +} + +manifest_get() +{ + local key=$1 + local value + if ! value=$("${PLUTIL_BIN}" -extract "${key}" raw -o - -- "${MANIFEST}" 2>/dev/null); then + die "Manifest is missing '${key}' or it has an unsupported value type: ${MANIFEST}" + fi + printf '%s\n' "${value}" +} + +require_equal() +{ + local label=$1 + local expected=$2 + local actual=$3 + if [[ "${actual}" != "${expected}" ]]; then + record_error "${label} mismatch: expected '${expected}', found '${actual}'" + fi +} + +find_brew() +{ + local candidate + + if [[ -n "${BREW_BIN}" ]]; then + return 0 + fi + + if command -v brew >/dev/null 2>&1; then + BREW_BIN=$(command -v brew) + return 0 + fi + + for candidate in /opt/homebrew/bin/brew /usr/local/bin/brew; do + if [[ -x "${candidate}" ]]; then + BREW_BIN=${candidate} + return 0 + fi + done + + BREW_BIN=/opt/homebrew/bin/brew +} + +manifest_formula_prefix() +{ + local index=$1 + local prefix=${FORMULA_PREFIXES[${index}]} + local suffix + + # Formula prefixes in the manifest are recorded against the qualification + # Homebrew installation. Preserve their keg suffix when Homebrew is installed + # somewhere else, such as /usr/local on Intel macOS. + if [[ -n "${QUALIFICATION_BREW_PREFIX}" \ + && "${prefix}" == "${QUALIFICATION_BREW_PREFIX}"/* \ + && -n "${HOST_BREW_PREFIX}" ]]; then + suffix=${prefix#${QUALIFICATION_BREW_PREFIX}} + prefix="${HOST_BREW_PREFIX}${suffix}" + fi + printf '%s\n' "${prefix}" +} + +formula_requirement() +{ + local index=$1 + local name=${FORMULA_NAMES[${index}]} + local policy=${FORMULA_VERSION_POLICIES[${index}]} + local version=${FORMULA_VERSIONS[${index}]} + local minimum=${FORMULA_MIN_VERSIONS[${index}]} + + case "${policy}" in + exact) printf '%s@%s\n' "${name}" "${version}" ;; + minimum) printf '%s>=%s\n' "${name}" "${minimum}" ;; + any) printf '%s\n' "${name}" ;; + esac +} + +write_spack_config() +{ + local escaped_brew + local escaped_cmake escaped_qualification_cmake escaped_mpi + local escaped_openmpi escaped_mpich escaped_qualification_openmpi escaped_qualification_mpich + local escaped_perl escaped_qualification_perl + local config_mpi_version config_openmpi_version config_mpich_version + local generated_template generated_config mpi_config + + [[ -n "${SPACK_CONFIG_OUT}" ]] || return 0 + [[ -f "${SPACK_CONFIG_TEMPLATE}" ]] || die "Spack environment template does not exist: ${SPACK_CONFIG_TEMPLATE}" + [[ "${SPACK_CONFIG_OUT}" != "${SPACK_CONFIG_TEMPLATE}" ]] || die "--spack-config-out must not overwrite the Spack environment template" + + case "${SPACK_CONFIG_OUT}" in + */*) [[ -d "${SPACK_CONFIG_OUT%/*}" ]] || die "Spack config output directory does not exist: ${SPACK_CONFIG_OUT%/*}" ;; + esac + + escaped_brew=$(printf '%s\n' "${HOST_BREW_PREFIX}" | sed 's/[|&\\]/\\&/g') + escaped_cmake=$(printf '%s\n' "${HOST_CMAKE_VERSION}" | sed 's/[|&\\]/\\&/g') + escaped_qualification_cmake=$(printf '%s\n' "${QUALIFICATION_CMAKE_VERSION}" | sed 's/[|&\\]/\\&/g') + escaped_perl=$(printf '%s\n' "${HOST_PERL_VERSION}" | sed 's/[|&\\]/\\&/g') + escaped_qualification_perl=$(printf '%s\n' "${QUALIFICATION_PERL_VERSION}" | sed 's/[|&\\]/\\&/g') + config_mpi_version=${MPI_VERSION:-${QUALIFICATION_OPENMPI_VERSION}} + escaped_mpi=$(printf '%s\n' "${config_mpi_version}" | sed 's/[|&\\]/\\&/g') + escaped_qualification_openmpi=$(printf '%s\n' "${QUALIFICATION_OPENMPI_VERSION}" | sed 's/[|&\\]/\\&/g') + escaped_qualification_mpich=$(printf '%s\n' "${QUALIFICATION_MPICH_VERSION}" | sed 's/[|&\\]/\\&/g') + config_openmpi_version=${QUALIFICATION_OPENMPI_VERSION} + config_mpich_version=${QUALIFICATION_MPICH_VERSION} + if [[ "${MPI_PROVIDER}" == "open-mpi" && -n "${MPI_VERSION}" ]]; then + config_openmpi_version=${MPI_VERSION} + fi + if [[ "${MPI_PROVIDER}" == "mpich" && -n "${MPI_VERSION}" ]]; then + config_mpich_version=${MPI_VERSION} + fi + escaped_openmpi=$(printf '%s\n' "${config_openmpi_version}" | sed 's/[|&\\]/\\&/g') + escaped_mpich=$(printf '%s\n' "${config_mpich_version}" | sed 's/[|&\\]/\\&/g') + generated_template="${WORK_DIR}/spack-macos-template.yaml" + generated_config="${WORK_DIR}/spack-macos.yaml" + sed \ + -e "s|/opt/homebrew|${escaped_brew}|g" \ + -e "s|cmake@=${escaped_qualification_cmake}|cmake@=${escaped_cmake}|g" \ + -e "s|perl@=${escaped_qualification_perl}|perl@=${escaped_perl}|g" \ + -e "s|openmpi@=${escaped_qualification_openmpi}|openmpi@=${escaped_openmpi}|g" \ + -e "s|mpich@=${escaped_qualification_mpich}|mpich@=${escaped_mpich}|g" \ + -e 's/ os=tahoe//g' \ + "${SPACK_CONFIG_TEMPLATE}" >"${generated_template}" + if ! awk \ + -v clang_version="${HOST_CLANG_VERSION}" \ + -v brew_prefix="${HOST_BREW_PREFIX}" ' + /^[[:space:]]*# GEOS_TPL_APPLE_CLANG_EXTERNAL$/ { + found = 1 + print " externals:" + print " - spec: \"apple-clang@=" clang_version " platform=darwin target=aarch64\"" + print " prefix: /usr" + print " extra_attributes:" + print " compilers:" + print " c: /usr/bin/clang" + print " cxx: /usr/bin/clang++" + print " environment:" + print " set:" + print " AR: /usr/bin/ar" + print " RANLIB: /usr/bin/ranlib" + print " CMAKE_AR: /usr/bin/ar" + print " CMAKE_RANLIB: /usr/bin/ranlib" + print " prepend_path:" + print " PATH: /usr/bin" + print " remove_path:" + print " PATH: " brew_prefix "/opt/binutils/bin" + next + } + { print } + END { + if (!found) exit 2 + } + ' "${generated_template}" >"${generated_config}"; then + die "Spack environment template is missing the Apple Clang insertion marker" + fi + if [[ "${MPI_PROVIDER}" == "mpich" ]]; then + mpi_config="${WORK_DIR}/spack-macos-mpi.yaml" + sed "s|require: \"openmpi@=${escaped_qualification_openmpi}\"|require: \"mpich@=${escaped_mpi}\"|g" \ + "${generated_config}" >"${mpi_config}" + mv "${mpi_config}" "${generated_config}" + fi + mv "${generated_config}" "${SPACK_CONFIG_OUT}" + echo "Wrote host-specific Spack environment: ${SPACK_CONFIG_OUT}" +} + +parse_args() +{ + while [[ $# -gt 0 ]]; do + case "$1" in + --check-only) + CHECK_ONLY=true + shift + ;; + --manifest) + [[ $# -ge 2 ]] || die "--manifest requires a path" + MANIFEST=$2 + shift 2 + ;; + --spack-config-out) + [[ $# -ge 2 ]] || die "--spack-config-out requires a path" + SPACK_CONFIG_OUT=$2 + shift 2 + ;; + --spack-config-out=*) + SPACK_CONFIG_OUT=${1#*=} + [[ -n "${SPACK_CONFIG_OUT}" ]] || die "--spack-config-out requires a path" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "Unknown argument '$1'. Use --help for usage." + ;; + esac + done +} + +validate_manifest() +{ + local schema_version formula_count spack_built_count + local i j name version policy policy_value minimum_version prefix sha + local path_count relative_path seen + + [[ -f "${MANIFEST}" ]] || die "Manifest does not exist: ${MANIFEST}" + "${PLUTIL_BIN}" -convert json -o - -- "${MANIFEST}" >/dev/null || die "Manifest is not valid JSON: ${MANIFEST}" + + schema_version=$(manifest_get schema_version) + [[ "${schema_version}" == "1" ]] || die "Unsupported manifest schema '${schema_version}'" + + # Read all supported platform fields now so a malformed manifest fails before + # any Homebrew command is attempted. + manifest_get supported_platform.os >/dev/null + manifest_get supported_platform.arch >/dev/null + manifest_get supported_platform.macos_major >/dev/null + manifest_get supported_platform.apple_clang_version >/dev/null + manifest_get supported_platform.sdk_major >/dev/null + manifest_get supported_platform.homebrew_prefix >/dev/null + manifest_get qualification_host.macos_product_version >/dev/null + manifest_get qualification_host.macos_build_version >/dev/null + manifest_get qualification_host.apple_clang_build >/dev/null + manifest_get qualification_host.sdk_version >/dev/null + manifest_get qualification_host.homebrew_version >/dev/null + + formula_count=$(manifest_get formulae) + is_nonnegative_integer "${formula_count}" || die "Manifest 'formulae' must be an array" + [[ "${formula_count}" -gt 0 ]] || die "Manifest must declare at least one formula" + + i=0 + while [[ ${i} -lt ${formula_count} ]]; do + name=$(manifest_get "formulae.${i}.name") + policy=exact + if policy_value=$("${PLUTIL_BIN}" -extract "formulae.${i}.version_policy" raw -o - -- "${MANIFEST}" 2>/dev/null); then + policy=${policy_value} + fi + case "${policy}" in + exact|any) + minimum_version= + ;; + minimum) + minimum_version=$(manifest_get "formulae.${i}.minimum_version") + case "${minimum_version}" in + ''|*[!0-9.]*) die "Formula '${name}' has an invalid minimum_version '${minimum_version}'" ;; + esac + ;; + *) + die "Formula '${name}' has an unsupported version_policy '${policy}'" + ;; + esac + version=$(manifest_get "formulae.${i}.brew_version") + prefix=$(manifest_get "formulae.${i}.prefix") + sha=$(manifest_get "formulae.${i}.formula_sha256") + + case "${name}" in + ''|*[!A-Za-z0-9_@+./-]*) die "Formula ${i} has an unsafe name '${name}'" ;; + esac + [[ -n "${version}" ]] || die "Formula '${name}' has an empty brew_version" + [[ "${prefix}" == /* ]] || die "Formula '${name}' prefix must be absolute" + is_sha256 "${sha}" || die "Formula '${name}' has an invalid formula_sha256" + + j=0 + while [[ ${j} -lt ${#FORMULA_NAMES[@]} ]]; do + seen=${FORMULA_NAMES[${j}]} + [[ "${seen}" != "${name}" ]] || die "Formula '${name}' is declared more than once" + j=$((j + 1)) + done + + # These fields are consumed by the Spack configuration rather than this + # installer, but validating them prevents the manifest from drifting into a + # partial or ambiguous mapping. + manifest_get "formulae.${i}.spack_package" >/dev/null + manifest_get "formulae.${i}.spack_version" >/dev/null + + path_count=$(manifest_get "formulae.${i}.required_paths") + is_nonnegative_integer "${path_count}" || die "Formula '${name}' required_paths must be an array" + [[ "${path_count}" -gt 0 ]] || die "Formula '${name}' must declare at least one required path" + j=0 + while [[ ${j} -lt ${path_count} ]]; do + relative_path=$(manifest_get "formulae.${i}.required_paths.${j}") + case "${relative_path}" in + ''|/*|..|../*|*/../*|*/..) die "Formula '${name}' has unsafe required path '${relative_path}'" ;; + esac + j=$((j + 1)) + done + + FORMULA_NAMES[i]=${name} + FORMULA_VERSIONS[i]=${version} + FORMULA_VERSION_POLICIES[i]=${policy} + FORMULA_MIN_VERSIONS[i]=${minimum_version} + FORMULA_PREFIXES[i]=${prefix} + FORMULA_SHA256S[i]=${sha} + case "${name}" in + cmake) QUALIFICATION_CMAKE_VERSION=${version} ;; + open-mpi) QUALIFICATION_OPENMPI_VERSION=${version} ;; + perl) QUALIFICATION_PERL_VERSION=${version} ;; + esac + i=$((i + 1)) + done + + spack_built_count=$(manifest_get spack_built) + is_nonnegative_integer "${spack_built_count}" || die "Manifest 'spack_built' must be an array" + [[ "${spack_built_count}" -gt 0 ]] || die "Manifest must document the Spack-built dependency set" + i=0 + while [[ ${i} -lt ${spack_built_count} ]]; do + manifest_get "spack_built.${i}.package" >/dev/null + manifest_get "spack_built.${i}.version" >/dev/null + i=$((i + 1)) + done +} + +validate_platform() +{ + local clang_output clang_version clang_build brew_output brew_version + local macos_major sdk_major qualification_macos_major qualification_sdk_major + local qualification_brew_prefix + + find_brew + [[ -x "${BREW_BIN}" ]] || die "Homebrew is required at ${BREW_BIN}; this script does not install Homebrew" + [[ -x "${PLUTIL_BIN}" ]] || die "Required plist utility is missing: ${PLUTIL_BIN}" + [[ -x "${UNAME_BIN}" && -x "${SW_VERS_BIN}" ]] || die "Required macOS platform tools are missing" + [[ -x "${XCRUN_BIN}" && -x "${CLANG_BIN}" ]] || die "Apple Command Line Tools are required" + + require_equal "operating system" "$(manifest_get supported_platform.os)" "$("${UNAME_BIN}" -s)" + require_equal "architecture" "$(manifest_get supported_platform.arch)" "$("${UNAME_BIN}" -m)" + HOST_MACOS_VERSION=$("${SW_VERS_BIN}" -productVersion) + macos_major=${HOST_MACOS_VERSION%%.*} + qualification_macos_major=$(manifest_get supported_platform.macos_major) + if [[ "${macos_major}" != "${qualification_macos_major}" ]]; then + echo "INFO: macOS ${HOST_MACOS_VERSION} differs from qualification host major ${qualification_macos_major}; continuing" + fi + HOST_SDK_VERSION=$("${XCRUN_BIN}" --show-sdk-version) + sdk_major=${HOST_SDK_VERSION%%.*} + qualification_sdk_major=$(manifest_get supported_platform.sdk_major) + if [[ "${sdk_major}" != "${qualification_sdk_major}" ]]; then + echo "INFO: macOS SDK ${HOST_SDK_VERSION} differs from qualification host major ${qualification_sdk_major}; continuing" + fi + + clang_output=$("${CLANG_BIN}" --version) + clang_version=$(printf '%s\n' "${clang_output}" | sed -n 's/^Apple clang version \([^ ]*\).*/\1/p' | sed -n '1p') + clang_build=$(printf '%s\n' "${clang_output}" | sed -n 's/^Apple clang version [^ ]* (clang-\([^)]*\)).*/\1/p' | sed -n '1p') + [[ -n "${clang_version}" && -n "${clang_build}" ]] || record_error "Could not parse Apple Clang identity from '${CLANG_BIN} --version'" + HOST_CLANG_VERSION=${clang_version} + HOST_CLANG_BUILD=${clang_build} + + brew_output=$("${BREW_BIN}" --version) + brew_version=$(printf '%s\n' "${brew_output}" | sed -n 's/^Homebrew //p' | sed -n '1p') + [[ -n "${brew_version}" ]] || record_error "Could not parse Homebrew version from '${BREW_BIN} --version'" + + if ! HOST_BREW_PREFIX=$("${BREW_BIN}" --prefix); then + record_error "Homebrew could not report its prefix" + else + qualification_brew_prefix=$(manifest_get supported_platform.homebrew_prefix) + QUALIFICATION_BREW_PREFIX=${qualification_brew_prefix} + if [[ "${HOST_BREW_PREFIX}" != "${qualification_brew_prefix}" ]]; then + echo "INFO: Homebrew prefix is '${HOST_BREW_PREFIX}'; qualification host used '${qualification_brew_prefix}'" + fi + fi + + echo "Host details: macOS ${HOST_MACOS_VERSION} ($("${SW_VERS_BIN}" -buildVersion)), SDK ${HOST_SDK_VERSION}, Apple Clang ${clang_version} build ${clang_build}, Homebrew ${brew_version}" + echo "Compiler defaults: C/C++=Apple Clang; Fortran=Homebrew GCC" + + [[ ${ERROR_COUNT} -eq 0 ]] || die "Platform preflight failed with ${ERROR_COUNT} error(s); no formulas were installed" +} + +formula_metadata_matches() +{ + local index=$1 + local name expected_version expected_sha policy minimum_version info_file formula_count + local full_name stable revision actual_version actual_sha + + name=${FORMULA_NAMES[${index}]} + expected_version=${FORMULA_VERSIONS[${index}]} + expected_sha=${FORMULA_SHA256S[${index}]} + policy=${FORMULA_VERSION_POLICIES[${index}]} + minimum_version=${FORMULA_MIN_VERSIONS[${index}]} + info_file="${WORK_DIR}/formula-${index}.json" + + if ! "${BREW_BIN}" info --json=v2 "${name}" >"${info_file}"; then + record_error "Homebrew could not inspect formula '${name}'" + return 1 + fi + if ! "${PLUTIL_BIN}" -convert json -o - -- "${info_file}" >/dev/null; then + record_error "Homebrew returned invalid JSON for formula '${name}'" + return 1 + fi + if ! formula_count=$("${PLUTIL_BIN}" -extract formulae raw -o - -- "${info_file}" 2>/dev/null); then + record_error "Homebrew metadata for '${name}' has no formulae array" + return 1 + fi + if [[ "${formula_count}" != "1" ]]; then + record_error "Expected one Homebrew metadata record for '${name}', found '${formula_count}'" + return 1 + fi + + full_name=$("${PLUTIL_BIN}" -extract formulae.0.full_name raw -o - -- "${info_file}" 2>/dev/null || true) + stable=$("${PLUTIL_BIN}" -extract formulae.0.versions.stable raw -o - -- "${info_file}" 2>/dev/null || true) + revision=$("${PLUTIL_BIN}" -extract formulae.0.revision raw -o - -- "${info_file}" 2>/dev/null || true) + actual_sha=$("${PLUTIL_BIN}" -extract formulae.0.ruby_source_checksum.sha256 raw -o - -- "${info_file}" 2>/dev/null || true) + + if [[ "${full_name}" != "${name}" ]]; then + record_error "Formula identity mismatch for '${name}': Homebrew reported '${full_name}'" + return 1 + fi + if [[ -z "${stable}" ]] || ! is_nonnegative_integer "${revision}"; then + record_error "Formula '${name}' has invalid stable-version metadata" + return 1 + fi + actual_version=${stable} + if [[ "${revision}" -gt 0 ]]; then + actual_version="${stable}_${revision}" + fi + + case "${policy}" in + exact) + if [[ "${actual_version}" != "${expected_version}" ]]; then + record_error "Formula '${name}' metadata drift: expected version '${expected_version}', found '${actual_version}'" + return 1 + fi + if [[ "${actual_sha}" != "${expected_sha}" ]]; then + record_error "Formula '${name}' source drift: expected checksum '${expected_sha}', found '${actual_sha}'" + return 1 + fi + ;; + minimum) + if ! version_at_least "${actual_version}" "${minimum_version}"; then + record_error "Formula '${name}' is too old: requires at least '${minimum_version}', found '${actual_version}'" + return 1 + fi + ;; + any) + ;; + esac + return 0 +} + +installed_formula_version() +{ + local name=$1 + local line + local -a fields + line=$("${BREW_BIN}" list --versions "${name}" 2>/dev/null || true) + if [[ -z "${line}" ]]; then + return 1 + fi + # Homebrew prints: . Multiple installed + # versions are rejected rather than choosing one implicitly. + IFS=' ' read -r -a fields <<< "${line}" + if [[ ${#fields[@]} -ne 2 ]]; then + printf '__AMBIGUOUS__\n' + return 0 + fi + printf '%s\n' "${fields[1]}" +} + +select_mpi_provider() +{ + local detected_version + + if detected_version=$(installed_formula_version mpich); then + MPI_PROVIDER=mpich + MPI_VERSION=${detected_version} + return 0 + fi + if detected_version=$(installed_formula_version open-mpi); then + MPI_PROVIDER=open-mpi + MPI_VERSION=${detected_version} + return 0 + fi + + # OpenMPI is the fallback only when neither supported MPI is installed. + MPI_PROVIDER=open-mpi + MPI_VERSION= +} + +validate_mpi_provider() +{ + local actual_prefix relative_path shared_library + + [[ "${MPI_PROVIDER}" == "mpich" ]] || return 0 + if [[ "${MPI_VERSION}" == "__AMBIGUOUS__" ]]; then + record_error "MPI formula 'mpich' has multiple installed versions" + return 1 + fi + if ! actual_prefix=$("${BREW_BIN}" --prefix mpich); then + record_error "Homebrew could not report the installed prefix for 'mpich'" + return 1 + fi + for relative_path in bin/mpicc bin/mpicxx bin/mpifort; do + if [[ ! -e "${actual_prefix}/${relative_path}" ]]; then + record_error "MPI formula 'mpich' is missing required path '${actual_prefix}/${relative_path}'" + fi + done + shared_library= + for shared_library in "${actual_prefix}"/lib/libmpi*.dylib; do + if [[ -e "${shared_library}" ]]; then + break + fi + shared_library= + done + if [[ -z "${shared_library}" ]]; then + record_error "MPI formula 'mpich' is missing a shared MPI library under '${actual_prefix}/lib'" + fi + [[ ${ERROR_COUNT} -eq 0 ]] || die "MPI preflight failed; no formulas were installed" +} + +should_skip_formula() +{ + local index=$1 + [[ "${FORMULA_NAMES[${index}]}" == "open-mpi" && "${MPI_PROVIDER}" == "mpich" ]] +} + +validate_formula_installation() +{ + local index=$1 + local allow_missing=$2 + local name expected_version expected_prefix actual_version actual_prefix + local policy minimum_version + local path_count j relative_path role_suffix + + name=${FORMULA_NAMES[${index}]} + expected_version=${FORMULA_VERSIONS[${index}]} + policy=${FORMULA_VERSION_POLICIES[${index}]} + minimum_version=${FORMULA_MIN_VERSIONS[${index}]} + expected_prefix=$(manifest_formula_prefix "${index}") + + if ! actual_version=$(installed_formula_version "${name}"); then + if [[ "${allow_missing}" == "true" ]]; then + MISSING_FORMULAS[${#MISSING_FORMULAS[@]}]=${name} + echo "MISSING: $(formula_requirement "${index}")" + return 0 + fi + record_error "Formula '${name}' is still missing after installation (requires $(formula_requirement "${index}"))" + return 1 + fi + if [[ "${actual_version}" == "__AMBIGUOUS__" ]]; then + record_error "Formula '${name}' has multiple installed versions" + return 1 + fi + case "${policy}" in + exact) + if [[ "${actual_version}" != "${expected_version}" ]]; then + record_error "Formula '${name}' receipt drift: expected '${expected_version}', found '${actual_version}'" + return 1 + fi + ;; + minimum) + if ! version_at_least "${actual_version}" "${minimum_version}"; then + record_error "Formula '${name}' receipt is too old: requires at least '${minimum_version}', found '${actual_version}'" + return 1 + fi + ;; + any) + ;; + esac + + if ! actual_prefix=$("${BREW_BIN}" --prefix "${name}"); then + record_error "Homebrew could not report the installed prefix for '${name}'" + return 1 + fi + if [[ "${actual_prefix}" != "${expected_prefix}" ]]; then + record_error "Formula '${name}' prefix mismatch: expected '${expected_prefix}', found '${actual_prefix}'" + return 1 + fi + + case "${name}" in + cmake) HOST_CMAKE_VERSION=${actual_version} ;; + perl) HOST_PERL_VERSION=${actual_version} ;; + open-mpi) + MPI_VERSION=${actual_version} + ;; + esac + role_suffix= + if [[ "${name}" == "gcc" ]]; then + role_suffix=" [Fortran compiler only]" + fi + + path_count=$(manifest_get "formulae.${index}.required_paths") + j=0 + while [[ ${j} -lt ${path_count} ]]; do + relative_path=$(manifest_get "formulae.${index}.required_paths.${j}") + if [[ ! -e "${expected_prefix}/${relative_path}" ]]; then + record_error "Formula '${name}' is missing required path '${expected_prefix}/${relative_path}'" + fi + j=$((j + 1)) + done + echo "OK: ${name}@${actual_version} (${actual_prefix})${role_suffix}" + return 0 +} + +preflight_formulae() +{ + local i + MISSING_FORMULAS=() + i=0 + while [[ ${i} -lt ${#FORMULA_NAMES[@]} ]]; do + if should_skip_formula "${i}"; then + i=$((i + 1)) + continue + fi + # Check source identity for exact-pinned formulas even when already + # installed. This makes stale API caches and silently rewritten formulas + # visible drift. + formula_metadata_matches "${i}" || true + validate_formula_installation "${i}" true || true + i=$((i + 1)) + done + [[ ${ERROR_COUNT} -eq 0 ]] || die "Formula preflight failed with ${ERROR_COUNT} error(s); no formulas were installed" +} + +revalidate_formulae() +{ + local i starting_errors + starting_errors=${ERROR_COUNT} + i=0 + while [[ ${i} -lt ${#FORMULA_NAMES[@]} ]]; do + if should_skip_formula "${i}"; then + i=$((i + 1)) + continue + fi + formula_metadata_matches "${i}" || true + validate_formula_installation "${i}" false || true + i=$((i + 1)) + done + if [[ ${ERROR_COUNT} -ne ${starting_errors} ]]; then + die "Post-install validation failed with $((ERROR_COUNT - starting_errors)) error(s)" + fi +} + +main() +{ + parse_args "$@" + validate_manifest + + WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/geos-macos-tpl-deps.XXXXXX") + trap cleanup EXIT HUP INT TERM + + export HOMEBREW_NO_ANALYTICS=1 + export HOMEBREW_NO_AUTO_UPDATE=1 + export HOMEBREW_NO_INSTALL_CLEANUP=1 + export HOMEBREW_NO_INSTALL_UPGRADE=1 + export HOMEBREW_NO_ENV_HINTS=1 + + validate_platform + select_mpi_provider + validate_mpi_provider + if [[ -n "${MPI_VERSION}" ]]; then + echo "MPI provider: ${MPI_PROVIDER}@${MPI_VERSION}" + else + echo "MPI provider: open-mpi (fallback)" + fi + preflight_formulae + + if [[ ${#MISSING_FORMULAS[@]} -gt 0 ]]; then + if [[ "${CHECK_ONLY}" == "true" ]]; then + die "${#MISSING_FORMULAS[@]} required formula(s) are missing; check-only mode made no changes" + fi + echo "Installing preflighted formulas: ${MISSING_FORMULAS[*]}" + if ! "${BREW_BIN}" install "${MISSING_FORMULAS[@]}"; then + die "Homebrew failed while installing the preflighted formula set" + fi + fi + + revalidate_formulae + write_spack_config + echo "macOS Homebrew TPL dependency validation completed successfully." +} + +main "$@" diff --git a/scripts/spack_configs/macOS/README.md b/scripts/spack_configs/macOS/README.md new file mode 100644 index 00000000..14af9b0b --- /dev/null +++ b/scripts/spack_configs/macOS/README.md @@ -0,0 +1,152 @@ +# macOS Homebrew prerequisites + +The macOS TPL build uses a deliberately narrow Homebrew boundary. Homebrew +provides the C/Fortran toolchain, MPI, BLAS, CMake, Perl, Python, and selected +build tools listed in [`homebrew-manifest.json`](homebrew-manifest.json). +Diffutils and zlib are intentionally absent from the Homebrew list and are +built by Spack. + +The setup is fail-closed for the dependency set. It does not install Homebrew, +update Homebrew, upgrade or downgrade an installed formula, or silently accept +a changed exact-pinned formula definition. The macOS, SDK, Apple Clang, and +Homebrew versions recorded in the manifest identify the qualification host; +they are reported for traceability but are not exact host-version gates. + +## One-time prerequisite + +Install Homebrew for the machine's architecture using the official installer. +In every shell used for setup and building, make sure Homebrew is on `PATH`: + +```console +eval "$(brew shellenv)" +``` + +The dependency script discovers Homebrew through `PATH` (and also checks the +standard Apple Silicon and Intel install paths). It validates exact versions +and source checksums for the exact-pinned formulas in the manifest. No GEOS tap +or other Homebrew tap is required. CMake only has to satisfy the project +minimum of `3.24`; an installed `mpich` or `open-mpi` satisfies the MPI +requirement. +When neither MPI implementation is installed, normal mode installs `open-mpi` +as the fallback. + +## Audit or install + +From the `thirdPartyLibs` repository root, audit without changing anything: + +```console +scripts/setupMacOS-TPL-deps.bash --check-only +``` + +To install formulas that are missing and write a host-specific Spack +environment file: + +```console +SPACK_CONFIG="${TMPDIR:-/tmp}/geosx-macos-spack.yaml" +scripts/setupMacOS-TPL-deps.bash --spack-config-out "${SPACK_CONFIG}" +``` + +The generated file contains the detected Apple Clang version, active Homebrew +prefix, selected MPI provider, and generic Darwin constraints. Keep the file +with the build until the TPL installation is complete; it can then be removed. + +Apple Clang is the default compiler for C and C++ on macOS. Homebrew GCC is +used only for Fortran because Apple Clang does not provide a Fortran compiler. +The environment uses macOS `curl` for source downloads so certificate +validation uses the system trust store rather than Homebrew Python's OpenSSL +backend. + +Then run the TPL build as a separate step. The canonical invocation for this +configuration is: + +```console +scripts/uberenv/uberenv.py \ + --spack-env-file="${SPACK_CONFIG}" \ + --prefix=/absolute/path/to/geos-tpls \ + --spec="%c,cxx=apple-clang %fortran=gcc@16.2.0" +``` + +Use another absolute `--prefix` when the TPL installation belongs elsewhere; +do not reuse a prefix containing a lock file or installation from a different +dependency set. + +Installation occurs only when all of the following preflight checks pass: + +- the host is Darwin/arm64 and the Apple Command Line Tools are available; +- CMake is at least `3.24`, and either `mpich` or `open-mpi` is available; +- Homebrew reports the exact stable formula version, formula revision, and Ruby + source checksum for the exact-pinned formulas in the manifest; +- every formula already installed has the exact receipt version and prefix; +- every required executable, header, and library from an installed formula is + present. + +If the preflight succeeds, all missing formulas are installed in one Homebrew +invocation. If neither MPI implementation is present, `open-mpi` is added to +that invocation. The complete set is then checked again, including +metadata, receipts, prefixes, and required paths. A partial or changed +installation is an error. + +The script exports `HOMEBREW_NO_AUTO_UPDATE=1`, so the formula metadata visible +to the invoked Homebrew is authoritative for that run. A stale local API cache +is reported as drift instead of being mistaken for the tested formula set. + +The generated Spack environment selects the installed MPI provider and its +concrete version. It also selects `/usr/bin/ar` and +`/usr/bin/ranlib` and puts `/usr/bin` ahead of user PATH entries while packages +are built. It also removes Homebrew's keg-only `binutils/bin` directory from +that build PATH. This keeps GNU `ar` from producing archives that Apple's +linker cannot consume; GEOS still receives the required Homebrew `addr2line` +executable through its absolute path in the generated host-config. No global +PATH setup is required beyond Homebrew's `shellenv`. + +The checked-in Spack file is the qualification template. Use the generated +file for a build so the compiler identity and Homebrew paths match the current +machine. This configuration remains Apple-Silicon-only (`arm64`/`aarch64`); +the portability change is for macOS and toolchain revisions. + +## Expected drift behavior + +Homebrew core formulas are moving names, not immutable version selectors. If a +required formula is missing and core no longer offers the manifest version, the +script stops before installing anything. Do not work around this with `brew +upgrade`, an unreviewed downgrade, or `--force` linking. Qualify the newer +dependency set with a clean TPL build and update the manifest and macOS Spack +configuration together. + +The checked-in exact formula pins and source checksums come from the official +Homebrew formula API snapshot dated 2026-09-04 and are selected for +qualification. They are not yet described as qualified until a clean TPL build +and its smoke tests pass. The manifest records the exact host used to select +them for traceability, but macOS patch/build revisions, Apple Clang build +revisions, Apple Clang versions, CMake versions at or above `3.24`, MPI provider +choice, and Homebrew executable patch releases are informational rather than +support gates. Homebrew-managed transitive dependencies are not separate Spack +externals; the post-install executable and link-library checks are the local +compatibility guard for this boundary. + +## Updating the manifest + +Treat a manifest change as a toolchain change: + +1. obtain exact-pinned formula versions (`stable`, plus `_` when the + formula revision is nonzero) and `ruby_source_checksum.sha256` from + `brew info --json=v2` or the official formula API; keep CMake at or above + `3.24` and keep either `mpich` or `open-mpi` available; +2. update the matching external version and prefix in the macOS Spack + environment; +3. run `scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash`; +4. build into a new, empty TPL prefix; +5. verify the generated Spack lock file uses the declared Homebrew externals + while diffutils and zlib are non-external and Perl is the declared Homebrew + external; and +6. compile and run MPI, BLAS, and zlib smoke tests before calling the new set + qualified. + +The dependency script prepares and validates Homebrew and can emit the +host-specific Spack environment. Run uberenv separately after it succeeds. It +also does not initialize BLT. A direct CMake configuration of this repository +requires the BLT submodule, so initialize it first when needed: + +```console +git submodule update --init cmake/blt +``` diff --git a/scripts/spack_configs/macOS/homebrew-manifest.json b/scripts/spack_configs/macOS/homebrew-manifest.json new file mode 100644 index 00000000..c1dfddcc --- /dev/null +++ b/scripts/spack_configs/macOS/homebrew-manifest.json @@ -0,0 +1,201 @@ +{ + "schema_version": 1, + "source": { + "description": "Official Homebrew formula API metadata and version policies selected for GEOS macOS TPL qualification", + "as_of": "2026-09-04" + }, + "supported_platform": { + "os": "Darwin", + "arch": "arm64", + "macos_major": "26", + "apple_clang_version": "17.0.0", + "sdk_major": "26", + "homebrew_prefix": "/opt/homebrew" + }, + "qualification_host": { + "macos_product_version": "26.6.2", + "macos_build_version": "25G83", + "apple_clang_build": "1700.6.3.2", + "sdk_version": "26.2", + "homebrew_version": "6.0.21" + }, + "formulae": [ + { + "name": "gcc", + "brew_version": "16.2.0", + "formula_sha256": "0683955ef01d30162abfcfff3fe9b0e73eebba9a5f9920fb53cb94bc8cdc3f43", + "prefix": "/opt/homebrew/opt/gcc", + "spack_package": "gcc", + "spack_version": "16.2.0", + "required_paths": [ + "bin/gfortran-16" + ] + }, + { + "name": "openblas", + "brew_version": "0.3.34", + "formula_sha256": "b6c9d393f4c2a6ebe1b66354decd9d7da8ce66952f9e760454fd601689e2d89a", + "prefix": "/opt/homebrew/opt/openblas", + "spack_package": "openblas", + "spack_version": "0.3.34", + "required_paths": [ + "include/cblas.h", + "lib/libopenblas.dylib" + ] + }, + { + "name": "open-mpi", + "version_policy": "any", + "brew_version": "5.0.10", + "formula_sha256": "c642ed42caecd2f2f26aa4232fea968583408fa54097f79acf0c9a30a299c82a", + "prefix": "/opt/homebrew/opt/open-mpi", + "spack_package": "openmpi", + "spack_version": "5.0.10", + "required_paths": [ + "bin/mpicc", + "bin/mpicxx", + "bin/mpifort", + "lib/libmpi.dylib" + ] + }, + { + "name": "cmake", + "version_policy": "minimum", + "minimum_version": "3.24", + "brew_version": "4.4.3", + "formula_sha256": "55fab0cd335d245c13704744585837a4e593aad16335097fc554df712e2dba1b", + "prefix": "/opt/homebrew/opt/cmake", + "spack_package": "cmake", + "spack_version": "4.4.3", + "required_paths": [ + "bin/cmake" + ] + }, + { + "name": "readline", + "brew_version": "8.3.3", + "formula_sha256": "660d4099f7dcd652c78f530672ffaf01103a00b3ee72aca4059f10c1038b1228", + "prefix": "/opt/homebrew/opt/readline", + "spack_package": "readline", + "spack_version": "8.3", + "required_paths": [ + "include/readline/readline.h", + "lib/libreadline.dylib" + ] + }, + { + "name": "m4", + "brew_version": "1.4.21", + "formula_sha256": "79b4221c141d51a12b59824d0dd69e59c90ee582ca9451150ea973278e19fabf", + "prefix": "/opt/homebrew/opt/m4", + "spack_package": "m4", + "spack_version": "1.4.21", + "required_paths": [ + "bin/m4" + ] + }, + { + "name": "pkgconf", + "brew_version": "3.0.6", + "formula_sha256": "53cae4d107ccc45a50359ea12428bddec0ecd0ef1fe8283a3ace27cd497ef4e0", + "prefix": "/opt/homebrew/opt/pkgconf", + "spack_package": "pkgconf", + "spack_version": "3.0.6", + "required_paths": [ + "bin/pkg-config", + "bin/pkgconf" + ] + }, + { + "name": "autoconf", + "brew_version": "2.73", + "formula_sha256": "036d4f18fa1b9072705af36ebc9c8d02700853c0149e04feeb0fc2232aecab96", + "prefix": "/opt/homebrew/opt/autoconf", + "spack_package": "autoconf", + "spack_version": "2.73", + "required_paths": [ + "bin/autoconf" + ] + }, + { + "name": "automake", + "brew_version": "1.18.1_1", + "formula_sha256": "187e1f49c7831094765bc2630eef07270d5080670f67b3b4a610391cb73bd233", + "prefix": "/opt/homebrew/opt/automake", + "spack_package": "automake", + "spack_version": "1.18.1", + "required_paths": [ + "bin/automake" + ] + }, + { + "name": "libtool", + "brew_version": "2.6.2", + "formula_sha256": "1b21318c46a9bba74d7013aef9d76ee0144f1b747aceecaa7ba92438462c3a79", + "prefix": "/opt/homebrew/opt/libtool", + "spack_package": "libtool", + "spack_version": "2.6.2", + "required_paths": [ + "bin/glibtool", + "bin/glibtoolize" + ] + }, + { + "name": "gettext", + "brew_version": "1.0", + "formula_sha256": "bd0322f7114c68dd627c974ec16ceaa6295bf68eac55995ee2dfb59ce6fefd51", + "prefix": "/opt/homebrew/opt/gettext", + "spack_package": "gettext", + "spack_version": "1.0", + "required_paths": [ + "bin/gettext", + "lib/libintl.dylib" + ] + }, + { + "name": "binutils", + "brew_version": "2.47", + "formula_sha256": "f1fd62bf787bec32ca4f0bf872372bddd6432f0db24b5c5d6c68fb0b340d047e", + "prefix": "/opt/homebrew/opt/binutils", + "spack_package": "binutils", + "spack_version": "2.47", + "required_paths": [ + "bin/addr2line" + ] + }, + { + "name": "python@3.14", + "brew_version": "3.14.7", + "formula_sha256": "e7e3a023de8b88f5a7e2e09c861e9e15fb0d0a2d78b6bb619a1b27e7da155b9a", + "prefix": "/opt/homebrew/opt/python@3.14", + "spack_package": "python", + "spack_version": "3.14.7", + "required_paths": [ + "bin/python3.14" + ] + }, + { + "name": "perl", + "version_policy": "any", + "brew_version": "5.44.0", + "formula_sha256": "19d8fe283a3699d99d4a81d521b2cf5317f5e8ad728319138518b2e803e70dcb", + "prefix": "/opt/homebrew/opt/perl", + "spack_package": "perl", + "spack_version": "5.44.0", + "required_paths": [ + "bin/perl" + ] + } + ], + "spack_built": [ + { + "package": "diffutils", + "version": "3.12" + }, + { + "package": "zlib", + "version": "1.3.2", + "variants": "+pic+shared" + } + ] +} diff --git a/scripts/spack_configs/macOS/spack.yaml b/scripts/spack_configs/macOS/spack.yaml index 693e973b..1c577927 100644 --- a/scripts/spack_configs/macOS/spack.yaml +++ b/scripts/spack_configs/macOS/spack.yaml @@ -8,6 +8,10 @@ spack: test_stage: $spack/../test_stage build_stage: - $spack/../build_stage + # macOS curl uses the system trust store, which handles the certificate + # chain presented by the network's VTK mirror path. Homebrew Python's + # OpenSSL urllib backend rejects that otherwise valid macOS trust chain. + url_fetch_method: curl # Regular TPLs do not need views view: false @@ -17,146 +21,169 @@ spack: - ../defaults.yaml - ../versions.yaml - compilers: - - compiler: - spec: apple-clang@16.0.0 - paths: - cc: /usr/bin/clang - cxx: /usr/bin/clang++ - f77: /opt/homebrew/bin/gfortran - fc: /opt/homebrew/bin/gfortran - flags: - cppflags: -I/opt/homebrew/opt/openblas/include - ldflags: -L/opt/homebrew/opt/openblas/lib - operating_system: sequoia - target: aarch64 - modules: [] - environment: - set: - PATH: /usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/opt/homebrew/sbin - AR: /usr/bin/ar - RANLIB: /usr/bin/ranlib - CMAKE_AR: /usr/bin/ar - CMAKE_RANLIB: /usr/bin/ranlib - extra_rpaths: [] - - compiler: - spec: apple-clang@17.0.0 - paths: - cc: /usr/bin/clang - cxx: /usr/bin/clang++ - f77: /opt/homebrew/bin/gfortran - fc: /opt/homebrew/bin/gfortran - flags: - cppflags: -I/opt/homebrew/opt/openblas/include - ldflags: -L/opt/homebrew/opt/openblas/lib - operating_system: sequoia - target: aarch64 - modules: [] - environment: - set: - PATH: /usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/opt/homebrew/sbin - AR: /usr/bin/ar - RANLIB: /usr/bin/ranlib - CMAKE_AR: /usr/bin/ar - CMAKE_RANLIB: /usr/bin/ranlib - extra_rpaths: [] - packages: all: target: [aarch64] - compiler: [apple-clang] - providers: - mpi: [openmpi] - blas: [openblas] - lapack: [openblas] - zlib-api: [zlib] variants: "~openmp" + # Apple Clang is the default C/C++ provider on macOS. GCC is selected + # only for Fortran because Apple Clang does not provide a Fortran front end. + require: + - "%[when=%c]c=apple-clang %[when=%cxx]cxx=apple-clang" + + # Spack 1.2 models compilers as package externals. Apple Clang supplies + # C/C++, while Homebrew GCC supplies only the Fortran language virtual. + c: + require: "apple-clang" + cxx: + require: "apple-clang" + fortran: + require: "gcc@=16.2.0 languages:=fortran" + + apple-clang: + buildable: false + # GEOS_TPL_APPLE_CLANG_EXTERNAL + + gcc: + buildable: false + externals: + - spec: "gcc@=16.2.0 languages:=fortran platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/gcc + extra_attributes: + compilers: + fortran: /opt/homebrew/opt/gcc/bin/gfortran-16 + environment: + set: + AR: /usr/bin/ar + RANLIB: /usr/bin/ranlib + CMAKE_AR: /usr/bin/ar + CMAKE_RANLIB: /usr/bin/ranlib + prepend_path: + PATH: /usr/bin + remove_path: + PATH: /opt/homebrew/opt/binutils/bin geosx: - variants: "+addr2line~openmp" + require: "+addr2line ~openmp" + + # Lock virtual dependencies to the matching Homebrew externals. The setup + # script rewrites the MPI requirement to MPICH when it is already present; + # otherwise OpenMPI is the fallback. zlib-api remains buildable so Spack + # supplies zlib instead of a nonexistent keg. + mpi: + buildable: false + require: "openmpi@=5.0.10" + blas: + buildable: false + require: "openblas@=0.3.34" + lapack: + buildable: false + require: "openblas@=0.3.34" + pkgconfig: + buildable: false + require: "pkgconf@=3.0.6" + zlib-api: + require: "zlib@=1.3.2" + + # Perl is provided by Homebrew. Diffutils and zlib are intentionally + # Spack-built rather than modeled as Homebrew kegs. + perl: + buildable: false + externals: + - spec: "perl@=5.44.0 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/perl + zlib: + buildable: true + require: "@=1.3.2 +pic +shared" + diffutils: + buildable: true + require: "@=3.12" openblas: - buildable: False + buildable: false externals: - - spec: openblas@0.3.29 - prefix: /opt/homebrew/opt/openblas + - spec: >- + openblas@=0.3.34 + +fortran ~ilp64 +pic +shared +static + +dynamic_dispatch +locking ~bignuma ~consistent_fpcsr + threads=openmp max_num_threads=56 symbol_suffix=none + platform=darwin target=aarch64 + prefix: /opt/homebrew/opt/openblas - # Lock down which MPI we are using openmpi: - buildable: False + buildable: false externals: - - spec: openmpi@5.0.6 - prefix: /opt/homebrew/opt/open-mpi + - spec: >- + openmpi@=5.0.10 + +fortran +ipv6 +romio + ~internal-hwloc ~internal-libevent ~internal-pmix ~static + fabrics:=none schedulers:=sge + platform=darwin target=aarch64 + prefix: /opt/homebrew/opt/open-mpi - # System level packages to not build + # MPICH is accepted when it is already installed. The setup script selects + # the matching provider in the host-specific environment it generates. + mpich: + buildable: false + externals: + - spec: "mpich@=5.0.1 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/mpich + + # Homebrew system packages. Diffutils and zlib remain Spack-built for a + # complete, valid prefix. cmake: - version: [3.29.6] buildable: false + require: "@3.24:" externals: - - spec: cmake@3.29.6 - prefix: /opt/homebrew/opt/cmake + - spec: "cmake@=4.4.3 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/cmake readline: buildable: false externals: - - spec: readline@8.2.13 + - spec: "readline@=8.3 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/readline - + m4: - buildable: False - externals: - - spec: m4@1.4.6 - prefix: /opt/homebrew/opt/m4 - perl: buildable: false externals: - - spec: perl@5.34.1 - prefix: /opt/homebrew/opt/perl - pkg-config: + - spec: "m4@=1.4.21 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/m4 + pkgconf: buildable: false externals: - - spec: pkg-config@2.3.0 + - spec: "pkgconf@=3.0.6 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/pkgconf - diffutils: - buildable: False - externals: - - spec: diffutils@3.11 - prefix: /opt/homebrew/opt/diffutils - + autoconf: - buildable: False + buildable: false externals: - - spec: autoconf@2.72 - prefix: /opt/homebrew/opt/autoconf + - spec: "autoconf@=2.73 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/autoconf automake: - buildable: False + buildable: false externals: - - spec: automake@1.17 - prefix: /opt/homebrew/opt/automake + - spec: "automake@=1.18.1 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/automake libtool: - buildable: False + buildable: false externals: - - spec: libtool@2.5.4 - prefix: /opt/homebrew/opt/libtool + - spec: "libtool@=2.6.2 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/libtool gettext: - buildable: False - externals: - - spec: gettext@0.23.1 - prefix: /opt/homebrew/opt/gettext - - addr2line: - buildable: False + buildable: false externals: - - spec: addr2line@2.43.1 - prefix: /opt/homebrew/opt/binutils + - spec: "gettext@=1.0 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/gettext - zlib: - buildable: False + binutils: + buildable: false externals: - - spec: zlib@1.3.1 - prefix: /opt/homebrew/opt/zlib + - spec: >- + binutils@=2.47 libs:=static + platform=darwin target=aarch64 + prefix: /opt/homebrew/opt/binutils python: buildable: false externals: - - spec: python@3.14.3 - prefix: /opt/homebrew/opt/python@3.14 + - spec: "python@=3.14.7 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/python@3.14 diff --git a/scripts/spack_packages/packages/geosx/package.py b/scripts/spack_packages/packages/geosx/package.py index 2c095bb8..533c7b63 100644 --- a/scripts/spack_packages/packages/geosx/package.py +++ b/scripts/spack_packages/packages/geosx/package.py @@ -227,7 +227,7 @@ class Geosx(CMakePackage, CudaPackage, ROCmPackage): # depends_on("mathpresso cxxflags='-fPIC'", when='+mathpresso') depends_on('grpc', when='+grpc') - depends_on('addr2line', when='+addr2line') + depends_on('binutils', when='+addr2line', type='run') # SPHINX_END_DEPENDS @@ -648,7 +648,7 @@ def geos_hostconfig(self, spec, prefix, py_site_pkgs_dir=None): cfg.write('# addr2line\n') cfg.write('#{0}\n\n'.format('-' * 80)) cfg.write(cmake_cache_option('ENABLE_ADDR2LINE', True)) - cfg.write(cmake_cache_path('ADDR2LINE_EXEC', os.path.join(spec['addr2line'].prefix.bin, 'addr2line'))) + cfg.write(cmake_cache_path('ADDR2LINE_EXEC', os.path.join(spec['binutils'].prefix.bin, 'addr2line'))) cfg.write('#{0}\n'.format('-' * 80)) cfg.write('# Other\n') @@ -836,7 +836,7 @@ def lvarray_hostconfig(self, spec, prefix, py_site_pkgs_dir=None): cfg.write('# addr2line\n') cfg.write('#{0}\n\n'.format('-' * 80)) cfg.write(cmake_cache_option('ENABLE_ADDR2LINE', True)) - cfg.write(cmake_cache_path('ADDR2LINE_EXEC', os.path.join(spec['addr2line'].prefix.bin, 'addr2line'))) + cfg.write(cmake_cache_path('ADDR2LINE_EXEC', os.path.join(spec['binutils'].prefix.bin, 'addr2line'))) def cmake_args(self): pass diff --git a/scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash b/scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash new file mode 100755 index 00000000..b3a40479 --- /dev/null +++ b/scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash @@ -0,0 +1,734 @@ +#!/bin/bash + +# Focused, dependency-free tests for setupMacOS-TPL-deps.bash. The production +# script runs against a fake Homebrew and fake platform tools; no real formula +# or Homebrew state is changed. + +set -u + +TEST_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P) +REPO_ROOT=$(CDPATH='' cd -- "${TEST_DIR}/../../.." && pwd -P) +SCRIPT="${REPO_ROOT}/scripts/setupMacOS-TPL-deps.bash" +REAL_MANIFEST="${REPO_ROOT}/scripts/spack_configs/macOS/homebrew-manifest.json" +SPACK_YAML="${REPO_ROOT}/scripts/spack_configs/macOS/spack.yaml" +PLUTIL=/usr/bin/plutil + +TEST_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/geos-macos-homebrew-tests.XXXXXX") +STATE="${TEST_ROOT}/state" +FAKE_BIN="${TEST_ROOT}/bin" +FAKE_PREFIX="${TEST_ROOT}/homebrew" +MANIFEST="${TEST_ROOT}/manifest.json" +LAST_OUTPUT="${TEST_ROOT}/last-output.txt" + +PASS_COUNT=0 +FAIL_COUNT=0 + +cleanup() +{ + case "${TEST_ROOT}" in + "${TMPDIR:-/tmp}"/geos-macos-homebrew-tests.*) + rm -rf -- "${TEST_ROOT}" + ;; + esac +} +trap cleanup EXIT HUP INT TERM + +pass() +{ + echo "PASS: $1" + PASS_COUNT=$((PASS_COUNT + 1)) +} + +fail() +{ + echo "FAIL: $1" >&2 + if [[ -f "${LAST_OUTPUT}" ]]; then + sed 's/^/ | /' "${LAST_OUTPUT}" >&2 + fi + FAIL_COUNT=$((FAIL_COUNT + 1)) +} + +expect_success() +{ + local label=$1 + shift + if "$@" >"${LAST_OUTPUT}" 2>&1; then + pass "${label}" + else + fail "${label} (expected success)" + fi +} + +expect_failure() +{ + local label=$1 + shift + if "$@" >"${LAST_OUTPUT}" 2>&1; then + fail "${label} (expected failure)" + else + pass "${label}" + fi +} + +assert_file_contains() +{ + local label=$1 + local file=$2 + local pattern=$3 + if [[ -f "${file}" ]] && grep -F -q -- "${pattern}" "${file}"; then + pass "${label}" + else + echo "Expected '${pattern}' in ${file}" >"${LAST_OUTPUT}" + fail "${label}" + fi +} + +assert_file_contains_line() +{ + local label=$1 + local file=$2 + local line=$3 + if [[ -f "${file}" ]] && grep -F -x -q -- "${line}" "${file}"; then + pass "${label}" + else + echo "Expected exact line '${line}' in ${file}" >"${LAST_OUTPUT}" + fail "${label}" + fi +} + +assert_file_absent_or_empty() +{ + local label=$1 + local file=$2 + if [[ ! -s "${file}" ]]; then + pass "${label}" + else + cp "${file}" "${LAST_OUTPUT}" + fail "${label}" + fi +} + +formula_key() +{ + printf '%s\n' "$1" | sed 's#[/@]#_#g' +} + +set_candidate() +{ + local formula=$1 + local stable=$2 + local revision=$3 + local sha=$4 + local key + key=$(formula_key "${formula}") + printf '%s\n' "${stable}" >"${STATE}/candidate_${key}_stable" + printf '%s\n' "${revision}" >"${STATE}/candidate_${key}_revision" + printf '%s\n' "${sha}" >"${STATE}/candidate_${key}_sha" +} + +install_fixture() +{ + local formula=$1 + local version=$2 + local tool=$3 + local key prefix + key=$(formula_key "${formula}") + prefix="${FAKE_PREFIX}/opt/${formula}" + printf '%s\n' "${version}" >"${STATE}/installed_${key}" + mkdir -p "${prefix}/bin" + : >"${prefix}/bin/${tool}" +} + +create_fake_tools() +{ + mkdir -p "${STATE}" "${FAKE_BIN}" "${FAKE_PREFIX}/opt" + + cat >"${FAKE_BIN}/uname" <<'EOF' +#!/bin/bash +case "${1:-}" in + -s) cat "${FAKE_BREW_STATE}/os" ;; + -m) cat "${FAKE_BREW_STATE}/arch" ;; + *) exit 2 ;; +esac +EOF + + cat >"${FAKE_BIN}/sw_vers" <<'EOF' +#!/bin/bash +case "${1:-}" in + -productVersion) cat "${FAKE_BREW_STATE}/macos_version" ;; + -buildVersion) cat "${FAKE_BREW_STATE}/macos_build" ;; + *) exit 2 ;; +esac +EOF + + cat >"${FAKE_BIN}/xcrun" <<'EOF' +#!/bin/bash +if [[ "${1:-}" == "--show-sdk-version" ]]; then + cat "${FAKE_BREW_STATE}/sdk_version" +else + exit 2 +fi +EOF + + cat >"${FAKE_BIN}/clang" <<'EOF' +#!/bin/bash +if [[ "${1:-}" == "--version" ]]; then + echo "Apple clang version $(cat "${FAKE_BREW_STATE}/clang_version") (clang-$(cat "${FAKE_BREW_STATE}/clang_build"))" + echo "Target: arm64-apple-darwin" +else + exit 2 +fi +EOF + + cat >"${FAKE_BIN}/brew" <<'EOF' +#!/bin/bash +set -u + +state=${FAKE_BREW_STATE:?} +prefix=${FAKE_HOMEBREW_PREFIX:?} +printf '%s\n' "$*" >>"${state}/commands.log" + +key_for() +{ + printf '%s\n' "$1" | sed 's#[/@]#_#g' +} + +case "${1:-}" in + --version) + echo "Homebrew $(cat "${state}/brew_version")" + ;; + --prefix) + if [[ $# -eq 1 ]]; then + echo "${prefix}" + else + echo "${prefix}/opt/$2" + fi + ;; + info) + [[ "${2:-}" == "--json=v2" ]] || exit 2 + formula=${3:?} + key=$(key_for "${formula}") + stable=$(cat "${state}/candidate_${key}_stable") + revision=$(cat "${state}/candidate_${key}_revision") + sha=$(cat "${state}/candidate_${key}_sha") + printf '{"formulae":[{"full_name":"%s","versions":{"stable":"%s"},"revision":%s,"ruby_source_checksum":{"sha256":"%s"}}]}\n' \ + "${formula}" "${stable}" "${revision}" "${sha}" + ;; + list) + [[ "${2:-}" == "--versions" ]] || exit 2 + formula=${3:?} + key=$(key_for "${formula}") + if [[ -f "${state}/installed_${key}" ]]; then + echo "${formula##*/} $(cat "${state}/installed_${key}")" + fi + ;; + install) + shift + install_count=0 + printf 'install-env:no-upgrade=%s:auto-update=%s:cleanup=%s\n' \ + "${HOMEBREW_NO_INSTALL_UPGRADE:-}" "${HOMEBREW_NO_AUTO_UPDATE:-}" "${HOMEBREW_NO_INSTALL_CLEANUP:-}" \ + >>"${state}/commands.log" + for formula in "$@"; do + install_count=$((install_count + 1)) + key=$(key_for "${formula}") + stable=$(cat "${state}/candidate_${key}_stable") + revision=$(cat "${state}/candidate_${key}_revision") + version=${stable} + if [[ "${revision}" -gt 0 ]]; then + version="${stable}_${revision}" + fi + if [[ "${FAKE_INSTALL_WRONG:-0}" == "1" ]]; then + version=99.0 + fi + printf '%s\n' "${version}" >"${state}/installed_${key}" + mkdir -p "${prefix}/opt/${formula}/bin" + : >"${prefix}/opt/${formula}/bin/${formula}-tool" + if [[ "${FAKE_INSTALL_FAIL_AFTER:-0}" -eq "${install_count}" ]]; then + exit 42 + fi + done + ;; + update|upgrade|cleanup) + echo "Forbidden mutating command: $1" >&2 + exit 99 + ;; + *) + echo "Unexpected fake brew arguments: $*" >&2 + exit 2 + ;; +esac +EOF + + chmod +x "${FAKE_BIN}/uname" "${FAKE_BIN}/sw_vers" "${FAKE_BIN}/xcrun" \ + "${FAKE_BIN}/clang" "${FAKE_BIN}/brew" +} + +write_manifest() +{ + cat >"${MANIFEST}" <"${STATE}/os" + printf 'arm64\n' >"${STATE}/arch" + printf '26.99.7\n' >"${STATE}/macos_version" + printf '25Z999\n' >"${STATE}/macos_build" + printf '26.42\n' >"${STATE}/sdk_version" + printf '17.0.0\n' >"${STATE}/clang_version" + printf '1700.99.1\n' >"${STATE}/clang_build" + printf '99.7.3\n' >"${STATE}/brew_version" + set_candidate alpha 1.0 0 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + set_candidate beta 2.0 1 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + set_candidate cmake 4.4.3 0 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + set_candidate open-mpi 5.0.10 0 dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd + set_candidate perl 5.44.0 0 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + install_fixture alpha 1.0 alpha-tool + install_fixture beta 2.0_1 beta-tool + install_fixture cmake 4.4.3 cmake-tool + install_fixture open-mpi 5.0.10 open-mpi-tool + install_fixture perl 5.44.0 perl-tool +} + +run_setup() +{ + FAKE_BREW_STATE="${STATE}" \ + FAKE_HOMEBREW_PREFIX="${FAKE_HOST_PREFIX:-${FAKE_PREFIX}}" \ + FAKE_INSTALL_WRONG="${FAKE_INSTALL_WRONG:-0}" \ + FAKE_INSTALL_FAIL_AFTER="${FAKE_INSTALL_FAIL_AFTER:-0}" \ + GEOS_TPL_BREW_BIN="${FAKE_BIN}/brew" \ + GEOS_TPL_UNAME_BIN="${FAKE_BIN}/uname" \ + GEOS_TPL_SW_VERS_BIN="${FAKE_BIN}/sw_vers" \ + GEOS_TPL_XCRUN_BIN="${FAKE_BIN}/xcrun" \ + GEOS_TPL_CLANG_BIN="${FAKE_BIN}/clang" \ + /bin/bash "${SCRIPT}" --manifest "${MANIFEST}" "$@" +} + +run_setup_with_wrong_install() +{ + FAKE_INSTALL_WRONG=1 run_setup "$@" +} + +spack_package_block() +{ + local package=$1 + awk -v heading=" ${package}:" ' + $0 == heading { found = 1; print; next } + found && $0 ~ /^ [A-Za-z0-9_-]+:$/ { exit } + found { print } + ' "${SPACK_YAML}" +} + +assert_manifest_field() +{ + local label=$1 + local key=$2 + local expected=$3 + local actual + actual=$("${PLUTIL}" -extract "${key}" raw -o - "${REAL_MANIFEST}" 2>/dev/null || true) + if [[ "${actual}" == "${expected}" ]]; then + pass "${label}" + else + echo "Expected ${key}=${expected}, found ${actual}" >"${LAST_OUTPUT}" + fail "${label}" + fi +} + +test_committed_manifest() +{ + local count i name found_forbidden spack_count + local spack_package spack_version prefix block package version + local external_count prefix_count package_heading_count + if ! "${PLUTIL}" -convert json -o - "${REAL_MANIFEST}" >/dev/null; then + fail "committed manifest is valid JSON" + return + fi + count=$("${PLUTIL}" -extract formulae raw -o - "${REAL_MANIFEST}") + if [[ "${count}" != "14" ]]; then + echo "Expected 14 formulae, found ${count}" >"${LAST_OUTPUT}" + fail "committed manifest formula count" + return + fi + found_forbidden=false + i=0 + while [[ ${i} -lt ${count} ]]; do + name=$("${PLUTIL}" -extract "formulae.${i}.name" raw -o - "${REAL_MANIFEST}") + case "${name}" in + diffutils|zlib) found_forbidden=true ;; + esac + i=$((i + 1)) + done + if [[ "${found_forbidden}" == "true" ]]; then + echo "Perl, diffutils, or zlib appeared in Homebrew formulae" >"${LAST_OUTPUT}" + fail "Spack-built packages are excluded from Homebrew formulae" + return + fi + spack_count=$("${PLUTIL}" -extract spack_built raw -o - "${REAL_MANIFEST}") + if [[ "${spack_count}" == "2" ]]; then + pass "committed manifest schema and hybrid boundary" + else + echo "Expected 2 Spack-built packages, found ${spack_count}" >"${LAST_OUTPUT}" + fail "committed manifest schema and hybrid boundary" + fi + + i=0 + while [[ ${i} -lt ${count} ]]; do + name=$("${PLUTIL}" -extract "formulae.${i}.name" raw -o - "${REAL_MANIFEST}") + spack_package=$("${PLUTIL}" -extract "formulae.${i}.spack_package" raw -o - "${REAL_MANIFEST}") + spack_version=$("${PLUTIL}" -extract "formulae.${i}.spack_version" raw -o - "${REAL_MANIFEST}") + prefix=$("${PLUTIL}" -extract "formulae.${i}.prefix" raw -o - "${REAL_MANIFEST}") + block=$(spack_package_block "${spack_package}") + external_count=$(printf '%s\n' "${block}" | grep -E -c '^[[:space:]]+- spec:') + prefix_count=$(printf '%s\n' "${block}" | grep -F -x -c -- " prefix: ${prefix}") + package_heading_count=$(grep -F -x -c -- " ${spack_package}:" "${SPACK_YAML}") + if [[ -n "${block}" ]] \ + && printf '%s\n' "${block}" | grep -F -q -- "buildable: false" \ + && printf '%s\n' "${block}" | grep -F -q -- "@=${spack_version}" \ + && [[ "${external_count}" == "1" ]] \ + && [[ "${prefix_count}" == "1" ]] \ + && [[ "${package_heading_count}" == "1" ]]; then + pass "manifest ${name} maps to an exact non-buildable Spack external" + else + printf 'Manifest mapping for %s did not match Spack package block:\n%s\n' \ + "${name}" "${block}" >"${LAST_OUTPUT}" + fail "manifest ${name} maps to an exact non-buildable Spack external" + fi + i=$((i + 1)) + done + + i=0 + while [[ ${i} -lt ${spack_count} ]]; do + package=$("${PLUTIL}" -extract "spack_built.${i}.package" raw -o - "${REAL_MANIFEST}") + version=$("${PLUTIL}" -extract "spack_built.${i}.version" raw -o - "${REAL_MANIFEST}") + block=$(spack_package_block "${package}") + if [[ -n "${block}" ]] \ + && printf '%s\n' "${block}" | grep -F -q -- "buildable: true" \ + && printf '%s\n' "${block}" | grep -F -q -- "@=${version}"; then + pass "manifest ${package} remains Spack-built at the declared version" + else + printf 'Spack-built mapping for %s did not match package block:\n%s\n' \ + "${package}" "${block}" >"${LAST_OUTPUT}" + fail "manifest ${package} remains Spack-built at the declared version" + fi + i=$((i + 1)) + done + + assert_manifest_field "GCC requires the versioned Fortran executable" \ + formulae.0.required_paths.0 bin/gfortran-16 + assert_manifest_field "OpenBLAS uses the selected-for-qualification version" \ + formulae.1.brew_version 0.3.34 + assert_manifest_field "readline maps its Homebrew patch release to Spack 8.3" \ + formulae.4.spack_version 8.3 + assert_manifest_field "CMake uses the Homebrew core formula" \ + formulae.3.name cmake + assert_manifest_field "CMake uses the project minimum version" \ + formulae.3.minimum_version 3.24 + assert_manifest_field "MPI accepts an installed OpenMPI version" \ + formulae.2.version_policy any + assert_manifest_field "binutils maps to the binutils Spack package" \ + formulae.11.spack_package binutils + assert_manifest_field "Python uses the selected-for-qualification version" \ + formulae.12.brew_version 3.14.7 + assert_file_contains "macOS defaults C/C++ to Apple Clang" \ + "${SPACK_YAML}" '%[when=%c]c=apple-clang %[when=%cxx]cxx=apple-clang' + assert_file_contains "macOS reserves GCC for Fortran" \ + "${SPACK_YAML}" 'require: "gcc@=16.2.0 languages:=fortran"' + assert_file_contains "macOS Spack downloads use curl" \ + "${SPACK_YAML}" "url_fetch_method: curl" +} + +create_fake_tools +write_manifest +test_committed_manifest + +# Host releases newer than the recorded qualification host are accepted. The +# Homebrew formula versions and source checksums remain exact. +reset_state +expect_success "qualification-host macOS/SDK versions accept patch drift" run_setup --check-only + +reset_state +expect_success "an exact installed contract is a no-op" run_setup +expect_success "a second exact run is idempotent" run_setup +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "idempotent exact runs never invoke brew install" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "idempotent exact runs never invoke brew install" +fi +assert_file_contains "setup identifies Apple Clang as C/C++ default" \ + "${LAST_OUTPUT}" "Compiler defaults: C/C++=Apple Clang; Fortran=Homebrew GCC" +assert_file_contains "setup identifies the Fortran compiler" \ + "${LAST_OUTPUT}" "Fortran=Homebrew GCC" + +reset_state +printf '3.24.0\n' >"${STATE}/installed_cmake" +rm -rf -- "${FAKE_PREFIX}/opt/cmake" +mkdir -p "${FAKE_PREFIX}/opt/cmake/bin" +: >"${FAKE_PREFIX}/opt/cmake/bin/cmake-tool" +expect_success "CMake at the project minimum is accepted" run_setup --check-only + +reset_state +printf '3.23.9\n' >"${STATE}/installed_cmake" +rm -rf -- "${FAKE_PREFIX}/opt/cmake" +mkdir -p "${FAKE_PREFIX}/opt/cmake/bin" +: >"${FAKE_PREFIX}/opt/cmake/bin/cmake-tool" +expect_failure "CMake below the project minimum is rejected" run_setup --check-only + +reset_state +set_candidate cmake 4.5.0 0 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +expect_success "newer CMake metadata is accepted" run_setup --check-only + +reset_state +set_candidate open-mpi 6.0.0 0 eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +expect_success "newer OpenMPI metadata is accepted" run_setup --check-only + +reset_state +rm -f "${STATE}/installed_open-mpi" +rm -rf -- "${FAKE_PREFIX}/opt/open-mpi" +printf '5.0.1\n' >"${STATE}/installed_mpich" +mkdir -p "${FAKE_PREFIX}/opt/mpich/bin" "${FAKE_PREFIX}/opt/mpich/lib" +: >"${FAKE_PREFIX}/opt/mpich/bin/mpicc" +: >"${FAKE_PREFIX}/opt/mpich/bin/mpicxx" +: >"${FAKE_PREFIX}/opt/mpich/bin/mpifort" +: >"${FAKE_PREFIX}/opt/mpich/lib/libmpi.dylib" +GENERATED_MPI_CONFIG="${TEST_ROOT}/generated-mpich-spack.yaml" +expect_success "installed MPICH satisfies MPI without OpenMPI" run_setup --check-only --spack-config-out "${GENERATED_MPI_CONFIG}" +assert_file_contains "generated config selects MPICH" "${GENERATED_MPI_CONFIG}" 'require: "mpich@=5.0.1"' +if ! grep -F -q -- 'require: "openmpi@=' "${GENERATED_MPI_CONFIG}"; then + pass "generated config does not require OpenMPI when MPICH is present" +else + fail "generated config does not require OpenMPI when MPICH is present" +fi + +reset_state +rm -f "${STATE}/installed_open-mpi" +rm -rf -- "${FAKE_PREFIX}/opt/open-mpi" +expect_success "OpenMPI is installed only as the MPI fallback" run_setup +assert_file_contains_line "fallback install receives open-mpi" "${STATE}/commands.log" "install open-mpi" + +reset_state +printf '27.0\n' >"${STATE}/macos_version" +expect_success "newer macOS major is accepted" run_setup --check-only +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "newer macOS check-only performs no install" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "newer macOS check-only performs no install" +fi + +reset_state +printf 'x86_64\n' >"${STATE}/arch" +expect_failure "non-Apple-Silicon architecture is rejected" run_setup --check-only +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "architecture rejection performs no install" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "architecture rejection performs no install" +fi + +reset_state +printf '27.0\n' >"${STATE}/sdk_version" +expect_success "newer SDK major is accepted" run_setup --check-only +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "newer SDK check-only performs no install" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "newer SDK check-only performs no install" +fi + +reset_state +printf '21.0.0\n' >"${STATE}/clang_version" +expect_success "newer Apple Clang is accepted" run_setup --check-only +if grep -F -q -- 'Apple Clang 21.0.0 differs' "${LAST_OUTPUT}"; then + fail "Apple Clang qualification notice is omitted" +else + pass "Apple Clang qualification notice is omitted" +fi + +reset_state +printf '21.0.0\n' >"${STATE}/clang_version" +GENERATED_CONFIG="${TEST_ROOT}/generated-spack.yaml" +expect_success "host-specific Spack config is generated" run_setup --check-only --spack-config-out "${GENERATED_CONFIG}" +assert_file_contains "generated config requires Apple Clang for C/C++" "${GENERATED_CONFIG}" '%[when=%c]c=apple-clang %[when=%cxx]cxx=apple-clang' +assert_file_contains "generated config uses detected Apple Clang" "${GENERATED_CONFIG}" "apple-clang@=21.0.0" +if ! grep -F -q -- 'apple-clang@=17.0.0' "${GENERATED_CONFIG}"; then + pass "generated config does not use qualification-host Apple Clang" +else + fail "generated config does not use qualification-host Apple Clang" +fi +assert_file_contains "generated config uses detected Perl" "${GENERATED_CONFIG}" "perl@=5.44.0" +assert_file_contains "generated config uses detected Homebrew prefix" "${GENERATED_CONFIG}" "prefix: ${FAKE_PREFIX}/opt/openblas" +if [[ -f "${GENERATED_CONFIG}" ]] && ! grep -F -q -- 'os=tahoe' "${GENERATED_CONFIG}"; then + pass "generated config has no hard-coded macOS release" +else + cp "${GENERATED_CONFIG}" "${LAST_OUTPUT}" + fail "generated config has no hard-coded macOS release" +fi + +reset_state +FAKE_HOST_PREFIX="${TEST_ROOT}/alternate-homebrew" +mkdir -p "${FAKE_HOST_PREFIX}/opt/alpha/bin" "${FAKE_HOST_PREFIX}/opt/beta/bin" \ + "${FAKE_HOST_PREFIX}/opt/cmake/bin" "${FAKE_HOST_PREFIX}/opt/open-mpi/bin" \ + "${FAKE_HOST_PREFIX}/opt/perl/bin" +: >"${FAKE_HOST_PREFIX}/opt/alpha/bin/alpha-tool" +: >"${FAKE_HOST_PREFIX}/opt/beta/bin/beta-tool" +: >"${FAKE_HOST_PREFIX}/opt/cmake/bin/cmake-tool" +: >"${FAKE_HOST_PREFIX}/opt/open-mpi/bin/open-mpi-tool" +: >"${FAKE_HOST_PREFIX}/opt/perl/bin/perl-tool" +expect_success "alternate Homebrew prefix is accepted" run_setup --check-only +FAKE_HOST_PREFIX= + +reset_state +rm -f "${STATE}/installed_beta" +rm -rf -- "${FAKE_PREFIX}/opt/beta" +expect_failure "check-only reports an exact installable formula as missing" run_setup --check-only +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "check-only never invokes brew install" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "check-only never invokes brew install" +fi + +reset_state +rm -f "${STATE}/installed_beta" +rm -rf -- "${FAKE_PREFIX}/opt/beta" +expect_success "normal mode installs and revalidates only the missing formula" run_setup +assert_file_contains_line "brew install receives only beta" "${STATE}/commands.log" "install beta" +assert_file_contains "install disables upgrades and auto-update" "${STATE}/commands.log" "install-env:no-upgrade=1:auto-update=1:cleanup=1" +if ! grep -E -q '^(update|upgrade|cleanup)( |$)' "${STATE}/commands.log"; then + pass "script never invokes brew update, upgrade, or cleanup" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "script never invokes brew update, upgrade, or cleanup" +fi + +reset_state +printf '9.9\n' >"${STATE}/installed_beta" +expect_failure "installed receipt drift is rejected" run_setup +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "receipt drift prevents all installation" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "receipt drift prevents all installation" +fi + +reset_state +set_candidate beta 2.1 0 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +expect_failure "formula version metadata drift is rejected" run_setup + +reset_state +set_candidate beta 2.0 1 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +expect_failure "formula source checksum drift is rejected" run_setup + +reset_state +rm -f "${FAKE_PREFIX}/opt/beta/bin/beta-tool" +expect_failure "missing required formula path is rejected" run_setup + +reset_state +rm -f "${STATE}/installed_alpha" +rm -rf -- "${FAKE_PREFIX}/opt/alpha" +printf '9.9\n' >"${STATE}/installed_beta" +expect_failure "a later preflight error prevents installing an earlier missing formula" run_setup +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "formula preflight is atomic" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "formula preflight is atomic" +fi + +reset_state +rm -f "${STATE}/installed_beta" +rm -rf -- "${FAKE_PREFIX}/opt/beta" +expect_failure "post-install receipt mismatch is detected" run_setup_with_wrong_install + +reset_state +rm -f "${STATE}/installed_alpha" "${STATE}/installed_beta" +rm -rf -- "${FAKE_PREFIX}/opt/alpha" "${FAKE_PREFIX}/opt/beta" +FAKE_INSTALL_FAIL_AFTER=1 expect_failure \ + "a partial Homebrew install failure is surfaced" run_setup +if [[ -f "${STATE}/installed_alpha" && ! -f "${STATE}/installed_beta" ]]; then + pass "partial failure fixture stopped between formula installs" +else + echo "Expected only alpha to be installed by the partial-failure fixture" >"${LAST_OUTPUT}" + fail "partial failure fixture stopped between formula installs" +fi + +echo "${PASS_COUNT} passed; ${FAIL_COUNT} failed" +[[ ${FAIL_COUNT} -eq 0 ]] diff --git a/scripts/uberenv b/scripts/uberenv index bf2f438f..f42c433b 160000 --- a/scripts/uberenv +++ b/scripts/uberenv @@ -1 +1 @@ -Subproject commit bf2f438fc1fe97bb8290a11e5d4f2d56085b4ee3 +Subproject commit f42c433ba8c0d227efb95d67b7ac67427b5d07fb