# C Development Ubuntu Setup
Adding C development on top of an Ubuntu machine already configured per [[General_Development_Ubuntu_Setup]]. Most of the stack carries over — Ghostty, Starship, Git, GitHub CLI, VS Code, Claude Code, and the bash setup all work identically. This guide only covers the C-specific additions, and everything installs the same way on `amd64` and `arm64`. It's the Linux counterpart to [[C_Development_Mac_Tahoe_Setup]].
> [!tip] AI assistance is optional
> The reference to Claude Code above assumes you completed Part 7 of the General guide. If you skipped it or use a different AI tool, 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 `~/.cargo/bin`, Go under `~/go/`, Rust under `~/.rustup/` and `~/.cargo/`, C tooling under apt. No conflicts.
---
## What you already have
If you ran the prerequisites in the General guide, you installed `build-essential`, which already gives you:
| Tool | What it is | Path |
|------|------------|------|
| `gcc` | The GNU C compiler (native for your arch) | `/usr/bin/gcc` |
| `g++` | The GNU C++ compiler | `/usr/bin/g++` |
| `make` | Build automation (GNU Make) | `/usr/bin/make` |
| `ar`, `ld`, `as` | Binary utilities (binutils) | `/usr/bin/` |
Verify:
```bash
gcc --version
make --version
```
> [!info] gcc is the native Linux compiler
> On macOS the default compiler is Apple Clang. On Linux it's **GCC**, and it's a first-class, fully native compiler on both `amd64` and `arm64`. CMake will pick `gcc` as `cc` automatically. We'll also install Clang/LLVM below for the editor tooling (`clangd`, `clang-format`, `clang-tidy`) and as an alternative compiler.
---
## What to add
```bash
sudo apt install -y cmake ninja-build clang clangd clang-format clang-tidy lldb gdb valgrind
```
| Tool | Why |
|------|-----|
| `cmake` | De-facto standard build system configurator; most open-source C projects use it |
| `ninja-build` | Fast build tool that CMake targets instead of `make` (much faster on large projects) |
| `clang` | The LLVM C/C++ compiler — useful as an alternative to gcc and for its sanitizers |
| `clangd` | The language server that powers modern editor intelligence |
| `clang-format` | Code formatter |
| `clang-tidy` | Static-analysis linter |
| `gdb` | The GNU debugger — the native, no-friction debugger on Linux |
| `lldb` | LLVM's debugger (optional alternative to gdb) |
| `valgrind` | Memory error and leak detector |
> [!info] No PATH gymnastics needed
> On macOS, Homebrew's LLVM tools have to be added to PATH manually so they don't shadow Apple's. On Ubuntu, `clangd`, `clang-format`, and `clang-tidy` are independent apt packages installed straight onto your PATH (`/usr/bin/`). Nothing to configure.
### Debugger choice: gdb (the easy default on Linux)
On Linux, **use `gdb`**. It's the native debugger, requires zero code-signing or entitlement dances (unlike `lldb` on macOS), and the Microsoft C/C++ VS Code extension drives it out of the box via the `cppdbg` adapter. `lldb` is installed above too if you prefer it, but for most workflows gdb is the path of least resistance.
> [!tip] Valgrind actually works here
> On Apple Silicon macOS, Valgrind isn't available, so the Mac guide falls back to AddressSanitizer. On Linux, **Valgrind runs natively on both `amd64` and `arm64`** — so you get both Valgrind *and* the sanitizers. See [Optional: static analysis and memory checking](#optional-static-analysis-and-memory-checking) below.
---
## VS Code extensions for C
```bash
# Microsoft's C/C++ extension (IntelliSense, cppdbg/gdb 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
# 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 and includes the `cppdbg` debugger we use below.
---
## VS Code settings
Append to your existing `settings.json` (`~/.config/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
mkdir -p src tests
```
### Project layout
```
calc-c/
├── CMakeLists.txt
├── .clang-format
├── .gitignore
├── src/
│ ├── calc_lib.h
│ ├── calc_lib.c
│ └── main.c
└── tests/
├── unity.h
├── unity.c
└── test_calc.c
```
### `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
*.so
```
### 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`. On Linux we use the `cppdbg` adapter with gdb:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug calc",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/calc",
"args": ["6", "+", "7"],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
```
Set a breakpoint on the `calc_add` function, hit **F5**, and you'll step through native code with full variable inspection.
> [!info] Prefer lldb?
> If you installed `lldb` and the `lldb-dap` adapter (`code --install-extension llvm-vs-code-extensions.lldb-dap`), change `"type"` to `"lldb-dap"` and drop the `MIMode`/`setupCommands` lines. Both work; gdb is the lower-friction default on Linux.
### Publish 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 gcc (clang also available)
- 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`
- Memory check: `valgrind --leak-check=full ./build/calc 6 + 7`
- 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, apt-installable graphics library that runs natively on both `amd64` and `arm64`.
### Why Raylib?
| Option | Pros | Cons |
|--------|------|------|
| **Raylib** | Single dependency, apt-installable, modern, native, simple immediate-mode API | Custom widget look (not native GTK controls) |
| **GTK** | Full native widget toolkit | Heavier dependency chain, more boilerplate |
| **SDL2** | Ubiquitous for games/media | Lower-level than Raylib — you build your own buttons |
| **Dear ImGui** | Excellent for debug UIs and tools | Bindings are C++ (cimgui wraps to C) |
For a pedagogical calculator in C, Raylib is the right default.
### Install Raylib
Raylib and its windowing/GL/audio dependencies are all in apt:
```bash
sudo apt install -y libraylib-dev \
libgl1-mesa-dev libxi-dev libxcursor-dev libxrandr-dev \
libxinerama-dev libwayland-dev libxkbcommon-dev libasound2-dev
```
> [!info] If `libraylib-dev` is missing or too old
> The version in apt is usually fine. For the newest raylib, the community PPA `ppa:texus/raylib` provides current `libraylib5-dev`/`libraylib6-dev` builds for both architectures. The system-library deps above are still required either way.
### `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 apt)
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: sudo apt install libraylib-dev")
endif()
```
> [!info] If `find_package(raylib)` fails
> Some apt builds of raylib don't ship CMake config files. If CMake can't find it, link manually instead: replace the `find_package`/`target_link_libraries` lines with `target_link_libraries(calc_gui PRIVATE calc_lib raylib m dl pthread GL)`. The PPA build includes the CMake config.
### 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.
---
## 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 (the general config already enables it).
---
## Optional: static analysis and memory checking
This is where Linux pulls ahead of Apple Silicon — you get the full toolkit:
- **`clang-tidy`** — runs automatically when enabled via clangd's `--clang-tidy` flag (already set in the VS Code settings above).
- **Valgrind** — the gold-standard memory checker, available on both `amd64` and `arm64`:
```bash
valgrind --leak-check=full --show-leak-kinds=all ./build/calc 6 + 7
```
- **AddressSanitizer** — built into gcc and clang. Catches buffer overflows, use-after-free, leaks:
```bash
cmake -B build-asan -G Ninja -DCMAKE_C_FLAGS="-fsanitize=address -g -O1"
cmake --build build-asan
./build-asan/calc 6 + 7
```
- **UndefinedBehaviorSanitizer** — `-fsanitize=undefined`. Catches signed overflow, null derefs, etc.
> [!tip] Valgrind vs sanitizers
> Use **sanitizers** during development (fast, compiled into the binary) and **Valgrind** for a thorough pass (slower, no recompile needed, catches some things sanitizers miss). Don't combine ASan and Valgrind in the same run — they conflict.
---
## direnv — auto-activate this project's environment
With direnv hooked into your shell (see [[General_Development_Ubuntu_Setup]]), an `.envrc` in the project root loads when you `cd` in. For this CMake project it's handy for running built binaries from `build/` by name and pinning the compiler CMake configures with.
```bash
# from the project root
cat > .envrc <<'EOF'
# run build/calc as just `calc` after building
PATH_add build
# optional: pin the compiler for CMake's first configure
export CC=clang
export CXX=clang++
EOF
direnv allow
```
> [!info] `.envrc` doesn't replace your build directory
> CMake still configures and builds into `build/` as usual; `PATH_add build` only makes the resulting executables callable by name. Swap `CC`/`CXX` to `gcc`/`g++` if you prefer — they're read by CMake's first `configure`, so set them before `build/` exists (or delete and re-create it).
---
## 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
> Install build basics: `sudo apt install -y build-essential`. Confirm with `gcc --version`.
> [!warning] raylib window fails to open in a VM
> Raylib needs OpenGL. Enable 3D acceleration in your hypervisor, or use a virtio-gpu display. The CLI and tests don't need a display — only `calc_gui` does.
> [!warning] gdb won't attach: "ptrace: Operation not permitted"
> Some hardened/VM setups restrict ptrace. As a quick fix for a dev box: `echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope` (resets on reboot; make permanent in `/etc/sysctl.d/`).
---
## Summary — the one-shot C addition
> [!warning] This is a checklist, not a script
> Copy and paste one block at a time. After running these, also **append the C/C++ settings block** above to `~/.config/Code/User/settings.json`.
```bash
# Compilers, build system, debugger, analysis tools
sudo apt install -y cmake ninja-build clang clangd clang-format clang-tidy lldb gdb valgrind
# GUI library + its system dependencies (optional, for calc_gui demo)
sudo apt install -y libraylib-dev libgl1-mesa-dev libxi-dev libxcursor-dev \
libxrandr-dev libxinerama-dev libwayland-dev libxkbcommon-dev libasound2-dev
# 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.vscode-clangd
# Verify
gcc --version && clang --version && cmake --version && ninja --version && gdb --version && valgrind --version
```
About five minutes of work on top of the general setup.
---
## Related notes
- [[General_Development_Ubuntu_Setup]]
- [[C_Development_Mac_Tahoe_Setup]] — the macOS counterpart
- [[Python_Development_Ubuntu_Setup]]
- [[Ruby_Development_Ubuntu_Setup]]
- [[Go_Development_Ubuntu_Setup]]
- [[Rust_Development_Ubuntu_Setup]]
- [CMake Tutorial](https://cmake.org/cmake/help/latest/guide/tutorial/index.html)
- [GDB Quick Reference](https://sourceware.org/gdb/current/onlinedocs/gdb/)
- [Valgrind Quick Start](https://valgrind.org/docs/manual/quick-start.html)