From a0748751cb8631ea69370c6efc41ba3eae3591db Mon Sep 17 00:00:00 2001 From: leynos Date: Tue, 21 Jul 2026 23:05:36 +0200 Subject: [PATCH] Expose uv's libpython on LD_LIBRARY_PATH for PyO3 baseline builds (#372) PyO3 embedding crates (the auto-initialize pattern) link their test binaries against the interpreter PyO3's build script resolves from PATH. Setup uv puts a uv-managed CPython ahead of the system python3, and its libpython lives under uv's private install directory, off the loader's default search path. The unmutated baseline cargo test binary then aborts with "libpython3.NN.so: cannot open shared object file" and no mutant is ever scored. After provisioning Python, derive the interpreter's LIBDIR via sysconfig and add it to LD_LIBRARY_PATH for the later steps, before Run mutation testing. This is harmless for non-PyO3 callers: nothing links libpython, so nothing loads from the added directory. Pin a shape-test invariant that the export precedes the run step, and document the automatic handling in the caller guide. --- .github/workflows/mutation-cargo.yml | 24 ++++++++++++++ docs/mutation-cargo-workflow.md | 5 +++ .../tests/test_mutation_workflow_shape.py | 32 +++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/.github/workflows/mutation-cargo.yml b/.github/workflows/mutation-cargo.yml index 4935b7c2..3d77f229 100644 --- a/.github/workflows/mutation-cargo.yml +++ b/.github/workflows/mutation-cargo.yml @@ -247,6 +247,30 @@ jobs: cache-dependency-glob: | **/workflow_scripts/*.py + - name: Expose libpython for PyO3 test binaries + if: ${{ env.ACT != 'true' }} + shell: bash + run: | + set -euo pipefail + # PyO3 embedding crates (the auto-initialize pattern) link their + # test binaries against the interpreter PyO3's build script resolves + # from PATH. "Setup uv" places a uv-managed CPython ahead of the + # system python3, and its libpython lives under uv's private install + # directory, which is not on the dynamic loader's default search + # path. The baseline `cargo test` binary then aborts at run time with + # "libpython3.NN.so.1.0: cannot open shared object file" and no mutant + # is ever scored (issue #372). Add the resolved interpreter's LIBDIR + # to LD_LIBRARY_PATH for every later step. This is harmless for + # non-PyO3 callers: no test binary links libpython, so nothing loads + # from the added directory. + libdir="$(python3 -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR") or "")')" + if [[ -z "${libdir}" || ! -d "${libdir}" ]]; then + echo "No Python LIBDIR resolved (${libdir:-empty}); leaving LD_LIBRARY_PATH unchanged" >&2 + exit 0 + fi + echo "Adding ${libdir} to LD_LIBRARY_PATH for PyO3 test binaries" + echo "LD_LIBRARY_PATH=${libdir}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" >> "${GITHUB_ENV}" + - name: Relocate workflow source # Move the workflow checkout out of the caller's workspace so # tree-scanning tests in the caller's suite (manifest sweeps, diff --git a/docs/mutation-cargo-workflow.md b/docs/mutation-cargo-workflow.md index 56473dcb..229b25c1 100644 --- a/docs/mutation-cargo-workflow.md +++ b/docs/mutation-cargo-workflow.md @@ -99,6 +99,11 @@ jobs: - GitHub disables cron triggers after 60 days of repository inactivity; a quiet repository silently stops running, which is an acceptable failure mode for an informational workflow. +- Callers with PyO3 embedding crates (the `auto-initialize` pattern) + are handled automatically: the workflow adds the uv-provisioned + interpreter's `LIBDIR` to `LD_LIBRARY_PATH` before running + cargo-mutants, so test binaries linked against `libpython` load it at + run time. No `setup-commands` workaround is needed. ## Local validation diff --git a/workflow_scripts/tests/test_mutation_workflow_shape.py b/workflow_scripts/tests/test_mutation_workflow_shape.py index 78618042..23e9d54f 100644 --- a/workflow_scripts/tests/test_mutation_workflow_shape.py +++ b/workflow_scripts/tests/test_mutation_workflow_shape.py @@ -55,6 +55,38 @@ def _step_names(steps: list[dict[str, object]]) -> list[object]: return [step.get("name") for step in steps] +def test_mutants_job_exposes_libpython_before_running() -> None: + """The mutants job puts libpython on LD_LIBRARY_PATH before cargo-mutants. + + PyO3 embedding crates link their test binaries against the uv-managed + interpreter that "Setup uv" places on PATH, whose libpython is not on the + loader's default search path. Without exporting its LIBDIR onto + LD_LIBRARY_PATH the baseline `cargo test` binary aborts at run time and no + mutant is scored (issue #372). The export must land before the + "Run mutation testing" step that builds and runs those binaries. + """ + jobs = _jobs("mutation-cargo.yml") + assert "mutants" in jobs, "mutation-cargo.yml must declare a mutants job" + steps = _steps(jobs["mutants"]) + names = _step_names(steps) + exposers = [ + index + for index, step in enumerate(steps) + if isinstance(step.get("run"), str) + and "LD_LIBRARY_PATH=" in typ.cast("str", step.get("run")) + and 'get_config_var("LIBDIR")' in typ.cast("str", step.get("run")) + ] + assert exposers, ( + "mutation-cargo.yml mutants job must export the interpreter LIBDIR " + "onto LD_LIBRARY_PATH for PyO3 test binaries (issue #372)" + ) + assert "Run mutation testing" in names, "mutants job must run cargo-mutants" + run_index = names.index("Run mutation testing") + assert min(exposers) < run_index, ( + "the LD_LIBRARY_PATH export must precede the Run mutation testing step" + ) + + @pytest.mark.parametrize("workflow_name", WORKFLOW_NAMES) def test_every_workflow_checkout_is_followed_by_relocation( workflow_name: str,