# C Development Mac Tahoe Setup
Adding C development on top of an Apple Silicon Mac already configured per [[General_Development_Mac_Tahoe_Setup]]. Most of the stack carries over — Ghostty, Starship, Git, GitHub CLI, VS Code, Claude Code, and the zsh setup all work identically. This guide only covers the C-specific additions.
> [!tip] AI assistance is optional
> The reference to Claude Code above assumes you completed Part 7 of the General guide. If you skipped it (which is fine — your dev environment works without any AI) or use a different AI tool (Copilot, Cursor, Continue, etc.), this guide's instructions still work — the language toolchain doesn't depend on any AI being present.
> [!info] Coexistence
> C, Python, Ruby, Go, and Rust can all live on the same system without interfering. Each language's toolchain installs to its own prefix: `uv` under `~/.local/share/uv/`, `rv` under `~/.rv/`, Go under `~/go/`, Rust under `~/.rustup/` and `~/.cargo/`, C/CMake under Homebrew. No conflicts.
---
## What you already have
If you followed the Python setup guide, Xcode Command Line Tools came with your initial setup, which means you already have:
| Tool | What it is | Path |
|------|------------|------|
| `clang` | C compiler (Apple's LLVM build, native arm64) | `/usr/bin/clang` |
| `clang++` | C++ compiler | `/usr/bin/clang++` |
| `lldb` | Debugger (LLVM) | `/usr/bin/lldb` |
| `make` | Build automation (GNU Make 3.81) | `/usr/bin/make` |
| `ar`, `ld`, `as` | Binary utilities | `/usr/bin/` |
Verify:
```bash
clang --version
lldb --version
make --version
xcode-select -p
```
If anything's missing, run `xcode-select --install`.
---
## What to add
```bash
brew install cmake ninja llvm
```
| Tool | Why |
|------|-----|
| `cmake` | De-facto standard build system configurator; most open-source C projects use it |
| `ninja` | Fast build tool that CMake targets instead of `make` (much faster on large projects) |
| `llvm` | Homebrew's newer LLVM — provides `clangd` (language server), `clang-format`, `clang-tidy`, newer `clang`, and `lldb-dap` |
### Why Homebrew LLVM *and* Apple Clang?
Apple Clang compiles and debugs fine. But it doesn't ship `clangd`, `clang-format`, or `clang-tidy` — the tools that power modern editor intelligence and code style enforcement. Homebrew's `llvm` gives you those without replacing Apple Clang.
The Homebrew LLVM binaries are **not on your PATH by default** because they'd shadow Apple's. To make them discoverable:
```bash
# Add to ~/.zshrc (optional — needed for clangd, clang-format, clang-tidy)
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
```
Reload with `source ~/.zshrc`.
### Debugger choice: lldb, not gdb
On Apple Silicon, **use `lldb`**. `gdb` via Homebrew requires manual code-signing to attach to processes and isn't worth the hassle for standard workflows. `lldb` is already installed, natively arm64, actively maintained by Apple, and supported by VS Code via the modern `lldb-dap` adapter.
The only reason to install `gdb` on macOS in 2026 is cross-compiling for embedded Linux targets — and even then, you'd want `gdb-multiarch` or a target-specific toolchain rather than plain `brew install gdb`.
---
## VS Code extensions for C
```bash
# Microsoft's C/C++ extension (IntelliSense, cppdbg debugger)
code --install-extension ms-vscode.cpptools
code --install-extension ms-vscode.cpptools-extension-pack
# CMake integration — configure, build, and run from the command palette
code --install-extension ms-vscode.cmake-tools
code --install-extension twxs.cmake
# Modern LLDB debug adapter (better than cppdbg on Apple Silicon)
code --install-extension llvm-vs-code-extensions.lldb-dap
# clangd — language server alternative to Microsoft's IntelliSense
code --install-extension llvm-vs-code-extensions.vscode-clangd
```
> [!warning] clangd vs Microsoft IntelliSense — pick one
> Running both causes dueling diagnostics. Convention: if you install `vscode-clangd`, disable Microsoft's IntelliSense with `"C_Cpp.intelliSenseEngine": "disabled"`. If you prefer Microsoft's, skip the clangd extension. clangd is faster and more standards-correct; Microsoft's is more beginner-friendly.
---
## VS Code settings
Append to your existing `settings.json` (`~/Library/Application Support/Code/User/settings.json`):
```json
{
// ── C/C++ ─────────────────────────────────────────────────
"[c]": {
"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd",
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnSave": true
},
"[cpp]": {
"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd",
"editor.tabSize": 4,
"editor.formatOnSave": true
},
// Disable Microsoft IntelliSense if using clangd (pick one)
"C_Cpp.intelliSenseEngine": "disabled",
// clangd settings
"clangd.arguments": [
"--background-index",
"--clang-tidy",
"--header-insertion=iwyu",
"--completion-style=detailed"
],
// CMake Tools
"cmake.configureOnOpen": true,
"cmake.buildDirectory": "${workspaceFolder}/build",
"cmake.generator": "Ninja"
}
```
---
## Full demo: A calculator program
This walks through building, running, debugging, and publishing a simple four-function calculator from scratch.
### Create the project
```bash
cd ~/projects
mkdir calc-c && cd calc-c
```
### Project layout
Refactor into a library + CLI so the operations can be unit-tested:
```
calc-c/
├── CMakeLists.txt
├── .clang-format
├── .gitignore
├── src/
│ ├── calc_lib.h
│ ├── calc_lib.c
│ └── main.c
└── tests/
├── unity.h
├── unity.c
└── test_calc.c
```
Create the directories:
```bash
mkdir -p src tests
```
### `src/calc_lib.h` — the public API
```c
#ifndef CALC_LIB_H
#define CALC_LIB_H
/* Result type: carries either a value or an error */
typedef enum {
CALC_OK = 0,
CALC_ERR_DIVIDE_BY_ZERO,
CALC_ERR_UNKNOWN_OP,
} calc_status_t;
typedef struct {
calc_status_t status;
double value;
} calc_result_t;
calc_result_t calc_add(double a, double b);
calc_result_t calc_sub(double a, double b);
calc_result_t calc_mul(double a, double b);
calc_result_t calc_div(double a, double b);
/* Dispatch by operator string: "+", "-", "*", "x", "/" */
calc_result_t calc_calculate(double a, const char *op, double b);
#endif /* CALC_LIB_H */
```
### `src/calc_lib.c` — implementation
```c
#include "calc_lib.h"
#include <string.h>
calc_result_t calc_add(double a, double b) {
return (calc_result_t){CALC_OK, a + b};
}
calc_result_t calc_sub(double a, double b) {
return (calc_result_t){CALC_OK, a - b};
}
calc_result_t calc_mul(double a, double b) {
return (calc_result_t){CALC_OK, a * b};
}
calc_result_t calc_div(double a, double b) {
if (b == 0.0) {
return (calc_result_t){CALC_ERR_DIVIDE_BY_ZERO, 0.0};
}
return (calc_result_t){CALC_OK, a / b};
}
calc_result_t calc_calculate(double a, const char *op, double b) {
if (strcmp(op, "+") == 0) return calc_add(a, b);
if (strcmp(op, "-") == 0) return calc_sub(a, b);
if (strcmp(op, "*") == 0 || strcmp(op, "x") == 0) return calc_mul(a, b);
if (strcmp(op, "/") == 0) return calc_div(a, b);
return (calc_result_t){CALC_ERR_UNKNOWN_OP, 0.0};
}
```
### `src/main.c` — the CLI
```c
#include <stdio.h>
#include <stdlib.h>
#include "calc_lib.h"
static void print_usage(const char *prog) {
fprintf(stderr, "Usage: %s <number> <op> <number>\n", prog);
fprintf(stderr, " op: + - * /\n");
fprintf(stderr, "Example: %s 6 + 7\n", prog);
}
int main(int argc, char *argv[]) {
if (argc != 4) {
print_usage(argv[0]);
return 1;
}
double a = strtod(argv[1], NULL);
double b = strtod(argv[3], NULL);
calc_result_t result = calc_calculate(a, argv[2], b);
switch (result.status) {
case CALC_OK:
printf("%g\n", result.value);
return 0;
case CALC_ERR_DIVIDE_BY_ZERO:
fprintf(stderr, "Error: division by zero\n");
return 1;
case CALC_ERR_UNKNOWN_OP:
fprintf(stderr, "Error: unknown operator '%s'\n", argv[2]);
print_usage(argv[0]);
return 1;
}
return 1;
}
```
### Add Unity for unit testing
[Unity](https://github.com/ThrowTheSwitch/Unity) is a minimal C test framework — three files total. Download it:
```bash
curl -LO https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity.h
curl -LO https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity.c
curl -LO https://raw.githubusercontent.com/ThrowTheSwitch/Unity/master/src/unity_internals.h
mv unity.h unity.c unity_internals.h tests/
```
### `tests/test_calc.c` — unit tests
```c
#include "unity.h"
#include "calc_lib.h"
void setUp(void) {}
void tearDown(void) {}
void test_add(void) {
calc_result_t r = calc_calculate(6, "+", 7);
TEST_ASSERT_EQUAL_INT(CALC_OK, r.status);
TEST_ASSERT_EQUAL_DOUBLE(13.0, r.value);
}
void test_sub(void) {
calc_result_t r = calc_calculate(10, "-", 3);
TEST_ASSERT_EQUAL_INT(CALC_OK, r.status);
TEST_ASSERT_EQUAL_DOUBLE(7.0, r.value);
}
void test_mul_star(void) {
calc_result_t r = calc_calculate(4, "*", 5);
TEST_ASSERT_EQUAL_INT(CALC_OK, r.status);
TEST_ASSERT_EQUAL_DOUBLE(20.0, r.value);
}
void test_mul_x(void) {
calc_result_t r = calc_calculate(4, "x", 5);
TEST_ASSERT_EQUAL_INT(CALC_OK, r.status);
TEST_ASSERT_EQUAL_DOUBLE(20.0, r.value);
}
void test_div(void) {
calc_result_t r = calc_calculate(20, "/", 4);
TEST_ASSERT_EQUAL_INT(CALC_OK, r.status);
TEST_ASSERT_EQUAL_DOUBLE(5.0, r.value);
}
void test_div_by_zero(void) {
calc_result_t r = calc_calculate(5, "/", 0);
TEST_ASSERT_EQUAL_INT(CALC_ERR_DIVIDE_BY_ZERO, r.status);
}
void test_unknown_operator(void) {
calc_result_t r = calc_calculate(1, "?", 2);
TEST_ASSERT_EQUAL_INT(CALC_ERR_UNKNOWN_OP, r.status);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_add);
RUN_TEST(test_sub);
RUN_TEST(test_mul_star);
RUN_TEST(test_mul_x);
RUN_TEST(test_div);
RUN_TEST(test_div_by_zero);
RUN_TEST(test_unknown_operator);
return UNITY_END();
}
```
### `CMakeLists.txt` — the build configuration
```cmake
cmake_minimum_required(VERSION 3.20)
project(calc C)
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# The calculator library (reusable by both CLI and tests)
add_library(calc_lib STATIC src/calc_lib.c)
target_include_directories(calc_lib PUBLIC src)
target_compile_options(calc_lib PRIVATE -Wall -Wextra -Wpedantic)
# The CLI executable
add_executable(calc src/main.c)
target_link_libraries(calc PRIVATE calc_lib)
target_compile_options(calc PRIVATE -Wall -Wextra -Wpedantic)
# Tests (only built if testing is enabled)
enable_testing()
add_executable(test_calc tests/test_calc.c tests/unity.c)
target_link_libraries(test_calc PRIVATE calc_lib)
target_include_directories(test_calc PRIVATE tests)
add_test(NAME unit_tests COMMAND test_calc)
```
The `CMAKE_EXPORT_COMPILE_COMMANDS ON` line is important — it generates `build/compile_commands.json`, which `clangd` reads to understand include paths and flags. Without it, clangd shows spurious errors.
### `.clang-format` — code style (optional but recommended)
```yaml
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 100
AllowShortFunctionsOnASingleLine: Inline
```
### `.gitignore`
```
build/
*.o
*.a
*.dylib
*.dSYM/
```
### Build and run
```bash
# Configure (generates build/ with Ninja build files)
cmake -B build -G Ninja
# Build
cmake --build build
# Run the CLI
./build/calc 6 + 7
./build/calc 10 - 3
./build/calc 4 x 5
./build/calc 20 / 4
# Run unit tests
./build/test_calc
# Or via CTest:
cd build && ctest --output-on-failure && cd ..
```
### Debug in VS Code
Create `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug calc",
"type": "lldb-dap",
"request": "launch",
"program": "${workspaceFolder}/build/calc",
"args": ["6", "+", "7"],
"cwd": "${workspaceFolder}",
"stopOnEntry": false
}
]
}
```
Set a breakpoint on the `add` function, hit **F5**, and you'll step through native arm64 code with full variable inspection.
### Publish to GitHub
Same workflow as any project (see [[General_Development_Mac_Tahoe_Setup#Publishing a repo to GitHub]]):
```bash
git init
git add .
git commit -m "Initial commit: four-function calculator"
gh repo create calc-c --private --source=. --remote=origin --push
```
### `CLAUDE.md` for this project
```markdown
# Project: calc-c
## Purpose
Four-function command-line calculator in C — pedagogical example.
## Conventions
- C17 standard, compiled with Apple clang
- Library (src/calc_lib.{c,h}) + CLI (src/main.c) + tests (tests/test_calc.c)
- Build: CMake + Ninja (build dir: ./build/)
- Format: clang-format (LLVM base, 4-space indent, 100-col limit)
- Warnings: -Wall -Wextra -Wpedantic
- Tests with Unity (bundled in tests/)
## Commands
- Configure: `cmake -B build -G Ninja`
- Build: `cmake --build build`
- Run CLI: `./build/calc <a> <op> <b>`
- Run tests: `./build/test_calc` or `cd build && ctest --output-on-failure`
- Format: `clang-format -i src/*.c src/*.h tests/test_calc.c`
- Clean: `rm -rf build/`
## Style
- snake_case for functions and variables
- SCREAMING_SNAKE_CASE for macros and enum values
- Static linkage for internal helpers
- Return calc_result_t (status + value) instead of errno-style out-params
```
---
## Full demo: A calculator GUI
Same calculator, wrapped in a graphical interface. This uses **Raylib**, a simple, modern, Homebrew-installable graphics library that runs natively on Apple Silicon.
### Why Raylib?
| Option | Pros | Cons |
|--------|------|------|
| **Raylib** | Single dependency, Homebrew-installable, modern, native arm64, simple immediate-mode API | Custom widget look (not native macOS controls) |
| **GTK** | Full widget toolkit, cross-platform | Heavy dependency chain, dated macOS integration |
| **SDL2** | Ubiquitous for games/media | Lower-level than Raylib — you build your own buttons |
| **Dear ImGui** | Excellent for debug UIs and tools | Binds are C++ (cimgui wraps to C) |
For a pedagogical calculator in C, Raylib is the right default. It's one dependency, one header include, and the API is designed to be learnable in an afternoon.
### Install Raylib
```bash
brew install raylib
```
This installs the Raylib headers and library to `/opt/homebrew/`.
### `src/calc_gui.c` — the GUI
```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "raylib.h"
#include "calc_lib.h"
#define SCREEN_WIDTH 300
#define SCREEN_HEIGHT 400
#define BUTTON_WIDTH 65
#define BUTTON_HEIGHT 60
#define PADDING 10
#define DISPLAY_HEIGHT 70
typedef struct {
const char *label;
int row;
int col;
} button_t;
static const button_t BUTTONS[] = {
{"7", 0, 0}, {"8", 0, 1}, {"9", 0, 2}, {"/", 0, 3},
{"4", 1, 0}, {"5", 1, 1}, {"6", 1, 2}, {"*", 1, 3},
{"1", 2, 0}, {"2", 2, 1}, {"3", 2, 2}, {"-", 2, 3},
{"0", 3, 0}, {".", 3, 1}, {"=", 3, 2}, {"+", 3, 3},
};
static const int NUM_BUTTONS = sizeof(BUTTONS) / sizeof(BUTTONS[0]);
typedef struct {
char display[64];
char current[32];
double stored;
char pending_op[4];
int has_stored;
} calc_state_t;
static void state_reset(calc_state_t *s) {
s->current[0] = '\0';
s->stored = 0.0;
s->pending_op[0] = '\0';
s->has_stored = 0;
strcpy(s->display, "0");
}
static void state_append(calc_state_t *s, const char *ch) {
size_t len = strlen(s->current);
if (len + strlen(ch) < sizeof(s->current) - 1) {
strcat(s->current, ch);
strcpy(s->display, s->current);
}
}
static void state_apply_pending(calc_state_t *s) {
if (s->current[0] == '\0') return;
double value = strtod(s->current, NULL);
if (!s->has_stored || s->pending_op[0] == '\0') {
s->stored = value;
s->has_stored = 1;
} else {
calc_result_t r = calc_calculate(s->stored, s->pending_op, value);
if (r.status != CALC_OK) {
strcpy(s->display, "Error");
state_reset(s);
strcpy(s->display, "Error");
return;
}
s->stored = r.value;
}
/* Format: integer if whole, otherwise %g */
if (s->stored == (long long)s->stored) {
snprintf(s->display, sizeof(s->display), "%lld", (long long)s->stored);
} else {
snprintf(s->display, sizeof(s->display), "%g", s->stored);
}
s->current[0] = '\0';
}
static void state_handle_press(calc_state_t *s, const char *label) {
if ((label[0] >= '0' && label[0] <= '9') || label[0] == '.') {
state_append(s, label);
} else if (strcmp(label, "+") == 0 || strcmp(label, "-") == 0 ||
strcmp(label, "*") == 0 || strcmp(label, "/") == 0) {
state_apply_pending(s);
strncpy(s->pending_op, label, sizeof(s->pending_op) - 1);
} else if (strcmp(label, "=") == 0) {
state_apply_pending(s);
s->pending_op[0] = '\0';
}
}
int main(void) {
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Calculator");
SetTargetFPS(60);
calc_state_t state;
state_reset(&state);
const int grid_start_x = PADDING;
const int grid_start_y = PADDING + DISPLAY_HEIGHT + PADDING;
while (!WindowShouldClose()) {
/* Keyboard input */
int key = GetCharPressed();
while (key > 0) {
char ch[2] = {(char)key, '\0'};
if ((key >= '0' && key <= '9') || key == '.' ||
key == '+' || key == '-' || key == '*' ||
key == '/' || key == '=') {
state_handle_press(&state, ch);
}
key = GetCharPressed();
}
if (IsKeyPressed(KEY_ENTER)) state_handle_press(&state, "=");
if (IsKeyPressed(KEY_ESCAPE)) state_reset(&state);
if (IsKeyPressed(KEY_BACKSPACE)) {
size_t len = strlen(state.current);
if (len > 0) {
state.current[len - 1] = '\0';
if (state.current[0] == '\0') {
strcpy(state.display, "0");
} else {
strcpy(state.display, state.current);
}
}
}
/* Draw */
BeginDrawing();
ClearBackground((Color){30, 30, 35, 255});
/* Display */
Rectangle display_rect = {PADDING, PADDING, SCREEN_WIDTH - 2 * PADDING, DISPLAY_HEIGHT};
DrawRectangleRec(display_rect, (Color){50, 50, 55, 255});
DrawRectangleLinesEx(display_rect, 1, GRAY);
int display_width = MeasureText(state.display, 28);
DrawText(state.display,
display_rect.x + display_rect.width - display_width - 10,
display_rect.y + 20, 28, RAYWHITE);
/* Buttons */
Vector2 mouse = GetMousePosition();
int mouse_clicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);
for (int i = 0; i < NUM_BUTTONS; i++) {
button_t btn = BUTTONS[i];
Rectangle r = {
grid_start_x + btn.col * (BUTTON_WIDTH + 5),
grid_start_y + btn.row * (BUTTON_HEIGHT + 5),
BUTTON_WIDTH, BUTTON_HEIGHT
};
int hovered = CheckCollisionPointRec(mouse, r);
int is_op = (strchr("+-*/=", btn.label[0]) != NULL);
Color fill = is_op ? (Color){200, 120, 40, 255}
: (Color){70, 70, 75, 255};
if (hovered) fill = (Color){fill.r + 30, fill.g + 30, fill.b + 30, 255};
DrawRectangleRec(r, fill);
int tw = MeasureText(btn.label, 20);
DrawText(btn.label, r.x + (r.width - tw) / 2, r.y + 20, 20, RAYWHITE);
if (hovered && mouse_clicked) {
state_handle_press(&state, btn.label);
}
}
EndDrawing();
}
CloseWindow();
return 0;
}
```
### Update `CMakeLists.txt` to build the GUI
Append to `CMakeLists.txt`:
```cmake
# GUI executable (requires raylib from Homebrew)
find_package(raylib QUIET)
if(raylib_FOUND)
add_executable(calc_gui src/calc_gui.c)
target_link_libraries(calc_gui PRIVATE calc_lib raylib)
target_compile_options(calc_gui PRIVATE -Wall -Wextra -Wpedantic)
else()
message(STATUS "raylib not found — skipping calc_gui target. Install with: brew install raylib")
endif()
```
On Apple Silicon, raylib from Homebrew is at `/opt/homebrew/`, which CMake picks up automatically.
### Tests for the GUI state machine
Since the state machine (`state_reset`, `state_handle_press`, `state_apply_pending`) is separate from Raylib's drawing, it can be unit-tested. Add to `tests/test_calc.c`:
```c
/* State machine tests — put these after the existing tests, before main() */
/* Copy the state struct & helpers from calc_gui.c into a header, */
/* or extract them into calc_gui_state.{c,h} for a cleaner split. */
/* This example assumes you've factored them into calc_gui_state.h. */
#include "calc_gui_state.h"
void test_state_initial(void) {
calc_state_t s;
state_reset(&s);
TEST_ASSERT_EQUAL_STRING("0", s.display);
}
void test_state_simple_addition(void) {
calc_state_t s;
state_reset(&s);
state_handle_press(&s, "6");
state_handle_press(&s, "+");
state_handle_press(&s, "7");
state_handle_press(&s, "=");
TEST_ASSERT_EQUAL_STRING("13", s.display);
}
void test_state_division_by_zero(void) {
calc_state_t s;
state_reset(&s);
state_handle_press(&s, "5");
state_handle_press(&s, "/");
state_handle_press(&s, "0");
state_handle_press(&s, "=");
TEST_ASSERT_EQUAL_STRING("Error", s.display);
}
/* ... and register them with RUN_TEST(test_state_...) in main() */
```
> [!tip] Factoring for testability
> To actually run these tests, extract the `calc_state_t` struct and its helper functions from `src/calc_gui.c` into `src/calc_gui_state.h` + `src/calc_gui_state.c`. Link both `calc_lib` and `calc_gui_state` into the test executable. The Raylib drawing stays in `src/calc_gui.c` and doesn't need to be tested directly.
### Run the GUI
```bash
cmake -B build -G Ninja
cmake --build build
./build/calc_gui
```
A 300×400 window opens with the calculator UI. Click buttons or use the keyboard — digits, `+`, `-`, `*`, `/`, `Enter` (for `=`), `Esc` (to clear), and `Backspace` (to delete the last digit).
---
## Starship prompt — C auto-detection
Starship's built-in `[c]` module shows the compiler and standard when a `.c` file or `CMakeLists.txt` exists in the directory. No config needed, but if you want to customize, append to `~/.config/starship.toml`:
```toml
[c]
format = "[$symbol($version(-$name) )]($style)"
symbol = " "
style = "149 bold"
disabled = false
```
---
## Optional: static analysis and memory checking
- **`clang-tidy`** (from `brew install llvm`) — runs automatically when enabled via clangd's `--clang-tidy` flag
- **AddressSanitizer** — built into Apple Clang. Enable per-project: add `-fsanitize=address -g` to `target_compile_options` and linker flags. Catches buffer overflows, use-after-free, leaks.
- **UndefinedBehaviorSanitizer** — `-fsanitize=undefined`. Catches signed overflow, null derefs, etc.
- **Valgrind** — **not available on Apple Silicon macOS**. Use AddressSanitizer instead; it catches most of what Valgrind would.
To build with sanitizers:
```bash
cmake -B build-asan -G Ninja -DCMAKE_C_FLAGS="-fsanitize=address -g -O1"
cmake --build build-asan
./build-asan/calc 6 + 7
```
---
## Troubleshooting
> [!warning] clangd shows "file not found" for standard headers
> Build the project at least once so `compile_commands.json` is generated. Ensure `CMAKE_EXPORT_COMPILE_COMMANDS ON` is in your CMakeLists.txt. Restart VS Code after the first build.
> [!warning] `cmake` can't find a compiler
> Run `xcode-select --install` to install/reinstall Command Line Tools. Then: `sudo xcode-select --reset`.
> [!warning] lldb-dap extension says "command not found"
> The `lldb-dap` binary ships with `brew install llvm`. If PATH isn't exporting Homebrew LLVM, set the path explicitly in VS Code settings: `"lldb-dap.executable-path": "/opt/homebrew/opt/llvm/bin/lldb-dap"`.
> [!warning] `brew install gdb` — should I bother?
> No, for Apple Silicon. Use `lldb`. If you truly need gdb for an embedded target, use a cross-compilation toolchain that includes `gdb-multiarch` rather than the standalone Homebrew formula.
---
## Summary — the one-shot C addition
> [!warning] This is a checklist, not a script
> The block below is **not a shell script you can save and run.** It's a sequence of commands to **copy and paste into your terminal one block at a time**, in order. After running these, you also need to **append the recommended C/C++ settings block** from earlier in this guide to `~/Library/Application Support/Code/User/settings.json` — without that, VS Code's clangd and CMake integration won't be configured.
```bash
# Build system + modern LLVM
brew install cmake ninja llvm
# GUI library (optional, for calc_gui demo)
brew install raylib
# VS Code extensions
code --install-extension ms-vscode.cpptools
code --install-extension ms-vscode.cpptools-extension-pack
code --install-extension ms-vscode.cmake-tools
code --install-extension twxs.cmake
code --install-extension llvm-vs-code-extensions.lldb-dap
code --install-extension llvm-vs-code-extensions.vscode-clangd
# Optional: Homebrew LLVM on PATH for clangd/clang-format/clang-tidy
echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
# Verify
clang --version && lldb --version && cmake --version && ninja --version
```
About five minutes of work on top of the general setup.
---
## Related notes
- [[General_Development_Mac_Tahoe_Setup]]
- [[Python_Development_Mac_Tahoe_Setup]]
- [[Ruby_Development_Mac_Tahoe_Setup]]
- [[Go_Development_Mac_Tahoe_Setup]]
- [[Rust_Development_Mac_Tahoe_Setup]]
- [CMake Cheat Sheet](https://cmake.org/cmake/help/latest/guide/tutorial/index.html)
- [LLDB Command Reference](https://lldb.llvm.org/use/map.html)