modify install script

This commit is contained in:
2026-02-21 06:00:18 +01:00
parent 00ddc5dbcc
commit 63cdf2685e

344
install
View File

@@ -1,48 +1,60 @@
#!/usr/bin/env bash #!/bin/bash
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 &&
pwd)"
PROFILES_DIR="${SCRIPT_DIR}/profiles"
STOW_ARGS=(
"--no-folding"
"--adopt"
"--dir" "${PROFILES_DIR}"
"--target" "${HOME}"
)
SCRIPT_NAME="${0##*/}"
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)
readonly SCRIPT_DIR readonly SCRIPT_DIR
readonly PROFILES_DIR=${SCRIPT_DIR}/profiles readonly PROFILES_DIR
readonly -a STOW_ARGS=(--no-folding --adopt --dir "${PROFILES_DIR}" --target "${HOME}") readonly -a STOW_ARGS
readonly SCRIPT_NAME=${0##*/} readonly SCRIPT_NAME
print_opt() { print_opt() {
local -r option=$1 local -r option="$1"
local -r description=$2 local -r description="$2"
printf " %-22s %s\n" "${option}" "${description}" printf " %-22s %s\n" "${option}" "${description}"
} }
shell_quote() { shell_quote() {
local -r string=$1 local -r string="$1"
printf "'%s'" "${string//'/'\\''}" printf "'%s'" "${string//'/'\\''/}"
} }
array_contains() { array_contains() {
local -r target_item=$1; shift local -r target_item="$1"
local -a -r array=("$@") shift
local item local -a -r array=("$@")
for item in "${array[@]}"; do local item
if [[ ${item} == "${target_item}" ]]; then for item in "${array[@]}"; do
return 0 if [[ ${item} == "${target_item}" ]]; then
fi return 0
done fi
return 1 done
return 1
} }
die() { die() {
local -r message=$1 local -r message="$1"
printf "%s: %b\n" "${SCRIPT_NAME}" "${message}" >&2 printf "%s: %b\n" "${SCRIPT_NAME}" "${message}" >&2
exit 1 exit 1
} }
invalid_option() { invalid_option() {
local -r option=$1 local -r option="$1"
die "invalid option $(shell_quote "${option}")\nTry '${SCRIPT_NAME} --help' for usage." die "invalid option $(shell_quote "${option}")
Try '${SCRIPT_NAME} --help' for usage."
} }
check_deps() { check_deps() {
if ! command -v stow >/dev/null 2>&1; then if ! command -v stow >/dev/null 2>&1; then
cat >&2 <<EOF cat >&2 <<EOF
Error: 'stow' not found in PATH. Error: 'stow' not found in PATH.
GNU Stow is a symlink farm manager used to deploy dotfiles/packages GNU Stow is a symlink farm manager used to deploy dotfiles/packages
@@ -59,13 +71,13 @@ source if GNU Stow is not packaged or your system requires it.
Project: https://www.gnu.org/software/stow/ Project: https://www.gnu.org/software/stow/
EOF EOF
exit 1 exit 1
fi fi
} }
require_profiles_dir() { require_profiles_dir() {
if [[ ! -d ${PROFILES_DIR} ]]; then if [[ ! -d "${PROFILES_DIR}" ]]; then
cat >&2 <<EOF cat >&2 <<EOF
Error: profiles directory not found: ${PROFILES_DIR} Error: profiles directory not found: ${PROFILES_DIR}
This directory must contain profile subdirectories with dotfiles. This directory must contain profile subdirectories with dotfiles.
@@ -77,83 +89,98 @@ Make sure you:
More information about what is profile can be found using '${SCRIPT_NAME} --help profile'. More information about what is profile can be found using '${SCRIPT_NAME} --help profile'.
EOF EOF
exit 1 exit 1
fi fi
mapfile -t PROFILES < <(find "${PROFILES_DIR}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n') mapfile -t PROFILES < <(find "${PROFILES_DIR}" -mindepth 1 -maxdepth 1 \
readonly PROFILES -type d -printf '%f\n')
readonly PROFILES
} }
parse_args() { parse_args() {
while (( $# > 0 )); do while (($# > 0)); do
case $1 in case $1 in
-h) -h)
show_general_help show_general_help
exit 0 exit 0
;; ;;
--help) --help)
if (( $# >= 2 )) && [[ $2 ]] && [[ $2 != -* ]]; then if (($# >= 2)) && [[ $2 ]] && [[ $2 != -* ]]; then
show_help_topic "$2" show_help_topic "$2"
else else
show_general_help show_general_help
fi fi
exit 0 exit 0
;; ;;
--help=*) --help=*)
local -r topic=${1#*=} local -r topic="${1#*=}"
[[ -z ${topic} ]] && show_general_help && exit 0 [[ -z ${topic} ]] && show_general_help && exit 0
show_help_topic "${topic}" show_help_topic "${topic}"
exit 0 exit 0
;; ;;
-l|--list-profiles) list_profiles=true; shift ;; -l | --list-profiles)
-P|--prune-symlinks) prune_symlinks=true; shift ;; list_profiles="true"
-v|--verbose) verbose=true; shift ;; shift
-p|--profile) ;;
( (( $# < 2 )) || [[ -z "$2" ]] ) && die "--profile requires an argument" -P | --prune-symlinks)
profile=$2 prune_symlinks="true"
shift 2 shift
;; ;;
*) invalid_option "$1" ;; -v | --verbose)
esac verbose="true"
done shift
;;
-p | --profile)
( (($# < 2)) || [[ -z "$2" ]]) && die "--profile requires an argument"
profile="$2"
shift 2
;;
*) invalid_option "$1" ;;
esac
done
} }
validate_args() { validate_args() {
if [[ ${list_profiles} && ( ${prune_symlinks} || ${verbose} || ${profile} ) ]]; then if [[ -n "${list_profiles}" ]] &&
die "options '--list-profiles' and others cannot be used together" [[ -n "${prune_symlinks}" || -n "${verbose}" || -n "${profile}" ]]; then
fi die "options '--list-profiles' and others cannot be used together"
fi
if [[ ${profile} ]] && ! array_contains "${profile}" "${PROFILES[@]}"; then if [[ -n "${profile}" ]] &&
die "profile $(shell_quote "${profile}") cannot be found" ! array_contains "${profile}" "${PROFILES[@]}"; then
fi die "profile $(shell_quote "${profile}") cannot be found"
fi
} }
show_general_help() { show_general_help() {
echo "Usage: ${SCRIPT_NAME} [options]" echo "Usage: ${SCRIPT_NAME} [options]"
echo " ${SCRIPT_NAME} -l | --list-profiles" echo " ${SCRIPT_NAME} -l | --list-profiles"
echo " ${SCRIPT_NAME} --help[=TOPIC]" echo " ${SCRIPT_NAME} --help[=TOPIC]"
echo echo
echo "Options:" echo "Options:"
print_opt "-h, --help[=TOPIC]" "show general help or help for a specific topic" print_opt "-h, --help[=TOPIC]" \
print_opt "-p, --profile PROFILE" "select profile" "show general help or help for a specific topic"
print_opt "-v, --verbose" "increase verbosity" print_opt "-p, --profile PROFILE" "select profile"
print_opt "-P, --prune-symlinks" "remove dangling symlinks in home directory" print_opt "-v, --verbose" "increase verbosity"
print_opt "-l, --list-profiles" "list available profiles (cannot be combined with other options)" print_opt "-P, --prune-symlinks" \
echo "remove dangling symlinks in home directory"
echo "Topics:" print_opt "-l, --list-profiles" \
print_opt "profile" "explanation of profile structure and usage" "list available profiles (cannot be combined with other options)"
echo echo
echo "Profile:" echo "Topics:"
echo " Directory containing dotfiles plus optional parent and hooks/." print_opt "profile" "explanation of profile structure and usage"
echo " Used by the installer to group and deploy related configurations." echo
echo "Profile:"
echo " Directory containing dotfiles plus optional parent and hooks/."
echo " Used by the installer to group and deploy related configurations."
} }
show_help_topic() { show_help_topic() {
local topic=$1 local topic="$1"
case ${topic} in case "${topic}" in
profile) profile)
cat <<EOF cat <<EOF
A profile is a directory under 'profiles/' that groups dotfiles to be installed A profile is a directory under 'profiles/' that groups dotfiles to be
together using GNU Stow. installed together using GNU Stow.
Contents: Contents:
dotfiles/ files or directories to deploy (e.g. .config) dotfiles/ files or directories to deploy (e.g. .config)
@@ -166,97 +193,100 @@ Only executable files inside hooks are executed.
Profiles may inherit from a parent. The installer builds the dependency Profiles may inherit from a parent. The installer builds the dependency
tree automatically and installs profiles in the correct order. tree automatically and installs profiles in the correct order.
Use .stow-local-ignore to prevent linking internal files such as hooks/ and parent. Use .stow-local-ignore to prevent linking internal files such as hooks/
and parent.
EOF EOF
;; ;;
*) die "unknown help topic: ${topic}" ;; *) die "unknown help topic: ${topic}" ;;
esac esac
} }
run_pre_hooks() { run_pre_hooks() {
local -r target_profile=$1 local -r target_profile="$1"
for hook in "${PROFILES_DIR}/${target_profile}"/hooks/pre/*; do for hook in "${PROFILES_DIR}/${target_profile}"/hooks/pre/*; do
if [[ -f ${hook} && -x ${hook} ]]; then if [[ -f "${hook}" && -x "${hook}" ]]; then
echo "Running pre-install hook ${hook##*/}" echo "Running pre-install hook ${hook##*/}"
"${hook}" "${hook}"
fi fi
done done
} }
run_post_hooks() { run_post_hooks() {
local -r target_profile=$1 local -r target_profile="$1"
for hook in "${PROFILES_DIR}/${target_profile}"/hooks/post/*; do for hook in "${PROFILES_DIR}/${target_profile}"/hooks/post/*; do
if [[ -f ${hook} && -x ${hook} ]]; then if [[ -f "${hook}" && -x "${hook}" ]]; then
echo "Running post-install hook ${hook##*/}" echo "Running post-install hook ${hook##*/}"
"${hook}" "${hook}"
fi fi
done done
} }
prune_symlinks() { prune_symlinks() {
echo -n "Removing dangling symlinks in home directory... " echo -n "Removing dangling symlinks in home directory... "
pushd "${HOME}" >/dev/null || exit 1 pushd "${HOME}" >/dev/null || exit 1
local common_files local common_files
local -r target_profile=$1 local -r target_profile="$1"
mapfile -t common_files < <(comm -12 <(find "${HOME}" -type l -printf '%P\n' 2>/dev/null | sort) \ mapfile -t common_files < <(comm -12 <(find "${HOME}" -type l \
<(find "${PROFILES_DIR}/${target_profile}" -type f -printf '%P\n' | sort)) -printf '%P\n' 2>/dev/null | sort) \
local target_file <(find "${PROFILES_DIR}/${target_profile}" -type f -printf '%P\n' | sort))
for target_file in "${common_files[@]}"; do local target_file
local -a rm_args=(--force) for target_file in "${common_files[@]}"; do
[[ ${verbose} ]] && rm_args+=(--verbose) local -a rm_args=("--force")
rm "${rm_args[@]}" -- "${target_file}" [[ ${verbose} ]] && rm_args+=("--verbose")
done rm "${rm_args[@]}" -- "${target_file}"
popd >/dev/null || exit 1 done
popd >/dev/null || exit 1
echo "OK" echo "OK"
} }
stow_profile() { stow_profile() {
local -r target_profile=$1 local -r target_profile="$1"
if stow "${STOW_ARGS[@]}" "${target_profile}" 2>&1; then if stow "${STOW_ARGS[@]}" "${target_profile}" 2>&1; then
echo "Profile ${target_profile} has been stowed" echo "Profile ${target_profile} has been stowed"
else else
echo "Failed to create symlinks pointing to dotfiles in profile ${target_profile}" echo "Failed to create symlinks pointing to dotfiles" \
fi "in profile ${target_profile}"
fi
} }
fetch_parents() { fetch_parents() {
local -r target_profile=$1 local -r target_profile="$1"
if [[ -s ${PROFILES_DIR}/${target_profile}/parent ]]; then if [[ -s "${PROFILES_DIR}/${target_profile}/parent" ]]; then
local parent local parent
parent=$(cat "${PROFILES_DIR}/${target_profile}/parent") parent="$(cat "${PROFILES_DIR}/${target_profile}/parent")"
if [[ -s ${PROFILES_DIR}/${parent}/parent ]]; then if [[ -s "${PROFILES_DIR}/${parent}/parent" ]]; then
fetch_parents "${parent}" fetch_parents "${parent}"
fi
echo "${parent}"
fi fi
echo "${parent}"
fi
} }
build_deptree() { build_deptree() {
local -r target_profile=$1 local -r target_profile="$1"
fetch_parents "${target_profile}" fetch_parents "${target_profile}"
echo "${target_profile}" echo "${target_profile}"
} }
install_dotfiles() { install_dotfiles() {
local target_profile local target_profile
for target_profile in $(build_deptree "${profile}"); do for target_profile in $(build_deptree "${profile}"); do
run_pre_hooks "${target_profile}" run_pre_hooks "${target_profile}"
[[ ${prune_symlinks} ]] && prune_symlinks "${target_profile}" [[ -n "${prune_symlinks}" ]] && prune_symlinks "${target_profile}"
stow_profile "${target_profile}" stow_profile "${target_profile}"
run_post_hooks "${target_profile}" run_post_hooks "${target_profile}"
done done
} }
main() { main() {
check_deps check_deps
parse_args "$@" parse_args "$@"
require_profiles_dir require_profiles_dir
(( $# == 0 )) && show_general_help && exit 1 (($# == 0)) && show_general_help && exit 1
validate_args validate_args
[[ ${list_profiles} ]] && echo "${PROFILES[@]}" && exit 0 [[ -n "${list_profiles}" ]] && echo "${PROFILES[@]}" && exit 0
install_dotfiles install_dotfiles
} }
main "$@" main "$@"