View / Edit on GitHub: scripts/provision.sh
Quick Start Provision Script
Main entry point for Install Doctor that ensures Homebrew and a few dependencies are installed before cloning the repository and running Chezmoi.
Overview
This script ensures Homebrew is installed and then installs a few dependencies that Install Doctor relies on. After setting up the minimal amount of changes required, it clones the Install Doctor repository (which you can customize the location of so you can use your own fork). It then proceeds by handing things over to Chezmoi which handles the dotfile application and synchronous scripts. Task is used in conjunction with Chezmoi to boost the performance in some spots by introducing asynchronous features.
Note: https://install.doctor/start
points to this file.
Dependencies
The chart below shows the dependencies we rely on to get Install Doctor going. The dependencies that are bolded are mandatory. The ones that are not bolded are conditionally installed only if they are required.
Dependency | Description |
---|---|
Chezmoi | Dotfile configuration manager (on-device provisioning) |
Task | Task runner used on-device for task parallelization and dependency management |
ZX / Node.js | ZX is a Node.js abstraction that allows for better scripts |
Gum | Gum is a terminal UI prompt CLI (which allows sweet, interactive prompts) |
Glow | Glow is a markdown renderer used for applying terminal-friendly styles to markdown |
There are also a handful of system packages that are installed like curl
and git
. Then, during the Chezmoi provisioning
process, there are a handful of system packages that are installed to ensure things run smoothly. You can find more details
about these extra system packages by browsing through the home/.chezmoiscripts/${DISTRO_ID}/
folder and other applicable
folders (e.g. universal
).
Although Install Doctor comes with presets that install a whole gigantic amount of software, it can actually be quite good at provisioning minimal server environments where you want to keep the binaries to a minimum.
Variables
Specify certain environment variables to customize the behavior of Install Doctor. With the right combination of environment variables, this script can be run completely headlessly. This allows us to do things like test our provisioning script on a wide variety of operating systems.
Variable | Description |
---|---|
START_REPO (or REPO ) | Variable to specify the Git fork to use when provisioning |
ANSIBLE_PROVISION_VM | For Qubes, determines the name of the VM used to provision the system |
DEBUG_MODE (or DEBUG ) | Set to true to enable verbose logging |
For a full list of variables you can use to customize Install Doctor, check out our Customization and Secrets documentation.
Links
Install Doctor homepage Install Doctor documentation portal (includes tips, tricks, and guides on how to customize the system to your liking)
Script Functions
logg
setEnvironmentVariables
ensureBasicDeps
fixHomebrewSharePermissions
ensureHomebrew
handleRequiredReboot
printFullDiskAccessNotice
ensureFullDiskAccess
importCloudFlareCert
setCIEnvironmentVariables
ensureWarpDisconnected
setupPasswordlessSudo
ensureSysWhonix
ensureDom0Updated
ensureSysWhonixRunning
ensureTemplateVMsUpdated
ensureProvisioningVMPermissions
createAndInitProvisionVM
runStartScriptInProvisionVM
handleQubesDom0
installBrewPackage
ensureHomebrewDeps
cloneChezmoiSourceRepo
initChezmoiAndPrompt
beforeRebootDarwin
runChezmoi
removePasswordlessSudo
postProvision
provisionLogic
logg
This function logs with style using Gum if it is installed, otherwise it uses echo
. It is also capable of leveraging Glow to render markdown.
When Glow is not installed, it uses cat
. The following sub-commands are available:
Sub-Command | Description |
---|---|
error | Logs a bright red error message |
info | Logs a regular informational message |
md | Tries to render the specified file using glow if it is installed and uses cat as a fallback |
prompt | Alternative that logs a message intended to describe an upcoming user input prompt |
star | Alternative that logs a message that starts with a star icon |
start | Same as success |
success | Logs a success message that starts with green checkmark |
warn | Logs a bright yellow warning message |
setEnvironmentVariables
Ensure Ubuntu / Debian run in noninteractive
mode. Detect START_REPO
format and determine appropriate git address,
otherwise use the master Install Doctor branch
ensureBasicDeps
This function ensures dependencies like git
and curl
are installed. More specifically, this function will:
- Check if
curl
,git
,expect
,rsync
, andunbuffer
are on the system - If any of the above are missing, it will then use the appropriate system package manager to satisfy the requirements. Note that some of the requirements are not scanned for in order to keep it simple and fast.
- On macOS, the official Xcode Command Line Tools are installed.
fixHomebrewSharePermissions
This function removes group write permissions from the Homebrew share folder which is required for the ZSH configuration.
ensureHomebrew
This function ensures Homebrew is installed and available in the PATH
. It handles the installation of Homebrew on both Linux and macOS.
It will attempt to bypass sudo password entry if it detects that it can do so. The function also has some error handling in regards to various
directories falling out of the correct ownership and permission states. Finally, it loads Homebrew into the active profile (allowing other parts of the script
to use the brew
command).
With Homebrew installed and available, the script finishes by installing the gcc
Homebrew package which is a very common dependency.
handleRequiredReboot
This function determines whether or not a reboot is required on the target system.
On Linux, it will check for the presence of the /var/run/reboot-required
file to determine
whether or not a reboot is required. On macOS, it will reboot /Library/Updates/index.plist
to determine whether or not a reboot is required.
After determining whether or not a reboot is required, the script will attempt to automatically reboot the machine.
printFullDiskAccessNotice
Prints information describing why full disk access is required for the script to run on macOS.
ensureFullDiskAccess
This script ensures the terminal running the provisioning process has full disk access permissions. It also prints information regarding the process of how to enable the permission as well as information related to the specific reasons that the terminal needs full disk access. More specifically, the scripts need full disk access to modify various system files and permissions.
Ensures the terminal running the provisioning process script has full disk access on macOS. It does this by attempting to read a file that requires full disk access. If it does not, the program opens the preferences pane where the user can grant access so that the script can continue.
Links
importCloudFlareCert
Applies changes that require input from the user such as using Touch ID on macOS when importing certificates into the system keychain.
- Ensures CloudFlare Teams certificate is imported into the system keychain
setCIEnvironmentVariables
Load default settings if it is in a CI setting
ensureWarpDisconnected
Disconnect from WARP, if connected
setupPasswordlessSudo
Notify user that they can press CTRL+C to prevent /etc/sudoers
from being modified (which is currently required for headless installs on some systems).
Additionally, this function will add the current user to /etc/sudoers
so that headless automation is possible.
ensureSysWhonix
Ensure sys-whonix is configured (for Qubes dom0)
ensureDom0Updated
Ensure dom0 is updated
ensureSysWhonixRunning
Ensure sys-whonix is running
ensureTemplateVMsUpdated
Ensure TemplateVMs are updated
ensureProvisioningVMPermissions
Ensure provisioning VM can run commands on any VM
createAndInitProvisionVM
Create provisioning VM and initialize the provisioning process from there
runStartScriptInProvisionVM
Restart the provisioning process with the same script but from the provisioning VM
handleQubesDom0
Perform Qubes dom0 specific logic like updating system packages, setting up the Tor VM, updating TemplateVMs, and beginning the provisioning process using Ansible and an AppVM used to handle the provisioning process
installBrewPackage
Helper function used by [[ensureHomebrewDeps]] to ensure a Homebrew package is installed after first checking if it is already available on the system.
ensureHomebrewDeps
Installs various dependencies using Homebrew.
- Ensures Glow, Gum, Chezmoi, Node.js, and ZX are installed.
- If the system is macOS, then also install
gsed
andcoreutils
.
cloneChezmoiSourceRepo
Ensure the ${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi
directory is cloned and up-to-date using the previously
set START_REPO
as the source repository.
initChezmoiAndPrompt
Guide the user through the initial setup by showing TUI introduction and accepting input through various prompts.
- Show
chezmoi-intro.md
withglow
- Prompt for the software group if the
SOFTWARE_GROUP
variable is not defined - Run
chezmoi init
when the Chezmoi configuration is missing (i.e.${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/chezmoi.yaml
)
beforeRebootDarwin
When a reboot is triggered by softwareupdate on macOS, other utilities that require a reboot are also installed to save on reboots.
runChezmoi
Save the log of the provision process to $HOME/.local/var/log/install.doctor/install.doctor.$(date +%s).log
and add the Chezmoi
--force
flag if the HEADLESS_INSTALL
variable is set to true
.
removePasswordlessSudo
Ensure temporary passwordless sudo privileges are removed from /etc/sudoers
postProvision
Render the docs/terminal/post-install.md
file to the terminal at the end of the provisioning process
provisionLogic
The provisionLogic
function is used to define the order of the script. All of the functions it relies on are defined
above.
Source Code
#!/usr/bin/env bash
# @file Quick Start Provision Script
# @brief Main entry point for Install Doctor that ensures Homebrew and a few dependencies are installed before cloning the repository and running Chezmoi.
# @description
# This script ensures Homebrew is installed and then installs a few dependencies that Install Doctor relies on.
# After setting up the minimal amount of changes required, it clones the Install Doctor repository (which you
# can customize the location of so you can use your own fork). It then proceeds by handing things over to
# Chezmoi which handles the dotfile application and synchronous scripts. Task is used in conjunction with
# Chezmoi to boost the performance in some spots by introducing asynchronous features.
#
# **Note**: `https://install.doctor/start` points to this file.
#
# ## Dependencies
#
# The chart below shows the dependencies we rely on to get Install Doctor going. The dependencies that are bolded
# are mandatory. The ones that are not bolded are conditionally installed only if they are required.
#
# | Dependency | Description |
# |--------------------|--------------------------------------------------------------------------------------|
# | **Chezmoi** | Dotfile configuration manager (on-device provisioning) |
# | **Task** | Task runner used on-device for task parallelization and dependency management |
# | **ZX / Node.js** | ZX is a Node.js abstraction that allows for better scripts |
# | Gum | Gum is a terminal UI prompt CLI (which allows sweet, interactive prompts) |
# | Glow | Glow is a markdown renderer used for applying terminal-friendly styles to markdown |
#
# There are also a handful of system packages that are installed like `curl` and `git`. Then, during the Chezmoi provisioning
# process, there are a handful of system packages that are installed to ensure things run smoothly. You can find more details
# about these extra system packages by browsing through the `home/.chezmoiscripts/${DISTRO_ID}/` folder and other applicable
# folders (e.g. `universal`).
#
# Although Install Doctor comes with presets that install a whole gigantic amount of software, it can actually
# be quite good at provisioning minimal server environments where you want to keep the binaries to a minimum.
#
# ## Variables
#
# Specify certain environment variables to customize the behavior of Install Doctor. With the right combination of
# environment variables, this script can be run completely headlessly. This allows us to do things like test our
# provisioning script on a wide variety of operating systems.
#
# | Variable | Description |
# |---------------------------|-----------------------------------------------------------------------------------|
# | `START_REPO` (or `REPO`) | Variable to specify the Git fork to use when provisioning |
# | `ANSIBLE_PROVISION_VM` | **For Qubes**, determines the name of the VM used to provision the system |
# | `DEBUG_MODE` (or `DEBUG`) | Set to true to enable verbose logging |
#
# For a full list of variables you can use to customize Install Doctor, check out our [Customization](https://install.doctor/docs/customization)
# and [Secrets](https://install.doctor/docs/customization/secrets) documentation.
#
# ## Links
#
# [Install Doctor homepage](https://install.doctor)
# [Install Doctor documentation portal](https://install.doctor/docs) (includes tips, tricks, and guides on how to customize the system to your liking)
# @description This function logs with style using Gum if it is installed, otherwise it uses `echo`. It is also capable of leveraging Glow to render markdown.
# When Glow is not installed, it uses `cat`. The following sub-commands are available:
#
# | Sub-Command | Description |
# |-------------|-----------------------------------------------------------------------------------------------------|
# | `error` | Logs a bright red error message |
# | `info` | Logs a regular informational message |
# | `md` | Tries to render the specified file using `glow` if it is installed and uses `cat` as a fallback |
# | `prompt` | Alternative that logs a message intended to describe an upcoming user input prompt |
# | `star` | Alternative that logs a message that starts with a star icon |
# | `start` | Same as `success` |
# | `success` | Logs a success message that starts with green checkmark |
# | `warn` | Logs a bright yellow warning message |
logg() {
TYPE="$1"
MSG="$2"
if [ "$TYPE" == 'error' ]; then
if command -v gum > /dev/null; then
gum style --border="thick" "$(gum style --foreground="#ff0000" "โ") $(gum style --bold --background="#ff0000" --foreground="#ffffff" " ERROR ") $(gum style --bold "$MSG")"
else
echo "ERROR: $MSG"
fi
elif [ "$TYPE" == 'info' ]; then
if command -v gum > /dev/null; then
gum style " $(gum style --foreground="#00ffff" "โ") $(gum style --faint "$MSG")"
else
echo "INFO: $MSG"
fi
elif [ "$TYPE" == 'md' ]; then
if command -v glow > /dev/null; then
glow "$MSG"
else
cat "$MSG"
fi
elif [ "$TYPE" == 'prompt' ]; then
if command -v gum > /dev/null; then
gum style " $(gum style --foreground="#00008b" "โถ") $(gum style --bold "$MSG")"
else
echo "PROMPT: $MSG"
fi
elif [ "$TYPE" == 'star' ]; then
if command -v gum > /dev/null; then
gum style " $(gum style --foreground="#d1d100" "โ") $(gum style --bold "$MSG")"
else
echo "STAR: $MSG"
fi
elif [ "$TYPE" == 'start' ]; then
if command -v gum > /dev/null; then
gum style " $(gum style --foreground="#00ff00" "โถ") $(gum style --bold "$MSG")"
else
echo "START: $MSG"
fi
elif [ "$TYPE" == 'success' ]; then
if command -v gum > /dev/null; then
gum style " $(gum style --foreground="#00ff00" "โ") $(gum style --bold "$MSG")"
else
echo "SUCCESS: $MSG"
fi
elif [ "$TYPE" == 'warn' ]; then
if command -v gum > /dev/null; then
gum style " $(gum style --foreground="#d1d100" "โ") $(gum style --bold --background="#ffff00" --foreground="#000000" " WARNING ") $(gum style --bold "$MSG")"
else
echo "WARNING: $MSG"
fi
else
if command -v gum > /dev/null; then
gum style " $(gum style --foreground="#00ff00" "โถ") $(gum style --bold "$TYPE")"
else
echo "$MSG"
fi
fi
}
# @description Ensure Ubuntu / Debian run in `noninteractive` mode. Detect `START_REPO` format and determine appropriate git address,
# otherwise use the master Install Doctor branch
setEnvironmentVariables() {
export DEBIAN_FRONTEND=noninteractive
export HOMEBREW_NO_ENV_HINTS=true
if [ -z "$START_REPO" ] && [ -z "$REPO" ]; then
export START_REPO="https://github.com/megabyte-labs/install.doctor.git"
else
if [ -n "$REPO" ] && [ -z "$START_REPO" ]; then
export START_REPO="$REPO"
fi
if [[ "$START_REPO" == *"/"* ]]; then
# Either full git address or GitHubUser/RepoName
if [[ "$START_REPO" == *":"* ]] || [[ "$START_REPO" == *"//"* ]]; then
export START_REPO="$START_REPO"
else
export START_REPO="https://github.com/${START_REPO}.git"
fi
else
export START_REPO="https://github.com/$START_REPO/install.doctor.git"
fi
fi
}
# @description This function ensures dependencies like `git` and `curl` are installed. More specifically, this function will:
#
# 1. Check if `curl`, `git`, `expect`, `rsync`, and `unbuffer` are on the system
# 2. If any of the above are missing, it will then use the appropriate system package manager to satisfy the requirements. *Note that some of the requirements are not scanned for in order to keep it simple and fast.*
# 3. On macOS, the official Xcode Command Line Tools are installed.
ensureBasicDeps() {
if ! command -v curl > /dev/null || ! command -v git > /dev/null || ! command -v expect > /dev/null || ! command -v rsync > /dev/null || ! command -v unbuffer > /dev/null; then
if command -v apt-get > /dev/null; then
### Debian / Ubuntu
logg info 'Running sudo apt-get update' && sudo apt-get update
logg info 'Running sudo apt-get install -y build-essential curl expect git moreutils rsync procps file' && sudo apt-get install -y build-essential curl expect git moreutils rsync procps file
elif command -v dnf > /dev/null; then
### Fedora
logg info 'Running sudo dnf groupinstall -y "Development Tools"' && sudo dnf groupinstall -y 'Development Tools'
logg info 'Running sudo dnf install -y curl expect git moreutils rsync procps-ng file' && sudo dnf install -y curl expect git moreutils rsync procps-ng file
elif command -v yum > /dev/null; then
### CentOS
logg info 'Running sudo yum groupinstall -y "Development Tools"' && sudo yum groupinstall -y 'Development Tools'
logg info 'Running sudo yum install -y curl expect git moreutils rsync procps-ng file' && sudo yum install -y curl expect git moreutils rsync procps-ng file
elif command -v pacman > /dev/null; then
### Archlinux
logg info 'Running sudo pacman update' && sudo pacman update
logg info 'Running sudo pacman -Syu base-devel curl expect git moreutils rsync procps-ng file' && sudo pacman -Syu base-devel curl expect git moreutils rsync procps-ng file
elif command -v zypper > /dev/null; then
### OpenSUSE
logg info 'Running sudo zypper install -yt pattern devel_basis' && sudo zypper install -yt pattern devel_basis
logg info 'Running sudo zypper install -y curl expect git moreutils rsync procps file' && sudo zypper install -y curl expect git moreutils rsync procps file
elif command -v apk > /dev/null; then
### Alpine
logg info 'Running sudo apk add build-base curl expect git moreutils rsync ruby procps file' && sudo apk add build-base curl expect git moreutils rsync ruby procps file
elif [ -d /Applications ] && [ -d /Library ]; then
### macOS
logg info "Ensuring Xcode Command Line Tools are installed.."
if ! xcode-select -p >/dev/null 2>&1; then
logg info "Command Line Tools for Xcode not found"
### This temporary file prompts the 'softwareupdate' utility to list the Command Line Tools
touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress;
XCODE_PKG="$(softwareupdate -l | grep "\*.*Command Line" | tail -n 1 | sed 's/^[^C]* //')"
logg info "Installing from softwareupdate" && softwareupdate -i "$XCODE_PKG" && logg success "Successfully installed $XCODE_PKG"
fi
elif [[ "$OSTYPE" == 'cygwin' ]] || [[ "$OSTYPE" == 'msys' ]] || [[ "$OSTYPE" == 'win32' ]]; then
### Windows
logg info 'Running choco install -y curl expect git moreutils rsync' && choco install -y curl expect git moreutils rsync
elif command -v nix-env > /dev/null; then
### NixOS
logg warn "TODO - Add support for NixOS"
elif [[ "$OSTYPE" == 'freebsd'* ]]; then
### FreeBSD
logg warn "TODO - Add support for FreeBSD"
elif command -v pkg > /dev/null; then
### Termux
logg warn "TODO - Add support for Termux"
elif command -v xbps-install > /dev/null; then
### Void
logg warn "TODO - Add support for Void"
fi
fi
}
### Ensure Homebrew is loaded
loadHomebrew() {
if ! command -v brew > /dev/null; then
if [ -f /usr/local/bin/brew ]; then
logg info "Using /usr/local/bin/brew" && eval "$(/usr/local/bin/brew shellenv)"
elif [ -f "${HOMEBREW_PREFIX:-/opt/homebrew}/bin/brew" ]; then
logg info "Using ${HOMEBREW_PREFIX:-/opt/homebrew}/bin/brew" && eval "$("${HOMEBREW_PREFIX:-/opt/homebrew}/bin/brew" shellenv)"
elif [ -d "$HOME/.linuxbrew" ]; then
logg info "Using $HOME/.linuxbrew/bin/brew" && eval "$("$HOME/.linuxbrew/bin/brew" shellenv)"
elif [ -d "/home/linuxbrew/.linuxbrew" ]; then
logg info 'Using /home/linuxbrew/.linuxbrew/bin/brew' && eval "(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
else
logg info 'Could not find Homebrew installation'
fi
fi
}
### Ensures Homebrew folders have proper owners / permissions
fixHomebrewPermissions() {
if command -v brew > /dev/null; then
logg info 'Applying proper permissions on Homebrew folders'
sudo chmod -R go-w "$(brew --prefix)/share"
BREW_DIRS="share etc/bash_completion.d"
for BREW_DIR in $BREW_DIRS; do
if [ -d "$(brew --prefix)/$BREW_DIR" ]; then
sudo chown -Rf "$(whoami)" "$(brew --prefix)/$BREW_DIR"
fi
done
logg info 'Running brew update --force --quiet' && brew update --force --quiet
fi
}
# @description This function removes group write permissions from the Homebrew share folder which
# is required for the ZSH configuration.
fixHomebrewSharePermissions() {
if [ -f /usr/local/bin/brew ]; then
sudo chmod -R g-w /usr/local/share
elif [ -f "${HOMEBREW_PREFIX:-/opt/homebrew}/bin/brew" ]; then
sudo chmod -R g-w "${HOMEBREW_PREFIX:-/opt/homebrew}/share"
elif [ -d "$HOME/.linuxbrew" ]; then
sudo chmod -R g-w "$HOME/.linuxbrew/share"
elif [ -d "/home/linuxbrew/.linuxbrew" ]; then
sudo chmod -R g-w /home/linuxbrew/.linuxbrew/share
fi
}
### Installs Homebrew
ensurePackageManagerHomebrew() {
if ! command -v brew > /dev/null; then
### Select install type based off of whether or not sudo privileges are available
if command -v sudo > /dev/null && sudo -n true; then
logg info 'Installing Homebrew. Sudo privileges available.'
echo | bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || BREW_EXIT_CODE="$?"
fixHomebrewSharePermissions
else
logg info 'Installing Homebrew. Sudo privileges not available. Password may be required.'
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || BREW_EXIT_CODE="$?"
fixHomebrewSharePermissions
fi
### Attempt to fix problematic installs
if [ -n "$BREW_EXIT_CODE" ]; then
logg warn 'Homebrew was installed but part of the installation failed to complete successfully.'
fixHomebrewPermissions
fi
fi
}
### Ensures gcc is installed
ensureGcc() {
if command -v brew > /dev/null; then
if ! brew list | grep gcc > /dev/null; then
logg info 'Installing Homebrew gcc' && brew install --quiet gcc
else
logg info 'Homebrew gcc is available'
fi
else
logg error 'Failed to initialize Homebrew' && exit 1
fi
}
# @description This function ensures Homebrew is installed and available in the `PATH`. It handles the installation of Homebrew on both **Linux and macOS**.
# It will attempt to bypass sudo password entry if it detects that it can do so. The function also has some error handling in regards to various
# directories falling out of the correct ownership and permission states. Finally, it loads Homebrew into the active profile (allowing other parts of the script
# to use the `brew` command).
#
# With Homebrew installed and available, the script finishes by installing the `gcc` Homebrew package which is a very common dependency.
ensureHomebrew() {
loadHomebrew
ensurePackageManagerHomebrew
loadHomebrew
ensureGcc
}
# @description This function determines whether or not a reboot is required on the target system.
# On Linux, it will check for the presence of the `/var/run/reboot-required` file to determine
# whether or not a reboot is required. On macOS, it will reboot `/Library/Updates/index.plist`
# to determine whether or not a reboot is required.
#
# After determining whether or not a reboot is required, the script will attempt to automatically
# reboot the machine.
handleRequiredReboot() {
if [ -d /Applications ] && [ -d /System ]; then
### macOS
if ! defaults read /Library/Updates/index.plist InstallAtLogout 2>&1 | grep 'does not exist' > /dev/null; then
logg info 'There appears to be an update that requires a reboot'
logg info 'Attempting to reboot gracefully' && osascript -e 'tell application "Finder" to shut down'
fi
elif [ -f /var/run/reboot-required ]; then
### Linux
logg info '/var/run/reboot-required is present so a reboot is required'
if command -v systemctl > /dev/null; then
logg info 'systemctl present so rebooting with sudo systemctl start reboot.target' && sudo systemctl start reboot.target
elif command -v reboot > /dev/null; then
logg info 'reboot available as command so rebooting with sudo reboot' && sudo reboot
elif command -v shutdown > /dev/null; then
logg info 'shutdown command available so rebooting with sudo shutdown -r now' && sudo shutdown -r now
else
logg warn 'Reboot required but unable to determine appropriate restart command'
fi
fi
}
# @description Prints information describing why full disk access is required for the script to run on macOS.
printFullDiskAccessNotice() {
if [ -d /Applications ] && [ -d /System ]; then
logg md "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi/docs/terminal/full-disk-access.md"
fi
}
# @description
# This script ensures the terminal running the provisioning process has full disk access permissions. It also
# prints information regarding the process of how to enable the permission as well as information related to
# the specific reasons that the terminal needs full disk access. More specifically, the scripts need full
# disk access to modify various system files and permissions.
#
# Ensures the terminal running the provisioning process script has full disk access on macOS. It does this
# by attempting to read a file that requires full disk access. If it does not, the program opens the preferences
# pane where the user can grant access so that the script can continue.
#
# #### Links
#
# * [Detecting Full Disk Access permission on macOS](https://www.dzombak.com/blog/2021/11/macOS-Scripting-How-to-tell-if-the-Terminal-app-has-Full-Disk-Access.html)
ensureFullDiskAccess() {
if [ -d /Applications ] && [ -d /System ]; then
if ! plutil -lint /Library/Preferences/com.apple.TimeMachine.plist > /dev/null ; then
printFullDiskAccessNotice
logg star 'Opening Full Disk Access preference pane.. Grant full-disk access for the terminal you would like to run the provisioning process with.' && open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
logg info 'You may have to force quit the terminal and have it reload.'
if [ ! -f "$HOME/.zshrc" ] || ! cat "$HOME/.zshrc" | grep '# TEMPORARY FOR INSTALL DOCTOR MACOS' > /dev/null; then
echo 'bash <(curl -sSL https://install.doctor/start) # TEMPORARY FOR INSTALL DOCTOR MACOS' >> "$HOME/.zshrc"
fi
exit 0
else
logg success 'Current terminal has full disk access'
if [ -f "$HOME/.zshrc" ]; then
if command -v gsed > /dev/null; then
sudo gsed -i '/# TEMPORARY FOR INSTALL DOCTOR MACOS/d' "$HOME/.zshrc" || logg warn "Failed to remove kickstart script from .zshrc"
else
sudo sed -i '/# TEMPORARY FOR INSTALL DOCTOR MACOS/d' "$HOME/.zshrc" || logg warn "Failed to remove kickstart script from .zshrc"
fi
fi
fi
fi
}
# @description Applies changes that require input from the user such as using Touch ID on macOS when
# importing certificates into the system keychain.
#
# * Ensures CloudFlare Teams certificate is imported into the system keychain
importCloudFlareCert() {
if [ -d /Applications ] && [ -d /System ] && [ -z "$HEADLESS_INSTALL" ]; then
### Acquire certificate
if [ ! -f "$HOME/.local/etc/ssl/cloudflare/Cloudflare_CA.crt" ]; then
logg info 'Downloading Cloudflare_CA.crt from https://developers.cloudflare.com/cloudflare-one/static/documentation/connections/Cloudflare_CA.crt to determine if it is already in the System.keychain'
CRT_TMP="$(mktemp)"
curl -sSL https://developers.cloudflare.com/cloudflare-one/static/documentation/connections/Cloudflare_CA.crt > "$CRT_TMP"
else
CRT_TMP="$HOME/.local/etc/ssl/cloudflare/Cloudflare_CA.crt"
fi
### Validate / import certificate
security verify-cert -c "$CRT_TMP" > /dev/null 2>&1
if [ $? != 0 ]; then
logg info '**macOS Manual Security Permission** Requesting security authorization for Cloudflare trusted certificate'
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$CRT_TMP" && logg success 'Successfully imported Cloudflare_CA.crt into System.keychain'
fi
### Remove temporary file, if necessary
if [ ! -f "$HOME/.local/etc/ssl/cloudflare/Cloudflare_CA.crt" ]; then
rm -f "$CRT_TMP"
fi
fi
}
# @description Load default settings if it is in a CI setting
setCIEnvironmentVariables() {
if [ -n "$CI" ] || [ -n "$TEST_INSTALL" ]; then
logg info "Automatically setting environment variables since the CI environment variable is defined"
logg info "Setting NO_RESTART to true" && export NO_RESTART=true
logg info "Setting HEADLESS_INSTALL to true " && export HEADLESS_INSTALL=true
logg info "Setting SOFTWARE_GROUP to Full-Desktop" && export SOFTWARE_GROUP="Full-Desktop"
logg info "Setting FULL_NAME to Brian Zalewski" && export FULL_NAME="Brian Zalewski"
logg info "Setting PRIMARY_EMAIL to [email protected]" && export PRIMARY_EMAIL="[email protected]"
logg info "Setting PUBLIC_SERVICES_DOMAIN to lab.megabyte.space" && export PUBLIC_SERVICES_DOMAIN="lab.megabyte.space"
logg info "Setting RESTRICTED_ENVIRONMENT to false" && export RESTRICTED_ENVIRONMENT=false
logg info "Setting WORK_ENVIRONMENT to false" && export WORK_ENVIRONMENT=false
logg info "Setting HOST to $(hostname -s)" && export HOST="$(hostname -s)"
fi
}
# @description Disconnect from WARP, if connected
ensureWarpDisconnected() {
if [ -z "$DEBUG" ]; then
if command -v warp-cli > /dev/null; then
if warp-cli status | grep 'Connected' > /dev/null; then
logg info "Disconnecting from WARP" && warp-cli disconnect && logg success "Disconnected WARP to prevent conflicts"
fi
fi
fi
}
# @description Notify user that they can press CTRL+C to prevent `/etc/sudoers` from being modified (which is currently required for headless installs on some systems).
# Additionally, this function will add the current user to `/etc/sudoers` so that headless automation is possible.
setupPasswordlessSudo() {
sudo -n true || SUDO_EXIT_CODE=$?
logg info 'Your user will temporarily be granted passwordless sudo for the duration of the script'
if [ -n "$SUDO_EXIT_CODE" ] && [ -z "$SUDO_PASSWORD" ] && command -v chezmoi > /dev/null && [ -f "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi/home/.chezmoitemplates/secrets/SUDO_PASSWORD" ] && [ -f "${XDG_CONFIG_HOME:-$HOME/.config}/age/chezmoi.txt" ]; then
logg info "Acquiring SUDO_PASSWORD by using Chezmoi to decrypt ${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi/home/.chezmoitemplates/secrets/SUDO_PASSWORD"
SUDO_PASSWORD="$(cat "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi/home/.chezmoitemplates/secrets/SUDO_PASSWORD" | chezmoi decrypt)"
export SUDO_PASSWORD
fi
if [ -n "$SUDO_PASSWORD" ]; then
logg info 'Using the acquired sudo password to automatically grant the user passwordless sudo privileges for the duration of the script'
echo "$SUDO_PASSWORD" | sudo -S sh -c "echo '$(whoami) ALL=(ALL:ALL) NOPASSWD: ALL # TEMPORARY FOR INSTALL DOCTOR' | sudo -S tee -a /etc/sudoers > /dev/null"
echo ""
# Old method below does not work on macOS due to multiple sudo prompts
# printf '%s\n%s\n' "$SUDO_PASSWORD" | sudo -S echo "$(whoami) ALL=(ALL:ALL) NOPASSWD: ALL # TEMPORARY FOR INSTALL DOCTOR" | sudo -S tee -a /etc/sudoers > /dev/null
else
logg info 'Press CTRL+C to bypass this prompt to either enter your password when needed or perform a non-privileged installation'
logg info 'Note: Non-privileged installations are not yet supported'
echo "$(whoami) ALL=(ALL:ALL) NOPASSWD: ALL # TEMPORARY FOR INSTALL DOCTOR" | sudo tee -a /etc/sudoers > /dev/null
fi
}
# @description Ensure sys-whonix is configured (for Qubes dom0)
ensureSysWhonix() {
CONFIG_WIZARD_COUNT=0
function configureWizard() {
if xwininfo -root -tree | grep "Anon Connection Wizard"; then
WINDOW_ID="$(xwininfo -root -tree | grep "Anon Connection Wizard" | sed 's/^ *\([^ ]*\) .*/\1/')"
xdotool windowactivate "$WINDOW_ID" && sleep 1 && xdotool key 'Enter' && sleep 1 && xdotool key 'Tab Tab Enter' && sleep 24 && xdotool windowactivate "$WINDOW_ID" && sleep 1 && xdotool key 'Enter' && sleep 300
qvm-shutdown --wait sys-whonix
sleep 3
qvm-start sys-whonix
if xwininfo -root -tree | grep "systemcheck | Whonix" > /dev/null; then
WINDOW_ID_SYS_CHECK="$(xwininfo -root -tree | grep "systemcheck | Whonix" | sed 's/^ *\([^ ]*\) .*/\1/')"
if xdotool windowactivate "$WINDOW_ID_SYS_CHECK"; then
sleep 1
xdotool key 'Enter'
fi
fi
else
sleep 3
CONFIG_WIZARD_COUNT=$((CONFIG_WIZARD_COUNT + 1))
if [[ "$CONFIG_WIZARD_COUNT" == '4' ]]; then
echo "The sys-whonix anon-connection-wizard utility did not open."
else
echo "Checking for anon-connection-wizard again.."
configureWizard
fi
fi
}
}
# @description Ensure dom0 is updated
ensureDom0Updated() {
if [ ! -f /root/dom0-updated ]; then
sudo qubesctl --show-output state.sls update.qubes-dom0
sudo qubes-dom0-update --clean -y
touch /root/dom0-updated
fi
}
# @description Ensure sys-whonix is running
ensureSysWhonixRunning() {
if ! qvm-check --running sys-whonix; then
qvm-start sys-whonix --skip-if-running
configureWizard > /dev/null
fi
}
# @description Ensure TemplateVMs are updated
ensureTemplateVMsUpdated() {
if [ ! -f /root/templatevms-updated ]; then
# timeout of 10 minutes is added here because the whonix-gw VM does not like to get updated
# with this method. Anyone know how to fix this?
sudo timeout 600 qubesctl --show-output --skip-dom0 --templates state.sls update.qubes-vm &> /dev/null || true
while read -r RESTART_VM; do
qvm-shutdown --wait "$RESTART_VM"
done< <(qvm-ls --all --no-spinner --fields=name,state | grep Running | grep -v sys-net | grep -v sys-firewall | grep -v sys-whonix | grep -v dom0 | awk '{print $1}')
sudo touch /root/templatevms-updated
fi
}
# @description Ensure provisioning VM can run commands on any VM
ensureProvisioningVMPermissions() {
echo "/bin/bash" | sudo tee /etc/qubes-rpc/qubes.VMShell
sudo chmod 755 /etc/qubes-rpc/qubes.VMShell
echo "${ANSIBLE_PROVISION_VM:=provision}"' dom0 allow' | sudo tee /etc/qubes-rpc/policy/qubes.VMShell
echo "$ANSIBLE_PROVISION_VM"' $anyvm allow' | sudo tee -a /etc/qubes-rpc/policy/qubes.VMShell
sudo chown "$(whoami):$(whoami)" /etc/qubes-rpc/policy/qubes.VMShell
sudo chmod 644 /etc/qubes-rpc/policy/qubes.VMShell
}
# @description Create provisioning VM and initialize the provisioning process from there
createAndInitProvisionVM() {
qvm-create --label red --template debian-11 "$ANSIBLE_PROVISION_VM" &> /dev/null || true
qvm-volume extend "$ANSIBLE_PROVISION_VM:private" "40G"
if [ -f ~/.vaultpass ]; then
qvm-run "$ANSIBLE_PROVISION_VM" 'rm -f ~/QubesIncoming/dom0/.vaultpass'
qvm-copy-to-vm "$ANSIBLE_PROVISION_VM" ~/.vaultpass
qvm-run "$ANSIBLE_PROVISION_VM" 'cp ~/QubesIncoming/dom0/.vaultpass ~/.vaultpass'
fi
}
# @description Restart the provisioning process with the same script but from the provisioning VM
runStartScriptInProvisionVM() {
qvm-run --pass-io "$ANSIBLE_PROVISION_VM" 'curl -sSL https://install.doctor/start > ~/start.sh && bash ~/start.sh'
}
# @description Perform Qubes dom0 specific logic like updating system packages, setting up the Tor VM, updating TemplateVMs, and
# beginning the provisioning process using Ansible and an AppVM used to handle the provisioning process
handleQubesDom0() {
if command -v qubesctl > /dev/null; then
ensureSysWhonix
ensureDom0Updated
ensureSysWhonixRunning
ensureTemplateVMsUpdated
ensureProvisioningVMPermissions
createAndInitProvisionVM
runStartScriptInProvisionVM
exit 0
fi
}
# @description Helper function used by [[ensureHomebrewDeps]] to ensure a Homebrew package is installed after
# first checking if it is already available on the system.
installBrewPackage() {
if ! command -v "$1" > /dev/null; then
logg 'Installing '"$1"''
brew install --quiet "$1"
fi
}
# @description Installs various dependencies using Homebrew.
#
# 1. Ensures Glow, Gum, Chezmoi, Node.js, and ZX are installed.
# 2. If the system is macOS, then also install `gsed` and `coreutils`.
ensureHomebrewDeps() {
### Base dependencies
installBrewPackage "glow"
installBrewPackage "gum"
installBrewPackage "chezmoi"
installBrewPackage "node"
installBrewPackage "zx"
### macOS
if [ -d /Applications ] && [ -d /System ]; then
### gsed
installBrewPackage "gsed"
### unbuffer / expect
if ! command -v unbuffer > /dev/null; then
brew install --quiet expect
fi
### gtimeout / coreutils
if ! command -v gtimeout > /dev/null; then
brew install --quiet coreutils
fi
### ts / moreutils
if ! command -v ts > /dev/null; then
brew install --quiet moreutils
fi
fi
}
# @description Ensure the `${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi` directory is cloned and up-to-date using the previously
# set `START_REPO` as the source repository.
cloneChezmoiSourceRepo() {
if [ -d "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi/.git" ]; then
logg info "Changing directory to ${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi" && cd "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi"
if ! git config --get http.postBuffer > /dev/null; then
logg info 'Setting git http.postBuffer value high for large source repository' && git config http.postBuffer 524288000
fi
logg info "Pulling the latest changes in ${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi" && git pull origin master
else
logg info "Ensuring ${XDG_DATA_HOME:-$HOME/.local/share} is a folder" && mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}"
logg info "Cloning ${START_REPO} to ${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi" && git clone "${START_REPO}" "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi"
logg info "Changing directory to ${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi" && cd "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi"
logg info 'Setting git http.postBuffer value high for large source repository' && git config http.postBuffer 524288000
fi
}
# @description Guide the user through the initial setup by showing TUI introduction and accepting input through various prompts.
#
# 1. Show `chezmoi-intro.md` with `glow`
# 2. Prompt for the software group if the `SOFTWARE_GROUP` variable is not defined
# 3. Run `chezmoi init` when the Chezmoi configuration is missing (i.e. `${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/chezmoi.yaml`)
initChezmoiAndPrompt() {
### Show `chezmoi-intro.md` with `glow`
if command -v glow > /dev/null; then
glow "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi/docs/terminal/chezmoi-intro.md"
fi
### Prompt for the software group if the `SOFTWARE_GROUP` variable is not defined
if command -v gum > /dev/null; then
if [ -z "$SOFTWARE_GROUP" ]; then
# logg prompt 'Select the software group you would like to install. If your environment is a macOS, Windows, or environment with the DISPLAY environment variable then desktop software will be installed too. The software groups are in the '"${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/chezmoi.yaml"' file.'
SOFTWARE_GROUP="Full"
# TODO - Uncomment this when other SOFTWARE_GROUP types are implemented properly
# SOFTWARE_GROUP="$(gum choose "Basic" "Server" "Standard" "Full")"
export SOFTWARE_GROUP
fi
else
logg error 'Woops! Gum needs to be installed for the guided installation. Try running brew install gum' && exit 1
fi
if [ ! -f "${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/chezmoi.yaml" ]; then
### Run `chezmoi init` when the Chezmoi configuration is missing
logg info 'Running chezmoi init since the '"${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/chezmoi.yaml"' is not present'
chezmoi init
fi
}
# @description When a reboot is triggered by softwareupdate on macOS, other utilities that require
# a reboot are also installed to save on reboots.
beforeRebootDarwin() {
logg info "Ensuring macfuse is installed" && brew install --cask --no-quarantine --quiet macfuse
}
# @description Save the log of the provision process to `$HOME/.local/var/log/install.doctor/install.doctor.$(date +%s).log` and add the Chezmoi
# `--force` flag if the `HEADLESS_INSTALL` variable is set to `true`.
runChezmoi() {
### Set up logging
mkdir -p "$HOME/.local/var/log/install.doctor"
LOG_FILE="$HOME/.local/var/log/install.doctor/chezmoi-apply-$(date +%s).log"
### Apply command flags
COMMON_MODIFIERS="--no-pager"
FORCE_MODIFIER=""
if [ -n "$HEADLESS_INSTALL" ]; then
logg info 'Running chezmoi apply forcefully because HEADLESS_INSTALL is set'
FORCE_MODIFIER="--force"
fi
# TODO: https://github.com/twpayne/chezmoi/discussions/3448
KEEP_GOING_MODIFIER="-k"
if [ -n "$KEEP_GOING" ]; then
logg info 'Instructing chezmoi to keep going in the case of errors because KEEP_GOING is set'
KEEP_GOING_MODIFIER="-k"
fi
DEBUG_MODIFIER=""
if [ -n "$DEBUG_MODE" ] || [ -n "$DEBUG" ]; then
logg info "Either DEBUG_MODE or DEBUG environment variables were set so Chezmoi will be run in debug mode"
export DEBUG_MODIFIER="-vvvvv --debug --verbose"
fi
### Run chezmoi apply
if command -v unbuffer > /dev/null; then
if command -v caffeinate > /dev/null; then
logg info "Running: unbuffer -p caffeinate chezmoi apply $COMMON_MODIFIERS $DEBUG_MODIFIER $KEEP_GOING_MODIFIER $FORCE_MODIFIER"
unbuffer -p caffeinate chezmoi apply $COMMON_MODIFIERS $DEBUG_MODIFIER $KEEP_GOING_MODIFIER $FORCE_MODIFIER 2>&1 | tee /dev/tty | ts '[%Y-%m-%d %H:%M:%S]' > "$LOG_FILE" || CHEZMOI_EXIT_CODE=$?
else
logg info "Running: unbuffer -p chezmoi apply $COMMON_MODIFIERS $DEBUG_MODIFIER $KEEP_GOING_MODIFIER $FORCE_MODIFIER"
unbuffer -p chezmoi apply $COMMON_MODIFIERS $DEBUG_MODIFIER $KEEP_GOING_MODIFIER $FORCE_MODIFIER 2>&1 | tee /dev/tty | ts '[%Y-%m-%d %H:%M:%S]' > "$LOG_FILE" || CHEZMOI_EXIT_CODE=$?
fi
logg info "Unbuffering log file $LOG_FILE"
UNBUFFER_TMP="$(mktemp)"
unbuffer cat "$LOG_FILE" > "$UNBUFFER_TMP"
mv -f "$UNBUFFER_TMP" "$LOG_FILE"
else
if command -v caffeinate > /dev/null; then
logg info "Running: caffeinate chezmoi apply $COMMON_MODIFIERS $DEBUG_MODIFIER $KEEP_GOING_MODIFIER $FORCE_MODIFIER"
caffeinate chezmoi apply $COMMON_MODIFIERS $DEBUG_MODIFIER $KEEP_GOING_MODIFIER $FORCE_MODIFIER 2>&1 | tee /dev/tty | ts '[%Y-%m-%d %H:%M:%S]' > "$LOG_FILE" || CHEZMOI_EXIT_CODE=$?
else
logg info "Running: chezmoi apply $COMMON_MODIFIERS $DEBUG_MODIFIER $KEEP_GOING_MODIFIER $FORCE_MODIFIER"
chezmoi apply $COMMON_MODIFIERS $DEBUG_MODIFIER $KEEP_GOING_MODIFIER $FORCE_MODIFIER 2>&1 | tee /dev/tty | ts '[%Y-%m-%d %H:%M:%S]' > "$LOG_FILE" || CHEZMOI_EXIT_CODE=$?
fi
fi
### Handle exit codes in log
if cat "$LOG_FILE" | grep 'chezmoi: exit status 140' > /dev/null; then
beforeRebootDarwin
logg info "Chezmoi signalled that a reboot is necessary to apply a system update"
logg info "Running softwareupdate with the reboot flag"
sudo softwareupdate -i -a -R --agree-to-license && exit
fi
### Handle actual process exit code
if [ -n "$CHEZMOI_EXIT_CODE" ]; then
logg error "Chezmoi encountered an error and exitted with an exit code of $CHEZMOI_EXIT_CODE"
else
logg success 'Finished provisioning the system'
fi
}
# @description Ensure temporary passwordless sudo privileges are removed from `/etc/sudoers`
removePasswordlessSudo() {
if command -v gsed > /dev/null; then
sudo gsed -i '/# TEMPORARY FOR INSTALL DOCTOR/d' /etc/sudoers || logg warn 'Failed to remove passwordless sudo from the /etc/sudoers file'
else
sudo sed -i '/# TEMPORARY FOR INSTALL DOCTOR/d' /etc/sudoers || logg warn 'Failed to remove passwordless sudo from the /etc/sudoers file'
fi
}
# @description Render the `docs/terminal/post-install.md` file to the terminal at the end of the provisioning process
postProvision() {
logg success 'Provisioning complete!'
if command -v glow > /dev/null && [ -f "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi/docs/terminal/post-install.md" ]; then
glow "${XDG_DATA_HOME:-$HOME/.local/share}/chezmoi/docs/terminal/post-install.md"
fi
}
# @description The `provisionLogic` function is used to define the order of the script. All of the functions it relies on are defined
# above.
provisionLogic() {
loadHomebrew
logg info "Setting environment variables" && setEnvironmentVariables
logg info "Handling CI variables" && setCIEnvironmentVariables
logg info "Ensuring WARP is disconnected" && ensureWarpDisconnected
logg info "Applying passwordless sudo" && setupPasswordlessSudo
logg info "Ensuring system Homebrew dependencies are installed" && ensureBasicDeps
logg info "Cloning / updating source repository" && cloneChezmoiSourceRepo
if [ -d /Applications ] && [ -d /System ]; then
### macOS only
logg info "Ensuring full disk access from current terminal application" && ensureFullDiskAccess
logg info "Ensuring CloudFlare certificate imported into system certificates" && importCloudFlareCert
fi
logg info "Ensuring Homebrew is available" && ensureHomebrew
logg info "Installing Homebrew packages" && ensureHomebrewDeps
logg info "Handling Qubes dom0 logic (if applicable)" && handleQubesDom0
logg info "Handling pre-provision logic" && initChezmoiAndPrompt
logg info "Running the Chezmoi provisioning" && runChezmoi
logg info "Ensuring temporary passwordless sudo is removed" && removePasswordlessSudo
logg info "Determing whether or not reboot" && handleRequiredReboot
logg info "Handling post-provision logic" && postProvision
}
provisionLogic