#!/usr/bin/env bash # Summary: Rehash pyenv shims (run this after installing executables) set -e [ -n "$PYENV_DEBUG" ] && set -x SHIM_PATH="${PYENV_ROOT}/shims" PROTOTYPE_SHIM_PATH="${SHIM_PATH}/.pyenv-shim" # Create the shims directory if it doesn't already exist. mkdir -p "$SHIM_PATH" acquire_lock() { # Ensure only one instance of pyenv-rehash is running at a time by # setting the shell's `noclobber` option and attempting to write to # the prototype shim file. If the file already exists, print a warning # to stderr and exit with a non-zero status. local ret set -o noclobber echo > "$PROTOTYPE_SHIM_PATH" 2>| /dev/null || ret=1 set +o noclobber [ -z "${ret}" ] } # If we were able to obtain a lock, register a trap to clean up the # prototype shim when the process exits. trap release_lock EXIT remove_prototype_shim() { rm -f "$PROTOTYPE_SHIM_PATH" } release_lock() { remove_prototype_shim } if [ ! -w "$SHIM_PATH" ]; then echo "pyenv: cannot rehash: $SHIM_PATH isn't writable" exit 1 fi unset acquired start=$SECONDS while (( SECONDS <= start + ${PYENV_REHASH_TIMEOUT:-60} )); do if acquire_lock 2>/dev/null; then acquired=1 break else # POSIX sleep(1) doesn't provide subsecond precision, but many others do sleep 0.1 2>/dev/null || sleep 1 fi done if [ -z "${acquired}" ]; then echo "pyenv: cannot rehash: $PROTOTYPE_SHIM_PATH exists" exit 1 fi # The prototype shim file is a script that re-execs itself, passing # its filename and any arguments to `pyenv exec`. This file is # hard-linked for every executable and then removed. The linking # technique is fast, uses less disk space than unique files, and also # serves as a locking mechanism. create_prototype_shim() { cat > "$PROTOTYPE_SHIM_PATH" </dev/null 2>&1; then rm -f "$SHIM_PATH"/* fi break done } # List basenames of executables for every Python version list_executable_names() { local version file pyenv-versions --bare --skip-aliases | \ while read -r version; do for file in "${PYENV_ROOT}/versions/${version}/bin/"*; do echo "${file##*/}" done done } # The basename of each argument passed to `make_shims` will be # registered for installation as a shim. In this way, plugins may call # `make_shims` with a glob to register many shims at once. make_shims() { local file shim for file; do shim="${file##*/}" register_shim "$shim" done } if ((${BASH_VERSINFO[0]} > 3)); then declare -A registered_shims # Registers the name of a shim to be generated. register_shim() { registered_shims["$1"]=1 } # Install all shims registered via `make_shims` or `register_shim` directly. install_registered_shims() { local shim file for shim in "${!registered_shims[@]}"; do file="${SHIM_PATH}/${shim}" [ -e "$file" ] || cp "$PROTOTYPE_SHIM_PATH" "$file" done } # Once the registered shims have been installed, we make a second pass # over the contents of the shims directory. Any file that is present # in the directory but has not been registered as a shim should be # removed. remove_stale_shims() { local shim for shim in "$SHIM_PATH"/*; do if [[ ! ${registered_shims["${shim##*/}"]} ]]; then rm -f "$shim" fi done } else # Same for bash < 4. registered_shims=" " register_shim() { registered_shims="${registered_shims}${1} " } install_registered_shims() { local shim file for shim in $registered_shims; do file="${SHIM_PATH}/${shim}" [ -e "$file" ] || cp "$PROTOTYPE_SHIM_PATH" "$file" done } remove_stale_shims() { local shim for shim in "$SHIM_PATH"/*; do if [[ "$registered_shims" != *" ${shim##*/} "* ]]; then rm -f "$shim" fi done } fi shopt -s nullglob # Create the prototype shim, then register shims for all known # executables. create_prototype_shim remove_outdated_shims # shellcheck disable=SC2046 make_shims $(list_executable_names | sort -u) # Allow plugins to register shims. OLDIFS="$IFS" IFS=$'\n' scripts=(`pyenv-hooks rehash`) IFS="$OLDIFS" for script in "${scripts[@]}"; do source "$script" done install_registered_shims remove_stale_shims