#!/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"

declare last_acquire_error

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
  last_acquire_error="$( { ( echo -n > "$PROTOTYPE_SHIM_PATH"; ) 2>&1 1>&3 3>&1-; } 3>&1)" || ret=1
  set +o noclobber
  [ -z "${ret}" ]
}

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" >&2
  exit 1
fi

declare acquired tested_for_other_write_errors
declare start=$SECONDS
PYENV_REHASH_TIMEOUT=${PYENV_REHASH_TIMEOUT:-60}
while (( SECONDS <= start + PYENV_REHASH_TIMEOUT )); do
  if acquire_lock; then
    acquired=1

    # 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

    break
  else
    #Landlock sandbox subsystem in the Linux kernel returns false information in access() as of 6.14.0,
    # making -w "$SHIM_PATH" not catch the fact that the shims dir is not writable in this case.
    #Bash doesn't provide access to errno to check for non-EEXIST error code in acquire_lock.
    #So check for writablity by trying to write to a different file,
    # in a way that taxes the usual use case as little as possible.
    if [[ -z $tested_for_other_write_errors ]]; then
      ( t="$(TMPDIR="$SHIM_PATH" mktemp)" && rm "$t" ) && tested_for_other_write_errors=1 ||
        { echo "pyenv: cannot rehash: $SHIM_PATH isn't writable" >&2; break; }
    fi
    # 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
  if [[ -n $tested_for_other_write_errors ]]; then
      echo "pyenv: cannot rehash: couldn't acquire lock"\
        "$PROTOTYPE_SHIM_PATH for $PYENV_REHASH_TIMEOUT seconds. Last error message:" >&2
      echo "$last_acquire_error" >&2
  fi
  exit 1
fi
unset tested_for_other_write_errors

# 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" <<SH
#!/usr/bin/env bash
set -e
[ -n "\$PYENV_DEBUG" ] && set -x

program="\${0##*/}"

export PYENV_ROOT="$PYENV_ROOT"
SHIM_PATH=\${0%/*}
if [[ \$SHIM_PATH != "$PYENV_ROOT/shims" ]]; then
  export _PYENV_SHIM_PATH="\$SHIM_PATH"
fi
exec "$(command -v pyenv)" exec "\$program" "\$@"
SH
  chmod +x "$PROTOTYPE_SHIM_PATH"
}

# If the contents of the prototype shim file differ from the contents
# of the first shim in the shims directory, assume pyenv has been
# upgraded and the existing shims need to be removed.
remove_outdated_shims() {
  local shim
  for shim in "$SHIM_PATH"/*; do
    if ! diff "$PROTOTYPE_SHIM_PATH" "$shim" >/dev/null 2>&1; then
      rm -f "$SHIM_PATH"/*
    fi
    break
  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 $(pyenv-versions --executables)


# 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
