Long before I trusted package managers to handle everything, I learned Linux administration the hard way: downloading tarballs, reading README files, and running ./configure && make && make install more times than I can count. Even now, with excellent package managers everywhere, there are still plenty of situations where building from source is the right — or only — option. This guide covers the whole process, the tools behind it, and the judgment calls I’ve picked up along the way.
Why Build From Source At All
Package managers are great, but they don’t always have what you need. I build from source when:
- The software isn’t packaged for my distribution at all
- I need a newer or older version than what the distro repository offers
- I need custom compile-time options (specific feature flags, different library backends, optimization for my exact CPU)
- I’m applying a patch or modification that isn’t upstream yet
- I’m working with development or nightly builds not meant for general packaging
- I need reproducible, portable builds for a specific deployment target
The General Workflow
Most C/C++ open-source projects, and a good chunk of everything else, follow roughly the same pattern:
tar -xzf software-1.0.tar.gz
cd software-1.0
./configure
make
sudo make install
That’s the classic four-step dance, and understanding what each step actually does will make troubleshooting dramatically easier when something inevitably doesn’t go smoothly.
Step 1: Get Development Tools Installed First
Before attempting any build from source, install a proper build toolchain. On Debian/Ubuntu, the build-essential metapackage covers the fundamentals:
sudo apt update
sudo apt install build-essential
This pulls in gcc, g++, make, and standard C library headers. On RHEL/Fedora/CentOS, the equivalent is the “Development Tools” group:
sudo dnf groupinstall "Development Tools"
Individual projects often need additional development libraries beyond this baseline — things like libssl-dev, zlib1g-dev, libxml2-dev — which the project’s README or INSTALL file should list as dependencies. I always read that file first before running anything.
Step 2: The configure Script
The configure script is generated by GNU Autotools (specifically autoconf) and its job is to probe your system, check for required libraries, headers, and compiler capabilities, and then generate a Makefile tailored to what it found.
./configure
Running it with no options uses sensible defaults — typically installing to /usr/local. You can see all available options for a given project:
./configure --help
Common options I use regularly:
./configure --prefix=/opt/myapp
--prefix controls the installation root. I use this constantly to keep source-built software cleanly separated from distro-packaged software, usually installing under /opt/<appname> or /usr/local rather than directly into /usr, which I reserve exclusively for the package manager’s territory.
./configure --enable-feature-x --disable-feature-y
Most projects expose --enable-*/--disable-* flags for optional functionality, letting you trim dependencies you don’t need or turn on functionality that’s off by default.
./configure --with-openssl=/usr/local/openssl
--with-* flags typically point the build at a specific external library location, useful when you have multiple versions of a dependency installed and need to be explicit about which one to link against.
What configure Actually Checks
Under the hood, configure runs a long series of small test compilations to detect things like: which compiler is available and what it supports, whether specific headers exist, whether specific library functions are present, byte order (endianness), and pointer size. Each check appears as a line like checking for zlib.h... yes in the output, and any failure at this stage almost always means a missing development package, which the error message usually names directly.
Step 3: Compiling with make
make
make reads the generated Makefile and compiles source files into object files, then links those into the final binaries and libraries, only rebuilding files that have actually changed since the last build (tracked via file modification timestamps), which is why incremental rebuilds during development are so much faster than the initial full build.
Parallelizing the Build
make -j$(nproc)
Just like kernel builds, most software builds benefit enormously from parallel compilation across all available CPU cores.
Building Specific Targets
Many projects define multiple targets beyond the default:
make check # run the project's test suite
make doc # build documentation
make clean # remove build artifacts
make distclean # remove build artifacts AND configure-generated files
I run make check religiously before installing anything on a production system, since it catches environment-specific build problems before they become runtime surprises.
Step 4: Installing with make install
sudo make install
This copies compiled binaries, libraries, headers, and documentation into the locations determined by the --prefix (and related --bindir, --libdir, etc.) options passed to configure earlier. I always run a plain make -n install first (dry run) to preview exactly what will be copied where, before committing to it with root privileges:
make -n install
Tracking What Got Installed
Raw make install doesn’t register anything with your package manager, meaning apt remove or dnf remove won’t ever know about it. This is one of the biggest downsides of building from source directly. I handle this a couple of ways:
Using checkinstall (Debian/Ubuntu) to wrap the install step into an actual trackable .deb package:
sudo apt install checkinstall
sudo checkinstall
This runs make install under the hood but records every installed file into a proper Debian package, so dpkg -r <package> can cleanly remove it later.
Using stow for filesystem-level tracking, installing into a dedicated directory and symlinking into place:
./configure --prefix=/usr/local/stow/myapp-1.0
make && make install
cd /usr/local/stow
sudo stow myapp-1.0
Removal is then just sudo stow -D myapp-1.0, which cleanly removes only the symlinks it created.
CMake-Based Projects
Not everything uses Autotools. A huge number of modern C/C++ projects use CMake instead:
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/opt/myapp -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
sudo make install
-DCMAKE_BUILD_TYPE=Release enables compiler optimizations; Debug instead keeps debug symbols and disables optimization, useful when you need to debug the software itself with gdb.
Some newer CMake versions support a unified build/install invocation:
cmake --build . -j$(nproc)
cmake --install .
Language-Specific Build Systems
Plenty of software isn’t C/C++ at all, and “building from source” looks different depending on the ecosystem:
Python:
python3 -m venv venv
source venv/bin/activate
pip install --break-system-packages .
# or, for editable/development installs:
pip install --break-system-packages -e .
Rust (Cargo):
cargo build --release
sudo cp target/release/mybinary /usr/local/bin/
Go:
go build -o myapp .
sudo cp myapp /usr/local/bin/
Node.js:
npm install
npm run build
I mention these because “build from source” as a phrase covers a lot of ground beyond the classic Autotools flow, and each ecosystem has developed its own conventions for dependency resolution and compilation.
Handling Missing Dependencies
The single most common obstacle during a from-source build is a missing development library. The error usually looks something like:
configure: error: OpenSSL development files not found
or during the make stage:
fatal error: openssl/ssl.h: No such file or directory
The fix is almost always installing the corresponding -dev (Debian) or -devel (RHEL) package:
sudo apt install libssl-dev # Debian/Ubuntu
sudo dnf install openssl-devel # RHEL/Fedora
If you’re unsure which package provides a missing header, searching your package manager’s file index helps:
apt-file search openssl/ssl.h # Debian/Ubuntu, requires apt-file installed
dnf provides "*/openssl/ssl.h" # RHEL/Fedora
Verifying the Build Environment
Before diving into a build, I check a few basics that resolve a huge fraction of mysterious build failures upfront:
gcc --version
make --version
pkg-config --version
pkg-config deserves special mention — many configure scripts and CMake files rely on it to locate installed libraries and their compile/link flags. If it’s missing or a library’s .pc file isn’t in pkg-config‘s search path, builds fail in confusing ways that have nothing obviously to do with pkg-config itself.
pkg-config --cflags --libs openssl
A Full Real-World Example
Here’s a realistic sequence for building a hypothetical library from source, cleanly, with tracking:
# 1. Install build dependencies
sudo apt update
sudo apt install build-essential libssl-dev zlib1g-dev checkinstall
# 2. Download and extract
wget https://example.org/releases/mylib-2.3.0.tar.gz
tar -xzf mylib-2.3.0.tar.gz
cd mylib-2.3.0
# 3. Configure with a custom prefix
./configure --prefix=/usr/local --enable-shared
# 4. Build in parallel
make -j$(nproc)
# 5. Run the test suite before trusting it
make check
# 6. Install via checkinstall for trackability
sudo checkinstall --pkgname=mylib --pkgversion=2.3.0
Troubleshooting Common Build Failures
“configure: command not found” — the extracted archive might not actually contain a configure script; check if it needs autoreconf -i run first (common when building directly from a git checkout rather than an official release tarball).
Linker errors like “undefined reference to…” — usually a missing -l<library> flag or the library isn’t installed at all; check the project’s dependency list again and confirm the corresponding -dev/-devel package is present.
“Permission denied” during make install — you forgot sudo, or you’re installing into a system directory without appropriate privileges; alternatively, reconsider whether a --prefix pointing to your home directory makes more sense for that particular piece of software.
Build succeeds but the binary won’t run — “error while loading shared libraries” — the dynamic linker can’t find a shared library you just built; either run sudo ldconfig after installing to refresh the linker cache, or set LD_LIBRARY_PATH to include the custom install location.
sudo ldconfig
# or
export LD_LIBRARY_PATH=/opt/myapp/lib:$LD_LIBRARY_PATH
Old cached configure results causing weird behavior after changing options — run make distclean and re-run ./configure fresh rather than fighting a stale config.cache.
Performance and Optimization Notes
Compiling with target-specific optimization flags can produce genuinely faster binaries than generic distro packages built for broad compatibility:
CFLAGS="-O3 -march=native" ./configure
make -j$(nproc)
-march=native tells GCC to optimize specifically for the CPU it’s currently running on, which is fantastic for a build that will run on the exact same machine, but a bad idea if you intend to copy the resulting binary to different hardware, since it may use instructions unavailable on other CPUs.
Security Considerations
Building from source means you’re trusting the source itself far more directly than you’d trust a signed distro package. I always verify checksums or GPG signatures on downloaded tarballs when the project provides them:
wget https://example.org/releases/mylib-2.3.0.tar.gz.asc
gpg --verify mylib-2.3.0.tar.gz.asc mylib-2.3.0.tar.gz
I also avoid running make install as root any more than strictly necessary, preferring a --prefix under a directory a regular user can write to when the software doesn’t genuinely need to live in system-wide paths.
Uninstalling Software Built with a Raw make install
If you skipped checkinstall or stow and ran a plain sudo make install, uninstalling cleanly later becomes genuinely difficult unless the project’s Makefile provides an uninstall target — some do, many don’t:
sudo make uninstall
If that target doesn’t exist, your options are limited: manually deleting files based on make -n install‘s earlier dry-run output (assuming you saved it), or reinstalling with checkinstall retroactively isn’t possible after the fact — it needs to wrap the original install step. This is precisely why I treat checkinstall or stow as close to mandatory now for anything I intend to keep around, rather than a nice-to-have; the five extra minutes it takes upfront saves real headaches during later cleanup or upgrades.
Comparing Build-from-Source with Language-Native Package Managers
For certain ecosystems, a “build from source” instinct sometimes fights against tooling that already solves the problem more cleanly. Python’s pip, Rust’s cargo, and Node’s npm all have their own dependency resolution and build orchestration, and reaching for a manual ./configure && make workflow on a project that already ships a proper pyproject.toml, Cargo.toml, or package.json is usually unnecessary friction. I reserve the classic Autotools/CMake workflow described in this guide specifically for C/C++ projects and system-level software, and lean on each ecosystem’s native tooling for everything else, even when the end result is still, technically, “building from source.”
Keeping Track of Multiple Source-Built Versions
On development machines where I need to switch between multiple versions of the same source-built software, I install each version under its own versioned prefix and use symlinks to control which one is “active”:
./configure --prefix=/opt/mylib-2.3.0
make -j$(nproc) && sudo make install
./configure --prefix=/opt/mylib-2.4.0
make -j$(nproc) && sudo make install
sudo ln -sfn /opt/mylib-2.4.0 /opt/mylib-current
Scripts and other software then reference /opt/mylib-current, and switching versions becomes a matter of repointing a single symlink rather than reinstalling or fighting over a shared, single-version install location.
Summary
Building software from source comes down to a rhythm: get your dependencies sorted, let configure (or cmake) probe your system and generate build instructions, let make compile everything, and install deliberately with a tracking mechanism so you’re not left with untracked files scattered across your filesystem. It’s more hands-on than apt install, but it gives you control that package managers can’t — exact versions, custom compile flags, and the ability to build things your distribution simply doesn’t package. Once you’ve done it a few times, reading a project’s README and translating it into a working build becomes second nature.
References
- GNU Autoconf/Automake manual (gnu.org/software/autoconf/, gnu.org/software/automake/)
- CMake official documentation (cmake.org/cmake/help/latest/)
- Debian New Maintainers’ Guide, on packaging source builds
- Individual project INSTALL/README files, always the primary authoritative source for build instructions