Fetching contributors…
Cannot retrieve contributors at this time
executable file 202 lines (166 sloc) 6.67 KB
#!/usr/bin/env bash
set -e # fail fast
set -o pipefail # don't ignore exit codes when piping output
# set -x # enable debugging
# Configure directories
bp_dir=$(cd $(dirname $0); cd ..; pwd)
# Load some convenience functions like status(), echo(), and indent()
source $bp_dir/bin/
# Output npm debug info on error
trap cat_npm_debug_log ERR
# Look in package.json's engines.node field for a semver range
semver_range=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .engines.node)
# Resolve node version using
node_version=$(curl --silent --get --data-urlencode "range=${semver_range}"
# Recommend using semver ranges in a safe manner
if [ "$semver_range" == "null" ]; then
protip "Specify a node version in package.json"
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"
# Output info about requested range and resolved node version
if [ "$semver_range" == "" ]; then
status "Defaulting to latest stable node: $node_version"
status "Requested node range: $semver_range"
status "Resolved node version: $node_version"
# Download node from Heroku's S3 mirror of
status "Downloading and installing node"
curl $node_url -s -o - | tar xzf - -C $build_dir
# Move node (and npm) into ./vendor and make them executable
mkdir -p $build_dir/vendor
mv $build_dir/node-v$node_version-linux-x64 $build_dir/vendor/node
chmod +x $build_dir/vendor/node/bin/*
# Run subsequent node/npm commands from the build path
cd $build_dir
is_gruntfile_present= [ -f $build_dir/grunt.js ] || [ -f $build_dir/Gruntfile.js ] || [ -f $build_dir/ ];
if $is_gruntfile_present; then
status "Found Gruntfile"
status "Augmenting package.json with grunt and grunt-cli"
# Do this because we want to cache it with other node_modules. Otherwise it will be pruned.
# It doesn't matter if grunt and/or grunt-cli is already there
awk '{ if ( $0 ~ /"dependencies".*/ ) {
printf "%s\n%s\n%s\n", $0, "\"grunt-cli\": \"*\",", "\"grunt\": \"*\",";
} else {
print $0;
}' package.json > tmp-package.json;
cp tmp-package.json package.json;
rm tmp-package.json;
status "No Gruntfile (grunt.js, Gruntfile.js, found"
# If node_modules directory is checked into source control then
# rebuild any native deps. Otherwise, restore from the build cache.
if test -d $build_dir/node_modules; then
status "Found existing node_modules directory; skipping cache"
status "Rebuilding any native dependencies"
npm rebuild 2>&1 | indent
elif test -d $cache_dir/node/node_modules; 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
if test -f $cache_dir/node/.heroku/node-version && [ $(cat $cache_dir/node/.heroku/node-version) != "$node_version" ]; then
status "Node version changed since last build; rebuilding dependencies"
npm rebuild 2>&1 | indent
# Scope config var availability only to `npm install`
if [ -d "$env_dir" ]; then
status "Exporting config vars to environment"
export_env_dir $env_dir
status "Installing dependencies"
# Make npm output to STDOUT instead of its default STDERR
npm install --userconfig $build_dir/.npmrc --production 2>&1 | indent
# Persist goodies like node-version in the slug
mkdir -p $build_dir/.heroku
# Save resolved node version in the slug for later reference
echo $node_version > $build_dir/.heroku/node-version
# Purge node-related cached content, being careful not to purge the top-level
# cache, for the sake of heroku-buildpack-multi apps.
rm -rf $cache_dir/node_modules # (for apps still on the older caching strategy)
rm -rf $cache_dir/node
mkdir -p $cache_dir/node
# If app has a node_modules directory, cache it.
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
# Copy goodies to the cache
cp -r $build_dir/.heroku $cache_dir/node
status "Cleaning up node-gyp and npm artifacts"
rm -rf "$build_dir/.node-gyp"
rm -rf "$build_dir/.npm"
# If Procfile is absent, try to create one using `npm start`
if [ ! -e $build_dir/Procfile ]; then
npm_start=$(cat $build_dir/package.json | $bp_dir/vendor/jq -r .scripts.start)
# If `scripts.start` is set in package.json, or a server.js file
# is present in the app root, then create a default Procfile
if [ "$npm_start" != "null" ] || [ -f $build_dir/server.js ]; then
status "No Procfile found; Adding npm start to new Procfile"
echo "web: npm start" > $build_dir/Procfile
status "Procfile not found and npm start script is undefined"
protip "Create a Procfile or specify a start script in package.json"
# install compass
status "Installing Compass"
export GEM_HOME=$build_dir/.gem/ruby/1.9.1
if test -d $cache_dir/ruby/.gem; then
status "Restoring ruby gems directory from cache"
cp -r $cache_dir/ruby/.gem $build_dir
HOME=$build_dir gem update compass --user-install --no-rdoc --no-ri
HOME=$build_dir gem install compass --user-install --no-rdoc --no-ri
# cache ruby gems compass
rm -rf $cache_dir/ruby
mkdir -p $cache_dir/ruby
# If app has a gems directory, cache it.
if test -d $build_dir/.gem; then
status "Caching ruby gems directory for future builds"
cp -r $build_dir/.gem $cache_dir/ruby
status "Building runtime environment"
mkdir -p $build_dir/.profile.d
echo "export PATH=\"\$HOME/vendor/node/bin:\$HOME/bin:\$HOME/node_modules/.bin:\$PATH\";" > $build_dir/.profile.d/
echo "export PATH=\"\$HOME/.gem/ruby/1.9.1/bin:\$PATH\"" > $build_dir/.profile.d/
# Check and run Grunt
if $is_gruntfile_present; then
# get the env vars
if [ -d "$env_dir" ]; then
status "Exporting config vars to environment"
export_env_dir $env_dir
status "Running grunt heroku:$NODE_ENV task"
$build_dir/node_modules/.bin/grunt heroku:$NODE_ENV
# Post package.json to nomnom service
# Use a subshell so failures won't break the build.
curl \
--data @$build_dir/package.json \
--fail \
--silent \
--request POST \
--header "content-type: application/json" \$REQUEST_ID \
> /dev/null
) &