#!/bin/bash # This program contains parts of narwhal's "sea" program, # as well as bits borrowed from Tim Caswell's "nvm" # nave install # Fetch the version of node and install it in nave's folder. # nave use # Install the if it isn't already, and then start # a subshell with that version's folder at the start of the # $PATH # nave use program.js # Like "nave use", but have the subshell start the program.js # immediately. # When told to use a version: # Ensure that the version exists, install it, and # then add its prefix to the PATH, and start a subshell. if [ "$NAVE_DEBUG" != "" ]; then set -x fi if [ -z "$BASH" ]; then cat >&2 <&2 } fail () { err "$@" [ -z $_TESTING_NAVE_NO_EXIT ] && exit 1 } nave_fetch () { local version=$(ver "$1") if nave_has "$version"; then return 0 fi local src="$NAVE_SRC/$version" rimraf "$src" mkdirp "$src" local tarfile tarfile="$(get "v$version/node-v$version.tar.gz" -#Lf)" if [ $? -eq 0 ]; then cp "$tarfile" "$src".tgz $tar xz -C "$src" --strip-components=1 < "$src".tgz if [ $? -eq 0 ]; then err "fetched $version" return 0 fi fi rm "$src".tgz rimraf "$src" err "Couldn't fetch $version" return 1 } get_shasum () { local dir=$1 local base=$2 get_html "$dir/SHASUMS256.txt" | grep "$base" | awk '{print $1}' } get_tgz () { local path=$1 local base=$(basename "$path") local dir=$(dirname "$path") shift local shasum=$(get_shasum "$dir" "$base") local cache=$NAVE_DIR/cache if [ "$shasum" == "" ]; then # this should not happen, blow away cache and try 1 more time rm -- "$cache/$dir/SHASUMS256.txt"* || true shasum=$(get_shasum "$dir" "$base") fi if [ "$shasum" == "" ]; then err "shasum not found for $base. aborting download." return 2 fi if [ -f "$cache/$dir/$shasum.tgz" ]; then echo "$cache/$dir/$shasum.tgz" return fi get_ "$NODEDIST/$path" "$@" > "$cache/$dir/$base" if [ $? -ne 0 ]; then rm "$cache/$dir/$base" return 2 fi local actualshasum=$(shasum -a 256 "$cache/$dir/$base" | awk '{print $1}') if ! [ "$shasum" = "$actualshasum" ]; then err "shasum mismatch, expect $shasum, got $actualshasum" rm "$cache/$dir/$base" return 2 fi mv "$cache/$dir/$base" "$cache/$dir/$shasum.tgz" echo "$cache/$dir/$shasum.tgz" } get_timestamp () { local file=$1 local n=$(cat $file 2>/dev/null) local numre='^[0-9]+$' if [ -n "$n" ] && [[ $n =~ $numre ]]; then echo $n else echo 0 fi } get_html () { local path=$1 local base=$(basename "$path") local dir=$(dirname "$path") shift if [ "$dir" = "/" ]; then dir="" fi if [ "$base" = "/" ]; then base="" fi if [ "$base" = "" ]; then base="index.html" fi local cache="$NAVE_DIR/cache/$dir" mkdirp "$cache" local tsfile="$cache/${base}-timestamp" local dur=$NAVE_CACHE_DUR if ! [ "$cache/$base" = "$cache/index.html" ]; then dur=$[ $dur * 365 ] fi if [ -f "$tsfile" ] && \ [ -f "$cache/$base" ] && \ [ $[ $(date '+%s') - $(get_timestamp $tsfile) ] -lt $dur ]; then cat "$cache/$base" else get_ "$NODEDIST/$path" -s "$@" > "$cache/${base}.tmp" local ret=$? if [ "$ret" -ne 0 ]; then rm "$cache/${base}.tmp" ret=2 if [ -f "$cache/$base" ]; then cat "$cache/$base" ret=$cacheret fi else mv "$cache/${base}.tmp" "$cache/$base" date '+%s' > "$tsfile" cat "$cache/$base" fi return $ret fi } get_ () { curl "$@" -f -H "user-agent:$NAVEUA" || return 2 } get () { local path=$1 local base=$(basename "$path") local dir=$(dirname "$path") shift case "$base" in *.tar.gz|*.tgz) get_tgz "$path" "$@" ;; *) get_html "$path" "$@" ;; esac } bin_available () { local version="$1" if [ "$NAVE_SRC_ONLY" = "1" ]; then return 1 elif osarch >/dev/null; then # binaries started with node 0.8.6 case "$version" in 0.8.[012345]) return 1 ;; 0.[01234567].*) return 1 ;; *) return 0 ;; esac else return 1 fi } build_binary () { local version="$1" if ! bin_available "$version"; then return 1 fi local targetfolder="$2" local t="$version-$(osarch)" local url="v$version/node-v${t}.tar.gz" # have to do in 2 commands or else the "local" returns 0! local tarfile tarfile=$(get "$url" -#Lf) local ret=$? if [ $ret -eq 0 ]; then # it worked! cat "$tarfile" | $tar xz -C "$targetfolder" --strip-components 1 else return $ret fi } build () { local version="$1" local targetfolder="$2" # shortcut - try the binary if possible. if bin_available $version; then if build_binary "$version" "$targetfolder"; then return 0 else nave_uninstall "$version" err "Binary install failed, trying source." fi fi if ! nave_fetch "$version"; then # fetch failed, don't continue and try to build it. return 1 fi local src="$NAVE_SRC/$version" local jobs=$NAVE_JOBS jobs=${jobs:-$JOBS} jobs=${jobs:-$(sysctl -n hw.ncpu)} jobs=${jobs:-2} ( cd -- "$src" &>/dev/null source_naverc if [ "$NAVE_CONFIG" == "" ]; then NAVE_CONFIG=() fi if ! JOBS=$jobs ./configure "${NAVE_CONFIG[@]}" --prefix="$2"; then err "Failed to configure $version" return 1 fi if ! JOBS=$jobs make -j$jobs; then err "Failed to make $version" return 1 fi if ! make install; then err "Failed to install $version" return 1 fi ) } nave_cache () { local cache="$NAVE_DIR/cache" local subcmd="$1" shift mkdirp "$cache" case "$subcmd" in clear|empty|clean) rm -rf "$cache" ;; ls) find "$cache" "$@" ;; tree) tree "$cache" "$@" ;; *) cat >&2 </dev/null) local wn=$(which node) local prefix=${PREFIX:-/usr/local} if [ -x "$wn" ] && [[ "$current" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then local p="${wn/\/bin\/node/}" if [ "$p" != "" ] && [ -d "$prefix" ]; then prefix=$p fi fi current="${current/v/}" if [ "$current" == "$version" ]; then return 0 fi build "$version" "$prefix" } nave_install () { if [ $# -eq 2 ]; then add_named_env "$1" "$2" return $? fi local version=$(ver "$1" "NONAMES") # special case for newer macOS machines using arm64 if [[ "$(uname -m)" =~ "arm64" ]] && [[ "$version" =~ ^([0-9]|10|12|14)\. ]]; then if type arch &>/dev/null; then echo "attempting to switch to x86 for old node version" >&2 # have to use the system's bash, in case $(which bash) is # compiled for this architecture only, more likely the builtin # one is set up for backwards compatibility. arch -x86_64 /bin/bash "$0" install "$version" return $? else echo "Warning: using old node version on arm64, might fail" >&2 fi fi if [ -z "$version" ]; then err "Must supply a version ('lts', 'stable', 'latest' or numeric)" return 1 fi if nave_installed "$version"; then return 0 fi local install="$NAVE_ROOT/$version" mkdirp "$install" build "$version" "$install" local ret=$? if [ $ret -ne 0 ]; then rimraf "$install" return $ret fi } nave_exit () { if [ -n "$NAVEPATH" ]; then export PATH=${PATH//$NAVEPATH:/} fi export NAVE_EXIT='1' unset NAVE_NPX unset NAVE_AUTO_RC unset NAVE_AUTO_CFG unset NAVE_DEBUG unset NAVE_JOBS unset NAVELVL unset NAVEPATH unset NAVEVERSION unset NAVENAME unset NAVEVERSIONARG unset NAVE unset NAVE_SRC unset NAVE_CONFIG unset NAVE_ROOT unset NODE_PATH unset NAVE_LOGIN unset NAVE_DIR unset npm_config_binroot unset npm_config_root unset npm_config_manroot unset npm_config_prefix if [ "$#" -gt 0 ]; then "$SHELL" -c "$(enquote_all "$@")" else exec "$SHELL" fi } naverc_filename () { echo $(cd -- $NAVE_DIR/.. &>/dev/null; pwd)/.naverc } # __nave_findup # It has a funky name so we can use it in the shell rcfile, and # then delete it, without accidentally clobbering anything else. __nave_findup () { local dir="$1" local file="$2" while ! [ "$dir" = "/" ] && ! [ "$dir" = "." ]; do if [ -f "$dir"/"$file" ]; then echo "$dir"/"$file" return 0 elif [ -f "$dir"/.nvmrc ] && [ "$file" = ".naverc" ]; then echo "$dir"/.nvmrc return 0 elif [ -e "$dir"/.git ] || [ -h "$dir"/.git ] || [ "$dir" = "$HOME" ]; then break else dir=$(dirname -- "$dir") fi done return 1 } local_naverc_filename () { __nave_findup "$1" ".naverc" || return 1 } source_naverc () { local naverc=$(naverc_filename) if [ -f "$naverc" ]; then . "$naverc" fi } write_shell_rcfile () { mkdirp "$NAVE_DIR" "could not make NAVE_DIR ($NAVE_DIR)" # set up the naverc init file. # For zsh compatibility, we name this file ".zshenv" instead of # the more reasonable "naverc" name. # Important! Update this number any time the init content is changed. local rcversion="#6" local rcfile="$NAVE_DIR/.zshenv" if ! [ -f "$rcfile" ] \ || [ "$(head -n1 "$rcfile")" != "$rcversion" ]; then local homercfile=$(naverc_filename) cat > "$rcfile" < "$NAVE_DIR/.zlogout" </dev/null))" done } nave_ls_all () { nave_ls && (echo ""; nave_ls_remote) } ver () { local version="$1" local nonames="$2" version="${version#v}" case $version in lts-* | lts/*) nave_lts $version ;; lts | latest | stable) nave_$version ;; +([0-9])) nave_version_family "$version\."'[0-9]+' ;; +([0-9]).) nave_version_family "$version"'[0-9]+' ;; +([0-9]).+([0-9])) nave_version_family "$version" ;; +([0-9]).+([0-9]).) nave_version_family "$version" ;; +([0-9]).+([0-9]).+([0-9])) echo $version ;; *) [ "$nonames" = "" ] && echo $version ;; esac } nave_version_family () { local family="$1" family="${family#v}" family="${family%.}" get / | grep -E -o 'v'$family'\.[0-9]+' | sed -e 's|^v||g' \ | semver_sort | tail -n1 } semver_sort () { sort -u -k 1,1n -k 2,2n -k 3,3n -t . } nave_latest () { get / \ | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' \ | semver_sort \ | tail -n1 } nave_stable () { nave_lts "$@" } nave_lts () { # err "nave_lts $@" local lts="$1" case $lts in "" | "lts/*") lts="$(get / | grep -E -o 'latest-[^v][^/]+' | sort | uniq | tail -n1)" lts=${lts/latest-/} ;; lts/*) lts=$(basename "$lts") ;; latest-*) lts=${lts/latest-/} # err "nave_lts lts latest [$lts]" ;; esac # err "nave_lts lts=[$lts]" get latest-$lts/ | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 } version_list () { echo "$1:" grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' \ | semver_sort \ | organize_version_list } organize_version_list () { local i=0 local v while read v; do if [ $i -eq 8 ]; then i=0 echo "$v" else let 'i = i + 1' echo -ne "$v\t" fi done echo "" [ $i -ne 0 ] && echo "" return 0 } nave_has () { local version=$(ver "$1") [ -x "$NAVE_SRC/$version/configure" ] } nave_installed () { local version=$(ver "$1") [ -x "$NAVE_ROOT/$version/bin/node" ] } nave_use () { if [ -z "$1" ]; then err "Must supply a version" return 1 fi local version=$(ver "$1" NONAMES) # if it's not a version number, then treat as a name. if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then nave_named "$@" return $? fi local versionarg="$1" if [ "$version" == "$NAVENAME" ]; then # we're already here if [ $# -gt 1 ]; then shift if [ "$1" == "exec" ]; then shift fi exec "$@" else return 0 fi fi nave_install "$version" local ret=$? if [ $ret -ne 0 ]; then err "failed to install $version" return $ret fi local prefix="$NAVE_ROOT/$version" local lvl=$[ ${NAVELVL-0} + 1 ] if [ $# -gt 1 ]; then shift nave_exec "$lvl" "$version" "$version" "$prefix" "$versionarg" "$@" else nave_login "$lvl" "$version" "$version" "$prefix" "$versionarg" fi } # internal nave_exec () { nave_run "exec" "$@" } nave_login () { nave_run "login" "$@" } nave_run () { write_shell_rcfile local exec="$1" shift local lvl="$1" shift local name="$1" shift local version="$1" shift local prefix="$1" shift local versionarg="$1" shift # err "nave_run exec=$exec lvl=$lvl name=$name version=$version prefix=$prefix versionarg=$versionarg" local bin="$prefix/bin" local lib="$prefix/lib/node" local man="$prefix/share/man" mkdirp "$bin" mkdirp "$lib" mkdirp "$man" # now $@ is the command to run, or empty if it's not an exec. local args=() local isLogin local runShell=$SHELL if [ "$exec" == "exec" ]; then isLogin="" # source the nave env file, then run the command. args=("-c" ". $(enquote_all $NAVE_DIR/.zshenv); $(enquote_all "$@")") else case "$(basename "$SHELL")" in zsh) isLogin="1" # no need to set rcfile, since ZDOTDIR is set. args=() ;; bash) isLogin="1" # bash, use --rcfile argument args=("--rcfile" "$NAVE_DIR/.zshenv") ;; *) isLogin="1" # use bash so we can source the rcfile we've prepared but then tell # bash to run the user's preferred shell runShell="bash" args=("-c" ". $NAVE_DIR/.zshenv && $SHELL") ;; esac fi local nave="$version" if [ "$version" != "$name" ]; then nave="$name"-"$version" fi # use exec to take over this shell process with whatever we're # executing, whether that's a login or a command. otherwise there # are actually TWO subshells, rather than one. local path=$bin if [ "$NAVE_NPX" = "1" ]; then # walk up cwd to find the first package.json or node_modules local p=$PWD while [ -d "$p" ] && [ -n "$p" ] && ! [ "$p" = "/" ]; do if [ -d "$p/node_modules" ] || [ -f "$p/package.json" ]; then path="$p/node_modules/.bin:$path" break else p=$(dirname -- "$p") fi done fi NAVE_EXIT='0' \ NAVELVL=$lvl \ NAVEPATH="$path" \ NAVEVERSION="$version" \ NAVEVERSIONARG="$versionarg" \ NAVENAME="$name" \ NAVE="$nave" \ npm_config_binroot="$bin"\ npm_config_root="$lib" \ npm_config_manroot="$man" \ npm_config_prefix="$prefix" \ NODE_PATH="$lib" \ NAVE_LOGIN="$isLogin" \ NAVE_DIR="$NAVE_DIR" \ NAVE_NPX="$NAVE_NPX" \ ZDOTDIR="$NAVE_DIR" \ exec "$runShell" "${args[@]}" } nave_named () { local name="$1" shift local version=$(ver "$1" NONAMES) local versionarg if [ "$version" != "" ]; then versionarg="$1" shift fi add_named_env "$name" "$version" || fail "failed to create $name env" if [ "$name" == "$NAVENAME" ] && [ "$version" == "$NAVEVERSION" ]; then if [ $# -gt 0 ]; then if [ "$1" == "exec" ]; then shift fi exec "$@" else return 0 fi fi if [ "$version" = "" ]; then version="$(ver "$("$NAVE_ROOT/$name/bin/node" -v 2>/dev/null)")" versionarg="$version" fi local prefix="$NAVE_ROOT/$name" local lvl=$[ ${NAVELVL-0} + 1 ] # get the version if [ $# -gt 0 ]; then nave_exec "$lvl" "$name" "$version" "$prefix" "$versionarg" "$@" else nave_login "$lvl" "$name" "$version" "$prefix" "$versionarg" fi } add_named_env () { local name="$1" local version="$2" local cur="$(ver "$("$NAVE_ROOT/$name/bin/node" -v 2>/dev/null)" "NONAMES")" if [ "$name" == "" ]; then err "Must provide a name" return 1 fi if [ "$version" != "" ]; then version="$(ver "$version" "NONAMES")" else version="$cur" fi if [ "$version" = "" ]; then echo "What version of node?" read -p "lts, lts/, latest, x.y, or x.y.z > " version version=$(ver "$version" "NONAMES") if [ "$version" = "" ]; then err "Invalid version specifier" return 1 fi fi # if that version is already there, then nothing to do. if [ "$cur" = "$version" ]; then return 0 fi err "Creating new env named '$name' using node $version" nave_install "$version" local ret=$? if [ $ret -ne 0 ]; then err "failed to install $version" return $ret fi mkdirp "$NAVE_ROOT/$name/bin" mkdirp "$NAVE_ROOT/$name/lib/node" mkdirp "$NAVE_ROOT/$name/lib/node_modules" mkdirp "$NAVE_ROOT/$name/share/man" ln -sf -- "$NAVE_ROOT/$version/bin/node" "$NAVE_ROOT/$name/bin/node" ln -sf -- "$NAVE_ROOT/$version/bin/npm" "$NAVE_ROOT/$name/bin/npm" ln -sf -- "$NAVE_ROOT/$version/bin/node-waf" "$NAVE_ROOT/$name/bin/node-waf" ln -sf -- "$NAVE_ROOT/$version/bin/npx" "$NAVE_ROOT/$name/bin/npx" } nave_clean () { rm -rf "$NAVE_SRC/$(ver "$1")" "$NAVE_SRC/$(ver "$1")".tgz "$NAVE_SRC/$(ver "$1")"-*.tgz } nave_uninstall () { rimraf "$NAVE_ROOT/$(ver "$1")" } nave_help () { [ -z $_TESTING_NAVE_NO_HELP ] && cat < COMMANDS install Install the version specified (ex: 12.8.0) install Install the version as a named env use Enter a subshell where is being used use Enter a subshell, and run "", then exit use Create a named env, using the specified version. If the name already exists, but the version differs, then it will update the link. usemain Install in /usr/local/bin (ie, use as your main nodejs) clean Delete the source code for uninstall Delete the install for ls List versions currently installed ls-remote List remote node versions ls-all List remote and local node versions latest Show the most recent dist version cache Clear or view the cache help Output help information auto Find a .naverc and then be in that env If no .naverc is found, then alias for 'nave exit' auto cd into , then find a .naverc, and be in that env If no .naverc is found, then alias for 'nave exit' in the specified directory. auto cd into , then find a .naverc, and run a command in that env If no .naverc is found, then alias for 'nave exit ' in the specified directory. should-auto Exits with 1 if the nave auto env already matches the config, or 0 if a change should be made (ie, by calling 'nave auto') An explicit call to 'nave use' or 'nave exit' will tell nave that it should NOT auto. get Print out various nave config values. exit Unset all the NAVE environs (use with 'exec') exit Run the specified command in a nave-free environment (Note that nave will still set NAVE_EXIT=1 in order to prevent 'nave should-auto' from evaluating true.) VERSION STRINGS Any command that calls for a version can be provided any of the following "version-ish" identifies: - x.y.z A specific SemVer tuple - x.y Major and minor version number - x Just a major version number - lts The most recent LTS (long-term support) node version - lts/ The latest in a named LTS set. (argon, boron, etc.) - lts/* Same as just "lts" - latest The most recent (non-LTS) version - stable Backwards-compatible alias for "lts". To exit a nave subshell, type 'exit' or press ^D. To run nave *without* a subshell, do 'exec nave use '. To clear the settings from a nave env, use 'exec nave exit' ENVIRONMENT VARIABLES The following environment variables can be set to change nave's behavior. NAVE_DIR Root directory for nave to operate in. Defaults to \$XDG_CONFIG_HOME/nave if set (eg, ~/.config/nave), or ~/.nave otherwise. NAVE_NPX Set this to '1' to add node_modules/.bin to the PATH NAVE_DEBUG Set this to '1' to run in debug mode. NAVE_CACHE_DUR Duration in seconds to cache version information (86400) NAVEUA User-agent header to send when fetching version information NAVE_SRC_ONLY Set to '1' to *only* build node from source, and never use binary distributions. (This is much slower!) NAVE_JOBS Set to the number of JOBS to use when building node. Defaults to the number of CPUs on the system. NODEDIST The distribution server to fetch node from. Defaults to https://nodejs.org/dist NAVE_CONFIG Arguments to pass to ./configure when building from source. Nave sets the following environment variables when in use: NAVE A descriptive string of the nave setting in use. NAVENAME The name, in named subshells, otherwise \$NAVEVERSION NAVEVERSION The version of node in use. NAVEVERSIONARG The version specifier provided ("18.13" or "lts", etc.) NAVELVL The number of subshells currently in use (like bash \$SHLVL) NAVE_LOGIN '1' in interactive nave subshells, '0' otherwise. NAVE_ROOT Location of nave installed environments NAVE_SRC Location of downloaded Node.js source NAVE_AUTO_RC The .naverc file used by 'nave auto' NAVE_AUTO_CFG The contents of the .naverc file used by 'nave auto' CONFIGURATION FILES Nave subshells will source the same .bashrc, .bash_profile, .zprofile, etc. configuration files as normal shells, based on whether it is being run as a login shell, or to run a specific command. In addition, after sourcing the normal shell profile files, nave looks for environment specific '.nave_profile_*' files, first in the \$NAVE_DIR (~/.config/nave if present, otherwise ~/.nave), and then by walking up from the current working directory. The walk up process stops when it encounters a folder with a '.git' entry, or when it hits the user's \$HOME directory. The environment-specific .nave_profile files that it searches for are: .nave_profile_\${NAVE} .nave_profile_\${NAVENAME}, if a named environment .nave_profile_\${NAVEVERSION}, eg .nave_profile_18.13.0 .nave_profile_\${NAVEVERSION major.minor}, eg .nave_profile_18.13 .nave_profile_\${NAVEVERSION major}, eg .nave_profile_18 .nave_profile Finally, it will always source \${NAVE_DIR}/../.naverc if present. (eg, ~/.config/.naverc or ~/.naverc) These may be used to set project-specific confirations, env variables, or other behavior based on the Nave environment in use, without the use of configuration files in the home directory. The 'nave auto' command will walk up the directory tree looking for a '.naverc' or '.nvmrc' file, and use the contents as arguments to 'nave use'. USAGE } if [ -z $_TESTING_NAVE_NO_MAIN ]; then main "$@" else get_nave_dir fi