Homebrew Setup for Apple/Intel Compatibility
Jan 11, 2025
Background
Modern macOS machines use M series chips (aka. “Apple silicon”). These chips are based on the ARM
architecture, which is typically referred to as arm64
. Prior to this, most Apple machines used
Intel processors that were based on the x86_64
architecture. You can check which architecture your
macOS device has via the arch
command. This change has caused compatibility problems over the last
several years where some programs have not been ported to the new architecture. Apple has provided a
compatibility software solution known as Rosetta that is further described
here. This is a well known solution to the compatibility
problem and apparently is reasonably performant, etc.
I prefer and have extensively used the popular Homebrew package manager (aka.
brew
) for macOS (and OSX before it). I install nearly all of my developer packages via this tool
when there is a supported “Homebrew Formulae” available for it. Based on open source project
documentation and discussions in forums and blogs, this seems to be what many developers do on macOS
devices.
I will show here how to have a “dual” brew
setup where you can quickly toggle your terminal to use
arm64
vs x86_64
as the arch
while resolving brew
and its installed packages appropriately
for the given architecture.
Motivating example
There are many possible scenarios for why you may want to have a “dual” installation of brew
. This
is just to serve as a motivating example with a specific solution shown exercising the setup.
I recently had a scenario where I needed to install ruby
via rbenv
for use with gems that
required the x86_64
architecture. However, I am doing this using an arm64
macOS device. To do
this, I needed my ruby
version to be installed using the x86_64
architecture. However, I already
used brew
to install many packages, including ruby
, based on their arm64
versions. This is
what is native to my device and what I’d prefer where possible.
The best approach I found is to install ruby
and related packages for x86_64
is by using brew
for x86_64
as well. This means an isolated brew
installations for arm64
and x86_64
is
needed.
Solution
Refer to this StackOverflow answer for “How can I run two isolated installations of Homebrew?” following steps 1-5 there as a prerequisite to the rest of my recommendations here. Here is a summary of those steps, given in more detail in the StackOverflow answer above, that should be performed first:
- Install
brew
natively on Apple Silicon (will install to/opt/homebrew
by default). This should be thearm64
version. - Install Intel-emulated
brew
(will install to/usr/local
by default). This is thex86_64
version.- If you haven’t yet installed Rosetta 2, you’ll need to run
softwareupdate --install-rosetta
first. Alternatively, this is described more here.
- If you haven’t yet installed Rosetta 2, you’ll need to run
- Create an alias for the
x86_64
version ofbrew
, eg.brow
because “O”” for “old”. - Add the
arm64
version ofbrew
to yourPATH
.
I will assume the zsh
shell is used for the rest of this, but it shouldn’t
be too difficult to translate to another shell environment as needed.
Below I will share snippets to add .zshrc
to support toggling within a terminal between an
environment using arm64
or x86_64
version of brew
packages (isolated from one another). I’ll
provide an explanation to each part as needed. This may sometimes be done as shell comments inline.
It is often a good idea to leave those comments in the script to help remember what this is for.
I want to make it obvious when I’m in the terminal, which environment for brew
I’m working in:
# This customizes the iTerm title bar to show useful info for in context of the
# tab showing in the terminal.
precmd() {
local max_length=25 # Max total length for the tab title
local prefix=".." # Prefix to indicate truncation
local prefix_length=${#prefix}
local allowed_length=$((max_length - prefix_length)) # Remaining length for the path
local truncated_path
local current_path="$PWD"
# Check if the path exceeds the max length
if [[ ${#current_path} -gt $max_length ]]; then
# Extract the last allowed_length characters from the path
truncated_path="${prefix}${current_path: -$allowed_length}"
else
# If the path is short enough, use it as-is
truncated_path="$current_path"
fi
# Set the iTerm2 tab and window titles, respectively.
print -Pn "\e]1;$truncated_path ($(uname -m))\a"
print -Pn "\e]2;%n@%m: $truncated_path ($(uname -m))\a"
}
current_arch() {
echo "Current arch: $(uname -m)"
echo "ARCH env var: ${ARCH:-NONE}"
}
I want to be able to quickly toggle the environment my terminal is currently using:
# Toggles for using arm64 vs x86_64 homebrew.
switch_arch() {
local new_arch=$1
if [[ "$new_arch" != "arm64" && "$new_arch" != "x86_64" ]]; then
echo "Invalid architecture. Use 'arm64' or 'x86_64'"
return 1
fi
# Set the ARCH environment variable
export ARCH=$new_arch
# Only switch if we're not already on the desired architecture
if [[ "$(uname -m)" != "$new_arch" ]]; then
echo "Switching to new arch: ${ARCH}"
# Use arch -x86_64 or arch -arm64 to execute subsequent commands
if [[ "$new_arch" == "x86_64" ]]; then
# Switch to x86_64 architecture
exec arch -x86_64 /bin/zsh
else
# Switch to arm64 architecture
exec arch -arm64 /bin/zsh
fi
fi
}
# Initial architecture setup - runs when shell starts
initial_arch_setup() {
# If ARCH is set (like from VSCode), switch to that architecture
if [[ -n "$ARCH" ]]; then
switch_arch "$ARCH"
fi
}
# Run the initial setup
initial_arch_setup
These are helpful aliases:
alias zarm='ARCH=arm64 initial_arch_setup'
alias zros='ARCH=x86_64 initial_arch_setup'
alias brow='arch -x86_64 /usr/local/Homebrew/bin/brew'
alias ib='PATH=/usr/local/bin'
Any time this .zshrc
file is loaded, we want to make sure the environment is setup as expected,
eg. ensuring the correct PATH
is used:
if [[ "$(uname -m)" == "arm64" ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
else
alias brew='arch -x86_64 /usr/local/bin/brew'
export PATH="/usr/local/bin:$PATH"
eval "$(/usr/local/bin/brew shellenv)"
fi
The ruby + rbenv setup
This should be applicable to other languages that have similar environmental setups to ruby
via
rbenv
, eg. python
, node
, etc.
# For Ruby
if [[ $(uname -m) == "arm64" ]]; then
echo "Using rbenv with arm64"
export RBENV_ROOT="${HOME}/.rbenv"
if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi
rbenv global 3.2.0
else
echo "Using brew/rbenv with x86_64"
export RBENV_ROOT="${HOME}/.rbenv_x86"
# Initialize rbenv for x86
if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi
rbenv global 3.2.0
fi