Build systems, Makefile, and a setup script that reproduces this environment on another machine. - CP.sublime-build: incremental make-based build, clang++, flags shared with clangd via compile_flags.txt - compete_CPP.sublime-build: competitive-programming workflow, runs against inputf.in and diffs the result against expectedf.out - Makefile: builds every .cpp in the tree into a binary beside its source - setup.sh: export/install/check, path-neutral payload, idempotent - README: setup notes, the Sublime $-expansion trap, and the -MMD -MP header-dependency limitation to fix later The .sublime-workspace is deliberately excluded: it holds Sublime's global recent-file history and absolute paths. Only the pane layout is portable, extracted to sublime/layout.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCtkkFDkSGi868JWuinwcF
9.7 KiB
Mocpp — C++ learning exercises
Chapter exercises, one binary per .cpp. Built with clang++, edited in
Sublime Text with clangd for diagnostics and completion.
Layout
Mocpp/
├── Makefile # builds every .cpp in the tree
├── compile_flags.txt # fallback flags (used if a chapter dir has none)
├── README.md
├── source.cpp
└── ch1/
├── compile_flags.txt # flags for this chapter — clangd reads this too
├── 1-1.cpp → 1-1 # each .cpp builds to a binary beside it
└── 1-4.cpp → 1-4
Building
From Sublime:
| Key | Action |
|---|---|
Ctrl+B |
build everything (incremental — silent if nothing changed) |
Ctrl+Shift+B → Build all + Run this file |
build, then run the focused file |
Ctrl+Shift+B → Build all + Run this file (with stdin) |
same, for std::cin exercises |
Ctrl+Shift+B → Run this file only |
compile just the focused file, skip make |
Ctrl+Shift+B → Clean |
delete all binaries, force a full rebuild |
From a terminal — identical result, make is the single source of truth:
make # build what changed
make clean # remove all binaries
The Sublime build file lives at
~/.config/sublime-text/Packages/User/CP.sublime-build.
Flags: one file, two consumers
compile_flags.txt is read by both clangd (editor squiggles) and the
Makefile (the actual compile). That is deliberate — it is why the warnings you
see while typing are the warnings you get when building. Edit flags there and
nowhere else.
Current flags: -std=c++20 -Wall -Wextra -Wconversion
Starting a new chapter
mkdir ch2
cp ch1/compile_flags.txt ch2/
Without its own copy a directory falls back to the root compile_flags.txt, so
it still gets warnings — but keeping a copy per chapter means you can vary the
standard per chapter later.
TODO: add -MMD -MP when you start writing your own headers
This is the one known limitation of the current Makefile. Read this before
you create your first .h file.
The build tracks only .cpp timestamps. It has no idea headers exist. So if you
edit a header, make does nothing and leaves you running a stale binary — with
no error and no warning. Verified behaviour, not theory:
$ ./ch1/1-1
hello v1
$ sed -i 's/hello v1/hello v2/' ch1/greet.h # edit ONLY the header
$ make
# ← silence. nothing rebuilt.
$ ./ch1/1-1
hello v1 # ← WRONG. still the old code.
This will cost you an hour of debugging code that is already correct.
The fix (tested, works)
-MMD makes clang emit a .d file listing every header the source included.
-MP adds a dummy target for each header, so a deleted header does not break
the build. -include $(DEPS) feeds those back to make, which then knows the
binary depends on the headers too.
Three changes to the Makefile:
BINS := $(basename $(SRCS))
DEPS := $(addsuffix .d,$(BINS)) # 1. add this line
%: %.cpp
@flags=$$(cat $(dir $<)compile_flags.txt 2>/dev/null || cat $(ROOT)compile_flags.txt 2>/dev/null); \
echo "compiling $(notdir $<)"; \
$(CXX) $$flags -MMD -MP $< -o $@ # 2. add -MMD -MP here
-include $(DEPS) # 3. add this line
clean:
@rm -f $(BINS) $(DEPS) # 4. also remove .d files
@echo "cleaned"
After that, editing a header rebuilds what depends on it:
$ sed -i 's/hello v1/hello v2/' ch1/greet.h
$ make
compiling 1-1.cpp
$ ./ch1/1-1
hello v2
The generated ch1/1-1.d is just a make rule:
ch1/1-1: ch1/1-1.cpp ch1/greet.h
ch1/greet.h:
The leading - in -include matters: it stops make erroring on the first build,
when no .d files exist yet.
compete_CPP — the contest workflow
A second build system, for problems that read input from stdin (Codeforces,
LeetCode, or any book exercise using std::cin). Select it with
Tools → Build System → compete_CPP.
It compiles the focused .cpp, feeds it inputf.in on stdin, writes stdout to
outputf.out, and prints both — so you never retype a sample input.
| Variant | Does |
|---|---|
Ctrl+B |
compile, run with inputf.in, show input and output |
| Run + check against expectedf.out | same, then diff the answer — prints PASS or FAIL |
| Run (type input live) | no redirect; type input into the build panel |
| Setup: create inputf.in / expectedf.out here | creates the three files in the current folder |
| DIAGNOSTIC | prints Sublime's variables |
The three files live next to the source, not at the repo root. That was the
bug in the original tutorial build: it looked for inputf.in in the source's
folder while the files sat one level up, so they were never read.
Typical loop: open a problem, run Setup, paste the sample input into
inputf.in and the expected answer into expectedf.out, then hit
Run + check until it says PASS.
--- outputf.out ---
6
--- diff (expected vs actual) ---
PASS
A wrong answer shows the diff and exits non-zero:
-99
+6
FAIL
A compile error stops before running, so you never diff a stale binary.
Note this build compiles only the focused file — it does not use the
Makefile. That is intentional: contest problems are self-contained single files.
Use the CP build system for the chapter exercises.
Gotcha: $ in the Sublime build file
Sublime expands both ${name} and bare $name in cmd, shell_cmd, and
working_dir. Any name it does not recognise becomes an empty string before
bash runs.
So a shell variable like $SRC is silently deleted, and a guard such as
if [ -z "$SRC" ] becomes if [ -z "" ] — always true. The symptom is a build
that insists there is no active file no matter what you do.
Rules for editing CP.sublime-build:
- Prefer Sublime's own
${file},${file_path},${file_base_name}plus$(...), and use no bash variables at all. - Any
$meant for bash must be escaped\$(written\\$in the JSON). - Test a variant with the DIAGNOSTIC build, which prints what Sublime actually substituted.
Reference: https://www.sublimetext.com/docs/build_systems.html
Saving the window layout
Three mechanisms, easy to confuse:
| What | Where | Saves |
|---|---|---|
| Project | Mocpp.sublime-project |
folders, settings, excludes — commit this |
| Workspace | Mocpp.sublime-workspace (auto) |
open tabs, pane layout, cursors — do NOT commit |
| Hot exit | automatic | last session, even with no project |
Open it once with Project → Open Project → Mocpp.sublime-project. From then
on Sublime writes a .sublime-workspace beside it holding your exact pane
arrangement and open files, and restores them next launch.
Keybindings for the contest layout (code left, input top-right, output
bottom-right), in Packages/User/Default (Linux).sublime-keymap:
| Key | Layout |
|---|---|
Ctrl+Alt+Shift+C |
three-pane contest layout |
Ctrl+Alt+Shift+1 |
back to a single pane |
Sublime's built-in Alt+Shift+1..5 (columns) and Alt+Shift+8/9 (grids) still
work; the bindings above just add a split the menu does not offer.
If you commit this directory to git:
*.sublime-workspace
Installing this setup on another machine
setup.sh moves the whole environment. Clone the repo, run install:
git clone <this repo> ~/git/Mocpp
cd ~/git/Mocpp
./setup.sh install
| Command | Does |
|---|---|
./setup.sh check |
verify the toolchain, change nothing |
./setup.sh install |
install sublime/ onto this machine, then build |
./setup.sh export |
re-capture this machine's Sublime config into sublime/ |
Run export after changing a build system, so the payload in sublime/
stays current, then commit. The script never embeds copies of the config — it
installs from sublime/, so there is nothing to drift out of sync.
What install does:
- checks for
clang++,clangd,make,git, and prints the right package-manager command for your distro if any are missing - finds Sublime's config dir (differs on Linux and macOS) and names the keymap per platform
- substitutes
__PROJECT_ROOT__in the build files with wherever you cloned to, so the repo need not live at the same path - merges the Package Control list rather than overwriting yours, so Sublime offers to install the missing packages on next start
- runs
makeas a smoke test and fails loudly if the toolchain is broken
Re-running is a no-op: files are compared after substitution, so nothing is
rewritten and no .bak files accumulate. Anything it would genuinely overwrite
is backed up as <name>.bak-<timestamp> first.
Why the workspace is not copied
compete.sublime-workspace is deliberately not part of the payload and is
gitignored. Sublime stores your global recent-file history in it — on this
machine 17 of its 21 entries were unrelated personal paths, including documents
and TLS certificates. It also hardcodes absolute paths.
Only the pane layout is portable, so export extracts just that into
sublime/layout.json, and install builds a clean workspace from it. The same
layout is also bound to Ctrl+Alt+Shift+C, so it survives even without a
workspace file.
When this setup runs out
It is built for single-file exercises. Move to CMake generating
compile_commands.json once you have multiple source files linking into one
program, external libraries, or more than one build configuration:
cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
ln -sf build/compile_commands.json .
Then delete compile_flags.txt — if both exist in a directory,
compile_commands.json wins and the other becomes a silently ignored second
source of truth. Point the Sublime build at ninja -C build.