Skip to content

Instantly share code, notes, and snippets.

@double-z
Created June 24, 2017 17:36
Show Gist options
  • Save double-z/cef86808d9b76036af2cb9fb93a679d5 to your computer and use it in GitHub Desktop.
Save double-z/cef86808d9b76036af2cb9fb93a679d5 to your computer and use it in GitHub Desktop.
Experiments to deal with node/npm/react on rails with habitat scaffolding
pkg_name=sharetribe
pkg_origin=zz.
pkg_version="0.1.6"
pkg_deps=(core/node/7.8.0)
pkg_build_deps=(core/node/7.8.0 core/mysql-client core/tzdata)
pkg_scaffolding=zz/scaffolding-ruby-react
# scaffolding_env[SECRET_KEY_BASE]=('4454a5818096fc29cebdb5ad03e619fdd80be79c3d6041ec9ea00b36b00a246143ff391012cf4d54b29377a17dba96d00c19e9d8ab9e3411c57a74607031896b')
# scaffolding_node_pkg_manager=npm
do_prepare() {
# attach
cp -rfpv . "$CACHE_PATH/"
# if [[ -d "client" ]]; then
# build_line "COPYING from $(pwd) to $CACHE_PATH"
# cp -rfv package.json "$CACHE_PATH/"
# cp -rfv client "$CACHE_PATH/"
# fi
do_default_prepare
return $?
}
# shellcheck shell=bash disable=SC2086
scaffolding_load() {
_setup_funcs
_setup_vars
pushd "$SRC_PATH" > /dev/null
: ${node_enabled=:}
_detect_package_json
rc=$?
if [[ $rc -eq 0 ]]; then
export node_enabled="true"
_detect_node
_detect_node_pkg_manager
fi
_detect_gemfile
_detect_app_type
_detect_missing_gems
_detect_process_bins
_update_vars
_update_pkg_build_deps
_update_pkg_deps
_update_bin_dirs
_update_svc_run
popd > /dev/null
}
do_ruby_prepare() {
local gem_dir gem_path
# Determine Ruby engine, ABI version, and Gem path by running `ruby` itself.
eval "$(ruby -rubygems -rrbconfig - <<-'EOF'
puts "local ruby_engine=#{defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'}"
puts "local ruby_version=#{RbConfig::CONFIG['ruby_version']}"
puts "local gem_path='#{Gem.path.join(':')}'"
EOF
)"
# Strip out any home directory entries at the front of the gem path.
# shellcheck disable=SC2001
gem_path=$(echo "$gem_path" | sed 's|^/root/\.gem/[^:]\{1,\}:||')
# Compute gem directory where gems will be ultimately installed to
gem_dir="$scaffolding_app_prefix/vendor/bundle/$ruby_engine/$ruby_version"
# Compute gem directory where gems are initially installed to via Bundler
_cache_gem_dir="$CACHE_PATH/vendor/bundle/$ruby_engine/$ruby_version"
# Silence Bundler warning when run as root user
export BUNDLE_SILENCE_ROOT_WARNING=1
GEM_HOME="$gem_dir"
build_line "Setting GEM_HOME=$GEM_HOME"
GEM_PATH="$gem_dir:$gem_path"
build_line "Setting GEM_PATH=$GEM_PATH"
export GEM_HOME GEM_PATH
}
do_node_prepare() {
export NODE_ENV=production
npm_dir="$scaffolding_app_prefix/vendor/bundle/$ruby_engine/$ruby_version"
npm_client_dir="$CACHE_PATH"
npm_install="$CACHE_PATH/client"
npm_client_install="$scaffolding_app_prefix/client"
build_line "Setting NODE_ENV=$NODE_ENV"
}
do_default_prepare() {
do_ruby_prepare
do_node_prepare
}
do_default_build() {
# TODO fin: add cache loading of `$CACHE_PATH/vendor`
# TODO fin: add cache loading of `$CACHE_PATH/node_modules`
# TODO fin: support prebuild step?
scaffolding_bundle_install
# build_line "Cleaning old cached gems"
# _bundle clean --dry-run --force
# TODO fin: add cache saving of `$CACHE_PATH/vendor`
scaffolding_remove_gem_cache
scaffolding_fix_rubygems_shebangs
if [[ $node_enabled = "true" ]]; then
scaffolding_npm_install
scaffolding_fix_node_shebangs
scaffolding_fix_node_client_shebangs
fi
scaffolding_setup_app_config
scaffolding_setup_node_app_config
scaffolding_setup_database_config
# TODO fin: support postbuild step?
# TODO fin: add cache saving of `$CACHE_PATH/node_modules`
}
do_default_install() {
if [[ $node_enabled = "true" ]]; then
scaffolding_install_node_modules
scaffolding_install_node_client
fi
scaffolding_install_app
scaffolding_install_gems
# Do Node npm install
scaffolding_generate_binstubs
scaffolding_vendor_bundler
scaffolding_install_libexec
scaffolding_fix_binstub_shebangs
scaffolding_log_process_bins
attach
scaffolding_run_assets_precompile
scaffolding_create_dir_symlinks
scaffolding_create_files_symlinks
scaffolding_create_process_bins
}
# do_node_install() {
# # scaffolding_install_app
# scaffolding_install_node_modules
# scaffolding_create_process_bins
# }
# do_default_install() {
# do_ruby_install
# do_node_install
# }
# This becomes the `do_default_build_config` implementation thanks to some
# function "renaming" above. I know, right?
_new_do_default_build_config() {
local key dir env_sh
_stock_do_default_build_config
if [[ ! -f "$PLAN_CONTEXT/hooks/init" ]]; then
build_line "No user-defined init hook found, generating init hook"
mkdir -p "$pkg_prefix/hooks"
cat <<EOT >> "$pkg_prefix/hooks/init"
#!/bin/sh
set -e
export HOME="$pkg_svc_data_path"
. '$pkg_svc_config_path/app_env.sh'
# Create a directory for each app symlinked dir under $pkg_svc_var_path
$(
for dir in "${scaffolding_symlinked_dirs[@]}"; do
echo "mkdir -pv '$pkg_svc_var_path/$dir'"
done
)
$(
case "$_app_type" in
(rails5|rails42|rails41)
cat <<_RAILS_
# Check that the 'SECRET_KEY_BASE' environment variable is non-empty
if [ -z "\${SECRET_KEY_BASE:-}" ]; then
>&2 echo "Required environment variable SECRET_KEY_BASE is not set."
>&2 echo "Set this package's config setting 'secret_key_base' to a"
>&2 echo "non-empty value. You can run 'rails secret' in development"
>&2 echo "to generate a random key string."
>&2 echo ""
>&2 echo "Aborting..."
exit 5
fi
_RAILS_
;;
esac
if [[ "${_uses_pg:-}" == "true" \
|| "${_uses_mysql:-}" == "true" ]]; then
cat <<_PG_
# Confirm an initial database connection
if ! $pkg_prefix/libexec/is_db_connected; then
>&2 echo ""
>&2 echo "A database connection is required for this app to properly boot."
>&2 echo "Is the database not running or are the database connection"
>&2 echo "credentials incorrect?"
>&2 echo ""
{{~#if bind.database}}
>&2 echo "This app started with a database bind and will discovery the"
>&2 echo "hostname and port number in the Habitat ring."
>&2 echo ""
>&2 echo "There are 3 remaining config settings which must be set correctly:"
{{else}}
>&2 echo "This app started without a database bind meaning that the"
>&2 echo "database is assumed to be running outside of a Habitat ring."
>&2 echo "Therefore, you must provide all the database connection values."
>&2 echo ""
>&2 echo "There are 5 config settings which must be set correctly:"
{{~/if}}
>&2 echo ""
{{~#unless bind.database}}
>&2 echo " * db.host - The database hostname or IP address (Current: {{#if cfg.db.host}}{{cfg.db.host}}{{else}}<unset>{{/if}})"
>&2 echo " * db.port - The database listen port number (Current: {{#if cfg.db.port}}{{cfg.db.port}}{{else}}5432{{/if}})"
{{~/unless}}
>&2 echo " * db.user - The database username (Current: {{#if cfg.db.user}}{{cfg.db.user}}{{else}}<unset>{{/if}})"
>&2 echo " * db.password - The database password (Current: {{#if cfg.db.password}}<set>{{else}}<unset>{{/if}})"
>&2 echo " * db.name - The database name (Current: {{#if cfg.db.name}}{{cfg.db.name}}{{else}}<unset>{{/if}})"
>&2 echo ""
>&2 echo "Aborting..."
exit 15
fi
_PG_
fi
)
EOT
chmod 755 "$pkg_prefix/hooks/init"
fi
if [[ -f "$CACHE_PATH/default.scaffolding.toml" ]]; then
build_line "Appending Scaffolding defaults to $pkg_prefix/default.toml"
cat "$CACHE_PATH/default.scaffolding.toml" >> "$pkg_prefix/default.toml"
fi
env_sh="$pkg_prefix/config/app_env.sh"
mkdir -p "$(dirname "$env_sh")"
for key in "${!scaffolding_env[@]}"; do
echo "export $key='${scaffolding_env[$key]}'" >> "$env_sh"
done
}
#
# Ruby
#
scaffolding_bundle_install() {
local start_sec elapsed dot_bundle
# Attempt to preserve any original Bundler config by moving it to the side
if [[ -f .bundle/config ]]; then
mv .bundle/config .bundle/config.prehab
dot_bundle=true
elif [[ -d .bundle ]]; then
dot_bundle=true
fi
build_line "Installing dependencies using $(_bundle --version)"
start_sec="$SECONDS"
_bundle_install \
"$CACHE_PATH/vendor/bundle" \
--retry 5
elapsed=$((SECONDS - start_sec))
elapsed=$(echo $elapsed | awk '{printf "%dm%ds", $1/60, $1%60}')
build_line "Bundle completed ($elapsed)"
# If we preserved the original Bundler config, move it back into place
if [[ -f .bundle/config.prehab ]]; then
rm -f .bundle/config
mv .bundle/config.prehab .bundle/config
rm -f .bundle/config.prehab
fi
# If not `.bundle/` directory existed before, then clear it out now
if [[ -z "${dot_bundle:-}" ]]; then
rm -rf .bundle
fi
}
scaffolding_npm_install() {
if [[ -n "${_uses_git:-}" ]]; then
if ! git check-ignore node_modules && [[ -d node_modules ]]; then
warn "Detected directory 'node_modules' is not in .gitignore and is"
warn "not empty. The contents of 'node_modules' will not be used when"
warn "building this app."
warn "It is not recommended to commit your node modules into your"
warn "codebase."
fi
fi
build_line "Installing dependencies using $node_pkg_manager $("$node_pkg_manager" --version)"
start_sec="$SECONDS"
case "$node_pkg_manager" in
npm)
if [[ ! -f "$CACHE_PATH/package.json" ]]; then
cp -av package.json "$CACHE_PATH/"
fi
if [[ -f "npm-shrinkwrap.json" ]]; then
cp -av npm-shrinkwrap.json "$CACHE_PATH/"
fi
if [[ -n "$HAB_NONINTERACTIVE" ]]; then
export NPM_CONFIG_PROGRESS=false
fi
pushd "$CACHE_PATH" > /dev/null
npm install \
--unsafe-perm \
--production \
--loglevel error \
--fetch-retries 5 \
--userconfig "$CACHE_PATH/.npmrc"
npm list --json > npm-list.json
popd > /dev/null
;;
yarn)
local extra_args
if [[ -n "$HAB_NONINTERACTIVE" ]]; then
extra_args="--no-progress"
fi
yarn install $extra_args \
--pure-lockfile \
--ignore-engines \
--production \
--modules-folder "$CACHE_PATH/node_modules" \
--cache-folder "$CACHE_PATH/yarn_cache"
;;
*)
local e
e="Internal error: package manager variable"
e="$e not correctly set: '$node_pkg_manager'"
exit_with "$e" 9
;;
esac
elapsed=$((SECONDS - start_sec))
elapsed=$(echo $elapsed | awk '{printf "%dm%ds", $1/60, $1%60}')
build_line "Dependency installation completed ($elapsed)"
return 0
}
scaffolding_install_node_modules() {
build_line "Installing vendored Node modules to $scaffolding_app_prefix/node_modules"
rm -rf "$scaffolding_app_prefix/node_modules"
cp -a "$CACHE_PATH/node_modules" "$scaffolding_app_prefix/"
if [[ -f "$CACHE_PATH/npm-list.json" \
&& ! -f "$scaffolding_app_prefix/npm-list.json" ]]; then
cp -av "$CACHE_PATH/npm-list.json" "$scaffolding_app_prefix/"
fi
}
scaffolding_install_node_client() {
build_line "Installing vendored Node modules to $scaffolding_app_prefix/client/node_modules"
if [[ -d "$scaffolding_app_prefix/client" ]]; then
rm -rfv "$scaffolding_app_prefix/client"
fi
cp -a "$CACHE_PATH/client" "$scaffolding_app_prefix/"
}
scaffolding_remove_gem_cache() {
build_line "Removing installed gem cache"
rm -rf "$_cache_gem_dir/cache"
}
scaffolding_fix_rubygems_shebangs() {
local shebang
shebang="#!$(pkg_path_for "$_ruby_pkg")/bin/ruby"
build_line "Fixing Ruby shebang for RubyGems bins"
find "$_cache_gem_dir/bin" -type f | while read -r bin; do
sed -e "s|^#!.\{0,\}\$|${shebang}|" -i "$bin"
done
}
scaffolding_fix_node_shebangs() {
local shebang bin_path
shebang="#!$(pkg_path_for "$_node_pkg")/bin/node"
bin_path="$CACHE_PATH/node_modules/.bin"
build_line "Fixing Node shebang for node_module bins"
if [[ -d "$bin_path" ]]; then
find "$bin_path" -type f -o -type l | while read -r bin; do
sed -e "s|^#!.\{0,\}\$|${shebang}|" -i "$(readlink -f "$bin")"
done
fi
}
scaffolding_fix_node_client_shebangs() {
local shebang bin_path
shebang="#!$(pkg_path_for "$_node_pkg")/bin/node"
bin_path="$CACHE_PATH/client/node_modules/.bin"
build_line "Fixing Node shebang for client node_module bins"
if [[ -d "$bin_path" ]]; then
find "$bin_path" -type f -o -type l | while read -r bin; do
sed -e "s|^#!.\{0,\}\$|${shebang}|" -i "$(readlink -f "$bin")"
done
fi
}
scaffolding_setup_app_config() {
local t
t="$CACHE_PATH/default.scaffolding.toml"
echo "" >> "$t"
if _default_toml_has_no secret_key_base \
&& [[ -v "scaffolding_env[SECRET_KEY_BASE]" ]]; then
{ echo "# Rails' secret key base is required and must be non-empty"
echo "# You can run 'rails secret' in development to generate"
echo "# a random key string."
echo 'secret_key_base = ""'
echo ""
} >> "$t"
fi
if _default_toml_has_no lang; then
echo 'lang = "en_US.UTF-8"' >> "$t"
fi
if _default_toml_has_no rack_env \
&& [[ -v "scaffolding_env[RACK_ENV]" ]]; then
echo 'rack_env = "production"' >> "$t"
fi
if _default_toml_has_no rails_env \
&& [[ -v "scaffolding_env[RAILS_ENV]" ]]; then
echo 'rails_env = "production"' >> "$t"
fi
if _default_toml_has_no app; then
echo "" >> "$t"
echo '[app]' >> "$t"
if _default_toml_has_no app.port; then
echo "port = $scaffolding_app_port" >> "$t"
fi
fi
}
scaffolding_setup_node_app_config() {
local t
t="$CACHE_PATH/default.scaffolding.toml"
if _default_toml_has_no node_env \
&& [[ -v "scaffolding_env[NODE_ENV]" ]]; then
echo 'node_env = "production"' >> "$t"
fi
}
scaffolding_setup_database_config() {
local _adapter
if [[ "${_uses_pg:-}" == "true" ]]; then
_adapter="postgres"
elif [[ "${_uses_mysql:-}" == "true" ]]; then
_adapter="mysql2"
fi
if [[ -n "${_adapter:-}" ]]; then
local db t
db="${_adapter}://{{cfg.db.user}}:{{cfg.db.password}}"
db="${db}@{{#if bind.database}}{{bind.database.first.sys.ip}}{{else}}{{#if cfg.db.host}}{{cfg.db.host}}{{else}}db.host.not.set{{/if}}{{/if}}"
db="${db}:{{#if bind.database}}{{bind.database.first.cfg.port}}{{else}}{{#if cfg.db.port}}{{cfg.db.port}}{{else}}5432{{/if}}{{/if}}"
db="${db}/{{cfg.db.name}}"
_set_if_unset scaffolding_env DATABASE_URL "$db"
# Add an optional binding called `database` which will be the PostgreSQL
# database
_set_if_unset pkg_binds_optional database "port"
t="$CACHE_PATH/default.scaffolding.toml"
if _default_toml_has_no db; then
{ echo ""
echo "[db]"
} >> "$t"
if _default_toml_has_no db.name; then
echo "name = \"${pkg_name}_production\"" >> "$t"
fi
if _default_toml_has_no db.user; then
echo "user = \"${pkg_name}\"" >> "$t"
fi
if _default_toml_has_no db.password; then
echo "password = \"${pkg_name}\"" >> "$t"
fi
fi
fi
}
##
# Begin Install Functions
scaffolding_install_app() {
build_line "Installing app codebase to $scaffolding_app_prefix"
mkdir -pv "$scaffolding_app_prefix"
if [[ -n "${_uses_git:-}" ]]; then
# Use git commands to skip any git-ignored files and directories including
# the `.git/ directory. Current on-disk state of all files is used meaning
# that dirty and unstaged files are included which should help while
# working on package builds.
build_line "Using Git to install app from $(pwd)"
{ git ls-files; git ls-files --exclude-standard --others; } \
| _tar_pipe_app_cp_to "$scaffolding_app_prefix"
else
# Use find to enumerate all files and directories for copying. This is the
# safe-fallback strategy if no version control software is detected.
build_line "NOT Using Git to install app from $(pwd)"
find . | _tar_pipe_app_cp_to "$scaffolding_app_prefix"
fi
}
scaffolding_install_gems() {
mkdir -pv "$scaffolding_app_prefix/vendor"
build_line "Installing vendored gems to $scaffolding_app_prefix/vendor/bundle"
cp -a "$CACHE_PATH/vendor/bundle" "$scaffolding_app_prefix/vendor/"
}
scaffolding_generate_binstubs() {
build_line "Generating app binstubs in $scaffolding_app_prefix/binstubs"
rm -rf "$scaffolding_app_prefix/.bundle"
pushd "$scaffolding_app_prefix" > /dev/null
_bundle_install \
"$scaffolding_app_prefix/vendor/bundle" \
--local \
--quiet \
--binstubs="$scaffolding_app_prefix/binstubs"
popd > /dev/null
}
scaffolding_vendor_bundler() {
build_line "Vendoring $(_bundle --version)"
gem install \
--local "$(pkg_path_for bundler)/cache/bundler-${_bundler_version}.gem" \
--install-dir "$GEM_HOME" \
--bindir "$scaffolding_app_prefix/binstubs" \
--no-ri \
--no-rdoc
_wrap_ruby_bin "$scaffolding_app_prefix/binstubs/bundle"
_wrap_ruby_bin "$scaffolding_app_prefix/binstubs/bundler"
}
scaffolding_install_libexec() {
local shebang src dst
shebang="#!$(pkg_path_for "$_ruby_pkg")/bin/ruby"
build_line "Installing support programs into $pkg_prefix/libexec"
mkdir -pv "$pkg_prefix/libexec"
# TODO fin: Well, that's super awkward to find my own scaffolding path,
# perhaps the build program should set a `*_PATH` variable before invoking
# the scaffolding so there's an absolute path reference back?
find "$(pkg_path_for scaffolding-ruby-react)/libexec" -type f | while read -r src; do
dst="$pkg_prefix/libexec/$(basename "$src")"
cp -av "$src" "$dst"
sed -e "s|^#!/usr/bin/env .\{0,\}\$|${shebang}|" -i "$dst"
_create_process_bin "${dst%.*}" "bundle exec $dst"
done
}
scaffolding_fix_binstub_shebangs() {
local shebang
shebang="#!$(pkg_path_for "$_ruby_pkg")/bin/ruby"
build_line "Fixing Ruby shebang for binstubs"
find "$scaffolding_app_prefix/binstubs" -type f | while read -r binstub; do
if grep -q '^#!/usr/bin/env /.*/bin/ruby$' "$binstub"; then
sed -e "s|^#!/usr/bin/env /.\{0,\}/bin/ruby\$|${shebang}|" -i "$binstub"
fi
done
}
scaffolding_run_assets_precompile() {
# TODO fin: early exit if existing assets are found, meaning they've been
# committed or at least not ignored.
if _has_gem rake && _has_rakefile; then
pushd "$scaffolding_app_prefix" > /dev/null
if _rake -P --trace | grep -q '^rake assets:precompile$'; then
build_line "Detected and running Rake 'assets:precompile'"
_rake assets:precompile
fi
popd > /dev/null
fi
}
scaffolding_create_dir_symlinks() {
local entry dir target
for entry in "${scaffolding_symlinked_dirs[@]}"; do
dir="$scaffolding_app_prefix/$entry"
target="$pkg_svc_var_path/$entry"
build_line "Creating directory symlink to '$target' for '$dir'"
rm -rf "$dir"
mkdir -p "$(dirname "$dir")"
ln -sfv "$target" "$dir"
done
}
scaffolding_create_files_symlinks() {
return 0
}
scaffolding_create_process_bins() {
local bin cmd
for bin in "${!scaffolding_process_bins[@]}"; do
cmd="${scaffolding_process_bins[$bin]}"
_create_process_bin "$pkg_prefix/bin/${pkg_name}-${bin}" "$cmd"
done
}
scaffolding_log_process_bins() {
local bin cmd
for bin in "${!scaffolding_process_bins[@]}"; do
cmd="${scaffolding_process_bins[$bin]}"
build_line "Proccess_bin: $cmd"
done
}
_setup_funcs() {
# Use the stock `do_default_build_config` by renaming it so we can call the
# stock behavior. How does this rate on the evil scale?
_rename_function "do_default_build_config" "_stock_do_default_build_config"
_rename_function "_new_do_default_build_config" "do_default_build_config"
}
_setup_vars() {
# The default Ruby package if one cannot be detected
_default_ruby_pkg="core/ruby"
# The absolute path to the `gemfile-parser` program
_gemfile_parser="$(pkg_path_for scaffolding-ruby-react)/bin/gemfile-parser"
# `$scaffolding_ruby_pkg` is empty by default
: "${scaffolding_ruby_pkg:=}"
# The list of PostgreSQL-related gems
_pg_gems=(pg activerecord-jdbcpostgresql-adapter jdbc-postgres
jdbc-postgresql jruby-pg rjack-jdbc-postgres
tgbyte-activerecord-jdbcpostgresql-adapter)
_mysql_gems=(mysql mysql2)
# The version of Bundler in use
_bundler_version="$("$(pkg_path_for bundler)/bin/bundle" --version \
| awk '{print $NF}')"
# The install prefix path for the app
scaffolding_app_prefix="$pkg_prefix/app"
#
: "${scaffolding_app_port:=8000}"
# If `${scaffolding_env[@]` is not yet set, setup the hash
if [[ ! "$(declare -p scaffolding_env 2> /dev/null || true)" =~ "declare -A" ]]; then
declare -g -A scaffolding_env
fi
# If `${scaffolding_process_bins[@]` is not yet set, setup the hash
if [[ ! "$(declare -p scaffolding_process_bins 2> /dev/null || true)" =~ "declare -A" ]]; then
declare -g -A scaffolding_process_bins
fi
#
if [[ ! "$(declare -p scaffolding_symlinked_dirs 2> /dev/null || true)" =~ "declare -a" ]]; then
declare -g -a scaffolding_symlinked_dirs
fi
#
if [[ ! "$(declare -p scaffolding_symlinked_files 2> /dev/null || true)" =~ "declare -a" ]]; then
declare -g -a scaffolding_symlinked_files
fi
#
: "${_app_type:=}"
# # Node
# The default Node package if one cannot be detected
_default_node_pkg="core/node"
# `$scaffolding_node_pkg` is empty by default
: "${scaffolding_node_pkg:=}"
# `$scaffolding_node_pkg` is empty by default
: "${scaffolding_node_pkg_manager:=}"
_jq="$(pkg_path_for jq-static)/bin/jq"
}
_detect_package_json() {
if [[ ! -f package.json ]]; then
exit_with \
"Node Scaffolding cannot find package.json in the root directory." 5
fi
# shellcheck disable=SC2002
if ! cat package.json | "$_jq" . > /dev/null; then
exit_with "Failed to parse package.json as JSON." 6
fi
# TODO fin: do we check for any lockfiles here?
}
_detect_node_pkg_manager() {
if [[ -n "$scaffolding_node_pkg_manager" ]]; then
case "$scaffolding_node_pkg_manager" in
npm)
node_pkg_manager=npm
build_line "Detected package manager in Plan, using '$node_pkg_manager'"
;;
yarn)
node_pkg_manager=yarn
build_line "Detected package manager in Plan, using '$node_pkg_manager'"
;;
*)
local e
e="Variable 'scaffolding_pkg_manager' can only be"
e="$e set to: 'npm', 'yarn', or empty."
exit_with "$e" 9
;;
esac
elif [[ -f yarn.lock ]]; then
node_pkg_manager=yarn
build_line "Detected yarn.lock in root directory, using '$node_pkg_manager'"
else
node_pkg_manager=npm
build_line "No package manager could be detected, using default '$node_pkg_manager'"
fi
}
_detect_gemfile() {
if [[ ! -f Gemfile ]]; then
exit_with "Ruby Scaffolding cannot find Gemfile in the root directory" 5
fi
if [[ ! -f Gemfile.lock ]]; then
local uid gid
build_line "No Gemfile.lock found, running 'bundle lock'"
"$(pkg_path_for bundler)/bin/bundle" lock
# Set ownership of `Gemfile.lock` to be the same as `Gemfile`.
uid="$(stat -c "%u" Gemfile)"
gid="$(stat -c "%g" Gemfile)"
chown -v "${uid}:${gid}" Gemfile.lock
fi
}
_detect_app_type() {
_detect_rails5_app \
|| _detect_rails42_app \
|| _detect_rails41_app \
|| _detect_rails4_app \
|| _detect_rails3_app \
|| _detect_rails2_app \
|| _detect_rack_app \
|| _detect_ruby_app
}
_detect_missing_gems() {
if [[ "$_app_type" == "rails5" ]] && ! _has_gem tzinfo-data; then
local e
e="A required gem 'tzinfo-data' is missing from the Gemfile."
e="$e If a 'gem \"tzinfo-data\", platforms: [...]' line exists,"
e="$e simply remove the comma and 'platforms:' section,"
e="$e run 'bundle update' to update the Gemfile.lock, and retry the build."
exit_with "$e" 10
fi
if _has_gem railties \
&& _compare_gem railties --greater-than-eq 3.0.0 --less-than 5.0.0 \
&& ! _has_gem rails_12factor; then
local e
e="A required gem 'rails_12factor' is missing from the Gemfile."
e="$e Add the gem to your Gemfile,"
e="$e run 'bundle install' to update the Gemfile.lock, and retry the build."
exit_with "$e" 10
fi
}
# shellcheck disable=SC2016
_detect_process_bins() {
if [[ -f Procfile ]]; then
local line bin cmd
build_line "Procfile detected, reading processes"
# Procfile parsing was heavily inspired by the implementation in
# gliderlabs/herokuish. Thanks to:
# https://github.com/gliderlabs/herokuish/blob/master/include/procfile.bash
while read -r line; do
if [[ "$line" =~ ^#.* ]]; then
continue
else
bin="${line%%:*}"
cmd="${line#*:}"
_set_if_unset scaffolding_process_bins "$(trim "$bin")" "$(trim "$cmd")"
fi
done < Procfile
fi
case "$_app_type" in
rails*)
_set_if_unset scaffolding_process_bins "web" \
'bundle exec rails server -p $PORT'
_set_if_unset scaffolding_process_bins "console" \
'bundle exec rails console'
;;
rack)
_set_if_unset scaffolding_process_bins "web" \
'bundle exec rackup config.ru -p $PORT'
_set_if_unset scaffolding_process_bins "console" \
'bundle exec irb'
;;
esac
if _has_gem rake && _has_rakefile; then
_set_if_unset scaffolding_process_bins "rake" 'bundle exec rake'
fi
_set_if_unset scaffolding_process_bins "sh" 'sh'
}
_update_vars() {
_set_if_unset scaffolding_env LANG "{{cfg.lang}}"
_set_if_unset scaffolding_env PORT "{{cfg.app.port}}"
# Export the app's listen port
_set_if_unset pkg_exports port "app.port"
_set_if_unset scaffolding_env NODE_ENV "{{cfg.node_env}}"
case "$_app_type" in
rails*)
scaffolding_symlinked_dirs+=(log tmp public/system)
if _compare_gem railties --less-than 4.1.0; then
scaffolding_symlinked_files+=(config/secrets.yml)
fi
_set_if_unset scaffolding_env RAILS_ENV "{{cfg.rails_env}}"
_set_if_unset scaffolding_env RACK_ENV "{{cfg.rack_env}}"
if _compare_gem railties --greater-than-eq 5.0.0; then
_set_if_unset scaffolding_env RAILS_LOG_TO_STDOUT "enabled"
fi
if _compare_gem railties --greater-than-eq 4.2.0; then
_set_if_unset scaffolding_env RAILS_SERVE_STATIC_FILES "enabled"
fi
if _compare_gem railties --greater-than-eq 4.1.0; then
_set_if_unset scaffolding_env SECRET_KEY_BASE "{{cfg.secret_key_base}}"
fi
;;
rack)
_set_if_unset scaffolding_env RACK_ENV "{{cfg.rack_env}}"
;;
esac
if _has_gem activerecord && _compare_gem activerecord \
--less-than 4.1.0.beta1; then
build_line "Using config/database.yml for DB config"
scaffolding_symlinked_files+=(config/database.yml)
else
build_line "Using DATABASE_URL env_var for DB config"
fi
}
_update_pkg_build_deps() {
# Order here is important--entries which should be first in
# `${pkg_build_deps[@]}` should be called last.
_detect_git
_detect_yarn
}
_update_pkg_deps() {
# Order here is important--entries which should be first in `${pkg_deps[@]}`
# should be called last.
_add_busybox
_detect_sqlite3
_detect_pg
_detect_nokogiri
_detect_execjs
_detect_webpacker
_detect_ruby
_detect_node
}
append_to_pkg_bin_dirs() {
local arr
arr=(${@})
for dir in "${arr[@]}"; do
build_line "adding directory $dir to pkg_bin_dirs[@]"
pkg_bin_dirs=($(_return_or_append_to_set "$(trim $dir)" "${pkg_bin_dirs[@]}" ))
done
}
_update_bin_dirs_ruby() {
local ruby_bin_dirs
# Add the `bin/` directory and the app's `binstubs/` directory to the bin
# dirs so they will be on `PATH. We do this after the existing values so
# that the Plan author's `${pkg_bin_dirs[@]}` will always win.
ruby_bin_dirs=(
bin
$(basename "$scaffolding_app_prefix")/binstubs
)
append_to_pkg_bin_dirs ${ruby_bin_dirs[@]}
}
_update_bin_dirs_node() {
local node_bin_dirs
# Add the `bin/` directory and the app's `node_modules/.bin/` directory to
# the bin dirs so they will be on `PATH. We do this after the existing
# values so that the Plan author's `${pkg_bin_dirs[@]}` will always win.
# TODO: get this from package.json $cacheDirectories via jq
node_bin_dirs=(
$(basename "$scaffolding_app_prefix")/client/node_modules/.bin
$(basename "$scaffolding_app_prefix")/node_modules/.bin
)
append_to_pkg_bin_dirs ${node_bin_dirs[@]}
}
_update_bin_dirs() {
_update_bin_dirs_ruby
[[ -n $node_enabled ]] && _update_bin_dirs_node
}
_update_svc_run() {
if [[ -z "$pkg_svc_run" ]]; then
pkg_svc_run="$pkg_prefix/bin/${pkg_name}-web"
build_line "Setting pkg_svc_run='$pkg_svc_run'"
fi
}
_add_busybox() {
build_line "Adding Busybox package to run dependencies"
pkg_deps=(core/busybox-static ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
}
_detect_execjs() {
if _has_gem execjs; then
build_line "Detected 'execjs' gem in Gemfile.lock, adding node packages"
# TODO: use the version `core/node/$ver` here too
# pkg_deps=(core/node ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
fi
}
_detect_git() {
if [[ -d ".git" ]]; then
build_line "Detected '.git' directory, adding git packages as build deps"
pkg_build_deps=(core/git ${pkg_build_deps[@]})
debug "Updating pkg_build_deps=(${pkg_build_deps[*]}) from Scaffolding detection"
_uses_git=true
fi
}
_detect_nokogiri() {
if _has_gem nokogiri; then
build_line "Detected 'nokogiri' gem in Gemfile.lock, adding libxml2 & libxslt packages"
export BUNDLE_BUILD__NOKOGIRI="--use-system-libraries"
pkg_deps=(core/libxml2 core/libxslt ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
fi
}
_detect_pg() {
for gem in "${_pg_gems[@]}"; do
if _has_gem "$gem"; then
build_line "Detected '$gem' gem in Gemfile.lock, adding postgresql package"
pkg_deps=(core/postgresql ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
_uses_pg=true
return 0
fi
done
}
_detect_mysql() {
for gem in "${_mysql_gems[@]}"; do
if _has_gem "$gem"; then
build_line "Detected '$gem' gem in Gemfile.lock, adding mysql-client package"
pkg_deps=(core/mysql-client ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
_uses_mysql=true
return 0
fi
done
}
_detect_rack_app() {
if _has_gem rack; then
build_line "Detected Rack app type"
_app_type="rack"
return 0
else
return 1
fi
}
_detect_rails2_app() {
if _has_gem railties && _compare_gem railties \
--greater-than-eq 2.0.0 --less-than 3.0.0; then
build_line "Detected Rails 2 app type"
warn "Rails 2 app types not yet supported with this Scaffolding"
exit_with "App type not supported" 2
_app_type="rails2"
return 0
else
return 1
fi
}
_detect_rails3_app() {
if _has_gem railties && _compare_gem railties \
--greater-than-eq 3.0.0 --less-than 4.0.0; then
build_line "Detected Rails 3 app type"
warn "Rails 3 app types not yet supported with this Scaffolding"
exit_with "App type not supported" 2
_app_type="rails3"
return 0
else
return 1
fi
}
_detect_rails4_app() {
if _has_gem railties && _compare_gem railties \
--greater-than-eq 4.0.0.beta --less-than 4.1.0.beta1; then
build_line "Detected Rails 4 app type"
warn "Rails 4 app types not yet supported with this Scaffolding"
exit_with "App type not supported" 2
_app_type="rails4"
return 0
else
return 1
fi
}
_detect_rails41_app() {
if _has_gem railties && _compare_gem railties \
--greater-than-eq 4.1.0.beta1 --less-than 5.0.0; then
build_line "Detected Rails 4.1 app type"
warn "Rails 4.1 app types not yet supported with this Scaffolding"
exit_with "App type not supported" 2
_app_type="rails41"
return 0
else
return 1
fi
}
_detect_rails42_app() {
if _has_gem railties && _compare_gem railties \
--greater-than-eq 4.2.0 --less-than 5.0.0; then
build_line "Detected Rails 4.2 app type"
_app_type="rails42"
return 0
else
return 1
fi
}
_detect_rails5_app() {
if _has_gem railties && _compare_gem railties \
--greater-than-eq 5.0.0 --less-than 6.0.0; then
build_line "Detected Rails 5 app type"
_app_type="rails5"
return 0
else
return 1
fi
}
_detect_ruby_app() {
build_line "Detected Ruby app type"
warn "Ruby app types not yet supported with this Scaffolding"
exit_with "App type not supported" 2
_app_type="ruby"
return 0
}
_detect_ruby() {
local lockfile_version
if [[ -n "$scaffolding_ruby_pkg" ]]; then
_ruby_pkg="$scaffolding_ruby_pkg"
build_line "Detected Ruby version in Plan, using '$_ruby_pkg'"
else
lockfile_version="$($_gemfile_parser ruby-version ./Gemfile.lock || true)"
if [[ -n "$lockfile_version" ]]; then
# TODO fin: Add more robust Gemfile to Habitat package matching
case "$lockfile_version" in
*)
_ruby_pkg="core/ruby/$(
echo "$lockfile_version" | cut -d ' ' -f 2)"
;;
esac
build_line "Detected Ruby version '$lockfile_version' in Gemfile.lock, using '$_ruby_pkg'"
else
_ruby_pkg="$_default_ruby_pkg"
build_line "No Ruby version detected in Plan or Gemfile.lock, using default '$_ruby_pkg'"
fi
fi
pkg_deps=($_ruby_pkg ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
}
_detect_sqlite3() {
if _has_gem sqlite3; then
build_line "Detected 'sqlite3' gem in Gemfile.lock, adding sqlite packages"
pkg_deps=(core/sqlite ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
fi
}
_detect_webpacker() {
if _has_gem webpacker; then
build_line "Detected 'webpacker' gem in Gemfile.lock, adding yarn packages"
pkg_deps=(core/yarn ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
fi
}
_detect_node() {
if [[ -n "$scaffolding_node_pkg" ]]; then
_node_pkg="$scaffolding_node_pkg"
build_line "Detected Node.js version in Plan, using '$_node_pkg'"
else
local _node_version
_node_version="$(_json_val package.json .engines.node)"
if [[ -z "$_node_version" ]]; then
build_line "Unable to Determine Node Version from package.json"
else
node_version="$_node_version"; export node_version
fi
if [[ -n "$node_version" ]]; then
# TODO fin: Add more robust packages.json to Habitat package matching
case "$node_version" in
*)
_node_pkg="core/node/$node_version"
;;
esac
build_line "Detected Node.js version '$node_version' in package.json, using '$_node_pkg'"
elif [[ -f .nvmrc && -n "$(cat .nvmrc)" ]]; then
node_version="$(trim "$(cat .nvmrc)")"
# TODO fin: Add more robust .nvmrc to Habitat package matching
case "$node_version" in
*)
_node_pkg="core/node/$node_version"
;;
esac
build_line "Detected Node.js version '$node_version' in .nvmrc, using '$_node_pkg'"
else
_node_pkg="$_default_node_pkg"
build_line "No Node.js version detected in Plan, package.json, or .nvmrc, using default '$_node_pkg'"
fi
fi
pkg_deps=($_node_pkg ${pkg_deps[@]})
debug "Updating pkg_deps=(${pkg_deps[*]}) from Scaffolding detection"
}
_detect_yarn() {
# TODO fin: support custom version of Yarn package?
if [[ "$node_pkg_manager" == "yarn" ]]; then
build_line "Adding Yarn package to build dependencies"
pkg_build_deps=(core/yarn ${pkg_build_deps[@]})
debug "Updating pkg_build_deps=(${pkg_build_deps[*]}) from Scaffolding detection"
fi
}
# **Internal** Invokes the `bundle` program using the chosen version of Ruby.
# This way, we should avoid most Bundler warnings about mismatched Ruby
# versions specfied in the Gemfile.
_bundle() {
local bundler_prefix
bundler_prefix="$(pkg_path_for bundler)"
env \
-u RUBYOPT \
-u GEMRC \
GEM_HOME="$bundler_prefix" \
GEM_PATH="$bundler_prefix" \
"$(pkg_path_for "$_ruby_pkg")/bin/ruby" \
"$bundler_prefix/bin/bundle.real" ${*:-}
}
_rake() {
case "$_app_type" in
rails*)
RACK_ENV=production \
RAILS_ENV=production \
RAILS_GROUP=assets \
_bundle exec rake ${*:-}
;;
rack)
RACK_ENV=production \
_bundle exec rake ${*:-}
;;
*)
_bundle exec rake ${*:-}
;;
esac
}
_bundle_install() {
local path
path="$1"
shift
# TODO: make `--without development:test:staging`
# respect env. shit that goes for all over
_bundle install ${*:-} \
--jobs "$(nproc)" \
--without development:test:staging \
--path "$path" \
--shebang="$(pkg_path_for "$_ruby_pkg")/bin/ruby" \
--no-clean \
--deployment
}
_compare_gem() {
local gem result
gem="$1"
shift
result="$($_gemfile_parser compare-gem-version ${*:-} \
./Gemfile.lock "$gem" 2> /dev/null || true)"
if [[ "$result" == "true" ]]; then
return 0
else
return 1
fi
}
_create_process_bin() {
local bin cmd env_sh
bin="$1"
cmd="$2"
env_sh="$pkg_svc_config_path/app_env.sh"
build_line "Creating ${bin} process bin"
cat <<EOF > "$bin"
#!$(pkg_path_for busybox-static)/bin/sh
set -e
if test -n "\$DEBUG"; then set -x; fi
export HOME="$pkg_svc_data_path"
if [ -f "$env_sh" ]; then
. "$env_sh"
else
>&2 echo "No app env file found: '$env_sh'"
>&2 echo "Have you not started this service ($pkg_origin/$pkg_name) before?"
>&2 echo ""
>&2 echo "Aborting..."
exit 1
fi
cd $scaffolding_app_prefix
exec $cmd \$@
EOF
chmod -v 755 "$bin"
}
_default_toml_has_no() {
local key toml
key="$1"
toml="$PLAN_CONTEXT/default.toml"
if [[ ! -f "$toml" ]]; then
return 0
fi
if [[ "$(rq -t < "$toml" "at \"${key}\"")" == "null" ]]; then
return 0
else
return 1
fi
}
# With thanks to:
# https://github.com/heroku/heroku-buildpack-nodejs/blob/master/lib/json.sh
# shellcheck disable=SC2002
_json_val() {
local json
json="$1"
path="$2"
cat "$json" | "$_jq" --raw-output "$path // \"\""
}
_has_gem() {
local result
result="$($_gemfile_parser has-gem ./Gemfile.lock "$1" 2> /dev/null || true)"
if [[ "$result" == "true" ]]; then
return 0
else
return 1
fi
}
_has_rakefile() {
local candidate candidates
candidates=(Rakefile rakefile rakefile.rb Rakefile.rb)
for candidate in "${candidates[@]}"; do
if [[ -f "$candidate" ]]; then
return 0
fi
done
return 1
}
# Heavily inspired from:
# https://gist.github.com/Integralist/1e2616dc0b165f0edead9bf819d23c1e
_rename_function() {
local orig_name new_name
orig_name="$1"
new_name="$2"
declare -F "$orig_name" > /dev/null \
|| exit_with "No function named $orig_name, aborting" 97
eval "$(echo "${new_name}()"; declare -f "$orig_name" | tail -n +2)"
}
_set_if_unset() {
local hash key
hash="$1"
key="$2"
val="$3"
if [[ ! -v "$hash[$key]" ]]; then
eval "$hash[$key]='$val'"
fi
}
# **Internal** Use a "tar pipe" to copy the app source into a destination
# directory. This function reads from `stdin` for its file/directory manifest
# where each entry is on its own line ending in a newline. Several filters and
# changes are made via this copy strategy:
#
# * All user and group ids are mapped to root/0
# * No extended attributes are copied
# * Some file editor backup files are skipped
# * Some version control-related directories are skipped
# * Any `./habitat/` directory is skipped
# * Any `./vendor/bundle` directory is skipped as it may have native gems
_tar_pipe_app_cp_to() {
local dst_path tar
dst_path="$1"
tar="$(pkg_path_for tar)/bin/tar"
"$tar" -cp \
--owner=root:0 \
--group=root:0 \
--no-xattrs \
--exclude-backups \
--exclude-vcs \
--exclude='habitat' \
--exclude='vendor/bundle' \
--files-from=- \
-f - \
| "$tar" -x \
-C "$dst_path" \
-f -
}
_wrap_ruby_bin() {
local bin="$1"
build_line "Adding wrapper $bin to ${bin}.real"
mv -v "$bin" "${bin}.real"
cat <<EOF > "$bin"
#!$(pkg_path_for busybox-static)/bin/sh
set -e
if test -n "\$DEBUG"; then set -x; fi
export GEM_HOME="$GEM_HOME"
export GEM_PATH="$GEM_PATH"
unset RUBYOPT GEMRC
exec $(pkg_path_for $_ruby_pkg)/bin/ruby ${bin}.real \$@
EOF
chmod -v 755 "$bin"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment