Linux From Scratch (LFS)¶
What is LFS?¶
Linux From Scratch (LFS) is a project that provides instructions for building your own custom Linux operating system directly from source code.
It is designed for Linux users who want to gain a deeper understanding of the inner workings of the operating system.
These notes will document building my own Linux operating system by using LFS as a base. The OS will use common enterprise technologies (e.g., systemd) rather than more obscure tools. I made this choice to gain a deeper understanding of the OS components used in enterprise environments.
Starting Out¶
LFS follows Linux standards as closely as possible.
The standards followed:
- POSIX.1-2008
- The POSIX specification.
- Filesystem Hierarchy Standard (FHS) Version 3.0
- The filesystem structure.
- Linux Standard Base (LSB) Version 5.0 (2015)
- The LSB consists of several separate specifications:
- Core
- Desktop
- Runtime Languages
- Imaging
- The LSB consists of several separate specifications:
The core packages that are included in a system built to the LFS standard are listed here along with explanations on why they were included.
Chapter Overview¶
For chapters 1-4:
-
Anything done as the root user after Section 2.4 must have the
LFSenvironment variable set FOR THE ROOT USER. -
The
LFSvariable must be set at all times, and theumaskneeds to be set to0022.
For chapters 5-6:
-
The partition should be at
/mnt/lfsand always be mounted. -
Chapters 5 and 6 must be done as the
lfsuser system account.- Create the
lfsuser and group. - Use
su - lfsfor each task in these chapters.
- Create the
For chapters 7-10:
/mnt/lfsshould be mounted.
Setting up the Build Environment¶
The LFS project is built within a pre-existing Linux system.
A disk partition is made for the LFS filesyste and that's where the new OS is
going to be built.
The list of dependencies for builting an LFS system can be found here.
They provide a shell script on that page that checks for the dependencies on the host system and ensures they're on the correct versions.
My host system used for building is Ubuntu Server 22.04.3, using the default LVM installation.
Adding a New Virtual Disk¶
Step one is to create a partition on which to build the LFS system.
With an LVM installation on a VM, there are two options.
- Add a second virtual disk.
- Shrink and create space for another partition in LVM.
I'm going to opt for adding a second virtual disk to the VM in Proxmox.
From the Proxmox Web UI, go to the VM, select the Hardware tab, and add a new hard disk. Choose your storage pool (default is fine if there are no others set up).
My current disk for build is listed as /dev/sdb in the system.
This will be the dedicated LFS disk referred to for the rest of the build in
this document.
Paritioning The Disk¶
- Book Source: Chapter 2.4
There are several partitions required for a Linux system:
- The root partition
/ - The swap partition
- The EFI boot parition
- The GRUB BIOS partition (optional but needed for BIOS/Legacy boot and Secure Boot)
The disk partitioning can be automated via sfdisk.
Define sizes for the partitions needed via an sfdisk script:
# Partition table type
label: gpt
unit: sectors
first-lba: 2048
# BIOS boot partition
# This `type` specifies a BIOS Boot Partition
size=1M, type=21686148-6449-6E6D-744E-6574626F6F74, name="BIOS_Boot"
# 1. /boot (EFI System Partition) - 1GB
# type=U is the shortcut for EFI System
size=1G, type=U, name="EFI_System"
# 2. Swap - 4GB
# type=S is the shortcut for Linux Swap
size=4G, type=S, name="Linux_Swap"
# 3. / (Root) - Remainder of disk
# type=L is the shortcut for Linux Filesystem
type=L, name="Linux_Root"
Save that script into a file, then pass the file via stdin to sfdisk:
Check the disk:
The output should look like:
Device Start End Sectors Size Type
/dev/sdb1 2048 4095 2048 1M unknown
/dev/sdb2 4096 2101247 2097152 1G EFI System
/dev/sdb3 2101248 10489855 8388608 4G Linux swap
/dev/sdb4 10489856 67108830 56618975 27G Linux filesystem
/dev/sdb1is our BIOS boot partition./dev/sdb2is our EFI system partition./dev/sdb3is our SWAP partition./dev/sdb4is our root filesystem partition.
Formatting Partitions with Filesystems¶
- Book Source: Chapter 2.5
Now we can format these partitions.
- The BIOS boot partition (/dev/sdb1) requires no filesystem.
- EFI system partition requires FAT32
- SWAP partition requires SWAP
- Rootfs requires EXT4
Disk should look like this:
kolkhis@lfs-builder:~$ lsblk -f /dev/sdb
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS
sdb
├─sdb1
├─sdb2 vfat FAT32 08D0-FC00
├─sdb3 swap 1 e81488ff-eb87-4fdf-bcbc-c6bc2b335a91
└─sdb4 ext4 1.0 ea99de3f-5513-4338-8e88-878a51dade4c
LFS Variable/Umask¶
- Book Source: Chapter 2.6
LFS requires a $LFS variable to be set to the desired mountpoint for the LFS
partition.
The umask also needs to be set to 022.
Mounting the Partition¶
- Book Source: Chapter 2.7
Mount the root filesystem partition (/dev/sdb4).
If we are using a swap partition, ensure that it is enabled using the swapon command:
sudo swapon /dev/sdb3
sudo swapon
#NAME TYPE SIZE USED PRIO
#/swap.img file 3G 0B -2
#/dev/sdb3 partition 4G 0B -3
Make the Mount Persistent¶
Get the UUID of the root fs block device.
Then add an entry in /etc/fstab using the UUID.
Packages¶
- Book Source: Chapter 3.1
The $LFS/sources directory will be used to store the packages and their
sources.
It's recommended to make this writable and "sticky" (only the owner can delete
files inside).
The packages can be fetched individually or from a curated list specifically for LFS.
A full list, line-by-line, can be found here.
Use wget to fetch all tarballs and patches.
curl -O https://www.linuxfromscratch.org/lfs/view/stable-systemd/wget-list-systemd
wget --input-file=wget-list-systemd --continue --directory-prefix=$LFS/sources
Change ownership of the $LFS/sources directory to root (user and group).
All packages that must be present in $LFS/sources can be found
here,
and the patches can be found
here.
Final Preparations¶
- Book Source
- https://www.linuxfromscratch.org/lfs/view/stable-systemd/chapter04/creatingminlayout.html
We need to create a directory structure on the rootfs to install the new tools we downloaded.
The script they provide:
mkdir -pv $LFS/{etc,var} $LFS/usr/{bin,lib,sbin}
for i in bin lib sbin; do
ln -sv usr/$i $LFS/$i
done
case $(uname -m) in
x86_64) mkdir -pv $LFS/lib64 ;;
esac
This creates the directories:
/mnt/lfs/etc/mnt/lfs/var/mnt/lfs/usr/bin/mnt/lfs/usr/lib/mnt/lfs/usr/sbin
As well as symlinks everything in /mnt/lfs/usr/bin, /mnt/lfs/usr/lib, and /mnt/lfs/usr/sbin to `
When creating symlinks, output should be.
# for i in bin lib sbin; do sudo ln -sv "usr/$i" "$LFS/$i"; done
'/mnt/lfs/bin' -> 'usr/bin'
'/mnt/lfs/lib' -> 'usr/lib'
'/mnt/lfs/sbin' -> 'usr/sbin'
Additionally, if on x86_64 architecture, create the directory $LFS/lib64.
Finally, one more directory:
Adding an LFS User¶
We're creating a new user account (unprivileged) for the LFS system.
Create a user account named lfs.
By default, a new group with the same name is created with useradd, but LFS
suggests using groupadd first to create the group.
Create a psasword for the new user:
Change ownership of everything in $LFS to the new user.
chown -v lfs $LFS/{usr{,/*},var,etc,tools}
# and if on x86 architecture, do that dir
case $(uname -m) in
x86_64) chown -v lfs $LFS/lib64 ;;
esac
Finally, switch to the user.
Setting up the Environment¶
As the lfs user, create a .bash_profile.
This ensures that we don't read the host system's runtime configuration files,
and will only read the .bashrc file in the /home/lfs directory.
The .bashrc will be as follows.
cat > ~/.bashrc << "EOF"
set +h
umask 022
LFS=/mnt/lfs
LC_ALL=POSIX
LFS_TGT=$(uname -m)-lfs-linux-gnu
PATH=/usr/bin
if [ ! -L /bin ]; then PATH=/bin:$PATH; fi
PATH=$LFS/tools/bin:$PATH
CONFIG_SITE=$LFS/usr/share/config.site
export LFS LC_ALL LFS_TGT PATH CONFIG_SITE
EOF
If the host system has an /etc/bash.bashrc file, remove it to prevent the
environment being populated with unwanted environment vars.
Now, prepping to build all the stuff.
make can spawn multiple processes by using the -j flag.
Alternatively, set the MAKEFLAGS var to the desired -j value.
For example, if you have an 8 core processor with 16 efficiency cores, you
could spawn up to 32 instances of make.
Check how many cores you have available with nproc.
My build environment has 2 cores.
So, we'll do -j2.
I am extending the CPUs for the host VM that will be building the packages. I'm allocating 24 cores to it to build faster.
Many packages come with a test suite. Some packages' test suites are more important than others (e.g., gcc, glibc, etc.).
Building the LFS Cross Toolchain and Temporary Tools¶
This is where the real work of building a new system begins.
This section is split into three parts:
- Building a cross compiler and its associated libraries.
-
Using this cross toolchain to build several utilities in a way that isolates them from the host distribution.
-
Entering the chroot environment (which further improves host isolation) and constructing the remaining tools needed to build the final system.
We're cross-compiling the entire toolchain. This means we're building the toolchain on the host system, but it's going to produce code for the target system (the LFS system). The reason for this is that it will not be dependent on the host system.
...anything that is cross-compiled cannot depend on the host environment.
- Note: It's known installing GCC pass 2 will break the cross-toolchain
Important Concepts¶
A few definitions for the rest of the document:
- The build: The machine where we build the programs (the host system).
- The host: This is where the built programs will run (the LFS system).
- The target: The machine that the compiler produces code for.
All the packages in the book use an autoconf-based building system.
This accepts system types in the form cpu-vendor-kernel-os (referred to as a
system triplet).
The vendor field is often omitted.
The kernel and os began as a single system field, which is why it's
called a triplet even though it's 4 fields.
A simple way to determine your system triplet is to run the config.guess script that comes with the source for many packages.
The dynamic linker (or dynamic loader) used on my host system is /lib64/ld-linux-x86-64.so.2
(symlinked from /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2).
This finds and loads the shared libraries needed by a program.
Check your system's dynamic linker with ldd gcc, or with the suggested command:
Using it on /bin/bash:
readelf -l /bin/bash | grep interpreter
# [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
The cross-compiler will not be part of the final LFS build, it's just used for compiling the programs for the LFS system.
In order to cross-compile a package for the LFS temporary system, the name of the system triplet is slightly adjusted by changing the "vendor" field in the LFS_TGT variable so it says "lfs" and LFS_TGT is then specified as “the host” triplet via
--host...
Prepping for the Build¶
The cross-compiler will be installed in a separate $LFS/tools directory, since
it will not be part of the final system.
We'll be using the --with-sysroot option when building the cross-linker and
cross-compiler, to tell them where to find the needed files for "the host" (the
LFS system)
Binutils is installed first because the configure runs of both gcc and glibc perform various feature tests on the assembler and linker
Then, we'll get a C compiler.
It's going to be "cc1-based libgcc", which does require glibc for full functionality (e.g., exception handling and threads), but it's enough to compile glibc by itself.
After installing the compiler, we sanitize Linux API headers. This allows glibc to interface with all Linux kernel features.
Once we've done that, we can install glibc. This is the first package we
cross-compile.
When compiling glibc:
- We'll use the
--host=$LFS_TGToption. - Also the
--build=$(../scripts/config.guess)to enable the "cross-compilation mode". - The
DESTDIRvariable is used to force installation into the LFS file system.
Build Process Synopsis¶
Place all the sources and patches in a directory that will be accessible from
the chroot environment, such as /mnt/lfs/sources/.
-
Change to the /mnt/lfs/sources/ directory.
-
For each package:
-
Using the tar program, extract the package to be built. In Chapter 5 and Chapter 6, ensure you are the lfs user when extracting the package.
-
Do not use any method except the tar command to extract the source code. Notably, using the
cp -Rcommand to copy the source code tree somewhere else can destroy timestamps in the source tree, and cause the build to fail. -
Change to the directory created when the package was extracted.
-
Follow the instructions for building the package.
-
Change back to the sources directory when the build is complete.
-
Delete the extracted source directory unless instructed otherwise.
-
We'll use tar to extract packages to be built.
Installing/Compiling the Packages¶
The cross-compiler and all its associated tools will be installed $LFS/tools.
But the libraries will be installed into the final destination (the LFS system).
Installing Cross Binutils¶
Binutils needs to be installed first because glibc and gcc use this for tests on the available linker and assembler.
The binutils docs suggest making a dedicated build directory.
time the commands to configure and build.../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
# Time:
# real 0m6.452s
# user 0m4.585s
# sys 0m2.860s
make.Then make install.
Installing Cross GCC¶
GCC requires the GMP, MPFR and MPC packages They'll be built with GCC.
Unpack each package into the GCC source directory and rename the resulting directories so the GCC build procedures will automatically use them.
cd $LFS/sources
tar -xJvf ./gcc-15.2.0.tar.xz
cd ./gcc-15.2.0
tar -xJvf ../mpfr-4.2.2.tar.xz
mv -v mpfr-4.2.2 mpfr
tar -xJvf ../gmp-6.3.0.tar.xz
mv -v gmp-6.3.0 gmp
tar -xzvf ../mpc-1.3.1.tar.gz
mv -v mpc-1.3.1 mpc
On x86_64 architecture, set default directory name for 64-bit libraries to lib.
GCC docs recommends a dedicated build directory.
Then we can use the configure script.
time ../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++
# real 0m4.832s
# user 0m2.984s
# sys 0m2.537s
- Full breakdown of command can be found here: https://www.linuxfromscratch.org/lfs/view/stable-systemd/chapter05/gcc-pass1.html
Then we can make and make install
ERRORS¶
Error:
libtool: link: (cd ".libs" && rm -f "libcp1plugin.so.0" && ln -s "libcp1plugin.so.0.0.0" "libcp1plugin.so.0")
libtool: link: (cd ".libs" && rm -f "libcp1plugin.so" && ln -s "libcp1plugin.so.0.0.0" "libcp1plugin.so")
libtool: link: ( cd ".libs" && rm -f "libcp1plugin.la" && ln -s "../libcp1plugin.la" "libcp1plugin.la" )
make[3]: Leaving directory '/mnt/lfs/sources/gcc-15.2.0/build/libcc1'
make[2]: Leaving directory '/mnt/lfs/sources/gcc-15.2.0/build/libcc1'
make[1]: Leaving directory '/mnt/lfs/sources/gcc-15.2.0/build'
make: *** [Makefile:1048: all] Error 2
Failing at line 1048 of the makefile:
all:
+@r=`${PWD_COMMAND}`; export r; \
s=`cd $(srcdir); ${PWD_COMMAND}`; export s; \
$(MAKE) $(RECURSE_FLAGS_TO_PASS) \
$(PGO_BUILD_GEN_FLAGS_TO_PASS) all-host all-target \
Error with verbose output:
checking for x86_64-lfs-linux-gnu-gcc... /mnt/lfs/sources/gcc-15.2.0/build/./gcc/xgcc -B/mnt/lfs/sources/gcc-15.2.0/build/./gcc/ -B/mnt/lfs/tools/x86_64-lfs-linux-gnu/bin/ -B/mnt/lfs/tools/x86_64-lfs-linux-gnu/lib/ -isystem /mnt/lfs/tools/x86_64-lfs-linux-gnu/include -isystem /mnt/lfs/tools/x86_64-lfs-linux-gnu/sys-include
checking for suffix of object files... configure: error: in `/mnt/lfs/sources/gcc-15.2.0/build/x86_64-lfs-linux-gnu/libgcc':
configure: error: cannot compute suffix of object files: cannot compile
See `config.log' for more details
make[1]: *** [Makefile:14102: configure-target-libgcc] Error 1
make[1]: Leaving directory '/mnt/lfs/sources/gcc-15.2.0/build'
make: *** [Makefile:1048: all] Error 2
make -d:
checking whether ln -s works... yes
checking for x86_64-lfs-linux-gnu-gcc... /mnt/lfs/sources/gcc-15.2.0/build/./gcc/xgcc -B/mnt/lfs/sources/gcc-15.2.0/build/./gcc/ -B/mnt/lfs/tools/x86_64-lfs-linux-gnu/bin/ -B/mnt/lfs/tools/x86_64-lfs-linux-gnu/lib/ -isystem /mnt/lfs/tools/x86_64-lfs-linux-gnu/include -isystem /mnt/lfs/tools/x86_64-lfs-linux-gnu/sys-include
checking for suffix of object files... configure: error: in `/mnt/lfs/sources/gcc-15.2.0/build/x86_64-lfs-linux-gnu/libgcc':
configure: error: cannot compute suffix of object files: cannot compile
See `config.log' for more details
Reaping losing child 0x5582d453d240 PID 270198
make[1]: *** [Makefile:14102: configure-target-libgcc] Error 1
Removing child 0x5582d453d240 PID 270198 from chain.
make[1]: Leaving directory '/mnt/lfs/sources/gcc-15.2.0/build'
Reaping losing child 0x5612af9fada0 PID 269347
make: *** [Makefile:1048: all] Error 2
Removing child 0x5612af9fada0 PID 269347 from chain.
cd /mnt/lfs/sources/gcc-15.2.0/build/x86_64-lfs-linux-gnu/libgcc grep -A20 -B5 "cannot compute suffix of object files" /mnt/lfs/sources/gcc-15.2.0/build/x86_64-lfs-linux-gnu/libgcc/config.log
cd ..
cat gcc/limitx.h gcc/glimits.h gcc/limity.h > \
`dirname $($LFS_TGT-gcc -print-libgcc-file-name)`/include/limits.h