Cross-compiling Rust Lambdas on macOS without Docker

The standard way of developing Lambda functions in Rust is to use a custom Lambda runtime provided by AWS and cross-compile everything – one binary per function – before deployment.

The runtime documentation now (finally!) contains a section called “Building for Amazon Linux 2” that explains a more recent way of cross-compiling Lambdas using Tier 1 targets without resorting to musl.

For example, if you prefer arm64 over x86_64 as I do, you typically run these commands to build Lambdas for that architecture:

# Add ARM64 target platform once
rustup target add aarch64-unknown-linux-gnu

# Build Lambda functions
cargo build --target aarch64-unknown-linux-gnu --release

Easy, right? Unfortunately, that’s not the end of the story, not on macOS anyway.

As it happens, rustup target add only installs the Rust standard library for a given target. You still need a linker for the target platform and a cross compiler for crates that include C/C++ code.1

As luck would have it, you can get both for macOS by installing one of the cross compiler toolchains provided by this lovely project:

brew tap messense/macos-cross-toolchains
brew install aarch64-unknown-linux-gnu

Afterward, set these variables in your environment (e.g. in bashrc):

export CC_aarch64_unknown_linux_gnu=aarch64-unknown-linux-gnu-gcc
export CXX_aarch64_unknown_linux_gnu=aarch64-unknown-linux-gnu-g++
export AR_aarch64_unknown_linux_gnu=aarch64-unknown-linux-gnu-ar
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-unknown-linux-gnu-gcc

Now cargo build --target aarch64-unknown-linux-gnu will work as expected on macOS and produce binaries ready to be deployed to AWS Lambda – all without Docker.


One more tip before wrapping up: the easiest way to avoid any OpenSSL cross-compile issues under macOS is not using it in the first place. Use rustls instead. Most packages have a feature flag for it. Here’s an example from dilbert-feed:

# Cargo.toml
[dependencies]
aws-sdk-s3 = { version = "0.2", features = ["rustls"] }
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }

  1. This might be surprising to Go programmers who are used to GOOS=... GOARCH=... go build working out of the box.