Skip to content

Instantly share code, notes, and snippets.

@SteveBenner
Last active May 28, 2025 01:57
Show Gist options
  • Save SteveBenner/77aa8a1d46ede9fa4f24c8a74166fd09 to your computer and use it in GitHub Desktop.
Save SteveBenner/77aa8a1d46ede9fa4f24c8a74166fd09 to your computer and use it in GitHub Desktop.
Epic macOS bootstrap script for provisioning all your software needs
#!/usr/bin/env ruby
#
# This is my master system initialization script for macOS. It is designed to be run from anywhere.
# There are no dependencies other than Ruby. The script explains what it will do as it goes.
# The script returns true if it ran a typical provisioning process without any flags, and false otherwise.
#
# TODO: Improve Ruby auto-detection so you don't have to relaunch the script as much
# 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
#
# TODO: FIX MANIFEST PROCESSING
# TODO: FIX ERROR - httyparty not found
# TODO: FIX ERROR - bitbucket repo download
# TODO: FIX HOMEBREW LINKING
require 'optparse'
require 'open-uri'
require 'open3'
require 'fileutils'
require 'yaml'
# This method MUST be defined before optparse is used
UNBREW_URL = 'https://gist.githubusercontent.com/SteveBenner/11254428/raw/unbrew.rb'
$unbrew_path = File.expand_path '~/unbrew.rb'
def self.uninstall_homebrew
FileUtils.mkdir_p File.dirname $unbrew_path
FileUtils.rm $unbrew_path if File.exist? $unbrew_path # We want the LATEST version of the script
puts 'Downloading `unbrew.rb` from Github...'
File.open($unbrew_path, 'wb') { |file| file.write URI.open(UNBREW_URL).read }
File.chmod 0755, $unbrew_path
puts 'Executing `unbrew.rb`...'
result = system "ruby #{$unbrew_path} -v"
if result
puts "\nHomebrew successfully removed from your system. Proceeding..."
else
puts "No files were found! It looks like Homebrew doesn't exist on your system. Proceeding..."
end
puts
sleep 1
end
optparser = OptionParser.new do |opts|
opts.on '-h', '--help', 'Prints this help message' do
puts <<~STR
Example usage (run the script with one of the following flags after the filename, such as `ruby bootstrap.rb -h`):
-p, --print Print out a comprehensive list of all script operations (in the order they occur)
-s, --spec Print the `manifest.yml` spec
-u, --uninstall Completely REMOVE Homebrew from your system
-h, --help Prints this help message
STR
exit false
end
opts.on '-p', '--print', 'Print a comprehensive list of all script operations (in the order they occur)' 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
exit false
end
opts.on '-s', '--spec', 'Print the `manifest.yml` 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
- github
- bitbucket
- ...
config: # Configuration settings for installing Ruby and other software
ruby:
- install_location: # Directory to install Ruby into
homebrew: # Homebrew packages
formulae:
- package1
- ...
casks:
- cask1
- ...
npm_packages: # NPM packages
- package1
- ...
applications: # A list of applications that were installed in `/Applications`
- app1.app
- ...
delete:
apps: # List of applications under /Applications to remove from local disk
- app1
- ...
dirs: # List of directory paths to delete from the local disk
- /path/to/directory1
- ...
YAML
exit false
end
opts.on '-u', '--uninstall', 'Completely REMOVE Homebrew from your system' do
uninstall_homebrew
exit false
end
end
optparser.version = '1.1'
optparser.summary_width = 16
optparser.parse!
puts <<~STR
This master initialization script will provision a mac with all your software, libraries, and configurations.
It explains what it does as it goes, and interactively bootstraps your entire setup, step by step.
- It is designed to consume a `manifest.yml` file containing references to dotfiles, packages, and applications.
- The manifest file can be in the same folder as the script, in the working directory, or on your pCloud Drive.
- The script looks for a `dotfiles` folder in the manifest, in the directory the script is in, and on your pCloud.
- The script will link any directories listed under `dirs` in the manifest into your local home directory.
WITH or WITHOUT a manifest file, you can use this script to:
- Install and configure a Homebrew installation
- Remove a Homebrew installation and any vestigial Homebrew files from your system
- Upgrade your Bash and Ruby from the default system versions, and configure your system to use Homebrew versions
- Install or upgrade Git to the latest version (necessary if using the script to download repositories)
- Download and link your dotfiles from GitHub or Bitbucket, or detect and link a local dotfiles directory
- Download repositories directly from GitHub or Bitbucket
- View the manifest file spec, so you can create your own manifest file (it is very simple)
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).
- To REMOVE a Homebrew installation from your system, run `ruby bootstrap.rb -u`
- For a description of the `manifest.yml` spec, run `ruby bootstrap.rb -s`
- To print out a comprehensive list of all the operations the script performs (in order) run `ruby bootstrap.rb -p`
Press enter to proceed, or type 'exit' to quit.
STR
input = gets.chomp.downcase
exit if input == 'exit'
def symbolize_keys(obj)
case obj
when Hash
obj.each_with_object({}) do |(k, v), result|
# Convert key to symbol if it's a string
key = k.is_a?(String) ? k.to_sym : k
# Recursively symbolize nested hashes and arrays
result[key] = symbolize_keys(v)
end
when Array
obj.map { |v| symbolize_keys(v) }
else
obj
end
end
def detect_homebrew_installations
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}" }
installations
end
def latest_ruby_from_ruby_install
output = `ruby-install`
versions = {}
current_category = nil
output.each_line do |line|
line = line.strip
next if line.empty? || line == 'Stable ruby versions:'
if line.end_with?(":")
# Category header such as "ruby:"
current_category = line.chomp(":")
versions[current_category] = []
elsif current_category && line =~ /^\d[\d\.]*$/ # Version line like "3.4.1"
versions[current_category] << line
end
end
# Use the "ruby" category only (ignore jruby, etc.)
versions['ruby'].map { |v| Gem::Version.new(v) }.max.to_s
end
def ruby_install_location
if defined?(MANIFEST) && MANIFEST.dig(:config, :ruby, :install_location)
File.expand_path MANIFEST.dig :config, :ruby, :install_location
else
File.expand_path '~/.rubies'
end
end
def ruby_path
Dir[File.expand_path("#{ruby_install_location}/ruby-*")].sort_by do |f|
f.scan(/\d+/).map(&:to_i)
end.last
end
def local_ruby_version(install_location = nil)
install_location = install_location || ruby_install_location
# Look for directories named like "ruby-X.Y.Z"
ruby_dirs = Dir.exist?(install_location) ? Dir.entries(install_location).select do |entry|
entry =~ /^ruby-\d+\.\d+\.\d+$/
end : []
return nil if ruby_dirs.empty?
# Extract version strings and determine the highest version
versions = ruby_dirs.map { |dir| dir.match(/^ruby-(\d+\.\d+\.\d+)$/)[1] }
versions.map { |v| Gem::Version.new(v) }.max.to_s
end
def update_ruby_path
# TODO: separate out the relaunching phase
unless ruby_path
puts "No Ruby installation found in #{ruby_install_location}"
return nil
end
ruby_version = ruby_path.match(/ruby-(\d+\.\d+\.\d+)/)[1]
raise 'ERROR: Ruby version was not able to be parsed.' unless ruby_version
major_minor = ruby_version.split('.')[0..1].join('.')
ENV['GEM_HOME'] = File.expand_path("~/.gem/ruby/#{major_minor}.0")
ENV['GEM_PATH'] = ENV['GEM_HOME']
new_ruby_bin = File.join ruby_path, 'bin'
new_gem_bin = File.join ENV['GEM_HOME'], 'bin'
ENV['PATH'] = "#{new_ruby_bin}:#{new_gem_bin}:#{ENV['PATH']}"
# If the current interpreter version is lower than desired, re-exec the script with the new Ruby
current_version = Gem::Version.new RUBY_VERSION
latest_version = Gem::Version.new ruby_version
if current_version < latest_version
puts "Re-launching script with Ruby #{ruby_version}...\n"
exec "#{new_ruby_bin}/ruby", $0, *ARGV
end
puts "Ruby path updated! Now using Ruby version #{ruby_version} from #{new_ruby_bin}"
ruby_version
end
def update_homebrew_path(brew_prefix)
# Set the necessary Homebrew environment variables in Ruby
ENV['HOMEBREW_PREFIX'] = brew_prefix
ENV['HOMEBREW_CELLAR'] = "#{brew_prefix}/Cellar"
ENV['HOMEBREW_REPOSITORY'] = brew_prefix
# Update the PATH to include Homebrew's `bin` and `sbin` directories
ENV['PATH'] = "#{brew_prefix}/bin:#{brew_prefix}sbin:#{ENV['PATH']}"
end
def install_homebrew
puts 'Installing Homebrew into /opt/homebrew...'
install_command = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
system install_command
puts 'Homebrew successfully installed! Proceeding...'
sleep 1
end
def install_libraries_for_ruby(libraries)
system "#{$brew_prefix}/bin/brew cleanup" # Old/bad symlinks can mess with library installs
system "brew install #{libraries.join ' '}"
puts "Successfully installed libraries: #{libraries.join ', '}"
sleep 1
end
def install_ruby
puts 'Installing ruby-install...'
system 'brew install ruby-install'
# Install into the location specified in the config (defaults to ~/.rubies for use with chruby)
location = (defined?(MANIFEST) && MANIFEST.dig(:config, :ruby, :install_location)) ?
File.expand_path(MANIFEST.dig(:config, :ruby, :install_location)) :
File.expand_path('~/.rubies')
# Parse the latest ruby version
output = `ruby-install`
versions = {}
current_category = nil
output.each_line do |line|
line = line.strip
next if line.empty? || line == "Stable ruby versions:"
# If the line ends with a colon, it's a category header.
if line.end_with?(":")
current_category = line.chomp(":")
versions[current_category] = []
# Otherwise, if the line looks like a version number (e.g., "3.1.6")
elsif current_category && line =~ /^\d[\d\.]*$/
versions[current_category] << line
end
end
# Now extract the latest Ruby version from the "ruby" category.
latest_ruby = versions["ruby"].map { |v| Gem::Version.new(v) }.max.to_s
# Construct the `ruby-install` command with library paths
ruby_install_command = %W[
ruby-install
ruby
-i #{location}/ruby-#{latest_ruby}
--
--with-openssl-dir=#{RUBY_LIB_PREFIXES['openssl@3']}
--with-readline-dir=#{RUBY_LIB_PREFIXES['readline']}
--with-libyaml-dir=#{RUBY_LIB_PREFIXES['libyaml']}
--with-gdbm-dir=#{RUBY_LIB_PREFIXES['gdbm']}
--with-libffi-dir=#{RUBY_LIB_PREFIXES['libffi']}
].join ' '
if File.exist? "#{location}/ruby-#{latest_ruby}/bin/ruby"
puts 'It appears that Ruby is already installed and up to date! Proceeding...'
else
system ruby_install_command
puts "Ruby #{latest_ruby} successfully installed! Proceeding..."
end
$ruby_updated = true
update_ruby_path
sleep 1
puts 'Installing chruby Ruby manager...'
system 'brew install chruby'
puts 'chruby successfully installed! Proceeding...'
sleep 1
end
def install_bash
puts 'Installing latest version of Bash...'
system 'brew install bash'
puts 'Bash successfully installed! Proceeding...'
sleep 1
end
def install_git
puts 'Installing the latest version of Git...'
system 'brew install git'
puts 'Git successfully installed! Proceeding...'
sleep 1
end
def check_if_git_installed
unless system 'git --version'
puts 'Git was not found! Would you like to install it at this point? [y/n]'
input = gets.chomp.downcase
if input == 'y'
install_git
else
puts 'This operation requires git. Please rerun the script to continue. Exiting...'
end
end
end
def check_if_ruby_updated
latest_ruby = latest_ruby_from_ruby_install
local_ruby = update_ruby_path
unless $ruby_updated || local_ruby == latest_ruby
puts 'Your Ruby is out of date! Install now? [y/n]'
input = gets.chomp.downcase
if input == 'y'
install_ruby
else
puts 'This operation requires a modern version of Ruby. Please rerun the script to continue. Exiting...'
end
end
end
def setup_github_personal_access_token
puts <<~STR
You must provide a PAT (Personal Access Token) to continue. Instructions for setting up a PAT:
1. Navigate to https://github.com
2. Click your profile picture in the top-right corner
3. Select 'Settings' from the dropdown menu
4. In the left sidebar, click 'Developer Settings'
5. Under 'Developer Settings', click on 'Personal access tokens'
6. Choose 'Tokens (classic)'
7. Click 'Generate new token' and select 'classic' from the dropdown menu
8. Give it a name in the 'Note' field
9. Under 'Select scopes', check 'public_repo', 'read:org', and 'read:user'
10. Click 'Generate token'
11. Copy the generated token to your clipboard (there is a copy button)
Once you have your PAT, paste it here and then press enter. Without a PAT, press enter to cancel this operation.
STR
input = gets.chomp
input.empty? ? nil : input
end
def setup_bitbucket_personal_access_token
puts <<~STR
You must provide a PAT (Personal Access Token) to continue. Instructions for setting up a PAT:
1. Navigate to https://bitbucket.org
2. Click the gear icon in the top right corner
3. Select 'Personal Bitbucket settings' from the dropdown menu
4. In the left sidebar, click 'App passwords'
5. Click 'Create app password'
6. Name your app password
7. Check the following permissions: 'Account - Read' and 'Repositories - Read'
8. Click 'Create'
9. Copy the generated token to your clipboard
Once you have your PAT, paste it here and then press enter. Without a PAT, press enter to cancel this operation.
STR
input = gets.chomp
input.empty? ? nil : input
end
def clone_dotfiles(source)
pat = case source
when :github then setup_github_personal_access_token
when :bitbucket then setup_bitbucket_personal_access_token
else nil
end
abort 'This operation requires a Personal Access Token. Exiting...' unless pat
check_if_git_installed
check_if_ruby_updated
if source == :github
# Install the GitHub API gem
`gem install octokit`
require 'octokit'
# Initialize Octokit client
client = Octokit::Client.new access_token: pat
client.auto_paginate = true
# Search for 'dotfiles' repository
repos = client.repositories
dotfiles_repo = repos.find { |repo| repo.name.downcase == 'dotfiles' }
end
if source == :bitbucket
`gem install bitbucket-api`
require 'bitbucket-api'
puts 'Enter your Bitbucket username:'
bb_username = gets.chomp
BitbucketApi.configure do |config|
config.username = bb_username
config.app_password = pat # Use the correct method for app password
end
client = BitbucketApi::Client.new # Do not pass any arguments here!
repos = client.repositories.all
# Search for 'dotfiles' repository
dotfiles_repo = repos.find { |repo| repo.name.downcase == 'dotfiles' }
end
# Prompt for alternative repo if 'dotfiles' not found
if dotfiles_repo.nil?
puts "No repository named 'dotfiles' found in your account."
puts 'Please enter the name of the repository you want to download:'
repo_name = gets.chomp
dotfiles_repo = repos.find { |repo| repo.name.downcase == repo_name.downcase }
if dotfiles_repo.nil?
abort "Repository named '#{repo_name}' not found. Exiting..."
end
end
# Get the clone URL (HTTPS)
clone_url = source == :github ?
dotfiles_repo.clone_url :
dotfiles_repo.links.find { |link| link.name == 'https' }&.href
# Clone the repository using Git
puts "Cloning repository '#{dotfiles_repo.name}'..."
success = system "git clone #{clone_url} #{File.expand '~/.dotfiles'}"
if success
puts 'Successfully cloned repo into ~/.dotfiles. Proceeding...'
else
abort 'Failed to clone the repository. Exiting...'
end
end
def clone_repos(source)
cloned_repos = 0
pat = case source
when :github then setup_github_personal_access_token
when :bitbucket then setup_bitbucket_personal_access_token
when :gist then setup_github_personal_access_token
else nil
end
abort 'This operation requires a Personal Access Token. Exiting...' unless pat
check_if_git_installed
check_if_ruby_updated
puts 'Temporarily installing necessary API gems...'
case source
when :github
`gem install octokit`
require 'octokit'
client = Octokit::Client.new access_token: pat
client.auto_paginate = true
repos = client.repositories
when :gist
`gem install octokit`
require 'octokit'
client = Octokit::Client.new access_token: pat
client.auto_paginate = true
repos = client.gists
when :bitbucket
`gem install httparty`
require 'httparty'
puts ENV['PATH']
bb_username = 'SteveBenner09'
app_password = 'ATBBK8K3mY725NqgtDue2XG26K569A027A13'
# puts 'Please enter your Bitbucket username:'
# Define basic auth credentials as a hash
auth = { username: bb_username, password: app_password }
$bb_api_base_path = 'https://api.bitbucket.org/2.0'
# Method to retrieve all workspaces for the authenticated user.
def get_repositories(workspace_slug, auth)
repos = []
# Start with the full URL
url = "#{$bb_api_base_path}/repositories/#{workspace_slug}"
loop do
response = HTTParty.get(url, basic_auth: auth)
data = response.parsed_response
# Debug output:
puts "Requesting #{url}..."
puts "Response code: #{response.code}"
puts "Response body: #{response.body}..."
if data['next']
puts "Next page URL: #{data['next']}"
else
puts "No 'next' link in the response."
end
unless data.is_a?(Hash)
puts "Unexpected response format."
break
end
repos.concat(data['values'] || [])
break unless data['next']
# Use the next URL directly.
url = data['next']
end
repos
end
# Method to get all workspaces for the authenticated user with pagination.
def get_workspaces(auth)
workspaces = []
endpoint = "workspaces"
loop do
full_url = "#{$bb_api_base_path}/#{endpoint}"
response = HTTParty.get(full_url, basic_auth: auth)
data = response.parsed_response
puts "Requesting #{endpoint}..."
puts "Response code: #{response.code}"
puts "Response body: #{response.body}..."
if data['next']
puts "Next page URL: #{data['next']}"
else
puts "No 'next' link in the response."
end
unless data.is_a?(Hash)
puts "Unexpected response format."
break
end
workspaces.concat(data['values'] || [])
break unless data['next']
endpoint = URI(data['next']).request_uri
end
workspaces
end
# Get workspaces.
workspaces = get_workspaces(auth)
if workspaces.empty?
puts "No workspaces found for user #{bb_username}. Exiting."
exit 1
end
puts "Found #{workspaces.size} workspace(s)."
# Define target directory for cloning Bitbucket repositories.
target_dir = File.expand_path('~/bitbucket')
FileUtils.mkdir_p target_dir
cloned_repos = 0
workspaces.each do |ws|
ws_slug = ws['slug']
puts "\nProcessing workspace: #{ws_slug}"
repos = get_repositories(ws_slug, auth)
puts " Found #{repos.size} repositories in workspace #{ws_slug}."
repos.each do |repo|
repo_name = repo['name']
clone_links = repo['links']['clone']
https_link = clone_links.find { |link| link['name'].downcase == 'https' }
if https_link.nil?
puts " No HTTPS clone URL found for repository #{repo_name}. Skipping..."
next
end
clone_url = https_link['href']
repo_path = File.join(target_dir, repo_name)
if Dir.exist?(repo_path)
puts " Repository #{repo_name} already exists at #{repo_path}. Skipping..."
else
cloned_repos += 1
puts " Cloning #{repo_name} into #{target_dir}..."
unless system("git clone #{clone_url} #{repo_path}")
puts " Failed to clone #{repo_name}."
end
end
end
end
puts "Cloned #{cloned_repos} repositories into #{target_dir}. Proceeding...\n\n"
else raise 'Must be :github, :gist, or :bitbucket'
end
target_dir = case source
when :github then File.expand_path "~/github"
when :bitbucket then File.expand_path "~/bitbucket"
when :gist then File.expand_path '~/.gists'
else raise 'Must be :github, :gist, or :bitbucket'
end
FileUtils.mkdir_p target_dir
unless source == :bitbucket
if repos && !repos.empty?
puts "\nCloning git repositories...\n"
repos.each do |repo|
case source
when :github
repo_name = repo.name
clone_url = repo.clone_url.sub 'https://', "https://x-access-token:#{pat}@"
when :gist
# Get and normalize the gist title
gist_title = repo.description || "untitled-gist-#{repo.id[0..6]}"
repo_name = gist_title.downcase.gsub(/[^a-z0-9]+/, '-') # Convert to slug
.gsub(/-{2,}/, '-') # Remove double dashes
.gsub(/(^-|-$)/, '')[0..64] # Trim edges and limit length
clone_url = repo.git_pull_url.sub('https://', "https://x-access-token:#{pat}@")
else raise 'Must be :github or :gist'
end
next unless repo_name
repo_path = File.join target_dir, repo_name
if Dir.exist? repo_path
puts "Repo already exists: #{repo_name}. Skipping..."
next
end
puts "Cloning #{repo_name} into #{target_dir}..."
success = system "git clone #{clone_url} #{repo_path}"
puts(if success
cloned_repos += 1
"Successfully cloned #{repo_name}"
else
"Failed to clone #{repo_name}"
end)
end
else 'No repositories found. Proceeding...'
end
end
puts "Cloned #{cloned_repos} repositories into #{target_dir}. Proceeding..."
end
def configure_shell(profile)
# TODO: add smart detection of bad values, and remove them line by line
profile_path = File.expand_path profile, Dir.home
# Create the profile file if it doesn't exist
unless File.exist? profile_path
puts "Creating #{profile_path}..."
FileUtils.touch profile_path
end
# Read the current contents of the profile
contents = File.read profile_path
# Add Ruby to the PATH
ruby_export = "export PATH=\"#{ruby_path}/bin:$PATH\""
# Define the Homebrew PATH export, as well as chruby and Bundler/Ruby gems sourcing lines
brew_export = "export PATH=\"#{ENV['HOMEBREW_PREFIX']}/bin:#{ENV['HOMEBREW_PREFIX']}/sbin:$PATH\""
chruby_source = <<~STR
source #{$brew_prefix}/opt/chruby/share/chruby/chruby.sh
source #{$brew_prefix}/opt/chruby/share/chruby/auto.sh
STR
gem_export = <<~STR
export GEM_HOME="$HOME/.gem/ruby/#{local_ruby_version}"
export GEM_PATH="$GEM_HOME"
export PATH="$GEM_HOME/bin:$PATH"
STR
unless contents.include? ruby_export
File.open profile_path, 'a' do |file|
file.puts '# Ruby'
file.puts ruby_export
file.puts
end
puts "Added Ruby PATH export to #{profile_path}"
end
unless contents.include? brew_export
File.open profile_path, 'a' do |file|
file.puts '# Homebrew'
file.puts brew_export
file.puts
end
puts "Added Homebrew PATH export to #{profile_path}"
end
unless contents.include? chruby_source
File.open profile_path, 'a' do |file|
file.puts '# chruby Ruby manager'
file.puts chruby_source
file.puts
end
puts "Added chruby sourcing to #{profile_path}"
end
unless contents.include? gem_export
File.open profile_path, 'a' do |file|
file.puts '# Bundler/Ruby gems'
file.puts gem_export
file.puts
end
puts "Added Bundler sourcing to #{File.realpath profile_path}"
end
puts "Shell profile: #{profile} successfully configured. Proceeding..."
end
### MAIN OPERATIONS ###
# Detect Homebrew, and install/link if not found. Link existing installation, or uninstall if necessary.
homebrew_installations = detect_homebrew_installations
case homebrew_installations.size
when 0
puts 'Homebrew not found. Proceeding to install...'
install_homebrew
$brew_prefix = detect_homebrew_installations.first
update_homebrew_path $brew_prefix
when 1
puts "Homebrew installation detected! Proceeding...\n\n"
$brew_prefix = homebrew_installations.first
update_homebrew_path $brew_prefix
else
puts 'Uh oh! It appears there is more than one installation of Homebrew on your computer!'
puts 'To proceed, you must remove all but ONE installation. Run unbrew to do so? [y/n]'
input = gets.chomp.downcase
if input == 'y'
update_homebrew_path homebrew_installations.first
uninstall_homebrew
else
abort 'Exiting...'
end
end
# Install essential software
puts 'Install Ruby? [y/n]'
input = gets.chomp.downcase
if input == 'y'
# Install libraries for Ruby
RUBY_LIBRARIES = %w[openssl@3 readline libyaml gdbm libffi]
install_libraries_for_ruby RUBY_LIBRARIES
# Configure Ruby library prefixes for the installation command
RUBY_LIB_PREFIXES = RUBY_LIBRARIES.each_with_object({}) do |lib, hash|
hash[lib] = `brew --prefix #{lib}`.chomp
end
install_ruby
else
puts "Proceeding...\n\n"
end
puts 'Install Bash? [y/n]'
input = gets.chomp.downcase
if input == 'y'
install_bash
else
puts "Proceeding...\n\n"
end
puts 'Install git? [y/n] - Note that you MUST have git installed in order to download repositories.'
input = gets.chomp.downcase
if input == 'y'
install_git
else
puts "Proceeding...\n\n"
end
# Detect pCloud
PCLOUD_LOCATION = File.expand_path "#{Dir.home}/pCloud Drive"
if Dir.exist? PCLOUD_LOCATION
puts 'pCloud drive successfully located! The script will look there for your repositories. Proceeding...'
PCLOUD_DIR = PCLOUD_LOCATION
else
puts 'No pCloud drive detected. If you have pCloud installed, you may need to enable drive using the app menu.'
puts 'Press enter to continue...' # Give the user a chance to set up their pCloud
gets
if Dir.exist? PCLOUD_LOCATION
puts 'pCloud Drive successfully located! The script will look there for your repositories. Proceeding...'
PCLOUD_DIR = PCLOUD_LOCATION
sleep 1
else
puts 'Continuing without pCloud drive...'
sleep 1
end
end
# Detect manifest file
puts 'Detecting manifest file...'
MANIFEST_PATHS = [
File.join(__dir__, 'manifest.yml'), # Directory of the script
File.join(Dir.pwd, 'manifest.yml'), # Current working directory
File.join('/pCloud Drive', 'manifest.yml'),
File.join(__dir__, 'manifest.yaml'),
File.join(Dir.pwd, 'manifest.yaml'),
File.join('/pCloud Drive', 'manifest.yaml')
].compact # Remove nil values if ENV['MANIFEST'] is not set
MANIFEST_FILE = MANIFEST_PATHS.find { |path| File.exist? path }
if defined? MANIFEST_FILE && !MANIFEST_FILE.nil?
print "Manifest file located at: #{MANIFEST_FILE}. Parsing..."
MANIFEST = symbolize_keys YAML.load_file(MANIFEST_FILE)
puts ' Manifest successfully loaded! Proceeding...'
else
puts 'No manifest file detected. Proceeding...'
end
# Primary logic
if defined? MANIFEST
# Detect dotfiles location (from a manifest OR mounted pCloud drive) or download them
if MANIFEST[:dotfiles]
DOTFILES_LOCATION = MANIFEST[:dotfiles]
puts "Dotfiles found at: #{DOTFILES_LOCATION}! Proceeding..."
elsif defined?(PCLOUD_DIR) && Dir.exist?("#{PCLOUD_DIR}/dotfiles")
DOTFILES_LOCATION = "#{PCLOUD_DIR}/dotfiles"
puts "Dotfiles found at: #{DOTFILES_LOCATION}! Proceeding..."
elsif Dir.exist? File.join(__dir__, 'dotfiles') # Directory of the script
DOTFILES_LOCATION = File.join(__dir__, 'dotfiles')
elsif Dir.exist? File.join(Dir.pwd, 'dotfiles') # Current working directory
DOTFILES_LOCATION = File.join(Dir.pwd, 'dotfiles')
else # Prompt the user to optionally download their dotfiles from GitHub or Bitbucket
puts 'No dotfiles directory found. Would you like to download a dotfiles repo from one of the following?'
puts 'NOTE: This will necessitate setting up a Personal Access Token on GitHub, as well as installing Ruby and Git.'
puts 'Enter a number corresponding to one of the platforms below to continue (input anything else to skip):'
puts '1) GitHub'
puts '2) Bitbucket'
input = gets.chomp.downcase
case input
when '1'
clone_dotfiles :github
DOTFILES_LOCATION = '~/.dotfiles'
when '2'
clone_dotfiles :bitbucket
DOTFILES_LOCATION = '~/.dotfiles'
else puts 'Proceeding without dotfiles...'
end
sleep 1
end
# Link any directories listed under `dirs` in the manifest (from pCloud drive) into HOME
if defined?(PCLOUD_DIR) && MANIFEST[:dirs]
MANIFEST[:dirs].each do |dir|
unless Dir.exist? "#{PCLOUD_DIR}/#{dir}"
puts "#{dir} not found in pCloud. Skipping..."
next
end
if Dir.exist? File.expand_path "~/#{dir}"
puts "#{dir} already exists in your local user home directory. Skipping..."
else
puts "Linking #{dir}..."
File.symlink "#{PCLOUD_DIR}/#{dir}", File.expand_path("~/#{dir}")
end
end
puts 'Directories successfully linked from pCloud. Proceeding...'
sleep 1
end
if MANIFEST[:homebrew]
if MANIFEST[:homebrew][:formulae]
puts 'Installing Homebrew formulae...'
failures = []
MANIFEST[:homebrew][:formulae].each do |formula|
begin
success = system "#{$brew_prefix}/bin/brew install #{formula}"
if success
puts "#{formula} successfully installed."
else
raise "Installation of #{formula} failed."
end
rescue StandardError => e
failures << formula
puts "#{e.message}: Installation failed for #{formula}."
end
end
if !failures.empty?
puts "The following formulae failed to install: #{failures.join(', ')}"
else
puts "All formulae installed successfully."
end
end
if MANIFEST[:homebrew][:casks]
puts "Installing Homebrew casks..."
failures = []
MANIFEST[:homebrew][:casks].each do |cask|
begin
success = "#{$brew_prefix}/bin/brew install #{cask} --cask"
if success
puts "#{cask} successfully installed."
else
raise "Installation of #{cask} failed."
end
rescue StandardError => e
failures << cask
puts "#{e.message}: Installation failed for #{cask}."
ensure
sleep 1
end
end
if !failures.empty?
puts "The following casks failed to install: #{failures.join(', ')}"
else
puts "All casks installed successfully."
end
end
puts "Homebrew formulae and casks successfully installed. Proceeding...\n\n"
end
end
# Link dotfiles and dotfolders from the dotfiles directory (excluding `.git` and `.idea`)
if defined? DOTFILES_LOCATION
DOTFILES_DIR = File.expand_path DOTFILES_LOCATION
puts "Linking dotfiles from #{DOTFILES_DIR} to user local home directory..."
excluded_entries = %w[.git .idea] # DO NOT LINK THESE!
# Process each item in dotfiles directory
Dir.each_child(DOTFILES_DIR) do |entry|
next if excluded_entries.include? entry
next unless entry.start_with? '.' # ONLY copy files and folders whose name begins with a dot
source = File.join DOTFILES_DIR, entry
target = File.join File.expand_path('~'), entry
# Handle existing items
if File.exist?(target) || File.symlink?(target)
puts "Removing existing: #{entry}"
FileUtils.rm_rf target
end
# Create symbolic link
puts "Linking: #{entry} → ~/#{entry}"
FileUtils.ln_s source, target
end
puts "Successfully linked #{Dir.children(DOTFILES_DIR).count - excluded_entries.count} items"
puts
else
puts "No dotfiles directory found. Skipping linking phase...\n\n"
end
sleep 1
update_ruby_path # TODO: see if I can remove this
# Configure local user shell profiles and repair if necessary
puts 'Configuring shell profiles for use with Homebrew and Ruby...'
%w[.zshrc .zprofile .bashrc .bash_profile].each { |profile| configure_shell profile }
puts "\nSuccess! Your shell is now fully configured for use with Homebrew and Ruby!\n\n"
sleep 1
%i[GitHub gist Bitbucket].each do |source|
puts "Would you like to clone your #{source} repositories? [y/n]"
input = gets.chomp.downcase
if input == 'y'
clone_repos source.downcase
else
puts "Proceeding...\n\n"
end
end
# Detect Gemfile and install the bundle, if one exists
if File.exist? File.expand_path '~/Gemfile'
puts 'Gemfile detected! Installing bundler and your gems...'
check_if_ruby_updated
`gem install bundler`
`bundle install`
sleep 1
end
# Cleanup
puts "Cleaning up...\n\n"
`gem uninstall octokit httparty -a -x -I` # Force uninstallation without prompt, and ignore dependencies
File.delete $unbrew_path if File.exist? $unbrew_path
puts 'Congratulations!!! Your system is now fully provisioned.'
puts 'For more information or to report an issue with this script, refer to:'
puts 'https://gist.github.com/SteveBenner/21b7d4aacba464cc1961f490db7903f9'
exit true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment