diff --git a/nvm.sh b/nvm.sh index d5d4c088..d2e7f680 100755 --- a/nvm.sh +++ b/nvm.sh @@ -218,6 +218,52 @@ nvm_is_version_installed() { return 1 } +# Sanity-check an installed version's layout: a non-empty, executable bin/node +# and, if present, an npm entry that resolves. nvm_is_version_installed only +# tests the bin/node exec bit, which a zero-byte binary and a dangling npm +# symlink both pass; this catches those, so a partial install is not reported +# as a success. It deliberately does NOT execute node: a correctly installed +# binary can still fail to run on an incompatible host (e.g. a newer node on an +# older glibc), which is not a broken install - and a corrupt download is +# already rejected by the checksum check before extraction. +nvm_validate_install() { + local VERSION + VERSION="${1-}" + if [ -z "${VERSION}" ]; then + return 1 + fi + + local VERSION_PATH + VERSION_PATH="$(nvm_version_path "${VERSION}" 2>/dev/null)" + if [ -z "${VERSION_PATH}" ] || [ ! -d "${VERSION_PATH}" ]; then + return 1 + fi + + local NVM_NODE_BINARY + NVM_NODE_BINARY='node' + if [ "_$(nvm_get_os)" = '_win' ]; then + NVM_NODE_BINARY='node.exe' + fi + + local NVM_NODE_PATH + NVM_NODE_PATH="${VERSION_PATH}/bin/${NVM_NODE_BINARY}" + # A zero-byte file with the exec bit set still passes `[ -x ]` (the shell + # would run it as an empty script), so require a non-empty executable. + if [ ! -s "${NVM_NODE_PATH}" ] || [ ! -x "${NVM_NODE_PATH}" ]; then + nvm_err "The installed node binary at ${NVM_NODE_PATH} is missing or empty." + return 1 + fi + + # npm ships with every supported node/io.js version; if its entry is a + # symlink it must resolve (one pointing at a removed target counts as broken). + if [ -h "${VERSION_PATH}/bin/npm" ] && [ ! -e "${VERSION_PATH}/bin/npm" ]; then + nvm_err "npm for ${VERSION} is a dangling symlink." + return 1 + fi + + return 0 +} + nvm_print_npm_version() { if nvm_has "npm"; then local NPM_VERSION @@ -3817,7 +3863,7 @@ nvm() { EXIT_CODE=0 - if nvm_is_version_installed "${VERSION}"; then + if nvm_is_version_installed "${VERSION}" && nvm_validate_install "${VERSION}"; then nvm_err "${VERSION} is already installed." nvm use "${VERSION}" EXIT_CODE=$? @@ -3919,6 +3965,11 @@ nvm() { fi fi + if [ $EXIT_CODE -eq 0 ] && ! nvm_validate_install "${VERSION}"; then + nvm_err "The install of ${VERSION} reported success but failed verification; not activating it." + EXIT_CODE=1 + fi + if [ $EXIT_CODE -eq 0 ]; then if nvm_use_if_needed "${VERSION}" && nvm_install_npm_if_needed "${VERSION}"; then if [ -n "${LTS-}" ]; then @@ -4789,7 +4840,7 @@ nvm() { nvm_echo nvm_err nvm_grep nvm_cd \ nvm_die_on_prefix nvm_get_make_jobs nvm_get_minor_version \ nvm_has_solaris_binary nvm_is_merged_node_version \ - nvm_is_natural_num nvm_is_version_installed \ + nvm_is_natural_num nvm_is_version_installed nvm_validate_install \ nvm_list_aliases nvm_make_alias nvm_print_alias_path \ nvm_print_default_alias nvm_print_formatted_alias nvm_resolve_local_alias \ nvm_sanitize_path nvm_has_colors nvm_process_parameters \ diff --git a/test/fast/Unit tests/nvm install verifies the installed version b/test/fast/Unit tests/nvm install verifies the installed version new file mode 100755 index 00000000..8ac87399 --- /dev/null +++ b/test/fast/Unit tests/nvm install verifies the installed version @@ -0,0 +1,71 @@ +#!/bin/sh + +cleanup() { + [ -n "${NVM_DIR}" ] && [ -d "${NVM_DIR}" ] && rm -rf "${NVM_DIR}" + unset -f die cleanup make_healthy make_broken \ + nvm_remote_version nvm_has_executable nvm_binary_available \ + nvm_install_binary nvm_install_source nvm_use_if_needed \ + nvm_install_npm_if_needed nvm_install_default_packages nvm_ensure_default_set + unset NVM_DIR version version_path REINSTALL_MARKER OUTPUT EXIT_CODE +} + +die() { echo "$@" ; cleanup ; exit 1; } + +: nvm.sh +\. ../../../nvm.sh + +unset npm_config_prefix NPM_CONFIG_PREFIX + +NVM_DIR="$(mktemp -d)" +[ -n "${NVM_DIR}" ] || die 'unable to create temp NVM_DIR' + +version='v20.0.0' +version_path="${NVM_DIR}/versions/node/${version}" +REINSTALL_MARKER="${NVM_DIR}/reinstalled" + +# Stay offline and keep the post-install activation path inert; only the +# verification behaviour is under test here. +nvm_remote_version() { nvm_echo "${version}"; } +nvm_has_executable() { return 0; } +nvm_binary_available() { return 0; } +nvm_install_source() { return 1; } +nvm_use_if_needed() { return 0; } +nvm_install_npm_if_needed() { return 0; } +nvm_install_default_packages() { return 0; } +nvm_ensure_default_set() { return 0; } + +make_healthy() { + mkdir -p "${version_path}/bin" + printf '#!/bin/sh\necho "%s"\n' "${version}" > "${version_path}/bin/node" + chmod +x "${version_path}/bin/node" +} +make_broken() { + mkdir -p "${version_path}/bin" + : > "${version_path}/bin/node" # zero-byte but +x: passes nvm_is_version_installed + chmod +x "${version_path}/bin/node" +} + +# 1) An install that reports success but leaves a broken node must fail loudly +# and must not be activated. +rm -rf "${version_path}" +nvm_install_binary() { make_broken; return 0; } +OUTPUT="$(nvm install -b "${version}" 2>&1)"; EXIT_CODE=$? +[ "${EXIT_CODE}" != "0" ] || die "a broken install should fail; got exit 0, output >${OUTPUT}<" +case "${OUTPUT}" in + *'failed verification'*) ;; + *) die "expected a verification-failure message; got >${OUTPUT}<" ;; +esac + +# 2) An already-present but broken version must not short-circuit as "already +# installed"; it must be reinstalled. +rm -rf "${version_path}" +make_broken +nvm_install_binary() { command touch "${REINSTALL_MARKER}"; make_healthy; return 0; } +OUTPUT="$(nvm install -b "${version}" 2>&1)"; EXIT_CODE=$? +case "${OUTPUT}" in + *'is already installed'*) die "a broken version should not be treated as already installed: >${OUTPUT}<" ;; +esac +[ -f "${REINSTALL_MARKER}" ] || die 'a broken already-present version was not reinstalled' +[ "${EXIT_CODE}" = "0" ] || die "the healing reinstall should succeed; got ${EXIT_CODE}, output >${OUTPUT}<" + +cleanup diff --git a/test/fast/Unit tests/nvm_validate_install b/test/fast/Unit tests/nvm_validate_install new file mode 100755 index 00000000..d3a2e9c4 --- /dev/null +++ b/test/fast/Unit tests/nvm_validate_install @@ -0,0 +1,53 @@ +#!/bin/sh + +cleanup() { + [ -n "${NVM_DIR}" ] && [ -d "${NVM_DIR}" ] && rm -rf "${NVM_DIR}" + unset -f die cleanup + unset NVM_DIR version version_path +} + +die() { echo "$@" ; cleanup ; exit 1; } + +: nvm.sh +\. ../../../nvm.sh + +type nvm_validate_install > /dev/null 2>&1 || die 'nvm_validate_install is not available' + +NVM_DIR="$(mktemp -d)" +[ -n "${NVM_DIR}" ] || die 'Unable to create temporary folder' + +version='v20.0.0' +version_path="${NVM_DIR}/versions/node/${version}" + +# No version directory at all: not valid. +nvm_validate_install "${version}" 2>/dev/null && die 'a missing version dir should not validate' + +# A healthy install: a non-empty node plus an npm symlink that resolves. +# (validation checks layout, not execution, so bin/node need not be a real +# runnable binary here - just non-empty and executable.) +mkdir -p "${version_path}/bin" "${version_path}/lib/node_modules/npm/bin" || die 'setup mkdir failed' +printf '#!/bin/sh\necho "%s"\n' "${version}" > "${version_path}/bin/node" +chmod +x "${version_path}/bin/node" +echo 'npm-cli' > "${version_path}/lib/node_modules/npm/bin/npm-cli.js" +( cd "${version_path}/bin" && ln -s ../lib/node_modules/npm/bin/npm-cli.js npm ) +nvm_validate_install "${version}" 2>/dev/null || die 'a healthy install should validate' + +# A binary that is correctly installed but cannot run on this host (e.g. a +# newer node on an older glibc) must STILL validate: whether it runs is the +# host's concern, not a broken install. +printf '#!/bin/sh\nexit 1\n' > "${version_path}/bin/node" +chmod +x "${version_path}/bin/node" +nvm_validate_install "${version}" 2>/dev/null || die 'a non-runnable but present binary should still validate' + +# A zero-byte but executable node: passes `[ -x ]` but must not validate. +: > "${version_path}/bin/node" +chmod +x "${version_path}/bin/node" +nvm_validate_install "${version}" 2>/dev/null && die 'a zero-byte node should not validate' + +# A non-empty node again, but with a dangling npm symlink: must not validate. +printf '#!/bin/sh\necho "%s"\n' "${version}" > "${version_path}/bin/node" +chmod +x "${version_path}/bin/node" +rm -f "${version_path}/lib/node_modules/npm/bin/npm-cli.js" +nvm_validate_install "${version}" 2>/dev/null && die 'a dangling npm symlink should not validate' + +cleanup