Commit ce7f4793 authored by Hunter Loftis's avatar Hunter Loftis

bring caching logic to the forefront

parent 97f67177
#!/usr/bin/env bash
# bin/compile <build-dir> <cache-dir> <env-dir>
####### Configure environment
### Configure environment
set -o errexit # always exit on error
set -o errtrace # trap errors in functions as well
set -o pipefail # don't ignore exit codes when piping output
set -o posix # more strict failures in subshells
# set -x # enable debugging
# Configure directories
build_dir=$1
cache_dir=$2
env_dir=$3
bp_dir=$(cd $(dirname $0); cd ..; pwd)
heroku_dir=$build_dir/.heroku
mkdir -p $heroku_dir/node
warnings=$(mktemp)
# Load dependencies
source $bp_dir/lib/common.sh
source $bp_dir/lib/build.sh
source $bp_dir/lib/warnings.sh
# Avoid GIT_DIR leak from previous build steps
unset GIT_DIR
# Provide hook to deal with errors
trap build_failed ERR
####### Determine current state
head "Reading application state"
read_current_state
show_current_state
if [ "$iojs_engine" == "" ]; then
warn_node_engine "$node_engine"
else
warn_node_engine "$iojs_engine"
fi
warn_node_modules "$modules_source"
####### Vendor in binaries
head "Installing binaries"
if [ "$iojs_engine" == "" ]; then
install_node "$node_engine"
else
install_iojs "$iojs_engine"
fi
install_npm
####### Build the project's dependencies
head "Building dependencies"
cd $build_dir
build_dependencies
####### Create a Procfile if possible
head "Checking startup method"
ensure_procfile "$start_method" "$build_dir"
warn_start "$start_method"
####### Finalize the build
head "Finalizing build"
write_profile
write_export
clean_npm
clean_cache
create_cache
build_succeeded
set -o nounset # fail on unset variables
unset GIT_DIR # Avoid GIT_DIR leak from previous build steps
### Configure directories
BUILD_DIR=${1:-}
CACHE_DIR=${2:-}
ENV_DIR=${3:-}
BP_DIR=$(cd $(dirname ${0:-}); cd ..; pwd)
mkdir -p "$BUILD_DIR/.heroku/node/"
cd $BUILD_DIR
export PATH="$BUILD_DIR/.heroku/node/bin":$PATH
### Load dependencies
source $BP_DIR/lib/output.sh
source $BP_DIR/lib/json.sh
source $BP_DIR/lib/failure.sh
source $BP_DIR/lib/environment.sh
source $BP_DIR/lib/binaries.sh
source $BP_DIR/lib/cache.sh
source $BP_DIR/lib/dependencies.sh
### Handle errors
handle_failure() {
header "Build failed"
failure_message | indent
}
trap 'handle_failure' ERR
### Check initial state
[ -e "$BUILD_DIR/node_modules" ] && PREBUILD=true || PREBUILD=false
### Failures that should be caught immediately
fail_invalid_package_json "$BUILD_DIR"
warn_prebuilt_modules "$BUILD_DIR"
warn_missing_package_json "$BUILD_DIR"
### Compile
create_env() {
write_profile "$BP_DIR" "$BUILD_DIR"
write_export "$BP_DIR" "$BUILD_DIR"
export_env_dir "$ENV_DIR"
create_default_env
}
header "Creating runtime environment"
create_env # can't indent the whole thing because piping causes subshells; no exporting possible
list_node_config | indent
install_bins() {
local node_engine=$(read_json "$BUILD_DIR/package.json" ".engines.node")
local iojs_engine=$(read_json "$BUILD_DIR/package.json" ".engines.iojs")
local npm_engine=$(read_json "$BUILD_DIR/package.json" ".engines.npm")
if [ -n "$iojs_engine" ]; then
echo "engines.iojs (package.json): $iojs_engine (iojs)"
else
echo "engines.node (package.json): ${node_engine:-unspecified}"
fi
echo "engines.npm (package.json): ${npm_engine:-unspecified (use default)}"
echo ""
if [ -n "$iojs_engine" ]; then
warn_node_engine "$iojs_engine"
install_iojs "$iojs_engine" "$BUILD_DIR/.heroku/node"
else
warn_node_engine "$node_engine"
install_nodejs "$node_engine" "$BUILD_DIR/.heroku/node"
fi
install_npm "$npm_engine" "$BUILD_DIR/.heroku/node"
warn_old_npm
}
header "Installing binaries"
install_bins | indent
restore_cache() {
local cache_status=$(get_cache_status)
if [ "$cache_status" == "disabled" ]; then
echo "Skipping (cache disabled)"
elif [ "$cache_status" == "invalidated" ]; then
echo "Skipping (cache invalidated)"
else
local cache_directories=$(get_cache_directories)
if [ "$cache_directories" == "" ]; then
echo "Loading 1 from cacheDirectories (default):"
restore_cache_directories "$BUILD_DIR" "$CACHE_DIR" "node_modules"
else
echo "Loading $(echo $cache_directories | wc -w | xargs) from cacheDirectories (package.json):"
restore_cache_directories "$BUILD_DIR" "$CACHE_DIR" $cache_directories
fi
fi
}
header "Restoring cache"
restore_cache | indent
build_dependencies() {
if $PREBUILD; then
echo "Prebuild detected (node_modules already exists)"
rebuild_node_modules "$BUILD_DIR"
else
install_node_modules "$BUILD_DIR"
fi
}
header "Building dependencies"
build_dependencies | indent
cache_build() {
local cache_directories=$(get_cache_directories)
echo "Clearing previous node cache"
clear_cache
if [ "$cache_directories" == "" ]; then
echo "Saving 1 cacheDirectories (default):"
save_cache_directories "$BUILD_DIR" "$CACHE_DIR" "node_modules"
else
echo "Saving $(echo $cache_directories | wc -w | xargs) cacheDirectories (package.json):"
save_cache_directories "$BUILD_DIR" "$CACHE_DIR" $cache_directories
fi
}
header "Caching build"
cache_build | indent
summarize_build() {
cd $BUILD_DIR
(npm ls --depth=0 | tail -n +2 || true) 2>/dev/null
}
header "Build succeeded!"
summarize_build | indent
......@@ -2,9 +2,8 @@
# bin/detect <build-dir>
if [ -f $1/package.json ]; then
echo "Node.js" && exit 0
elif [ -f $1/server.js ]; then
echo "Node.js" && exit 0
else
echo "no" && exit 1
echo "Node.js"
exit 0
fi
exit 1
#!/usr/bin/env bash
# bin/release <build-dir>
cat << EOF
addons: []
default_process_types: {}
default_process_types:
web: npm start
EOF
needs_resolution() {
local semver=$1
if ! [[ "$semver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
return 0
else
return 1
fi
}
install_nodejs() {
local version="$1"
local dir="$2"
if needs_resolution "$version"; then
echo "Resolving node version ${version:-(latest stable)} via semver.io..."
local version=$(curl --silent --get --data-urlencode "range=${version}" https://semver.herokuapp.com/node/resolve)
fi
echo "Downloading and installing node $version..."
local download_url="http://s3pository.heroku.com/node/v$version/node-v$version-$os-$cpu.tar.gz"
curl "$download_url" -s -o - | tar xzf - -C /tmp
mv /tmp/node-v$version-$os-$cpu/* $dir
chmod +x $dir/bin/*
}
install_iojs() {
local version="$1"
local dir="$2"
if needs_resolution "$version"; then
echo "Resolving iojs version ${version:-(latest stable)} via semver.io..."
version=$(curl --silent --get --data-urlencode "range=${version}" https://semver.herokuapp.com/iojs/resolve)
fi
echo "Downloading and installing iojs $version..."
local download_url="https://iojs.org/dist/v$version/iojs-v$version-$os-$cpu.tar.gz"
curl $download_url -s -o - | tar xzf - -C /tmp
mv /tmp/iojs-v$version-$os-$cpu/* $dir
chmod +x $dir/bin/*
}
install_npm() {
local version="$1"
if [ "$version" == "" ]; then
echo "Using default npm version: `npm --version`"
else
if needs_resolution "$version"; then
echo "Resolving npm version ${version} via semver.io..."
version=$(curl --silent --get --data-urlencode "range=${version}" https://semver.herokuapp.com/npm/resolve)
fi
if [[ `npm --version` == "$version" ]]; then
echo "npm `npm --version` already installed with node"
else
echo "Downloading and installing npm $version (replacing version `npm --version`)..."
npm install --unsafe-perm --quiet -g npm@$version 2>&1 >/dev/null
fi
fi
}
build_failed() {
local warn=$(cat $warnings)
head "Build failed"
echo ""
info "We're sorry this build is failing!"
info ""
info "Are you running into a common issue?"
info "https://devcenter.heroku.com/articles/troubleshooting-node-deploys"
info ""
if [ "$warn" != "" ]; then
info "During the build we spotted some likely problems:"
info ""
echo "$warn" | indent
else
info "If you're stuck, please submit a ticket so we can help:"
info "https://help.heroku.com/"
fi
info ""
info "Love,"
info "Heroku"
}
build_succeeded() {
head "Build succeeded!"
echo ""
(npm ls --depth=0 || true) 2>/dev/null | indent
}
get_start_method() {
local build_dir=$1
if test -f $build_dir/Procfile; then
echo "Procfile"
elif [[ $(read_json "$build_dir/package.json" ".scripts.start") != "" ]]; then
echo "npm start"
elif test -f $build_dir/server.js; then
echo "server.js"
else
echo ""
fi
}
get_modules_source() {
local build_dir=$1
if test -d $build_dir/node_modules; then
echo "prebuilt"
elif test -f $build_dir/npm-shrinkwrap.json; then
echo "npm-shrinkwrap.json"
elif test -f $build_dir/package.json; then
echo "package.json"
else
echo ""
fi
}
get_modules_cached() {
local cache_dir=$1
if test -d $cache_dir/node/node_modules; then
echo "true"
else
echo "false"
fi
}
# Sets:
# iojs_engine
# node_engine
# npm_engine
# start_method
# modules_source
# npm_previous
# node_previous
# modules_cached
# environment variables (from ENV_DIR)
read_current_state() {
info "package.json..."
assert_json "$build_dir/package.json"
iojs_engine=$(read_json "$build_dir/package.json" ".engines.iojs")
node_engine=$(read_json "$build_dir/package.json" ".engines.node")
npm_engine=$(read_json "$build_dir/package.json" ".engines.npm")
info "build directory..."
start_method=$(get_start_method "$build_dir")
modules_source=$(get_modules_source "$build_dir")
info "cache directory..."
npm_previous=$(file_contents "$cache_dir/node/npm-version")
node_previous=$(file_contents "$cache_dir/node/node-version")
modules_cached=$(get_modules_cached "$cache_dir")
info "environment variables..."
export_env_dir $env_dir
export NPM_CONFIG_PRODUCTION=${NPM_CONFIG_PRODUCTION:-true}
export NODE_MODULES_CACHE=${NODE_MODULES_CACHE:-true}
}
show_current_state() {
echo ""
if [ "$iojs_engine" == "" ]; then
info "Node engine: ${node_engine:-unspecified}"
else
achievement "iojs"
info "Node engine: $iojs_engine (iojs)"
fi
info "Npm engine: ${npm_engine:-unspecified}"
info "Start mechanism: ${start_method:-none}"
info "node_modules source: ${modules_source:-none}"
info "node_modules cached: $modules_cached"
echo ""
printenv | grep ^NPM_CONFIG_ | indent
info "NODE_MODULES_CACHE=$NODE_MODULES_CACHE"
}
install_node() {
local node_engine=$1
# Resolve non-specific node versions using semver.herokuapp.com
if ! [[ "$node_engine" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
info "Resolving node version ${node_engine:-(latest stable)} via semver.io..."
node_engine=$(curl --silent --get --data-urlencode "range=${node_engine}" https://semver.herokuapp.com/node/resolve)
fi
# Download node from Heroku's S3 mirror of nodejs.org/dist
info "Downloading and installing node $node_engine..."
node_url="http://s3pository.heroku.com/node/v$node_engine/node-v$node_engine-linux-x64.tar.gz"
curl $node_url -s -o - | tar xzf - -C /tmp
# Move node (and npm) into .heroku/node and make them executable
mv /tmp/node-v$node_engine-linux-x64/* $heroku_dir/node
chmod +x $heroku_dir/node/bin/*
PATH=$heroku_dir/node/bin:$PATH
}
install_iojs() {
local iojs_engine=$1
# Resolve non-specific iojs versions using semver.herokuapp.com
if ! [[ "$iojs_engine" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
info "Resolving iojs version ${iojs_engine:-(latest stable)} via semver.io..."
iojs_engine=$(curl --silent --get --data-urlencode "range=${iojs_engine}" https://semver.herokuapp.com/iojs/resolve)
fi
# TODO: point at /dist once that's available
info "Downloading and installing iojs $iojs_engine..."
download_url="https://iojs.org/dist/v$iojs_engine/iojs-v$iojs_engine-linux-x64.tar.gz"
curl $download_url -s -o - | tar xzf - -C /tmp
# Move iojs/node (and npm) binaries into .heroku/node and make them executable
mv /tmp/iojs-v$iojs_engine-linux-x64/* $heroku_dir/node
chmod +x $heroku_dir/node/bin/*
PATH=$heroku_dir/node/bin:$PATH
}
install_npm() {
# Optionally bootstrap a different npm version
if [ "$npm_engine" != "" ]; then
if ! [[ "$npm_engine" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
info "Resolving npm version ${npm_engine} via semver.io..."
npm_engine=$(curl --silent --get --data-urlencode "range=${npm_engine}" https://semver.herokuapp.com/npm/resolve)
fi
if [[ `npm --version` == "$npm_engine" ]]; then
info "npm `npm --version` already installed with node"
else
info "Downloading and installing npm $npm_engine (replacing version `npm --version`)..."
npm install --unsafe-perm --quiet -g npm@$npm_engine 2>&1 >/dev/null | indent
fi
warn_old_npm `npm --version`
else
info "Using default npm version: `npm --version`"
fi
}
function build_dependencies() {
if [ "$modules_source" == "" ]; then
info "Skipping dependencies (no source for node_modules)"
elif [ "$modules_source" == "prebuilt" ]; then
info "Rebuilding any native modules for this architecture"
npm rebuild 2>&1 | indent
info "Installing any new modules"
npm install --unsafe-perm --quiet --userconfig $build_dir/.npmrc 2>&1 | indent
else
restore_cache
info "Installing node modules"
npm install --unsafe-perm --quiet --userconfig $build_dir/.npmrc 2>&1 | indent
fi
}
ensure_procfile() {
local start_method=$1
local build_dir=$2
if [ "$start_method" == "Procfile" ]; then
info "Found Procfile"
elif test -f $build_dir/Procfile; then
info "Procfile created during build"
elif [ "$start_method" == "npm start" ]; then
info "No Procfile; Adding 'web: npm start' to new Procfile"
echo "web: npm start" > $build_dir/Procfile
elif [ "$start_method" == "server.js" ]; then
info "No Procfile; Adding 'web: node server.js' to new Procfile"
echo "web: node server.js" > $build_dir/Procfile
else
info "None found"
fi
}
write_profile() {
info "Creating runtime environment"
mkdir -p $build_dir/.profile.d
echo "export PATH=\"\$HOME/.heroku/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\"" > $build_dir/.profile.d/nodejs.sh
echo "export NODE_HOME=\"\$HOME/.heroku/node\"" >> $build_dir/.profile.d/nodejs.sh
cat $bp_dir/lib/concurrency.sh >> $build_dir/.profile.d/nodejs.sh
}
write_export() {
info "Exporting binary paths"
echo "export PATH=\"$build_dir/.heroku/node/bin:$build_dir/node_modules/.bin:\$PATH\"" > $bp_dir/export
echo "export NODE_HOME=\"$build_dir/.heroku/node\"" >> $bp_dir/export
}
clean_npm() {
info "Cleaning npm artifacts"
rm -rf "$build_dir/.node-gyp"
rm -rf "$build_dir/.npm"
}
# Caching
create_cache() {
info "Caching results for future builds"
mkdir -p $cache_dir/node
echo `node --version` > $cache_dir/node/node-version
echo `npm --version` > $cache_dir/node/npm-version
if test -d $build_dir/node_modules; then
cp -r $build_dir/node_modules $cache_dir/node
fi
write_user_cache
}
clean_cache() {
info "Cleaning previous cache"
rm -rf "$cache_dir/node_modules" # (for apps still on the older caching strategy)
rm -rf "$cache_dir/node"
}
get_cache_status() {
local node_version=`node --version`
local npm_version=`npm --version`
# Did we bust the cache?
if ! $modules_cached; then
echo "No cache available"
elif ! $NODE_MODULES_CACHE; then
echo "Cache disabled with NODE_MODULES_CACHE"
elif [ "$node_previous" != "" ] && [ "$node_version" != "$node_previous" ]; then
echo "Node version changed ($node_previous => $node_version); invalidating cache"
elif [ "$npm_previous" != "" ] && [ "$npm_version" != "$npm_previous" ]; then
echo "Npm version changed ($npm_previous => $npm_version); invalidating cache"
else
echo "valid"
fi
}
restore_cache() {
local directories=($(cache_directories))
local cache_status=$(get_cache_status)
if [ "$directories" != -1 ]; then
info "Restoring ${#directories[@]} directories from cache:"
for directory in "${directories[@]}"
do
local source_dir="$cache_dir/node/$directory"
if [ -e $source_dir ]; then
if [ "$directory" == "node_modules" ]; then
restore_npm_cache
else
info "- $directory"
cp -r $source_dir $build_dir/
fi
fi
done
elif [ "$cache_status" == "valid" ]; then
restore_npm_cache
info "$cache_status"
else
touch $build_dir/.npmrc
fi
}
restore_npm_cache() {
info "Restoring node modules from cache"
cp -r $cache_dir/node/node_modules $build_dir/
info "Pruning unused dependencies"
npm --unsafe-perm prune 2>&1 | indent
}
cache_directories() {
local package_json="$build_dir/package.json"
local key=".cache_directories"
local check=$(key_exist $package_json $key)
local result=-1
if [ "$check" != -1 ]; then
result=$(read_json "$package_json" "$key[]")
fi
local key=".cacheDirectories"
local check=$(key_exist $package_json $key)
if [ "$check" != -1 ]; then
result=$(read_json "$package_json" "$key[]")
fi
echo $result
}
key_exist() {
local file=$1
local key=$2
local output=$(read_json $file $key)
if [ -n "$output" ]; then
echo 1
else
echo -1
fi
}
write_user_cache() {
local directories=($(cache_directories))
if [ "$directories" != -1 ]; then
info "Storing directories:"
for directory in "${directories[@]}"
do
info "- $directory"
cp -r $build_dir/$directory $cache_dir/node/
done
fi
}
source $BP_DIR/lib/binaries.sh
create_signature() {
echo "$(node --version); $(npm --version)"
}
save_signature() {
echo "$(get_signature)" > $CACHE_DIR/node/signature
}
load_signature() {
if test -f $CACHE_DIR/node/signature; then
cat $CACHE_DIR/node/signature
else
echo ""
fi
}
signature_changed() {
if ! [ "$(create_signature)" == "$(load_signature)" ]; then
return 1
else
return 0
fi
}
get_cache_status() {
if ! ${NODE_MODULES_CACHE:-true}; then
echo "disabled"
elif signature_changed; then
echo "invalidated"
else
echo "valid"
fi
}
get_cache_directories() {
local dirs1=$(read_json "$BUILD_DIR/package.json" ".cacheDirectories | .[]?")
local dirs2=$(read_json "$BUILD_DIR/package.json" ".cache_directories | .[]?")
if [ -n "$dirs1" ]; then
echo "$dirs1"
else
echo "$dirs2"
fi
}
restore_cache_directories() {
local build_dir=${1:-}
local cache_dir=${2:-}
for cachepath in ${@:3}; do
if [ -e "$build_dir/$cachepath" ]; then
echo "- $cachepath (exists - skipping)"
else
if [ -e "$cache_dir/node/$cachepath" ]; then
echo "- $cachepath"
mkdir -p "$build_dir/$cachepath"
cp -a "$cache_dir/node/$cachepath" $(dirname "$build_dir/$cachepath")
else
echo "- $cachepath (not cached - skipping)"
fi
fi
done
}
clear_cache() {
rm -rf $CACHE_DIR/node
}
save_cache_directories() {
local build_dir=${1:-}
local cache_dir=${2:-}
mkdir -p $cache_dir/node
for cachepath in ${@:3}; do
if [ -e "$build_dir/$cachepath" ]; then
echo "- $cachepath"
mkdir -p "$cache_dir/node/$cachepath"
cp -a "$build_dir/$cachepath" $(dirname "$cache_dir/node/$cachepath")
else
echo "- $cachepath (nothing to cache)"
fi
done
}
error() {
echo " ! $*" >&2
echo ""
return 1
}
head() {
echo ""
echo "-----> $*"
}
info() {
#echo "`date +\"%M:%S\"` $*"
echo " $*"
}
warning() {
local tip=$1
local url=$2
echo "- $tip" >> $warnings
echo " ${url:-https://devcenter.heroku.com/articles/nodejs-support}" >> $warnings
echo "" >> $warnings
}
achievement() {
local msg=$1
echo " ACHIEVEMENT UNLOCKED: $msg :)"
echo ""
}
assert_json() {
local file=$1
if test -f $file; then
if ! cat $file | $bp_dir/vendor/jq '.' > /dev/null; then
error "Unable to parse $file as JSON"
fi
fi
}
file_contents() {
if test -f $1; then
echo "$(cat $1)"
else
echo ""
fi
}
read_json() {
local file=$1
local node=$2
if test -f $file; then
cat $file | $bp_dir/vendor/jq --raw-output "$node // \"\"" || return 1
else
echo ""
fi
}
# sed -l basically makes sed replace and buffer through stdin to stdout
# so you get updates while the command runs and dont wait for the end
# e.g. npm install | indent
indent() {
c='s/^/ /'
case $(uname) in
Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries
*) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data
esac
}
cat_npm_debug_log() {
test -f $build_dir/npm-debug.log && cat $build_dir/npm-debug.log
}
export_env_dir() {
env_dir=$1
if [ -d "$env_dir" ]; then
whitelist_regex=${2:-''}
blacklist_regex=${3:-'^(PATH|GIT_DIR|CPATH|CPPATH|LD_PRELOAD|LIBRARY_PATH)$'}
if [ -d "$env_dir" ]; then
for e in $(ls $env_dir); do
echo "$e" | grep -E "$whitelist_regex" | grep -qvE "$blacklist_regex" &&
export "$e=$(cat $env_dir/$e)"
:
done
fi
fi
}
install_node_modules() {
local build_dir=${1:-}
if [ -e $build_dir/package.json ]; then
cd $build_dir
echo "Pruning any extraneous modules"
npm prune --unsafe-perm --userconfig $build_dir/.npmrc 2>&1
if [ -e $build_dir/npm-shrinkwrap.json ]; then
echo "Installing node modules (package.json + shrinkwrap)"
else
echo "Installing node modules (package.json)"
fi
npm install --unsafe-perm --userconfig $build_dir/.npmrc 2>&1
else
echo "Skipping (no package.json)"
fi
}
rebuild_node_modules() {
local build_dir=${1:-}
if [ -e $build_dir/package.json ]; then
cd $build_dir
echo "Pruning any extraneous modules"
npm prune --unsafe-perm --userconfig $build_dir/.npmrc 2>&1
echo "Rebuilding any native modules"
npm rebuild 2>&1
if [ -e $build_dir/npm-shrinkwrap.json ]; then
echo "Installing any new modules (package.json + shrinkwrap)"
else
echo "Installing any new modules (package.json)"
fi
npm install --unsafe-perm --userconfig $build_dir/.npmrc 2>&1
else
echo "Skipping (no package.json)"
fi
}
create_default_env() {
export NPM_CONFIG_PRODUCTION=${NPM_CONFIG_PRODUCTION:-true}
export NPM_CONFIG_LOGLEVEL=${NPM_CONFIG_LOGLEVEL:-error}
export NODE_MODULES_CACHE=${NODE_MODULES_CACHE:-true}
}
list_node_config() {
echo ""
printenv | grep ^NPM_CONFIG_ || true
printenv | grep ^NODE_ || true
}
export_env_dir() {
local env_dir=$1
if [ -d "$env_dir" ]; then
local whitelist_regex=${2:-''}
local blacklist_regex=${3:-'^(PATH|GIT_DIR|CPATH|CPPATH|LD_PRELOAD|LIBRARY_PATH)$'}
if [ -d "$env_dir" ]; then
for e in $(ls $env_dir); do
echo "$e" | grep -E "$whitelist_regex" | grep -qvE "$blacklist_regex" &&
export "$e=$(cat $env_dir/$e)"
:
done
fi
fi
}
write_profile() {
local bp_dir="$1"
local build_dir="$2"
mkdir -p $build_dir/.profile.d
cp $bp_dir/profile/* $build_dir/.profile.d/
}
write_export() {
local bp_dir="$1"
local build_dir="$2"
echo "export PATH=\"$build_dir/.heroku/node/bin:$build_dir/node_modules/.bin:\$PATH\"" > $bp_dir/export
echo "export NODE_HOME=\"$build_dir/.heroku/node\"" >> $bp_dir/export
}
warnings=$(mktemp -t heroku-buildpack-nodejs-XXXX)
failure_message() {
local warn="$(cat $warnings)"
echo ""
echo "We're sorry this build is failing! You can troubleshoot common issues here:"
echo "https://devcenter.heroku.com/articles/troubleshooting-node-deploys"
echo ""
if [ "$warn" != "" ]; then
echo "Some possible problems:"
echo ""
echo "$warn"
else
echo "If you're stuck, please submit a ticket so we can help:"
echo "https://help.heroku.com/"
fi
echo ""
echo "Love,"
echo "Heroku"
echo ""
}
fail_invalid_package_json() {
if ! cat ${1:-}/package.json | $JQ "." 1>/dev/null; then
error "Unable to parse package.json"
return 1
fi
}
warning() {
local tip=${1:-}
local url=${2:-https://devcenter.heroku.com/articles/nodejs-support}
echo "- $tip" >> $warnings
echo " $url" >> $warnings
echo "" >> $warnings
}
warn_node_engine() {
local node_engine=$1
local node_engine=${1:-}
if [ "$node_engine" == "" ]; then
warning "Node version not specified in package.json" "https://devcenter.heroku.com/articles/nodejs-support#specifying-a-node-js-version"
elif [ "$node_engine" == "*" ]; then
......@@ -9,26 +46,24 @@ warn_node_engine() {
fi
}
warn_node_modules() {
local modules_source=$1
if [ "$modules_source" == "prebuilt" ]; then
warn_prebuilt_modules() {
local build_dir=${1:-}
if [ -e "$build_dir/node_modules" ]; then
warning "node_modules checked into source control" "https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-"
elif [ "$modules_source" == "" ]; then
warning "No package.json found"
fi
}
warn_start() {
local start_method=$1
if [ "$start_method" == "" ]; then
warning "No Procfile, package.json start script, or server.js file found" "https://devcenter.heroku.com/articles/nodejs-support#runtime-behavior"
warn_missing_package_json() {
local build_dir=${1:-}
if ! [ -e "$build_dir/package.json" ]; then
warning "No package.json found"
fi
}
warn_old_npm() {
local npm_version=$1
local npm_version="$(npm --version)"
if [ "${npm_version:0:1}" -lt "2" ]; then
local latest_npm=$(curl --silent --get https://semver.herokuapp.com/npm/stable)
local latest_npm="$(curl --silent --get https://semver.herokuapp.com/npm/stable)"
warning "This version of npm ($npm_version) has several known issues - consider upgrading to the latest release ($latest_npm)" "https://devcenter.heroku.com/articles/nodejs-support#specifying-an-npm-version"
fi
}
get_os() {
uname | tr A-Z a-z
}
get_cpu() {
if [[ "$(uname -p)" = "i686" ]]; then
echo "x86"
else
echo "x64"
fi
}
os=$(get_os)
cpu=$(get_cpu)
export JQ="$BP_DIR/vendor/jq-$os"
read_json() {
local file=$1
local key=$2
if test -f $file; then
cat $file | $JQ --raw-output "$key // \"\"" || return 1
else
echo ""
fi
}
info() {
echo " $*"
}
# sed -l basically makes sed replace and buffer through stdin to stdout
# so you get updates while the command runs and dont wait for the end
# e.g. npm install | indent
indent() {
c='s/^/ /'
case $(uname) in
Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries
*) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data
esac
}
header() {
echo ""
echo "-----> $*"
}
error() {
echo " ! $*" >&2
echo ""
}
......@@ -8,3 +8,8 @@ test-cedar-14:
test-cedar-10:
@echo "Running tests in docker (cedar)..."
@docker run -v $(shell pwd):/buildpack:ro --rm -it fabiokung/cedar bash -c 'cp -r /buildpack /buildpack_test; cd /buildpack_test/; test/run;'
shell:
@echo "Opening cedar-14 shell..."
@docker run -v $(shell pwd):/buildpack:ro --rm -it heroku/cedar:14 bash -c 'cp -r /buildpack /buildpack_test; cd /buildpack_test/; bash'
@echo ""
......@@ -27,6 +27,9 @@ detect_memory() {
esac
}
export PATH="$HOME/.heroku/node/bin:$HOME/bin:$HOME/node_modules/.bin:$PATH"
export NODE_HOME="$HOME/.heroku/node"
calculate_concurrency
log_concurrency
......
{
"name": "client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"repository": {
"type": "git",
"url": "http://github.com/example/example.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"underscore": "^1.8.3"
}
}
......@@ -7,13 +7,11 @@
"url" : "http://github.com/example/example.git"
},
"engines": {
"node": "0.10.18"
},
"dependencies": {
"bower": "1.3.12"
"node": "0.12.4"
},
"dependencies": {},
"scripts": {
"postinstall": "bower install --allow-root"
"postinstall": "npm install --prefix=server && npm install --prefix=client"
},
"cacheDirectories": ["bower_components", "node_modules"]
"cacheDirectories": ["server/node_modules", "client/node_modules", "non/existent"]
}
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"repository": {
"type": "git",
"url": "http://github.com/example/example.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"lodash": "^3.9.3"
}
}
......@@ -6,8 +6,8 @@
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"dependencies": {
"forcefailure": "*"
"scripts": {
"postinstall": "exit 1"
},
"engines": {
"node": "*"
......
......@@ -10,7 +10,7 @@
"hashish": "*"
},
"engines": {
"node": "0.10.x"
"node": "0.10.38"
},
"scripts": {
"postinstall": "exit 1"
......
......@@ -7,7 +7,7 @@
"url" : "http://github.com/example/example.git"
},
"engines": {
"node": "~0.10.20"
"node": "0.12.4"
},
"dependencies": {
"some-crazy-dep-that-doesnt-exist": "*"
......
......@@ -10,6 +10,6 @@
"lodash": "1.0.0"
},
"engines": {
"node": "~0.10.0"
"node": "0.12.4"
}
}
......@@ -10,6 +10,6 @@
"lodash": "^1.0.0"
},
"engines": {
"node": "~0.10.0"
"node": "0.12.4"
}
}
A fake README, to keep npm from polluting stderr.
\ No newline at end of file
{
"name": "node-buildpack-test-app",
"version": "0.0.1",
"description": "node buildpack integration test app",
"repository" : {
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"dependencies": {
"hashish": "*"
},
"engines": {
"node": "~0.10.0"
}
}
A fake README, to keep npm from polluting stderr.
\ No newline at end of file
{
"name": "node-buildpack-test-app",
"version": "0.0.1",
"description": "node buildpack integration test app",
"repository" : {
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"dependencies": {
"hashish": "*"
},
"engines": {
"node": "~0.10.0"
},
"scripts": {
"start": "echo foo"
}
}
A fake README, to keep npm from polluting stderr.
\ No newline at end of file
{
"name": "node-buildpack-test-app",
"version": "0.0.1",
"description": "node buildpack integration test app",
"repository" : {
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"dependencies": {
"hashish": "*"
},
"engines": {
"node": "~0.10.0"
}
}
A fake README, to keep npm from polluting stderr.
\ No newline at end of file
A fake README, to keep npm from polluting stderr.
\ No newline at end of file
#!/usr/bin/env bash
mktmpdir() {
local dir=$(mktemp -t testXXXXX)
rm -rf $dir
mkdir $dir
echo $dir
}
compile() {
local fixture=$1
local bp_dir=$(mktmpdir)
local build_dir=$(mktmpdir)
local cache_dir=$(mktmpdir)
local env_dir=$(mktmpdir)
echo "Compiling $fixture"
echo "in $build_dir"
echo "(caching in $cache_dir)"
cp -a $(pwd)/* ${bp_dir}
cp -a ${bp_dir}/test/fixtures/$fixture/. ${build_dir}
"$bp_dir/bin/compile" "$build_dir" "$cache_dir"
}
fixture=${1:-'stable-node'}
compile "$fixture"
#!/usr/bin/env bash
# See README.md for info on running these tests.
testDetectWithPackageJson() {
detect "stable-node"
assertCaptured "Node.js"
testDisableCache() {
cache=$(mktmpdir)
env_dir=$(mktmpdir)
compile "node-modules-cache-1" $cache
assertCaptured "lodash@1.0.0"
assertEquals "1" "$(ls -1 $cache/node/node_modules | grep lodash | wc -l | tr -d ' ')"
assertCapturedSuccess
compile "node-modules-cache-2" $cache
assertCaptured "lodash@1.0.0"
assertCapturedSuccess
echo "false" > $env_dir/NODE_MODULES_CACHE
compile "node-modules-cache-2" $cache $env_dir
assertCaptured "lodash@1.3.1"
assertCapturedSuccess
}
testDetectWithServer() {
detect "server-present-only"
testDetectWithPackageJson() {
detect "stable-node"
assertCaptured "Node.js"
assertCapturedSuccess
}
......@@ -22,26 +35,22 @@ testBadJson() {
compile "bad-json"
assertCaptured "Build failed"
assertCaptured "We're sorry this build is failing"
assertNotCaptured "build directory..."
assertNotCaptured "Installing binaries"
assertCapturedError 1 "Unable to parse"
}
testIoJs() {
compile "iojs"
assertCaptured "Node engine: 1.0."
assertCaptured "(iojs)"
assertCaptured "detected node version: v1.0."
assertCaptured "engines.iojs (package.json): 1.0."
assertCaptured "Downloading and installing iojs 1.0."
assertCapturedSuccess
}
testNoVersion() {
compile "no-version"
assertCaptured "Node engine: unspecified"
assertCaptured "engines.node (package.json): unspecified"
assertCaptured "Resolving node version (latest stable) via semver.io"
assertCaptured "Downloading and installing node 0.12"
assertNotCaptured "Node version not specified in package.json"
assertNotCaptured "We're sorry this build is failing"
assertCaptured "Downloading and installing node 0.12."
assertCapturedSuccess
}
......@@ -50,13 +59,12 @@ testSpecificVersion() {
assertNotCaptured "Resolving node version"
assertCaptured "Downloading and installing node 0.10.29"
assertCaptured "Using default npm version: 1.4.14"
assertNotCaptured "We're sorry this build is failing"
assertCapturedSuccess
}
testStableVersion() {
compile "stable-node"
assertCaptured "Downloading and installing node 0.10"
assertCaptured "Downloading and installing node 0.10."
assertNotCaptured "We're sorry this build is failing"
assertCapturedSuccess
}
......@@ -64,7 +72,7 @@ testStableVersion() {
testUnstableVersion() {
compile "unstable-version"
assertCaptured "Resolving node version 0.11.x via semver.io"
assertCaptured "Downloading and installing node 0.11"
assertCaptured "Downloading and installing node 0.11."
assertCapturedSuccess
}
......@@ -75,6 +83,11 @@ testOldNpm() {
assertCapturedError
}
testOldNpm2() {
compile "failing-build"
assertCaptured "This version of npm (1.4.28) has several known issues"
}
testNonexistentNpm() {
compile "nonexistent-npm"
assertCaptured "version not found: npm@1.1.65"
......@@ -124,27 +137,24 @@ testWarningsOnFailure() {
testTicketOnFailure() {
compile "invalid-dependency"
assertCaptured "troubleshooting-node-deploys"
assertNotCaptured "During the build we spotted some likely problems"
assertCaptured "please submit a ticket"
assertNotCaptured "possible problems"
assertCapturedError
}
testInfoEmpty() {
compile "info-empty"
assertCaptured "Node engine: unspecified"
assertCaptured "Npm engine: unspecified"
assertCaptured "Start mechanism: none"
assertCaptured "node_modules source: package.json"
assertCaptured "node_modules cached: false"
assertCaptured "engines.node (package.json): unspecified"
assertCaptured "engines.npm (package.json): unspecified"
assertCaptured "Installing node modules (package.json)"
assertCapturedSuccess
}
testDangerousRangeStar() {
compile "dangerous-range-star"
assertCaptured "Dangerous semver range"
assertCaptured "Node engine: *"
assertCaptured "Resolving node version * via semver.io"
assertCaptured "Downloading and installing node 0.12"
assertCaptured "Downloading and installing node 0.12."
assertCapturedError
}
......@@ -171,50 +181,57 @@ testInvalidDependency() {
testNodeModulesCached() {
cache=$(mktmpdir)
compile "caching" $cache
assertCaptured "Caching results for future builds"
assertEquals "1" "$(ls -1 $cache/ | grep node | wc -l)"
assertEquals "1" "$(ls -1 $cache/node | grep node_modules | wc -l)"
assertCaptured "Saving 1 cacheDirectories (default)"
assertCaptured "- node_modules"
assertEquals "1" "$(ls -1 $cache/node | grep node_modules | wc -l | tr -d ' ')"
assertCapturedSuccess
}
testBuildWithCache() {
cache=$(mktmpdir)
compile "stable-node" $cache
assertCaptured "node_modules cached: false"
assertCaptured "Caching results for future builds"
assertCaptured "- node_modules (not cached - skipping)"
assertEquals "1" "$(ls -1 $cache/node | grep node_modules | wc -l | tr -d ' ')"
assertCapturedSuccess
compile "stable-node" $cache
assertCaptured "node_modules cached: true"
assertCaptured "Restoring node modules from cache"
assertNotCaptured "- node_modules (not cached - skipping)"
assertCapturedSuccess
}
testBuildWithUserCacheDirectories() {
cache=$(mktmpdir)
compile "cache-directories" $cache
assertCaptured "Saving 2 cacheDirectories"
assertEquals "1" "$(ls -1 $cache/node | grep bower_components | wc -l | tr -d ' ')"
assertEquals "1" "$(ls -1 $cache/node | grep node_modules | wc -l | tr -d ' ')"
assertCapturedSuccess
assertEquals "1" "$(ls -1 $cache/node | grep bower_components | wc -l)"
assertEquals "1" "$(ls -1 $cache/node | grep node_modules | wc -l)"
compile "cache-directories" $cache
assertCaptured "Restoring 2 directories from cache:"
assertCaptured "Loading 2 from cacheDirectories"
assertCaptured "- node_modules"
assertCaptured "- bower_components"
assertCaptured "Restoring node modules from cache"
assertCapturedSuccess
}
testBuildWithUserCacheDirectoriesCamel() {
cache=$(mktmpdir)
compile "cache-directories-camel" $cache
assertCaptured "- non/existent (nothing to cache)"
assertEquals "1" "$(ls -1 $cache/node/server | grep node_modules | wc -l | tr -d ' ')"
assertEquals "1" "$(ls -1 $cache/node/client | grep node_modules | wc -l | tr -d ' ')"
assertCapturedSuccess
assertEquals "1" "$(ls -1 $cache/node | grep bower_components | wc -l)"
assertEquals "1" "$(ls -1 $cache/node | grep node_modules | wc -l)"
compile "cache-directories" $cache
assertCaptured "Restoring 2 directories from cache:"
assertCaptured "- bower_components"
assertCaptured "Restoring node modules from cache"
compile "cache-directories-camel" $cache
assertCaptured "Loading 3 from cacheDirectories"
assertCaptured "- server/node_modules"
assertCaptured "- client/node_modules"
assertCaptured "- non/existent (not cached - skipping)"
assertCapturedSuccess
}
......@@ -224,13 +241,11 @@ testModulesCheckedIn() {
assertCapturedSuccess
compile "modules-checked-in" $cache
assertCaptured "node_modules source: prebuilt"
assertCaptured "Rebuilding any native modules for this architecture"
assertCaptured "Prebuild detected"
assertCaptured "Rebuilding any native modules"
assertCaptured "(preinstall script)"
assertCaptured "Installing any new modules"
assertCaptured "(postinstall script)"
assertNotCaptured "Restoring node modules"
assertNotCaptured "Pruning unused dependencies"
assertCapturedSuccess
}
......@@ -241,52 +256,18 @@ testUserConfig() {
assertCapturedError 1 ""
}
testProcfile() {
compile "procfile-present-only"
assertCaptured "Start mechanism: Procfile"
assertNotCaptured "new Procfile"
assertCapturedSuccess
}
testProcfileAbsentNpmStartPresent() {
compile "procfile-absent-npm-start-present"
assertCaptured "Start mechanism: npm start"
assertCaptured "Adding 'web: npm start' to new Procfile"
assertFile "web: npm start" "Procfile"
assertCapturedSuccess
}
testProcfileAbsentNpmStartAbsent() {
compile "procfile-absent-npm-start-absent"
assertCaptured "Start mechanism: none"
assertCaptured "None found"
assertNotCaptured "new Procfile"
testDefaultProcType() {
release "stable-node"
assertCaptured "web: npm start"
assertCapturedSuccess
}
testDynamicProcfile() {
compile "dynamic-procfile"
assertCaptured "Procfile created during build"
assertFileContains "web: node index.js customArg" "${compile_dir}/Procfile"
assertCapturedSuccess
}
testProcfileAbsentServerPresent() {
compile "procfile-absent-server-present"
assertCaptured "Start mechanism: server.js"
assertCaptured "'web: node server.js' to new Procfile"
assertFile "web: node server.js" "Procfile"
assertCapturedSuccess
}
testServerPresentOnly() {
compile "server-present-only"
assertCaptured "Skipping dependencies"
assertCaptured "'web: node server.js' to new Procfile"
assertFile "web: node server.js" "Procfile"
assertCapturedSuccess
}
testEnvVars() {
env_dir=$(mktmpdir)
echo "false" > $env_dir/NPM_CONFIG_PRODUCTION
......@@ -329,7 +310,6 @@ testDevDependencies() {
testOptionalDependencies() {
env_dir=$(mktmpdir)
#echo "true" > $env_dir/NPM_CONFIG_OPTIONAL
compile "optional-dependencies" "$(mktmpdir)" $env_dir
assertNotCaptured "NPM_CONFIG_OPTIONAL"
assertCaptured "less"
......@@ -353,24 +333,6 @@ testNoOptionalDependencies() {
assertCapturedSuccess
}
testDisableCache() {
cache=$(mktmpdir)
env_dir=$(mktmpdir)
compile "node-modules-cache-1" $cache
assertCaptured "lodash@1.0.0"
assertCapturedSuccess
compile "node-modules-cache-2" $cache
assertCaptured "lodash@1.0.0"
assertCapturedSuccess
echo "false" > $env_dir/NODE_MODULES_CACHE
compile "node-modules-cache-2" $cache $env_dir
assertCaptured "lodash@1.3.1"
assertCapturedSuccess
}
testNpmrc() {
compile "dev-dependencies"
assertNotCaptured "lodash"
......@@ -408,35 +370,35 @@ testMultiExport() {
}
testConcurrency1X() {
MEMORY_AVAILABLE=512 capture ${bp_dir}/lib/concurrency.sh
MEMORY_AVAILABLE=512 capture $(pwd)/profile/nodejs.sh
assertCaptured "Detected 512 MB available memory, 512 MB limit per process (WEB_MEMORY)"
assertCaptured "Recommending WEB_CONCURRENCY=1"
assertCapturedSuccess
}
testConcurrency2X() {
MEMORY_AVAILABLE=1024 capture ${bp_dir}/lib/concurrency.sh
MEMORY_AVAILABLE=1024 capture $(pwd)/profile/nodejs.sh
assertCaptured "Detected 1024 MB available memory, 512 MB limit per process (WEB_MEMORY)"
assertCaptured "Recommending WEB_CONCURRENCY=2"
assertCapturedSuccess
}
testConcurrencyPX() {
MEMORY_AVAILABLE=6144 capture ${bp_dir}/lib/concurrency.sh
MEMORY_AVAILABLE=6144 capture $(pwd)/profile/nodejs.sh
assertCaptured "Detected 6144 MB available memory, 512 MB limit per process (WEB_MEMORY)"
assertCaptured "Recommending WEB_CONCURRENCY=12"
assertCapturedSuccess
}
testConcurrencyCustomLimit() {
MEMORY_AVAILABLE=1024 WEB_MEMORY=256 capture ${bp_dir}/lib/concurrency.sh
MEMORY_AVAILABLE=1024 WEB_MEMORY=256 capture $(pwd)/profile/nodejs.sh
assertCaptured "Detected 1024 MB available memory, 256 MB limit per process (WEB_MEMORY)"
assertCaptured "Recommending WEB_CONCURRENCY=4"
assertCapturedSuccess
}
testConcurrencySaneMaximum() {
MEMORY_AVAILABLE=6144 WEB_MEMORY=32 capture ${bp_dir}/lib/concurrency.sh
MEMORY_AVAILABLE=6144 WEB_MEMORY=32 capture $(pwd)/profile/nodejs.sh
assertCaptured "Detected 6144 MB available memory, 32 MB limit per process (WEB_MEMORY)"
assertCaptured "Recommending WEB_CONCURRENCY=32"
assertCapturedSuccess
......@@ -446,10 +408,9 @@ testConcurrencySaneMaximum() {
# Utils
pushd $(dirname 0) >/dev/null
bp_dir=$(pwd)
popd >/dev/null
source ${bp_dir}/test/utils
source $(pwd)/test/utils
mktmpdir() {
dir=$(mktemp -t testXXXXX)
......@@ -459,19 +420,35 @@ mktmpdir() {
}
detect() {
capture ${bp_dir}/bin/detect ${bp_dir}/test/fixtures/$1
capture $(pwd)/bin/detect $(pwd)/test/fixtures/$1
}
compile_dir=""
default_process_types_cleanup() {
file="/tmp/default_process_types"
if [ -f "$file" ]; then
rm "$file"
fi
}
compile() {
default_process_types_cleanup
bp_dir=$(mktmpdir)
compile_dir=$(mktmpdir)
cp -r ${bp_dir}/test/fixtures/$1/. ${compile_dir}
cp -a $(pwd)/* ${bp_dir}
cp -a ${bp_dir}/test/fixtures/$1/. ${compile_dir}
capture ${bp_dir}/bin/compile ${compile_dir} ${2:-$(mktmpdir)} $3
}
release() {
bp_dir=$(mktmpdir)
cp -a $(pwd)/* ${bp_dir}
capture ${bp_dir}/bin/release ${bp_dir}/test/fixtures/$1
}
assertFile() {
assertEquals "$1" "$(cat ${compile_dir}/$2)"
}
source ${bp_dir}/test/shunit2
source $(pwd)/test/shunit2
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment