Skip to content

Instantly share code, notes, and snippets.

@SteveBenner
Created February 9, 2025 12:46
Show Gist options
  • Save SteveBenner/29f58cba1e995d36a6bd49cbdfca99a7 to your computer and use it in GitHub Desktop.
Save SteveBenner/29f58cba1e995d36a6bd49cbdfca99a7 to your computer and use it in GitHub Desktop.
Epic macOS bootstrap script for provisioning all your software needs
#!/usr/bin/env ruby
# encoding: utf-8
#
# This is my master system initialization script for macOS
# - It is built using rake, but can bootstraps EVERYTHING (including rake!) as it runs
# - It is designed to be run from anywhere, with NO dependencies other than Ruby
# - The script is interactive, and explains what it does as it goes, prompting the user where needed
# - The script returns `true` if it completed a typical provisioning process, `false` otherwise
#
# TODO: Install gems for a local bundle
# TODO: When relaunching the script, save your progress
# TODO: Compose a way to, when relaunching the script, skip to where you left off from
# TODO: Add ssh key/env setup
# TODO: Add support for google drive
# TODO: Refactor so that script never exits prematurely during the flow
# TODO: Add support for 'deletions', a list of apps and/or directories to delete from the disk
# TODO: Update unbrew to remove installed casks from /Applications
require 'rubygems'
require 'tempfile'
require 'open-uri'
require 'open3'
require 'yaml'
$stdout.sync = true # Ensure console output appears immediately by preventing internal buffering
### HELPERS (MUST BE DEFINED FIRST!) ###
def putsm(str); puts "#{str} Proceeding...\n\n"; end
def prompt(msg, binary = true)
puts "#{msg}#{binary ? ' (y/n)' : ''}" # By default, print yes/no in the prompt
input = gets.chomp
(input.downcase == 'exit') ? exit(false) : input
end
### CONSTANTS ###
VERSION = 0.1
SUPPORTED_OS = %w[14.0.1 15.1] # Supported versions of macOS this script has been verified to work with
UNBREW_URL = 'https://gist.githubusercontent.com/SteveBenner/11254428/raw/unbrew.rb'
CONFIG = { # Do NOT alter the initial CONFIG state unless you know how it affects the rest of the script
homebrew_installations: [],
installed: {
homebrew_packages: []
}
}
# Track the progress of the bootstrap operation, resuming where you left off if having to re-execute the script
BOOTSTRAP_PROGRESS = YAML.safe_load ENV['BOOTSTRAP_PROGRESS'], permitted_classes: [Symbol] if ENV['BOOTSTRAP_PROGRESS']
PROGRESS = if defined?(BOOTSTRAP_PROGRESS) && BOOTSTRAP_PROGRESS.is_a?(Hash)
BOOTSTRAP_PROGRESS.transform_keys &:to_sym
else
{
homebrew_installed: false,
brew_packages_installed: false,
ruby_installed: false,
gems_installed: false,
bundle_installed: false,
bash_installed: false,
git_installed: false,
node_installed: false,
node_packages_installed: false,
repos_cloned: false,
dirs_copied: false,
dirs_removed: false,
apps_removed: false,
shell_profiles_configured: false
}
end
if defined?(BOOTSTRAP_PROGRESS) && BOOTSTRAP_PROGRESS.is_a?(Hash)
PROGRESS = BOOTSTRAP_PROGRESS.transform_keys &:to_sym
end
### BOOTSTRAP AND CONFIGURE RAKE ###
unless ENV['RUNNING_RAKE']
begin
require 'rake'
rescue LoadError
puts 'Rake gem not found. Bootstrapping rake...'
system('gem install rake') || raise('ERROR: Failed to install rake.')
puts 'Rake successfully installed!'
end
# Set an environment flag so that when the file is reloaded under rake,
# we don’t go through this bootstrap process again.
ENV['RUNNING_RAKE'] = 'true'
# Exec this file under rake, passing along any command-line arguments.
exec 'rake', '-f', __FILE__, *ARGV
end
Rake::FileUtilsExt.verbose true
### TASKS ###
task default: 'bootstrap:provision'
desc 'Print out the welcome message and usage information (prints automatically when running the `help` task)'
task :help do
puts <<~STR
This master initialization script will interactively and completely PROVISION a macOS system (for the local user).
It is a Rake file, meaning to operate it you run: `rake -f bootstrap.rake <task>` where <task> is a task name.
To print out a list of all the available tasks, run: `rake -T -f bootstrap.rake`.
PROVISIONING (optional at every step of the way):
- You can install and configure Homebrew (unless already installed and configured)
- You can install and configure the latest version of Ruby, Bash, and Git
- You can install software packages (such as applications or libraries) via Homebrew, NPM, and Bundler
- You can use Git to download your repositories from GitHub/Bitbucket into your local user home folder
⌐-- You can use Git to download your DOTFILES from GitHub/Bitbucket, OR use a local/network DOTFILES directory
⌐-- You can symbolically link your DOTFILES into your user home directory
| ⌐ You can use a MANIFEST file to specify your packages, Ruby install location, and directories to copy/remove
||
⨽|_ DOTFILES directory (hidden configuration files):
| - A `dotfiles` directory contains DOTFILES (files and folders with a '.' at the start of their name)
| - Any DOTFILES in the `dotfiles` directory will be linked into your local user home folder
| - You can clone a remote `dotfiles` directory, or auto-detect one from the same folder the script is in
| - If you have a pCloud and/or Google drive enabled, the script looks there for a `dotfiles` folder
|
⨽_ MANIFEST file (simple YAML config file):
- You can choose to run the script using a `manifest.yml` file, which is auto-detected
- For more info about the MANIFEST file syntax, run `rake -f bootstrap.rake help:manifest`
- The script looks for this file in the same folder it is in, as well as on your pCloud and/or Google drive
WARNING: The script operates DESTRUCTIVELY, assuming you want to REPLACE any existing local config files
NOTE: It is HIGHLY recommended to upgrade the system Bash and Ruby IMMEDIATELY, primarily for security reasons!
NOTE: This script must be run by the user (as opposed to root)
NOTE: To REMOVE a Homebrew installation from your system, run `rake -f bootstrap.rake uninstall:brew`
STR
end
namespace :help do
# TODO: update this output
desc 'Print out a comprehensive list of all script operations (in the order in which they occur)'
task :operations do
puts <<~STR
The script will perform the following operations in this order:
1. Automatically detect if pCloud is installed and drive enabled (if it is, the script looks there for repos)
2. Install and configure Homebrew (does nothing if Homebrew already exists and is properly configured)
- If Homebrew is installed but broken, link Homebrew files and reconfigure the shell accordingly
- If Homebrew is not installed, install it (link/configure before and after copying dotfiles)
3. Detect a manifest file and proceed accordingly:
- Install libraries used with Ruby: openssl, readline, libyaml, gdbm, and libffi
- Install and link the latest versions of Ruby, Bash, and Git (using config options in the manifest)
A. If NO manifest file exists:
- Prompt the user to optionally download dotfiles from GitHub or Bitbucket
- TEMPORARILY install ruby gems to facilitate access to the GitHub and/or Bitbucket APIs if necessary
- The script walks the user through the process of setting up access to their GitHub or BitBucket account
- Once the user inputs their access credentials into the script, it automatically clones into `~/.dotfiles`
- Once downloaded, your dotfiles and dotfolders are linked into the local user home directory
- Prompt the user to optionally download git repos (and gists) from GitHub and BitBucket
- Downloaded repos/gists are placed into `~/github`, `~/gists`, and `~/bitbucket` directories
- Existing repos and gists are ignored
B. If a manifest file DOES exist:
- If there is a `dotfiles` entry, use it as the local path to your dotfiles
- Link dotfiles and dotfolders into the user home directory, and dotfiles to ~/.dotfiles
- If no `dotfiles` entry exists, prompt the user to optionally download from GitHub or Bitbucket
- If a `dirs` entry exists in the manifest, link any listed directories from pCloud into the home directory
- Prompt the user to optionally download git repos (and gists) from GitHub and BitBucket
- TEMPORARILY install ruby gems to facilitate access to the GitHub and/or Bitbucket APIs if necessary
- The script walks the user through the process of setting up access to their GitHub or BitBucket account
- Once the user inputs their access credentials into the script, it automatically clones all repositories
- Downloaded repos/gists are placed into `~/github`, `~/gists`, and `~/bitbucket` directories
- Existing repos and gists are ignored
- If a Gemfile is present in the dotfiles directory, it will be linked to the home directory as usual
- Gems will be installed via Bundler, and your shell and Ruby environments will be appropriately configured
- Install packages listed under `homebrew` (`formulae` and `casks`) and then `npm_packages` in the manifest
- Compare applications listed under `applications` with what is already installed on disk, and print a report
4. Configure the shell environment
- The shell is configured via the `.bashrc` startup script. If `.bashrc` exists in dotfiles already,
the script will scan it for a Homebrew configuration and repair the configuration if necessary.
- If no `.bashrc` profile exists, the script creates one and configures it for Homebrew/Bash/Ruby/Git
5. Cleanup: The script will run `brew doctor` and `brew cleanup`, and removes any temporarily installed gems
STR
end
desc 'Print the `manifest.yml` spec with usage examples'
task :spec do
puts <<~YAML
# This is an example of a `manifest.yml` document.
---
dotfiles: /local/path/to/dotfiles # Absolute path to a local DOTFILES directory
dirs: # Names of directories to link to your home directory
- /relative/path/to/dir1 # The script looks in your pCloud and Google drive if either is detected
- ...
config: # Configuration settings for installing Ruby and other software
ruby:
- install_location: # Directory to install Ruby into (defaults to `~/.rubies`)
homebrew: # Homebrew packages (formulae and casks can be mixed together)
- package1
- ...
npm_packages: # Node Package Manager packages
- package1
- ...
applications: # A list of applications that were installed in your last `/Applications` dir
- App1.app
- ...
delete:
apps: # List of applications under `/Applications` to remove from local disk
- app1
- ...
dirs: # List of absolute directory paths to delete from the local disk
- /path/to/directory1
- ...
YAML
end
end
namespace :bootstrap do
desc 'Performs the complete beginning-to-end provisioning process, bootstrapping everything as needed'
task :provision => ['detect:homebrew', :help] do
os_version = `sw_vers -productVersion`.chomp
puts "Wild! You're running macOS #{os_version}, which is not in the list of officially supported OSes yet."
puts 'Ready to adventure into this brave new world and try out the bootstrap script on your system anyway?'
puts 'Consider commenting on the official '
puts "\nPress ENTER to continue. Type 'exit' at any point to exit the program."
input = gets.chomp
exit if input == 'exit' || !input.empty?
CONFIG[]
end
end
namespace :detect do
desc 'Detects existence of one or more Homebrew installations'
task :homebrew do
puts 'Detecting Homebrew installations...'
known_prefixes = %w[/opt/homebrew /usr/local]
installations = known_prefixes.select { |prefix| File.exist? "#{prefix}/bin/brew" }
installations.each { |installation| puts "Homebrew found at: #{installation}" }
CONFIG[:homebrew_installations] = installations
end
end
namespace :install do
end
namespace :remove do
desc 'Downloads `unbrew.rb` (my unofficial Homebrew uninstaller) and uses it to purge your system of Homebrew files'
task :homebrew do
end
end
namespace :config do
end
namespace :generate do
desc 'Generates a `manifest.yml` file based on installed packages and directories in your home folder'
task :manifest do
puts 'Generating manifest...'
CONFIG[:installed][:brew_packages] = `brew ls`.split "\n" if system 'command -v brew'
end
end
# todo: run brew doctor and brew clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment