Step 3 — Install Homebrew LLVM (The Full Toolchain)
Goal
Install upstream LLVM via Homebrew. This gives us the headers, libraries, and command-line tools (opt, llc, lli, mlir-opt, llvm-config) that Apple Clang does not include. Required from Phase 5 onward.
Why Apple Clang Isn't Enough
You already have Apple Clang from Step 1. It compiles C++ fine. But it lacks:
libLLVM.dylibC++ headers — needed to write a compiler that emits LLVM IR programmatically (Phase 5).llvm-config— the helper that tells CMake'sfind_package(LLVM)where to look.opt— the LLVM optimizer driver.opt -O3 input.ll > output.ll.llc— the LLVM static compiler. Lowers.ll→ target assembly.lli— the LLVM IR interpreter/JIT. Run.llfiles directly.mlir-opt,mlir-translate— MLIR'soptand bridge to LLVM (Phase 7).lld— LLVM's linker. Faster than systemld, used for cross-linking.
Apple intentionally omits these because Apple Clang is a product, not a development SDK. Homebrew LLVM fills the gap.
Install
brew install llvm
This installs to /opt/homebrew/opt/llvm/ on Apple Silicon (or /usr/local/opt/llvm/ on Intel Macs). Homebrew deliberately does NOT symlink to /usr/local/bin to avoid shadowing Apple Clang.
Disk: ~3 GB. Time: 5–10 minutes (cached binary download).
Make It Discoverable
Homebrew prints instructions; the relevant parts:
# Add to your ~/.zshrc (or ~/.bashrc):
export LLVM_HOME="/opt/homebrew/opt/llvm"
export PATH="$LLVM_HOME/bin:$PATH"
# For CMake's find_package(LLVM) and find_package(MLIR):
export LLVM_DIR="$LLVM_HOME/lib/cmake/llvm"
export MLIR_DIR="$LLVM_HOME/lib/cmake/mlir"
# For dyld to find LLVM libraries at runtime (rarely needed; CMake usually handles rpath):
# export DYLD_LIBRARY_PATH="$LLVM_HOME/lib:$DYLD_LIBRARY_PATH"
After editing, reload:
source ~/.zshrc
Why prepend
$LLVM_HOME/bintoPATH? Soclang++andclangrefer to Homebrew LLVM during compiler-engineering work. Apple Clang is still at/usr/bin/clang++for any tool that hard-codes that path.
Verify
which clang++
# Expected: /opt/homebrew/opt/llvm/bin/clang++
clang++ --version
# Expected: clang version 18.x.x or 19.x.x or 20.x.x
# (NOT "Apple clang")
llvm-config --version
# Expected: 18.x.x / 19.x.x / 20.x.x (matches clang++)
llvm-config --prefix
# Expected: /opt/homebrew/opt/llvm
llc --version | head -3
opt --version | head -3
lli --version | head -3
mlir-opt --version | head -3
All four should print a version line. If mlir-opt: command not found, your Homebrew LLVM is too old or wasn't built with MLIR. brew upgrade llvm should fix it (current Homebrew LLVM bottles include MLIR by default).
Inspect What's Available
ls "$LLVM_HOME/bin" | head -30
# clang, clang++, lld, ld64.lld, llc, lli, llvm-ar, llvm-as, llvm-cov, llvm-dis,
# llvm-dwarfdump, llvm-link, llvm-mc, llvm-nm, llvm-objcopy, llvm-objdump,
# llvm-readelf, llvm-readobj, llvm-rtdyld, llvm-symbolizer, llvm-undname,
# mlir-cpu-runner, mlir-opt, mlir-tblgen, mlir-translate, opt, ...
Every one of these is a tool you'll meet at some point.
The llvm-config Story
llvm-config is a small helper that prints LLVM build information. Try:
llvm-config --includedir
# /opt/homebrew/opt/llvm/include
llvm-config --libdir
# /opt/homebrew/opt/llvm/lib
llvm-config --libs core support irreader
# -lLLVMIRReader -lLLVMBitReader -lLLVMAsmParser -lLLVMCore -lLLVMRemarks ...
llvm-config --cxxflags
# -I/opt/homebrew/opt/llvm/include -std=c++17 -fno-exceptions -fno-rtti ...
CMake's find_package(LLVM) internally calls llvm-config to discover all of this. As long as LLVM_DIR is set (or llvm-config is on PATH), CMake "just works".
Try It — Compile and Run LLVM IR
Save this as /tmp/hello.ll:
@.str = private unnamed_addr constant [14 x i8] c"hello, llvm!\0A\00"
declare i32 @printf(ptr, ...)
define i32 @main() {
%1 = call i32 (ptr, ...) @printf(ptr @.str)
ret i32 0
}
Run it three different ways — each demonstrates a different tool:
# 1. Interpret/JIT directly (lli)
lli /tmp/hello.ll
# → hello, llvm!
# 2. Compile to assembly (llc), then assemble + link (clang++)
llc /tmp/hello.ll -o /tmp/hello.s
clang++ /tmp/hello.s -o /tmp/hello-static && /tmp/hello-static
# → hello, llvm!
# 3. Optimize, then compile (opt + llc)
opt -O3 /tmp/hello.ll -S -o /tmp/hello.opt.ll
llc /tmp/hello.opt.ll -o /tmp/hello.opt.s
clang++ /tmp/hello.opt.s -o /tmp/hello-opt && /tmp/hello-opt
# → hello, llvm!
You just performed all three roles of LLVM: as an interpreter (lli), as a static compiler (llc), and as an optimizer (opt). Every later lab will revisit these tools.
What Just Happened
- You installed upstream LLVM separately from Apple Clang.
- You prepended its
bintoPATHsoclang++now refers to Homebrew's, NOT Apple's. - You exported
LLVM_DIRso CMake'sfind_package(LLVM)works without extra flags. - You wrote raw LLVM IR by hand, then exercised
lli,llc, andopt— proof that the toolchain is wired correctly.
Common Pitfalls
clang++ --versionstill says "Apple clang": you forgot tosource ~/.zshrcor you opened a new terminal that doesn't load it. Check withecho $PATH | tr : '\n' | head.mlir-opt: command not found: very old Homebrew LLVM (pre-15). Runbrew upgrade llvm.dyld: Library not loaded: @rpath/libLLVM.dylib: a binary you built can't find its libs at runtime. Either add$LLVM_HOME/libtoDYLD_LIBRARY_PATH, or have CMake setINSTALL_RPATH(set(CMAKE_INSTALL_RPATH "${LLVM_HOME}/lib")).- CMake can't
find_package(LLVM): confirmLLVM_DIRis exported. Trycmake -DLLVM_DIR="$LLVM_HOME/lib/cmake/llvm" ...as a one-off.