Compiling Programs

4 min read

Core idea

Compiling is the act of translating human-readable source code into the binary instructions a CPU executes directly. On Linux the canonical user-facing ritual is three commands: ./configure runs a shell script that probes your system (compiler version, available libraries, header locations) and generates a Makefile; make reads that Makefile and runs the compiler and linker on every source file whose output is stale; make install copies the finished binary, libraries, and documentation into system directories like /usr/local/bin. Underneath, four tools do the real work — the preprocessor folds in #include headers, the compiler emits assembly, the assembler turns it into object files, and the linker stitches object files and shared libraries into a single executable. The configure-make-install convention is just the user-friendly skin over that machinery, and it is the reason that the same tar | configure | make recipe builds software on Ubuntu, Fedora, BSD, and macOS alike.

Shotts's argument: Compiling looks intimidating because the binaries are usually invisible — they're installed by your package manager. But the same three commands work on almost every GNU project, and reading the messages they produce teaches you more about how Linux is actually assembled than any single tutorial.

Why it matters

Packages don't cover everything

Distribution repositories are huge but not exhaustive. Cutting-edge versions, niche tools, security patches, and your own forks live in source form. Knowing the build dance unlocks all of them — and is the only way to install software on systems where the package manager is intentionally minimal (containers, embedded Linux, BSDs).

make is dependency tracking made physical

Behind the curtain, make is a tiny rule engine: each target declares its prerequisites and a recipe to rebuild itself if any prerequisite is newer. That model — "build only what's stale" — appears in every modern build tool (Bazel, Gradle, Webpack, Nix). Reading a hand-written Makefile once is the fastest way to internalize the idea, and the skill transfers to every dependency-tracked task (data pipelines, CI caching, image builds).

Reading a build teaches the layout of Linux

./configure reports where it finds headers, libraries, and the compiler. The output is essentially a guided tour of /usr/include, /usr/lib, /usr/local, and $PATH. Watching it run on an unfamiliar machine is how seasoned engineers diagnose "why isn't my system finding libssl?" — they know the order in which the build script searches.

Key takeaways

Mental model

The three-step user ritual

The configure-make-install loop is the same on every well-packaged GNU project. Each command has a distinct job and a distinct failure mode, so when something breaks, the current stage tells you where to look.

The three-step user ritual

What the compiler actually does

Inside make, the heavy lifting happens in four sub-stages. Most users never see them named, but the messages from gcc reveal them.

What the compiler actually does

make's dependency graph

A Makefile is a set of rules of the shape target: prereqs<tab>recipe. make walks the graph from the requested target downward, runs the recipe only when the target file is missing or older than any prereq. That single rule is enough to express incremental compilation, parallel builds (make -j8), and arbitrary task pipelines (LaTeX builds, asset bundling, data ETL).

Practical application

Three habits keep source builds painless. First, build under ~/src (your personal sandbox), not /usr/src (distribution-owned). If anything goes wrong, only your home directory is affected. Second, never skip ./configure's output. Its tail tells you what was found and what wasn't; a missing --with-foo is the single most common cause of "feature X doesn't work" after a successful build. Third, prefer make install to copying binaries by hand. The install target knows to write to /usr/local/share/man/ for man pages, /usr/local/include/ for headers, and /usr/local/lib/ for libraries — replicating that by hand is error-prone, and it leaves no clean way to uninstall.

For a quick verification cycle, the rhythm is make && echo OK || echo FAIL. After the build succeeds, run the binary from inside the source tree (./newprogram) before make install so any failures are obviously the build's, not the system's. Only when the in-tree binary works should you escalate to sudo make install.

Example

Imagine you want the GNU diction style checker — a small program that scans text for clichés and verbose phrases — but it isn't in your distribution's repository. The end-to-end recipe demonstrates every concept above.

# 1. Set up a personal source directory
mkdir -p ~/src && cd ~/src

# 2. Download and verify the tarball lays out cleanly
curl -O https://ftp.gnu.org/gnu/diction/diction-1.11.tar.gz
tar tzvf diction-1.11.tar.gz | head      # confirm everything is under diction-1.11/

# 3. Unpack and enter the source tree
tar xzf diction-1.11.tar.gz
cd diction-1.11
cat README INSTALL                       # read what the maintainers want you to know

# 4. Probe the system and generate a Makefile
./configure                              # tail of the output lists what was found

# 5. Build — only modified files are recompiled
make                                     # produces ./diction and ./style executables

# 6. Sanity-check the binary in place
echo "This is utilized to demonstrate that it is the case that diction works." | ./diction

# 7. Install system-wide
sudo make install                        # copies to /usr/local/bin/, /usr/local/share/man/

# 8. Confirm it's on $PATH
which diction && diction --version

What this teaches in one pass:

  • tar tzvf … | head is the inspection-before-extraction habit — it tells you what will land in your current directory.
  • ./configure's output mentions every external dependency it probed. Missing ones turn into actionable errors before any source code is touched.
  • The first make does the heavy lifting; a second make immediately after prints Nothing to be done because every target is up to date — proof that the dependency graph works.
  • touch diction.c && make rebuilds only the affected modules and relinks the executable — a tiny demonstration of incremental compilation.
  • sudo make install is the only command that needs root, because it writes to system directories.

If you later need to uninstall, well-behaved projects support sudo make uninstall — yet another reason to use the install target rather than hand-copying binaries.

Continue exploring

Tags