Skip to content

Instantly share code, notes, and snippets.

@hunterloftis
Created December 3, 2014 23:31
Show Gist options
  • Save hunterloftis/c19c833e5cc105b4f5fb to your computer and use it in GitHub Desktop.
Save hunterloftis/c19c833e5cc105b4f5fb to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
####### Configure environment
set -e # fail fast
set -o pipefail # don't ignore exit codes when piping output
# 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
# Load some convenience functions like status(), echo(), and indent()
source $bp_dir/bin/common.sh
# Avoid GIT_DIR leak from previous build steps
status "Resetting git environment"
unset GIT_DIR
# Load config vars into environment
if [ -d "$env_dir" ]; then
status "Exporting config vars to environment"
export_env_dir $env_dir
fi
####### Determine current state
# What was the last released node version?
if test -f $cache_dir/node/node-version; then
previous_node_version=$(cat $cache_dir/node/node-version)
else
previous_node_version=""
fi
# Is there a requested semver range for node?
semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .engines.node)
node_version=""
if [ "$semver_range" == "null" ]; then
semver_range=""
has_semver=false
has_specific_version=false
else
has_semver=true
if [[ "semver_range" =~ "^\d+\.\d+\.\d+$" ]]; then
has_specific_version=true
node_version=semver_range
else
has_specific_version=false
fi
fi
# Start by assuming we can't start
start_method=""
# Does server.js exist?
if [ -f $build_dir/server.js ]; then
has_server=true
start_method="server.js"
else
has_server=false
fi
# Can we use npm start (default = node server.js)?
npm_start=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .scripts.start)
if [ "$npm_start" != "null" ]; then
has_start=true
start_method="scripts.start"
else
has_start=false
fi
# Is a Procfile provided?
if test -d $build_dir/Procfile; then
has_procfile=true
start_method="Procfile"
else
has_procfile=false
fi
# By default, we'll build node_modules from package.json
node_modules_source="package.json"
# Do we have cached node_modules?
elif test -d $cache_dir/node/node_modules; then
node_modules_cached=true
else
node_modules_cached=false
fi
# Does node_modules dir already exist?
if test -d $build_dir/node_modules; then
node_modules_source="prebuilt"
fi
####### Provide debugging info and feedback
status "Node Buildpack v63"
status "Previous node version: ${previous_node_version:none}" | indent
status "Requested node version: ${semver_range:missing}" | indent
status "App startup method: $start_method" | indent
status "node_modules source: $node_modules_source" | indent
status "Have node_modules cache: $node_modules_cached" | indent
# Abort of starting is impossible
if [ "$start_method" == "" ]; then
status "Unable to start app: you can fix this by creating any of the following:"
status "- A Procfile" | indent
status "- A start script in package.json" | indent
status "- server.js in your project's root" | indent
error "Can't start"
fi
# Warn about dangerous engines.node ranges
if [ "$semver_range" == "" ]; then
protip "You should specify a node version in package.json - will query for latest stable."
elif [ "$semver_range" == "*" ]; then
protip "Avoid using semver ranges like '*' in engines.node"
elif [ ${semver_range:0:1} == ">" ]; then
protip "Avoid using semver ranges starting with '>' in engines.node"
fi
# Warn about checked-in node_modules
if [ "$node_modules_source" == "prebuilt" ]; then
protip "Avoid checking node_modules into source control (see NPM FAQ)"
fi
####### Vendor in binaries
# Resolve non-specific node versions using semver.io
if [ ! $has_specific_version ]; then
status "Resolving node version $semver_range with semver.io..."
node_version=$(curl --silent --get --data-urlencode "range=${semver_range}" https://semver.io/node/resolve)
fi
# Is this a new version of node?
if [ "$previous_node_version" != "" ] && [ "$node_version" != "$previous_node_version" ]; then
new_node_version=true
else
new_node_version=false
fi
# Create home for node
mkdir -p $heroku_dir/node
mkdir -p $heroku_dir/temp
# Download node from Heroku's S3 mirror of nodejs.org/dist
status "Downloading and installing node $node_version..."
node_url="http://s3pository.heroku.com/node/v$node_version/node-v$node_version-linux-x64.tar.gz"
curl $node_url -s -o - | tar xzf - -C $heroku_dir/temp
# Move node (and npm) into .heroku/node and make them executable
mv $temp_dir/node-v$node_version-linux-x64 $heroku_dir/node
chmod +x $heroku_dir/node/bin/*
PATH=$heroku_dir/node/bin:$PATH
# Run subsequent commands from the build directory
cd $build_dir
####### Build the project's dependencies
if [ "$node_modules_source" == "prebuilt" ]; then
status "Found existing node_modules directory; skipping cache and install"
status "Rebuilding any native dependencies"
npm rebuild 2>&1 | indent
else
if $new_node_version; then
status "Changing node versions, $previous_node_version => $node_version; skipping cache"
npm rebuild 2>&1 | indent
elif $node_modules_cached then
status "Restoring node_modules directory from cache"
cp -r $cache_dir/node/node_modules $build_dir/
status "Pruning cached dependencies not specified in package.json"
npm prune 2>&1 | indent
fi
status "Installing dependencies"
npm install --userconfig $build_dir/.npmrc 2>&1 | indent # Make npm output to STDOUT instead of its default STDERR
status "Updating dependencies"
npm update
status "Deduping dependencies"
npm dedupe
fi
####### Ensure a Procfile
if [ ! $has_procfile ]; then
status "No Procfile found; Adding 'web: npm start' to new Procfile"
echo "web: npm start" > $build_dir/Procfile
fi
####### Create the runtime environment (profile.d)
status "Building runtime environment"
mkdir -p $build_dir/.profile.d
# Add binary locations to the PATH
echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\";" > $build_dir/.profile.d/nodejs.sh
# Default to NODE_ENV=production
echo "export NODE_ENV=${NODE_ENV:production}" >> $build_dir/.profile.d/nodejs.sh
####### Clean up
status "Cleaning up build artifacts"
# Clear temporary files
rm -rf $heroku_dir/temp
# Pick up after npm
rm -rf "$build_dir/.node-gyp"
rm -rf "$build_dir/.npm"
# Clear and recreate the cache
rm -rf $cache_dir/node
mkdir -p $cache_dir/node
####### Build successful! Store results in cache
echo $node_version > $cache_dir/node/node-version
if test -d $build_dir/node_modules; then
status "Caching node_modules directory for future builds"
cp -r $build_dir/node_modules $cache_dir/node
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment