Skip to content

Instantly share code, notes, and snippets.

@seandstewart
Last active February 27, 2024 02:11
Show Gist options
  • Save seandstewart/5089e97c81c9e4367a0c7bd07ba8962b to your computer and use it in GitHub Desktop.
Save seandstewart/5089e97c81c9e4367a0c7bd07ba8962b to your computer and use it in GitHub Desktop.
Setup your MacOS for modern Python development (Python3.8-9, Poetry, Pyenv, Pipx, plus many the necessary system packages and compiler links)
#!/usr/bin/env zsh
###############################
# -*- Runtime -*- #
###############################
main() {
prologue
local code
local restart
local invalid
code=0
restart=1
invalid=1
while getopts sptcrah-: OPT; do
# based on https://stackoverflow.com/a/28466267
if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG
OPT="${OPTARG%%=*}" # extract long option name
OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty)
OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=`
fi
if [ "$OPT" != "" ]; then
invalid=0
fi
case $OPT in
s|system) setupBaseSystem ;;
p|python) setupBasePythonVersions ;;
t|tools) setupDevelopmentTools ;;
c|commandPrompt) setupBaseShell ;;
r|rcfile) setupBaseRCFile ;;
a|all) setupBaseSystem; setupBasePythonVersions; setupBaseShell; setupDevelopmentTools; setupBaseRCFile; break;;
h|help) showHelp; restart=0 ;;
??* ) err "Illegal option --$OPT"; code=1; showHelp ;; # bad long option
*) err "Illegal option..."; code=1; showHelp ;;
esac
done
if [ $invalid = 1 ]; then
err "Nothing to do, got no options."
showHelp
exit 1
fi
if [ $restart = 1 ]; then
ok "All set! Happy hacking.( ^_^)o自自o(^_^ )"
infoMinor "Restarting shell to activate changes."
exec $SHELL
else
exit $code
fi
}
showHelp() {
echo ""
info "Description: Setup your machine for developing with Python."
info "Usage: bootstrap-macos [OPTIONS ...]"
bold "Options:"
italic " -h|--help: Show this message."
italic " -s|--system: Setup your base system requirements (C libraries, System packages)."
italic " -p|--python: Setup your base Python interpreter(s)."
italic " -t|--tools: Setup additional tools for Python development."
italic " -c|--commandPrompt: Setup your shell and command prompt with additional goodies."
italic " -r|--rcfile: Setup your ~/.zshrc file with basic components and configurations."
italic " -a|--all: Setup all of the above ^^.\n"
}
prologue() {
title "Hello $(whoami)!"
ok "Welcome to the MacOS Bootstrapper 🥾"
info "This script will run the necessary steps to get you started developing with Python."
infoMinor "If you've already run this script before, you can use it again to perform general updates."
}
###############################
# -*- Setup Routines -*- #
###############################
setupBaseShell() {
chownUserDirs
installOMZ
installStarship
}
setupBaseSystem() {
info "Setting up base system requirements."
chownUserDirs
xcode-select --install
installBrew
installSystemLibs
catch "Done setting up base system requirements." "Base system setup failed!"
}
setupBasePythonVersions() {
info "Setting up your base Python interpreter(s)..."
installLatestPyenv
local py39
local py38
py39="$(getLatestPython '3.9')"
py38="$(getLatestPython '3.8')"
info "Installing Python versions: $py39, $py38"
note "\t(This may take a while...)"
env_parallel installPython ::: $py39 $py38
pyenv global $py39
initPyenv
catch "Done setting up base python interpreter(s)." "Setting up interpreter(s) failed!"
}
setupDevelopmentTools() {
info "Setting up development tools."
installPipx
installPoetry
installCLITools
catch "Done setting up development tools." "Setting up development tools failed!"
}
setupBaseRCFile() {
infoMinor "Prepending a base RC file @ '$HOME/.zshrc'."
# Backup the current rc file for good measure.
cp "$HOME/.zshrc" "$HOME/.zshrc.backup"
# Export the user-configuration to its own backup.
awk 'x==1 {print} /END generated RC configuration/ {x=1}' < "$HOME/.zshrc.backup" > "$HOME/.zshrc.user"
# Prepend the boilerplate
cat > $HOME/.zshrc << EOF
$RC_FILE
EOF
# Add back any user config.
cat "$HOME/.zshrc.user" >> "$HOME/.zshrc"
okMinor "Done updating '$HOME/.zshrc'."
note "📝 Original RC file is saved to '$HOME/.zshrc.backup'."
note "📝 Detected user RC file is saved to '$HOME/.zshrc.user'."
}
###############################
# -*- Install Routines -*- #
###############################
installBrew() {
if ! command -v brew &> /dev/null
then
infoMinor "Installing brew..."
curl -fsSL "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" | bash
else
infoMinor "Updating brew..."
brew update
fi
catchMinor "Done installing homebrew." "Encountered an error installing brew."
}
installSystemLibs() {
infoMinor "Installing necessary system packages..."
brew install -q parallel pkg-config openssl libffi libxml2 libxmlsec1 libmagic kubectl
parallel --citation
source "$(which env_parallel.zsh)"
catchMinor "Done installing system packages." "Encountered an error installing system packages."
}
installLatestPyenv() {
if ! command -v pyenv &> /dev/null
then
infoMinor "Installing pyenv..."
curl "https://pyenv.run" | bash
export PATH="$HOME/.pyenv/bin:$PATH"
initPyenv
else
infoMinor "Updating pyenv..."
pyenv update
fi
catchMinor "Done installing latest pyenv." "Encountered an error the latest installing pyenv."
}
getLatestPython() {
pyenv install -l | grep -o " $1.\d*$" | grep -oE "[^ ]+$" | tail -1
}
installPython() {
eval $PY_FLAGS
initPyenv
notify "Installing Python @ $1..."
pyenv install -s $1
catchNotify "Done installing Python @ $1." "Couldn't install Python @ $1!"
}
installPipx() {
if ! command -v pipx &> /dev/null
then
infoMinor "Installing pipx..."
eval $PY_FLAGS
brew install pipx
catchMinor "Done installing pipx." "Encountered an error installing pipx"
fi
}
installPoetry() {
if ! command -v poetry &> /dev/null
then
infoMinor "Installing Poetry..."
eval $PY_FLAGS
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python3 -
poetry config virtualenvs.in-project true
catchMinor "Done installing poetry." "Encountered an error installing poetry."
else
infoMinor "Updating poetry..."
poetry self update
catchMinor "Done updating poetry." "Encountered an error updating poetry."
fi
mkdir -p "${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/poetry"
poetry completions zsh > "${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/poetry/_poetry"
}
installCLITools() {
local cmd
for tool in "${CLI_TOOLS[@]}"; do
cmd=$tool
if ! command -v $cmd &> /dev/null ; then
infoMinor "Installing $tool..."
pipx install $tool
catchMinor "Done installing $tool." "Encountered an error upgrading $tool."
else
infoMinor "Upgrading $tool..."
pipx upgrade $tool
catchMinor "Done upgrading $tool." "Encountered an error upgrading $tool."
fi
done
}
CLI_TOOLS=(
pre-commit
black
)
installStarship() {
infoMinor "Installing Starship (https://starship.rs/)..."
brew install starship
okMinor "Done installing Starship."
}
installOMZ() {
if ! command omz &> /dev/null ; then
infoMinor "Updating Oh My Zsh..."
omz update
catchMinor "Done updating OMZ." "Encountered an error updating Oh My Zsh."
else
infoMinor "Installing Oh My Zsh (https://ohmyz.sh/)..."
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
catchMinor "Done installing OMZ." "Encountered an error installing Oh My Zsh."
fi
infoMinor "Installing some useful plugins..."
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
}
initPyenv() {
eval "$(pyenv init -)"
eval "$(pyenv init --path)"
}
chownUserDirs() {
if [ "$CHOWNED" = "0" ]; then
local owner
owner="$(whoami)"
infoMinor "Making '$owner' owner of directories for managing system dependencies."
note "📝 You may be asked for an administrator password to complete this operation."
sudo mkdir -p ${USER_DIRS[*]} &> /dev/null
sudo chown -R $owner ${USER_DIRS[*]} &> /dev/null
chmod u+w ${USER_DIRS[*]} &> /dev/null
catchMinor "Done chown-ing system directories." "Encountered an error changing ownership."
CHOWNED=1
fi
}
CHOWNED=${CHOWNED:-0}
USER_DIRS=(
/usr/local/Caskroom
/usr/local/Cellar
/usr/local/Frameworks
/usr/local/Homebrew
/usr/local/bin
/usr/local/etc
/usr/local/include
/usr/local/lib
/usr/local/opt
/usr/local/sbin
/usr/local/share
/usr/local/var
)
###############################
# -*- Messaging -*- #
###############################
export BOLD="\033[1m"
export RESET_BOLD="\033[21m"
export ITALIC="\033[3m"
export RESET_ITALIC="\033[23m"
export DIM="\033[2m"
export BLINK="\033[5m"
export RESET_BLINK="\033[25m"
export RED="\033[1;31m"
export GREEN="\033[32m"
export YELLOW="\033[1;33m"
export MAGENTA="\033[1;35m"
export CYAN="\033[36m"
export RESET="\033[0m"
title() {
echo "$MAGENTA$1$RESET"
}
blink() {
echo "$BLINK$1$RESET"
}
bold() {
echo "$BOLD$1$RESET"
}
italic() {
echo "$ITALIC$1$RESET"
}
note() {
echo "$DIM$1$RESET"
}
info() {
echo "📝 $BOLD$CYAN$1$RESET"
}
infoMinor() {
echo "🔮 $CYAN$1$RESET"
}
ok() {
echo "✅ $BOLD$GREEN$1$RESET"
}
okMinor() {
echo "✨ $GREEN$1$RESET"
}
err() {
echo "🚨 $RED$1$RESET 🚨"
}
catchMinor() {
if [ "$?" = "0" ]; then
okMinor "${1:-'Success!'}"
else
err "${2:-'Got an error running the previous command!'}"
fi
}
catchNotify() {
if [ "$?" = "0" ]; then
notify "✨ ${1:-'Success!'}"
else
notify "🚨 ${2:-'Got an error running the previous command!'} 🚨"
fi
}
catch() {
if [ "$?" = "0" ]; then
ok "${1:-'Success!'}"
else
err "${2:-'Got an error running the previous command!'}"
fi
}
notify() {
osascript -e "display notification \"$1\" with title \"${2:-Bootstrapper 🥾}\""
}
###############################
# -*- General Setup Cruft -*- #
###############################
# Flags we need to install Python...
read -r -d '' PY_FLAGS << 'EOM'
PATH="$HOME/.pyenv/bin:$HOME/.local/bin:$PATH";
CRYPTOGRAPHY_SUPPRESS_LINK_FLAGS="1";
LDFLAGS="-L/usr/local/opt/openssl@1.1/lib";
CPPFLAGS="-I/usr/local/opt/openssl@1.1/include";
PKG_CONFIG_PATH=/usr/local/opt/libffi/lib/pkgconfig;
EOM
# A useful starter rcfile...
read -r -d '' RC_FILE << 'EOM'
# BEGIN generated RC configuration.
# Added by DevTools MacOS Bootstrapper
# If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:/usr/local/bin:$PATH
export PYENV_ROOT=$HOME/.pyenv
export PATH=$PYENV_ROOT/bin:$HOME/.local/bin:$PATH
export CRYPTOGRAPHY_SUPPRESS_LINK_FLAGS="1"
export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"
export PKG_CONFIG_PATH=/usr/local/opt/libffi/lib/pkgconfig
# Add profiling tool
zmodload zsh/zprof
plugins=(
git
dotenv
kubectl # if your shell is slow to startup, this may be the problem!
minikube
poetry
macos
zsh-syntax-highlighting
zsh-autosuggestions
)
export ZSH="$HOME/.oh-my-zsh"
source $ZSH/oh-my-zsh.sh
# Command completions
autoload -U bashcompinit
bashcompinit
eval "$(register-python-argcomplete pipx)"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(starship init zsh)"
# END generated RC configuration. ↓ Add Your own configuration values here ↓
EOM
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment