diff --git a/.github/actions/setup-proteus/action.yml b/.github/actions/setup-proteus/action.yml index 79fdfd0f1..95f2959ee 100644 --- a/.github/actions/setup-proteus/action.yml +++ b/.github/actions/setup-proteus/action.yml @@ -353,7 +353,9 @@ runs: uses: actions/cache@v5 with: path: socrates/ - key: socrates-${{ runner.os }}-${{ runner.arch }}-portable-${{ steps.runner-image.outputs.version }}-${{ steps.pins.outputs.socrates_ref }}-${{ hashFiles('tools/get_socrates.sh') }} + # Both files: get_socrates.sh sources the shared helpers in + # tools/_get_common.sh, so a change there also changes what is built. + key: socrates-${{ runner.os }}-${{ runner.arch }}-portable-${{ steps.runner-image.outputs.version }}-${{ steps.pins.outputs.socrates_ref }}-${{ hashFiles('tools/get_socrates.sh', 'tools/_get_common.sh') }} - name: Build SOCRATES (cache miss) if: steps.cache-socrates.outputs.cache-hit != 'true' diff --git a/docs/How-to/development_standards.md b/docs/How-to/development_standards.md index 85e058aa6..eb402d566 100644 --- a/docs/How-to/development_standards.md +++ b/docs/How-to/development_standards.md @@ -62,6 +62,31 @@ field sets) are written one entry per line with a trailing comma, grouped by module under a header comment, and ordered alphabetically within each group. This keeps two independent additions on different lines so they merge cleanly. +**Module install scripts** + +The `tools/get_*.sh` scripts share one sourced library, `tools/_get_common.sh`. +It provides `portable_realpath`, the checkout root (`proteus_root`) and tools +directory (`proteus_tools_dir`), the `--force` and install-path argument split +(`get_parse_args`), the dirty-checkout guard (`guard_dirty_checkout`), the +GitHub SSH probe (`github_use_ssh`), the https-to-SSH URL rewrite +(`github_ssh_url`), and the pin reader (`resolve_module_pin`). + +A new install script starts with the same bootstrap the existing ones use: + +```bash +source "$(dirname "${BASH_SOURCE[0]}")/_get_common.sh" || exit 1 +``` + +The `|| exit 1` is load-bearing: half the scripts set no `-e`, and one that +carried on past a failed `source` would run with `proteus_root` empty, putting +its work path at the filesystem root. + +Add a helper to the library rather than copying one into a script, and give it +a parameter for the variation a caller needs: `get_socrates.sh` passes a git +pathspec to `guard_dirty_checkout` so its regenerable `make/Mk_cmd` does not +count as local work. The helpers target bash 3.2 and run the same with or +without `set -euo pipefail`, because the scripts differ on that. + **Editing shared files** - When adding to the main coupling loop, add a stage function and call it rather diff --git a/tests/tools/test_install_scripts.py b/tests/tools/test_install_scripts.py index f876704f6..d6c701420 100644 --- a/tests/tools/test_install_scripts.py +++ b/tests/tools/test_install_scripts.py @@ -4,18 +4,36 @@ Reusable shell logic replicated inline from ``tools/get_petsc.sh`` and ``tools/get_spider.sh``: -- ``portable_realpath()``: cross-platform path resolution - ERR trap: exit-code and step-name capture - Platform detection: PETSC_ARCH assignment - Homebrew prefix fallback: architecture-aware default -- Workpath argument handling: ``$1`` override vs default - PETSc library detection: versioned ``.so``, ``.dylib``, missing +``tools/_get_common.sh``, the helper library every ``get_*.sh`` sources, is +sourced by the cases below rather than copied into them, so they run against +the shipped text: +- ``portable_realpath()``: cross-platform path resolution, including a + destination that does not exist yet +- the checkout root and tools directory the library derives from its own + location +- ``get_parse_args``: the ``--force`` switch and the optional install path +- ``guard_dirty_checkout``: the refusal to delete local work, and the + pathspec exclusion ``get_socrates.sh`` passes it +- ``github_use_ssh`` and ``github_ssh_url``: the SSH probe and URL rewrite +- ``resolve_module_pin``: a missing pin stops the install +- the bootstrap in each script, which stops when the library is absent, and + the invariant that no script carries a private copy of a helper + Blocks lifted out of the shipped scripts at run time, so that rewording a script re-runs its cases against the new text: -- ``tools/get_aragog.sh``: the dirty-checkout guard shared across ``get_*.sh`` -- ``tools/get_socrates.sh``: the portable-flag rewrite, its post-build flag - check, and the conditional AGNI-wrapper rebuild note +- ``tools/get_petsc.sh``: the install-path resolution, ``$1`` against the + ``./petsc/`` default +- ``tools/get_socrates.sh``: the install-path resolution, the portable-flag + rewrite, its post-build flag check, and the conditional AGNI-wrapper + rebuild note + +Whole scripts run against stubbed ``git`` and ``ssh``, to pin the clone +destination and transport each one resolves. Also pins invariants that live in checked-in configuration and documentation rather than in shell, each of which fails silently when its counterpart moves: @@ -23,6 +41,7 @@ scripts resolve through ``tools/_module_pins.py`` - the extras the ``setup-proteus`` composite action installs, against the extra keys pyproject declares +- the SOCRATES cache key, against every file its build step reads - the installation docs' guidance on editable installs, against those pins - CI config leaving USER at the runner default, which the action's macOS ``brew install`` step requires @@ -39,26 +58,61 @@ from __future__ import annotations import os +import shutil import subprocess import sys +from pathlib import Path import pytest +pytestmark = [pytest.mark.unit, pytest.mark.timeout(30)] + +TOOLS_DIR = Path(__file__).resolve().parents[2] / 'tools' +COMMON_LIB = TOOLS_DIR / '_get_common.sh' + # --------------------------------------------------------------------------- -# Helper: extract portable_realpath function from a script +# Helpers: run bash against the shipped helper library # --------------------------------------------------------------------------- -def _portable_realpath_fn() -> str: - """Return the bash source for ``portable_realpath()``.""" - return """\ -portable_realpath() { - if command -v realpath >/dev/null 2>&1; then - realpath "$1" - else - python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$1" - fi -} -""" +def _with_common(body: str, strict: bool = False) -> str: + """Return a bash snippet that sources the shipped helper library. + + The library is sourced, not copied, so a change to it re-runs through + every case below. ``strict`` mirrors the ``get_*`` scripts that enable + ``set -euo pipefail``: the helpers must behave the same either way. + """ + prelude = 'set -euo pipefail\n' if strict else '' + return f'{prelude}source "{COMMON_LIB}"\n{body}' + + +def _run_bash(snippet: str, *argv: str, **kwargs) -> subprocess.CompletedProcess: + """Run ``snippet`` with ``argv`` as its positional parameters.""" + return subprocess.run( + ['bash', '-c', snippet, 'get_test.sh', *argv], + capture_output=True, + text=True, + **kwargs, + ) + + +def _extract_script_block(script: str, start_marker: str, end_marker: str) -> str: + """Return the shipped lines of ``tools/