Compiling Software from Source
Why This Matters
You are setting up a high-performance web server and you need Nginx compiled with a specific third-party module that is not included in your distribution's package. Or you are on an embedded system with no package manager. Or a critical security fix was released two hours ago and your distribution has not packaged it yet. Or you simply need version 3.4 of a tool and your distro ships 3.1.
Compiling from source is one of the most empowering skills a Linux administrator can have. It is the original way software was distributed on Unix -- before package managers existed, everyone compiled from source. Today, you will not do it for every piece of software, but when you need it, nothing else will do.
This chapter walks you through the entire process: from installing build tools to the classic configure-make-install dance, CMake projects, understanding what Makefiles do, custom installation prefixes, creating packages from compiled software, and diagnosing the errors that inevitably appear along the way.
Try This Right Now
Check whether your system has the basic build tools installed:
$ gcc --version
gcc (Debian 12.2.0-14) 12.2.0
...
$ make --version
GNU Make 4.3
$ pkg-config --version
1.8.1
If any of these commands fail with "command not found," you need to install your distribution's development tools -- which is the first thing we cover below.
Why Compile from Source?
Before we dive into the how, let us be clear about the when and why. You should compile from source when:
- The package does not exist in your distribution's repository
- You need a newer version than what the repository provides
- You need custom compile options (enable/disable features, custom modules)
- You need to apply patches (security fixes, bug fixes, custom modifications)
- You are learning how software is built (understanding builds makes you a better debugger)
- You are on a minimal system with no package manager (embedded, rescue environments)
You should not compile from source when:
- The package is available in your distribution's repository (use the package manager instead)
- You are managing many servers (compiled software is harder to update and track)
- You cannot commit to monitoring for security updates (the package manager handles this for you)
┌───────────────────────────────────────────────────────────┐
│ Use Repository Package When: │
│ - Version in repo is sufficient │
│ - You want automatic security updates │
│ - You manage many identical servers │
│ - You want dependency tracking │
├───────────────────────────────────────────────────────────┤
│ Compile from Source When: │
│ - You need a specific version not in the repo │
│ - You need custom compile-time options │
│ - The software is not packaged at all │
│ - You need to apply custom patches │
│ - You are on a minimal or embedded system │
└───────────────────────────────────────────────────────────┘
Think About It: If you compile Nginx from source on a production server, who is responsible for applying security patches to it? How does this differ from using the distribution's package?
Installing Build Prerequisites
Before you can compile anything, you need a compiler, linker, and basic build tools.
Debian/Ubuntu
$ sudo apt update
$ sudo apt install build-essential
The build-essential meta-package installs:
- gcc -- the GNU C compiler
- g++ -- the GNU C++ compiler
- make -- the build automation tool
- libc6-dev -- C library development headers
- dpkg-dev -- Debian package development tools
For many projects, you will also need:
$ sudo apt install pkg-config autoconf automake libtool
Fedora/RHEL
$ sudo dnf groupinstall "Development Tools"
Or install individually:
$ sudo dnf install gcc gcc-c++ make autoconf automake libtool pkgconfig
Arch Linux
$ sudo pacman -S base-devel
Distro Note: The group/meta-package names differ, but they all install the same core tools: a C/C++ compiler, make, and essential development headers.
The Configure-Make-Install Dance
The vast majority of C and C++ projects on Linux follow the same three-step build process. Understanding it deeply will serve you for years.
┌──────────────────────────────────────────────────────────┐
│ │
│ Step 1: ./configure │
│ - Checks your system for required tools and libraries │
│ - Detects compiler, OS, architecture │
│ - Generates a Makefile tailored to YOUR system │
│ │
│ Step 2: make │
│ - Reads the Makefile │
│ - Compiles source code into object files │
│ - Links object files into executables and libraries │
│ │
│ Step 3: make install │
│ - Copies compiled binaries to /usr/local/bin │
│ - Copies libraries to /usr/local/lib │
│ - Copies headers to /usr/local/include │
│ - Copies man pages to /usr/local/share/man │
│ │
└──────────────────────────────────────────────────────────┘
Hands-On: Compiling a Real Program
Let us compile jq, the popular JSON processor, from source. This is a real-world example that demonstrates the full process.
Step 1: Download the source code.
$ mkdir -p ~/src && cd ~/src
$ wget https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-1.7.1.tar.gz
$ tar xzf jq-1.7.1.tar.gz
$ cd jq-1.7.1
Alternatively, clone from Git:
$ git clone https://github.com/jqlang/jq.git
$ cd jq
$ git checkout jq-1.7.1
$ git submodule update --init # Some projects have submodules
$ autoreconf -i # Generate configure script from Git source
Step 2: Inspect what you have.
$ ls
AUTHORS COPYING ChangeLog Makefile.am NEWS README.md configure configure.ac src/ ...
Key files:
- configure: The configuration script (generated by autoconf)
- configure.ac: The source for the configure script (autoconf macros)
- Makefile.am or Makefile.in: Templates that
configureturns into a Makefile - src/: The actual source code
- COPYING or LICENSE: The license
Step 3: Run configure.
$ ./configure --help | head -30
`configure' configures jq 1.7.1 to adapt to many kinds of systems.
Usage: ./configure [OPTION]... [VAR=VALUE]...
Installation directories:
--prefix=PREFIX install architecture-independent files in PREFIX
[/usr/local]
--exec-prefix=EPREFIX install architecture-dependent files in EPREFIX
[PREFIX]
Optional Features:
--enable-maintainer-mode enable make rules and dependencies
--disable-docs do not build documentation
--enable-all-static link jq with static libraries only
Always check --help first. This shows you what you can customize.
$ ./configure --prefix=/usr/local
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
...
checking for oniguruma... yes
...
configure: creating ./config.status
config.status: creating Makefile
The configure script:
- Checks that you have a working C compiler
- Checks for required libraries (like oniguruma for jq's regex support)
- Detects your operating system and architecture
- Writes a Makefile customized for your system
If configure fails, it tells you what is missing:
configure: error: Package requirements (oniguruma) were not met:
No package 'oniguruma' found
The fix is to install the missing development library:
# Debian/Ubuntu
$ sudo apt install libonig-dev
# Fedora/RHEL
$ sudo dnf install oniguruma-devel
# Arch
$ sudo pacman -S oniguruma
Distro Note: Development headers for library
libfooare typically namedlibfoo-devon Debian/Ubuntu andlibfoo-develon Fedora/RHEL. On Arch, the main package usually includes headers.
Step 4: Compile.
$ make
CC src/main.o
CC src/jq_parser.o
CC src/lexer.o
CC src/builtin.o
...
CCLD jq
To speed up compilation on a multi-core system:
$ make -j$(nproc)
nproc returns the number of CPU cores. make -j4 means "run up to 4 compile jobs in parallel."
Step 5: Test (optional but recommended).
Many projects include a test suite:
$ make check
PASS: tests/jqtest
PASS: tests/onigtest
PASS: tests/shtest
==================
All 3 tests passed
==================
Step 6: Install.
$ sudo make install
/usr/bin/install -c -d '/usr/local/bin'
/usr/bin/install -c jq '/usr/local/bin'
/usr/bin/install -c -d '/usr/local/lib'
/usr/bin/install -c -m 644 libjq.a '/usr/local/lib'
/usr/bin/install -c -d '/usr/local/include'
...
Step 7: Verify.
$ which jq
/usr/local/bin/jq
$ jq --version
jq-1.7.1
$ echo '{"name":"linux"}' | jq '.name'
"linux"
Understanding --prefix
The --prefix option controls where make install puts files. This is one of the most important options.
--prefix=/usr/local (default)
/usr/local/bin/jq
/usr/local/lib/libjq.a
/usr/local/include/jq.h
/usr/local/share/man/man1/jq.1
--prefix=/opt/jq-1.7.1
/opt/jq-1.7.1/bin/jq
/opt/jq-1.7.1/lib/libjq.a
/opt/jq-1.7.1/include/jq.h
/opt/jq-1.7.1/share/man/man1/jq.1
--prefix=$HOME/.local
~/.local/bin/jq (no sudo needed!)
~/.local/lib/libjq.a
~/.local/include/jq.h
Choosing a Prefix Strategy
| Prefix | Pros | Cons |
|---|---|---|
/usr/local (default) | In default PATH, shared by all users | Harder to uninstall, may conflict with packages |
/opt/program-version | Easy to remove (just delete the directory), multiple versions can coexist | Need to add to PATH manually |
$HOME/.local | No root required, user-isolated | Not available to other users |
The /opt/program-version approach is especially useful on servers:
$ ./configure --prefix=/opt/nginx-1.25.3
$ make -j$(nproc)
$ sudo make install
# Create a symlink so the "current" version is easy to reference
$ sudo ln -sf /opt/nginx-1.25.3 /opt/nginx
# Add to PATH
$ export PATH=/opt/nginx/sbin:$PATH
When you upgrade, install the new version to /opt/nginx-1.25.4 and update the symlink. Rolling back is just changing the symlink.
Think About It: Why does the default prefix put files in
/usr/localinstead of/usr? What problem does this separation solve?
Understanding Makefiles
When you run make, it reads a file called Makefile (or makefile). Understanding the basics of Makefiles helps you debug build problems.
A Makefile consists of rules:
target: dependencies
command
# Example:
jq: src/main.o src/jq_parser.o src/lexer.o
gcc -o jq src/main.o src/jq_parser.o src/lexer.o -ljq -lonig
src/main.o: src/main.c src/jq.h
gcc -c src/main.c -o src/main.o
Reading this rule: "To build jq, first make sure main.o, jq_parser.o, and lexer.o are up to date, then link them together."
Key concepts:
- target: What to build
- dependencies: What must exist (and be up-to-date) first
- command: How to build it (MUST be indented with a tab, not spaces)
Common Makefile targets:
$ make # Build the default target (usually "all")
$ make all # Build everything
$ make install # Install to prefix
$ make clean # Remove compiled files (object files, binaries)
$ make distclean # Remove everything generated by configure
$ make check # Run tests
$ make uninstall # Remove installed files (not always available)
CMake: The Modern Alternative
Many modern projects (especially C++ ones) use CMake instead of autotools. CMake generates Makefiles (or Ninja build files) from a CMakeLists.txt file.
The workflow is:
┌──────────────────────────────────────────────────────┐
│ │
│ Step 1: mkdir build && cd build │
│ (CMake strongly prefers out-of-source builds) │
│ │
│ Step 2: cmake .. [options] │
│ (Configure the project -- like ./configure) │
│ │
│ Step 3: make -j$(nproc) │
│ (Compile -- same as autotools) │
│ │
│ Step 4: sudo make install │
│ (Install -- same as autotools) │
│ │
└──────────────────────────────────────────────────────┘
Hands-On: Compiling a CMake Project
Let us compile htop (the interactive process viewer) from source. htop uses CMake.
# Install CMake
# Debian/Ubuntu:
$ sudo apt install cmake
# Fedora/RHEL:
$ sudo dnf install cmake
# Arch:
$ sudo pacman -S cmake
# Download and extract
$ cd ~/src
$ wget https://github.com/htop-dev/htop/releases/download/3.3.0/htop-3.3.0.tar.xz
$ tar xJf htop-3.3.0.tar.xz
$ cd htop-3.3.0
# Note: htop actually supports both autotools and CMake.
# We will use CMake here to demonstrate the workflow.
# Create a build directory (out-of-source build)
$ mkdir build && cd build
# Configure
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
-- The C compiler identification is GNU 12.2.0
-- Detecting C compiler ABI info - done
-- Looking for ncursesw
-- Found ncursesw: /usr/lib/x86_64-linux-gnu/libncursesw.so
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/src/htop-3.3.0/build
# Compile
$ make -j$(nproc)
[ 2%] Building C object CMakeFiles/htop.dir/Action.c.o
[ 5%] Building C object CMakeFiles/htop.dir/AvailableColumnsPanel.c.o
...
[100%] Linking C executable htop
# Install
$ sudo make install
CMake options use -D prefix:
$ cmake .. \
-DCMAKE_INSTALL_PREFIX=/opt/htop \
-DCMAKE_BUILD_TYPE=Release \
-DENABLE_UNICODE=ON
To see all available options:
$ cmake .. -LH
checkinstall: Creating Packages from Source
The biggest problem with make install is that your package manager does not know about the installed files. You cannot cleanly uninstall, and upgrades may conflict.
checkinstall solves this by intercepting make install and creating a .deb or .rpm package instead.
# Install checkinstall
# Debian/Ubuntu:
$ sudo apt install checkinstall
# Instead of: sudo make install
# Run:
$ sudo checkinstall --pkgname=jq-custom --pkgversion=1.7.1 --pkgrelease=1 \
--default make install
Creating package jq-custom...
OK
**********************************************************************
Done. The new package has been installed and saved to
/home/user/src/jq-1.7.1/jq-custom_1.7.1-1_amd64.deb
**********************************************************************
Now your package manager knows about it:
$ dpkg -l | grep jq-custom
ii jq-custom 1.7.1-1 amd64 Package created with checkinstall
# Clean uninstall through the package manager
$ sudo dpkg -r jq-custom
You can also save the .deb file and install it on other identical systems:
$ sudo dpkg -i jq-custom_1.7.1-1_amd64.deb
Distro Note: checkinstall supports creating
.deb(Debian/Ubuntu),.rpm(Fedora/RHEL), and Slackware packages. On Fedora, you may need to install it from source since it is not always in the official repos.
Common Compilation Errors and Fixes
Compilation will fail. It is not a question of if but when. Here are the errors you will encounter most often and exactly how to fix them.
Error: "configure: error: no acceptable C compiler found"
configure: error: in `/home/user/src/project':
configure: error: no acceptable C compiler found in $PATH
Fix: Install the compiler.
# Debian/Ubuntu
$ sudo apt install build-essential
# Fedora/RHEL
$ sudo dnf install gcc gcc-c++ make
Error: Missing library or header file
configure: error: Package requirements (libxml-2.0 >= 2.9) were not met:
No package 'libxml-2.0' found
Or during compilation:
src/parser.c:15:10: fatal error: libxml/parser.h: No such file or directory
#include <libxml/parser.h>
^~~~~~~~~~~~~~~~~
compilation terminated.
Fix: Install the development package for the missing library.
# Find the right package name
# Debian/Ubuntu:
$ apt search libxml2 | grep dev
libxml2-dev/stable 2.9.14+dfsg-1.3 amd64
$ sudo apt install libxml2-dev
# Fedora/RHEL:
$ dnf search libxml2 | grep devel
libxml2-devel.x86_64
$ sudo dnf install libxml2-devel
The pattern is consistent:
- Missing
libfoo-> installlibfoo-dev(Debian) orlibfoo-devel(Fedora)
Error: "make: *** No targets specified and no makefile found"
make: *** No targets specified and no makefile found. Stop.
Fix: You forgot to run ./configure first, or configure failed. Check for a configure script:
$ ls configure
If there is no configure script, check for:
CMakeLists.txt-- use cmakeautogen.shorbootstrap.sh-- run it to generate configureconfigure.ac-- runautoreconf -ito generate configureMakefile-- some projects ship a Makefile directly (just runmake)meson.build-- uses the Meson build system
Error: Linker errors ("undefined reference to")
/usr/bin/ld: src/main.o: undefined reference to `json_parse'
collect2: error: ld returned 1 exit status
Fix: A required library is not being linked. This usually means:
- The library is not installed (install the
-dev/-develpackage) - The library is installed but not found (set
PKG_CONFIG_PATHorLDFLAGS)
$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH
$ export LDFLAGS="-L/usr/local/lib"
$ export CFLAGS="-I/usr/local/include"
$ ./configure
Error: "Permission denied" during make install
install: cannot create regular file '/usr/local/bin/jq': Permission denied
Fix: Use sudo:
$ sudo make install
Or install to a location you own:
$ ./configure --prefix=$HOME/.local
$ make
$ make install # No sudo needed
Error: Version mismatch
configure: error: You need at least autoconf 2.69 to build this project
Fix: Install a newer version of the required tool, or download the release tarball instead of the Git source (release tarballs include pre-generated configure scripts and do not need autoconf).
Hands-On: Complete Build-from-Source Workflow
Let us put it all together with a structured example. We will compile tree (a simple directory listing program) from source.
# Step 1: Create a workspace
$ mkdir -p ~/src && cd ~/src
# Step 2: Download
$ wget https://gitlab.com/OldManProgrammer/unix-tree/-/archive/2.1.1/unix-tree-2.1.1.tar.gz
$ tar xzf unix-tree-2.1.1.tar.gz
$ cd unix-tree-2.1.1
# Step 3: Look at what we have
$ ls
CHANGES INSTALL LICENSE Makefile README.md doc/ man/ tree.c ...
# This project has a Makefile directly -- no configure step needed!
# Step 4: Read the install instructions
$ cat INSTALL
# (Always read the INSTALL or README file before building)
# Step 5: Build
$ make -j$(nproc)
gcc -O2 -Wall -o tree tree.c color.c hash.c html.c json.c unix.c xml.c
# Step 6: Test it
$ ./tree ~/src --dirsfirst -L 1
/home/user/src
├── jq-1.7.1
├── unix-tree-2.1.1
└── htop-3.3.0
# Step 7: Install to a custom prefix
$ make PREFIX=/opt/tree-2.1.1 install
# Or use checkinstall:
$ sudo checkinstall --pkgname=tree-custom --pkgversion=2.1.1 --default \
make PREFIX=/usr/local install
Build Systems Reference
Not every project uses autotools. Here is a quick reference for the build systems you will encounter:
| Build System | Identifier | Configure Step | Build Step |
|---|---|---|---|
| Autotools | configure, configure.ac | ./configure | make |
| CMake | CMakeLists.txt | cmake .. | make or cmake --build . |
| Meson | meson.build | meson setup build | ninja -C build |
| Plain Makefile | Makefile only | None | make |
| Go | go.mod | None | go build |
| Rust/Cargo | Cargo.toml | None | cargo build --release |
| Python | setup.py, pyproject.toml | None | pip install . |
For Meson (increasingly common in GNOME/freedesktop projects):
$ sudo apt install meson ninja-build # or dnf/pacman equivalent
$ meson setup build --prefix=/usr/local
$ ninja -C build
$ sudo ninja -C build install
Debug This
A colleague is trying to compile a project and hits this error:
$ ./configure
checking for pkg-config... /usr/bin/pkg-config
checking pkg-config is at least version 0.9.0... yes
checking for ZLIB... no
configure: error: zlib library not found
$ dpkg -l | grep zlib
ii zlib1g 1:1.2.13.dfsg-1 amd64 compression library - runtime
They say: "But zlib IS installed! I can see it right there!"
What is the problem?
The runtime library (zlib1g) is installed, but the development headers (zlib1g-dev) are not. The runtime library contains the .so file that programs link against at runtime. The development package contains the .h header files and the pkg-config metadata that the configure script needs at compile time.
# The fix:
$ sudo apt install zlib1g-dev
# Now configure will find it:
$ ./configure
checking for ZLIB... yes
This is the single most common compilation issue on Linux. The runtime package and the development package are separate. You always need the -dev (Debian) or -devel (Fedora) package to compile against a library.
What Just Happened?
┌──────────────────────────────────────────────────────────────┐
│ │
│ In this chapter, you learned: │
│ │
│ - When to compile from source vs. using packages: │
│ custom versions, custom options, unavailable packages. │
│ │
│ - The build-essential / Development Tools packages │
│ provide gcc, g++, make, and development headers. │
│ │
│ - The configure-make-install workflow: │
│ ./configure checks your system, make compiles, │
│ make install copies files to the prefix. │
│ │
│ - --prefix controls where files are installed. │
│ /usr/local (default), /opt/name-version, ~/.local │
│ │
│ - CMake projects use: mkdir build && cd build && │
│ cmake .. && make && make install │
│ │
│ - checkinstall creates .deb/.rpm packages from │
│ make install, giving you clean uninstall via │
│ the package manager. │
│ │
│ - Most compilation errors come from missing -dev/-devel │
│ packages. The pattern: find the library name, install │
│ its development package. │
│ │
│ - Always read INSTALL or README before building. │
│ Always check ./configure --help for options. │
│ │
└──────────────────────────────────────────────────────────────┘
Try This
Exercises
-
Basic build: Download and compile GNU
hellofrom source (https://ftp.gnu.org/gnu/hello/). This is the simplest possible autotools project -- the "Hello, World!" of compilation. Use./configure --prefix=$HOME/.local,make, andmake install. -
Custom prefix: Compile
jqfrom source with--prefix=/opt/jq-custom. After installation, verify you can run it by adding/opt/jq-custom/binto your PATH. Then remove it cleanly by deleting the/opt/jq-customdirectory. -
CMake project: Find a small CMake-based project on GitHub (the CMake tutorial projects work well) and build it using the out-of-source build workflow.
-
Error diagnosis: Intentionally try to compile a project without installing its required dependencies. Read the error messages carefully and identify which
-devpackages you need to install. -
checkinstall: Compile any small program and use checkinstall to create a
.debor.rpmpackage. Install it, verify it works, then remove it using your package manager.
Bonus Challenge
Download the Nginx source code from nginx.org. Compile it with a custom set of modules:
- Enable the
http_ssl_module(requireslibssl-dev) - Enable the
http_v2_module - Disable the
http_autoindex_module - Install to
/opt/nginx-custom
Read ./configure --help to find the exact flags. This is a realistic task -- sysadmins frequently compile Nginx with custom module sets.
What's Next
When you compile and install software, the resulting binaries depend on shared libraries -- .so files that are loaded at runtime. Chapter 59 explains how shared libraries work, how the dynamic linker finds them, and how to troubleshoot the dreaded "cannot open shared object file" error.