Create a gist now

Instantly share code, notes, and snippets.

@arrdem /install.sh
Last active Apr 5, 2017

What would you like to do?
A bootleg puppet
# -*- mode: bash; indent-tabs-mode: nil; sh-basic-offset: 2; fill-column: 100; -*-
# About
# ==============================
# A bootleg dotfiles installer.
#
# Usage
# ==============================
#
# $FORCE - if non-empty then install.sh will happily clobber existing files/dirs
# $DEBUG - if non-empty then install.sh will be verbose about what gets moved where
#
# $ bash ./install.sh
#
# Expects a repository of the following configuration:
# $ROOT/
# $ROOT/install.sh
# $ROOT/profiles.d
# $ROOT/packages.d
# $ROOT/hosts.d
#
# packages.d is expected to be a directory of packages.
# profiles.d and hosts.d are expected to be directories of profiles.
#
# Packages
# --------------------
#
# Packages consist of one or more directories of files to be installed, by default to ~/
# Packages may provide a README.*, an install and a build which are ignored
#
# If a build file is present, the build will be executed before the package is installed. This
# allows packages the opportunity to build platform-specific files and/or binaries.
#
# If an install file is present, that file will be executed with no arguments and is expected to
# install the package. This allows packages to provide arbitrary installation behavior, such as
# placing files elsewhere than ~, or just installing packages.
#
# If no install file is present, then all directories in the package directory will be recursively
# created under ~, and all files in the package directories will be symlinked into place. This
# behavior is used so that multiple packages can place files in the same directories.
#
# Profiles
# --------------------
#
# Profiles are a way to group together multiple packages, as well as to require packages.
#
# Profiles consist of a directory, containing an optional README.*, a requirements file, and zero or
# more package directories.
#
# The requirements file may consist of # comments, or lines of the format "profiles.d/$PROFILE" or
# "packages.d/$PACKAGE". All required packages and profiles will be installed before the packages
# included in the profile are installed.
#
# Example
# --------------------
#
# $ hostname
# test
# $ ls hosts.d/test
# requirements vim
# $ cat host.d/test/requirements
# profiles.d/default
# packages.d/git
#
# When install.sh is executed, the host profile named test will be installed. It consists of the
# default profile and the git package, as well as a vim package bundled in the host. These packages
# will be installed in that order.
function arrdem_installf () {
# $1 is the logical name of the file to be installed
# $2 is the absolute name of the file to be installed
# $3 is the absolute name of its destination
#
# Installs (links) a single file, debugging as appropriate
if [ -n "$DEBUG" ]; then
echo "[DBG] $1 -> $3"
fi
ln -s "$2" "$3"
}
export -f arrdem_installf
function arrdem_installd () {
# $1 is an un-normalized path which should be created (or cleared!)
dir="$(echo $1 | sed 's/\.\///g')"
if [ ! -d "$dir" ]; then
if [ -n "$FORCE" ]; then
rm -rf "$dir"
fi
mkdir -p "$dir"
fi
}
export -f arrdem_installd
function arrdem_stowf () {
# $1 is the stow target dir
# $2 is the name of the file to stow
f="$(echo $2 | sed 's/\.\///g')" # Strip leading ./
TGT="$1/$f"
ABS="$(realpath $f)"
if [ -h "$TGT" ] || [ -e "$TGT" ]; then
if [ "$(realpath $TGT)" != "$ABS" ]; then
if [ -n "$FORCE" ]; then
if [ -n "$DEBUG" ]; then
echo "[DBG] Clobbering existing $ABS"
fi
rm "$TGT"
arrdem_installf "$f" "$ABS" "$TGT"
else
echo "[WARNING] $TGT already exists! Not replacing!"
echo " Would have been replaced with $ABS"
fi
else
if [ -n "$DEBUG" ]; then
echo "[DEBUG] $TGT ($f) already installed, skipping"
fi
fi
else
arrdem_installf "$f" "$ABS" "$TGT"
fi
}
export -f arrdem_stowf
function arrdem_stow () {
# $1 is the install target dir
# $2 is the source package dir
#
# Makes all required directories and links all required files to install a given package.
# If a target directory doesn't exist, create it as a directory.
# For each file in the source, create symlinks into the target directory.
#
# This has the effect of creating merge mounts between multiple packages, which gnu stow doesn't
# support.
( cd "$2"
# Make all required directories if they don't exist
#
# If force is set and something is already there blow it the fsck away
find . -mindepth 1 \
-type d \
-exec bash -c 'arrdem_installd "$0/$1"' "$1" {} \;
# Link in all files.
#
# If the file already exists AND is a link to the thing we want to install, don't bother.
# Else if the file already exists and isn't the thing we want to install and force is set, clobber
# Else if the file already exists emit a warning
# Else link the file in as appropriate
#
# Note that this skips install, build and README files
find . -type f \
-not -name "INSTALL" \
-not -name "BUILD" \
-not -name "README.*" \
-exec bash -c 'arrdem_stowf "$0" "$1"' "$1" {} \;
)
}
export -f arrdem_stow
function install_package() {
# $1 is the path of the package to install
#
# Executes the package build script if present.
#
# Then executes the install script if present, otherwise using arrdem_stow to install the built
# package.
echo "[INFO - install_package] installing $1"
if [ -x "$1/BUILD" ]; then
( cd "$1";
./BUILD)
fi
if [ -e "$1/INSTALL" ]; then
( cd "$1";
./INSTALL)
else
arrdem_stow ~ "$1"
fi
}
export -f install_package
function install_profile() {
# $1 is the path of the profile to install
#
# Reads the requires file from the profile, installing all required profiles and packages, then
# installs any packages in the profile itself.
if [ -d "$1" ]; then
echo "[INFO] installing $1"
# Install requires
REQUIRES="$1/requires"
if [ -e "$REQUIRES" ]; then
cat $REQUIRES | while read -r require; do
echo "[INFO] $require"
case "$require" in
profiles.d/*)
echo "[INFO - install_profile($1)] recursively installing profile $require"
install_profile "$require"
;;
packages.d/*)
echo "[INFO - install_profile($1)] installing package $require"
install_package "$require"
;;
esac
done
fi
# Install the package(s)
find "$1" \
-maxdepth 1 \
-mindepth 1 \
-type d \
-exec bash -c 'install_package "$0"' {} \;
else
echo "[WARN] No such package $1!"
fi
}
function main() {
# Normalize hostname
HOSTNAME="$(hostname | tr '[:upper:]' '[:lower:]')"
HOST_DIR="hosts.d/$HOSTNAME"
if [ -d "$HOST_DIR" ]; then
# Install the host profile itself
#
# It is expected that the host requires default explicitly (or transitively) rather than getting
# it "for free".
echo "[INFO - main] installing profile $HOST_DIR"
install_profile "$HOST_DIR"
else
# Otherwise just install the default profile
echo "[INFO - main] installing fallback profile profile profiles.d/default"
install_profile "profiles.d/default"
fi
}
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment