diff --git a/CHANGELOG.md b/CHANGELOG.md index 3076247..f68167a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,35 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [6.0.0] (date goes here) +### Added + +- version specified using release stream codenames, like `argon` ([#423]) +- version specified using nightly et al ([#376]) +- `n exec` for running arbitrary command with node and npm in `PATH` ([#185]) +- `n run` with legacy aliases of `as` and `use` +- `n lsr` for listing matching remote versions, limited to 20 by default ([#383]) +- `n doctor` for displaying diagnostic information +- `n install` for people used to other products with this command +- `--insecure` to disable curl/wget certificate checks +- npm version to installed message ([#210] [#484] [#574]) + ### Changed -- wget now checks certificates (secure by default, same as curl setup). (#475 #509 ) +- **Breaking** wget now checks certificates (secure by default, same as curl setup). (#475 #509) +- failure messages go to stderr instead of stdout +- prefixed `N_NODE_MIRROR` to eventually replace `NODE_MIRROR` +- **Breaking** `n ls` now lists local download versions (rather than remote versions) +- lookup available versions using `index.tab` rather than screen-scraping (#560) + +### Fixed + +- download errors display informative message, instead of just `Invalid version` ([#482] [#492] et al) +- improve reliability of downloads from custom node mirrors, including removing broken `is_oss_ok` ([#560]) +- restrict downloads to versions with architecture available ([#463]) ## Removed -- support for `PROJECT_NAME` and `PROJECT_URL` for custom downloads (#342) +- **Breaking** support for `PROJECT_NAME` and `PROJECT_URL` for custom downloads ([#342]) ## [5.0.2] (2019-08-02) @@ -128,22 +150,32 @@ Only minor functional changes, but technically could break scripts relying on sp [#169]: https://github.com/tj/n/issues/169 +[#185]: https://github.com/tj/n/issues/185 [#187]: https://github.com/tj/n/issues/187 +[#210]: https://github.com/tj/n/issues/210 [#292]: https://github.com/tj/n/issues/292 [#327]: https://github.com/tj/n/issues/327 [#331]: https://github.com/tj/n/issues/331 [#335]: https://github.com/tj/n/issues/335 +[#342]: https://github.com/tj/n/issues/342 [#367]: https://github.com/tj/n/issues/367 +[#376]: https://github.com/tj/n/issues/376 +[#383]: https://github.com/tj/n/issues/383 [#391]: https://github.com/tj/n/issues/391 [#400]: https://github.com/tj/n/issues/400 [#416]: https://github.com/tj/n/issues/416 +[#423]: https://github.com/tj/n/issues/423 [#441]: https://github.com/tj/n/issues/441 [#448]: https://github.com/tj/n/issues/448 [#456]: https://github.com/tj/n/issues/456 +[#463]: https://github.com/tj/n/issues/463 [#465]: https://github.com/tj/n/issues/465 [#466]: https://github.com/tj/n/issues/466 [#467]: https://github.com/tj/n/issues/467 +[#482]: https://github.com/tj/n/issues/482 +[#484]: https://github.com/tj/n/issues/484 [#485]: https://github.com/tj/n/issues/485 +[#492]: https://github.com/tj/n/issues/492 [#512]: https://github.com/tj/n/issues/512 [#516]: https://github.com/tj/n/issues/516 [#518]: https://github.com/tj/n/issues/518 @@ -158,7 +190,9 @@ Only minor functional changes, but technically could break scripts relying on sp [#541]: https://github.com/tj/n/issues/541 [#545]: https://github.com/tj/n/issues/545 [#548]: https://github.com/tj/n/issues/548 +[#560]: https://github.com/tj/n/issues/560 [#562]: https://github.com/tj/n/issues/562 +[#574]: https://github.com/tj/n/issues/574 diff --git a/README.md b/README.md index 0cce2a8..e15f1b7 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,17 @@ Node.js version management: no subshells, no profile setup, no convoluted API, j (Note: `n` is not supported natively on Windows.) -- [`n` – Interactively Manage Your Node.js Versions](#n-%E2%80%93-Interactively-Manage-Your-Nodejs-Versions) - - [Installation](#Installation) - - [Third Party Installers](#Third-Party-Installers) - - [Installing/Activating Node Versions](#InstallingActivating-Node-Versions) - - [Removing Versions](#Removing-Versions) - - [Binary Usage](#Binary-Usage) - - [Help](#Help) - - [Custom Source](#Custom-Source) - - [Custom Architecture](#Custom-Architecture) - - [Optional Environment Variables](#Optional-Environment-Variables) +- [`n` – Interactively Manage Your Node.js Versions](#n-%e2%80%93-interactively-manage-your-nodejs-versions) + - [Installation](#installation) + - [Third Party Installers](#third-party-installers) + - [Installing Node Versions](#installing-node-versions) + - [Specifying Node Versions](#specifying-node-versions) + - [Removing Versions](#removing-versions) + - [Using Downloaded Node Versions Without Reinstalling](#using-downloaded-node-versions-without-reinstalling) + - [Miscellaneous](#miscellaneous) + - [Custom Source](#custom-source) + - [Custom Architecture](#custom-architecture) + - [Optional Environment Variables](#optional-environment-variables) ## Installation @@ -36,15 +37,13 @@ to install `n` to `bin/n` of the directory specified in the environment variable Once installed, `n` caches `node` versions in subdirectory `n/versions` of the directory specified in environment variable `N_PREFIX`, which defaults to `/usr/local`; and the _active_ `node` version is installed directly in `N_PREFIX`. -To avoid requiring `sudo` for `n` and `npm` global installs, it is recommended you either install to your home directory using `N_PREFIX`, or take ownership of the system directories: +To avoid requiring `sudo` for `n` and `npm` global installs, it is suggested you either install to your home directory using `N_PREFIX`, or take ownership of the system directories: -```bash -# make cache folder (if missing) and take ownership -sudo mkdir -p /usr/local/n -sudo chown -R $(whoami) /usr/local/n -# take ownership of node install destination folders -sudo chown -R $(whoami) /usr/local/bin /usr/local/lib /usr/local/include /usr/local/share -``` + # make cache folder (if missing) and take ownership + sudo mkdir -p /usr/local/n + sudo chown -R $(whoami) /usr/local/n + # take ownership of node install destination folders + sudo chown -R $(whoami) /usr/local/bin /usr/local/lib /usr/local/include /usr/local/share ### Third Party Installers @@ -60,17 +59,14 @@ n-install sets both `PREFIX` and `N_PREFIX` to `$HOME/n`, installs `n` to `$HOME As a result, both `n` itself and all `node` versions it manages are hosted inside a single, optionally configurable directory, which you can later remove with the included `n-uninstall` script. `n-update` updates `n` itself to the latest version. See the [n-install repo](https://github.com/mklement0/n-install) for more details. -## Installing/Activating Node Versions +## Installing Node Versions -Simply execute `n ` to install a version of `node`. If `` has already been installed (via `n`), `n` will activate that version. -A leading `v` is optional, and a partial version number installs the newest matching version. +Simply execute `n ` to download and install a version of `node`. If `` has already been downloaded, `n` will install from its cache. - n 4.9.1 - n 10 - n v8.11.3 + n 10.16.0 + n lts -Execute `n` on its own to view your currently installed versions. Use the up and down arrow keys to navigate and press enter to select. Use `q` or ^C (control + C) to exit the selection screen. -If you like vim key bindings during the selection of node versions, you can use `j` and `k` to navigate up or down without using arrows. +Execute `n` on its own to view your downloaded versions, and install the selected version. $ n @@ -78,15 +74,37 @@ If you like vim key bindings during the selection of node versions, you can use ο node/8.11.3 node/10.15.0 -Use or install the latest official release: + Use up/down arrow keys to select a version, return key to install, q to quit - n latest +(You can also use `j` and `k` to navigate up or down without using arrows.) -Use or install the latest LTS official release: +If the active node version does not change after install, try opening a new shell in case seeing a stale version. - n lts +## Specifying Node Versions -(If the active node version does not change after install, try opening a new shell in case seeing a stale version.) +There are a variety of ways of specifying the target node version for `n` commands. Most commands use the latest matching version, and `n ls-remote` lists multiple matching versions. + +Numeric version numbers can be complete or incomplete, with an optional leading `v`. + +- `4.9.1` +- `8`: 8.x.y versions +- `v6.1`: 6.1.x versions + +There are labels for two especially useful versions: + +- `lts`: newest Long Term Support official release +- `latest`, `current`: newest official release + +There is support for release streams: + +- `argon`, `boron`, `carbon`: codenames for LTS release streams + +The last form is for specifying [other releases](https://nodejs.org/download) available using the name of the remote download folder optionally followed by the complete or incomplete version. + +- `chakracore-release/latest` +- `nightly` +- `test/v11.0.0-test20180528` +- `rc/10` ## Removing Versions @@ -104,68 +122,52 @@ wish to use node and npm, or are switching to a different way of managing them. n uninstall -## Binary Usage +## Using Downloaded Node Versions Without Reinstalling -When running multiple versions of `node`, we can target -them directly by asking `n` for the binary path: +There are three commands for working directly with your downloaded versions of `node`, without reinstalling. - $ n bin 0.9.4 - /usr/local/n/versions/0.9.4/bin/node +You can show the path to the downloaded version: -Or by using a specific version through `n`'s `use` sub-command: + $ n which 6.14.3 + /usr/local/n/versions/6.14.3/bin/node - n use 0.9.4 some.js +Or run a downloaded `node` version with the `n run` command: -Flags also work here: + n run 8.11.3 --debug some.js - n as 0.9.4 --debug some.js +Or execute a command with `PATH` modified so `node` and `npm` will be from the downloaded `node` version. +(NB: this `npm` will be working with a different and empty global node_modules directory, and you should not install global +modules this way.) -## Help + n exec 10 my-script --fast test -Output can also be obtained from `n --help`. +## Miscellaneous - Usage: n [options/env] [COMMAND] [args] +Command line help can be obtained from `n --help`. - Environments: - n [COMMAND] [args] Uses default env (node) +List matching remote versions available for download: - Commands: + n ls-remote lts + n ls-remote latest + n lsr 10 + n --all lsr - n Output versions installed - n latest Install or activate the latest node release - n -a x86 latest As above but force 32 bit architecture - n lts Install or activate the latest LTS node release - n Install node - n use [args ...] Execute node with [args ...] - n bin Output bin path for - n rm Remove the given version(s) - n prune Remove all versions except the active version - n --latest Output the latest node version available - n --lts Output the latest LTS node version available - n ls Output the versions of node available +List downloaded versions in cache: - Options: + nvh ls - -V, --version Output version of n - -h, --help Display help information - -q, --quiet Disable curl output (if available) - -d, --download Download only - -a, --arch Override system architecture +Display diagnostics to help resolve problems: - Aliases: - - which bin - use as - list ls - - rm - stable lts + nvh doctor ## Custom Source -If you would like to use a different node mirror which has the same layout as the default , you can define `NODE_MIRROR`. +If you would like to use a different node mirror which has the same layout as the default , you can define `N_NODE_MIRROR`. The most common example is users in China can define: - export NODE_MIRROR=https://npm.taobao.org/mirrors/node + export N_NODE_MIRROR=https://npm.taobao.org/mirrors/node + +There is also `N_NODE_DOWNLOAD_MIRROR` for a different mirror with same layout as the default ## Custom Architecture @@ -193,5 +195,7 @@ By default `n` downloads archives from the mirror site which have been compresse In brief: -- `NODE_MIRROR`: See [Custom source](#custom-source) +- `N_NODE_MIRROR`: See [Custom source](#custom-source) +- `N_NODE_DOWNLOAD_MIRROR`: See [Custom source](#custom-source) - support for [NO_COLOR](http://no-color.org) and [CLICOLOR=0](https://bixense.com/clicolors) for controlling use of ANSI color codes +- `N_MAX_REMOTE_MATCHES` to change the default `ls-remote` maximum of 20 matching versions diff --git a/bin/n b/bin/n index 5ce433a..37778cf 100755 --- a/bin/n +++ b/bin/n @@ -15,7 +15,25 @@ log() { # abort() { - printf "\n ${SGR_RED}Error: %s${SGR_RESET}\n\n" "$*" && exit 1 + >&2 printf "\n ${SGR_RED}Error: %s${SGR_RESET}\n\n" "$*" && exit 1 +} + +# +# Synopsis: trace message ... +# Debugging output to stderr, not used in production code. +# + +function trace() { + >&2 printf "trace: %s\n" "$*" +} + +# +# Synopsis: echo_red message ... +# Highlight message in colour (on stdout). +# + +function echo_red() { + printf "${SGR_RED}%s${SGR_RESET}\n" "$*" } # @@ -23,8 +41,11 @@ abort() { # VERSION="6.0.0-0" + N_PREFIX="${N_PREFIX-/usr/local}" -BASE_VERSIONS_DIR="$N_PREFIX/n/versions" +N_PREFIX=${N_PREFIX%/} +readonly N_PREFIX +readonly CACHE_DIR=$N_PREFIX/n/versions N_NODE_MIRROR=${N_NODE_MIRROR:-${NODE_MIRROR:-https://nodejs.org/dist}} N_NODE_MIRROR=${N_NODE_MIRROR%/} @@ -68,6 +89,9 @@ elif [ -n "$HTTP_PASSWORD" ]; then abort "Must specify HTTP_USER when supplying HTTP_PASSWORD" fi +# Set by set_active_node +g_active_node= + ACTIVATE=true ARCH= @@ -109,6 +133,18 @@ set_arch() { fi } +# +# Synopsis: set_insecure +# Globals modified: +# - CURL_OPTIONS +# - WGET_OPTIONS +# + +function set_insecure() { + CURL_OPTIONS+=( "--insecure" ) + WGET_OPTIONS+=( "--no-check-certificate" ) +} + # # Synopsis: update_mirror_settings_for_version version # e.g. means using download mirror and folder is nightly @@ -219,63 +255,81 @@ handle_sigtstp() { display_help() { cat <<-EOF - Usage: n [options] [COMMAND] [args] +Usage: n [options] [COMMAND] [args] - Commands: +Commands: - n Output versions installed - n latest Install or activate the latest node release - n -a x86 latest As above but force 32 bit architecture - n lts Install or activate the latest LTS node release - n Install node - n use [args ...] Execute node with [args ...] - n bin Output bin path for - n rm Remove the given version(s) - n prune Remove all versions except the active version - n --latest Output the latest node version available - n --lts Output the latest LTS node version available - n ls Output the versions of node available - n uninstall Remove the installed node and npm + n Display downloaded node versions and install selection + n latest Install the latest node release (downloading if necessary) + n lts Install the latest LTS node release (downloading if necessary) + n Install node (downloading if necessary) + n run [args ...] Execute downloaded node with [args ...] + n which Output path for downloaded node + n exec [args...] Execute command with modified PATH, so downloaded node and npm first + n rm Remove the given downloaded version(s) + n prune Remove all downloaded versions except the installed version + n --latest Output the latest node version available + n --lts Output the latest LTS node version available + n ls Output downloaded versions + n ls-remote [version] Output matching versions available for download + n uninstall Remove the installed node and npm - Options: +Options: - -V, --version Output version of n - -h, --help Display help information - -q, --quiet Disable curl output (if available) - -d, --download Download only - -a, --arch Override system architecture + -V, --version Output version of n + -h, --help Display help information + -q, --quiet Disable curl output (if available) + -d, --download Download only + -a, --arch Override system architecture + --all ls-remote displays all matches instead of last 20 + --insecure Turn off certificate checking for https requests (may be needed from behind a proxy server) - Aliases: +Aliases: - which bin - use as - list ls - - rm - stable lts + which: bin + run: use, as + ls: list + lsr: ls-remote + rm: - + lts: stable + latest: current + +Versions: + + Numeric version numbers can be complete or incomplete, with an optional leading 'v'. + Versions can also be specified by label, or codename, + and other downloadable releases by / + + 4.9.1, 8, v6.1 Numeric versions + lts Newest Long Term Support official release + latest, current Newest official release + boron, carbon Codenames for release streams + and nightly, chakracore-release/latest, rc/10 et al EOF } err_no_installed_print_help() { - printf "\n ${SGR_RED}Error: no installed version${SGR_RESET}\n" display_help - exit 1 + abort "no downloaded versions yet, see above help for commands" } # -# Output version after selected. +# Synopsis: next_version_installed selected_version +# Output version after selected (which may be blank under some circumstances). # -next_version_installed() { - list_versions_installed | grep "$selected" -A 1 | tail -n 1 +function next_version_installed() { + display_cache_versions | grep "$1" -A 1 | tail -n 1 } # -# Output version before selected. +# Synopsis: prev_version_installed selected_version +# Output version before selected (which may be blank under some circumstances). # -prev_version_installed() { - list_versions_installed | grep "$selected" -B 1 | head -n 1 +function prev_version_installed() { + display_cache_versions | grep "$1" -B 1 | head -n 1 } # @@ -287,19 +341,26 @@ display_n_version() { } # -# Check for installed version, and populate $active +# Synopsis: set_active_node +# Checks cached downloads for a binary matching the active node. +# Globals modified: +# - g_active_node # -check_current_version() { - command -v node &> /dev/null - if test $? -eq 0; then - local current=$(node --version) - current=${current#v} - for bin in "${BINS[@]}"; do +function set_active_node() { + g_active_node= + local node_path="$(command -v node)" + if [[ -x "${node_path}" ]]; then + local installed_version=$(node --version) + installed_version=${installed_version#v} + for dir in "${CACHE_DIR}"/*/ ; do + local folder_name="${dir%/}" + folder_name="${folder_name##*/}" if diff &> /dev/null \ - "$BASE_VERSIONS_DIR/$bin/$current/bin/node" \ - "$(command -v node)" ; then - active="$bin/$current" + "${CACHE_DIR}/${folder_name}/${installed_version}/bin/node" \ + "${node_path}" ; then + g_active_node="${folder_name}/${installed_version}" + break fi done fi @@ -309,10 +370,10 @@ check_current_version() { # Display sorted versions directories paths. # -versions_paths() { - find "$BASE_VERSIONS_DIR" -maxdepth 2 -type d \ - | sed 's|'"$BASE_VERSIONS_DIR"'/||g' \ - | grep -E "/[0-9]+\.[0-9]+\.[0-9]+$" \ +display_versions_paths() { + find "$CACHE_DIR" -maxdepth 2 -type d \ + | sed 's|'"$CACHE_DIR"'/||g' \ + | grep -E "/[0-9]+\.[0-9]+\.[0-9]+" \ | sed 's|/|.|' \ | sort -k 1,1 -k 2,2n -k 3,3n -k 4,4n -t . \ | sed 's|\.|/|' @@ -325,7 +386,7 @@ versions_paths() { display_versions_with_selected() { selected="$1" echo - for version in $(versions_paths); do + for version in $(display_versions_paths); do if test "$version" = "$selected"; then printf " ${SGR_CYAN}ο${SGR_RESET} %s\n" "$version" else @@ -337,12 +398,12 @@ display_versions_with_selected() { } # -# List installed versions. +# Synopsis: display_cache_versions # -list_versions_installed() { - for version in $(versions_paths); do - echo "$version" +function display_cache_versions() { + for folder_and_version in $(display_versions_paths); do + echo "${folder_and_version}" done } @@ -350,11 +411,13 @@ list_versions_installed() { # Display current node --version and others installed. # -display_versions() { +menu_select_cache_versions() { enter_fullscreen - check_current_version + set_active_node + local selected="${g_active_node}" + clear - display_versions_with_selected "$active" + display_versions_with_selected "${selected}" trap handle_sigint INT trap handle_sigtstp SIGTSTP @@ -374,22 +437,26 @@ display_versions() { case "$arrow" in $UP) clear - display_versions_with_selected "$(prev_version_installed)" + selected="$(prev_version_installed "${selected}")" + display_versions_with_selected "${selected}" ;; $DOWN) clear - display_versions_with_selected "$(next_version_installed)" + selected="$(next_version_installed "${selected}")" + display_versions_with_selected "${selected}" ;; esac fi ;; "k") clear - display_versions_with_selected "$(prev_version_installed)" + selected="$(prev_version_installed "${selected}")" + display_versions_with_selected "${selected}" ;; "j") clear - display_versions_with_selected "$(next_version_installed)" + selected="$(next_version_installed "${selected}")" + display_versions_with_selected "${selected}" ;; "q") clear @@ -399,7 +466,7 @@ display_versions() { "") # enter key returns empty string leave_fullscreen - activate "$selected" + [[ -n "${selected}" ]] && activate "${selected}" exit ;; esac @@ -414,77 +481,6 @@ erase_line() { printf "\033[1A\033[2K" } -# -# Check if the HEAD response of is 200. -# -is_ok() { - if command -v curl > /dev/null; then - $GET -Is "$1" | head -n 1 | grep 200 > /dev/null - else - $GET -S --spider 2>&1 "$1" | head -n 1 | grep 200 > /dev/null - fi -} - -# -# Check if the OSS(Object Storage Service) mirror is ok. -# -is_oss_ok() { - if command -v curl > /dev/null; then - if $GET -Is $1 | head -n 1 | grep 302 > /dev/null; then - is_oss_ok $GET -Is $1 | grep Location | awk -F ': ' '{print $2}' - else - $GET -Is $1 | head -n 1 | grep 200 > /dev/null - fi - else - if $GET -S --spider 2>&1 $1 | head -n 1 | grep 302 > /dev/null; then - is_oss_ok $GET -S --spider 2>&1 $1 | grep Location | awk -F ': ' '{print $2}' - else - $GET -S --spider 2>&1 $1 | head -n 1 | grep 200 > /dev/null - fi - fi -} - -# -# Determine tarball url for -# - -tarball_url() { - local version=$1 - local uname="$(uname -a)" - local arch=x86 - local os= - local ext=gz - - # from nave(1) - case "$uname" in - Linux*) os=linux ;; - Darwin*) os=darwin ;; - SunOS*) os=sunos ;; - esac - - case "$uname" in - *x86_64*) arch=x64 ;; - *armv6l*) arch=armv6l ;; - *armv7l*) arch=armv7l ;; - *arm64*) arch=arm64 ;; - *aarch64*) arch=arm64 ;; - esac - - if [ "${arch}" = "armv6l" ] && [ "${BIN_NAME[$DEFAULT]}" = node ]; then - local semver=${version//./ } - local major="$(echo "$semver" | grep -o -E '[0-9]+' | head -1 | sed -e 's/^0\+//')" - local minor="$(echo "$semver" | awk '{print $2}' | grep -o -E '[0-9]+' | head -1 | sed -e 's/^0\+//')" - [[ "$major" -eq "" && "$minor" -lt 12 ]] && arch=arm-pi - fi - - [ -n "$ARCH" ] && arch="$ARCH" - - [ -n "$N_USE_XZ" ] && ext="xz" - - echo "${MIRROR[$DEFAULT]}v${version}/${BIN_NAME[$DEFAULT]}-v${version}-${os}-${arch}.tar.${ext}" - -} - # # Disable PaX mprotect for # @@ -516,7 +512,7 @@ disable_pax_mprotect() { activate() { local version="$1" - local dir=$BASE_VERSIONS_DIR/$version + local dir="$CACHE_DIR/$version" # Remove old npm to avoid potential issues with simple overwrite. if test -d "$dir/lib/node_modules/npm"; then if test -d "$N_PREFIX/lib/node_modules/npm"; then @@ -539,57 +535,29 @@ activate() { log "installed" "$("${installed_node}" --version) to ${installed_node}" log "active" "$("${active_node}" --version) at ${active_node}" else - log "installed" "$("${installed_node}" --version)" + local npm_version_str="" + local installed_npm="${N_PREFIX}/bin/npm" + local active_npm="$(command -v npm)" + if [[ -e "${active_npm}" && -e "${installed_npm}" && "${active_npm}" = "${installed_npm}" ]]; then + npm_version_str=" (with npm $(npm --version))" + fi + + log "installed" "$("${installed_node}" --version)${npm_version_str}" fi } -# -# Install latest version. -# - -install_latest() { - install "$(display_latest_version)" -} - -# -# Install latest stable version. -# (See additional comments for display_latest_stable_version) -# - -install_stable() { - install "$(display_latest_stable_version)" -} - -# -# Install latest LTS version. -# - -install_lts() { - install "$(display_latest_lts_version)" -} - # # Install # install() { - local version=${1#v} + [[ -z "$1" ]] && abort "version required" + local version + version="$(display_latest_resolved_version "$1")" || return 2 + [[ -n "${version}" ]] || abort "no version found for '$1'" + update_mirror_settings_for_version "$1" - local dots="${version//[^.]/}" - if test ${#dots} -lt 2; then - version="$($GET 2> /dev/null "${MIRROR[DEFAULT]}" \ - | grep -E "" \ - | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' \ - | grep -E -v '^0\.[0-7]\.' \ - | grep -E -v '^0\.8\.[0-5]$' \ - | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \ - | grep -E "^$version\." \ - | tail -n1)" - - test "$version" || abort "invalid version '${1#v}'" - fi - - local dir="${VERSIONS_DIR[$DEFAULT]}/$version" + local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" if test -n "$N_USE_XZ"; then local tarflag="-Jx" @@ -600,36 +568,33 @@ install() { if test -d "$dir"; then if [[ ! -e "$dir/n.lock" ]] ; then if "$ACTIVATE" ; then - activate "${BINS[$DEFAULT]}/$version" + activate "${g_mirror_folder_name}/${version}" fi exit fi fi echo - log installing "${BINS[$DEFAULT]}-v$version" + log installing "${g_mirror_folder_name}-v$version" local url="$(tarball_url "$version")" - is_ok "$url" || is_oss_ok "$url" || abort "invalid version '$version'" + is_ok "${url}" || abort "download preflight failed for '$version' (${url})" log mkdir "$dir" - if ! mkdir -p "$dir"; then - abort "sudo required" - else - touch "$dir/n.lock" - fi + mkdir -p "$dir" || abort "sudo required (or change ownership, or define N_PREFIX)" + touch "$dir/n.lock" - cd "$dir" + cd "${dir}" || abort "Failed to cd to ${dir}" log fetch "$url" - $GET "$url" | tar "$tarflag" --strip-components=1 + do_get "${url}" | tar "$tarflag" --strip-components=1 [ "$GET_SHOWS_PROGRESS" = "true" ] && erase_line rm -f "$dir/n.lock" disable_pax_mprotect bin/node if "$ACTIVATE" ; then - activate "${BINS[$DEFAULT]}/$version" + activate "${g_mirror_folder_name}/$version" fi echo } @@ -642,6 +607,21 @@ set_quiet() { command -v curl > /dev/null && CURL_OPTIONS+=( "--silent" ) && GET_SHOWS_PROGRESS="false" } +# +# Synopsis: do_get [option...] url +# Call curl or wget with combination of global and passed options. +# + +function do_get() { + if command -v curl &> /dev/null; then + curl "${CURL_OPTIONS[@]}" "$@" + elif command -v wget &> /dev/null; then + wget "${WGET_OPTIONS[@]}" "$@" + else + abort "curl or wget command required" + fi +} + # # Synopsis: do_get_index [option...] url # Call curl or wget with combination of global and passed options, @@ -660,166 +640,108 @@ function do_get_index() { } # -# Remove +# Synopsis: remove_versions version ... # -remove_versions() { - test -z "$1" && abort "version(s) required" - while test $# -ne 0; do - local version=${1#v} - rm -rf "${VERSIONS_DIR[$DEFAULT]}/$version" +function remove_versions() { + [[ -z "$1" ]] && abort "version(s) required" + while [[ $# -ne 0 ]]; do + local version + version="$(display_latest_resolved_version "$1")" || break + if [[ -n "${version}" ]]; then + update_mirror_settings_for_version "$1" + local dir="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + if [[ -s "${dir}" ]]; then + rm -rf "${dir}" + else + echo "$1 (${version}) not in downloads cache" + fi + else + echo "No version found for '$1'" + fi shift done } # -# Prune non-active versions +# Synopsis: prune_cache # -prune_versions() { - check_current_version - for version in $(versions_paths); do - if [ "$version" != "$active" ] - then - echo "$version" - rm -rf "${BASE_VERSIONS_DIR[$DEFAULT]}/$version" +function prune_cache() { + set_active_node + + for folder_and_version in $(display_versions_paths); do + if [[ "${folder_and_version}" != "${g_active_node}" ]]; then + echo "${folder_and_version}" + rm -rf "${CACHE_DIR:?}/${folder_and_version}" fi done } # -# Output bin path for +# Synopsis: find_cached_version version +# Finds cache directory for resolved version. +# Globals modified: +# - g_cached_version + +function find_cached_version() { + [[ -z "$1" ]] && abort "version required" + local version + version="$(display_latest_resolved_version "$1")" || exit 1 + [[ -n "${version}" ]] || abort "no version found for '$1'" + + update_mirror_settings_for_version "$1" + g_cached_version="${CACHE_DIR}/${g_mirror_folder_name}/${version}" + [[ -d "${g_cached_version}" ]] || abort "'$1' (${version}) not in downloads cache" +} + + +# +# Synopsis: display_bin_path_for_version version # -display_bin_path_for_version() { - test -z "$1" && abort "version required" - local version=${1#v} +function display_bin_path_for_version() { + find_cached_version "$1" + echo "${g_cached_version}/bin/node" +} - if [ "$version" = "latest" ]; then - version="$(display_latest_version)" - fi +# +# Synopsis: run_with_version version [args...] +# Run the given of node with [args ..] +# - if [ "$version" = "stable" ]; then - version="$(display_latest_stable_version)" - fi +function run_with_version() { + find_cached_version "$1" + shift # remove version from parameters + exec "${g_cached_version}/bin/node" "$@" +} - if [ "$version" = "lts" ]; then - version="$(display_latest_lts_version)" - fi +# +# Synopsis: exec_with_version command [args...] +# Modify the path to include and execute command. +# - local bin="${VERSIONS_DIR[$DEFAULT]}/$version/bin/node" - if test -f "$bin"; then - printf "%s\n" "$bin" +function exec_with_version() { + find_cached_version "$1" + shift # remove version from parameters + PATH="${g_cached_version}/bin:$PATH" exec "$@" +} + +# +# Synopsis: is_ok url +# Check the HEAD response of . +# + +function is_ok() { + # Note: both curl and wget can follow redirects, as present on some mirrors (e.g. https://npm.taobao.org/mirrors/node). + # The output is complicated with redirects, so keep it simple and use command status rather than parse output. + if command -v curl &> /dev/null; then + do_get --silent --head "$1" > /dev/null || return 1 else - abort "'$1' is not installed" + do_get --spider "$1" &> /dev/null || return 1 fi } -# -# Execute the given of node with [args ...] -# - -execute_with_version() { - test -z "$1" && abort "version required" - local version=${1#v} - - if [ "$version" = "latest" ]; then - version="$(display_latest_version)" - fi - - if [ "$version" = "stable" ]; then - version="$(display_latest_stable_version)" - fi - - if [ "$version" = "lts" ]; then - version="$(display_latest_lts_version)" - fi - - local bin="${VERSIONS_DIR[$DEFAULT]}/$version/bin/node" - - shift # remove version - - if test -f "$bin"; then - exec "$bin" "$@" - else - abort "'$version' is not installed" - fi -} - -# -# Display the latest release version. -# - -display_latest_version() { - $GET 2> /dev/null "${MIRROR[$DEFAULT]}" \ - | grep -E "" \ - | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' \ - | grep -E -v '^0\.[0-7]\.' \ - | grep -E -v '^0\.8\.[0-5]$' \ - | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \ - | tail -n1 -} - -# -# Display the latest stable release version. -# Note: Stable is no longer used by nodejs to identify versions, -# so this could be considered deprecated. To avoid breaking -# existing usages and minimise code churn in meantime, -# return the lts version. -# lts is the version recommended for most users. -# - -display_latest_stable_version() { - display_latest_lts_version -} - -# -# Display the latest lts release version. -# - -display_latest_lts_version() { - local folder_name="$($GET 2> /dev/null "${MIRROR[$DEFAULT]}" \ - | grep -E "" \ - | grep -E -o 'latest-[a-z]{2,}' \ - | sort \ - | tail -n1)" - - $GET 2> /dev/null "${MIRROR[$DEFAULT]}/$folder_name/" \ - | grep -E "" \ - | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' \ - | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \ - | tail -n1 -} - -# -# Display the versions available. -# - -display_remote_versions() { - check_current_version - local versions="" - versions="$($GET 2> /dev/null "${MIRROR[$DEFAULT]}" \ - | grep -E "" \ - | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' \ - | sort -u -k 1,1n -k 2,2n -k 3,3n -t . \ - | awk '{ print " " $1 }')" - - echo - local bin="${BINS[$DEFAULT]}" - for v in $versions; do - if test "$active" = "$bin/$v"; then - printf " ${SGR_CYAN}ο${SGR_RESET} %s\n" "$v" - else - if test -d "$BASE_VERSIONS_DIR/$bin/$v"; then - printf " %s\n" "$v" - else - printf " ${SGR_FAINT}%s${SGR_RESET}\n" "$v" - fi - fi - done - echo -} - # # Synopsis: display_tarball_platform # @@ -850,6 +772,7 @@ function display_tarball_platform() { ;; esac # (arch unlikely to be right for SunOS and AIX. Investigate further if/when anyone asks!) + [ -n "$ARCH" ] && arch="$ARCH" echo "${os}-${arch}" } @@ -867,6 +790,33 @@ function display_compatible_file_field { echo "${compatible_file_field}" } +# +# Synopsis: tarball_url version +# + +function tarball_url() { + local version="$1" + local ext=gz + [ -n "$N_USE_XZ" ] && ext="xz" + echo "${g_mirror_url}/v${version}/node-v${version}-$(display_tarball_platform).tar.${ext}" +} + +# +# Synopsis: display_latest_resolved_version version +# + +function display_latest_resolved_version() { + local version=${1#node/} + if is_exact_numeric_version "${version}"; then + # Just numbers, already resolved, no need to lookup first. + version="${version#v}" + echo "${version}" + else + # Complicated recognising exact version, KISS and awlays lookup for now. + N_MAX_REMOTE_MATCHES=1 display_remote_versions "$1" + fi +} + # # Synopsis: display_remote_index # index.tab reference: https://github.com/nodejs/nodejs-dist-indexer @@ -899,7 +849,7 @@ function display_match_limit(){ # Synopsis: display_remote_versions version # -function display_remote_versions2() { +function display_remote_versions() { local version="$1" update_mirror_settings_for_version "${version}" local match='.' @@ -1006,34 +956,223 @@ uninstall_installed() { delete_with_echo "${N_PREFIX}/share/systemtap/tapset/node.stp" } +# +# Synopsis: show_permission_suggestions +# + +function show_permission_suggestions() { + echo "Suggestions:" + echo "- run n with sudo, or" + echo "- define N_PREFIX to a writeable location, or" +} + +# +# Synopsis: show_diagnostics +# Show environment and check for common problems. +# + +function show_diagnostics() { + echo "This information is to help you diagnose issues, and useful when reporting an issue." + echo "Note: some output may contain passwords. Redact before sharing." + + printf "\n\n# Command Locations and Versions\n" + + printf "\n## bash\n" + command -v bash && bash --version + + printf "\n## n\n" + command -v n && n --version + + printf "\n## node\n" + if command -v node &> /dev/null; then + command -v node && node --version + node -e 'if (process.versions.v8) console.log("JavaScript engine: v8"); if (process.versions.chakracore) console.log("JavaScript engine: chakracore")' + + printf "\n## npm\n" + command -v npm && npm --version + fi + + printf "\n## tar\n" + if command -v tar &> /dev/null; then + command -v tar && tar --version + else + echo_red "tar not found. Needed for extracting downloads." + fi + + printf "\n## curl or wget\n" + if command -v curl &> /dev/null; then + command -v curl && curl --version + elif command -v wget &> /dev/null; then + command -v wget && wget --version + else + echo_red "Neither curl nor wget found. Need one of them for downloads." + fi + + printf "\n## uname\n" + uname -a + + + printf "\n\n# Settings\n" + + printf "\n\n## n\n" + echo "node mirror: ${N_NODE_MIRROR}" + echo "node downloads mirror: ${N_NODE_DOWNLOAD_MIRROR}" + echo "install destination: ${N_PREFIX}" + [[ -n "${N_PREFIX}" ]] && echo "PATH: ${PATH}" + echo "ls-remote max matches: ${N_MAX_REMOTE_MATCHES}" + + printf "\n\n## Proxy\n" + # disable "var is referenced but not assigned": https://github.com/koalaman/shellcheck/wiki/SC2154 + # shellcheck disable=SC2154 + [[ -n "${http_proxy}" ]] && echo "http_proxy: ${http_proxy}" + # shellcheck disable=SC2154 + [[ -n "${https_proxy}" ]] && echo "https_proxy: ${https_proxy}" + if command -v curl &> /dev/null; then + # curl supports lower case and upper case! + # shellcheck disable=SC2154 + [[ -n "${all_proxy}" ]] && echo "all_proxy: ${all_proxy}" + [[ -n "${ALL_PROXY}" ]] && echo "ALL_PROXY: ${ALL_PROXY}" + [[ -n "${HTTP_PROXY}" ]] && echo "HTTP_PROXY: ${HTTP_PROXY}" + [[ -n "${HTTPS_PROXY}" ]] && echo "HTTPS_PROXY: ${HTTPS_PROXY}" + if [[ -e "${CURL_HOME}/.curlrc" ]]; then + echo "have \$CURL_HOME/.curlrc" + elif [[ -e "${HOME}/.curlrc" ]]; then + echo "have \$HOME/.curlrc" + fi + elif command -v wget &> /dev/null; then + if [[ -e "${WGETRC}" ]]; then + echo "have \$WGETRC" + elif [[ -e "${HOME}/.wgetrc" ]]; then + echo "have \$HOME/.wgetrc" + fi + fi + + printf "\n\n# Checks\n" + + printf "\nChecking n install destination is in PATH...\n" + local install_bin="${N_PREFIX}/bin" + local path_wth_guards=":${PATH}:" + if [[ "${path_wth_guards}" =~ :${install_bin}/?: ]]; then + printf "good\n" + else + echo_red "'${install_bin}' is not in PATH" + fi + if command -v node &> /dev/null; then + printf "\nChecking n install destination priority in PATH...\n" + local node_dir="$(dirname "$(command -v node)")" + + local index=0 + local path_entry + local path_entries + local install_bin_index=0 + local node_index=999 + IFS=':' read -ra path_entries <<< "${PATH}" + for path_entry in "${path_entries[@]}"; do + (( index++ )) + [[ "${path_entry}" =~ ^${node_dir}/?$ ]] && node_index="${index}" + [[ "${path_entry}" =~ ^${install_bin}/?$ ]] && install_bin_index="${index}" + done + if [[ "${node_index}" -lt "${install_bin_index}" ]]; then + echo_red "There is a version of node installed which will be found in PATH before the n installed version." + else + printf "good\n" + fi + fi + + printf "\nChecking permissions for cache folder...\n" + # Most likely problem is ownership rather than than permissions as such. + local cache_root="${N_PREFIX}/n" + if [[ -e "${N_PREFIX}" && ! -w "${N_PREFIX}" && ! -e "${cache_root}" ]]; then + echo_red "You do not have write permission to create: ${cache_root}" + show_permission_suggestions + echo "- make a folder you own:" + echo " sudo mkdir -p \"${cache_root}\"" + echo " sudo chown $(whoami) \"${cache_root}\"" + elif [[ -e "${cache_root}" && ! -w "${cache_root}" ]]; then + echo_red "You do not have write permission to: ${cache_root}" + show_permission_suggestions + echo "- change folder ownership to yourself:" + echo " sudo chown -R $(whoami) \"${cache_root}\"" + elif [[ ! -e "${cache_root}" ]]; then + echo "Cache folder does not exist: ${cache_root}" + echo "This is normal if you have not done an install yet, as cache is only created when needed." + elif [[ -e "${CACHE_DIR}" && ! -w "${CACHE_DIR}" ]]; then + echo_red "You do not have write permission to: ${CACHE_DIR}" + show_permission_suggestions + echo "- change folder ownership to yourself:" + echo " sudo chown -R $(whoami) \"${CACHE_DIR}\"" + else + echo "good" + fi + + if [[ -e "${N_PREFIX}" ]]; then + # Most likely problem is ownership rather than than permissions as such. + printf "\nChecking permissions for install folders...\n" + local install_writeable="true" + for subdir in bin lib include share; do + if [[ -e "${N_PREFIX}/${subdir}" && ! -w "${N_PREFIX}/${subdir}" ]]; then + install_writeable="false" + echo_red "You do not have write permission to: ${N_PREFIX}/${subdir}" + break + fi + done + if [[ "${install_writeable}" = "true" ]]; then + echo "good" + else + show_permission_suggestions + echo "- change folder ownerships to yourself:" + echo " (cd \"${N_PREFIX}\" && sudo chown -R $(whoami) bin lib include share)" + fi + fi + + printf "\nChecking mirror is reachable...\n" + if is_ok "${N_NODE_MIRROR}/"; then + printf "good\n" + else + echo_red "mirror not reachable" + printf "Showing failing command and output\n" + if command -v curl &> /dev/null; then + ( set -x; do_get --head "${N_NODE_MIRROR}/" ) + else + ( set -x; do_get --spider "${N_NODE_MIRROR}/" ) + printf "\n" + fi + fi +} + # # Handle arguments. # if test $# -eq 0; then - test -z "$(versions_paths)" && err_no_installed_print_help - display_versions + test -z "$(display_versions_paths)" && err_no_installed_print_help + menu_select_cache_versions else while test $# -ne 0; do case "$1" in + --all) N_MAX_REMOTE_MATCHES=32000 ;; -V|--version) display_n_version ;; -h|--help|help) display_help; exit ;; -q|--quiet) set_quiet ;; -d|--download) ACTIVATE=false ;; - --latest) display_latest_version; exit ;; - --stable) display_latest_stable_version; exit ;; - --lts) NVH_MAX_REMOTE_MATCHES=1 display_remote_versions2 lts; exit ;; + --insecure) set_insecure ;; + --latest) display_remote_versions latest; exit ;; + --stable) display_remote_versions lts; exit ;; # [sic] old terminology + --lts) display_remote_versions lts; exit ;; -a|--arch) shift; set_arch "$1";; # set arch and continue bin|which) display_bin_path_for_version "$2"; exit ;; - as|use) shift; execute_with_version "$@"; exit ;; + run|as|use) shift; run_with_version "$@"; exit ;; + exec) shift; exec_with_version "$@" ;; + doctor) show_diagnostics ;; rm|-) shift; remove_versions "$@"; exit ;; - prune) prune_versions; exit ;; - latest) install_latest; exit ;; - stable) install_stable; exit ;; - lts) install_lts; exit ;; - ls|list) display_remote_versions; exit ;; - lsr|ls-remote|list-remote) shift; display_remote_versions2 "$1" ;; + prune) prune_cache; exit ;; + latest) install latest; exit ;; + stable) install stable; exit ;; + lts) install lts; exit ;; + ls|list) display_versions_paths ;; + lsr|ls-remote|list-remote) shift; display_remote_versions "$1" ;; uninstall) uninstall_installed ;; + i|install) shift; install "$1"; exit ;; *) install "$1"; exit ;; esac shift