0
0
mirror of https://github.com/tj/n.git synced 2024-11-29 05:42:48 +01:00
n/bin/n
DeeDeeG 41400d6a60 bin/n: Option to download xz-compressed tarball
To download an xz-compressed tarball, set the environment variable
"N_USE_XZ" to be non-empty before running n.
2019-06-23 10:06:15 -04:00

840 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# shellcheck disable=SC2155
# Disabled "Declare and assign separately to avoid masking return values": https://github.com/koalaman/shellcheck/wiki/SC2155
#
# Setup.
#
VERSION="5.0.0-next.0"
N_PREFIX="${N_PREFIX-/usr/local}"
BASE_VERSIONS_DIR="$N_PREFIX/n/versions"
#
# Log <type> <msg>
#
log() {
printf " ${SGR_CYAN}%10s${SGR_RESET} : ${SGR_FAINT}%s${SGR_RESET}${SGR_RESET}\n" "$1" "$2"
}
#
# Exit with the given <msg ...>
#
abort() {
printf "\n ${SGR_RED}Error: %s${SGR_RESET}\n\n" "$*" && exit 1
}
#
# All Bin (node+custom) configurations
#
BINS=("node")
MIRROR=("${NODE_MIRROR-https://nodejs.org/dist/}")
BIN_NAME=("node")
VERSIONS_DIR=("$BASE_VERSIONS_DIR/node")
if [ -n "$PROJECT_NAME" ]; then
BINS+=("$PROJECT_NAME")
BIN_NAME+=("$PROJECT_NAME")
if [ -z "$PROJECT_URL" ]; then
abort "Must specify PROJECT_URL when supplying PROJECT_NAME"
fi
MIRROR+=("${PROJECT_URL}")
VERSIONS_DIR+=("$BASE_VERSIONS_DIR/$PROJECT_NAME")
fi
#
# Ensure we have curl or wget support.
#
CURL_PARAMS=( "-L"
"-#")
WGET_PARAMS=( "--no-check-certificate"
"-q"
"-O-")
if [ -n "$HTTP_USER" ];then
if [ -z "$HTTP_PASSWORD" ]; then
abort "Must specify HTTP_PASSWORD when supplying HTTP_USER"
fi
CURL_PARAMS+=("-u $HTTP_USER:$HTTP_PASSWORD")
WGET_PARAMS+=("--http-password=$HTTP_PASSWORD"
"--http-user=$HTTP_USER")
elif [ -n "$HTTP_PASSWORD" ]; then
abort "Must specify HTTP_USER when supplying HTTP_PASSWORD"
fi
GET=
# wget support
command -v wget > /dev/null && GET="wget ${WGET_PARAMS[*]}"
command -v curl > /dev/null && GET="curl ${CURL_PARAMS[*]}" && QUIET=false
test -z "$GET" && abort "curl or wget required"
#
# State
#
DEFAULT=0
if [[ -t 1 ]]; then
QUIET=false
else
QUIET=true
fi
ACTIVATE=true
ARCH=
# ANSI escape codes
# https://en.wikipedia.org/wiki/ANSI_escape_code
# http://no-color.org
# https://bixense.com/clicolors
USE_COLOR="true"
if [[ -n "${CLICOLOR_FORCE+defined}" && "${CLICOLOR_FORCE}" != "0" ]]; then
USE_COLOR="true"
elif [[ -n "${NO_COLOR+defined}" || "${CLICOLOR}" = "0" || ! -t 1 ]]; then
USE_COLOR="false"
fi
readonly USE_COLOR
# Select Graphic Rendition codes
if [[ "${USE_COLOR}" = "true" ]]; then
# KISS and use codes rather than tput, avoid dealing with missing tput or TERM.
readonly SGR_RESET="\033[0m"
readonly SGR_FAINT="\033[2m"
readonly SGR_RED="\033[31m"
readonly SGR_CYAN="\033[36m"
else
readonly SGR_RESET=
readonly SGR_FAINT=
readonly SGR_RED=
readonly SGR_CYAN=
fi
#
# set_arch <arch> to override $(uname -a)
#
set_arch() {
if test -n "$1"; then
ARCH="$1"
else
abort "missing -a|--arch value"
fi
}
#
# Functions used when showing versions installed
#
enter_fullscreen() {
# Set cursor to be invisible
tput civis 2> /dev/null
# Save screen contents
tput smcup 2> /dev/null
stty -echo
}
leave_fullscreen() {
# Set cursor to normal
tput cnorm 2> /dev/null
# Restore screen contentsq
tput rmcup 2> /dev/null
stty echo
}
handle_sigint() {
leave_fullscreen
S="$?"
kill 0
exit $S
}
handle_sigtstp() {
leave_fullscreen
kill -s SIGSTOP $$
}
#
# Output usage information.
#
display_help() {
cat <<-EOF
Usage: n [options/env] [COMMAND] [args]
Environments:
n [COMMAND] [args] Uses default env (node)
n project [COMMAND] Uses custom env-variables to use non-official sources
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 <version> Install node <version>
n use <version> [args ...] Execute node <version> with [args ...]
n bin <version> Output bin path for <version>
n rm <version ...> 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
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
Aliases:
which bin
use as
list ls
- rm
stable lts
EOF
}
err_no_installed_print_help() {
printf "\n ${SGR_RED}Error: no installed version${SGR_RESET}\n"
display_help
exit 1
}
#
# Output version after selected.
#
next_version_installed() {
list_versions_installed | grep "$selected" -A 1 | tail -n 1
}
#
# Output version before selected.
#
prev_version_installed() {
list_versions_installed | grep "$selected" -B 1 | head -n 1
}
#
# Output n version.
#
display_n_version() {
echo "$VERSION" && exit 0
}
#
# Check for installed version, and populate $active
#
check_current_version() {
command -v node &> /dev/null
if test $? -eq 0; then
local current=$(node --version)
if [ -n "$PROJECT_VERSION_CHECK" ]; then
current="$(node -p "$PROJECT_VERSION_CHECK || process.exit(1)" || node --version)"
fi
current=${current#v}
for bin in "${BINS[@]}"; do
if diff &> /dev/null \
"$BASE_VERSIONS_DIR/$bin/$current/bin/node" \
"$(command -v node)" ; then
active="$bin/$current"
fi
done
fi
}
#
# 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]+$" \
| sed 's|/|.|' \
| sort -k 1,1 -k 2,2n -k 3,3n -k 4,4n -t . \
| sed 's|\.|/|'
}
#
# Display installed versions with <selected>
#
display_versions_with_selected() {
selected="$1"
echo
for version in $(versions_paths); do
if test "$version" = "$selected"; then
printf " ${SGR_CYAN}ο${SGR_RESET} %s\n" "$version"
else
printf " ${SGR_FAINT}%s${SGR_RESET}\n" "$version"
fi
done
echo
}
#
# List installed versions.
#
list_versions_installed() {
for version in $(versions_paths); do
echo "$version"
done
}
#
# Display current node --version and others installed.
#
display_versions() {
enter_fullscreen
check_current_version
clear
display_versions_with_selected "$active"
trap handle_sigint INT
trap handle_sigtstp SIGTSTP
ESCAPE_SEQ=$'\033'
UP=$'A'
DOWN=$'B'
while true; do
read -rsn 1 key
case "$key" in
$ESCAPE_SEQ)
# Handle ESC sequences followed by other characters, i.e. arrow keys
read -rsn 1 -t 1 tmp
if [[ "$tmp" == "[" ]]; then
read -rsn 1 -t 1 arrow
case "$arrow" in
$UP)
clear
display_versions_with_selected "$(prev_version_installed)"
;;
$DOWN)
clear
display_versions_with_selected "$(next_version_installed)"
;;
esac
fi
;;
"k")
clear
display_versions_with_selected "$(prev_version_installed)"
;;
"j")
clear
display_versions_with_selected "$(next_version_installed)"
;;
"q")
clear
leave_fullscreen
exit
;;
"")
# enter key returns empty string
leave_fullscreen
activate "$selected"
exit
;;
esac
done
}
#
# Move up a line and erase.
#
erase_line() {
printf "\033[1A\033[2K"
}
#
# Check if the HEAD response of <url> 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 <version>
#
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 <binary>
#
disable_pax_mprotect() {
test -z "$1" && abort "binary required"
local binary="$1"
# try to disable mprotect via XATTR_PAX header
local PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl-ng 2>&1)"
local PAXCTL_ERROR=1
if [ -x "$PAXCTL" ]; then
$PAXCTL -l && $PAXCTL -m "$binary" >/dev/null 2>&1
PAXCTL_ERROR="$?"
fi
# try to disable mprotect via PT_PAX header
if [ "$PAXCTL_ERROR" != 0 ]; then
PAXCTL="$(PATH="/sbin:/usr/sbin:$PATH" command -v paxctl 2>&1)"
if [ -x "$PAXCTL" ]; then
$PAXCTL -Cm "$binary" >/dev/null 2>&1
fi
fi
}
#
# Activate <version>
#
activate() {
local version="$1"
local dir=$BASE_VERSIONS_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
rm -rf "$N_PREFIX/lib/node_modules/npm"
fi
fi
# Copy (lib before bin to avoid error messages on Darwin when cp over dangling link)
for subdir in lib bin include share; do
if test -L "$N_PREFIX/$subdir"; then
find "$dir/$subdir" -mindepth 1 -maxdepth 1 -exec cp -fR "{}" "$N_PREFIX/$subdir" \;
else
cp -fR "$dir/$subdir" "$N_PREFIX"
fi
done
local installed_node="${N_PREFIX}/bin/node"
disable_pax_mprotect "${installed_node}"
local active_node="$(command -v node)"
if [[ -e "${active_node}" && -e "${installed_node}" && "${active_node}" != "${installed_node}" ]]; then
log "installed" "$("${installed_node}" --version) to ${installed_node}"
log "active" "$("${active_node}" --version) at ${active_node}"
else
log "installed" "$("${installed_node}" --version)"
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 <version>
#
install() {
local version=${1#v}
local dots="${version//[^.]*/}"
if test ${#dots} -lt 2; then
version="$($GET 2> /dev/null "${MIRROR[DEFAULT]}" \
| grep -E "</a>" \
| 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"
if test -n "$N_USE_XZ"; then
local tarflag="-Jx"
else
local tarflag="-zx"
fi
if test -d "$dir"; then
if [[ ! -e "$dir/n.lock" ]] ; then
if "$ACTIVATE" ; then
activate "${BINS[$DEFAULT]}/$version"
fi
exit
fi
fi
echo
log installing "${BINS[$DEFAULT]}-v$version"
local url="$(tarball_url "$version")"
is_ok "$url" || is_oss_ok "$url" || abort "invalid version '$version'"
log mkdir "$dir"
if ! mkdir -p "$dir"; then
abort "sudo required"
else
touch "$dir/n.lock"
fi
cd "$dir"
log fetch "$url"
$GET "$url" | tar "$tarflag" --strip-components=1
[ "$QUIET" == false ] && erase_line
rm -f "$dir/n.lock"
disable_pax_mprotect bin/node
if "$ACTIVATE" ; then
activate "${BINS[$DEFAULT]}/$version"
fi
echo
}
#
# Set curl to quiet (silent) mode.
#
set_quiet() {
command -v curl > /dev/null && GET="$GET -s" && QUIET=true
}
#
# Remove <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"
shift
done
}
#
# Prune non-active versions
#
prune_versions() {
check_current_version
for version in $(versions_paths); do
if [ "$version" != "$active" ]
then
echo "$version"
rm -rf "${BASE_VERSIONS_DIR[$DEFAULT]}/$version"
fi
done
}
#
# Output bin path for <version>
#
display_bin_path_for_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"
if test -f "$bin"; then
printf "%s\n" "$bin"
else
abort "'$1' is not installed"
fi
}
#
# Execute the given <version> 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 "</a>" \
| 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 "</a>" \
| grep -E -o 'latest-[a-z]{2,}' \
| sort \
| tail -n1)"
$GET 2> /dev/null "${MIRROR[$DEFAULT]}/$folder_name/" \
| grep -E "</a>" \
| 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 "</a>" \
| 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: delete_with_echo target
#
function delete_with_echo() {
if [[ -e "$1" ]]; then
echo "$1"
rm -rf "$1"
fi
}
#
# Synopsis: uninstall_installed
# Uninstall the installed node and npm (leaving alone the cache),
# so undo install, and may expose possible system installed versions.
#
uninstall_installed() {
# npm: https://docs.npmjs.com/misc/removing-npm
# rm -rf /usr/local/{lib/node{,/.npm,_modules},bin,share/man}/npm*
# node: https://stackabuse.com/how-to-uninstall-node-js-from-mac-osx/
# Doing it by hand rather than scanning cache, so still works if cache deleted first.
# This covers tarballs for at least node 4 through 10.
while true; do
read -r -p "Do you wish to delete node and npm from ${N_PREFIX}? " yn
case $yn in
[Yy]* ) break ;;
[Nn]* ) exit ;;
* ) echo "Please answer yes or no.";;
esac
done
echo ""
echo "Uninstalling node and npm"
delete_with_echo "${N_PREFIX}/bin/node"
delete_with_echo "${N_PREFIX}/bin/npm"
delete_with_echo "${N_PREFIX}/bin/npx"
delete_with_echo "${N_PREFIX}/include/node"
delete_with_echo "${N_PREFIX}/lib/dtrace/node.d"
delete_with_echo "${N_PREFIX}/lib/node_modules/npm"
delete_with_echo "${N_PREFIX}/share/doc/node"
delete_with_echo "${N_PREFIX}/share/man/man1/node.1"
delete_with_echo "${N_PREFIX}/share/systemtap/tapset/node.stp"
}
#
# Handle arguments.
#
[[ "${QUIET}" = "true" ]] && set_quiet
if test $# -eq 0; then
test -z "$(versions_paths)" && err_no_installed_print_help
display_versions
else
while test $# -ne 0; do
case "$1" in
-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) display_latest_lts_version; exit ;;
project) DEFAULT=1 ;;
-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 ;;
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 ;;
uninstall) uninstall_installed ;;
*) install "$1"; exit ;;
esac
shift
done
fi