#!/usr/bin/env bash # # Summary: Generate a python-build definition for a saved binary archive # # Usage: pyenv binary generate-installer --archive-url [-o ] # # Reads a metadata file written by `pyenv binary save' and emits a python-build # definition. Dropped into python-build's definition directory, it installs the # archive like any other version: `pyenv install' downloads it from , # checks the platform and the required system libraries, unpacks it, then # rewrites rpaths so the copy runs from its prefix. # # The archive must sit next to the metadata file so its checksum can be baked # into the definition; `pyenv binary save' writes the two together. # # A .meta file written by `pyenv binary save'. # The corresponsing binary package written by `pyenv binary save' # needs to be present alongside it. # --archive-url The URL for the resulting installation script to download # the package from. # -o Write the definition here (default: stdout). # set -e [ -n "$PYENV_DEBUG" ] && set -x # Provide pyenv completions if [ "$1" = "--complete" ]; then echo --archive-url echo -o exit fi metadata="" archive_url="" output="" while [ $# -gt 0 ]; do case "$1" in --archive-url ) [ $# -ge 2 ] || { echo "pyenv-binary: --archive-url needs a value" >&2; exit 1; } archive_url="$2"; shift 2 ;; -o ) [ $# -ge 2 ] || { echo "pyenv-binary: -o needs a value" >&2; exit 1; } output="$2"; shift 2 ;; -* ) echo "pyenv-binary: unknown option \`$1'" >&2; exit 1 ;; * ) [ -z "$metadata" ] || { echo "pyenv-binary: unexpected argument \`$1'" >&2; exit 1; } metadata="$1"; shift ;; esac done if [ -z "$metadata" ] || [ -z "$archive_url" ]; then pyenv-help --usage binary-generate-installer >&2 exit 1 fi version="" os="" arch="" distro="" libc="" archive="" deps="" while IFS='=' read -r key value; do case "$key" in version ) version="$value" ;; os ) os="$value" ;; arch ) arch="$value" ;; distro ) distro="$value" ;; libc ) libc="$value" ;; archive ) archive="$value" ;; dep ) deps="${deps:+$deps }$value" ;; esac done < "$metadata" # Fail here if the metadata is incomplete, rather than bake a blank into the # definition where a missing arch would give a confusing platform error. for field in version os arch archive; do if [ -z "${!field}" ]; then echo "pyenv-binary: metadata is missing \`${field}'" >&2 exit 1 fi done # macOS relocation needs install_name_tool and a different rpath scheme, so it is # not supported yet. Other systems relocate with patchelf. if [ "$os" = "Darwin" ]; then echo "pyenv-binary: macOS archives are not supported yet" >&2 exit 1 fi # The glibc floor only applies on Linux; require it there so the definition can # tell whether the target is new enough to run the binaries. if [ "$os" = "Linux" ] && [ -z "$libc" ]; then echo "pyenv-binary: metadata is missing \`libc'" >&2 exit 1 fi # python-build downloads the archive itself, so the checksum has to be baked in. # The archive sits next to the metadata `save' wrote it alongside. archive_path="$(dirname "$metadata")/${archive}" if [ ! -r "$archive_path" ]; then echo "pyenv-binary: cannot read the archive \`${archive_path}' to checksum it" >&2 exit 1 fi if command -v sha256sum >/dev/null 2>&1; then sha256="$(sha256sum "$archive_path")"; sha256="${sha256%% *}" elif command -v shasum >/dev/null 2>&1; then sha256="$(shasum -a 256 "$archive_path")"; sha256="${sha256%% *}" elif command -v openssl >/dev/null 2>&1; then sha256="$(openssl dgst -sha256 "$archive_path")"; sha256="${sha256##* }" else echo "pyenv-binary: need sha256sum, shasum or openssl to checksum the archive" >&2 exit 1 fi emit() { # The values known now go in as %q-quoted assignments so a stray quote or space # in the URL or metadata cannot break the definition or inject into it. The body # below is a quoted here-doc, verbatim. { printf '# python-build definition for prebuilt Python %s (%s/%s%s).\n' \ "$version" "$os" "$arch" "${distro:+, $distro}" echo "# Generated by \`pyenv binary generate-installer'." echo printf 'EXPECT_OS=%q\n' "$os" printf 'EXPECT_ARCH=%q\n' "$arch" printf 'EXPECT_LIBC=%q\n' "$libc" printf 'VERSION=%q\n' "$version" printf 'ARCHIVE_URL=%q\n' "$archive_url" printf 'SHA256=%q\n' "$sha256" printf 'DEPS=%q\n' "$deps" } cat <<'EOF' os="$(uname -s)" arch="$(uname -m)" if [ "$os" != "$EXPECT_OS" ] || [ "$arch" != "$EXPECT_ARCH" ]; then echo "pyenv-binary: this archive is for ${EXPECT_OS}/${EXPECT_ARCH}, not ${os}/${arch}" >&2 exit 1 fi # Refuse a target whose glibc is older than the one the archive was built on; # the dynamic loader would reject the binaries. case "$EXPECT_LIBC" in "glibc "* ) build_libc="${EXPECT_LIBC#glibc }" target_libc="$(getconf GNU_LIBC_VERSION 2>/dev/null || true)" case "$target_libc" in "glibc "* ) target_libc="${target_libc#glibc }" older="$(printf '%s\n%s\n' "$build_libc" "$target_libc" | sort -V | head -n1)" if [ "$older" = "$target_libc" ] && [ "$target_libc" != "$build_libc" ]; then echo "pyenv-binary: archive needs glibc ${build_libc} or newer, but this system has ${target_libc}" >&2 exit 1 fi ;; esac ;; esac # Required system libraries must already be present on the target. ldconfig -p # lists each library as " (...) => "; match the soname column # exactly so a longer name like libc.so.6.1 does not satisfy libc.so.6. if command -v ldconfig &>/dev/null && cache="$(ldconfig -p)"; then missing="" for dep in $DEPS; do printf '%s\n' "$cache" | awk -v d="$dep" '$1 == d { found = 1 } END { exit !found }' \ || missing="${missing} ${dep}" done if [ -n "$missing" ]; then echo "pyenv-binary: missing required system libraries:${missing}" >&2 exit 1 fi elif [ -n "$DEPS" ]; then echo "pyenv-binary: cannot check required system libraries (ldconfig with -p not found)" >&2 fi build_package_relocate() { pyenv binary relocate "$PREFIX_PATH" } install_package "Python-${VERSION}-binary" "${ARCHIVE_URL}#${SHA256}" copy relocate EOF } if [ -n "$output" ]; then emit > "$output" echo "Wrote definition to ${output}" else emit fi