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 (llvm@21)

No

Yes, default C++ compiler

Ships current libc++, std::print, better std::expected, modules.

CMake

No

Yes, for every project past drill #10

Industry-standard build config; produces compile_commands.json.

Ninja

No

Yes, as the CMake generator

Fast, quiet, parallel by default. Replace make.

Git

Yes (via Xcode CLT)

Yes

You already know why.

lldb

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)" — exports HOMEBREW_PREFIX, sets PATH and MANPATH for /opt/homebrew. Without this, brew may not be on your login shell’s PATH.

  • LLVM_PREFIX — one source of truth; if you later switch to llvm@22, change it once.

  • PATH="$LLVM_PREFIX/bin:$PATH" — puts brew LLVM’s clang++, clang-tidy, clang-format, lldb ahead of Apple’s versions. This is the switch that makes clang++ mean brew LLVM.

  • LDFLAGS — tells the linker to prefer brew’s libc++ and its bundled libunwind. Without this, you can compile C++23 headers that reference symbols missing from Apple’s libc++ 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 default Unix Makefiles. ninja is 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

-std=c++20

Tells the compiler which C++ standard to accept. Some tests in Phase 0 use -std=c++23.

Without this, clang defaults to gnu++17 and rejects std::span.

-Wall

Enable “all” common warnings (a misnomer — not literally all, just the common ones).

Catches uninitialized vars, unused vars, obvious mistakes.

-Wextra

The rest of the common warnings that -Wall skipped for backward compat.

Catches sign-comparison, missing field initializers, empty bodies.

-Wpedantic

Warn on anything not strictly conforming to the ISO C++ standard.

Portability. Also catches GNU extensions you didn’t mean to use.

-Werror

Turn every warning into a compile error.

The single most important flag on this list. Warnings are bugs.

-O2

Optimization level 2. -O0 for debug builds, -O2 for release / correctness testing.

Some UB bugs surface only when the optimizer inlines. Ship with -O2.

-g

Emit DWARF debug symbols.

lldb needs this to show source lines and variable names.

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

fatal error: 'print' file not found

Using Apple Clang; its libc++ lacks <print>.

Confirm which clang++ is /opt/homebrew/opt/llvm/bin/clang++. Re-source .zshrc.

undefined symbol: std::__1::...

Mixed linking: compiled with brew LLVM, linked against Apple libc++.

Add the LDFLAGS block from Step 4.

xcrun: error: invalid active developer path

Xcode CLT missing or moved.

xcode-select --install, then sudo xcode-select --reset.

CMake picks Apple Clang despite CC/CXX env vars

You configured once, then changed env; CMake cached the old compiler.

rm -rf build && cmake -B build. Never -DCMAKE_CXX_COMPILER= inside a stale build dir.

zsh: command not found: clang++ after edit

.zshrc didn’t source, or wrong file (.bashrc, .profile).

source ~/.zshrc in the terminal you’re using; check echo $0 says -zsh.

ld: warning: object file was built for newer macOS version

Toolchain SDK newer than deployment target. Usually harmless.

Set -mmacosx-version-min=14.0 if you need to silence it.

Sanitizer runs but reports zero errors on obviously-broken code

-fsanitize=... at compile time only; also needed at link time.

Pass sanitizer flag to both CXXFLAGS and LDFLAGS (see Makefile above).

lldb doesn’t stop at breakpoints

Built with -O2 — optimizer inlined the function or moved the line.

Rebuild with -O0 -g.


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::expected example fails to link, and they blame C++. It’s not C++ — it’s that they never learned which libc++ 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 -p prints the CLT path.

  • which clang++ prints /opt/homebrew/opt/llvm/bin/clang++.

  • clang++ --version says Homebrew clang 21.x or newer.

  • hello.cpp using <print> compiles with the full flag set and runs.

  • You have stepped through hello in lldb and 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, -g does.


Return to Phase 0 README · Next: 02_editor_and_debugger.md