Skip to content

Instantly share code, notes, and snippets.

@jaysoffian
Last active October 29, 2023 00:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaysoffian/3c67711d3f00c364365905d877cc4af4 to your computer and use it in GitHub Desktop.
Save jaysoffian/3c67711d3f00c364365905d877cc4af4 to your computer and use it in GitHub Desktop.
Like a Python venv, but for installing Ruby gems
#!/bin/bash
# gem-venv.sh
# ~~~~~~~~~~~
# Summary: Like a Python venv, but for installing Ruby gems.
#
# Create a directory and gem wrapper script for installing gems self-contained
# to that directory. Control which `gem` is used via the `-G/--gem` switch,
# defaulting to whatever `gem` is found in PATH.
#
# Optionally takes a list of gems to install using the just created directory
# and gem wrapper.
#
# Development notes:
# - Formatted with "shfmt -i 2 -ci -w gem-venv.sh"
# - Checked with "shellcheck gem-venv.sh"
set -euo pipefail
say() {
printf '%s\n' "$*"
}
emit_usage_header() {
cat <<'__EOF__'
Usage: gem-venv.sh [options] GEM_DIR [GEM_NAME...]
-G, --gem GEM Use GEM instead of the `gem` found in PATH
-h, --help Show help and quit
__EOF__
}
emit_usage_and_exit() {
cat >&2 <<__EOF__
$(emit_usage_header)
This is not the full help. Use "--help" for more information.
__EOF__
exit 1
}
emit_help_and_exit() {
cat <<__EOF__
$(emit_usage_header)
Create GEM_DIR and wrapper script GEM_DIR/bin/gem for installing gems
self-contained to GEM_DIR.
With one or more GEM_NAMES, run GEM_DIR/bin/gem install GEM_NAMES... after
creating GEM_DIR.
__EOF__
exit 0
}
abspath() {
(cd "$1" && pwd -P)
}
# emit a script which wraps gem itself
emit_gem_wrapper() {
local gem_dir
gem_dir=$(abspath "$1")
cat <<__EOF__
#!/bin/bash
# shellcheck disable=SC2016
set -eu
shopt -s nullglob
GEM_DIR="$gem_dir"
GEM_INSTALL_ARGS=(
--no-document
--no-user-install
--bindir "\$GEM_DIR/libexec"
)
wrap_binstubs() {
local path
for path in "\$GEM_DIR"/libexec/*; do
local name
name=\$(basename "\$path")
test gem = "\$name" && continue
(
printf '#!/bin/sh\\n'
printf 'GEM_DIR="%s" \\n' "\$GEM_DIR"
printf 'PATH="\$GEM_DIR/bin:\$PATH"'
test fastlane = "\$name" && printf ' FASTLANE_SELF_CONTAINED="true"'
printf ' GEM_HOME="\$GEM_DIR"'
printf ' GEM_PATH="\$GEM_DIR"'
printf ' exec "\$GEM_DIR/libexec/%s" "\$@"\\n' "\$name"
) > "\$GEM_DIR/bin/\$name"
chmod 0755 "\$GEM_DIR/bin/\$name"
done
}
command=
case "\${1:-}" in
install|update)
command="\$1"
shift
;;
esac
if test -n "\$command"; then
GEM_HOME="\$GEM_DIR" \\
GEM_PATH="\$GEM_DIR" \\
"\$GEM_DIR/libexec/gem" "\$command" "\${GEM_INSTALL_ARGS[@]}" "\$@"
wrap_binstubs
else
GEM_HOME="\$GEM_DIR" \\
GEM_PATH="\$GEM_DIR" \\
"\$GEM_DIR/libexec/gem" "\$@"
fi
__EOF__
}
main() {
local gem_exe="" gem_dir=""
local -a gems
while test "$#" -gt 0; do
case "$1" in
-h | --help) emit_help_and_exit ;;
--gem=*) gem_exe="${1/#--gem=/}" ;;
-G | --gem)
shift
gem_exe=$1
;;
*)
if test -n "$gem_dir"; then
gems+=("$1")
else
gem_dir=$1
fi
;;
esac
shift
done
test -n "$gem_dir" || emit_usage_and_exit
local gem_command
if test -f "$gem_dir/gem-venv.cfg"; then
say "Updating $gem_dir"
gem_command=update
if test -n "$gem_exe"; then
say "NOTICE: --gem option is ignored when updating"
fi
else
: "${gem_exe:="$(type -P gem)"}"
say "Creating $gem_dir using $gem_exe"
gem_command=install
mkdir -p "$gem_dir/libexec"
ln -s "$gem_exe" "$gem_dir/libexec/gem"
mkdir -p "$HOME/.gem/cache"
ln -s "$HOME/.gem/cache" "$gem_dir/cache"
fi
local gem_wrapper="$gem_dir/bin/gem"
mkdir -p "$gem_dir/bin"
emit_gem_wrapper "$gem_dir" >"$gem_wrapper"
chmod +x "$gem_wrapper"
"$gem_wrapper" env > "$gem_dir/gem-venv.cfg"
if test "${#gems[@]}" -gt 0; then
say "Running gem $gem_command ${gems[*]}"
"$gem_wrapper" "$gem_command" "${gems[@]}"
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment