Fathom-OS Build Series: Chapter 5 - Compiling a Cross-Toolchain

Part of the Fathom-OS Project Log.


Chapter 5 builds the cross-toolchain that everything else gets compiled with. This is five packages built in a strict order: Binutils Pass 1, GCC Pass 1, Linux API Headers, Glibc, and Libstdc++. Each one depends on the previous being built correctly. Because the packages are sequential steps building one thing rather than standalone milestones, the whole chapter is covered in a single post.

References:


Before You Start: The Pre-Flight Check

This trips people up, so it goes first. Every package in Chapters 5 and 6 must be built as the lfs user with the correct environment loaded. If you SSH into the build host the way I do, you land as your normal user, not lfs. The environment variables will be blank or wrong until you switch.

Switch to the lfs user with a login shell:

su - lfs

The - is required. It loads .bash_profile which sets up the clean build environment. Then confirm all four values before doing anything:

echo $LFS && echo $LFS_TGT && echo $MAKEFLAGS && umask

You want to see:

/lfs
x86_64-lfs-linux-gnu
-j10
0022

If any of those are blank, stop and fix the environment before building. The book stresses checking $LFS one last time in the General Compilation Instructions for exactly this reason. Run this check at the start of every session.


The Universal Build Workflow

The General Compilation Instructions define a workflow that applies to every package in this chapter:

  1. Change to $LFS/sources
  2. Extract the tarball with tar, never cp -R which destroys timestamps
  3. Change into the extracted directory
  4. Build and install per the package instructions
  5. Change back to $LFS/sources
  6. Delete the extracted source directory

Each package page in the book starts at step 4, assuming you already did the extract and enter from the general instructions. That is why the per-package commands begin at mkdir -v build rather than the tar extraction.


5.2 Binutils Pass 1

Binutils is built first because both Glibc and GCC test the linker and assembler to decide which of their own features to enable. The book has you wrap this one package in a time command to establish the 1 SBU baseline that every other build time in the book is measured against.

cd $LFS/sources
tar -xf binutils-2.46.0.tar.xz
cd binutils-2.46.0
mkdir -v build
cd build

Configure, compile, and install, timed:

time { ../configure --prefix=$LFS/tools --with-sysroot=$LFS --target=$LFS_TGT --disable-nls --enable-gprofng=no --disable-werror --enable-new-dtags --enable-default-hash-style=gnu && make && make install; }

On this build host the baseline came in at 44 seconds real time. That is 1 SBU for the rest of the build.

Clean up:

cd $LFS/sources
rm -rf binutils-2.46.0

5.3 GCC Pass 1

GCC needs GMP, MPFR, and MPC. The book has you unpack these three into the GCC source tree and rename them so the GCC build picks them up automatically.

cd $LFS/sources
tar -xf gcc-15.2.0.tar.xz
cd gcc-15.2.0
tar -xf ../mpfr-4.2.2.tar.xz
mv -v mpfr-4.2.2 mpfr
tar -xf ../gmp-6.3.0.tar.xz
mv -v gmp-6.3.0 gmp
tar -xf ../mpc-1.3.1.tar.gz
mv -v mpc-1.3.1 mpc

On x86_64, set the default 64-bit library directory name to lib:

case $(uname -m) in
  x86_64)
    sed -e '/m64=/s/lib64/lib/' \
        -i.orig gcc/config/i386/t-linux64
  ;;
esac

Build directory, configure, compile, install:

mkdir -v build
cd build
../configure \
  --target=$LFS_TGT \
  --prefix=$LFS/tools \
  --with-glibc-version=2.43 \
  --with-sysroot=$LFS \
  --with-newlib \
  --without-headers \
  --enable-default-pie \
  --enable-default-ssp \
  --disable-nls \
  --disable-shared \
  --disable-multilib \
  --disable-threads \
  --disable-libatomic \
  --disable-libgomp \
  --disable-libquadmath \
  --disable-libssp \
  --disable-libvtv \
  --disable-libstdcxx \
  --enable-languages=c,c++
make
make install

There is a required post-install step that is easy to miss. GCC installs a partial limits.h header. The full version is needed later, so the book has you build it now:

cd ..
cat gcc/limitx.h gcc/glimits.h gcc/limity.h > \
  `dirname $($LFS_TGT-gcc -print-libgcc-file-name)`/include/limits.h

Confirm it was created:

ls -l `dirname $($LFS_TGT-gcc -print-libgcc-file-name)`/include/limits.h

Clean up:

cd $LFS/sources
rm -rf gcc-15.2.0

5.4 Linux API Headers

This exposes the kernel’s API for Glibc to use. There is no configure or build directory here, just header extraction. Quick at less than 0.1 SBU.

cd $LFS/sources
tar -xf linux-6.18.10.tar.xz
cd linux-6.18.10
make mrproper
make headers
find usr/include -type f ! -name '*.h' -delete
cp -rv usr/include $LFS/usr

Confirm the headers landed:

ls $LFS/usr/include

The book lists asm, asm-generic, drm, linux, misc, mtd, rdma, scsi, sound, video, and xen. Kernel 6.18.10 also installs a few newer directories (cxl, fwctl, regulator). That is expected with a newer kernel, not a problem.

Clean up:

cd $LFS/sources
rm -rf linux-6.18.10

5.5 Glibc

This is the main C library and the most involved package in the chapter. It also has a critical set of sanity checks at the end that confirm the whole toolchain is wired together correctly.

Extract and create the loader symlinks for x86_64:

cd $LFS/sources
tar -xf glibc-2.43.tar.xz
cd glibc-2.43
case $(uname -m) in
  i?86) ln -sfv ld-linux.so.2 $LFS/lib/ld-lsb.so.3
  ;;
  x86_64) ln -sfv ../lib/ld-linux-x86-64.so.2 $LFS/lib64
  ln -sfv ../lib/ld-linux-x86-64.so.2 $LFS/lib64/ld-lsb-x86-64.so.3
  ;;
esac

The book notes this ln command is correct even though the syntax looks unusual. Apply the FHS patch:

patch -Np1 -i ../glibc-fhs-1.patch

Build directory, configparms, configure:

mkdir -v build
cd build
echo "rootsbindir=/usr/sbin" > configparms
../configure \
  --prefix=/usr \
  --host=$LFS_TGT \
  --build=$(../scripts/config.guess) \
  --disable-nscd \
  libc_cv_slibdir=/usr/lib \
  --enable-kernel=5.4

A warning about msgfmt being missing may appear during configure. The book says this is harmless.

Compile:

make

The Install Safety Check

Before installing, the book includes a Warning worth taking seriously. The install uses DESTDIR=$LFS to contain itself to the LFS partition. If you were root with an empty $LFS, the command would expand to install glibc straight into your host system and break it. Confirm you are safe first:

whoami
echo $LFS

whoami must return lfs and echo $LFS must return /lfs. Only then install:

make DESTDIR=$LFS install

Fix a hardcoded path in the ldd script:

sed '/RTLDLIST=/s@/usr@@g' -i $LFS/usr/bin/ldd

Toolchain Sanity Checks

This is the most important verification in Chapter 5. Six checks confirm the cross-compiler, linker, and libc all work together. Generate the test log:

echo 'int main(){}' | $LFS_TGT-gcc -x c - -v -Wl,--verbose &> dummy.log

Check 1, the program interpreter. The path must not contain /lfs:

readelf -l a.out | grep ': /lib'

Expected: [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

Check 2, the start files:

grep -E -o "$LFS/lib.*/S?crt[1in].*succeeded" dummy.log

Expected: three lines for Scrt1.o, crti.o, crtn.o ending in succeeded.

Check 3, the header search directories:

grep -B3 "^ $LFS/usr/include" dummy.log

Expected: the search block ending in /lfs/usr/include.

Check 4, the linker search paths. Every path must begin with =:

grep 'SEARCH.*/usr/lib' dummy.log |sed 's|; |\n|g'

Check 5, the correct libc:

grep "/lib.*/libc.so.6 " dummy.log

Expected: attempt to open /lfs/usr/lib/libc.so.6 succeeded

Check 6, the dynamic linker:

grep found dummy.log

Expected: found ld-linux-x86-64.so.2 at /lfs/usr/lib/ld-linux-x86-64.so.2

The book’s examples show /mnt/lfs because that is its example mount point. This build uses /lfs, so the paths show /lfs instead. That difference is expected and correct.

Clean up test files and the source:

rm -v a.out dummy.log
cd $LFS/sources
rm -rf glibc-2.43

5.6 Libstdc++

The last package in the chapter. Libstdc++ is part of the GCC sources, so the GCC tarball is unpacked again. Its installation was deferred from GCC Pass 1 because it depends on Glibc, which is now in place.

cd $LFS/sources
tar -xf gcc-15.2.0.tar.xz
cd gcc-15.2.0
mkdir -v build
cd build

Configure points at ../libstdc++-v3/configure, not the top-level configure:

../libstdc++-v3/configure \
  --host=$LFS_TGT \
  --build=$(../config.guess) \
  --prefix=/usr \
  --disable-multilib \
  --disable-nls \
  --disable-libstdcxx-pch \
  --with-gxx-include-dir=/tools/$LFS_TGT/include/c++/15.2.0
make
make DESTDIR=$LFS install

Remove the libtool archive files, which are harmful for cross-compilation:

rm -v $LFS/usr/lib/lib{stdc++{,exp,fs},supc++}.la

Clean up:

cd $LFS/sources
rm -rf gcc-15.2.0

Snapshot

With the cross-toolchain complete this is a good point to take a VM snapshot before moving on. A clean rollback point here means that if anything goes wrong in Chapter 6, the toolchain does not have to be rebuilt from scratch.


What Is Next

The cross-toolchain is built and verified. Chapter 6 uses it to cross-compile the temporary tools needed before entering the chroot environment. The book notes that if any package in Chapter 6, especially Binutils Pass 2 or GCC Pass 2, fails to build, it points to a problem in the Binutils, GCC, or Glibc work just completed. The six sanity checks passing is a strong sign that will not happen.


More updates to follow as each build phase completes.

Previous: Chapter 4: Final Preparations | Next: Chapter 6: Cross Compiling Temporary Tools


All session notes and build logs are committed to the private repository at github.com/QuietWireDev/fathom-os.