Enlightened Linux From Scratch

Software list

Software source archives/repositories

Some bootloader options:

Outline of the build process

Details

Preparing the environment

There is no particular reason to use an Arch ISO as your live environment. I just felt like it and had the ISO on my drive.

Required packages in the Arch live env:

git
base-devel

To install them, make space in the live env like so:

sudo mount -o remount,size=8G /run/archiso/cowspace

I recommend using an actually nice terminal (like st) through SSH to set up your ELFS system.

For that you need to set a password in the Arch VM, so that you can log into it with SSH, since the default password is empty and SSH really doesn't like that.

Then you need a directory in which our ELFS system will live:

For interactive use, I like cfdisk. So I used that, made an MBR partition table, partitioned /dev/vda with one partition, set it as bootable and of type Linux, and that's enough.

Then I formatted that partition with an ext4 filesystem and mounted it at /mnt.

Getting a musl cross toolchain set up

Once that's done, we can move to getting a musl toolchain.

You can get premade ones here: https://musl.cc

I downloaded mine from here: https://musl.cc/x86_64-linux-musl-cross.tgz

curl --output x86_64-linux-musl-cross.tgz https://musl.cc/x86_64-linux-musl-cross.tgz

Then I set up a little shell script we can source to actually use the toolchain.

export PATH="/mnt/src/x86_64-linux-musl-cross/bin:$PATH"
export CC=x86_64-linux-musl-gcc
export LD=x86_64-linux-musl-gcc
export CXX=x86_64-linux-musl-g++
export AR=x86_64-linux-musl-ar
export RANLIB=x86_64-linux-musl-ranlib
export STRIP=x86_64-linux-musl-strip
export CFLAGS="-Os -pipe -fstack-protector-strong"
export LDFLAGS="-static"

Building a kernel

Download the kernel sources:

curl --output linux-7.0.8.tar.xz https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-7.0.8.tar.xz

Make a boot directory and build it:

# ensure necessary modules are really loaded
lsmod | awk '{print $1}' | xargs modprobe -a 2>/dev/null || true

# generate a config based on the loaded modules
# in a VM (and with our expectations) this is sufficient to get a working kernel
make localmodconfig

# ensure the build uses the musl cross toolchain
export CROSS_COMPILE=x86_64-linux-musl-
export ARCH=x86_64

# build
make -j$(nproc)

Then install it to the boot partition:

cp -v ./arch/x86_64/boot/bzImage /mnt/boot/vmlinuz-7.0.8
cp -v System.map /mnt/boot/System.map-7.0.8

And that's it. We have a kernel.

Set up directory structure

cd /mnt
mkdir sbin
mkdir -p usr/bin
mkdir -p usr/include
mkdir -p usr/lib
mkdir -p usr/share/man
mkdir etc
ln -s ./usr/bin ./bin
ln -s ./usr/lib ./lib
ln -s ./usr/lib ./lib64

Set up libc

We already have our cross toolchain. We can cannibalize that for a libc for the host.

Just go into where your x86_64-linux-musl-cross toolchain lives, and then go into x86_64-linux-musl.

Then execute:

cp -v -r ./include/* /mnt/usr/include/
cp -v -r ./lib/* /mnt/usr/lib/

Build userland

For the suckless packages, the workflow is the same:

  1. Open config.mk
  2. Make sure PREFIX is set to /mnt/usr
  3. Make sure MANPREFIX is set to $(PREFIX)/share/man
  4. Make sure CC and AR are commented out, so that the cross toolchain is used
  5. Add -static to LDFLAGS wherever you can
  6. make
  7. make install

And now build the packages:

sbase

Suckless workflow.

ubase

Suckless workflow.

dash

Unfortunately dash uses GNU autotools.

Dash uses some utilites in its build process that need to be compiled using your host compiler:

cd src
gcc -o mksyntax mksyntax.c
gcc -o mkinit mkinit.c
gcc -o mknodes mknodes.c
gcc -o mksignames mksignames.c

Run the configure script with these options:

./configure --prefix=/mnt/usr --enable-static

Then build once and let it fail.

Then go into the src directory again and do:

gcc -o mksyntax mksyntax.c

Then build again, and it will work this time.

smdev

While building you will get a linker error, that the symbol makedev was not found. That symbol is defined in sys/sysmacros.h, which smdev.c expects to be pulled in indirectly through sys/types.h or sys/stat.h.

And that works fine on glibc, but does not work in musl.

So in smdev.c, you have to add under the other sys/*.h includes:

#include <sys/sysmacros.h>

From there, the usual suckless workflow applies.

nldev

For some reason the nldev Makefile adds /usr/include and /usr/lib to your include and lib directories.

Remove that, then classic suckless workflow from there.

sdhcp

At the time of writing this, that repo had issues, so I couldn't clone it. Since it's not strictly necessary for booting, you can skip it.

sinit

Just the suckless workflow. Builds with no issues.

svc

Nothing to build, since it's all scripts.

Open bin/svc, edit BASEDIR to be /etc/svc.d instead of /bin/svc.d

Then move the files:

cd bin
cp ./* /mnt/bin/
cd ..
cp -r svc.d /mnt/etc/

tcc

Sadly TCC uses GNU autotools.

You can configure it like so:

./configure --prefix=/mnt/usr --enable-static --sysincludepaths=/mnt/usr/include --libpaths=/mnt/usr/lib --crtprefix=/mnt/usr/lib64

The same issues as with dash apply, so before building execute this command:

gcc -DC2STR conftest.c -o c2str.exe

Build that then. The TCC tests will fail because you don't have a stdatomic.h header. That's fine, the compiler will still compile C code.

Gluing the mess together

Making the kernel bootable

TODO: document EFISTUB

Alternative: bootloader

TODO: gather the motivation to document at least one of those BIOS bootloaders

Setting up init scripts

Fundamental services
getty and login

Conclusion

You now have an Enlightened Linux From Scratch system.

You could've just installed Alpine Linux and compiled a custom kernel all along.

Please be so kind and donate to one of these distros, whose developers and maintainers do this bullshit for a living and are much better at it than I am: