Previously: LFS Part 4: Building the Cross-Toolchain — we built a cross-compiler that targets our new system.

The cross-toolchain works. We proved that in Part 4. Now we put it to work.

Chapter 6 of the LFS book is where the cross-compiler earns its keep. We're building 17 packages — the basic Unix utilities that'll live inside our chroot environment. These aren't the final versions. Think of them as scaffolding: good enough to build the real system in Chapter 8, then thrown away and rebuilt properly.

You're building a temporary workshop so you can build your real workshop.

Three Concepts Before We Start

What --host actually does

Every package in this chapter gets configured with --host=x86_64-lfs-linux-gnu. This tells the build system: "the machine that will run this software is different from the machine building it." In practice, it forces configure to use our cross-compiler instead of the host's gcc. Without it, we'd be building packages that depend on the host system's libraries — defeating the entire point.

Why "temporary"?

Every package we build here gets rebuilt from scratch in Chapter 8. These are disposable. Their only job is to provide enough tools inside chroot so we can compile the real versions without any dependency on the host. Think of it like building a ladder to reach the roof, then pulling the ladder up after you.

Why Pass 2?

In Part 4, we built Binutils and GCC "Pass 1" — but the host compiler built those. They work, but they carry the host's fingerprints. Pass 2 rebuilds them using our own cross-compiler. Same source code, but now every byte was produced by tools we control. That's how you break free from the host.

The Pattern

Every package follows the same ritual:

tar -xf package-x.y.z.tar.xz
cd package-x.y.z
./configure --prefix=/usr                   \
            --host=x86_64-lfs-linux-gnu     \
            --build=$(build-aux/config.guess)
make -j8
make DESTDIR=$LFS install

Extract. Configure. Make. Install. Next.

The 17 Packages — What They Are and Why They Matter

Text Processing & Macros

M4 — A macro processor. Takes template files with placeholders and expands them into real code. Autoconf depends on it, which means almost every ./configure script you've ever run was generated by M4 under the hood.

Sed — Stream editor. Transforms text in pipelines — find and replace, delete lines, insert text. Build scripts use it constantly to patch source code on the fly without opening a file.

Grep — Pattern matcher. Searches text for strings or regular expressions. Configure scripts call grep hundreds of times to check whether features exist, headers are present, or functions are available.

Gawk — GNU Awk. A pattern-scanning and text-processing language. Many build systems use awk scripts to parse output, generate code, or transform data during compilation.

Terminal & Shell

Ncurses — Terminal UI library. Every program that does anything fancy in a terminal — menus, colors, cursor movement, line editing — depends on ncurses. That includes bash itself. Without ncurses, your shell has no arrow keys, no history, no tab completion.

Bash — The shell. The command interpreter that runs every script and gives you your prompt. Without bash, there's no interactive system. Every #!/bin/bash script — and that's most of them in LFS — needs this.

File & Archive Tools

Coreutils — The 100+ basic commands: ls, cp, mv, cat, chmod, chown, mkdir, rm... the stuff you type every day. Without coreutils, a Unix system can't do anything. It's the most fundamental package after the kernel.

Diffutils — Compares files. diff, cmp, diff3. Build systems use diff to check outputs, verify results, and apply patches.

File — Identifies file types by examining their content, not their extension. The file command. Build scripts use it to detect binary formats and verify outputs.

Findutilsfind and xargs. Locates files by name, type, date, or any other attribute, and executes commands on them. Build systems use find extensively for cleanup, discovery, and batch operations.

Tar — Archive tool. Every source package in LFS is a tarball. Can't extract sources without it. Can't package anything without it. The T in .tar.gz.

Gzip — Compression. Most tarballs are gzipped (.tar.gz). The oldest and most universal compression format in the Unix world.

Xz — Better compression than gzip. Many LFS source packages use .tar.xz format because it produces significantly smaller archives.

Patch — Applies patches to source code. LFS applies several patches to fix bugs or adapt packages for our environment. Without patch, you'd be making those edits by hand.

Build System

Make — The build orchestrator. Reads Makefiles and runs compilation commands in the right order, tracking dependencies so it only rebuilds what changed. Every C/C++ project in LFS uses it.

Binutils Pass 2 — The assembler (as) and linker (ld), rebuilt with our cross-compiler instead of the host's. Pass 1 was built by the host system. Pass 2 is built by our tools — removing the host's fingerprints. Now the toolchain is truly ours.

GCC Pass 2 — The compiler itself, rebuilt. This produces a compiler that targets our new system and links against our glibc, not the host's. This is the compiler chroot will use. Everything from Chapter 8 depends on this binary being correct.

The Quick Wins

Most of these packages compile faster than you can read their README:

PackageTime
Make9s
Xz15s
Diffutils19s
Sed20s
File20s
Gzip21s
M423s
Patch24s
Grep25s
Tar28s
Bash32s
Findutils37s
Coreutils60s

13 packages in under 7 minutes. That's the kind of progress that keeps you going.

Ncurses — The Problem Child

Then there's ncurses. Took 3 attempts and about 12 minutes of debugging.

First attempt blew up with:

../include/ncurses_cfg.h:176:15: error: two or more data types in declaration specifiers
  176 | #define pid_t int
../include/ncurses_cfg.h:173:17: error: two or more data types in declaration specifiers
  173 | #define ssize_t int
/usr/include/x86_64-linux-gnu/bits/types/time_t.h:8:9: error: unknown type name '__time64_t'

Let's read this. The configure script decided pid_t should be int and ssize_t should be int. Why? Because configure was probing the host system's headers instead of the cross-compilation sysroot. It ran test programs against host headers, concluded these types don't exist (they do — just not where it was looking), and generated #define fallbacks. When those fallbacks collided with the host's actual type definitions during compilation — two definitions for the same type. Boom.

The diagnostic approach: When you see "two or more data types in declaration specifiers" in a cross-compile, the first thing to check is header contamination. Is the compiler picking up /usr/include (host) instead of the sysroot's headers? Check your --sysroot flag, your environment variables, and whether configure is using the right compiler for its test programs.

The fix: make sure the cross-compiler's sysroot is used exclusively. No host header contamination. Clean the build directory, reconfigure, rebuild. Third time worked.

Ncurses has always been like this. It's the package most likely to remind you that cross-compilation is a house of cards held together by environment variables.

M4 — Scary Errors, Working Binary

M4 threw these during the build:

make: *** [Makefile:2018: all-recursive] Error 1
make: *** [Makefile:2322: install] Error 2

Looks fatal. It's not. "Non-fatal" in this context means: the core binary compiled and installed successfully, but some ancillary target — tests, documentation, optional modules — failed. The all-recursive error means make was recursing into subdirectories and one of them returned non-zero. Since this is a cross-compile, test programs can't actually run on the build machine (they're compiled for the target), so test failures are expected.

How to verify: Check that $LFS/usr/bin/m4 exists. If you want to be thorough, run file $LFS/usr/bin/m4 and confirm it's an ELF binary for your target architecture. If the binary is there and correctly formatted, those errors didn't matter.

Gawk — The Silent Failure

Gawk's first attempt failed without drama. No screaming errors, just a quiet non-zero exit. A retry fixed it. About 2 minutes total including the do-over.

Silent failures are worse than loud ones. At least ncurses told you exactly what went wrong. When a build fails silently, check the last few lines of output, check config.log for clues, and try again before you start debugging — sometimes it's a transient issue with parallel make jobs stepping on each other.

Binutils Pass 2 & GCC Pass 2

These are the heavyweights, and the whole reason Pass 1 existed.

Binutils Pass 2 — 45 seconds. Clean build. We're rebuilding the assembler and linker using our own cross-compiler from Pass 1 instead of the host's compiler. Same source code, different builder. The result: tools that were produced entirely by our toolchain, with no dependency on the host.

GCC Pass 2 — ~7 minutes. The big one. This produces a compiler that runs on the build machine, produces code for the target system, and links against our glibc (not the host's). This is the compiler the chroot environment will use to build the final system. Everything from Chapter 8 onwards depends on this binary being correct. It's the most important build in this entire chapter.

The Scorecard

17 packages. 28 minutes. 3 hiccups.

PackageTimeStatus
M423s⚠️ Non-fatal errors
Ncurses~12min❌❌✅ (3 attempts)
Bash32s
Coreutils60s
Diffutils19s
File20s
Findutils37s
Gawk~2min⚠️ Retry needed
Grep25s
Gzip21s
Make9s
Patch24s
Sed20s
Tar28s
Xz15s
Binutils Pass 245s
GCC Pass 2~7min

14 out of 17 were clean, one-shot builds. The three that gave trouble — ncurses, M4, Gawk — are all known to be finicky with cross-compilation. If yours built clean on the first try, you got lucky. If they didn't, you're in good company.

What Did We Just Build?

You now have a minimal Unix environment: a shell (Bash), file tools (Coreutils, Findutils, File), text processing (Sed, Grep, Gawk, M4), a terminal library (Ncurses), compression and archives (Gzip, Xz, Tar), patching (Patch, Diffutils), and a complete build system (Make, Binutils, GCC).

That's enough to enter chroot and start building the real system. None of these are final — they'll all be rebuilt properly in Chapter 8 with optimizations, full test suites, and no cross-compilation hacks. But right now, they're the scaffolding that makes everything else possible.


Next up: LFS Part 6: Into the Chroot — we step inside the new system for the first time.


Compiled by AI. Proofread by caffeine.