Skip to main content

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.

DependencyDescription
ChezmoiDotfile configuration manager (on-device provisioning)
TaskTask runner used on-device for task parallelization and dependency management
ZX / Node.jsZX is a Node.js abstraction that allows for better scripts
GumGum is a terminal UI prompt CLI (which allows sweet, interactive prompts)
GlowGlow 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.

VariableDescription
START_REPO (or REPO)Variable to specify the Git fork to use when provisioning
ANSIBLE_PROVISION_VMFor 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.

Install Doctor homepage Install Doctor documentation portal (includes tips, tricks, and guides on how to customize the system to your liking)

Script Functions

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-CommandDescription
errorLogs a bright red error message
infoLogs a regular informational message
mdTries to render the specified file using glow if it is installed and uses cat as a fallback
promptAlternative that logs a message intended to describe an upcoming user input prompt
starAlternative that logs a message that starts with a star icon
startSame as success
successLogs a success message that starts with green checkmark
warnLogs 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:

  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.

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.

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.

  1. Ensures Glow, Gum, Chezmoi, Node.js, and ZX are installed.
  2. If the system is macOS, then also install gsed and coreutils.

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.

  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)

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