•7 min read

Compiling C Programs with Make: What I Wish I Knew

Compiling C Programs with Make: What I Wish I Knew

The first time I tried to compile something from source, I spent three hours on a Friday night Googling error messages. Three hours. For a program that was supposed to take five minutes.

I'd come from Python and JavaScript worlds where pip install and npm install just worked. Dependency resolution, lockfiles, friendly error messages — all handled. Then I needed a C library that wasn't on PyPI, and suddenly I was reading linker output like it was hieroglyphics.

It doesn't have to be that painful. Here's what actually works.

Audio Briefing
0:00 / 0:00

The configure && make && make install Pattern

Most C projects you'll encounter follow the same three-step ritual:

./configure
make -j$(nproc)
sudo make install

The configure script pokes around your system, figures out what libraries you have, and writes a Makefile specific to your machine. Then make does the actual compilation. Finally, make install copies the finished binary to somewhere your shell can find it.

Not every project uses autotools. Some just have a hand-written Makefile or use CMake instead. But enough use the configure/make pattern that you'll encounter it constantly. When in doubt, check the README or INSTALL file — they'll usually tell you exactly what to run.

Not sure what build system a project uses? Look for configure, CMakeLists.txt, Makefile, or meson.build in the root directory. That's your first clue.

Advertisement

The Part Nobody Warns You About: Dependencies

Here's where things usually fall apart. You run ./configure and it spits out something like:

checking for libqpdf... no
configure: error: Package requirements (libqpdf >= 8.0) were not met

This error has ended more compile sessions than I can count. The program needs a library, and you don't have it installed.

On Ubuntu or Debian, you'll want the -dev version of the package. The regular package gives you the runtime library, but you need the headers to actually compile anything:

sudo apt install -y libqpdf-dev

On macOS with Homebrew, it's simpler. Homebrew doesn't split packages into dev and runtime variants:

brew install qpdf

One more macOS tip: if you installed a library with Homebrew and configure still can't find it, the paths might not be set up correctly. I'll cover this in the next section.

The macOS Homebrew Path Problem

I use a MacBook for work. Homebrew is great, but it installs everything in non-standard locations. On Apple Silicon, libraries go to /opt/homebrew/. On Intel Macs, they go to /usr/local/.

The compiler doesn't know to look there automatically. So even if you've installed something with Homebrew, your configure script might still complain it can't find it.

When you see ld: library not found, you need to tell the compiler where to look:

CPPFLAGS="-I/opt/homebrew/include" \
LDFLAGS="-L/opt/homebrew/lib" \
./configure

CPPFLAGS adds include directories. LDFLAGS adds library directories. Without these, configure won't find headers or libraries that live outside the standard system paths.

Sometimes you need to do this for make directly, when a project doesn't use configure:

CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make

Different project, slightly different syntax. The principle is the same though: point the compiler at where Homebrew actually put the files.

Make It Fast: The -j Flag

This one took me embarrassingly long to discover.

By default, make compiles with one job — one CPU core, working as hard as it can, while the rest of your machine sits idle. On a modern 8-core laptop, that's using 12.5% of your potential.

What do you do instead?

make -j$(nproc)    # Linux
make -j$(sysctl -n hw.ncpu)  # macOS

The -j flag runs multiple compilation jobs in parallel. $(nproc) just asks Linux how many CPU cores you have. On macOS, sysctl -n hw.ncpu does the same thing.

For small projects, you won't notice much difference. For anything substantial — like compiling something with thousands of source files — this changes everything. What took five minutes might take forty-five seconds. For really large projects, it can mean the difference between a coffee break and lunch.

How many cores do you actually have? Most modern laptops have 4-8 cores. Run nproc or sysctl -n hw.ncpu and see what you get. Then use that number with -j.

Advertisement

When It Still Doesn't Work

Sometimes you've done everything right and things still fail. Here are the most common gotchas I've run into:

Symbol not found at link time — Usually means a library version mismatch. The program expects a newer version of something than you have, or maybe an older one with a different API. Check the project's documentation for the exact version it needs.

Permission denied on configure — Sometimes the configure script isn't executable yet. Just run chmod +x configure and try again.

Don't want to use sudo? — That's fair. System-wide installs require root, but you can install to your home directory instead:

./configure --prefix=$HOME/.local
make -j$(nproc)
make install

Now the program installs to ~/.local/bin/ instead of /usr/local/bin/. You'll want to add ~/.local/bin to your PATH if it isn't already.

What I wish I'd Known Sooner

Looking back at that Friday night three-hour debugging session, the problems were all solvable. I just didn't know what I didn't know.

A few things that would've saved me a lot of time:

The -dev package suffix matters on Ubuntu. Without it, you have the library but not the headers. Configure can't build against a library it can't see.

Homebrew puts things in non-standard locations. Setting CPPFLAGS and LDFLAGS isn't optional on macOS — it's how you tell the build system where to find what you installed.

The -j flag isn't an optimization. It's a requirement. One core compiles slowly. All your cores compile fast.

And finally, when configure fails, the error message usually tells you exactly what's missing. Read it before you Google. "Package requirements (libqpdf >= 8.0) were not met" isn't cryptic — it's a checklist item. Install that package, and move on.

Compiling from source isn't as scary as it looks. The build systems are older and the error messages are less friendly, but the process is actually pretty predictable once you've done it a few times. Give it another shot. Your first successful compile from source feels good.


Want me to walk through compiling something specific? Most of my recent compiles have gone wrong because I skipped one of these steps. Happy to help you debug whatever you're working on.

You Might Also Like

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement