From 3eb8603c738becdb0e6b6ea2dd025a9c2ead6694 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Tue, 6 Oct 2026 21:12:51 +0200 Subject: [PATCH 1/6] Stop adding the Homebrew library path when building PETSc on macOS With -L$(brew --prefix)/lib in LDFLAGS, a Homebrew SUNDIALS (7.x) sits ahead of the SUNDIALS 2.5 that PETSc downloads, so configure cannot link CVDense and CVSpgmr and stops with "Downloaded sundials2 could not be used". The system MPI does not need the path: mpicc adds its own -L. LDFLAGS now only carries -Wl,-w, which keeps the macOS 26 linker warnings from failing configure. --- tests/test_get_petsc_ldflags.py | 23 +++++++++++++++++++++++ tools/get_petsc.sh | 11 ++++------- 2 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 tests/test_get_petsc_ldflags.py diff --git a/tests/test_get_petsc_ldflags.py b/tests/test_get_petsc_ldflags.py new file mode 100644 index 00000000..bd2189e3 --- /dev/null +++ b/tests/test_get_petsc_ldflags.py @@ -0,0 +1,23 @@ +"""Tests for the macOS linker flags in ``tools/get_petsc.sh``.""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +pytestmark = [pytest.mark.unit, pytest.mark.timeout(30)] + +SCRIPT = Path(__file__).resolve().parents[1] / 'tools' / 'get_petsc.sh' + + +def test_macos_ldflags_add_no_homebrew_library_path(): + """The macOS LDFLAGS only silence linker warnings and add no library path, so a Homebrew + SUNDIALS 7 cannot shadow the SUNDIALS 2.5 PETSc downloads (configure then misses + CVDense); mpicc already carries the MPI library path.""" + text = SCRIPT.read_text() + assignments = re.findall(r'^\s*ldflags=(.*)$', text, re.M) + assert assignments == ['""', '"-Wl,-w"'] + assert 'brew --prefix' not in text + assert '-L' not in ''.join(assignments) diff --git a/tools/get_petsc.sh b/tools/get_petsc.sh index fd9adffd..f7b8dd82 100755 --- a/tools/get_petsc.sh +++ b/tools/get_petsc.sh @@ -341,13 +341,10 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # macOS provides Accelerate framework with BLAS/LAPACK blas_flag="" - if [[ "$(uname -m)" == "arm64" ]]; then - default_brew_prefix="/opt/homebrew" - else - default_brew_prefix="/usr/local" - fi - brew_prefix="$(brew --prefix 2>/dev/null || echo "$default_brew_prefix")" - ldflags="-L${brew_prefix}/lib -Wl,-w" + # -Wl,-w silences the deprecated-flag warnings macOS 26+ turns into PETSc configure errors. + # No Homebrew -L: mpicc carries its own, and a Homebrew SUNDIALS there would hide the + # SUNDIALS 2.5 PETSc downloads. + ldflags="-Wl,-w" fi if [[ -z "$mpi_flag" ]] && ! command -v mpirun >/dev/null 2>&1; then From 7e0566e0e499730e8c725789ba495dd809994c45 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Tue, 6 Oct 2026 21:24:53 +0200 Subject: [PATCH 2/6] Check that the PETSc build passes no library path The test now looks at every code line of get_petsc.sh for a -L library path instead of two exact LDFLAGS values. --- tests/test_get_petsc_ldflags.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/test_get_petsc_ldflags.py b/tests/test_get_petsc_ldflags.py index bd2189e3..bec1dc80 100644 --- a/tests/test_get_petsc_ldflags.py +++ b/tests/test_get_petsc_ldflags.py @@ -12,12 +12,9 @@ SCRIPT = Path(__file__).resolve().parents[1] / 'tools' / 'get_petsc.sh' -def test_macos_ldflags_add_no_homebrew_library_path(): - """The macOS LDFLAGS only silence linker warnings and add no library path, so a Homebrew - SUNDIALS 7 cannot shadow the SUNDIALS 2.5 PETSc downloads (configure then misses - CVDense); mpicc already carries the MPI library path.""" - text = SCRIPT.read_text() - assignments = re.findall(r'^\s*ldflags=(.*)$', text, re.M) - assert assignments == ['""', '"-Wl,-w"'] - assert 'brew --prefix' not in text - assert '-L' not in ''.join(assignments) +def test_the_petsc_build_passes_no_library_path(): + """No code line of get_petsc.sh passes a -L to the build, so no library directory can come + ahead of the SUNDIALS 2.5 that PETSc downloads; -Wl,-w stays.""" + code = [re.sub(r'(^|\s)#.*$', '', line).strip() for line in SCRIPT.read_text().splitlines()] + assert [line for line in code if re.search(r'(^|[\s"=\'])-L[/$"\']', line)] == [] + assert 'ldflags="-Wl,-w"' in code From 837501cb33e04ee41b2a08a51e939e55bdddbdec Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Tue, 6 Oct 2026 21:34:27 +0200 Subject: [PATCH 3/6] Test the arguments PETSc configure gets on macOS The test runs the shipped platform and configure steps of get_petsc.sh as macOS with a system MPI and a stub configure, and checks that no argument carries -L and that LDFLAGS=-Wl,-w arrives, which catches any spelling of a library path. --- tests/test_get_petsc_ldflags.py | 47 ++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/tests/test_get_petsc_ldflags.py b/tests/test_get_petsc_ldflags.py index bec1dc80..909f6aa5 100644 --- a/tests/test_get_petsc_ldflags.py +++ b/tests/test_get_petsc_ldflags.py @@ -1,8 +1,13 @@ -"""Tests for the macOS linker flags in ``tools/get_petsc.sh``.""" +"""Tests for the macOS configure arguments of ``tools/get_petsc.sh``. + +The platform and configure steps are read from the shipped script and run with stub tools +and a stub ``configure``: no build and no network. +""" from __future__ import annotations import re +import subprocess from pathlib import Path import pytest @@ -12,9 +17,37 @@ SCRIPT = Path(__file__).resolve().parents[1] / 'tools' / 'get_petsc.sh' -def test_the_petsc_build_passes_no_library_path(): - """No code line of get_petsc.sh passes a -L to the build, so no library directory can come - ahead of the SUNDIALS 2.5 that PETSc downloads; -Wl,-w stays.""" - code = [re.sub(r'(^|\s)#.*$', '', line).strip() for line in SCRIPT.read_text().splitlines()] - assert [line for line in code if re.search(r'(^|[\s"=\'])-L[/$"\']', line)] == [] - assert 'ldflags="-Wl,-w"' in code +def _stub(directory: Path, name: str, body: str) -> None: + """Write an executable stub command into ``directory``.""" + path = directory / name + path.write_text(body) + path.chmod(0o755) + + +def test_configure_on_macos_gets_no_library_path(tmp_path): + """On macOS with a system MPI, PETSc configure gets LDFLAGS=-Wl,-w and no -L in any + argument, so no library directory comes ahead of the SUNDIALS 2.5 it downloads.""" + text = SCRIPT.read_text() + helpers = re.search(r'^console\(\) \{.*?^\}\n\nannounce\(\) \{.*?^\}\n', text, re.S | re.M) + start = text.index('current_step="Determining platform-specific flags"') + block = text[start : text.index('# 9. Build PETSc')] + stubs = tmp_path / 'stubs' + stubs.mkdir() + _stub(stubs, 'xcrun', '#!/bin/bash\necho /sdk\n') + for name in ('mpicc', 'mpirun'): + _stub(stubs, name, '#!/bin/bash\nexit 0\n') + work = tmp_path / 'petsc' + work.mkdir() + _stub(work, 'configure', f'#!/bin/bash\nprintf "%s\\n" "$@" > "{tmp_path}/args"\n') + snippet = f'exec 3>&1\n{helpers.group(0)}OSTYPE=darwin24\nworkpath="{work}"\n{block}' + result = subprocess.run( + ['bash', '-c', snippet], + capture_output=True, + text=True, + env={'PATH': f'{stubs}:/usr/bin:/bin', 'HOME': str(tmp_path)}, + ) + assert result.returncode == 0, result.stderr + args = (tmp_path / 'args').read_text().splitlines() + assert 'LDFLAGS=-Wl,-w' in args + assert [a for a in args if '-L' in a] == [] + assert '--download-mpich' not in args and '--with-cxx=0' in args From 66243d3033be1077982134876ee385cf13d5f23c Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Tue, 6 Oct 2026 21:40:08 +0200 Subject: [PATCH 4/6] Check the environment and strict mode of the macOS configure test The stub configure also records its environment, the shipped steps run under set -euo pipefail as in the script, and the test checks that --download-sundials2 reaches configure. --- tests/test_get_petsc_ldflags.py | 35 ++++++++++++--------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/tests/test_get_petsc_ldflags.py b/tests/test_get_petsc_ldflags.py index 909f6aa5..1453e0ba 100644 --- a/tests/test_get_petsc_ldflags.py +++ b/tests/test_get_petsc_ldflags.py @@ -6,7 +6,6 @@ from __future__ import annotations -import re import subprocess from pathlib import Path @@ -17,37 +16,29 @@ SCRIPT = Path(__file__).resolve().parents[1] / 'tools' / 'get_petsc.sh' -def _stub(directory: Path, name: str, body: str) -> None: - """Write an executable stub command into ``directory``.""" - path = directory / name - path.write_text(body) - path.chmod(0o755) - - def test_configure_on_macos_gets_no_library_path(tmp_path): """On macOS with a system MPI, PETSc configure gets LDFLAGS=-Wl,-w and no -L in any - argument, so no library directory comes ahead of the SUNDIALS 2.5 it downloads.""" + argument or exported variable, so no library directory comes ahead of the SUNDIALS 2.5 + it downloads.""" text = SCRIPT.read_text() - helpers = re.search(r'^console\(\) \{.*?^\}\n\nannounce\(\) \{.*?^\}\n', text, re.S | re.M) start = text.index('current_step="Determining platform-specific flags"') block = text[start : text.index('# 9. Build PETSc')] - stubs = tmp_path / 'stubs' - stubs.mkdir() - _stub(stubs, 'xcrun', '#!/bin/bash\necho /sdk\n') - for name in ('mpicc', 'mpirun'): - _stub(stubs, name, '#!/bin/bash\nexit 0\n') - work = tmp_path / 'petsc' - work.mkdir() - _stub(work, 'configure', f'#!/bin/bash\nprintf "%s\\n" "$@" > "{tmp_path}/args"\n') - snippet = f'exec 3>&1\n{helpers.group(0)}OSTYPE=darwin24\nworkpath="{work}"\n{block}' + for name in ('xcrun', 'mpicc', 'mpirun'): + (tmp_path / name).write_text('#!/bin/bash\necho /sdk\n') + (tmp_path / 'configure').write_text('#!/bin/bash\nprintf "%s\\n" "$@" > args\nenv > env\n') + for stub in tmp_path.iterdir(): + stub.chmod(0o755) + snippet = 'set -euo pipefail\nannounce() { :; }\nOSTYPE=darwin24\nworkpath=.\n' + block result = subprocess.run( ['bash', '-c', snippet], capture_output=True, text=True, - env={'PATH': f'{stubs}:/usr/bin:/bin', 'HOME': str(tmp_path)}, + cwd=tmp_path, + env={'PATH': f'{tmp_path}:/usr/bin:/bin'}, ) assert result.returncode == 0, result.stderr args = (tmp_path / 'args').read_text().splitlines() - assert 'LDFLAGS=-Wl,-w' in args + assert {'LDFLAGS=-Wl,-w', '--download-sundials2'} <= set(args) assert [a for a in args if '-L' in a] == [] - assert '--download-mpich' not in args and '--with-cxx=0' in args + assert [v for v in (tmp_path / 'env').read_text().splitlines() if '-L' in v] == [] + assert '--download-mpich' not in args From 53d87d0bb2b6f40ac9e40ec6302e33fd6622b013 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Tue, 6 Oct 2026 22:12:19 +0200 Subject: [PATCH 5/6] Check named build variables and document the SUNDIALS clash The macOS configure test checks that LDFLAGS, LIBRARY_PATH, LIBS and CPATH stay unset and that no exported variable names the Homebrew library directory. The troubleshooting docs describe the "Downloaded sundials2 could not be used" failure that a Homebrew SUNDIALS causes, and that Intel Macs can still hit it. The install docs say the script uses the MPI on PATH. --- docs/How-to/installation.md | 13 +++++++++++++ tests/test_get_petsc_ldflags.py | 14 ++++++++++++-- tools/get_petsc.sh | 5 ++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/How-to/installation.md b/docs/How-to/installation.md index 23d97efd..0099bc4f 100644 --- a/docs/How-to/installation.md +++ b/docs/How-to/installation.md @@ -118,3 +118,16 @@ PETSC_ARCH=arch-xxx-yyy export PETSC_ARCH=arch-xxx-yyy ``` +## Troubleshooting + +### PETSc configure: "Downloaded sundials2 could not be used" + +PETSc configure on macOS stops with: + +``` +Downloaded sundials2 could not be used. Please check install in .../arch-darwin-c-opt +``` + +and `configure.log` reports `_CVDense` and `_CVSpgmr` as undefined symbols. + +A SUNDIALS 7 from Homebrew is in the linker search path ahead of the SUNDIALS 2.5 that PETSc downloads, and SUNDIALS 7 has no `CVDense`. `get_petsc.sh` passes no Homebrew library path, so on Apple Silicon, where Homebrew lives in `/opt/homebrew`, PETSc builds with a Homebrew SUNDIALS installed. On an Intel Mac, Homebrew installs into `/usr/local/lib`, which the linker searches by default, so a Homebrew SUNDIALS can still hide the downloaded one there. `brew list --versions sundials` shows whether Homebrew's SUNDIALS is installed. diff --git a/tests/test_get_petsc_ldflags.py b/tests/test_get_petsc_ldflags.py index 1453e0ba..7c2c45a9 100644 --- a/tests/test_get_petsc_ldflags.py +++ b/tests/test_get_petsc_ldflags.py @@ -16,6 +16,14 @@ SCRIPT = Path(__file__).resolve().parents[1] / 'tools' / 'get_petsc.sh' +# A stub configure: records its arguments, its environment and four named variables. +CONFIGURE_STUB = """#!/bin/bash +printf "%s\\n" "$@" > args +export -p > env +for v in LDFLAGS LIBRARY_PATH LIBS CPATH; do printf "%s=%s\\n" "$v" "${!v-unset}"; done > named +""" + + def test_configure_on_macos_gets_no_library_path(tmp_path): """On macOS with a system MPI, PETSc configure gets LDFLAGS=-Wl,-w and no -L in any argument or exported variable, so no library directory comes ahead of the SUNDIALS 2.5 @@ -25,7 +33,7 @@ def test_configure_on_macos_gets_no_library_path(tmp_path): block = text[start : text.index('# 9. Build PETSc')] for name in ('xcrun', 'mpicc', 'mpirun'): (tmp_path / name).write_text('#!/bin/bash\necho /sdk\n') - (tmp_path / 'configure').write_text('#!/bin/bash\nprintf "%s\\n" "$@" > args\nenv > env\n') + (tmp_path / 'configure').write_text(CONFIGURE_STUB) for stub in tmp_path.iterdir(): stub.chmod(0o755) snippet = 'set -euo pipefail\nannounce() { :; }\nOSTYPE=darwin24\nworkpath=.\n' + block @@ -40,5 +48,7 @@ def test_configure_on_macos_gets_no_library_path(tmp_path): args = (tmp_path / 'args').read_text().splitlines() assert {'LDFLAGS=-Wl,-w', '--download-sundials2'} <= set(args) assert [a for a in args if '-L' in a] == [] - assert [v for v in (tmp_path / 'env').read_text().splitlines() if '-L' in v] == [] + named = (tmp_path / 'named').read_text().split() + assert named == [f'{v}=unset' for v in ('LDFLAGS', 'LIBRARY_PATH', 'LIBS', 'CPATH')] + assert 'homebrew/lib' not in (tmp_path / 'env').read_text() assert '--download-mpich' not in args diff --git a/tools/get_petsc.sh b/tools/get_petsc.sh index f7b8dd82..512a714a 100755 --- a/tools/get_petsc.sh +++ b/tools/get_petsc.sh @@ -341,9 +341,8 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # macOS provides Accelerate framework with BLAS/LAPACK blas_flag="" - # -Wl,-w silences the deprecated-flag warnings macOS 26+ turns into PETSc configure errors. - # No Homebrew -L: mpicc carries its own, and a Homebrew SUNDIALS there would hide the - # SUNDIALS 2.5 PETSc downloads. + # -Wl,-w: macOS 26+ turns deprecated-flag warnings into configure errors. No Homebrew -L: + # mpicc brings its own, and a Homebrew SUNDIALS there would hide PETSc's SUNDIALS 2.5. ldflags="-Wl,-w" fi From 411a7a6c3e0521c35c1d9449587f2e8920861826 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Tue, 6 Oct 2026 22:30:24 +0200 Subject: [PATCH 6/6] Test the macOS PETSc setup without mpicc and without xcrun Without mpicc on PATH, configure gets --download-mpich and still only LDFLAGS=-Wl,-w with no library path; without xcrun, the script stops with its install hint before configure runs. Both cases run with PATH limited to the stub tools. --- tests/test_get_petsc_ldflags.py | 51 +++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/tests/test_get_petsc_ldflags.py b/tests/test_get_petsc_ldflags.py index 7c2c45a9..89bd4cce 100644 --- a/tests/test_get_petsc_ldflags.py +++ b/tests/test_get_petsc_ldflags.py @@ -24,31 +24,58 @@ """ -def test_configure_on_macos_gets_no_library_path(tmp_path): - """On macOS with a system MPI, PETSc configure gets LDFLAGS=-Wl,-w and no -L in any - argument or exported variable, so no library directory comes ahead of the SUNDIALS 2.5 - it downloads.""" +def _run_macos(tmp_path, tools, path_extra=':/usr/bin:/bin'): + """Run the shipped platform and configure steps as macOS with stub ``tools``.""" text = SCRIPT.read_text() start = text.index('current_step="Determining platform-specific flags"') block = text[start : text.index('# 9. Build PETSc')] - for name in ('xcrun', 'mpicc', 'mpirun'): + for name in tools: (tmp_path / name).write_text('#!/bin/bash\necho /sdk\n') (tmp_path / 'configure').write_text(CONFIGURE_STUB) for stub in tmp_path.iterdir(): stub.chmod(0o755) snippet = 'set -euo pipefail\nannounce() { :; }\nOSTYPE=darwin24\nworkpath=.\n' + block - result = subprocess.run( - ['bash', '-c', snippet], + return subprocess.run( + ['/bin/bash', '-c', snippet], capture_output=True, text=True, cwd=tmp_path, - env={'PATH': f'{tmp_path}:/usr/bin:/bin'}, + env={'PATH': f'{tmp_path}{path_extra}'}, ) - assert result.returncode == 0, result.stderr - args = (tmp_path / 'args').read_text().splitlines() - assert {'LDFLAGS=-Wl,-w', '--download-sundials2'} <= set(args) - assert [a for a in args if '-L' in a] == [] + + +def _configure_args(tmp_path) -> list[str]: + """Return the configure arguments, after checking no build variable names a library path.""" named = (tmp_path / 'named').read_text().split() assert named == [f'{v}=unset' for v in ('LDFLAGS', 'LIBRARY_PATH', 'LIBS', 'CPATH')] assert 'homebrew/lib' not in (tmp_path / 'env').read_text() + args = (tmp_path / 'args').read_text().splitlines() + assert [a for a in args if '-L' in a] == [] + return args + + +def test_configure_on_macos_gets_no_library_path(tmp_path): + """On macOS with a system MPI, PETSc configure gets LDFLAGS=-Wl,-w and no -L in any + argument or build variable, so no library directory comes ahead of the SUNDIALS 2.5 it + downloads.""" + result = _run_macos(tmp_path, ('xcrun', 'mpicc', 'mpirun')) + assert result.returncode == 0, result.stderr + args = _configure_args(tmp_path) + assert {'LDFLAGS=-Wl,-w', '--download-sundials2'} <= set(args) assert '--download-mpich' not in args + + +def test_configure_on_macos_without_mpicc_downloads_mpich(tmp_path): + """Without mpicc on PATH, PETSc downloads MPICH and still gets only LDFLAGS=-Wl,-w.""" + result = _run_macos(tmp_path, ('xcrun',), path_extra='') + assert result.returncode == 0, result.stderr + args = _configure_args(tmp_path) + assert {'LDFLAGS=-Wl,-w', '--download-sundials2', '--download-mpich'} <= set(args) + + +def test_macos_without_xcrun_stops_before_configure(tmp_path): + """Without xcrun the script stops with its install hint and never runs configure.""" + result = _run_macos(tmp_path, ('mpicc', 'mpirun'), path_extra='') + assert result.returncode == 1 + assert 'xcrun not found' in result.stderr and 'xcode-select --install' in result.stderr + assert not (tmp_path / 'args').exists()