Installing from Source

Most of the time, apt install puts software on your system in seconds. But sometimes the version in your distribution’s repositories is old, the package isn’t packaged for your distro at all, or you need to compile it with a specific feature enabled. Installing from source means downloading a program’s actual source code and using a compiler toolchain to build a runnable binary yourself, rather than downloading something already built. It gives you the newest code and full control over how it’s built, at the cost of manually handling dependencies, updates, and cleanup.

Overview: What “Building from Source” Actually Means

When apt installs a package, it downloads a pre-compiled binary plus metadata, and dpkg records every file it placed on disk so it can be cleanly removed later. Building from source skips all of that: you get plain text source files (.c, .cpp, etc.), and it’s up to you to turn them into an executable using a compiler like gcc.

A compiler translates source code into object files — machine code for your CPU’s instruction set, one object file per source file. A linker then combines those object files with any required libraries into a single executable. For any project with more than a handful of source files, doing this by hand would mean typing dozens of gcc commands in the right order, so nearly every project ships a build system that automates it.

The most common one you’ll meet on Linux is the GNU Autotools family (autoconf and automake). Projects built with it ship a configure script that probes your system — does a C compiler exist, is a required header like ncurses.h present, which libraries are installed — and then generates a Makefile tailored to your machine. Running make reads that Makefile, which lists build targets, the files each target depends on, and the shell commands needed to produce it, then compiles only what’s changed since the last build. make install (almost always run with sudo, since it writes outside your home directory) copies the finished binary, libraries, and man pages into system directories.

Source Install vs. Package Manager Install

Package manager (apt) Building from source
Pre-built binary, installs in seconds Compiled locally; can take minutes
Version fixed by the distro’s repos Any version, including unreleased branches
Tracked by dpkg, clean apt remove Not tracked; manual or make uninstall
Security patches via apt upgrade You are responsible for rebuilding on updates

Why /usr/local Instead of /usr

On Debian and Ubuntu, apt and dpkg consider /usr their territory — every file under it is supposed to be tracked in the package database. The Filesystem Hierarchy Standard reserves /usr/local for software installed outside the package manager precisely so the two never collide. Nearly every source build defaults its install prefix to /usr/local, which is why a source-installed binary usually ends up at /usr/local/bin rather than /usr/bin. Installing straight into /usr risks silently overwriting a file apt thinks it owns, with no record of what you changed.

When to Build from Source (and When Not To)

Build from source when you need a version newer than what your distro ships, the software isn’t packaged for Debian/Ubuntu or RHEL/Fedora at all, you need to compile in an optional feature that the distro’s build disabled, or you’re debugging the software itself. Stick with apt (Debian/Ubuntu) or dnf/yum (RHEL/Fedora) for everything else — you get automatic security updates, dependency resolution, and clean removal for free.

Syntax: The Standard Build Workflow

Autotools-based projects follow the same three-command pattern. It isn’t a single command with flags — it’s a sequence, shown here as a generic pattern:

./configure [--prefix=<install-path>] [--enable-<feature>] [--disable-<feature>]
make
sudo make install
Option Meaning
--prefix=DIR Install root; binaries go to DIR/bin, libs to DIR/lib, etc. Defaults to /usr/local.
--enable-FEATURE Turn on an optional feature the project supports building with.
--disable-FEATURE Turn off a feature that would otherwise build by default.
--with-PACKAGE Build using an optional external library or tool if present.
--without-PACKAGE Explicitly skip an optional dependency even if it’s installed.
--help List every flag a specific project’s configure script actually supports.

Not every project uses configure at all. Some, like Redis, ship a hand-written Makefile with no configure step. Others use CMake (cmake .. && make) or a language-specific build tool. Always check the project’s README or INSTALL file first instead of assuming the autotools pattern applies.

Examples

Example 1: Building htop from a Release Tarball

htop is a common target for a source build when a distro’s repo version lags behind upstream. Install the build toolchain and htop’s specific dependencies first, then download, verify, and build:

sudo apt update
sudo apt install -y build-essential autoconf automake pkg-config libncursesw5-dev wget

cd /tmp
wget https://github.com/htop-dev/htop/releases/download/3.3.0/htop-3.3.0.tar.xz
sha256sum htop-3.3.0.tar.xz

tar -xf htop-3.3.0.tar.xz
cd htop-3.3.0

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

Output:

checking for gcc... gcc
checking whether the C compiler works... yes
...
config.status: creating Makefile

  CC       htop.o
  CC       CommandLine.o
  CC       linux/LinuxProcess.o
  ...
  CCLD     htop

 /usr/bin/install -c htop '/usr/local/bin/htop'
 /usr/bin/install -c htop.1 '/usr/local/share/man/man1/htop.1'

On Debian/Ubuntu, libncursesw5-dev provides the ncurses headers htop’s configure looks for; on Fedora/RHEL the equivalent is sudo dnf install ncurses-devel after sudo dnf groupinstall "Development Tools". The sha256sum output should be compared against the checksum published on htop’s release page before you trust the tarball. Confirm the new binary is what you expect:

which htop
htop --version

Output:

/usr/local/bin/htop
htop 3.3.0

Example 2: Redis — a Project with No configure Step

Not every source build uses autotools. Redis ships a plain Makefile, so the workflow skips ./configure entirely:

cd /tmp
wget https://download.redis.io/redis-stable.tar.gz
tar -xzvf redis-stable.tar.gz
cd redis-stable
make -j"$(nproc)"
sudo make install
redis-server --version

Output:

    CC Makefile.dep
    CC adlist.o
    CC quicklist.o
    ...
    LINK redis-server

Hint: It's a good idea to run 'make test' ;)

redis-server v=7.4.0 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64

Redis’s Makefile installs straight to /usr/local/bin by default. Because there’s no configure probing your system, dependency problems here would only surface as compile errors from make itself — which is why reading a project’s README before building matters more than memorizing one universal command sequence.

Example 3: A Reusable Build Script

Wrapping the workflow in a script pays off if you rebuild the same tool often, and it’s a good demonstration of the quoting and error-handling habits this course’s Bash lessons cover:

#!/usr/bin/env bash
set -euo pipefail

readonly PKG_NAME="jq"
readonly PKG_VERSION="1.7.1"
readonly TARBALL="jq-${PKG_VERSION}.tar.gz"
readonly URL="https://github.com/jqlang/jq/releases/download/jq-${PKG_VERSION}/${TARBALL}"
readonly BUILD_DIR="/tmp/${PKG_NAME}-build"

echo "Downloading ${PKG_NAME} ${PKG_VERSION}..."
mkdir -p "$BUILD_DIR"
wget -O "${BUILD_DIR}/${TARBALL}" "$URL"

cd "$BUILD_DIR"
tar -xzf "$TARBALL"
cd "${PKG_NAME}-${PKG_VERSION}"

echo "Configuring and building..."
./configure --prefix=/usr/local
make -j"$(nproc)"

echo "Installing (requires sudo)..."
sudo make install

echo "Done. Installed version:"
"$PKG_NAME" --version

Output:

Downloading jq 1.7.1...
Configuring and building...
Installing (requires sudo)...
Done. Installed version:
jq-1.7.1

Every variable expansion is quoted ("$BUILD_DIR", "$TARBALL"), and set -euo pipefail makes the script stop immediately if wget fails or a variable is unset, instead of plowing ahead and trying to compile a tarball that was never downloaded.

How It Works Step by Step

Walking through what the htop build actually did:

  • 1. ./configure is itself a generated shell script. It runs a series of small test compiles to answer questions like “does a C compiler exist,” “is ncurses.h available,” and “does this system support the syscalls htop needs.” Based on the answers, it writes a Makefile from a Makefile.in template, substituting in your compiler path, install prefix, and enabled features.
  • 2. make reads that Makefile, which lists targets, the files each depends on, and the exact shell commands to build them. For every .c file it invokes gcc -c to produce a .o object file — but only if the source file or a header it includes is newer than the existing object file, which is why editing one file and rerunning make recompiles almost nothing.
  • 3. The linker (invoked via gcc) combines all the object files plus required libraries, such as -lncursesw, into the final htop executable.
  • 4. sudo make install runs the Makefile’s install target, which copies the built binary into <prefix>/bin and man pages into <prefix>/share/man. If the project also builds a shared library, this step (or a manual sudo ldconfig) refreshes the dynamic linker’s cache so ld.so can find the new .so file at runtime.
  • 5. None of this touched dpkg‘s database. As far as apt is concerned, htop was never installed — which is the core trade-off of building from source.

Common Mistakes

Mistake 1: Running the Whole Build as Root

It’s tempting to prefix every command with sudo to avoid permission errors:

sudo ./configure && sudo make && sudo make install

Only the final install step needs elevated privileges, since it’s the only one writing outside your home or temp directory. Running configure and make as root leaves root-owned files scattered in your source directory, which can cause confusing permission errors the next time you try to build there as your normal user.

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

Mistake 2: Skipping the Build Toolchain

Running configure on a fresh system without a compiler installed fails immediately:

checking for gcc... no
checking for cc... no
configure: error: no acceptable C compiler found in $PATH

Install the toolchain first — build-essential pulls in gcc, g++, and make on Debian/Ubuntu (the Fedora/RHEL equivalent is sudo dnf groupinstall "Development Tools"):

sudo apt install -y build-essential

Mistake 3: Trusting a Download Without Verifying It

Downloading a tarball over HTTPS doesn’t guarantee the file itself wasn’t tampered with on the server or a mirror. Skipping checksum verification means you’re compiling and running code you never actually confirmed matches what the project published — always compare sha256sum output (or a GPG signature, if offered) against the value on the project’s official release page before extracting the tarball.

Mistake 4: Forgetting ldconfig After Installing a Shared Library

If a source build installs a new shared library, the program that needs it can fail to start even though the file is right there on disk:

redis-server: error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory

The dynamic linker caches known library locations; a newly installed .so file isn’t found until that cache is refreshed:

sudo ldconfig

Best Practices

  • Check apt/dnf (and backports or a trusted PPA) before building from source — it’s less work and gives you automatic updates.
  • Always read the project’s README or INSTALL file first; not every project follows the configure/make/install pattern.
  • Install the matching -dev (Debian/Ubuntu) or -devel (Fedora/RHEL) header packages for any library configure complains about.
  • Verify the tarball’s checksum or GPG signature before extracting and building it.
  • Leave the install prefix at its default, /usr/local, so source installs never collide with files apt manages under /usr.
  • Use checkinstall instead of a bare make install when you want the result trackable and removable with dpkg -r:
    sudo apt install -y checkinstall
    sudo checkinstall --pkgname=jq-src --pkgversion=1.7.1
  • Keep the extracted source directory (or record the exact version) so you can run sudo make uninstall later if the project’s Makefile supports it:
    cd /tmp/htop-3.3.0
    sudo make uninstall
  • Use make -j"$(nproc)" to compile using all available CPU cores instead of one file at a time.
  • For experimental or short-lived builds, install to a dedicated prefix like /opt/myapp so removal is a single rm -rf.

Practice Exercises

  • Pick a small CLI tool that’s outdated or missing in your distro’s repos and build it from source using the ./configure && make && sudo make install flow. Confirm the result with which <tool> and <tool> --version, and check it landed under /usr/local.
  • On a fresh system or container without build-essential installed, run a project’s ./configure on purpose, read the exact error it prints, then install the missing toolchain and rerun it successfully.
  • Build the same tool a second time using checkinstall instead of a plain make install, then remove it cleanly with sudo dpkg -r <package-name> and confirm with which that it’s gone.

Summary

  • Installing from source means compiling a program’s source code yourself instead of downloading a pre-built package.
  • The classic autotools workflow is ./configure, make, then sudo make install — but not every project uses configure, so always check the README first.
  • Install build-essential (or Fedora/RHEL’s “Development Tools” group) and any project-specific -dev/-devel headers before building.
  • Verify a downloaded tarball’s checksum before building it, and leave the install prefix at /usr/local to avoid colliding with files apt manages.
  • A source install isn’t tracked by dpkg, so use checkinstall or keep the source directory around if you’ll need a clean uninstall later.
  • Because source installs bypass the package manager, security updates for that software become your responsibility.