CLI, git, and reproducible builds¶
Every program you write eventually has to be compiled, versioned, tested, and shipped โ in a way that anyone can reproduce exactly. This lesson walks through how CargoForge-C handles all three: the shell commands you use day-to-day, the Makefile that automates building, and the GitHub Actions workflow that re-runs those same steps on every change. Understanding this infrastructure is what separates "code that works on my machine" from code you can trust in production.
The mental model ๐ง ¶
A reproducible build is a recipe so precise that any kitchen produces the identical cake. "Works on my machine" is the opposite โ a recipe that says "bake until done," secretly depending on your oven, your altitude, your particular bag of flour.
CargoForge removes the guesswork. Git is the lab notebook: every change stamped with who, when, and exactly what. The compiler flags pinned in the Makefile โ -std=c99 -D_POSIX_C_SOURCE=200809L โ are the exact oven temperature and bake time. Because the dialect of C and the library level are nailed down, the same source compiled on your laptop and on the CI runner produces a binary that behaves identically: a port authority's server computes the same GM you do.
The payoff is trust. When a result looks wrong you can reproduce it exactly, walk the git history back to the commit that broke it, and know the fix is real โ not an accident of one machine's environment.
What this actually means (plain English)¶
No jargon โ here's what the ideas in this lesson actually mean, and why they matter.
- Subcommand = "the first word you give the program that tells it what job to do" โ
./cargoforge optimizepicks the stowage-and-stability path, whilevalidate,info, orserveeach activate a completely different code path insidemain.candcli.c. - Makefile = "a recipe book that only cooks the dishes whose ingredients have changed" โ instead of retyping the full
gccincantation for all ten source files, you runmakeand it timestamps each.oagainst its.c; only files that changed since last time are recompiled. - Compiler flags (
-std=c99 -D_POSIX_C_SOURCE=200809L) = "a contract that locks the dialect of C the project speaks" โ any machine with a conforming C99 compiler and a POSIX.1-2008 library will produce the same binary from the same source, which is why a stability calculation gives the same answer on a port authority's server as on your laptop. - Deterministic build = "the compiler, given the same inputs and flags, always produces bit-for-bit identical output" โ CargoForge-C achieves this by pinning the C standard, the POSIX level, and writing every
.cfile by hand with no generated or time-dependent code, so the physics results never drift between environments. - git commit = "a permanent, labelled snapshot of exactly what the code looked like at one moment, plus a message explaining why" โ the lesson uses the use-after-free fix (
fix: null cargo pointer on parse error) as a concrete example of a commit message that tells the why, not just the what. - Continuous Integration (CI) = "a robot that rebuilds and smoke-tests your code on a clean machine every time you push" โ the
.github/workflows/c-build.ymlworkflow runsmakeand then./cargoforge examples/...on a fresh Ubuntu VM, so a change that only worked because of a leftover file on your laptop gets caught before it can reachmain. make test-asan= "rebuild everything with memory-error detectors switched on, then run the test suite" โ AddressSanitizer and UBSan are the tools that catch the kind of heap-use-after-free bug described elsewhere in the course; thecleanstep inside the target is mandatory because sanitizer-compiled.ofiles cannot be mixed with ordinary ones.
Why it matters: if the build is not deterministic and the CI not clean, a stability number that looks correct on a developer's laptop can silently differ on the vessel's navigation system โ and in cargo stowage, a wrong GM or free-surface calculation is a safety defect, not just a test failure.
The shell and the binary¶
CargoForge-C compiles to a single executable called cargoforge. Once built, you run it from the terminal (also called a shell or command line). A shell is a text interface to your operating system: you type a command, press Enter, and the OS executes it.
The most basic interaction looks like this:
Breaking this down:
./cargoforgeโ the./prefix means "look in the current directory for a file namedcargoforgeand run it". Without./, the shell would search the system-widePATHinstead โ a list of folders the shell always checks when you type a bare command name, which is how typingmakeorgitworks from anywhere without spelling out where those programs live.optimizeโ a subcommand. CargoForge-C uses the same pattern as tools likegitordocker: the first argument selects what the program does. Other subcommands arevalidate,info,serve,version, andhelp.examples/sample_ship.cfgandexamples/sample_cargo.txtโ arguments to the subcommand: the ship configuration file and the cargo manifest.
Other output formats are available via a flag:
Valid formats are human (default), json, csv, table, and markdown. The json format is what the HTTP server and library consumers receive.
Stdin as input
Both the ship config and cargo manifest parsers accept "-" as the filename, which means "read from standard input." This lets you pipe data without touching the filesystem:
What make does¶
Compiling a C program by hand means invoking gcc with a list of source files, flags, and output names. For a project with ten source files and eight test binaries, that list becomes unmanageable โ and you'd have to recompile everything even if only one file changed. make solves both problems.
The Makefile at the root of CargoForge-C is a set of rules. Each rule says: "to produce target X, you need files Y and Z; if any of them are newer than X, run this command." make compares timestamps and only recompiles what changed.
The compiler flags¶
Every object file and binary is compiled with the same flags, declared at the top of the Makefile:
What each flag does:
| Flag | Meaning |
|---|---|
-O3 |
Optimize aggressively for speed (three levels above none) |
-Wall -Wextra |
Enable a broad set of compiler warnings; warnings are treated as useful diagnostics |
-std=c99 |
Require strict C99 conformance โ no compiler-specific extensions |
-D_POSIX_C_SOURCE=200809L |
Expose POSIX.1-2008 functions (like strtok_r, open_memstream) |
-Iinclude |
Add the include/ directory to the header search path |
-lm |
Link the math library (libm) for sin, cos, atan, etc. |
The -std=c99 and -D_POSIX_C_SOURCE flags together define a precise dialect of C. Any code that compiles under these flags will compile identically on any conforming C99 implementation โ Linux, macOS, or Windows with a POSIX layer. That is what "portable" means in practice.
Source file groups¶
The Makefile splits sources into two groups:
LIB_SRCS = $(SRC_DIR)/parser.c $(SRC_DIR)/analysis.c $(SRC_DIR)/placement_3d.c \
$(SRC_DIR)/constraints.c $(SRC_DIR)/json_output.c \
$(SRC_DIR)/hydrostatics.c $(SRC_DIR)/tanks.c \
$(SRC_DIR)/longitudinal_strength.c $(SRC_DIR)/imdg.c \
$(SRC_DIR)/libcargoforge.c
CLI_SRCS = $(SRC_DIR)/main.c $(SRC_DIR)/cli.c $(SRC_DIR)/visualization.c \
$(SRC_DIR)/server.c
LIB_SRCS is the pure engine โ the stability physics, parser, placement algorithm, and public API. It has no main() function and no terminal output. CLI_SRCS is the thin shell that wraps it: argument parsing, output formatting, the HTTP server. This separation is intentional: the same LIB_SRCS are compiled into libcargoforge.a and libcargoforge.dylib/.so for use by other programs, without dragging in any CLI behaviour.
The main targets¶
all: $(BUILD_DIR) $(TARGET)
$(TARGET): $(ALL_OBJS)
$(CC) $(CFLAGS) -o $(TARGET) $(ALL_OBJS) $(LDFLAGS)
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(HDRS)
$(CC) $(CFLAGS) -fPIC -c $< -o $@
The pattern rule $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c compiles any .c file in src/ to a .o (object file) in build/. The -fPIC flag ("position-independent code") is required for shared libraries; it costs nothing for a binary.
The $(TARGET) rule then links all the object files into the final executable. Linking is the step where all the separately compiled pieces are joined together and external libraries (like -lm) are resolved.
Platform detection¶
UNAME := $(shell uname -s)
ifeq ($(UNAME),Darwin)
SHARED_EXT = dylib
SHARED_FLAGS = -dynamiclib -install_name @rpath/libcargoforge.$(SHARED_EXT)
else
SHARED_EXT = so
SHARED_FLAGS = -shared -Wl,-soname,libcargoforge.$(SHARED_EXT)
endif
macOS uses .dylib for shared libraries; Linux uses .so. The Makefile queries the OS at build time with $(shell uname -s) and sets the right extension automatically. This is a simple but real example of cross-platform portability handled in the build system rather than the source code.
Other useful targets¶
| Target | What it does |
|---|---|
make |
Build cargoforge (the default all target) |
make lib |
Build libcargoforge.a and the shared library only |
make test |
Build and run all 8 test binaries |
make test-asan |
Rebuild with AddressSanitizer + UBSan, then run tests |
make test-valgrind |
Run each test binary under Valgrind memory checker |
make fuzz |
Run the random-input fuzzer (scripts/fuzz.sh) |
make clean |
Delete build/, the binary, libs, and all test binaries |
make install |
Copy binary, libs, and header to /usr/local (configurable via PREFIX) |
make validate |
Run the DNV-SE-0475 benchmark vessel validation |
make wasm |
Compile to WebAssembly with Emscripten (emcc required) |
Always run make clean before a sanitizer build
The test-asan target does this for you โ notice the rule: test-asan: clean all test. This is because object files compiled without sanitizer flags cannot be mixed with ones compiled with them. A partial rebuild would silently skip the sanitizer on older files.
git basics¶
git is a version control system โ a tool that keeps a complete, searchable history of every change ever made to a project, so nothing is ever truly lost and you can always see who changed what and when. It records every change to your code as a commit โ a snapshot with a unique ID (a 40-character hash), a message, an author, and a timestamp. A collection of commits forms a repository.
The most important commands:
git status # what files have changed since the last commit?
git diff # what exactly changed in those files?
git add src/parser.c # stage a file (mark it for inclusion in the next commit)
git commit -m "fix: null-out cargo pointer on parse error"
git log --oneline # one-line summary of recent commits
git push # send commits to the remote repository (GitHub)
Why commit messages matter¶
A commit message is a permanent record of why a change was made. The fix covered in Lesson 13 โ nulling out ship->cargo after freeing it to prevent a heap-use-after-free โ belongs in a commit message, not just a code comment. Future developers (including your future self) will read it and understand the intent without reconstructing it from the diff.
A common convention: start with a short prefix that classifies the change. For example:
- fix: โ a bug fix
- feat: โ a new feature
- test: โ adding or changing tests
- docs: โ documentation only
Branches and pull requests¶
When you want to work on a new feature or a fix without disturbing the main history, you create a branch โ a parallel line of commits. When the work is ready, you open a pull request (PR) on GitHub, which asks for the branch to be merged into main. This is where CI runs (see next section).
Why deterministic builds matter¶
"Deterministic" means: given the same source code and the same flags, the compiler always produces the same binary. CargoForge-C achieves this by:
- Pinning the C standard.
-std=c99excludes any GCC-specific behaviour that might vary between versions. - Explicit POSIX level.
-D_POSIX_C_SOURCE=200809Lis the same constant on any POSIX-conformant system. If this were unset, different Linux distributions might expose different sets of functions. - No generated code. There is no code-generation step โ every
.cfile is hand-written and committed. Nothing in the build is random or time-dependent.
This matters for safety-critical software. If a stability calculation gives a different answer on the port authority's server than on your laptop, you cannot trust either. Determinism is the foundation of correctness.
Continuous Integration (CI)¶
Continuous Integration (CI) means: every time someone pushes code or opens a pull request, an automated system checks whether the code still builds and passes tests. If it does not, the PR is blocked.
CargoForge-C uses GitHub Actions. The workflow is defined in .github/workflows/c-build.yml:
name: C/C++ CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Compile the project
run: make
- name: Run a basic test
run: ./cargoforge examples/sample_ship.cfg examples/sample_cargo.txt
Reading the workflow¶
on:โ when to run. This workflow runs on every push tomainand on every PR that targetsmain.jobs: build:โ a single job namedbuild. A job is a sequence of steps that runs on one machine.runs-on: ubuntu-latestโ GitHub provides a fresh Ubuntu virtual machine for each run. "Fresh" matters: the machine has no state from previous runs, so if something only works because a file was left over from last time, CI will catch it.actions/checkout@v4โ a pre-built action that clones the repository into the VM.run: makeโ exactly the same command you type locally. If the compiler flags, source list, or any header has a problem, this step fails and the PR is blocked.run: ./cargoforge examples/sample_ship.cfg examples/sample_cargo.txtโ a smoke test: run the binary on the sample files and verify it exits cleanly (exit code 0). A non-zero exit code fails the step.
What CI cannot replace¶
The CI workflow above is a minimal smoke test โ it verifies compilation and a basic end-to-end run, but it does not run the eight unit test binaries (make test), the memory-safety suite (make test-asan), or the benchmark validation (make validate). Those are available locally and can be added to the workflow as additional steps. The lesson: CI is only as thorough as the commands you put in it.
The environment is always clean
Local development accumulates state: cached binaries, leftover files, environment variables set in your shell profile. CI starts clean every time. A build that passes locally but fails in CI almost always means the local build relied on something not committed to the repository โ a missing make clean, an untracked file, or a system library that is not standard.
Putting it together: a complete workflow¶
Here is what happens from a code change to a merged PR:
- You edit
src/parser.cto fix a bug. make test-asanโ rebuild with sanitizers, run all 8 tests. Fix any failures.git add src/parser.cโ stage the changed file only.git commit -m "fix: null cargo pointer on parse error to prevent UAF"โ commit with a clear message.git push origin my-fix-branchโ push to GitHub.- Open a pull request. GitHub Actions automatically starts the CI job.
- CI runs
makethen./cargoforge examples/...on a clean Ubuntu VM. - If both steps pass, the PR shows a green check. A reviewer can then merge it to
main.
Recap¶
cargoforgeis invoked as./cargoforge <subcommand> <ship_cfg> <cargo_manifest>, with optional--formatfor output style.- The
Makefileencodes the complete build recipe: compiler, flags, source groups, test binaries, and platform detection.makeonly recompiles what changed. -std=c99 -D_POSIX_C_SOURCE=200809Lpins the language dialect exactly, making the build portable and deterministic across systems.gitrecords every change as a committed snapshot with a message; branches and pull requests gate changes through review and CI.- The GitHub Actions workflow in
.github/workflows/c-build.ymlre-runsmakeand a smoke test on a clean Ubuntu VM on every push and PR, catching regressions before they reachmain. - CI is only as thorough as its step list โ
make test,make test-asan, andmake validateare available but must be added explicitly to gain their protection in CI.
Check yourself¶
Why does pinning -std=c99 -D_POSIX_C_SOURCE=200809L matter for reproducibility, specifically?
It locks the exact C dialect and POSIX feature level the code assumes. Any machine with a conforming C99 compiler and POSIX.1-2008 library then produces the same behaviour from the same source โ a stability calculation gives the same answer on a port authority's server as on your laptop.
What does a Makefile pattern rule buy you over writing one compile recipe per source file?
One rule like $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(HDRS) covers every current and future source file with a single wildcard recipe โ add a fifteenth .c file tomorrow and it is built with zero new Makefile code.
Next: Lab 1 โ Build and Run It.