Building from Source
Building from source is the original Linux distribution mechanism — before package managers, you got software as a tarball, configured it for your system, compiled it, and installed it. Every LFS chapter is this ritual repeated; every BLFS package, every git clone project, every ancient tarball in an archive.org listing runs the same liturgy. This chapter teaches the liturgy and the build systems that automate it — the universal grammar of "source → installed".
The liturgy — three sacred steps
tar -xf package-1.0.tar.xz
cd package-1.0
./configure --prefix=/usr # adapt the build to THIS system
make -j$(nproc) # compile
make check # optional testsuite
sudo make install # install
configure probes the system — compiler present? headers? libraries? — and generates the Makefiles. make builds per the Makefile. make install copies binaries, headers, libraries, docs into --prefix. Three steps; a hundred years of software follows them.
configure — the system's interrogation
./configure --help # every package's custom options
./configure --prefix=/usr \ # WHERE it installs — the big one
--sysconfdir=/etc \ # config location
--localstatedir=/var \ # state location
--disable-static \ # don't ship .a libraries
--enable-shared \
--with-foo --without-bar # feature toggles
--prefix decides where everything lands: /usr (system), /usr/local (local additions, the classic default), /opt/package (self-contained). The LFS prefix dance — /tools, /usr, DESTDIR — is this flag doing deep work.
make DESTDIR=$STAGING install — the packager's trick: install believing prefix is /usr but landing in $STAGING/usr — how packages get built for one root and assembled for another.
The other build systems
| System | Liturgy | Found in |
|---|---|---|
| autotools | ./configure && make && make install | The classic — most tarballs |
| cmake | cmake -B build -DCMAKE_INSTALL_PREFIX=/usr && cmake --build build | KDE, modern C++ |
| meson | meson setup build --prefix=/usr && ninja -C build | GNOME, systemd-era |
| plain make | make && make install | Minimal projects |
| cargo/go/npm | the language's own | Rust, Go, Node projects |
cmake's -B build creates an out-of-tree build dir — the modern way; source tree stays pristine. Meson+ninja is the fastest. Autotools remains the most common on old-school tarballs — and the deepest to understand: configure.ac → autoconf → configure → Makefile.in → make.
What configure actually detects
./configure ... 2>&1 | tee config.log # keep the log — failures live there
tail -20 config.log # when it fails, the answer's here
configure compiles test programs — "checking for gcc... yes" means it ran a hello-world; "checking for foo.h... no" means a test compile failed. The fix is almost always a missing -dev/-devel package: libfoo-dev supplies the headers a build needs that the runtime libfoo doesn't. Debian's -dev, Red Hat's -devel, Slackware ships headers always — the host difference.
Patches — the surgical fixes
patch -p1 < fix.patch # apply — -p1 strips the a/ b/ prefixes
patch -R -p1 < fix.patch # reverse — the undo
diff -u old.c new.c > my.patch # create one
diff -Naur old/ new/ > big.patch # whole trees
LFS's build applies dozens of small patches — FHS placements, security fixes, cross-build fixes — each a surgical diff that upstream hasn't folded in yet.
The configure internals — what it actually runs
./configure --help | less # every option the package ships
cat config.log # the probe transcript — failures live here
less config.status # the record of what was detected
cat config.h # the generated feature macros
configure is a generated shell script — thousands of small test-compiles: "checking for gcc... yes" means it ran a hello-world; "checking foo.h usability... no" means a test compile failed and the config.log shows the compiler's actual error. config.status --recheck reruns; config.status regenerates the Makefiles.
The DESTDIR/prefix grammar — the packager's two knobs
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
make
make install # writes to /usr, /etc — REAL
# vs:
make DESTDIR=/tmp/stage install # writes to /tmp/stage/usr — STAGED
--prefix = where the software believes it lives (baked into paths); DESTDIR = where it actually lands (the packager's staging). LFS uses both — --prefix=/usr + DESTDIR=$LFS = "believe /usr, land in $LFS/usr" — exactly where the chroot needs it. The --sysconfdir/--localstatedir pair move /etc and /var away from /usr/etc//usr/var (the classic wrong defaults).
The meson + cmake grammar
# meson — the modern fast one
meson setup build --prefix=/usr --buildtype=release -Dfeature=enabled
ninja -C build
ninja -C build test
DESTDIR=/tmp/stage ninja -C build install
meson configure build -Doption=value # reconfigure
# cmake — the heavyweight
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DBUILD_TESTING=OFF -DWITH_X=ON
cmake --build build -j$(nproc)
cmake --install build --prefix /tmp/stage/usr
ctest --test-dir build # the tests
Both honor DESTDIR; meson's -D toggles the package's own options (meson_options.txt lists them); cmake's -DWITH_*/-DBUILD_* the same.
The canonical builds — what the majors actually look like
# binutils/gcc/glibc — the toolchain trio (LFS chapter 3-5 grammar):
../configure --prefix=/usr --enable-shared --disable-werror
make -j$(nproc); make check || true; make install
# a typical autotools package:
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var \
--disable-static --docdir=/usr/share/doc/foo-1.2
make -j$(nproc); make check; make install
# a meson package:
meson setup --prefix=/usr --buildtype=release -Dopt=val build/
ninja -C build/; ninja -C build/ test; ninja -C build/ install
# a cmake package:
cmake -B build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTING=OFF
cmake --build build -j$(nproc); ctest --test-dir build; cmake --install build
# a kernel module's DKMS-style build:
make -C /lib/modules/$(uname -r)/build M=$PWD modules
make -C /lib/modules/$(uname -r)/build M=$PWD modules_install
# a python package:
pip install --prefix=/usr/local .
python -m build; pip install dist/*.whl
# a rust binary:
cargo build --release; install -Dm755 target/release/foo /usr/bin/foo
# a go binary:
go build -o foo ./cmd/foo; install -Dm755 foo /usr/bin/foo
Each ecosystem's liturgy is the same shape — configure the build dir, build, test, install — the verbs differ.
Dependencies — the real work
The tarball itself is the easy half; the dependencies are the job:
./configure # missing dep = the error is your answer
apt build-dep package # Debian's dep-fetcher — installs every build-dep
ldd binary | grep 'not found' # runtime gaps
pkg-config --modversion foo # is it visible to builds?
apt build-dep, dnf builddep, Slackware's "install everything" — each distro's answer to "install the deps this needs". On LFS, dependency management is the book's ordering — the build sequence is topological.
Version sanity and the release hygiene
make V=1 # verbose builds — the actual commands
./configure --version # sometimes tells you what's expected
cat README INSTALL NEWS # the three files always worth a skim
And the trust layer: gpg --verify package.tar.xz.sig package.tar.xz, md5sum/sha256 checks — the sources are the system's DNA; verify before compiling. This handbook's LFS chapters verify checksums before the first configure — chapter 2 makes it law.
The install aftermath
make install DESTDIR=$D # staged install — packager's form
make install # real install — needs root for /usr
ldconfig # refresh the .so registry after new libs
command -v newtool # did it land in PATH?
make uninstall # often supported — often doesn't exist
checkinstall/make install into /usr/local keeps hand-built software quarantined from the package manager's /usr — the classic hygiene.
Where source builds go wrong
| Symptom | Cause |
|---|---|
configure: foo.h not found | Missing -dev/devel package |
config.log cryptic failure | Read the last 20 lines — always |
Installed but command not found | --prefix landed outside PATH — /usr/local/bin? |
libtool version fights | The tarball's ltmain vs the host's — autoreconf -fi |
| Build works, install fails on perms | sudo make install — or DESTDIR staging |
| Old build residue breaks rebuild | make distclean — or re-extract fresh |
make fails only parallel | Racy Makefile — -j1 |
| .la files breaking downstream | rm the .la files — libtool's legacy hazard |
Exercises
- Build a real tarball —
tar,configure,make, DESTDIR-staged install into/tmp/stage. teea configure log; find the exact probe that failed for a missing lib.apt build-depordnf builddepsomething — see the dep list as a set.- Write and apply a patch —
diff -u,patch -p1, thenpatch -Rit back. - Compare a cmake and an autotools build side by side — the different liturgies.