diff --git a/plugins/pyenv-binary/libexec/pyenv-binary-save b/plugins/pyenv-binary/libexec/pyenv-binary-save index fb818fcf..e4273681 100755 --- a/plugins/pyenv-binary/libexec/pyenv-binary-save +++ b/plugins/pyenv-binary/libexec/pyenv-binary-save @@ -27,10 +27,10 @@ if [ -z "$version" ]; then exit 1 fi -# A version is a single directory name under versions/. Reject a name with a -# slash or a dot-dot component so it cannot point outside there. +# A version is a single directory name under versions/. With no slash allowed, +# the only remaining names that could point elsewhere are `.' and `..'. case "$version" in -*/* | *..* ) +*/* | .. | . ) echo "pyenv-binary: invalid version name \`${version}'" >&2 exit 1 ;; @@ -69,6 +69,9 @@ system_deps() { for f in "${prefix}"/bin/python*; do [ -e "$f" ] && printf '%s\n' "$f" done + # CPython ships its extension modules as *.so (and *.dylib on macOS). A + # bare *.so.* is unusual for CPython itself, but the odd build carries a + # versioned copy alongside, so match it too rather than miss a dependency. find "${prefix}" -type f \( -name '*.so' -o -name '*.so.*' -o -name '*.dylib' \) } | sort -u | while IFS= read -r f; do if [ "$os" = "Darwin" ]; then diff --git a/plugins/pyenv-binary/test/save.bats b/plugins/pyenv-binary/test/save.bats index b2986d8e..1e1188ef 100644 --- a/plugins/pyenv-binary/test/save.bats +++ b/plugins/pyenv-binary/test/save.bats @@ -25,9 +25,9 @@ platform() { assert_failure "pyenv-binary: invalid version name \`foo/bar'" } -@test "rejects a version name that walks out of versions/" { - run pyenv-binary-save "../../etc" - assert_failure "pyenv-binary: invalid version name \`../../etc'" +@test "rejects the parent directory reference" { + run pyenv-binary-save ".." + assert_failure "pyenv-binary: invalid version name \`..'" } @test "packages an installed version" { @@ -49,3 +49,69 @@ platform() { assert_output_contains "platform=$(platform)" assert_output_contains "archive=3.12.7-$(platform).tar.gz" } + +# Put an executable on PATH that stands in for a system tool during the test. +stub() { + local name="$1" + local dir="${BATS_TEST_TMPDIR}/stubs" + mkdir -p "$dir" + { echo "#!/usr/bin/env bash"; cat -; } > "${dir}/${name}" + chmod +x "${dir}/${name}" + export PATH="${dir}:$PATH" +} + +@test "records only the libraries ldd resolves outside the prefix" { + create_version "3.12.7" + touch "${PYENV_ROOT}/versions/3.12.7/bin/python3.12" + + # A realistic ldd listing: the vdso and the loader have no `=>' mapping, + # libpython resolves inside the prefix, and libc/libm are external. + stub ldd <<'STUB' +prefix="${PYENV_ROOT}/versions/3.12.7" +cat < ${prefix}/lib/libpython3.12.so.1.0 (0x00007f4a3c000000) + libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f4a3bc00000) + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4a3b800000) + /lib64/ld-linux-x86-64.so.2 (0x00007f4a3c200000) +EOF +STUB + + run pyenv-binary-save "3.12.7" "${BATS_TEST_TMPDIR}/dist" + assert_success + + run grep '^dep=' "${BATS_TEST_TMPDIR}/dist/"*.meta + assert_output "dep=libc.so.6 +dep=libm.so.6" +} + +@test "records only the libraries otool resolves outside the prefix" { + create_version "3.12.7" + touch "${PYENV_ROOT}/versions/3.12.7/bin/python3.12" + + # Take the macOS path by faking the platform, then feed a realistic otool + # listing: the first line names the file, @rpath and in-prefix entries are + # bundled, and libSystem is the one external dependency. + stub uname <<'STUB' +case "$1" in + -s) echo Darwin ;; + -m) echo arm64 ;; + *) exec /usr/bin/uname "$@" ;; +esac +STUB + stub otool <<'STUB' +prefix="${PYENV_ROOT}/versions/3.12.7" +cat <