CTO Colin Copeland works on a MacBook Air

Last year, Apple announced that they would transition their entire Mac line from Intel processors to their ARM64 Apple Silicon chip called the M1. Several weeks ago, I started testing development on MacBook Air with the Apple M1 chip (pictured above).

Is development possible on the M1? Yes. Is it easy and straightforward? Not always, but if you can live with a few workarounds, you can make it work. While many software packages now natively support Apple Silicon (see Does it ARM?), the development space is still in the process of rolling out support, like Docker.

This post will likely age quite quickly! Many software packages have received Apple Silicon-related updates over the past few weeks.

My Development Environment

At a high level, I'm currently running the following on the M1:

  • Python and Django (Homebrew and Xcode)
  • JavaScript and React (Homebrew and nvm)
  • PostgreSQL (Homebrew)
  • Docker (Apple M1 Tech Preview)
  • Visual Studio Code (Insiders)

Other packages are used to help achieve this development environment, from iTerm2 to direnv, so M1 support looks good here. The areas I'm still having issues with or haven't had time to dig into yet are:

  • Python 3.7 under Rosetta 2
  • Building and running certain Docker images locally
  • Java

The info below covers steps (and gotchas) to configure a development environment like this.

Rosetta 2 and Command Line Tools for Xcode

First, make sure Rosetta 2 is installed. Rosetta 2 enables a Mac with Apple silicon to use apps built for a Mac with an Intel processor. Install it with:

softwareupdate --install-rosetta  --agree-to-license

Also, install Xcode's command-line tools for python3 and other useful libraries:

xcode-select --install

Activity Monitor includes a handy Architecture column indicating which processes are running on Apple vs. Intel (Rosetta 2) architecture. Here's a screenshot showing Python 3.7 running under Rosetta 2:

Activity Monitor Screenshot

Intel-emulated Terminal

With Rosetta 2 installed, you can use the arch command to run commands under Rosetta 2:

> arch
arm64
> arch -x86_64 bash -c "arch"
i386

My current .zshrc configuration is incompatible using Rosetta 2, so you may not be able to launch a zsh shell like this:

❯ arch -x86_64 zsh
arch: posix_spawnp: zsh: Bad CPU type in executable

So I use bash as noted in the first example, to install packages under Rosetta 2.

Install Homebrew

Homebrew does support Apple Silicon. However, a particular package may not run natively, so your mileage may vary. To support running both native arm64 and x86-emulated homebrew packages, you install them side-by-side.

Install arm64 brew into /opt/homebrew using the Alternative Installs method:

mkdir homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
sudo mv homebrew /opt/homebrew
export PATH="/opt/homebrew/bin:$PATH"
brew update

Install Intel-emulated Homebrew

Install Intel-emulated Homebrew to the default /usr/local:

arch --x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

Apple Silicon Workaround - nvm

nvm is available in Homebrew:

brew install nvm

I was able to install the latest version of node using nvm:

❯ nvm install v15

It builds from source and takes a while, but eventually produces an arm64 executable:

❯ file `which node`
/Users/colincopeland/.nvm/versions/node/v15.5.0/bin/node: Mach-O 64-bit executable arm64
❯ node -p process.arch
arm64

However, as noted in nvm install node fails to install on macOS Big Sur M1 Chip #2350, older versions of node require Rosetta 2.

You can install Node v12 like this:

❯ arch -x86_64 bash
❯ source /opt/homebrew/opt/nvm/nvm.sh
❯ nvm install v12
Downloading and installing node v12.20.0...
Downloading https://nodejs.org/dist/v12.20.0/node-v12.20.0-darwin-x64.tar.xz...
Computing checksum with shasum -a 256
Checksums matched!
Now using node v12.20.0 (npm v6.14.8)

Now you can switch seamlessly:

❯ nvm use 12
Now using node v12.20.0 (npm v6.14.8)
❯ node -p process.arch
x64
❯ nvm use default
Now using node v15.5.0 (npm v7.3.0)
❯ node -p process.arch
arm64

Python

Python 3.8 and Python 3.9 install easily using homebrew:

❯ brew install python@3.8 python@3.9
❯ file /opt/homebrew/bin/python3.9
/opt/homebrew/bin/python3.9: Mach-O 64-bit executable arm64

However, Issue 41100 indicates Python 3.7 and below will never be supported on Apple Silicon:

There are no plans to backport support to 3.7 and 3.6 which are in the security-fix-only phase of their release cycles.

Rosetta 2 is required for these versions. Additionally, not all Python libraries and packages run on Apple Silicon, so it's useful to install the Intel-emulated versions as well:

❯ arch -x86_64 bash
$ export PATH="/usr/local/Homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
$ brew install python@3.7 python@3.8
$ file /usr/local/opt/python@3.7/bin/python3.7
/usr/local/opt/python@3.7/bin/python3.7: Mach-O 64-bit executable x86_64

Xcode's command-line tools provide several versions of Python that can run natively on Apple Silicon. So with Xcode's help, I'm currently running the following versions of Python using Homebrew and Xcode:

arm64:

  • Python 2.7.16 (Xcode)
  • Python 3.8.7 (homebrew)
  • Python 3.9.1 (homebrew)

Intel-emulated (with Rosetta 2):

  • Python 3.7.9 (homebrew)
  • Python 3.8.7 (homebrew)

Python with direnv

I used to use pyenv and pyenv-virtualenv, but I wasn't able to install different versions successfully (even in Rosetta). I've now switched to direnv's layout_python to replace pyenv-virtualenv's management of project virtual environments, which so far has been great.

It's easy to auto-create and activate an arm64 Python 3.9-based virtual environment with:

# project-a/.envrc
layout python /opt/homebrew/bin/python3.9

And just as easy with an Intel-emulated Python 3.7:

# project-b/.envrc
layout python /usr/local/opt/python@3.7/bin/python3

Python packages requiring compilation

I found it useful to export these flags to ensure Python packages compile properly:

# useful Python C-library compliation flags
export LDFLAGS="-L$(brew --prefix openssl)/lib -L$(brew --prefix zlib)/lib"
export CPPFLAGS="-I$(brew --prefix openssl)/include -I$(brew --prefix zlib)/include"

Python Intel-emulated workarounds

Some packages fail to install on Apple Silicon:

❯ file `which python`
/Users/copelco/projects/.../.direnv/python-3.9.1/bin/python: Mach-O 64-bit executable arm64
❯ pip install numpy
*** snip ****
  ----------------------------------------
  ERROR: Failed building wheel for numpy

But can be installed using an Intel-emulated version:

❯ file `which python`
/Users/copelco/projects/.../.direnv/python-3.8.7/bin/python: Mach-O 64-bit executable x86_64
❯ pip install numpy

Issues with Intel-emulated Python 3.7

I've had the most issues with Python 3.7. Python 3.7 isn't supported on Apple Silicon. It can be installed via Homebrew using Rosetta 2, but I've had trouble using it with my Django projects.

For example, I've run into issues with packages with external dependencies (such as libffi):

❯ file `which python`
/Users/copelco/projects/.../.direnv/python-3.7.9/bin/python: Mach-O 64-bit executable x86_64
❯ pip install bcrypt
❯ python3 -m bcrypt
Traceback (most recent call last):
  File "/usr/local/Cellar/python@3.7/3.7.9_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/runpy.py", line 183, in _run_module_as_main
    mod_name, mod_spec, code = _get_module_details(mod_name, _Error)
  File "/usr/local/Cellar/python@3.7/3.7.9_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/runpy.py", line 142, in _get_module_details
    return _get_module_details(pkg_main_name, error)
  File "/usr/local/Cellar/python@3.7/3.7.9_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/runpy.py", line 109, in _get_module_details
    __import__(pkg_name)
  File "/Users/copelco/projects/test/.direnv/python-3.7.9/lib/python3.7/site-packages/bcrypt/__init__.py", line 25, in <module>
    from . import _bcrypt  # type: ignore
ImportError: dlopen(/Users/copelco/projects/test/.direnv/python-3.7.9/lib/python3.7/site-packages/_cffi_backend.cpython-37m-darwin.so, 2): Symbol not found: _ffi_type_double
  Referenced from: /Users/copelco/projects/test/.direnv/python-3.7.9/lib/python3.7/site-packages/_cffi_backend.cpython-37m-darwin.so
  Expected in: flat namespace
 in /Users/copelco/projects/test/.direnv/python-3.7.9/lib/python3.7/site-packages/_cffi_backend.cpython-37m-darwin.so

Docker

Docker can be installed using the Apple M1 Tech Preview. I've run into several issues using this version.

The first issue is some images depend on binaries that don't have arm64 variants yet. For example, npm install fails to find a sentry-cli-Linux-arm64 here:

------
 > [static_files  4/10] RUN npm install --silent:
#17 11.55 Error: Unable to download sentry-cli binary from https://downloads.sentry-cdn.com/sentry-cli/1.61.0/sentry-cli-Linux-arm64.
#17 11.55 Server returned 403: Forbidden.
------
executor failed running [/bin/sh -c npm install --silent]: exit code: 1

I've looked into using buildx to support multi-arch builds, such as in this How to Actually Deploy Docker Images Built on M1 Macs With Apple Silicon blog post. The problem is, in some cases, I only want to build the x86 variant.

For the time being, I'm using How to deploy on remote Docker hosts with docker-compose to build x86 images with Docker Contexts. This method allows me to point my docker CLI to a Linux box running on my local network to build images.

Kubernetes - kubectl CLI

kubectl on Apple Silicon is slated for Go 1.16. Following Install kubectl binary with curl on macOS, I wasn't able to run the binaries using Rosetta 2. However, Docker's [Apple M1 Tech Preview] comes with kubectl 1.19:

❯ kubectl.docker version
Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3", GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean", BuildDate:"2020-10-14T12:50:19Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"darwin/amd64"}

However, you can also use homebrew to install an Intel-emulated version of kubectl:

arch -x86_64 bash
export PATH="/usr/local/Homebrew/bin:$PATH"
brew install kubectl

Summary

That's it! If you have additional ideas and/or workarounds, I'm excited to hear about them in the comments.

New Call-to-action
blog comments powered by Disqus
Times
Check

Success!

Times

You're already subscribed

Times