LFS: Preparing the Build
Linux From Scratch begins before a single package compiles — with a host system, a dedicated partition, ninety source tarballs verified by checksum, and a build user wearing a sterilized environment. This chapter is the setup the LFS book assumes and newcomers most often fumble — plus the traps nobody warns you about: the musl-libc host that can't work, the bash-hash cache that poisons toolchains, the /tools symlink that is the build's escape hatch from its own host. Get this right and the build is stamina; get it wrong and nothing after it works.
The chicken-and-egg problem
You cannot compile an operating system without an operating system. The Linux you're building needs a host — an already-running Linux with a working compiler, core utilities, and disk space — to compile against, and against which the new system's early pieces link. The host isn't cheating; it's how all software births. Cross-compilation's whole art is the careful severance of host influence — chapter 13 (the cross-toolchain) is where that severance is engineered. This chapter supplies the host.
Step 1 — The host system
Three valid hosts, in ascending realism:
- A virtual machine — QEMU/KVM or VirtualBox, 2+ CPUs, 4 GB RAM, 30+ GB disk. The professional choice: snapshots let you rewind any mistake, and nothing you break is real.
- A dedicated partition or spare machine — the fullest experience; real GRUB, real hardware detection, real consequences.
- A live distribution on real hardware — possible but fragile; the live environment vanishes on reboot.
We use Slackware or Slint as the host throughout this handbook — its installer teaches partitioning by asking you to do it, its pkgtools leave the machinery visible, and it arrives with a complete development environment (gcc, make, binutils) already aboard. Any modern Linux works; the checks below tell you if yours qualifies.
The trap nobody warns about: your host's libc
The host must run glibc. LFS builds against GNU libc's headers and toolchain assumptions — an Alpine/musl or other non-glibc host will fail obscurely and early, in ways that look like compiler bugs but are actually libc mismatches. Check first:
ldd --version | head -1 # "ldd (GNU libc) 2.xx" = good; musl = wrong host
Docker images based on alpine, musl-based distros, and BusyBox-only environments are disqualified as hosts — fine as targets of curiosity, wrong as LFS mothers. WSL works for practicing tools but is a trap for the real build — no real partitions, no loop devices by default; the workaround (a loop-mounted disk image inside WSL) is documented below.
Verify the host qualifies
The LFS book carries a version_check.sh ritual — the minimum versions the build requires. On your host, prove each:
bash --version | head -1 # >= 3.2
binutils: ld --version | head -1 # >= 2.13.1
bison --version | head -1
bzip2 --version
coreutils: chown --version | head -1 # >= 6.9
diffutils: diff --version | head -1
findutils: find --version | head -1 # >= 4.2.31
gawk --version | head -1 # >= 4.0.1 — gawk specifically, NOT mawk
gcc --version | head -1 # >= 5.2
grep --version | head -1
gzip --version
uname -r # kernel >= 5.4
m4 --version | head -1
make --version | head -1 # >= 4.0
patch --version | head -1
perl -V:version # >= 5.8.8
python3 --version # >= 3.4
sed --version | head -1
tar --version | head -1 # >= 1.22
texinfo: makeinfo --version | head -1 # the one everyone forgets
xz --version
Every check must pass — the build's earliest packages lean on all of them. Slackware ships a clean pass out of the box; on Debian-family hosts apt install build-essential gawk bison texinfo closes the usual gaps. Two subtleties: gawk is required, not mawk (Debian defaults to mawk — install gawk explicitly), and texinfo's makeinfo is the classic missing package — several builds fail silently without it, weeks of confusion for a one-package fix.
Step 2 — The partition
The new system gets its own home — a dedicated partition it will fill. Before carving, one architectural decision: BIOS/MBR or UEFI/GPT. UEFI hosts want GPT (gdisk) and eventually an EFI System Partition; BIOS-era hosts take MBR (fdisk) and a simpler boot. For a VM you choose when creating the machine — this handbook covers both, with the GPT path primary.
# as root — find the target disk
fdisk -l
lsblk
# carve it — gdisk on GPT, fdisk on MBR
gdisk /dev/sda # new partition ~30G, type 8300 (Linux filesystem)
# UEFI builders add a small ESP: ~300M, type EF00, FAT32
# format the LFS partition
mkfs.ext4 /dev/sda3
# and the ESP if UEFI: mkfs.vfat /dev/sda1
# the staging ground
mkdir -pv /mnt/lfs
mount /dev/sda3 /mnt/lfs
Why ext4 and not something exotic: the build writes hundreds of thousands of small files — ext4's journaling and tooling (e2fsprogs, which you build inside LFS) are the boring, correct choice. Btrfs/XFS work but add variables nobody needs on a first build.
A swap partition helps if your host is lean — an existing swap can be shared: swapon /dev/sda2. On a VM, a second virtual disk is cleanest — zero risk to the host's own disk.
Make it persistent — the build reboots:
echo '/dev/sda3 /mnt/lfs ext4 defaults 0 0' >> /etc/fstab
The no-partition path: a loop-mounted image file
No spare partition? Build inside a disk image file — the WSL workaround and the safest experiment:
dd if=/dev/zero of=lfs.img bs=1G count=32 status=progress # 32 GB sparse-able image
mkfs.ext4 lfs.img
mkdir -pv /mnt/lfs
mount -o loop lfs.img /mnt/lfs
The whole LFS system lives in one file — backup it with cp, snapshot with cp --reflink on CoW filesystems, delete when done. Slightly slower than a real partition; infinitely safer.
The live-USB host — the trap of the ephemeral environment
Building LFS from a live USB (Slackware live, a rescue distro, any bootable image) is legal and occasionally the only option — but every live environment is ephemeral: reboot and your $LFS mount, your fstab edit, your lfs user, your dotfiles are all gone while the partition's contents survive. The fix is discipline: keep $LFS on the persistent partition, script the mount + fstab + useradd as one repeatable setup-host.sh, and re-run it after every boot. Treat the live environment as disposable scaffolding around a permanent partition — never the other way round.
Step 3 — $LFS and the sources tree
Everything hangs off one environment variable, $LFS — the mountpoint of the partition:
export LFS=/mnt/lfs
mkdir -pv $LFS/sources
chmod -v a+wt $LFS/sources # world-writable + sticky, /tmp-style
Download the book's package manifest — wget-list (the ~90 tarballs) and md5sums (their checksums) — into $LFS/sources, then fetch:
cd $LFS/sources
wget --input-file=wget-list --continue --directory-prefix=$LFS/sources
# verify every tarball against its checksum BEFORE building
pushd $LFS/sources && md5sum -c md5sums && popd
Check every checksum before building. A corrupt tarball discovered three chapters in is a weekend lost; md5sum -c catches it now. Where upstream ships GPG signatures, gpg --verify them too — the sources become the system's DNA. --continue resumes partial downloads — the ~90-package fetch survives dropped connections. For bulk downloads aria2c -i wget-list parallelizes across mirrors; wget is the book's way and everywhere.
The offline angle nobody plans for
Download all tarballs before starting, and keep $LFS/sources — mid-build network failures are common, and an LFS build with a dead network stalls forever waiting on a tarball. rsync -a $LFS/sources/ backups/lfs-sources/ once at the start is a two-minute insurance policy.
Architecture honesty
This handbook builds x86_64 (LFS_TGT=x86_64-lfs-linux-gnu). LFS supports several arches (and CLFS — Cross LFS — targets them all), but a first build has no business cross-arching: the cross-toolchain already asks gcc to build for a foreign target; changing the machine class on top doubles the failure space. Build native, on x86_64, for x86_64. 32-bit builds exist in the book's lineage but are a museum path — modern LFS assumes 64-bit, and a 32-bit host cannot produce a 64-bit kernel without serious gymnastics. Check before you start:
uname -m # must say x86_64 — the build assumes it everywhere
Step 4 — The toolchain staging area
mkdir -pv $LFS/tools # where the cross-toolchain will live
ln -sv $LFS/tools / # /tools symlink — the trick the whole build pivots on
That symlink is subtle and load-bearing — worth understanding before you make it. The cross-toolchain's early binaries are compiled with paths baked in as /tools/.... On the host, /tools resolves to $LFS/tools through this symlink. Later, inside the chroot — where $LFS is the root — /tools resolves to the same directory, same absolute path, no rewriting. It's how the build escapes its host's filesystem while still being built by it — a one-line filesystem judo move that makes the entire chroot transition seamless.
Step 5 — The lfs user
Build as a dedicated user, never root — one mis-typed make install as root overwrites your host's /usr and the host dies mid-build. This has happened to everyone who skipped the warning.
groupadd lfs
useradd -s /bin/bash -g lfs -m -k /dev/null lfs
passwd lfs
# hand the staging ground to lfs
chown -v lfs $LFS/{usr{,/*},lib,var,etc,bin,sbin,tools}
chown -v lfs $LFS/sources
Two details nobody thinks of: -s /bin/bash must be explicit — on Debian-family hosts new users default to dash, and dash is not bash (the builds require bash); -k /dev/null gives lfs an empty skeleton directory so no host dotfiles leak into its environment. And the UID you create now will own files inside the finished system until the book's cleanup stages normalize ownership — a small oddity you'll see in ls -l output later.
The sterile environment
Login as su - lfs (the - matters — a full login shell) and write the two files that make every build correct:
cat > ~/.bash_profile << "EOF"
exec env -i HOME=$HOME TERM=$TERM PS1='\u:\w\$ ' /bin/bash
EOF
env -i gives lfs a sterile environment — every login respawns bash with only HOME, TERM, and PS1: no inherited host variables, no leaked LD_LIBRARY_PATH, no distro-local CFLAGS quietly poisoning cross-compilation. TERM is kept so your terminal still renders correctly.
cat > ~/.bashrc << "EOF"
set +h # no hashing — always re-find binaries fresh
umask 022 # sane default permissions
LFS=/mnt/lfs
LC_ALL=POSIX # predictable locale for reproducible builds
LFS_TGT=x86_64-lfs-linux-gnu # the target triplet — our system's name
PATH=$LFS/tools/bin:$PATH # our toolchain first, host's second
MAKEFLAGS='-j$(nproc)' # parallel builds across all cores
export LFS LC_ALL LFS_TGT PATH MAKEFLAGS
EOF
source ~/.bash_profile
Each line earns its place, and three of them defend against specific ghosts:
set +h— bash caches binary locations in a hash table (hashshows it). Mid-build, your fresh$LFS/tools/bin/gccreplaces the host's gcc — but a hashed bash would keep running the old one.set +hforces PATH re-resolution every call. This single line prevents a class of "my new toolchain isn't being used" bugs that look like curses.LC_ALL=POSIX— builds that sort, grep, and awk differently under different locales produce different systems. POSIX locale makes every build byte-reproducible — the difference between a toolchain that works and one whose configure probes disagree with themselves.LFS_TGT— the GNU target triplet (x86_64-lfs-linux-gnu) naming your new system's architecture-vendor-OS — every--target=$LFS_TGTin the cross-toolchain builds for it; it's howgcclearns it is building a foreign system's compiler.MAKEFLAGS='-j$(nproc)'— parallel builds across every core. Hours become minutes — with one honest caveat: a handful of old packages have racy Makefiles; if a build fails weirdly with-j, retry serial before debugging anything else.
Step 6 — The final checks
su - lfs
echo $LFS $LFS_TGT # must print /mnt/lfs and the triplet
which gcc # host's gcc — until tools/ exists
mount | grep lfs # partition still mounted
df -h $LFS # space confirmed
id # you are lfs — NOT root
Then, and only then, the build begins.
The discipline nobody teaches: log everything
The single most useful LFS habit isn't in the book — record every command and its output. Builds fail silently, and the forensic trail is everything:
# as lfs — capture everything, forever
exec > >(tee -a $LFS/build.log) 2>&1
or simply run each package build inside script -a $LFS/logs/<pkg>.log. When a configure probe fails three packages deep, the log tells you which host assumption poisoned it. Disk space for logs is trivial; the information is irreplaceable. Keep a plain-text journal of which packages installed and in what order — when the finished system misbehaves, that journal is the difference between debugging and guessing.
Disk-space planning
30 GB is the floor; 40 GB is comfortable:
| What | Space |
|---|---|
$LFS/sources (~90 tarballs) | ~500 MB |
| Extracted + build trees | ~4–6 GB |
/tools cross-toolchain | ~1 GB |
| The finished system | ~3–4 GB |
| Slack (logs, temp, rebuilds) | the rest |
Running out of space mid-build is the second-most-common silent killer after wrong-user builds — df -h $LFS between major packages.
Eyes-free LFS
Every step in this chapter works in a virtual console with no GUI — the book's entire toolchain is text. Speakup users can build LFS from a talking console exactly as sighted users do from xterm; makeinfo-free documentation paths and man/info pages are all readable. The accessibility chapter of the finished system (BRLTTY, Speakup inside the kernel you build) is its own handbook page — but the build itself needs nothing graphical. See Speakup.
The source manifest — what ~90 tarballs means
The wget-list is the build's bill of materials — ~90 packages at pinned versions, each with a checksum. Reading the list once teaches what a Linux actually is:
the toolchain: binutils gcc glibc gmp mpfr mpc isl zstd
the build machinery: m4 bison flex autoconf automake libtool pkgconf ninja meson
the userland: bash coreutils diffutils findutils gawk grep sed tar gzip bzip2 xz
procps-ng psmisc util-linux e2fsprogs file less iproute2 kbd kmod
shadow attr acl libcap vim groff man-db man-pages inetutils
the libraries: ncurses readline zlib libffi expat libxml2 libxcrypt openssl
elfutils libtasn1 p11-kit libpipeline libseccomp gettext texinfo
scripting: perl python wheel setuptools markupsafe jinja2
system plumbing: eudev sysklogd sysvinit iana-etc linux (the kernel itself)
misc: check gdbm libtool make patch sudo-adjacent bits
Every tarball's version is pinned in the book's edition — mixing versions is the classic way to break the build: a newer gcc's stricter parsing chokes on older packages' code, an older binutils can't assemble a newer gcc's output. The book's manifest is a tested combination — treat it as such, or understand why each deviation is safe.
The lfs environment — the complete anatomy
# ~/.bash_profile — sterile login
exec env -i HOME=$HOME TERM=$TERM PS1='\u:\w\$ ' /bin/bash
# ~/.bashrc — the build environment
set +h
umask 022
LFS=/mnt/lfs
LC_ALL=POSIX
LFS_TGT=x86_64-lfs-linux-gnu
PATH=$LFS/tools/bin:$PATH
MAKEFLAGS='-j$(nproc)'
export LFS LC_ALL LFS_TGT PATH MAKEFLAGS
The env-var table — every variable's job:
| Variable | Job |
|---|---|
LFS | The mountpoint — every path the build writes roots here |
LFS_TGT | The target triplet — --target/--host for every cross-build |
LC_ALL=POSIX | Locale-independent builds — reproducible sorting/collation |
MAKEFLAGS=-j$(nproc) | Parallel builds — the hours-saver |
PATH=$LFS/tools/bin:... | The new toolchain found first |
set +h | No command-path hashing — always re-resolve |
umask 022 | Files 644/dirs 755 — sane defaults |
env -i | Sterile env — no host contamination |
The version-check roster — the complete minimums
The LFS book's host requirements — every tool, minimum version:
bash-3.2+ binutils-2.13.1+ bison-2.7+ bzip2-1.0.4+ coreutils-6.9+
diffutils-2.8.1+ findutils-4.2.31+ gawk-4.0.1+ gcc-5.4+ glibc-2.36+
grep-2.5.1a+ gzip-1.3.12+ linux-5.4+ m4-1.4.10+ make-4.0+
patch-2.5.4+ perl-5.8.8+ python-3.4+ sed-4.1.5+ tar-1.22+
texinfo-5.0+ xz-5.0.0+
The version_check.sh convention — a script that runs the roster — ships with the book; write it yourself from the table, run it before step 1 ends.
The disk layouts — the three valid schemes
Simple GPT/UEFI: BIOS/MBR classic: The VM-friendly:
sda1 ESP 300M vfat EF00 sda1 swap 4G 8200 vda 1 partition
sda2 swap 4G swap 8200 sda2 / 36G 8300 (no swap needed —
sda3 / 36G ext4 8300 the VM has host swap)
The ESP sizing rule: ~300MB FAT32, type EF00, mounted /boot/efi — not the root partition. BIOS/MBR needs no ESP — the bootloader lives in the boot sector. VM builds can skip swap entirely.
What an SBU is
The book measures build costs in SBUs — Standard Build Units: the time the first package (binutils pass 1) takes on your machine, used as the unit for every other package's estimate. "This package takes 5 SBU" means five times your binutils-1 build. On a modern multi-core host an SBU is a few minutes; measure yours once and every estimate in the book becomes concrete.
The host's hardware — the inventory the build needs
lscpu # cores, threads, arch, flags (avx etc)
lsblk # disks
free -h # RAM + swap
df -h # filesystems
lsusb; lspci # peripherals
uname -a # kernel + arch
cat /etc/os-release # which distro
The sizing rules: RAM — 4GB workable, 8GB comfortable, glibc/gcc want parallel-headroom; disk — 30GB minimum, 40GB comfortable; CPU — more cores = lower wall-time, glibc+gcc are the long poles; swap — needed if RAM <4GB or -j is aggressive (MAKEFLAGS=-j$(nproc) with 2GB RAM swaps itself to death — halve the -j or add swap).
The QEMU recipe — the complete host
# create the disk
qemu-img create -f qcow2 lfs.qcow2 40G
# boot the installer
qemu-system-x86_64 -m 4096 -smp 4 -enable-kvm \
-drive file=lfs.qcow2,if=virtio \
-cdrom slint64-15.0.iso -boot d \
-net nic -net user -display gtk
# snapshot before risky phases
qemu-img snapshot -c before-cross lfs.qcow2
qemu-img snapshot -l lfs.qcow2 # list
qemu-img snapshot -a before-cross lfs.qcow2 # revert
# UEFI variant: -bios /usr/share/edk2/ovmf/OVMF_CODE.fd
# serial console: -nographic -serial mon:stdio
The VM's virtues for LFS specifically: snapshots between build phases (cross-toolchain → temp-system → chroot → system packages → kernel), clones for parallel experiments, no hardware to break. virtio devices are the paravirtual drivers Linux supports natively — fast disk + net.
The download discipline — the whole 90-tarball ritual
cd $LFS/sources
# wget-list.txt + md5sums ship with the book's release
wget --input-file=wget-list.txt --continue --directory-prefix=$LFS/sources
# or parallel: aria2c -i wget-list.txt -d $LFS/sources -x4 -s4
pushd $LFS/sources && md5sum -c md5sums && popd
# where upstream signs: gpg --keyserver hkps://keys.openpgp.org --recv-keys <id>
# then: gpg --verify package.tar.xz.sig package.tar.xz
--continue resumes partial downloads; md5sum -c is the pre-build gate; GPG is the trust layer where upstream ships .sig files. Keep $LFS/sources after the build — it's the offline-rebuild insurance, and the manifest records exactly which versions produced the system.
The two books — choose now
LFS ships two editions: SysVinit and systemd. The choice is your new system's PID-1 — made now because later chapters fork on it. For this handbook we follow SysVinit — matching Slackware/Slint's BSD-style rc scripts, and because rc.M is a file you can read while a systemd unit graph is not — with systemd's parallel path flagged wherever the books diverge. Either teaches the same machine.
Where builds go wrong here
| Symptom | Almost always |
|---|---|
make install wrote to /usr on the host | You were root — always build as lfs |
| Tools not found after install | set +h missing — bash cached old paths |
| Weird locale-dependent failures | LC_ALL unset — export POSIX |
| Host leaked into a build | .bash_profile lacking env -i — sterile env required |
/tools empty inside chroot | The /tools symlink wasn't made on the host side |
| Build can't write $LFS | Partition unmounted after reboot — fstab entry |
| Tarball fails to extract | Checksum it before — corrupt download |
| Configure can't find libc | Host is musl-based — wrong host entirely |
| lfs's shell behaves oddly | Defaulted to dash — -s /bin/bash required |
A package fails only with -j | Racy Makefile — retry make serial |
makeinfo missing mid-build | texinfo — the forgotten requirement |
The honest time budget
| Phase | Wall time (modern host, -j$(nproc)) |
|---|---|
| This chapter — host, partition, sources, env | an evening |
| Cross-toolchain (ch. next) | 1–3 hours |
| Temporary system + chroot | 3–6 hours |
| Full system packages | 4–10 hours |
| Kernel + boot + first boot | 1–2 hours |
| Whole build | a weekend to a week |
The book says LFS is not a race — and it isn't. Slow is correct; every configure probe read properly is a bug you'll never meet.
Exercises
- Run the full version-check roster on your host; note every version that would disqualify it — and check
ldd --versionfor the glibc trap. - Partition a scratch VM disk by hand —
gdisk,mkfs.ext4, mount,fstab, reboot, verify it returns. - Build the loop-image variant (
lfs.img) alongside the partition — understand both paths. - Download five real upstream tarballs into
$LFS/sources;md5sum -cthem; then corrupt one deliberately and watch the check catch it. - As
lfs, deliberately break$PATH— then explain whyset +hexists. - Time a trivial
./configure && makeand compute your SBU intuition.
Timeline
| Step | What you did |
|---|---|
| Host | Verified a glibc-based Linux with dev tools |
| Partition | Carved, formatted, mounted /mnt/lfs, persisted in fstab |
| Sources | ~90 tarballs, checksum-verified, in $LFS/sources |
| Staging | $LFS/tools + the /tools symlink |
| User | lfs with sterile env, LFS_TGT, MAKEFLAGS, set +h |
| Next | LFS I — the cross-toolchain: binutils → gcc → glibc |
References
- linuxfromscratch.org — LFS book (SysV & systemd editions), chapters 2–4: host requirements, preparing a partition, packages & patches, final preparations
- The
version_check.shconvention — the book's own host-qualification script - wget-list / md5sums manifests shipped with each LFS release