A Guide to make for Non-C Programmers: From Installing Dependencies to Troubleshooting Builds

A complete practical guide for non-C programmers to compile C/C++ programs from source using make.
This article systematically walks non-C programmers through the complete process of compiling C/C++ programs from source using make. It covers installing build tools, manually managing dependencies, understanding how configure scripts generate Makefiles, troubleshooting common errors in the compilation and linking stages, solving library path issues via environment variables, and practical tips like single-file compilation and referencing build configs from other distributions.
For non-C programmers, compiling C/C++ programs from source has always been a headache. Many people's strategy is "run make, and if that doesn't work, find a pre-built binary, and if that doesn't work, give up." But when you switch from Linux to Mac, or need to compile some niche tools, having basic make compilation skills becomes indispensable.
This article is based on Julia Evans' practical experience and systematically walks through the complete process of compiling C programs with make. Even if you've never written a line of C code, you'll find it useful.

Installing a C Compiler and Basic Build Tools
On Ubuntu, a single command gets the job done:
sudo apt-get install build-essential
This installs the trio of gcc, g++, and make. On Mac, things are a bit more complicated — you'll typically need to install Xcode Command Line Tools.
Handling C Dependencies — A World Without Package Managers
C doesn't have a dependency manager like npm or pip. All dependencies must be installed manually. The good news is that precisely because of this, C programmers tend to keep dependencies to a minimum.
Why doesn't C have a language-level package manager? This is tied to C's history. C was born in 1972, long before the concept of modern package managers — npm was created in 2010, pip in 2008, and C's ecosystem was already highly mature and solidified before these tools existed. C programs have long relied on OS-level package managers (like apt, brew) for dependency management rather than language-level tools, which is one of the root causes of cross-platform compilation complexity. In recent years, C/C++-specific package managers like Conan and vcpkg have emerged, but their adoption in open-source toolchains remains limited.
Take paperjam as an example — its README clearly states that libqpdf-dev and libpaper-dev are needed:
sudo apt install -y libqpdf-dev libpaper-dev
Important note: Package names mentioned in READMEs almost always refer to Debian-based Linux distribution package names. If you're on Mac, brew install libqpdf-dev won't work — you'll usually need to find the corresponding Homebrew package name (e.g., brew install qpdf).
It's worth noting that in Linux package managers, the same library often has two packages: libqpdf (the runtime library, used by regular users to run programs) and libqpdf-dev (the development package, which includes header files, used by developers to compile programs). When compiling from source, you need the -dev version.
Understanding the Role of configure Scripts
Some C programs come with a Makefile, while others provide a ./configure script (part of the autotools system). SQLite's source code, for instance, uses the latter approach.
Historical background of autotools: Autotools is a build system developed by the GNU Project, consisting of three tools — autoconf, automake, and libtool — and was created in the early 1990s. Its core design goal was to solve cross-Unix platform portability issues — back then, different Unix systems (AIX, HP-UX, Solaris, etc.) had vastly different system calls and library interfaces. A configure script is essentially a shell script that probes the current system's compiler capabilities, library availability, and paths to generate a Makefile tailored to the current environment. Although autotools has been widely criticized for its complexity and slow configuration process, due to historical momentum, many important open-source projects (including SQLite and GCC itself) still use it.
What ./configure does is straightforward:
- Run it, and it outputs a flood of detection information
- If successful, it generates a
Makefile - If it fails, it means some dependencies are missing
For non-C programmers, all you need to know is "run ./configure to generate a Makefile" — there's no need to dive deep into how autotools works.
Running make and Handling Compilation/Linking Errors
Basic Usage
make # Basic compilation
make -j8 # Parallel compilation with 8 threads, much faster
During compilation, you'll typically see a flood of warning messages — just ignore them. You didn't write this code, and compiler warnings are not your problem.
Compilation and Linking: Two Critical Stages
Building a C program involves two key steps:
- Compiling: Converting source code into object files (.o files), using gcc or clang
- Linking: Combining object files into the final binary, using ld
This two-stage design stems from Unix's modular philosophy and forms the foundation for incremental builds in large projects. During the compilation stage, the compiler independently converts each .c source file into a machine code object file (.o), where function call addresses are unresolved placeholders. During the linking stage, the linker (ld) merges all .o files and external libraries, resolves all symbol references, and produces the final executable. This design means that modifying a single file only requires recompiling that file and re-linking, rather than recompiling the entire project — this is precisely the foundation of make's incremental build capability.
The difference between dynamic libraries (.so/.dylib) and static libraries (.a) also manifests during the linking stage: the former are loaded from disk at runtime, while the latter are embedded directly into the binary at link time. Understanding this is important because many compilation errors are fundamentally about the compiler or linker being unable to find where dependencies are located.
Solving "Library Not Found" Issues with Environment Variables
When you encounter an error like ld: library 'qpdf' not found, even if the library is installed, the compiler/linker might not know where to look. The solution is to pass the correct paths via environment variables:
CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make paperjam
To understand these flags, you first need to understand the different roles of header files and library files: header files (.h) are interface declaration files that tell the compiler the parameter types and return types of functions — they're needed during the compilation stage; library files (.so/.a/.dylib) contain the actual implementation code — they're needed during the linking stage. This is why you need -I and -L to specify two different types of search paths.
Here's what the key flags mean:
-I(compiler flag): Specifies the directory to search for header files-L(linker flag): Specifies the directory to search for library files-l(linker flag): Specifies the library to link (e.g.,-liconvmeans link the iconv library)
make's Implicit Variables
make has a set of built-in implicit variables that are automatically passed to the C compiler and linker. Common ones include CPPFLAGS, CXXFLAGS, LDFLAGS, and more.
You might not have noticed, but there are two ways to pass environment variables:
CXXFLAGS=xyz make: Won't override settings in the Makefilemake CXXFLAGS=xyz: Will override settings in the Makefile
Practical Compilation Tips
Compiling Only a Single File
If a repository contains multiple tools and you only want to compile one of them:
make qf # Only compile the qf tool
Compiling Simple Programs Without a Makefile
For simple single-file C programs (like blah.c), just run:
make blah
make will automatically expand this to cc -o blah blah.c, saving you the trouble of typing the compile command manually.
Referencing Build Configurations from Other Package Systems
This is an extremely valuable tip: when you hit compilation difficulties, look at how other Linux distributions build the same package. For example, the Nix package file for paperjam states:
env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-liconv
Related articles
TutorialsChatGPT Plus Subscription Guide: Are GPT-5.5, image-2, and Codex Worth the Upgrade?
A detailed look at ChatGPT Plus features — GPT-5.5, image-2, and Codex — with a Plus vs Pro comparison and a complete step-by-step subscription guide for users outside the US.
TutorialsHarness AI Engineering in Practice: Using Claude Code to Master Enterprise-Level E-Commerce Development
Deep dive into Harness AI Engineering: master enterprise e-commerce development with Claude Code using the Rules, Skills, Wiki, and Changes framework.
TutorialsCursor + Codex Dual-IDE Collaboration: A Practical Methodology for Open-Source Project Customization
A complete methodology for open-source project customization based on real-world experience, detailing the Cursor+Codex dual-IDE workflow, seven-stage process, MVP validation, and AI source code reading techniques.