On Arch Linux, there are 2 packages that provide the Rust toolchain: rust
itself and rustup
. When users try to install a package that depends on a Rust toolchain, they are presented with those options, the default one being the rust
package, which is a fixed version that (presumably) all other official Arch Linux packages use when building packages that depend on a Rust toolchain. The latter, rustup
, is provided by the Rust project as the official way to install a Rust toolchain, and can be used to install any version and component of the toolchain, e.g., newer/older, stable/beta, compiler/documentation, etc.
Similar to Rust, Node.js has an official distribution, but unlike Rust, not an official version manager - those are community managed. There are various to choose from, some more cross-platform than others. But to my knowledge, none work the way the Rust's version manager work: rustup
provides a global shim in /usr/bin/{rustc,cargo}
, etc., similar to the standalone, fixed version, rust
package, but redirects calls to them to the per-user ~/.rustup/toolchains
. This way, the package can be used to provides=(rust)
for the entire system, despite the binaries technically being per-user.
Given that none of the Node.js version managers work this way (again, at least to my knowledge, they all focus on per-user setups, with no global shim), I've decided to build one for myself that works the way I'd like it to. Similar to rustup
, this script expects to be symlinked as /usr/bin/{node,npm}
, etc., all of which redirect to this script and the per-user toolchains.
#!/usr/bin/bash # We know how strings work # shellcheck disable=SC2016 # We use `source`ing the "config" file as a poor man's easy to write/parse config # shellcheck disable=SC1090 # We deliberately use subshells to ensure variables don't leak # shellcheck disable=SC2030 # shellcheck disable=SC2031 # Enable "strict mode" set -euo pipefail NODEUP_HOME_DIR="${NODEUP_HOME_DIR:-"${HOME}/.nodeup"}" NODEUP_SETTINGS_FILE="${NODEUP_HOME_DIR}/settings" NODEUP_NODE_VERSIONS_DIR="${NODEUP_HOME_DIR}/versions" die() { if (( $# >= 1 )); then printf '%s\n' "$*" >&2 fi exit 1 } get_active_node_version() {( # Setup so shellcheck doesn't complain about undeclared variables nodeup_setting_active_node_version='' source "${NODEUP_SETTINGS_FILE}" printf '%s' "${nodeup_setting_active_node_version}" )} set_active_node_version() {( if (( $# != 1 )); then die 'Invalid usage of `set_active_node_version`!' fi source "${NODEUP_SETTINGS_FILE}" export nodeup_setting_active_node_version="$1" env | grep nodeup_setting_ | sed 's/^/export /g' > "${NODEUP_SETTINGS_FILE}" )} forward() { if (( $# < 1 )); then die 'Missing forward target!' fi local active_node_version='' active_node_version="$(get_active_node_version)" if [[ -z "${active_node_version}" ]]; then die 'No version of node available, install one using `nodeup install`' fi local node_dir="${NODEUP_NODE_VERSIONS_DIR}/${active_node_version}" if [[ ! -d "${node_dir}" ]]; then die "Node v${active_node_version} is not installed, install it using \`nodeup install ${active_node_version}\`" fi local target="$1" shift "${node_dir}/${target}" "$@" } nodeup_install() { if (( $# != 1 )); then die 'Invalid usage of `nodeup_install`!' fi local version="$1" local node_rootname="node-v${version}-win-x64" local nodezip_filename="${node_rootname}.zip" local nodezip__download_url="https://nodejs.org/dist/v${version}/${nodezip_filename}" local shatxt_filename='SHASUMS256.txt' local shatxt_download_url="https://nodejs.org/dist/v${version}/${shatxt_filename}" # Pretty sure this never expands to empty since we set it up top and never # mess with it afterwards, but shellcheck thinks otherwise rm -rf "${NODEUP_NODE_VERSIONS_DIR:?}/${version}" pushd "${NODEUP_NODE_VERSIONS_DIR}" >/dev/null curl -LOs "${nodezip__download_url}" curl -LOs "${shatxt_download_url}" if ! sha256sum -c "${shatxt_filename}" --status --ignore-missing; then rm "${nodezip_filename}" "${shatxt_filename}" die 'Integrity check failed!' fi # Nicer interface than unzip, also available by default bsdtar -xf "${nodezip_filename}" rm "${nodezip_filename}" "${shatxt_filename}" mv "${node_rootname}" "${version}" set_active_node_version "${version}" popd >/dev/null } nodeup_use() { if (( $# != 1 )); then die 'Invalid usafe of `nodeup_use`!' fi local version="$1" if [[ ! -d "${NODEUP_NODE_VERSIONS_DIR}/${version}" ]]; then die "Node v${version} is not installed, install it using \`nodeup install ${version}\`" fi set_active_node_version "${version}" } nodeup_ls() { find "${NODEUP_NODE_VERSIONS_DIR}" -mindepth 1 -maxdepth 1 -exec basename {} \; } nodeup_ls_remote() { curl -Ls 'https://nodejs.org/dist/index.json' | jq -r '.[].version' } nodeup_uninstall() { if (( $# != 1 )); then die 'Invalid usage of `nodeup_uninstall`!' fi local version="$1" # Pretty sure this never expands to empty since we set it up top and never # mess with it afterwards, but shellcheck thinks otherwise rm -rf "${NODEUP_NODE_VERSIONS_DIR:?}/${version}" } nodeup_main() { case "$1" in 'install') shift; nodeup_install "$@" ;; 'use') shift; nodeup_use "$@" ;; 'ls') shift; nodeup_ls "$@" ;; 'ls-remote') shift; nodeup_ls_remote "$@" ;; 'uninstall') shift; nodeup_uninstall "$@" ;; *) die "Unknown command: $1" ;; esac } main() { mkdir -p "${NODEUP_HOME_DIR}" || die "Failed to create \`${NODEUP_HOME_DIR}\`!" touch "${NODEUP_SETTINGS_FILE}" || die "Failed to create \`${NODEUP_SETTINGS_FILE}\`!" mkdir -p "${NODEUP_NODE_VERSIONS_DIR}" || die "Failed to create \`${NODEUP_NODE_VERSIONS_DIR}\`!" local argv0 argv0="$(basename "$0")" case "${argv0}" in nodeup) nodeup_main "$@" ;; *) forward "${argv0}" "$@" ;; esac } main "$@"
I wrote and tested this on a Windows machine with MSYS2 (hence a couple of Windows-specific things inside a Unix shellscript: Windows download links, and Windows Node.js package layout assumptions - the layout is slightly different for Unix-based systems) with the idea that I can rewrite this in a more easily cross-platform, maintainable, and/or performant language/manner if needed/desired, so I am also looking for feedback on the architecture. For this script specifically, shellcheck
seems happy with it, and my own testing and usage of it was fine, albeit minimal in function and debugability. Additionally, I wouldn't mind hearing about a Node.js version manager that does provide a global shim, should it turn out that I've simply missed its existence.
nvm
with this...\$\endgroup\$set -euo pipefail
- please read mywiki.wooledge.org/BashFAQ/105 to learn why most experienced shell programmers would not use that construct.\$\endgroup\$set -e
are dubious, or at least do not apply to me - I don't often find myself writing such constructs, plus shellcheck helps me on at least one of those flaws mentioned, so I imagine it might be helping on more. Good to know its quirks, though.\$\endgroup\$