01 — Toolchain Setup (macOS Apple Silicon)¶
Target machine: MacBook, Apple Silicon (M1/M2/M3/M4), macOS 14+ (Sonoma, Sequoia, or later). Time budget: 2 focused evenings (~4 hours) end-to-end, including a hello-world compile and one debugger session.
The goal of this document is not to install five tools. It is to install four tools with intent, understand why each exists, and finish with a compiler that can build C++20 code cleanly and a debugger that can step through it. Every command below has a reason. Read the reasons.
The tools you install, and why each one¶
Apple ships a lot of this already. Some of what ships is fine. Some of it is a trap. Here is the honest map:
Tool |
Ships with macOS? |
Do you use it? |
Why |
|---|---|---|---|
Apple Clang |
Yes (via Xcode CLT) |
Only as fallback / for Objective-C |
Lags mainline LLVM by 1–2 major versions; missing C++23 library bits. |
Homebrew LLVM ( |
No |
Yes, default C++ compiler |
Ships current |
CMake |
No |
Yes, for every project past drill #10 |
Industry-standard build config; produces |
Ninja |
No |
Yes, as the CMake generator |
Fast, quiet, parallel by default. Replace |
Git |
Yes (via Xcode CLT) |
Yes |
You already know why. |
|
Yes (via Xcode CLT) |
Yes |
Debugger. The LLDB from brew LLVM matches the compiler version. |
Rule of thumb: For anything C++20/23 involving <print>, <expected>, <ranges>, or modules, use the brew clang++. For quick one-off scripts where you don’t care about library features, Apple Clang is fine.
Step 1 — Xcode Command Line Tools¶
Even though you will not use Apple Clang as your main compiler, you need the CLT for the macOS SDK headers (/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk), for git, and for lldb fallback.
xcode-select --install
If a dialog pops up, click Install. If it errors with “command line tools are already installed,” you are done with this step. Verify:
xcode-select -p
# expected: /Library/Developer/CommandLineTools
Step 2 — Homebrew¶
On Apple Silicon, Homebrew installs to /opt/homebrew, not /usr/local. This matters for every PATH decision below.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
After the install completes, Homebrew prints two lines telling you to add its shellenv to your profile. Do exactly what it says. Then verify:
which brew
# expected: /opt/homebrew/bin/brew
brew --version
Step 3 — The C++ toolchain proper¶
One command. Read it, understand it, then run it.
brew install llvm cmake ninja git
This pulls in the latest packaged LLVM (as of 2026 that is 21.x or 22.x; the current llvm formula tracks llvm@21 or newer). Homebrew installs it into /opt/homebrew/opt/llvm/ and — critically — does not put it on your PATH by default. That’s a safety choice by brew: they don’t want to shadow Apple Clang for users who aren’t paying attention. You are paying attention, so you do want to shadow it. That is what Step 4 does.
Brew will also print a hint after install that looks like this:
To use the bundled libc++ please use the following LDFLAGS:
LDFLAGS="-L/opt/homebrew/opt/llvm/lib/c++ -L/opt/homebrew/opt/llvm/lib/unwind -lunwind"
Save that. You will need it when you want the newer libc++ features that Apple’s shipped libc++ does not yet include.
Step 4 — ~/.zshrc configuration¶
Add the following block to the end of ~/.zshrc. Open it with your editor of choice (nano ~/.zshrc, code ~/.zshrc, etc.) and paste:
# ---- Homebrew (Apple Silicon) --------------------------------------------
eval "$(/opt/homebrew/bin/brew shellenv)"
# ---- C++ toolchain: prefer Homebrew LLVM over Apple Clang ----------------
export LLVM_PREFIX="$(brew --prefix llvm)"
export PATH="$LLVM_PREFIX/bin:$PATH"
export LDFLAGS="-L$LLVM_PREFIX/lib/c++ -L$LLVM_PREFIX/lib/unwind -lunwind"
export CPPFLAGS="-I$LLVM_PREFIX/include"
# Convenience defaults for CMake
export CC="$LLVM_PREFIX/bin/clang"
export CXX="$LLVM_PREFIX/bin/clang++"
# Ninja as the default CMake generator
export CMAKE_GENERATOR="Ninja"
# Useful aliases
alias c++build='cmake -B build -G Ninja && cmake --build build'
alias c++clean='rm -rf build'
Line-by-line rationale:
eval "$(brew shellenv)"— exportsHOMEBREW_PREFIX, setsPATHandMANPATHfor/opt/homebrew. Without this,brewmay not be on your login shell’sPATH.LLVM_PREFIX— one source of truth; if you later switch tollvm@22, change it once.PATH="$LLVM_PREFIX/bin:$PATH"— puts brew LLVM’sclang++,clang-tidy,clang-format,lldbahead of Apple’s versions. This is the switch that makesclang++mean brew LLVM.LDFLAGS— tells the linker to prefer brew’slibc++and its bundledlibunwind. Without this, you can compile C++23 headers that reference symbols missing from Apple’slibc++and get baffling linker errors at the very end of a long build.CPPFLAGS— preprocessor include path. Rarely needed but harmless.CC/CXX— CMake reads these on first configure. If you forget to set them, CMake picks Apple Clang silently.CMAKE_GENERATOR=Ninja— replaces the defaultUnix Makefiles.ninjais quiet by default, prints only errors, builds in parallel automatically, and re-links minimally.
Apply and verify:
source ~/.zshrc
which clang++
# expected: /opt/homebrew/opt/llvm/bin/clang++ (NOT /usr/bin/clang++)
clang++ --version
# expected first line: Homebrew clang version 21.x or 22.x
If which clang++ still points to /usr/bin/clang++, your .zshrc did not load. Open a fresh terminal window and re-run.
Step 5 — First hello.cpp¶
Make a scratch directory. Do not put this in your roadmap folder; make it a throwaway.
mkdir -p ~/cpp-scratch/hello && cd ~/cpp-scratch/hello
Create hello.cpp:
#include <print> // C++23 header; requires brew LLVM libc++
#include <string_view>
int main() {
constexpr std::string_view who = "Raghul";
std::println("Hello, {}. Toolchain works.", who);
return 0;
}
Compile it. Type the flags by hand — do not copy-paste from here on the first run. You need to feel them.
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -O2 -g hello.cpp -o hello
./hello
# expected: Hello, Raghul. Toolchain works.
Every flag, explained¶
Memorize this table. You will use these flags on every build for twelve months.
Flag |
What it does |
Why you want it |
|---|---|---|
|
Tells the compiler which C++ standard to accept. Some tests in Phase 0 use |
Without this, clang defaults to |
|
Enable “all” common warnings (a misnomer — not literally all, just the common ones). |
Catches uninitialized vars, unused vars, obvious mistakes. |
|
The rest of the common warnings that |
Catches sign-comparison, missing field initializers, empty bodies. |
|
Warn on anything not strictly conforming to the ISO C++ standard. |
Portability. Also catches GNU extensions you didn’t mean to use. |
|
Turn every warning into a compile error. |
The single most important flag on this list. Warnings are bugs. |
|
Optimization level 2. |
Some UB bugs surface only when the optimizer inlines. Ship with |
|
Emit DWARF debug symbols. |
|
Common additional flags you’ll add later:
-fsanitize=address,undefined— turn on AddressSanitizer + UBSan. Slower, but catches use-after-free and signed overflow instantly. Use for tests, not for release.-stdlib=libc++— usually implied on macOS but explicit doesn’t hurt.-fno-omit-frame-pointer— pair with sanitizers for readable stack traces.
Step 6 — Debugger workflow with lldb¶
Build the same program with debug flags only (no optimization, so line numbers match):
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -O0 -g hello.cpp -o hello
lldb ./hello
Inside lldb:
(lldb) breakpoint set --file hello.cpp --line 6 # or: b hello.cpp:6
(lldb) run
(lldb) frame variable # or: fr v — show all locals
(lldb) print who # or: p who
(lldb) thread step-over # or: n — next line
(lldb) thread step-in # or: s — into call
(lldb) thread step-out # or: finish
(lldb) bt # backtrace
(lldb) continue # or: c
(lldb) quit
Short form is what you’ll actually type: b, r, n, s, c, p, bt, q. Learn these six. That’s the whole workflow.
Inspecting a pointer’s contents — this is the trick most returning devs forget:
(lldb) p *ptr # dereference and print
(lldb) memory read --size 4 --format d --count 10 ptr # read 10 ints starting at ptr
(lldb) p some_vector # lldb has a pretty-printer for std::vector
(lldb) p some_vector[3] # index into it
Step 7 — A Makefile starter¶
Before you learn CMake, one Makefile for tiny throwaway programs. Save as ~/cpp-scratch/Makefile.template:
# Toolchain
CXX := clang++
CXXSTD := -std=c++20
WARN := -Wall -Wextra -Wpedantic -Werror
OPT := -O2
DBG := -g
SAN := -fsanitize=address,undefined -fno-omit-frame-pointer
CXXFLAGS := $(CXXSTD) $(WARN) $(OPT) $(DBG)
LDFLAGS :=
# Sources
SRC := $(wildcard *.cpp)
BIN := $(patsubst %.cpp,%,$(SRC))
.PHONY: all clean debug sanitize
all: $(BIN)
%: %.cpp
$(CXX) $(CXXFLAGS) $(LDFLAGS) $< -o $@
debug: CXXFLAGS := $(CXXSTD) $(WARN) -O0 $(DBG)
debug: all
sanitize: CXXFLAGS := $(CXXSTD) $(WARN) -O1 $(DBG) $(SAN)
sanitize: LDFLAGS := $(SAN)
sanitize: all
clean:
rm -f $(BIN)
Usage: make, make debug, make sanitize, make clean. Copy this into every drill folder in W1–W2 before you graduate to CMake in W3.
Step 8 — First CMake project¶
By the end of W1, you should be building through CMake, not raw Makefiles. Bare minimum:
cmake_minimum_required(VERSION 3.28)
project(hello CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # for clangd
add_compile_options(-Wall -Wextra -Wpedantic -Werror)
add_executable(hello hello.cpp)
Build:
cmake -B build -G Ninja
cmake --build build
./build/hello
That CMAKE_EXPORT_COMPILE_COMMANDS ON line is not optional. clangd needs compile_commands.json to give you real autocomplete. Details in 02_editor_and_debugger.md.
Common breakage — and the fix for each¶
Every item below has bitten a returning C++ dev in their first week on Apple Silicon. If you hit one, don’t guess — match the symptom exactly.
Symptom |
Cause |
Fix |
|---|---|---|
|
Using Apple Clang; its |
Confirm |
|
Mixed linking: compiled with brew LLVM, linked against Apple |
Add the |
|
Xcode CLT missing or moved. |
|
CMake picks Apple Clang despite |
You configured once, then changed env; CMake cached the old compiler. |
|
|
|
|
|
Toolchain SDK newer than deployment target. Usually harmless. |
Set |
Sanitizer runs but reports zero errors on obviously-broken code |
|
Pass sanitizer flag to both |
|
Built with |
Rebuild with |
What most people get wrong¶
Returning devs treat toolchain setup as a checkbox: install five things, close the terminal, never think about compiler flags again. Then two months later they can’t explain why a
std::expectedexample fails to link, and they blame C++. It’s not C++ — it’s that they never learned whichlibc++they’re linking against. Spend a full evening on this file. Understand every export in your~/.zshrc. If you can’t explain a line, delete it and see what breaks.
Exit checklist for this file¶
xcode-select -pprints the CLT path.which clang++prints/opt/homebrew/opt/llvm/bin/clang++.clang++ --versionsays Homebrew clang 21.x or newer.hello.cppusing<print>compiles with the full flag set and runs.You have stepped through
helloinlldband printed a local variable.CMake + Ninja builds a project and emits
build/compile_commands.json.You can explain, out loud, what each of
-Wall,-Wextra,-Wpedantic,-Werror,-O2,-gdoes.
Return to Phase 0 README · Next: 02_editor_and_debugger.md