From 85fe47a2f0266720b439962d7e1e45f5918476c2 Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Fri, 4 Sep 2026 00:41:19 -0700 Subject: [PATCH 1/7] Update macOS TPL qualification and tool discovery - refresh the qualified Homebrew dependency snapshot - model Homebrew packages as exact Spack externals - select Apple ar and ranlib for macOS builds - keep Homebrew binutils as a run-only addr2line dependency - add validation and documentation for the Homebrew boundary --- .uberenv_config.json | 1 + scripts/setupMacOS-TPL-deps.bash | 507 ++++++++++++++ scripts/spack_configs/macOS/README.md | 127 ++++ .../macOS/homebrew-manifest.json | 196 ++++++ scripts/spack_configs/macOS/spack.yaml | 228 ++++--- .../spack_packages/packages/geosx/package.py | 8 +- .../test_setupMacOS_TPL_deps.bash | 634 ++++++++++++++++++ scripts/uberenv | 2 +- 8 files changed, 1595 insertions(+), 108 deletions(-) create mode 100755 scripts/setupMacOS-TPL-deps.bash create mode 100644 scripts/spack_configs/macOS/README.md create mode 100644 scripts/spack_configs/macOS/homebrew-manifest.json create mode 100755 scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash diff --git a/.uberenv_config.json b/.uberenv_config.json index 27503c20..0b76ec87 100644 --- a/.uberenv_config.json +++ b/.uberenv_config.json @@ -2,6 +2,7 @@ "package_name": "geosx", "package_version": "develop", "package_final_phase": "lvarray_hostconfig", +"package_host_config_pattern": "*-*@*.cmake", "package_source_dir": "../..", "spack_configs_path": "scripts/spack_configs", "spack_packages_path": "scripts/spack_packages/packages", diff --git a/scripts/setupMacOS-TPL-deps.bash b/scripts/setupMacOS-TPL-deps.bash new file mode 100755 index 00000000..edc31df2 --- /dev/null +++ b/scripts/setupMacOS-TPL-deps.bash @@ -0,0 +1,507 @@ +#!/bin/bash + +# Validate and, when necessary, install the exact Homebrew dependencies used by +# the supported macOS TPL configuration. Homebrew itself and required taps 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 + +# These overrides keep the production paths explicit while allowing the shell +# tests to inject deterministic stand-ins. +BREW_BIN=${GEOS_TPL_BREW_BIN:-/opt/homebrew/bin/brew} +PLUTIL_BIN=${GEOS_TPL_PLUTIL_BIN:-/usr/bin/plutil} +GIT_BIN=${GEOS_TPL_GIT_BIN:-/usr/bin/git} +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 + +declare -a TAP_NAMES=() +declare -a TAP_REMOTES=() +declare -a FORMULA_NAMES=() +declare -a FORMULA_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 exact macOS and Homebrew dependency set recorded in the checked-in +manifest. By default, missing formulas are installed only after the complete +preflight succeeds. Existing version drift is never upgraded or downgraded. + +Options: + --check-only Validate without installing anything. + --manifest PATH Use an alternate manifest (primarily for testing). + -h, --help Show this help text. + +Prerequisite: + brew tap geos-dev/geos +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 +} + +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 +} + +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 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "Unknown argument '$1'. Use --help for usage." + ;; + esac + done +} + +validate_manifest() +{ + local schema_version tap_count formula_count spack_built_count + local i j name remote version prefix sha 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 + + tap_count=$(manifest_get taps) + is_nonnegative_integer "${tap_count}" || die "Manifest 'taps' must be an array" + [[ "${tap_count}" -gt 0 ]] || die "Manifest must declare at least one required tap" + i=0 + while [[ ${i} -lt ${tap_count} ]]; do + name=$(manifest_get "taps.${i}.name") + remote=$(manifest_get "taps.${i}.remote") + [[ -n "${name}" && -n "${remote}" ]] || die "Tap ${i} has an empty name or remote" + TAP_NAMES[i]=${name} + TAP_REMOTES[i]=${remote} + i=$((i + 1)) + done + + 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") + 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_PREFIXES[i]=${prefix} + FORMULA_SHA256S[i]=${sha} + 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 actual_brew_prefix macos_version macos_major sdk_version sdk_major + + [[ -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 "${GIT_BIN}" ]] || die "Required Git executable is missing: ${GIT_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)" + macos_version=$("${SW_VERS_BIN}" -productVersion) + macos_major=${macos_version%%.*} + require_equal "macOS major version" "$(manifest_get supported_platform.macos_major)" "${macos_major}" + sdk_version=$("${XCRUN_BIN}" --show-sdk-version) + sdk_major=${sdk_version%%.*} + require_equal "macOS SDK major version" "$(manifest_get supported_platform.sdk_major)" "${sdk_major}" + + 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'" + require_equal "Apple Clang version" "$(manifest_get supported_platform.apple_clang_version)" "${clang_version}" + + 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 ! actual_brew_prefix=$("${BREW_BIN}" --prefix); then + record_error "Homebrew could not report its prefix" + else + require_equal "Homebrew prefix" "$(manifest_get supported_platform.homebrew_prefix)" "${actual_brew_prefix}" + fi + + echo "Host details: macOS ${macos_version} ($("${SW_VERS_BIN}" -buildVersion)), SDK ${sdk_version}, Apple Clang build ${clang_build}, Homebrew ${brew_version}" + + [[ ${ERROR_COUNT} -eq 0 ]] || die "Platform preflight failed with ${ERROR_COUNT} error(s); no formulas were installed" +} + +validate_taps() +{ + local tap_output i name expected_remote repo actual_remote + + if ! tap_output=$("${BREW_BIN}" tap); then + die "Homebrew could not list installed taps" + fi + + i=0 + while [[ ${i} -lt ${#TAP_NAMES[@]} ]]; do + name=${TAP_NAMES[${i}]} + expected_remote=${TAP_REMOTES[${i}]} + if ! printf '%s\n' "${tap_output}" | grep -F -x -q -- "${name}"; then + record_error "Required tap '${name}' is absent; run: brew tap ${name}" + i=$((i + 1)) + continue + fi + if ! repo=$("${BREW_BIN}" --repository "${name}"); then + record_error "Homebrew could not locate required tap '${name}'" + i=$((i + 1)) + continue + fi + if [[ ! -d "${repo}" ]]; then + record_error "Tap '${name}' repository does not exist at '${repo}'" + i=$((i + 1)) + continue + fi + if ! actual_remote=$("${GIT_BIN}" -C "${repo}" remote get-url origin); then + record_error "Could not inspect Git remote for tap '${name}'" + i=$((i + 1)) + continue + fi + require_equal "tap '${name}' remote" "${expected_remote}" "${actual_remote}" + i=$((i + 1)) + done + + [[ ${ERROR_COUNT} -eq 0 ]] || die "Tap preflight failed with ${ERROR_COUNT} error(s); no formulas were installed" +} + +formula_metadata_matches() +{ + local index=$1 + local name expected_version expected_sha 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}]} + 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 + + 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 + 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]}" +} + +validate_formula_installation() +{ + local index=$1 + local allow_missing=$2 + local name expected_version expected_prefix actual_version actual_prefix + local path_count j relative_path + + name=${FORMULA_NAMES[${index}]} + expected_version=${FORMULA_VERSIONS[${index}]} + expected_prefix=${FORMULA_PREFIXES[${index}]} + + if ! actual_version=$(installed_formula_version "${name}"); then + if [[ "${allow_missing}" == "true" ]]; then + MISSING_FORMULAS[${#MISSING_FORMULAS[@]}]=${name} + echo "MISSING: ${name}@${expected_version}" + return 0 + fi + record_error "Formula '${name}@${expected_version}' is still missing after installation" + return 1 + fi + if [[ "${actual_version}" == "__AMBIGUOUS__" ]]; then + record_error "Formula '${name}' has multiple installed versions" + return 1 + fi + if [[ "${actual_version}" != "${expected_version}" ]]; then + record_error "Formula '${name}' receipt drift: expected '${expected_version}', found '${actual_version}'" + return 1 + fi + + 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 + + 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})" + return 0 +} + +preflight_formulae() +{ + local i + MISSING_FORMULAS=() + i=0 + while [[ ${i} -lt ${#FORMULA_NAMES[@]} ]]; do + # Check source identity even when the formula is 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 + 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 + validate_taps + 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 exact preflighted formulas: ${MISSING_FORMULAS[*]}" + if ! "${BREW_BIN}" install "${MISSING_FORMULAS[@]}"; then + die "Homebrew failed while installing the preflighted formula set" + fi + fi + + revalidate_formulae + 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..b15399b8 --- /dev/null +++ b/scripts/spack_configs/macOS/README.md @@ -0,0 +1,127 @@ +# macOS Homebrew prerequisites + +The macOS TPL build uses a deliberately narrow Homebrew boundary. Homebrew +provides the C/Fortran toolchain, MPI, BLAS, CMake, Python, and selected build +tools listed in [`homebrew-manifest.json`](homebrew-manifest.json). Perl, +diffutils, and zlib are intentionally absent from the Homebrew list and are +built by Spack. + +The setup is fail-closed. It does not install Homebrew, update Homebrew, upgrade +or downgrade an installed formula, add taps, or silently accept a newer formula +definition. + +## One-time prerequisite + +Install Homebrew for Apple Silicon at `/opt/homebrew`, then add the GEOS tap: + +```console +brew tap geos-dev/geos +``` + +The dependency script verifies that this tap already exists, that its `origin` +remote is exactly `https://github.com/GEOS-DEV/homebrew-geos`, and that the +versioned CMake formula has the source checksum recorded in the manifest. It +never adds or rewrites the tap itself. + +## 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: + +```console +scripts/setupMacOS-TPL-deps.bash +``` + +Then run the TPL build as a separate step. The canonical invocation for this +configuration is: + +```console +scripts/setupMacOS-TPL-deps.bash +scripts/uberenv/uberenv.py \ + --spack-env-file=scripts/spack_configs/macOS/spack.yaml \ + --prefix=/absolute/path/to/geos-tpls \ + --spec="%c,cxx=apple-clang@17.0.0 %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, the macOS and SDK major versions match the support + contract, Apple Clang is the required upstream version, and Homebrew uses the + declared prefix; +- every required tap and tap remote matches; +- Homebrew reports the exact stable formula version, formula revision, and Ruby + source checksum 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. 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 Spack compiler environment 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. + +## 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. Either: + +1. add a versioned formula to the GEOS tap, or +2. qualify the newer dependency set with a clean TPL build and update the + manifest and macOS Spack configuration together. + +The checked-in 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, 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 each formula's package version (`stable`, plus `_` when the + formula revision is nonzero) and `ruby_source_checksum.sha256` from + `brew info --json=v2` or the official formula API; +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 Perl, diffutils, and zlib are non-external; and +6. compile and run MPI, BLAS, and zlib smoke tests before calling the new set + qualified. + +The dependency script only prepares and validates Homebrew. 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..2301c62d --- /dev/null +++ b/scripts/spack_configs/macOS/homebrew-manifest.json @@ -0,0 +1,196 @@ +{ + "schema_version": 1, + "source": { + "description": "Official Homebrew formula API metadata 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" + }, + "taps": [ + { + "name": "geos-dev/geos", + "remote": "https://github.com/GEOS-DEV/homebrew-geos" + } + ], + "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", + "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": "geos-dev/geos/cmake@3.31.6", + "brew_version": "3.31.6", + "formula_sha256": "83150ce2f83038812850ef40904b9cc67d468f2067809a822f23be4189167712", + "prefix": "/opt/homebrew/opt/cmake@3.31.6", + "spack_package": "cmake", + "spack_version": "3.31.6", + "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" + ] + } + ], + "spack_built": [ + { + "package": "perl", + "version": "5.42.2" + }, + { + "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..f6038471 100644 --- a/scripts/spack_configs/macOS/spack.yaml +++ b/scripts/spack_configs/macOS/spack.yaml @@ -17,146 +17,168 @@ 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" + # 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@=17.0.0" + cxx: + require: "apple-clang@=17.0.0" + fortran: + require: "gcc@=16.2.0 languages:=fortran" + + apple-clang: + buildable: false + externals: + - spec: "apple-clang@=17.0.0 platform=darwin os=tahoe target=aarch64" + prefix: /usr + extra_attributes: + compilers: + c: /usr/bin/clang + cxx: /usr/bin/clang++ + 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 + + gcc: + buildable: false + externals: + - spec: "gcc@=16.2.0 languages:=fortran platform=darwin os=tahoe 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. 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" + + # These tools/libraries are intentionally Spack-built rather than modeled + # as nonexistent Homebrew kegs. + zlib: + buildable: true + require: "@=1.3.2 +pic +shared" + perl: + buildable: true + require: "@=5.42.2" + 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 os=tahoe 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 os=tahoe target=aarch64 + prefix: /opt/homebrew/opt/open-mpi - # System level packages to not build + # Homebrew system packages. Perl, diffutils, and zlib are intentionally not + # external: Spack builds those dependencies for a complete, valid prefix. cmake: - version: [3.29.6] buildable: false externals: - - spec: cmake@3.29.6 - prefix: /opt/homebrew/opt/cmake + - spec: "cmake@=3.31.6 platform=darwin os=tahoe target=aarch64" + prefix: /opt/homebrew/opt/cmake@3.31.6 readline: buildable: false externals: - - spec: readline@8.2.13 + - spec: "readline@=8.3 platform=darwin os=tahoe 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 os=tahoe target=aarch64" + prefix: /opt/homebrew/opt/m4 + pkgconf: buildable: false externals: - - spec: pkg-config@2.3.0 + - spec: "pkgconf@=3.0.6 platform=darwin os=tahoe 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 os=tahoe 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 os=tahoe 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 os=tahoe 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 os=tahoe 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 os=tahoe 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 os=tahoe 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..38bfac91 100644 --- a/scripts/spack_packages/packages/geosx/package.py +++ b/scripts/spack_packages/packages/geosx/package.py @@ -77,7 +77,7 @@ class Geosx(CMakePackage, CudaPackage, ROCmPackage): # variant('examples', default=False, description='Build examples') variant('docs', default=False, description='Build docs') - variant('addr2line', default=True, + variant('addr2line', default=False, description='Add support for addr2line.') variant('mathpresso', default=True, description='Build mathpresso.') @@ -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..a3aece86 --- /dev/null +++ b/scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash @@ -0,0 +1,634 @@ +#!/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, +# tap, 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}/git" <<'EOF' +#!/bin/bash +if [[ "${1:-}" == "-C" && "${3:-}" == "remote" && "${4:-}" == "get-url" && "${5:-}" == "origin" ]]; then + cat "${FAKE_BREW_STATE}/tap_remote" +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 + ;; + --repository) + [[ "${2:-}" == "geos-dev/geos" ]] || exit 1 + cat "${state}/tap_repo" + ;; + tap) + if [[ -f "${state}/tap_present" ]]; then + echo "geos-dev/geos" + 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}/git" "${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" + printf 'https://example.invalid/geos\n' >"${STATE}/tap_remote" + mkdir -p "${STATE}/tap-repo" + printf '%s\n' "${STATE}/tap-repo" >"${STATE}/tap_repo" + : >"${STATE}/tap_present" + set_candidate alpha 1.0 0 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + set_candidate beta 2.0 1 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + install_fixture alpha 1.0 alpha-tool + install_fixture beta 2.0_1 beta-tool +} + +run_setup() +{ + FAKE_BREW_STATE="${STATE}" \ + FAKE_HOMEBREW_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_GIT_BIN="${FAKE_BIN}/git" \ + 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}" != "13" ]]; then + echo "Expected 13 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 + perl|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}" == "3" ]]; then + pass "committed manifest schema and hybrid boundary" + else + echo "Expected 3 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 "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 +} + +create_fake_tools +write_manifest +test_committed_manifest + +# Patch/build releases newer than the recorded qualification host are accepted +# as long as the supported major versions still match. +reset_state +expect_success "supported macOS/SDK major 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 + +reset_state +printf '27.0\n' >"${STATE}/macos_version" +expect_failure "unsupported macOS major is rejected" run_setup --check-only +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "macOS rejection performs no install" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "macOS rejection 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_failure "unsupported SDK major is rejected" run_setup --check-only +if ! grep -q '^install ' "${STATE}/commands.log"; then + pass "SDK rejection performs no install" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "SDK rejection performs no install" +fi + +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}/tap_present" +expect_failure "missing GEOS tap is rejected without auto-tapping" run_setup +if ! grep -E -q '^tap .+' "${STATE}/commands.log"; then + pass "script never invokes brew tap with an argument" +else + cp "${STATE}/commands.log" "${LAST_OUTPUT}" + fail "script never invokes brew tap with an argument" +fi + +reset_state +printf 'https://example.invalid/wrong\n' >"${STATE}/tap_remote" +expect_failure "GEOS tap remote drift 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..75fb5b9c 160000 --- a/scripts/uberenv +++ b/scripts/uberenv @@ -1 +1 @@ -Subproject commit bf2f438fc1fe97bb8290a11e5d4f2d56085b4ee3 +Subproject commit 75fb5b9c24b7c14d1e634dbd14fb0089dfa265b0 From 08d448a935d1137ce451f3a64749b623c3a6098a Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Fri, 4 Sep 2026 00:54:14 -0700 Subject: [PATCH 2/7] undo unrequired changes --- scripts/spack_packages/packages/geosx/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/spack_packages/packages/geosx/package.py b/scripts/spack_packages/packages/geosx/package.py index 38bfac91..533c7b63 100644 --- a/scripts/spack_packages/packages/geosx/package.py +++ b/scripts/spack_packages/packages/geosx/package.py @@ -77,7 +77,7 @@ class Geosx(CMakePackage, CudaPackage, ROCmPackage): # variant('examples', default=False, description='Build examples') variant('docs', default=False, description='Build docs') - variant('addr2line', default=False, + variant('addr2line', default=True, description='Add support for addr2line.') variant('mathpresso', default=True, description='Build mathpresso.') From 38b7479113427258207684baa90e01d4fe3cf2af Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Fri, 4 Sep 2026 08:36:59 -0700 Subject: [PATCH 3/7] Update Uberenv compiler mixing policy --- scripts/uberenv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/uberenv b/scripts/uberenv index 75fb5b9c..9a367469 160000 --- a/scripts/uberenv +++ b/scripts/uberenv @@ -1 +1 @@ -Subproject commit 75fb5b9c24b7c14d1e634dbd14fb0089dfa265b0 +Subproject commit 9a3674691089fea2348eb124de12ab9b704909f8 From a47ded148355454755821eb963465a7b3e814e19 Mon Sep 17 00:00:00 2001 From: "Victor A. P. Magri" Date: Fri, 4 Sep 2026 14:48:39 -0400 Subject: [PATCH 4/7] Several fixes --- scripts/setupMacOS-TPL-deps.bash | 497 ++++++++++++++---- scripts/spack_configs/macOS/README.md | 115 ++-- .../macOS/homebrew-manifest.json | 37 +- scripts/spack_configs/macOS/spack.yaml | 89 ++-- .../test_setupMacOS_TPL_deps.bash | 224 +++++--- 5 files changed, 694 insertions(+), 268 deletions(-) diff --git a/scripts/setupMacOS-TPL-deps.bash b/scripts/setupMacOS-TPL-deps.bash index edc31df2..cf2e3fd6 100755 --- a/scripts/setupMacOS-TPL-deps.bash +++ b/scripts/setupMacOS-TPL-deps.bash @@ -1,8 +1,8 @@ #!/bin/bash -# Validate and, when necessary, install the exact Homebrew dependencies used by -# the supported macOS TPL configuration. Homebrew itself and required taps must -# already exist. This script intentionally never updates or upgrades Homebrew. +# 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 @@ -11,12 +11,13 @@ 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 keep the production paths explicit while allowing the shell -# tests to inject deterministic stand-ins. -BREW_BIN=${GEOS_TPL_BREW_BIN:-/opt/homebrew/bin/brew} +# 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} -GIT_BIN=${GEOS_TPL_GIT_BIN:-/usr/bin/git} 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} @@ -24,11 +25,25 @@ 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 TAP_NAMES=() -declare -a TAP_REMOTES=() 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=() @@ -38,17 +53,21 @@ usage() cat <<'EOF' Usage: scripts/setupMacOS-TPL-deps.bash [options] -Validate the exact macOS and Homebrew dependency set recorded in the checked-in -manifest. By default, missing formulas are installed only after the complete -preflight succeeds. Existing version drift is never upgraded or downgraded. +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. - -Prerequisite: - brew tap geos-dev/geos EOF } @@ -92,6 +111,24 @@ is_sha256() 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 @@ -112,6 +149,149 @@ require_equal() 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 @@ -125,6 +305,16 @@ parse_args() 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 @@ -138,8 +328,9 @@ parse_args() validate_manifest() { - local schema_version tap_count formula_count spack_built_count - local i j name remote version prefix sha path_count relative_path seen + 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}" @@ -161,19 +352,6 @@ validate_manifest() manifest_get qualification_host.sdk_version >/dev/null manifest_get qualification_host.homebrew_version >/dev/null - tap_count=$(manifest_get taps) - is_nonnegative_integer "${tap_count}" || die "Manifest 'taps' must be an array" - [[ "${tap_count}" -gt 0 ]] || die "Manifest must declare at least one required tap" - i=0 - while [[ ${i} -lt ${tap_count} ]]; do - name=$(manifest_get "taps.${i}.name") - remote=$(manifest_get "taps.${i}.remote") - [[ -n "${name}" && -n "${remote}" ]] || die "Tap ${i} has an empty name or remote" - TAP_NAMES[i]=${name} - TAP_REMOTES[i]=${remote} - i=$((i + 1)) - done - 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" @@ -181,6 +359,24 @@ validate_manifest() 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") @@ -219,8 +415,15 @@ validate_manifest() 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 @@ -238,92 +441,68 @@ validate_manifest() validate_platform() { local clang_output clang_version clang_build brew_output brew_version - local actual_brew_prefix macos_version macos_major sdk_version sdk_major + 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 "${GIT_BIN}" ]] || die "Required Git executable is missing: ${GIT_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)" - macos_version=$("${SW_VERS_BIN}" -productVersion) - macos_major=${macos_version%%.*} - require_equal "macOS major version" "$(manifest_get supported_platform.macos_major)" "${macos_major}" - sdk_version=$("${XCRUN_BIN}" --show-sdk-version) - sdk_major=${sdk_version%%.*} - require_equal "macOS SDK major version" "$(manifest_get supported_platform.sdk_major)" "${sdk_major}" + 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'" - require_equal "Apple Clang version" "$(manifest_get supported_platform.apple_clang_version)" "${clang_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 ! actual_brew_prefix=$("${BREW_BIN}" --prefix); then + if ! HOST_BREW_PREFIX=$("${BREW_BIN}" --prefix); then record_error "Homebrew could not report its prefix" else - require_equal "Homebrew prefix" "$(manifest_get supported_platform.homebrew_prefix)" "${actual_brew_prefix}" + 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 ${macos_version} ($("${SW_VERS_BIN}" -buildVersion)), SDK ${sdk_version}, Apple Clang build ${clang_build}, Homebrew ${brew_version}" + 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" } -validate_taps() -{ - local tap_output i name expected_remote repo actual_remote - - if ! tap_output=$("${BREW_BIN}" tap); then - die "Homebrew could not list installed taps" - fi - - i=0 - while [[ ${i} -lt ${#TAP_NAMES[@]} ]]; do - name=${TAP_NAMES[${i}]} - expected_remote=${TAP_REMOTES[${i}]} - if ! printf '%s\n' "${tap_output}" | grep -F -x -q -- "${name}"; then - record_error "Required tap '${name}' is absent; run: brew tap ${name}" - i=$((i + 1)) - continue - fi - if ! repo=$("${BREW_BIN}" --repository "${name}"); then - record_error "Homebrew could not locate required tap '${name}'" - i=$((i + 1)) - continue - fi - if [[ ! -d "${repo}" ]]; then - record_error "Tap '${name}' repository does not exist at '${repo}'" - i=$((i + 1)) - continue - fi - if ! actual_remote=$("${GIT_BIN}" -C "${repo}" remote get-url origin); then - record_error "Could not inspect Git remote for tap '${name}'" - i=$((i + 1)) - continue - fi - require_equal "tap '${name}' remote" "${expected_remote}" "${actual_remote}" - i=$((i + 1)) - done - - [[ ${ERROR_COUNT} -eq 0 ]] || die "Tap preflight failed with ${ERROR_COUNT} error(s); no formulas were installed" -} - formula_metadata_matches() { local index=$1 - local name expected_version expected_sha info_file formula_count + 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 @@ -361,14 +540,26 @@ formula_metadata_matches() actual_version="${stable}_${revision}" fi - 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 + 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 } @@ -391,34 +582,106 @@ installed_formula_version() 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 path_count j relative_path + local policy minimum_version + local path_count j relative_path role_suffix name=${FORMULA_NAMES[${index}]} expected_version=${FORMULA_VERSIONS[${index}]} - expected_prefix=${FORMULA_PREFIXES[${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: ${name}@${expected_version}" + echo "MISSING: $(formula_requirement "${index}")" return 0 fi - record_error "Formula '${name}@${expected_version}' is still missing after installation" + 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 - if [[ "${actual_version}" != "${expected_version}" ]]; then - record_error "Formula '${name}' receipt drift: expected '${expected_version}', found '${actual_version}'" - 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}'" @@ -429,6 +692,18 @@ validate_formula_installation() 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 @@ -438,7 +713,7 @@ validate_formula_installation() fi j=$((j + 1)) done - echo "OK: ${name}@${actual_version} (${actual_prefix})" + echo "OK: ${name}@${actual_version} (${actual_prefix})${role_suffix}" return 0 } @@ -448,8 +723,13 @@ preflight_formulae() MISSING_FORMULAS=() i=0 while [[ ${i} -lt ${#FORMULA_NAMES[@]} ]]; do - # Check source identity even when the formula is already installed. This - # makes stale API caches and silently rewritten formulas visible drift. + 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)) @@ -463,6 +743,10 @@ revalidate_formulae() 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)) @@ -487,20 +771,27 @@ main() export HOMEBREW_NO_ENV_HINTS=1 validate_platform - validate_taps + 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 exact preflighted formulas: ${MISSING_FORMULAS[*]}" + 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." } diff --git a/scripts/spack_configs/macOS/README.md b/scripts/spack_configs/macOS/README.md index b15399b8..14af9b0b 100644 --- a/scripts/spack_configs/macOS/README.md +++ b/scripts/spack_configs/macOS/README.md @@ -1,27 +1,34 @@ # macOS Homebrew prerequisites The macOS TPL build uses a deliberately narrow Homebrew boundary. Homebrew -provides the C/Fortran toolchain, MPI, BLAS, CMake, Python, and selected build -tools listed in [`homebrew-manifest.json`](homebrew-manifest.json). Perl, -diffutils, and zlib are intentionally absent from the Homebrew list and are +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. It does not install Homebrew, update Homebrew, upgrade -or downgrade an installed formula, add taps, or silently accept a newer formula -definition. +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 Apple Silicon at `/opt/homebrew`, then add the GEOS tap: +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 -brew tap geos-dev/geos +eval "$(brew shellenv)" ``` -The dependency script verifies that this tap already exists, that its `origin` -remote is exactly `https://github.com/GEOS-DEV/homebrew-geos`, and that the -versioned CMake formula has the source checksum recorded in the manifest. It -never adds or rewrites the tap itself. +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 @@ -31,21 +38,32 @@ From the `thirdPartyLibs` repository root, audit without changing anything: scripts/setupMacOS-TPL-deps.bash --check-only ``` -To install formulas that are missing: +To install formulas that are missing and write a host-specific Spack +environment file: ```console -scripts/setupMacOS-TPL-deps.bash +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/setupMacOS-TPL-deps.bash scripts/uberenv/uberenv.py \ - --spack-env-file=scripts/spack_configs/macOS/spack.yaml \ + --spack-env-file="${SPACK_CONFIG}" \ --prefix=/absolute/path/to/geos-tpls \ - --spec="%c,cxx=apple-clang@17.0.0 %fortran=gcc@16.2.0" + --spec="%c,cxx=apple-clang %fortran=gcc@16.2.0" ``` Use another absolute `--prefix` when the TPL installation belongs elsewhere; @@ -54,49 +72,54 @@ dependency set. Installation occurs only when all of the following preflight checks pass: -- the host is Darwin/arm64, the macOS and SDK major versions match the support - contract, Apple Clang is the required upstream version, and Homebrew uses the - declared prefix; -- every required tap and tap remote matches; +- 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 in the manifest; + 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. The complete set is then checked again, including metadata, -receipts, prefixes, and required paths. A partial or changed installation is an -error. +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 Spack compiler environment 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. +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. Either: - -1. add a versioned formula to the GEOS tap, or -2. qualify the newer dependency set with a clean TPL build and update the - manifest and macOS Spack configuration together. +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 formula pins and source checksums come from the official +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, and Homebrew executable patch releases are informational rather than +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. @@ -105,22 +128,24 @@ compatibility guard for this boundary. Treat a manifest change as a toolchain change: -1. obtain each formula's package version (`stable`, plus `_` when the +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; + `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 Perl, diffutils, and zlib are non-external; and + 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 only prepares and validates Homebrew. 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: +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 index 2301c62d..c1dfddcc 100644 --- a/scripts/spack_configs/macOS/homebrew-manifest.json +++ b/scripts/spack_configs/macOS/homebrew-manifest.json @@ -1,7 +1,7 @@ { "schema_version": 1, "source": { - "description": "Official Homebrew formula API metadata selected for GEOS macOS TPL qualification", + "description": "Official Homebrew formula API metadata and version policies selected for GEOS macOS TPL qualification", "as_of": "2026-09-04" }, "supported_platform": { @@ -19,12 +19,6 @@ "sdk_version": "26.2", "homebrew_version": "6.0.21" }, - "taps": [ - { - "name": "geos-dev/geos", - "remote": "https://github.com/GEOS-DEV/homebrew-geos" - } - ], "formulae": [ { "name": "gcc", @@ -51,6 +45,7 @@ }, { "name": "open-mpi", + "version_policy": "any", "brew_version": "5.0.10", "formula_sha256": "c642ed42caecd2f2f26aa4232fea968583408fa54097f79acf0c9a30a299c82a", "prefix": "/opt/homebrew/opt/open-mpi", @@ -64,12 +59,14 @@ ] }, { - "name": "geos-dev/geos/cmake@3.31.6", - "brew_version": "3.31.6", - "formula_sha256": "83150ce2f83038812850ef40904b9cc67d468f2067809a822f23be4189167712", - "prefix": "/opt/homebrew/opt/cmake@3.31.6", + "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": "3.31.6", + "spack_version": "4.4.3", "required_paths": [ "bin/cmake" ] @@ -176,13 +173,21 @@ "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": "perl", - "version": "5.42.2" - }, { "package": "diffutils", "version": "3.12" diff --git a/scripts/spack_configs/macOS/spack.yaml b/scripts/spack_configs/macOS/spack.yaml index f6038471..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 @@ -21,40 +25,28 @@ spack: all: target: [aarch64] 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@=17.0.0" + require: "apple-clang" cxx: - require: "apple-clang@=17.0.0" + require: "apple-clang" fortran: require: "gcc@=16.2.0 languages:=fortran" apple-clang: buildable: false - externals: - - spec: "apple-clang@=17.0.0 platform=darwin os=tahoe target=aarch64" - prefix: /usr - extra_attributes: - compilers: - c: /usr/bin/clang - cxx: /usr/bin/clang++ - 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 + # GEOS_TPL_APPLE_CLANG_EXTERNAL gcc: buildable: false externals: - - spec: "gcc@=16.2.0 languages:=fortran platform=darwin os=tahoe target=aarch64" + - spec: "gcc@=16.2.0 languages:=fortran platform=darwin target=aarch64" prefix: /opt/homebrew/opt/gcc extra_attributes: compilers: @@ -73,8 +65,10 @@ spack: geosx: require: "+addr2line ~openmp" - # Lock virtual dependencies to the matching Homebrew externals. zlib-api - # remains buildable so Spack supplies zlib instead of a nonexistent keg. + # 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" @@ -90,14 +84,16 @@ spack: zlib-api: require: "zlib@=1.3.2" - # These tools/libraries are intentionally Spack-built rather than modeled - # as nonexistent Homebrew kegs. + # 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" - perl: - buildable: true - require: "@=5.42.2" diffutils: buildable: true require: "@=3.12" @@ -110,7 +106,7 @@ spack: +fortran ~ilp64 +pic +shared +static +dynamic_dispatch +locking ~bignuma ~consistent_fpcsr threads=openmp max_num_threads=56 symbol_suffix=none - platform=darwin os=tahoe target=aarch64 + platform=darwin target=aarch64 prefix: /opt/homebrew/opt/openblas openmpi: @@ -121,52 +117,61 @@ spack: +fortran +ipv6 +romio ~internal-hwloc ~internal-libevent ~internal-pmix ~static fabrics:=none schedulers:=sge - platform=darwin os=tahoe target=aarch64 + platform=darwin target=aarch64 prefix: /opt/homebrew/opt/open-mpi - # Homebrew system packages. Perl, diffutils, and zlib are intentionally not - # external: Spack builds those dependencies for a complete, valid prefix. + # 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: buildable: false + require: "@3.24:" externals: - - spec: "cmake@=3.31.6 platform=darwin os=tahoe target=aarch64" - prefix: /opt/homebrew/opt/cmake@3.31.6 + - spec: "cmake@=4.4.3 platform=darwin target=aarch64" + prefix: /opt/homebrew/opt/cmake readline: buildable: false externals: - - spec: "readline@=8.3 platform=darwin os=tahoe target=aarch64" + - spec: "readline@=8.3 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/readline m4: buildable: false externals: - - spec: "m4@=1.4.21 platform=darwin os=tahoe target=aarch64" + - spec: "m4@=1.4.21 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/m4 pkgconf: buildable: false externals: - - spec: "pkgconf@=3.0.6 platform=darwin os=tahoe target=aarch64" + - spec: "pkgconf@=3.0.6 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/pkgconf autoconf: buildable: false externals: - - spec: "autoconf@=2.73 platform=darwin os=tahoe target=aarch64" + - spec: "autoconf@=2.73 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/autoconf automake: buildable: false externals: - - spec: "automake@=1.18.1 platform=darwin os=tahoe target=aarch64" + - spec: "automake@=1.18.1 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/automake libtool: buildable: false externals: - - spec: "libtool@=2.6.2 platform=darwin os=tahoe target=aarch64" + - spec: "libtool@=2.6.2 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/libtool gettext: buildable: false externals: - - spec: "gettext@=1.0 platform=darwin os=tahoe target=aarch64" + - spec: "gettext@=1.0 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/gettext binutils: @@ -174,11 +179,11 @@ spack: externals: - spec: >- binutils@=2.47 libs:=static - platform=darwin os=tahoe target=aarch64 + platform=darwin target=aarch64 prefix: /opt/homebrew/opt/binutils python: buildable: false externals: - - spec: "python@=3.14.7 platform=darwin os=tahoe target=aarch64" + - spec: "python@=3.14.7 platform=darwin target=aarch64" prefix: /opt/homebrew/opt/python@3.14 diff --git a/scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash b/scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash index a3aece86..b3a40479 100755 --- a/scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash +++ b/scripts/tests/macos_homebrew/test_setupMacOS_TPL_deps.bash @@ -1,8 +1,8 @@ #!/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, -# tap, or Homebrew state is changed. +# script runs against a fake Homebrew and fake platform tools; no real formula +# or Homebrew state is changed. set -u @@ -178,15 +178,6 @@ if [[ "${1:-}" == "--version" ]]; then else exit 2 fi -EOF - - cat >"${FAKE_BIN}/git" <<'EOF' -#!/bin/bash -if [[ "${1:-}" == "-C" && "${3:-}" == "remote" && "${4:-}" == "get-url" && "${5:-}" == "origin" ]]; then - cat "${FAKE_BREW_STATE}/tap_remote" -else - exit 2 -fi EOF cat >"${FAKE_BIN}/brew" <<'EOF' @@ -213,15 +204,6 @@ case "${1:-}" in echo "${prefix}/opt/$2" fi ;; - --repository) - [[ "${2:-}" == "geos-dev/geos" ]] || exit 1 - cat "${state}/tap_repo" - ;; - tap) - if [[ -f "${state}/tap_present" ]]; then - echo "geos-dev/geos" - fi - ;; info) [[ "${2:-}" == "--json=v2" ]] || exit 2 formula=${3:?} @@ -278,7 +260,7 @@ esac EOF chmod +x "${FAKE_BIN}/uname" "${FAKE_BIN}/sw_vers" "${FAKE_BIN}/xcrun" \ - "${FAKE_BIN}/clang" "${FAKE_BIN}/git" "${FAKE_BIN}/brew" + "${FAKE_BIN}/clang" "${FAKE_BIN}/brew" } write_manifest() @@ -301,12 +283,6 @@ write_manifest() "sdk_version": "26.2", "homebrew_version": "6.0.12" }, - "taps": [ - { - "name": "geos-dev/geos", - "remote": "https://example.invalid/geos" - } - ], "formulae": [ { "name": "alpha", @@ -325,6 +301,37 @@ write_manifest() "spack_package": "beta", "spack_version": "2.0", "required_paths": ["bin/beta-tool"] + }, + { + "name": "cmake", + "version_policy": "minimum", + "minimum_version": "3.24", + "brew_version": "4.4.3", + "formula_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "prefix": "${FAKE_PREFIX}/opt/cmake", + "spack_package": "cmake", + "spack_version": "4.4.3", + "required_paths": ["bin/cmake-tool"] + }, + { + "name": "open-mpi", + "version_policy": "any", + "brew_version": "5.0.10", + "formula_sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "prefix": "${FAKE_PREFIX}/opt/open-mpi", + "spack_package": "openmpi", + "spack_version": "5.0.10", + "required_paths": ["bin/open-mpi-tool"] + }, + { + "name": "perl", + "version_policy": "any", + "brew_version": "5.44.0", + "formula_sha256": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "prefix": "${FAKE_PREFIX}/opt/perl", + "spack_package": "perl", + "spack_version": "5.44.0", + "required_paths": ["bin/perl-tool"] } ], "spack_built": [ @@ -337,7 +344,9 @@ EOF reset_state() { rm -f "${STATE}"/installed_* "${STATE}/commands.log" - rm -rf -- "${FAKE_PREFIX}/opt/alpha" "${FAKE_PREFIX}/opt/beta" + rm -rf -- "${FAKE_PREFIX}/opt/alpha" "${FAKE_PREFIX}/opt/beta" \ + "${FAKE_PREFIX}/opt/cmake" "${FAKE_PREFIX}/opt/open-mpi" \ + "${FAKE_PREFIX}/opt/mpich" "${FAKE_PREFIX}/opt/perl" printf 'Darwin\n' >"${STATE}/os" printf 'arm64\n' >"${STATE}/arch" printf '26.99.7\n' >"${STATE}/macos_version" @@ -346,24 +355,25 @@ reset_state() printf '17.0.0\n' >"${STATE}/clang_version" printf '1700.99.1\n' >"${STATE}/clang_build" printf '99.7.3\n' >"${STATE}/brew_version" - printf 'https://example.invalid/geos\n' >"${STATE}/tap_remote" - mkdir -p "${STATE}/tap-repo" - printf '%s\n' "${STATE}/tap-repo" >"${STATE}/tap_repo" - : >"${STATE}/tap_present" 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_PREFIX}" \ + 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_GIT_BIN="${FAKE_BIN}/git" \ GEOS_TPL_UNAME_BIN="${FAKE_BIN}/uname" \ GEOS_TPL_SW_VERS_BIN="${FAKE_BIN}/sw_vers" \ GEOS_TPL_XCRUN_BIN="${FAKE_BIN}/xcrun" \ @@ -411,8 +421,8 @@ test_committed_manifest() return fi count=$("${PLUTIL}" -extract formulae raw -o - "${REAL_MANIFEST}") - if [[ "${count}" != "13" ]]; then - echo "Expected 13 formulae, found ${count}" >"${LAST_OUTPUT}" + if [[ "${count}" != "14" ]]; then + echo "Expected 14 formulae, found ${count}" >"${LAST_OUTPUT}" fail "committed manifest formula count" return fi @@ -421,7 +431,7 @@ test_committed_manifest() while [[ ${i} -lt ${count} ]]; do name=$("${PLUTIL}" -extract "formulae.${i}.name" raw -o - "${REAL_MANIFEST}") case "${name}" in - perl|diffutils|zlib) found_forbidden=true ;; + diffutils|zlib) found_forbidden=true ;; esac i=$((i + 1)) done @@ -431,10 +441,10 @@ test_committed_manifest() return fi spack_count=$("${PLUTIL}" -extract spack_built raw -o - "${REAL_MANIFEST}") - if [[ "${spack_count}" == "3" ]]; then + if [[ "${spack_count}" == "2" ]]; then pass "committed manifest schema and hybrid boundary" else - echo "Expected 3 Spack-built packages, found ${spack_count}" >"${LAST_OUTPUT}" + echo "Expected 2 Spack-built packages, found ${spack_count}" >"${LAST_OUTPUT}" fail "committed manifest schema and hybrid boundary" fi @@ -486,20 +496,32 @@ test_committed_manifest() 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 -# Patch/build releases newer than the recorded qualification host are accepted -# as long as the supported major versions still match. +# Host releases newer than the recorded qualification host are accepted. The +# Homebrew formula versions and source checksums remain exact. reset_state -expect_success "supported macOS/SDK major versions accept patch drift" run_setup --check-only +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 @@ -510,15 +532,65 @@ 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_failure "unsupported macOS major is rejected" run_setup --check-only +expect_success "newer macOS major is accepted" run_setup --check-only if ! grep -q '^install ' "${STATE}/commands.log"; then - pass "macOS rejection performs no install" + pass "newer macOS check-only performs no install" else cp "${STATE}/commands.log" "${LAST_OUTPUT}" - fail "macOS rejection performs no install" + fail "newer macOS check-only performs no install" fi reset_state @@ -533,14 +605,56 @@ fi reset_state printf '27.0\n' >"${STATE}/sdk_version" -expect_failure "unsupported SDK major is rejected" run_setup --check-only +expect_success "newer SDK major is accepted" run_setup --check-only if ! grep -q '^install ' "${STATE}/commands.log"; then - pass "SDK rejection performs no install" + pass "newer SDK check-only performs no install" else cp "${STATE}/commands.log" "${LAST_OUTPUT}" - fail "SDK rejection performs no install" + 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" @@ -587,20 +701,6 @@ 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}/tap_present" -expect_failure "missing GEOS tap is rejected without auto-tapping" run_setup -if ! grep -E -q '^tap .+' "${STATE}/commands.log"; then - pass "script never invokes brew tap with an argument" -else - cp "${STATE}/commands.log" "${LAST_OUTPUT}" - fail "script never invokes brew tap with an argument" -fi - -reset_state -printf 'https://example.invalid/wrong\n' >"${STATE}/tap_remote" -expect_failure "GEOS tap remote drift is rejected" run_setup - reset_state rm -f "${STATE}/installed_alpha" rm -rf -- "${FAKE_PREFIX}/opt/alpha" From a4d63e12fafc1adee3a1b80b7e5c4cca24757d1e Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Fri, 4 Sep 2026 12:49:17 -0700 Subject: [PATCH 5/7] revert uberenv changes --- .uberenv_config.json | 4 ++-- scripts/uberenv | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.uberenv_config.json b/.uberenv_config.json index 0b76ec87..797fdaa4 100644 --- a/.uberenv_config.json +++ b/.uberenv_config.json @@ -2,7 +2,6 @@ "package_name": "geosx", "package_version": "develop", "package_final_phase": "lvarray_hostconfig", -"package_host_config_pattern": "*-*@*.cmake", "package_source_dir": "../..", "spack_configs_path": "scripts/spack_configs", "spack_packages_path": "scripts/spack_packages/packages", @@ -11,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/uberenv b/scripts/uberenv index 9a367469..f42c433b 160000 --- a/scripts/uberenv +++ b/scripts/uberenv @@ -1 +1 @@ -Subproject commit 9a3674691089fea2348eb124de12ab9b704909f8 +Subproject commit f42c433ba8c0d227efb95d67b7ac67427b5d07fb From 603315caab506ab3050ebde9acfb24a9cd65b04f Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Fri, 4 Sep 2026 20:36:39 -0700 Subject: [PATCH 6/7] update lc tpl scripts --- scripts/setupLC-TPL-uberenv.bash | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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}" "$@" & ;; From 7039c7a41c56e8f6d92cd34e8e3dc3c6aa6db9a8 Mon Sep 17 00:00:00 2001 From: "Victor A. P. Magri" Date: Fri, 11 Sep 2026 09:49:25 -0400 Subject: [PATCH 7/7] Update checkout action --- .github/workflows/docker_build_tpls.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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