Skip to content

Instantly share code, notes, and snippets.

@microadam
Created February 21, 2016 20:21
Show Gist options
  • Save microadam/ac8bbeed01ba56ec1664 to your computer and use it in GitHub Desktop.
Save microadam/ac8bbeed01ba56ec1664 to your computer and use it in GitHub Desktop.
#!/bin/sh
# This program contains parts of narwhal's "sea" program,
# as well as bits borrowed from Tim Caswell's "nvm"
# nave install <version>
# Fetch the version of node and install it in nave's folder.
# nave use <version>
# Install the <version> if it isn't already, and then start
# a subshell with that version's folder at the start of the
# $PATH
# nave use <version> 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
# Use fancy pants globs
shopt -s extglob
# Try to figure out the os and arch for binary fetching
uname="$(uname -a)"
os=
arch=
case "$uname" in
Linux\ *) os=linux ;;
Darwin\ *) os=darwin ;;
SunOS\ *) os=sunos ;;
esac
case "$uname" in
*i386*) arch=x86 ;;
*x86_64*) arch=x64 ;;
*raspberrypi*) arch=arm-pi ;;
esac
tar=${TAR-tar}
main () {
local SELF_PATH DIR SYM
# get the absolute path of the executable
SELF_PATH="$0"
if [ "${SELF_PATH:0:1}" != "." ] && [ "${SELF_PATH:0:1}" != "/" ]; then
SELF_PATH=./"$SELF_PATH"
fi
SELF_PATH=$( cd -P -- "$(dirname -- "$SELF_PATH")" \
&& pwd -P \
) && SELF_PATH=$SELF_PATH/$(basename -- "$0")
# resolve symlinks
while [ -h "$SELF_PATH" ]; do
DIR=$(dirname -- "$SELF_PATH")
SYM=$(readlink -- "$SELF_PATH")
SELF_PATH=$( cd -- "$DIR" \
&& cd -- $(dirname -- "$SYM") \
&& pwd \
)/$(basename -- "$SYM")
done
if [ -z "${NAVE_DIR+defined}" ]; then
if [ -d "$HOME" ]; then
NAVE_DIR="$HOME"/.nave
else
NAVE_DIR=/usr/local/lib/nave
fi
fi
if ! [ -d "$NAVE_DIR" ] && ! mkdir -p -- "$NAVE_DIR"; then
NAVE_DIR="$(dirname -- "$SELF_PATH")"
fi
# 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="#3"
local rcfile="$NAVE_DIR/.zshenv"
if ! [ -f "$rcfile" ] \
|| [ "$(head -n1 "$rcfile")" != "$rcversion" ]; then
cat > "$rcfile" <<RC
$rcversion
[ "\$NAVE_DEBUG" != "" ] && set -x || true
if [ "\$BASH" != "" ]; then
if [ "\$NAVE_LOGIN" != "" ]; then
[ -f ~/.bash_profile ] && . ~/.bash_profile || true
[ -f ~/.bash_login ] && . ~/.bash_login || true
[ -f ~/.profile ] && . ~/.profile || true
else
[ -f ~/.bashrc ] && . ~/.bashrc || true
fi
else
[ -f ~/.zshenv ] && . ~/.zshenv || true
export DISABLE_AUTO_UPDATE=true
if [ "\$NAVE_LOGIN" != "" ]; then
[ -f ~/.zprofile ] && . ~/.zprofile || true
[ -f ~/.zshrc ] && . ~/.zshrc || true
[ -f ~/.zlogin ] && . ~/.zlogin || true
else
[ -f ~/.zshrc ] && . ~/.zshrc || true
fi
fi
unset ZDOTDIR
export PATH=\$NAVEPATH:\$PATH
[ -f ~/.naverc ] && . ~/.naverc || true
RC
cat > "$NAVE_DIR/.zlogout" <<RC
[ -f ~/.zlogout ] && . ~/.zlogout || true
RC
fi
# couldn't write file
if ! [ -f "$rcfile" ] || [ "$(head -n1 "$rcfile")" != "$rcversion" ]; then
fail "Nave dir $NAVE_DIR is not writable."
fi
export NAVE_DIR
export NAVE_SRC="$NAVE_DIR/src"
export NAVE_ROOT="$NAVE_DIR/installed"
ensure_dir "$NAVE_SRC"
ensure_dir "$NAVE_ROOT"
local cmd="$1"
shift
case $cmd in
ls-remote | ls-all)
cmd="nave_${cmd/-/_}"
;;
# use)
# cmd="nave_named"
# ;;
install | fetch | use | clean | test | named | \
ls | uninstall | usemain | latest | stable | has | installed )
cmd="nave_$cmd"
;;
* )
cmd="nave_help"
;;
esac
$cmd "$@"
local ret=$?
if [ $ret -eq 0 ]; then
exit 0
else
echo "failed with code=$ret" >&2
exit $ret
fi
}
function enquote_all () {
local ARG ARGS
ARGS=""
for ARG in "$@"; do
[ -n "$ARGS" ] && ARGS="$ARGS "
ARGS="$ARGS'""$( echo " $ARG" \
| cut -c 2- \
| sed 's/'"'"'/'"'"'"'"'"'"'"'"'/g' \
)""'"
done
echo "$ARGS"
}
ensure_dir () {
if ! [ -d "$1" ]; then
mkdir -p -- "$1" || fail "couldn't create $1"
fi
}
remove_dir () {
if [ -d "$1" ]; then
rm -rf -- "$1" || fail "Could not remove $1"
fi
}
fail () {
echo "$@" >&2
exit 1
}
nave_fetch () {
local version=$(ver "$1")
if nave_has "$version"; then
return 0
fi
local src="$NAVE_SRC/$version"
remove_dir "$src"
ensure_dir "$src"
local url
local urls=(
"http://nodejs.org/dist/v$version/node-v$version.tar.gz"
"http://nodejs.org/dist/node-v$version.tar.gz"
"http://nodejs.org/dist/node-$version.tar.gz"
"https://iojs.org/dist/v$version/iojs-v$version.tar.gz"
)
for url in "${urls[@]}"; do
get -#Lf "$url" > "$src".tgz
if [ $? -eq 0 ]; then
$tar xzf "$src".tgz -C "$src" --strip-components=1
if [ $? -eq 0 ]; then
echo "fetched from $url" >&2
return 0
fi
fi
done
rm "$src".tgz
remove_dir "$src"
echo "Couldn't fetch $version" >&2
return 1
}
get () {
curl -H "user-agent:nave/$(curl --version | head -n1)" "$@"
return $?
}
build () {
local version="$1"
# shortcut - try the binary if possible.
if [ -n "$os" ]; then
local binavail
# binaries started with node 0.8.6
case "$version" in
0.8.[012345]) binavail=0 ;;
0.[1234567].*) binavail=0 ;;
*) binavail=0 ;;
esac
if [ $binavail -eq 1 ]; then
local t="$version-$os-$arch"
local tgz="$NAVE_SRC/$t.tgz"
for url in "https://nodejs.org/dist/v$version/node-v${t}.tar.gz" \
"http://iojs.org/dist/v$version/iojs-v${t}.tar.gz"; do
get -#Lf "$url" > "$tgz"
if [ $? -ne 0 ]; then
# binary download failed. oh well. cleanup, and proceed.
rm "$tgz"
else
# unpack straight into the build target.
$tar xzf "$tgz" -C "$2" --strip-components 1
if [ $? -ne 0 ]; then
rm "$tgz"
nave_uninstall "$version"
echo "Binary unpack failed, trying source." >&2
fi
# it worked!
echo "installed from binary" >&2
return 0
fi
done
echo "Binary download failed, trying source." >&2
fi
fi
nave_fetch "$version"
if [ $? != 0 ]; 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"
[ -f ~/.naverc ] && . ~/.naverc || true
if [ "$NAVE_CONFIG" == "" ]; then
NAVE_CONFIG=()
fi
JOBS=$jobs ./configure "${NAVE_CONFIG[@]}" --prefix="$2" \
|| fail "Failed to configure $version"
JOBS=$jobs make -j$jobs \
|| fail "Failed to make $version"
make install || fail "Failed to install $version"
) || fail "fail"
return $?
}
nave_usemain () {
if [ ${NAVELVL-0} -gt 0 ]; then
fail "Can't usemain inside a nave subshell. Exit to main shell."
fi
local version=$(ver "$1")
local current=$(command -v node >/dev/null 2>&1 && node -v)
local wn=$(which node || true)
local prefix="/usr/local"
if [ "x$wn" != "x" ]; then
prefix="${wn/\/bin\/node/}"
if [ "x$prefix" == "x" ]; then
prefix="/usr/local"
fi
fi
current="${current/v/}"
if [ "$current" == "$version" ]; then
return 0
fi
build "$version" "$prefix"
}
nave_install () {
local version=$(ver "$1")
if [ -z "$version" ]; then
fail "Must supply a version ('stable', 'latest' or numeric)"
fi
if nave_installed "$version"; then
return 0
fi
local install="$NAVE_ROOT/$version"
ensure_dir "$install"
build "$version" "$install"
local ret=$?
if [ $ret -ne 0 ]; then
remove_dir "$install"
return $ret
fi
}
nave_test () {
local version=$(ver "$1")
nave_fetch "$version"
local src="$NAVE_SRC/$version"
( cd -- "$src"
[ -f ~/.naverc ] && . ~/.naverc || true
if [ "$NAVE_CONFIG" == "" ]; then
NAVE_CONFIG=()
fi
./configure "${NAVE_CONFIG[@]}" || fail "failed to ./configure"
make test-all || fail "Failed tests"
) || fail "failed"
}
nave_ls () {
ls -- $NAVE_SRC | version_list "src" \
&& ls -- $NAVE_ROOT | version_list "installed" \
&& nave_ls_named \
|| return 1
}
nave_ls_remote () {
get -s http://nodejs.org/dist/ \
| version_list "node remote" \
|| return 1
get -s https://iojs.org/dist/ \
| version_list "io.js remote" \
|| return 1
}
nave_ls_named () {
echo "named:"
ls -- "$NAVE_ROOT" \
| egrep -v '[0-9]+\.[0-9]+\.[0-9]+' \
| sort \
| while read name; do
echo "$name: $(ver $($NAVE_ROOT/$name/bin/node -v 2>/dev/null))"
done
}
nave_ls_all () {
nave_ls \
&& (echo ""; nave_ls_remote) \
|| return 1
}
ver () {
local version="$1"
local nonames="$2"
version="${version/v/}"
case $version in
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])\.+([0-9])) echo $version ;;
*) [ "$nonames" = "" ] && echo $version ;;
esac
}
nave_version_family () {
local family="$1"
family="${family/v/}"
{ get -s http://nodejs.org/dist/;
get -s https://iojs.org/dist/; } \
| egrep -o $family'\.[0-9]+' \
| sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
| tail -n1
}
nave_latest () {
get -s http://nodejs.org/dist/ \
| egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \
| sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
| tail -n1
}
nave_stable () {
get -s http://nodejs.org/dist/ \
| egrep -o '[0-9]+\.[0-9]*[02468]\.[0-9]+' \
| sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
| tail -n1
}
version_list_named () {
egrep -v '[0-9]+\.[0-9]+\.[0-9]+' \
| sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
| organize_version_list \
|| return 1
}
version_list () {
echo "$1:"
egrep -o '[0-9]+\.[0-9]+\.[0-9]+' \
| sort -u -k 1,1n -k 2,2n -k 3,3n -t . \
| organize_version_list \
|| return 1
}
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" ] || return 1
}
nave_installed () {
local version=$(ver "$1")
[ -x "$NAVE_ROOT/$version/bin/node" ] || return 1
}
nave_use () {
local version=$(ver "$1")
# if it's not a version number, then treat as a name.
case "$version" in
+([0-9])\.+([0-9])\.+([0-9])) ;;
*)
nave_named "$@"
return $?
;;
esac
if [ -z "$version" ]; then
fail "Must supply a version"
fi
if [ "$version" == "$NAVENAME" ]; then
if [ $# -gt 1 ]; then
shift
"$@"
fi
return $?
fi
nave_install "$version" || fail "failed to install $version"
local prefix="$NAVE_ROOT/$version"
local lvl=$[ ${NAVELVL-0} + 1 ]
if [ $# -gt 1 ]; then
shift
nave_exec "$lvl" "$version" "$version" "$prefix" "$@"
return $?
else
nave_login "$lvl" "$version" "$version" "$prefix"
return $?
fi
}
# internal
nave_exec () {
nave_run "exec" "$@"
return $?
}
nave_login () {
nave_run "login" "$@"
return $?
}
nave_run () {
local exec="$1"
shift
local lvl="$1"
shift
local name="$1"
shift
local version="$1"
shift
local prefix="$1"
shift
local bin="$prefix/bin"
local lib="$prefix/lib/node"
local man="$prefix/share/man"
ensure_dir "$bin"
ensure_dir "$lib"
ensure_dir "$man"
# now $@ is the command to run, or empty if it's not an exec.
local exit_code
local args=()
local isLogin
if [ "$exec" == "exec" ]; then
isLogin=""
# source the nave env file, then run the command.
args=("-c" ". $(enquote_all $NAVE_DIR/.zshenv); $(enquote_all "$@")")
elif [ "$shell" == "zsh" ]; then
isLogin="1"
# no need to set rcfile, since ZDOTDIR is set.
args=()
else
isLogin="1"
# bash, use --rcfile argument
args=("--rcfile" "$NAVE_DIR/.zshenv")
fi
local nave="$version"
if [ "$version" != "$name" ]; then
nave="$name"-"$version"
fi
NAVELVL=$lvl \
NAVEPATH="$bin" \
NAVEVERSION="$version" \
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" \
ZDOTDIR="$NAVE_DIR" \
"$SHELL" "${args[@]}"
exit_code=$?
hash -r
return $exit_code
}
nave_named () {
local name="$1"
shift
local version=$(ver "$1" NONAMES)
if [ "$version" != "" ]; then
shift
fi
add_named_env "$name" "$version" || fail "failed to create $name env"
if [ "$name" == "$NAVENAME" ] && [ "$version" == "$NAVEVERSION" ]; then
if [ $# -gt 0 ]; then
"$@"
fi
return $?
fi
if [ "$version" = "" ]; then
version="$(ver "$("$NAVE_ROOT/$name/bin/node" -v 2>/dev/null)")"
fi
local prefix="$NAVE_ROOT/$name"
local lvl=$[ ${NAVELVL-0} + 1 ]
# get the version
if [ $# -gt 0 ]; then
nave_exec "$lvl" "$name" "$version" "$prefix" "$@"
return $?
else
nave_login "$lvl" "$name" "$version" "$prefix"
return $?
fi
}
add_named_env () {
local name="$1"
local version="$2"
local cur="$(ver "$($NAVE_ROOT/$name/bin/node -v 2>/dev/null)" "NONAMES")"
if [ "$version" != "" ]; then
version="$(ver "$version" "NONAMES")"
else
version="$cur"
fi
if [ "$version" = "" ]; then
echo "What version of node?"
read -p "stable, latest, x.y, or x.y.z > " version
version=$(ver "$version")
fi
# if that version is already there, then nothing to do.
if [ "$cur" = "$version" ]; then
return 0
fi
echo "Creating new env named '$name' using node $version" >&2
nave_install "$version" || fail "failed to install $version"
ensure_dir "$NAVE_ROOT/$name/bin"
ensure_dir "$NAVE_ROOT/$name/lib/node"
ensure_dir "$NAVE_ROOT/$name/lib/node_modules"
ensure_dir "$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"
}
nave_clean () {
rm -rf "$NAVE_SRC/$(ver "$1")" "$NAVE_SRC/$(ver "$1")".tgz "$NAVE_SRC/$(ver "$1")"-*.tgz
}
nave_uninstall () {
remove_dir "$NAVE_ROOT/$(ver "$1")"
}
nave_help () {
cat <<EOF
Usage: nave <cmd>
Commands:
install <version> Install the version passed (ex: 0.1.103)
use <version> Enter a subshell where <version> is being used
use <ver> <program> Enter a subshell, and run "<program>", then exit
use <name> <ver> Create a named env, using the specified version.
If the name already exists, but the version differs,
then it will update the link.
usemain <version> Install in /usr/local/bin (ie, use as your main nodejs)
clean <version> Delete the source code for <version>
uninstall <version> Delete the install for <version>
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
help Output help information
<version> can be the string "latest" to get the latest distribution.
<version> can be the string "stable" to get the latest stable version.
EOF
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment