Why the Build Matters
Every line of code in production went through a build: the transformation from source files into an artifact (binary, container image, deployable bundle) that can be tested and shipped. CI/CD systems are, at their core, automated build-and-test pipelines.
A systems engineer who understands the build understands why “it works on my machine” happens, how to make deployments reproducible, and how to shave minutes off every release.
Compile, Interpret, JIT
| Model | How it runs | Examples |
|---|---|---|
| Ahead-of-time (AOT) compile | Source → native machine code → run directly | C, Rust, Go |
| Interpreted | Source executed by another program, line by line | Bash, Python, JavaScript (classic) |
| Just-in-time (JIT) | Interpreted initially, hot code compiled to native at runtime | Java, .NET, modern JS engines |
The boundary is blurry (Python compiles to bytecode; V8 compiles to native). The mental model that matters: AOT gives fast startup and direct control; JIT gives portability plus adaptive optimization; interpreted gives simplicity.
Container runtimes and OS processes care about AOT because the binary is what the kernel executes.
The C Toolchain
A C program is not compiled in one step — it passes through four stages:
- Preprocessor (
cpp) — expands#includeand#definemacros into one giant translation unit. - Compiler (
cc) — translates C to assembly for the target CPU. - Assembler (
as) — turns assembly into an object file (.o) containing machine code plus a symbol table of functions/variables it defines and needs. - Linker (
ld) — combines object files and libraries, resolves symbols, and produces the final executable.
gcc -E main.c -o main.i # preprocess only
gcc -S main.c -o main.s # compile to assembly
gcc -c main.c -o main.o # assemble to object file
gcc main.o -o app # link
gcc -O2 -Wall -g main.c -o app # typical one-shot
The -O2/-O3 flags enable the optimizer — the same source can be 5–50× faster or slower based on optimization and target. This is why release builds differ from debug builds.
“Works in debug, breaks in release” is a real phenomenon: optimizer bugs, uninitialized reads, and strict aliasing surface only under optimization.
Static vs Dynamic Linking
The linker has two ways to pull in libraries:
- Static (
.a) — library code is copied into your executable. Self-contained, larger, version-immutable. - Dynamic (
.so/.dll) — the executable records a dependency; the OS loads the library at runtime. Smaller, upgradeable, but version-sensitive (lddshows dependencies; a missing.sois a classic “it won’t start” failure).
Containers essentially freeze a set of dynamically linked libraries into an image so the binary finds its dependencies — the modern answer to dependency drift.
Build Systems
A build system decides what to rebuild when files change and in what order. The minimum viable build tool is a script; the industry tools are:
| Tool | Model | Use when |
|---|---|---|
| Make | Rule-based on file timestamps | Small C projects, glue scripts |
| CMake | Generates Make/ninja files from a high-level spec | C/C++ projects, portable builds |
| Bazel / Buck | Hermetic, incremental, language-agnostic | Monorepos at scale; parallel, cacheable builds |
The key engineering properties of a good build are incrementality (rebuild only what changed), determinism (same input → same output), and hermeticity (no dependence on the state of the builder’s machine). Bazel enforces all three; Make requires discipline.
Package Managers
Builds are rarely self-contained — they depend on libraries that come from package managers, which resolve, download, and lock dependencies:
- Language-level:
npm,pip,cargo,go modules, Maven. - OS-level:
apt/yum/apk(also used to assemble container base images).
The critical practice is locking: a lockfile (package-lock.json, Cargo.lock, go.sum) records exact versions and hashes so every environment builds the identical dependency graph. “It works on my machine” is usually a missing or stale lockfile.
Reproducible Builds
A build is reproducible when the same source at the same revision produces byte-identical artifacts. Threats to reproducibility:
- timestamps embedded at compile time
- non-deterministic map iteration
- machine-specific paths
- un-pinned dependencies
Reproducibility is what makes supply-chain verification possible — you can verify that a published binary matches the source. Container images and CI both push toward this: pin everything, build from clean checkouts, and make the build the single source of truth.
CI Wiring
In practice the build lives inside a CI pipeline. A commit triggers a runner that:
- checks out the source at a known revision
- installs the locked toolchain
- builds
- runs tests
- produces an artifact that CD then deploys
The build stage failing is the cheapest failure in the entire system — which is why CI culture prizes fast, deterministic builds: the shorter the feedback loop, the faster teams can ship.
Practice Trajectory
- Build a small C program with
gccstep by step (-E,-S,-c, link) and inspect the assembly and symbols withnm/objdump. - Compare a static and a dynamic build (
-staticvs default), then runlddon the dynamic one and deliberately break a dependency. - Write a
Makefilewith phony targets and incremental compilation; verifymakeskips unchanged files. - Generate a lockfile for a project you know (e.g.,
npm install→package-lock.json) and explain what a full rebuild reads from it. - Time a clean CI build vs an incremental one and identify which stage dominates — then propose one change to speed it up.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Any deployment | The artifact is the contract — know how it was built |
| Debugging “works on my machine” | Check optimization flags, dynamic deps, and lockfiles |
| Team at scale | A hermetic build system is the foundation of CD |
| Supply-chain security | Lockfiles + reproducibility are the verification layer |