Skip to content

Commit

Permalink
WIP: Enable non-admin and non-sudo bootstrap.sh usage
Browse files Browse the repository at this point in the history
  • Loading branch information
br3ndonland committed Nov 6, 2023
1 parent b54eafb commit f9bcf59
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 77 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,25 @@ jobs:
run: |
sudo rm -rf /usr/local/Caskroom /usr/local/Homebrew /usr/local/bin/brew \
/usr/local/.??* /Applications/Xcode.app /Library/Developer/CommandLineTools
- name: Create a non-admin user account
run: |
if ${{ runner.os == 'Linux' }}; then
sudo useradd -m non-admin-user
elif ${{ runner.os == 'macOS' }}; then
sudo sysadminctl -addUser non-admin-user
fi
- name: Run bootstrap.sh with a non-admin user without Homebrew installed
run:
bootstrap_script_url="https://raw.githubusercontent.com/$STRAP_GITHUB_USER/dotfiles/$STRAP_DOTFILES_BRANCH/bootstrap.sh"
sudo --preserve-env --user=non-admin-user /usr/bin/env bash -c "$(curl -fsSL $bootstrap_script_url)"
- name: Run bootstrap.sh
run: |
bootstrap_script_url="https://raw.githubusercontent.com/$STRAP_GITHUB_USER/dotfiles/$STRAP_DOTFILES_BRANCH/bootstrap.sh"
/usr/bin/env bash -c "$(curl -fsSL $bootstrap_script_url)"
- name: Rerun bootstrap.sh to test idempotence
run: bash "$HOME/.dotfiles/bootstrap.sh"
- name: Rerun bootstrap.sh with a non-admin user
run: sudo --preserve-env --user=non-admin-user /usr/bin/env bash "$HOME/.dotfiles/bootstrap.sh"
- name: Check Homebrew configuration
run: brew config
- name: Check for potential problems with brew doctor
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,16 @@ The following environment variables can be used to configure _bootstrap.sh_, and
- `STRAP_DOTFILES_URL`: URL from which the dotfiles repo will be cloned. Defaults to `https://github.com/$STRAP_GITHUB_USER/dotfiles`, but any [Git-compatible URL](https://www.git-scm.com/docs/git-clone#_git_urls) can be used, so long as it is accessible at the time the script runs.
- `STRAP_DOTFILES_BRANCH`: Git branch to check out after cloning dotfiles repo. Defaults to `main`.

_bootstrap.sh_ will set up macOS and Homebrew, run scripts in the _scripts/_ directory, and install Homebrew packages and casks from the _[Brewfile](Brewfile)_.
There are some additional variables for advanced usage. Consult the _[bootstrap.sh](bootstrap.sh)_ script to see all supported variables.

A Brewfile is a list of [Homebrew](https://brew.sh/) packages and casks (applications) that can be installed in a batch by [Homebrew Bundle](https://github.com/Homebrew/homebrew-bundle). The Brewfile can even be used to install Mac App Store apps with the `mas` CLI. Note that you must sign in to the App Store ahead of time for `mas` to work.
_bootstrap.sh_ will set up macOS and Homebrew, run scripts in the _scripts/_ directory, and install Homebrew packages and casks from the _[Brewfile](Brewfile)_. A Brewfile is a list of [Homebrew](https://brew.sh/) packages and casks (applications) that can be installed in a batch by [Homebrew Bundle](https://github.com/Homebrew/homebrew-bundle). The Brewfile can even be used to install Mac App Store apps with the `mas` CLI. Note that you must sign in to the App Store ahead of time for `mas` to work.

The following list is a brief summary of permissions related to _bootstrap.sh_.

- Initial setup of Homebrew itself does not require an admin user account, but does require `sudo`. See the [Homebrew installation docs](https://docs.brew.sh/Installation), [Homebrew/install#312](https://github.com/Homebrew/install/issues/312), and [Homebrew/install#315](https://github.com/Homebrew/install/pull/315/files).
- [After Homebrew setup, use of `sudo` with `brew` commands is discouraged](https://docs.brew.sh/FAQ#why-does-homebrew-say-sudo-is-bad).
- After Homebrew setup, commands such as `brew bundle install --global` should be run from the same user account used for setup. Attempts to run `brew` commands from another user account will result in errors, because directories that need to be updated are owned by the setup account. If access to the setup account is not routinely available, an alternative approach could be to change ownership of Homebrew directories to a group that includes the user account used for Homebrew setup as well as other users that need to run Homebrew commands.
- _bootstrap.sh_ can run with limited functionality on non-admin and non-`sudo` user accounts. A plausible use case could exist in which an admin runs `bootstrap.sh` to configure the system initially, then a non-admin runs `bootstrap.sh` to configure their own account. In this use case, the non-admin user should not need admin or `sudo` privileges, because all the pertinent setup (FileVault disk encryption, XCode developer tools, Homebrew, etc) is already complete.

Users with more complex needs for multi-environment dotfiles management might consider a tool like [`chezmoi`](https://www.chezmoi.io/).

Expand Down
179 changes: 104 additions & 75 deletions bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Linux)
esac
[[ -z $HOMEBREW_PREFIX ]] && HOMEBREW_PREFIX="$DEFAULT_HOMEBREW_PREFIX"

STRAP_ADMIN=${STRAP_ADMIN:-0}
if groups | grep -qE "\b(admin)\b"; then STRAP_ADMIN=1; else STRAP_ADMIN=0; fi
STRAP_CI=${STRAP_CI:=0}
STRAP_DEBUG=${STRAP_DEBUG:-0}
[[ $1 = "--debug" || -o xtrace ]] && STRAP_DEBUG=1
Expand All @@ -51,8 +53,10 @@ sudo_askpass() {

cleanup() {
set +e
sudo_askpass rm -rf "$CLT_PLACEHOLDER" "$SUDO_ASKPASS" "$SUDO_ASKPASS_DIR"
sudo --reset-timestamp
if [ -n "$SUDO_ASKPASS" ]; then
sudo_askpass rm -rf "$CLT_PLACEHOLDER" "$SUDO_ASKPASS" "$SUDO_ASKPASS_DIR"
sudo --reset-timestamp
fi
if [ -z "$STRAP_SUCCESS" ]; then
if [ -n "$STRAP_STEP" ]; then
echo "!!! $STRAP_STEP FAILED" >&2
Expand All @@ -73,18 +77,41 @@ else
Q="$STRAP_QUIET_FLAG"
fi

# Prompt for sudo password and initialize (or reinitialize) sudo
sudo --reset-timestamp

clear_debug() {
set +x
}

reset_debug() {
if [ "$STRAP_DEBUG" -gt 0 ]; then
set -x
fi
}

abort() {
STRAP_STEP=""
echo "!!! $*" >&2
exit 1
}

escape() {
printf '%s' "${1//\'/\'}"
}

log_no_sudo() {
STRAP_STEP="$*"
echo "--> $*"
}

logk() {
STRAP_STEP=""
echo "OK"
}

logn_no_sudo() {
STRAP_STEP="$*"
printf -- "--> %s " "$*"
}

logskip() {
STRAP_STEP=""
echo "SKIPPED"
Expand All @@ -97,7 +124,7 @@ sudo_init() {
# Don't care about non-alphanumeric filenames when doing a specific match
# shellcheck disable=SC2010,SC2086
if ls /usr/lib/pam | grep $Q "pam_tid.so"; then
logn "Configuring sudo authentication using TouchID:"
logn_no_sudo "Configuring sudo authentication using TouchID:"
if [[ -f /etc/pam.d/sudo_local || -f /etc/pam.d/sudo_local.template ]]; then
# New in macOS Sonoma, survives updates.
PAM_FILE="/etc/pam.d/sudo_local"
Expand Down Expand Up @@ -161,28 +188,17 @@ sudo_refresh() {
reset_debug
}

abort() {
STRAP_STEP=""
echo "!!! $*" >&2
exit 1
}
log() {
STRAP_STEP="$*"
sudo_refresh
echo "--> $*"
}

logn() {
STRAP_STEP="$*"
sudo_refresh
printf -- "--> %s " "$*"
}
logk() {
STRAP_STEP=""
echo "OK"
}
escape() {
printf '%s' "${1//\'/\'}"
}

# Given a list of scripts in the dotfiles repo, run the first one that exists
run_dotfile_scripts() {
Expand All @@ -191,7 +207,7 @@ run_dotfile_scripts() {
cd ~/.dotfiles
for i in "$@"; do
if [ -f "$i" ] && [ -x "$i" ]; then
log "Running dotfiles $i:"
log_no_sudo "Running dotfiles $i:"
if [ "$STRAP_DEBUG" -eq 0 ]; then
"$i" 2>/dev/null
else
Expand All @@ -207,7 +223,7 @@ run_dotfile_scripts() {
[ "$USER" = "root" ] && abort "Run bootstrap.sh as yourself, not root."

# shellcheck disable=SC2086
if [ "$MACOS" -gt 0 ]; then
if [ "$MACOS" -gt 0 ] && [ "$STRAP_ADMIN" -gt 0 ]; then
[ "$STRAP_CI" -eq 0 ] && caffeinate -s -w $$ &
groups | grep $Q -E "\b(admin)\b" || abort "Add $USER to admin."
logn "Configuring security settings:"
Expand All @@ -234,23 +250,21 @@ if [ "$MACOS" -gt 0 ]; then
fi

# Check for and enable full-disk encryption
logn "Checking full-disk encryption status:"
VAULT_MSG="FileVault is (On|Off, but will be enabled after the next restart)."
# shellcheck disable=SC2086
if fdesetup status | grep $Q -E "$VAULT_MSG"; then
logk
elif [ "$MACOS" -eq 0 ] || [ "$STRAP_CI" -gt 0 ]; then
echo
logn "Skipping full-disk encryption."
elif [ "$STRAP_INTERACTIVE" -gt 0 ]; then
echo
log "Enabling full-disk encryption on next reboot:"
sudo_askpass fdesetup enable -user "$USER" |
tee ~/Desktop/"FileVault Recovery Key.txt"
logk
else
echo
abort "Run 'sudo fdesetup enable -user \"$USER\"' for full-disk encryption."
if [ "$MACOS" -eq 0 ] || [ "$STRAP_ADMIN" -eq 0 ] || [ "$STRAP_CI" -gt 0 ]; then
logskip "Skipping full-disk encryption."
elif [ "$MACOS" -gt 0 ] && [ "$STRAP_ADMIN" -gt 0 ]; then
logn "Checking full-disk encryption status:"
VAULT_MSG="FileVault is (On|Off, but will be enabled after the next restart)."
# shellcheck disable=SC2086
if fdesetup status | grep $Q -E "$VAULT_MSG"; then
logk
elif sudo_askpass fdesetup enable -user "$USER" |
tee ~/Desktop/"FileVault Recovery Key.txt"; then
log "Full-disk encryption will be enabled after next reboot:"
logk
else
abort "Run 'sudo fdesetup enable -user \"$USER\"' for full-disk encryption."
fi
fi

# Set up Xcode Command Line Tools
Expand Down Expand Up @@ -295,15 +309,34 @@ check_xcode_license() {
fi
}

if [ "$MACOS" -gt 0 ]; then
check_software_updates() {
log "Checking for software updates:"
# shellcheck disable=SC2086
if softwareupdate -l 2>&1 | grep $Q "No new software available."; then
logk
else
if [ "$MACOS" -gt 0 ] && [ "$STRAP_CI" -eq 0 ]; then
echo
log "Installing software updates:"
sudo_askpass softwareupdate --install --all
check_xcode_license
else
logskip "Skipping software updates."
fi
logk
fi
}

if [ "$MACOS" -gt 0 ] && [ "$STRAP_ADMIN" -gt 0 ]; then
install_xcode_clt
check_xcode_license
check_software_updates
else
log "Not macOS. Xcode CLT install and license check skipped."
logskip "Xcode Command-Line Tools install and license check skipped."
fi

configure_git() {
logn "Configuring Git:"
log_no_sudo "Configuring Git:"
if [ "$STRAP_CI" -gt 0 ]; then
git config --global commit.gpgsign false
git config --global gpg.format openpgp
Expand Down Expand Up @@ -342,34 +375,17 @@ configure_git() {
# The first call to `configure_git` is needed for cloning the dotfiles repo.
configure_git

# Check for and install any remaining software updates
logn "Checking for software updates:"
# shellcheck disable=SC2086
if softwareupdate -l 2>&1 | grep $Q "No new software available."; then
logk
else
if [ "$MACOS" -gt 0 ] && [ "$STRAP_CI" -eq 0 ]; then
echo
log "Installing software updates:"
sudo_askpass softwareupdate --install --all
check_xcode_license
else
log "Skipping software updates."
fi
logk
fi

# Set up dotfiles
# shellcheck disable=SC2086
if [ ! -d "$HOME/.dotfiles" ]; then
if [ -z "$STRAP_DOTFILES_URL" ] || [ -z "$STRAP_DOTFILES_BRANCH" ]; then
abort "Please set STRAP_DOTFILES_URL and STRAP_DOTFILES_BRANCH."
fi
log "Cloning $STRAP_DOTFILES_URL to ~/.dotfiles."
log_no_sudo "Cloning $STRAP_DOTFILES_URL to ~/.dotfiles."
git clone $Q "$STRAP_DOTFILES_URL" ~/.dotfiles
fi
strap_dotfiles_branch_name="${STRAP_DOTFILES_BRANCH##*/}"
log "Checking out $strap_dotfiles_branch_name in ~/.dotfiles."
log_no_sudo "Checking out $strap_dotfiles_branch_name in ~/.dotfiles."
# shellcheck disable=SC2086
(
cd ~/.dotfiles
Expand Down Expand Up @@ -423,7 +439,7 @@ install_homebrew() {

set_up_brew_skips() {
local brewfile_path casks ci_skips mas_ids mas_prefix
log "Setting up Homebrew Bundle formula installs to skip."
log_no_sudo "Setting up Homebrew Bundle formula installs to skip."
ci_skips="awscli black jupyterlab mkvtoolnix zsh-completions"
[ "$STRAP_CI" -gt 0 ] && HOMEBREW_BUNDLE_BREW_SKIP="$ci_skips"
if [ -f "$HOME/.Brewfile" ]; then
Expand All @@ -433,26 +449,26 @@ set_up_brew_skips() {
else
abort "No Brewfile found"
fi
log "Setting up Homebrew Bundle cask installs to skip."
log_no_sudo "Setting up Homebrew Bundle cask installs to skip."
if [ "$MACOS" -gt 0 ] && [ "$brewfile_path" == "$HOME/.Brewfile" ]; then
casks="$(brew bundle list --global --cask --quiet | tr '\n' ' ')"
elif [ "$MACOS" -gt 0 ] && [ "$brewfile_path" == "Brewfile" ]; then
casks="$(brew bundle list --cask --quiet | tr '\n' ' ')"
else
log "Cask commands are only supported on macOS."
log_no_sudo "Cask commands are only supported on macOS."
fi
HOMEBREW_BUNDLE_CASK_SKIP="${casks%% }"
log "Setting up Homebrew Bundle Mac App Store (mas) installs to skip."
log_no_sudo "Setting up Homebrew Bundle Mac App Store (mas) installs to skip."
mas_ids=""
mas_prefix='*mas*, id: '
while read -r brewfile_line; do
# shellcheck disable=SC2295
[[ $brewfile_line == *$mas_prefix* ]] && mas_ids+="${brewfile_line##$mas_prefix} "
done <"$brewfile_path"
HOMEBREW_BUNDLE_MAS_SKIP="${mas_ids%% }"
log "HOMEBREW_BUNDLE_BREW_SKIP='$HOMEBREW_BUNDLE_BREW_SKIP'"
log "HOMEBREW_BUNDLE_CASK_SKIP='$HOMEBREW_BUNDLE_CASK_SKIP'"
log "HOMEBREW_BUNDLE_MAS_SKIP='$HOMEBREW_BUNDLE_MAS_SKIP'"
log_no_sudo "HOMEBREW_BUNDLE_BREW_SKIP='$HOMEBREW_BUNDLE_BREW_SKIP'"
log_no_sudo "HOMEBREW_BUNDLE_CASK_SKIP='$HOMEBREW_BUNDLE_CASK_SKIP'"
log_no_sudo "HOMEBREW_BUNDLE_MAS_SKIP='$HOMEBREW_BUNDLE_MAS_SKIP'"
export HOMEBREW_BUNDLE_BREW_SKIP="$HOMEBREW_BUNDLE_BREW_SKIP"
export HOMEBREW_BUNDLE_CASK_SKIP="$HOMEBREW_BUNDLE_CASK_SKIP"
export HOMEBREW_BUNDLE_MAS_SKIP="$HOMEBREW_BUNDLE_MAS_SKIP"
Expand Down Expand Up @@ -505,15 +521,28 @@ run_brew_installs() {
fi
}

# Install Homebrew: https://docs.brew.sh/Installation
script_url="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"
NONINTERACTIVE=$STRAP_CI \
/usr/bin/env bash -c "$(curl -fsSL $script_url)" || install_homebrew

# Set up Homebrew on Linux: https://docs.brew.sh/Homebrew-on-Linux
[ "$LINUX" -gt 0 ] && run_dotfile_scripts scripts/linuxbrew.sh

run_brew_installs || abort "Homebrew installs were not successful."
# Install Homebrew
# https://docs.brew.sh/Installation
# https://docs.brew.sh/Homebrew-on-Linux
# Homebrew installs require `sudo`, but not necessarily admin
# https://docs.brew.sh/FAQ#why-does-homebrew-say-sudo-is-bad
# https://github.com/Homebrew/install/issues/312
# https://github.com/Homebrew/install/pull/315/files
if [ -z "$SUDO_ASKPASS" ]; then
log_no_sudo "No sudo access. Homebrew installation requires sudo."
else
if [ "$MACOS" -gt 0 ]; then
script_url="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"
NONINTERACTIVE=$STRAP_CI \
/usr/bin/env bash -c "$(curl -fsSL $script_url)" || install_homebrew
logk
elif [ "$LINUX" -gt 0 ]; then
# Set up Homebrew on Linux: https://docs.brew.sh/Homebrew-on-Linux
run_dotfile_scripts scripts/linuxbrew.sh
logk
fi
run_brew_installs || abort "Homebrew installs were not successful."
fi

run_dotfile_scripts scripts/strap-after-setup.sh

Expand Down

0 comments on commit f9bcf59

Please sign in to comment.