Installing Gentoo the Hard Way

/linux

In this article, we'll proceed to install Gentoo.

This article follows the story of manually installing Linux distros which begins here https://julienlargetpiet.tech/articles/manually-installing-arch-and-void-linux-on-the-same-disk-with-uefi-and-grub.html where non Gentoo relative commands are explained in depth.

Indeed, we'll focus on Gentoo specific architecture here.

Setup

We'll download the ISO here: https://distfiles.gentoo.org/releases/amd64/autobuilds/current-install-amd64-minimal/.

Choose the adequat architecture, for me it's amd64.

Now, just copy the ISO onto an USB stick, or use Ventoy if you want.

For me I just do:

  
  

sudo dd if=/path/to/iso of=/dev/sdb conv=fsync bs=4M status=progress

  
  

Where /dev/sdb is of course my USB stick.

Now I just plug the USB stick onto my PC and boot onto it.

Hmm, you probably won't have this problem, but my screen cropped the tty and no way to fix this at this point, so I'll pipe the output of the next commands into sed 's/^/ /' to be able to properly see their output.

The ^ represents the start of each line, so I substituate it with 6 blank spaces.

Finally, I'm on the live Gentoo, let's begin the installation process !

Partitioning

Now, I will just output the disks informations on this system:

  
  

lsblk -o NAME,SIZE,MODEL,FSTYPE,MOUNTPOINTS | sed 's/^/     /'

  
  

Output:

  
  

/dev/sda     465.8G   WDC WD5000AZLX-0  
└─/dev/sda1 465.8G   ext4

/dev/sdb      14.9G   USB Flash Drive   

  
  

My USB stick is obviously /dev/sdb and the target disk for the installation is /dev/sda.

The latter already contain a /dev/sda1 partition formatted as an ext4 filesystem, I'll wipe it out !

Also, I must detect if my PC supports UEFI.

Then, I just check /sys/firmware/efi:

  
  

ls /sys/firmware/efi

  
  

I see some files, then my PC accepts UEFI boot method.

We'll make 2 partitions, /dev/sda1 for the ESP (about 500M) and one big /dev/sda2 for the whole system.

Hmm, I prefere parted over fdisk so I'll do:

  
  

parted /dev/sda --script mklabel gpt \
  mkpart ESP FAT32 1MiB 513MiB \
  set 1 esp on \
  mkpart root ext4 513MiB 100%

  
  

Then, we'll format it:

  
  

mkfs.fat -F 32 -n ESP /dev/sda1
mkfs.ext4 -L root /dev/sda2

  
  

Setting up /mnt

We'll mount the disk so we can access it from the live system:

  
  

mount /dev/sda2 /mnt/gentoo
mkdir -p /mnt/gentoo/boot/efi
mount /dev/sda1 /mnt/gentoo/boot/efi

  
  

For this step, I'll assume that we're connected through an Ethernet cable, so this ping must be successfull:

  
  

ping -c 3 julienlargetpiet.tech

  
  

If you just have the Wi-Fi antenna available, then detect its interface name with ip link, set it up with ip link set dev INTERFACE-NAME up, connect to the AP (Access Point) through nmcli if availaible (nmcli device wifi connect "SSID-OR-BSSID" password "PASSWORD" ifname INTERFACE-NAME) or through wpa_supplicant and wpa_passpharse.

Here's a much more detailed article about Wi-Fi on Linux:

https://julienlargetpiet.tech/articles/i-just-wanted-wi-fi-to-work-on-linux.html

Now that we checked that we had internet, we cd /mnt/gentoo and download a file containing the name of the latest tar file to download on the Gentoo server containing the required files for Gentoo:

  
  

/mnt/gentoo > wget https://distfiles.gentoo.org/releases/amd64/autobuilds/current-stage3-amd64-openrc/latest-stage3-amd64-openrc.txt

/mnt/gentoo > cat latest-stage3-amd64-openrc.txt

  
  

(And yess we'll use OpenRC instead of systemd)

Output:

  
  

Hash: SHA256

# Latest as of ...
...

stage3-amd64-openrc-20260811T083102Z.tar.xz

--- BEGIN PGP SIGNATURE ---

...

--- END PGP SIGNATURE ---


  
  

So now we download this tar file:

  
  

/mnt/gentoo > wget https://distfiles.gentoo.org/releases/amd64/autobuilds/current-stage3-amd64-openrc/stage3-amd64-openrc-20260811T083102Z.tar.xz

  
  

A .tar file is a file containing files and directories, the additional .xz extension means that this file is compresses using xz.

We use xz as it follow:

  
  

xz file.txt

  
  

turns:

  
  

file.txt

  
  

into:

  
  

file.txt.xz

  
  

and removes the original file.txt by default.

We can keep the original file while compressing with:

  
  

xz -k file.txt

  
  

To decompress it:

  
  

xz -d file.txt.xz

  
  

or equivalently:

  
  

unxz file.txt.xz

  
  

We can also add the -k flag to keep the .xz file.

Also, decompress to standard output instead of writing teste.txt with the -c flag:

  
  

xz -dc file.txt.xz

  
  

which is useful for things like:

  
  

xz -dc archive.tar.xz | tar -x

  
  

Which decompress the compressed .xz file (keeps it) and we pass the stdout to tar -x which then will extract the previously compressed archive.

But tar also supports direct decompression of .xz file with the -J flag.

Well, back to the installation.

Before extracting the Stage 3 archive, we should verify its integrity.

Gentoo provides a SHA256 checksum file alongside the Stage 3 archive, so we download it:

  
  

wget https://distfiles.gentoo.org/releases/amd64/autobuilds/current-stage3-amd64-openrc/stage3-amd64-openrc-20260811T083102Z.tar.xz.sha256

  
  

Then we ask sha256sum to verify the downloaded archive against that checksum:

  
  

sha256sum -c stage3-amd64-openrc-20260811T083102Z.tar.xz.sha256

  
  

If the archive is intact, we should get:

  
  

stage3-amd64-openrc-20260811T083102Z.tar.xz: OK

  
  

-c means check.

A .sha256 file usually contains both:

  
  

<expected SHA256 hash>  <filename>

  
  

For example:

  
  

8f3a...c91d  stage3-amd64-openrc-20260811T083102Z.tar.xz

  
  

So when we ran the last command, sha256sum does this:

  
  

read expected hash from .sha256 file
        |
        V
read the filename from that same file
        |
        V
compute SHA256 of that local file
        |
        V
compare computed hash with expected hash

  
  

But we still need to verify that the downloaded archive really comes from Gentoo, because an attacker able to replace the tarball could theoretically also replace its SHA256 file.

That's kind of what OpenBSD advise to do, I speak about that in this article at the following section:

https://julienlargetpiet.tech/articles/installing-openbsd-the-hard-way.html#installing-base-system

Gentoo also provides a detached OpenPGP signature alongside the Stage 3 archive:

  
  

wget https://distfiles.gentoo.org/releases/amd64/autobuilds/current-stage3-amd64-openrc/stage3-amd64-openrc-20260811T083102Z.tar.xz.asc

  
  

The .asc file is not the public key. It is the cryptographic signature of the Stage 3 archive, created using Gentoo's private signing key.

We must separately obtain/import Gentoo's trusted release public key and verify its fingerprint through a trusted Gentoo source.

sec-keys/openpgp-keys-gentoo-release is the Gentoo package name that contains the Gentoo release public keys.

When we boot the official Gentoo live ISO, that package is already installed in the live environment, so the key file is already present at something like:

  
  

/usr/share/openpgp-keys/gentoo-release.asc

  
  

So we can import it into the gpg environment:

  
  

gpg --import /usr/share/openpgp-keys/gentoo-release.asc

  
  

Once that public key is present in our GPG keyring, we can verify the archive:

  
  

gpg --verify stage3-amd64-openrc-20260811T083102Z.tar.xz.asc \
             stage3-amd64-openrc-20260811T083102Z.tar.xz

  
  

If the signature is valid and the signing key is trusted as Gentoo's real release key, we have authenticated the Stage 3 archive.

If you were not using the official Gentoo live ISO, Gentoo also documents fetching its key bundle directly:

  
  

wget -O - https://qa-reports.gentoo.org/output/service-keys.gpg | gpg --import

  
  

-O means write output to the following filename.

The following “filename” is:

  
  

-

  
  

and by convention - means standard output.

Then you can verify the signature in the same way

So the complete trust chain is:

  
  

trusted Gentoo public key
        |
        V
gpg --import ...
        |
        V
public key stored in ~/.gnupg keyring

  
  

And:

  
  

Stage3.tar.xz + Stage3.tar.xz.asc
        |
        V
gpg --verify ...
        |
        V
GPG retrieves Gentoo public key from keyring
        |
        V
signature valid / invalid

  
  

Now that we verified the authenticity of the tarball, we run:

  
  

/mnt/gentoo > tar xJpvf stage3-amd64-openrc-20260811T083102Z.tar.xz \
  --xattrs-include='*.*' \
  --numeric-owner

  
  

-p is for preserving file and folders permissions.

In addition to preserving this, we have --xattrs-include="*.*" meaning that tar should also preserve all metadata that matches this patter "." (essentially all).

Indeed, maybe you didn't know about that but the inode corresponding to the file can have several key-value pairs, where the key is in fact composed of a namespace ant its attribute (such as user.comment), for example we use setfattr to set the attribute(s) of a file and getfattr to get its attribute(s).

  
  

setfattr -n user.comment -v "this is my comment" teste.txt.xz

setfattr -n user.comment2 -v "this is my comment 2" teste.txt.xz

  
  

And then to only get the key:

  
  

getfattr teste.txt.xz

  
  

Output:

  
  

# file: teste.txt.xz
user.comment
user.comment2

  
  

So now we can just query the particular value of a key:

  
  

getfattr -n user.comment2 teste.txt.xz

  
  

Output:

  
  

# file: teste.txt.xz
user.comment2="this is my comment 2"

  
  

Or we can directly query all (dump) with the -d flag:

  
  

getfattr -d teste.txt.xz

  
  

Output:

  
  

# file: teste.txt.xz
user.comment="this is my comment"
user.comment2="this is my comment 2"

  
  

Those commands also work for folders.

The available namespaces are:

  • user.* -> general-purpose attributes for ordinary users and applications. On files we own, we can usually create arbitrary keys here, like user.comment.

And others namespaces that I'll describe in another article.

Now, --numeric-owner tells tar to resolve owner and group owner by respectively the UID and the GID instead of using the username and group name because those names often differs from the source environment to the destination environment (here /mnt).

A Gentoo Stage 3 tarball is the prebuilt minimal userspace filesystem tree that gives you a bootstrappable Gentoo system. It contains the standard directory hierarchy, core libraries, shell and basic utilities, Portage, package database metadata, configuration skeletons, ownerships, permissions, symlinks, and other filesystem metadata needed for the chosen architecture (here amd64).

We verify the hierarchy:

  
  

/mnt/gentoo > ls -la *

  
  

We should see something like:

  
  

bin
boot
dev
etc
home
lib
lib64
mnt
opt
proc
root
run
sbin
sys
tmp
usr
var

  
  

First chroot

Here I've just cd ../...

Now, we still have to mount some folders because at this point we're using the live kernel that directly communicates with those special folders.

Those folders contains special files, no creatable by decompressing tar because hardware-dependant (devices etcetera).

So because we also have to run commands on /mnt/gentoo to finalyze our system, and that those commands needs a complete envirnment to run.

Then, we'll sort of "make a proxy" of this environment into /mnt/gentoo.

But first, we can already copy the DNS configuration file from the live system to the target system:

  
  

cp --dereference /etc/resolv.conf /mnt/gentoo/etc/.

  
  

Here, we use --dereference flag because this can be a symlink and we don't want to copy the symlink itself, but the content it points to.

For example, here on my Debian12 machine, /etc/resolv.conf is a symlink:

  
  

ls -l /etc/resolv.conf

  
  

Output:

  
  

ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 déc.  18  2025 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf

  
  

Setting the configuration directly at /etc/resolv.conf instead of a symlink to elsewhere is totally normal because programms that will search for DNS configuration are not able to see the difference from an actual normal file rather than a symlink at their layer.

Finally, we can proxy the live system to /mnt/gentoo:

  
  

mount --types proc /proc /mnt/gentoo/proc

mount --rbind /sys /mnt/gentoo/sys
mount --make-rslave /mnt/gentoo/sys

mount --rbind /dev /mnt/gentoo/dev
mount --make-rslave /mnt/gentoo/dev

mount --bind /run /mnt/gentoo/run
mount --make-slave /mnt/gentoo/run

  
  

The --types proc (or -t proc) explicitely describes the filesystem to mount.

Indeed, even if mount could have infered it, I prefere to be explicit because there is also sysfs, devtmpfs...

On my Debian 12 I got:

  
  

findmnt /dev

  
  

Output:

  
  

TARGET
     SOURCE
          FSTYPE   OPTIONS
/dev udev devtmpfs rw,nosuid,relatime,size=7117792k,nr_inodes=1779448,mode=755,inode64

  
  

And also for example:

  
  

findmnt /sys

  
  

Output:

  
  

TARGET
     SOURCE
           FSTYPE OPTIONS
/sys sysfs sysfs  rw,nosuid,nodev,noexec,relatime

  
  

But why no --make-rslave on it ?

Because for /proc we don't need to do that if we ask the kernel to make a new instance of /proc at another location (here /mnt/gentoo/proc).

Its' conceptually like:

  
  

kernel
  |-- provides procfs
  |
  |-- mounted at /proc
  |
  |-- mounted again at /mnt/gentoo/proc

  
  

We could theorically also make the same thing for the other special filesystem, here's an example for /sys:

  
  

mount -t sysfs /sys /mnt/gentoo/sys

  
  

But /sys can contain nested mounts created by other kernel subsystems or services

If you did only the above command, we would get a fresh top-level sysfs, but we would not automatically reproduce every separate mount living underneath the live system's /sys.

Now, the environment is correctly set, so we enter it:

  
  

chroot /mnt/gentoo /bin/bash

  
  

And we do:

  
  

source /etc/profile

  
  

chroot changes the filesystem root, but it does not rebuild the shell environment (inherits the live system’s environment). Therefore, after entering the Gentoo targeted install mount point, we source /etc/profile so the current shell loads Gentoo's environment variables and shell configuration instead of relying only on values inherited from the live system.

We can also make the difference visually clear between the 2 bash processes by modifying the current PS1:

  
  

export PS1="(chroot) ${PS1}"

  
  

It just prepend "(chroot)" to the current PS1.

First contacts with emerge

The package manager of Gentoo is called Portage and its CLI is emerge.

For example we have:

  
  

emerge --sync

  
  

To update the packages database (versions, etcetera) (see later).

  
  

emerge pkg

  
  

To simply install a package (in this case pkg).

"install" here means that Portage resolves, fetches, builds if necessary, and merges/installs the package. emerge --fetchonly pkg is the download-only operation.

  
  

emerge --ask pkg

  
  

Resolves what would need to be done to install pkg, show us the planned package operations, then ask for confirmation before actually doing them.

Without --ask, Portage proceeds with the resolved operation without that confirmation prompt.

A very common Gentoo form is:

  
  

emerge --ask --verbose pkg

  
  

or:

  
  

emerge -av pkg

  
  

A typical output of that command would be:

  
  

These are the packages that would be merged, in order:

Calculating dependencies... done!
Dependency resolution took 1.17 s.


[ebuild  N     ] dev-libs/libfoo-1.4.2  USE="ssl -debug" 0 KiB
[ebuild  N     ] dev-libs/libbar-2.1.0  USE="threads" 0 KiB
[ebuild  N     ] app-misc/myprogram-3.7.1  USE="ssl threads -gtk" 850 KiB

Total: 3 packages (3 new), Size of downloads: 850 KiB

Would you like to merge these packages? [Yes/No]

  
  

"merge" means "install", "unmerge" means "uninstall" in the Portage terminology.

A typical general update command is:

  
  

emerge --update --deep --newuse @world

  
  

We can shorten it into:

  
  

emerge -uDN @world

  
  

This introduces the concept of packages set.

Semantically, that's packages group.

In Gentoo, we have the following:

  • @system -> essential packages required by the active Gentoo profile.

  • @selected -> packages you explicitly asked Portage to install.

  • @world -> the overall managed system: roughly @system plus @selected.

  • @preserved-rebuild -> packages that should be rebuilt because preserved libraries are still in use.

  • @module-rebuild -> packages that build kernel modules and may need rebuilding after a kernel change.

--update tells Portage to prefer newer eligible versions of packages in the targeted set. It does not necessarily traverse their complete dependency trees. emerge --sync, by contrast, synchronizes the local Gentoo repository itself.

That's why we use the --deep flag, it tells Portage to also consider dependencies recursively.

And --newuse tells to Portage to rebuild packages that have seen their USE-flag change over their last build.

So even if a package has not received any update, if its USE-flag(s) have changed since its last build, then it'll be rebuilt.

To change the USE flag of a package we edit /etc/portage/package.use/pkg where pkg is the targeted package.

We can do that for example:

  
  

echo 'www-client/firefox wayland' >> /etc/portage/package.use/firefox

  
  

Back to the installation.

We need to download the Gentoo package repository metadata and ebuilds needed by Portage to install and manage software.

For that we run:

  
  

(chroot) > emerge-webrsync

  
  

The base system applications themselves mostly came from the Stage 3 tarball. emerge-webrsync just gives Portage the repository information it needs for future package operations.

To be sure that the local repository contains the last packages version (typically contained under /var/db/repos/gentoo) we do:

  
  

(chroot) > emerge --sync

  
  

Choosing the profile

A profile is a system-wide policy/configuration baseline for Gentoo. It substantially influences how Portage configures and resolves the system.

Indeed, it will change this symlink /etc/portage/make.profile to point to the one selected, for example /var/db/repos/gentoo/profiles/selected-profile.

That profile defines the following:

  • default USE flags

  • forced USE flags

  • masked USE flags

  • package masks (blocked packages)

  • default package choices

  • the @system package set

  • architecture defaults

  • init-system defaults (here it'll be OpenRC)

  • desktop-related defaults

  • compiler/toolchain-related policy

To see all the available profiles, run:

  
  

(chroot) > eselect profile list

  
  

Personaly, I've chosen [3] default/linux/amd64/23.0/desktop (stable).

So I did:

  
  

(chroot) > eselect profile set 3

  
  

We can verify the selected profile with:

  
  

(chroot) > eselect profile show

  
  

Expected output:

  
  

default/linux/amd64/23.0/desktop

  
  

Locals an timezone

This is the general Linux step for the timezone:

  
  

(chroot) > ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime

  
  

Because I'm based in Paris, "Bonjour j'aime la baguette".

Alternatively, we could have set up the timezone, for me I would have run:

  
  

(chroot) > echo "Europe/Paris" > /etc/timezone

  
  

And then, we rerun the configuration script of the package sys-libs:

  
  

(chroot) > emerge --config sys-libs/timezone-data

  
  

It would have set the symlink correctly.

Then, set up your locals:

  
  

(chroot) > printf '%s\n' \
'en_US.UTF-8 UTF-8' \
'fr_FR.UTF-8 UTF-8' \
> /etc/locale.gen

  
  

Then we run:

  
  

(chroot) > locale-gen

  
  

It will compile the locale definitions to /usr/lib/locale for applications to use though the linux API.

Now, we'll choose the default locale for the system:

  
  

(chroot) > eselect locale list

  
  

As you guess, I've chosen fr_FR.utf8.

So I run:

  
  

(chroot) > eselect locale set 5

  
  

(5 is the related numero for my locale)

I now reload the environment:

  
  

(chroot) > env-update
(chroot) > source /etc/profile
(chroot) > export PS1="(chroot) ${PS1}"

  
  

Now we can verify simply with locale to see our default locale, for me:

  
  

LANG=fr_FR.utf8

  
  

Installing the kernel

I installed the precompiled kernel.

"Heresy, heresy ! On Gentoo we don't install binaries !!!" I may hear, but don't worry we'll cover the manually compiled path later on.

So, I ran:

  
  

(chroot) > emerge --ask sys-kernel/gentoo-kernel-bin

  
  

And that's all.

Or maybe this will fail because sys-kernel/gentoo-kernel-bin needs a dependency built with a special USE flag.

With:

  
  

(chroot) > emerge --ask --autounmask-write pkg

  
  

Portage may propose:

  
  

The following USE changes are necessary:
# required by pkg
>=dev-libs/bar-1.2 foo

  
  

and then write that change into under:

  
  

/etc/portage/package.use/

  
  
  • --autounmask -> calculate and show configuration changes that would solve the dependency problem

  • --autounmask-write -> also write those proposed changes into /etc/portage

This command has not yet installed the kernel onto the system because its configuration file are still not accepted.

They are written as candidates (CONFIG_PROTECT), for example:

  
  

/etc/portage/package.use/._cfg0000_somefile

  
  

Then:

  
  

(chroot) > dispatch-conf

  
  

or:

  
  

(chroot) > etc-update

  
  

lets us actually merge or use that proposed USE-flag change into the real configuration.

Here we can simply select use-new to replace our old config files by the one proposed.

It's safe only when we're sure replacing the existing config is intended. But that's a good practice to inspect the diff before choosing.

Then we (re)run:

  
  

(chroot) > emerge --ask sys-kernel/gentoo-kernel-bin

  
  

and Portage resolves again using the now-active configuration.

Now, the kernel vmlinuz and the initramfs must be present inside /boot.

With:

  
  

ls -l /boot

  
  

We can see:

  
  

config-<version>-gentoo-dist-bin
initramfs-<version>-gentoo-dist-bin.img
System.map-<version>-gentoo-dist-bin
vmlinuz-<version>-gentoo-dist-bin

  
  

So the flow is:

  
  

emerge --ask --autounmask-write pkg
        |
        V
Portage proposes/writes config changes
        |
        V
CONFIG_PROTECT may create ._cfg* files
        |
        V
dispatch-conf or etc-update
        |
        V
review/merge/replace by the changes
        |
        V
rerun emerge --ask pkg

  
  

Now, the manual way, here it is.

First, we download the latest selected version of Gentoo’s patched Linux kernel.

  
  

(chroot) > emerge --ask sys-kernel/gentoo-sources

  
  

It'll download it under /usr/src/linux-<version>-gentoo/.

At this point, there's only one kernel version (the one we've just download) present in the system, so:

  
  

(chroot) > eselect kernel list

  
  

Should propose only one choice.

You start seeing multiple choices later if you keep older kernel sources installed.

So select it:

  
  

(chroot) > eselect kernel set 1

  
  

Now:

  
  

cd /usr/src/linux

  
  

Because /usr/src/linux is a symlink to the selected versioned source tree.

The advantage of /usr/src/linux is that it always points to the kernel source tree currently selected by eselect kernel.

Now that we are in the kernel directory, we can configure it with menuconfig:

  
  

(chroot) /usr/src/linux > make menuconfig

  
  

This opens the kernel configuration interface where you choose things such as:

  
  

CPU features
filesystems
NVMe/SATA drivers
USB
network drivers
Wi-Fi
graphics
sound
device drivers
kernel features

  
  

The resulting configuration is written to:

  
  

/usr/src/linux/.config

  
  

Then compile:

  
  

(chroot) /usr/src/linux > make -j"$(nproc)"

  
  

This compiles both the kernel and all features you selected as modules with all cores (nproc gives us that).

On x86_64, the actual compressed kernel image ends up around:

  
  

/usr/src/linux/arch/x86/boot/bzImage

  
  

Then, we install the kernel modules:

  
  

(chroot) /usr/src/linux > make modules_install

  
  

This puts them under:

  
  

/lib/modules/<kernel-version>/

  
  

This is more or less a bunch of cp commands.

Now that the modules are installed, we still have to install the kernel:

We can do that with:

  
  

(chroot) /usr/src/linux > make install

  
  

or do the equivalent manual installation.

The manual version is conceptually:

  
  

(chroot) /usr/src/linux > cp arch/x86/boot/bzImage /boot/vmlinuz-6.x.y-gentoo
(chroot) /usr/src/linux > cp System.map /boot/System.map-6.x.y-gentoo

  
  

System.map is a symbol table for the compiled kernel. It maps kernel symbol names to addresses.

Conceptually:

  
  

ffffffff81000000  _text
ffffffff81012340  some_kernel_function
ffffffff81234560  another_function

  
  

That is useful for debugging things like kernel crashes, stack traces, and address-to-symbol resolution.

So if a kernel diagnostic contains an address like:

  
  

ffffffff81012340

  
  

System.map allow to associate it with:

  
  

some_kernel_function

  
  

On the manual kernel installation, we can still need the initramfs depending on how the kernel is built (menuconfig options).

The crucial difference in menuconfig is:

  • [*] or <*> -> built directly into the kernel

  • <M> -> kernel module

If our root filesystem is ext4 and we configure:

  
  

<M> The Extended 4 (ext4) filesystem

  
  

then we have a problem: the ext4.ko module normally lives in:

  
  

/lib/modules/<kernel-version>/

  
  

which itself is on our root filesystem. But the kernel needs ext4 support before it can mount that filesystem, we need a temporary filesystem able to loead the modules, that's initramfs.

So if we're not sure, we install it:

  
  

(chroot) > emerge --ask sys-kernel/dracut

  
  

And then we generate the initramfs for our specific kernel version (--kver):

  
  

(chroot) > dracut --kver <version>-gentoo

  
  

Wich will create:

which typically creates something like:

  
  

/boot/initramfs-6.x.y-gentoo.img

  
  

Then GRUB can load both:

  
  

vmlinuz-6.x.y-gentoo
initramfs-6.x.y-gentoo.img

  
  

/etc/fstab

We'll take the UUID of the partition to identify them, which is more robust.

We can identify them on /dev/sda with:

  
  

(chroot) > blkid /dev/sda1 /dev/sda2

  
  

Which will output something like:

  
  

/dev/sda1: UUID="D582-D4F9" BLOCK_SIZE="512" TYPE="vfat" PARTLABEL="ESP" PARTUUID="174a0abe-1272-fa44-b63f-411ad529cc20"
/dev/sda2: UUID="070eb80c-2ffc-45a0-83ab-3ee9c37a7dc8" BLOCK_SIZE="4096" TYPE="ext4" PARTLABEL="root" PARTUUID="60e9a40e-a90f-974e-af3c-7235b7e5d05f"

  
  

We can respectively assign the UUID to some variables:

  
  

ESP_UUID=$(blkid -o value -s UUID /dev/sda1)
ROOT_UUID=$(blkid -o value -s UUID /dev/sda2)

  
  

And then write this to /etc/fstab:

  
  

printf '%s\n' \
"UUID=$ROOT_UUID / ext4 defaults,noatime 0 1" \
"UUID=$ESP_UUID /boot/efi vfat defaults 0 2" \
> /etc/fstab

  
  

Final configuration

We have first to set up the root password:

  
  

(chroot) > passwd

  
  

Now, setup the hostname, I'll call it "gentoo":

  
  

(chroot) > echo "gentoo" > /etc/hostname

  
  

Now, we install the tool that will ask the dhcp server our network configuration (IPV4 etcetera), and that's dhcpcd:

  
  

(chroot) > emerge --ask net-misc/dhcpcd

  
  

And we enable it for future boot (not with systemd because it's not present, but with openrc).

  
  

(chroot) > rc-update add dhcpcd default

  
  

The default is a runlevel, with openrc we have:

  1. sysinit -> very early system initialization

  2. boot -> services needed while booting

  3. default -> normal everyday operating state

  4. shutdown -> shutdown-related services

So default is roughly the normal multi-user state of the machine. It is where you normally enable services such as networking, SSH, cron, display managers, etc.

You can see which services belong to which runlevels with:

  
  

rc-update show

  
  

And specifically:

  
  

rc-update show default

  
  

To remove it again:

  
  

rc-update del dhcpcd default

  
  

The other major OpenRC command is rc-service. rc-update controls when a service should run automatically, while rc-service controls the service right now.

For example:

  
  

rc-service dhcpcd start
rc-service dhcpcd stop
rc-service dhcpcd restart
rc-service dhcpcd status

  
  

Here's a table showing the commands for openrc and systemctl for the same purpose:

systemd OpenRC
systemctl start sshd rc-service sshd start
systemctl stop sshd rc-service sshd stop
systemctl restart sshd rc-service sshd restart
systemctl status sshd rc-service sshd status
systemctl enable sshd rc-update add sshd default
systemctl disable sshd rc-update del sshd default
systemctl enable --now sshd rc-update add sshd default && rc-service sshd start
systemctl is-enabled sshd rc-update show default | grep sshd
systemctl list-units --type=service rc-status

With OpenRC:

  
  

rc-update add dhcpcd default

  
  

registers the service in a runlevel. OpenRC then knows that when it enters default, dhcpcd should be running.

OpenRC does use runlevel directories containing symlinks to service scripts. Typically we see something like:

  
  

/etc/runlevels/
|-- sysinit/
|-- boot/
|-- default/
|-- nonetwork/
|-- shutdown/

  
  

So,

  
  

/etc/runlevels/default/sshd

  
  

is typically a symlink to:

  
  

/etc/init.d/sshd

  
  

And that init script then knows which executable to run, what dependencies it has, what arguments to pass, and how to stop it.

With systemd, a custom service does not need anything under /etc/init.d/. The unit file itself is the service definition (under /etc/systemd/system/service-name).

For example:

  
  

[Unit]
Description=My custom service

[Service]
ExecStart=/usr/local/bin/mydaemon

[Install]
WantedBy=multi-user.target

  
  

That is enough for systemd because the unit file tells it what binary to launch and how the service behaves.

With OpenRC, the equivalent service definition is usually an init script under:

  
  

/etc/init.d/mydaemon

  
  

For a simple daemon, it can be very small:

  
  

#!/sbin/openrc-run

command="/usr/local/bin/mydaemon"
command_background="yes"
pidfile="/run/mydaemon.pid"

  
  

Then make it executable:

  
  

chmod +x /etc/init.d/mydaemon

  
  

and enable it:

  
  

rc-update add mydaemon default

  
  

Nevertheless, a systemd unit’s ExecStart= can point to any executable command, including an /etc/init.d/... shell script.

But it is usually considered a compatibility approach rather than the preferred native systemd design.

Grub

Now that we've done the minimum configurations, we do need to install bootloader and in this article it will be Grub:

  
  

(chroot) > emerge --ask sys-boot/grub

  
  

Ouch, I get a circular dependency error:

  
  

Error: circular dependencies:
...
dev-libs/glib
dev-python/docutils
dev-python/pillow
media-libs/harfbuzz
...

  
  

A circular dependency means that the current dependency graph has no valid build order. For example, building A requires B, while the selected configuration of B eventually requires A again.

Some dependency edges exist only when a particular USE flag is enabled. Portage noticed that disabling Pillows truetype USE flag removes one of the edges forming the cycle:

  
  

A -> ... -> Pillow[truetype] -> ... -> A

  
  

becomes:

  
  

A -> ... -> Pillow

  
  

with the problematic dependency no longer required.

Portage can then find a valid build order. Once those packages are installed, we remove the temporary USE override and rebuild Pillow with its normal USE configuration.

That's why we have:

  
  

It might be possible to break this cycle
by applying the following change:

dev-python/pillow-12.3.0 (Change USE: -truetype)

  
  

So now we can create an arbitrary file nunder /etc/portage/package.use/ containing this:

  
  

dev-python/pillow -truetype

  
  

Because when installing a package, Portage will read all the use flags written in all files in this directory.

So, we do:

  
  

(chroot) > echo "dev-python/pillow -truetype" > /etc/portage/package.use/grub-bootstrap

  
  

Because:

  
  

Pillow with truetype enabled
        |
        V
needs some additional library

  
  

but:

  
  

Pillow with truetype disabled
        |
        V
does not need that library

  
  

Indeed, in Gentoo USE syntax:

  
  

truetype

  
  

means enable the truetype USE flag, while:

  
  

-truetype

  
  

means disable it.

Then:

  
  

(chroot) > emerge --ask sys-boot/grub

  
  

Portage recalculates dependencies with:

  
  

Pillow:
truetype = OFF

  
  

and can hopefully build the dependency chain.

Just a little sidebar here.

Personaly, at this point I had to let the compilation all the night to finish, and at the morning I had the bad surprise to note that there wasn't even a video output from my PC.

So I had to completely shut it down, disconnect the power source, remove the RAM during several minutes and finally reboot it.

I booted on the live Gentoo ISO and mounted the /dev/sda partitions like we've done before.

I checked the Portage emerge last logs with:

  
  

(chroot) > tail -n 30 /var/log/emerge.log

  
  

And hopefully the package was successfully installed:

  
  

Completed emerge (186 of 186)
*** Finished. Cleaning up...
*** exiting successfully.
*** terminating.

  
  

Once the bootstrap is complete, that restriction is no longer necessarily required, because the packages involved in the circular dependency now already exist on the system. So we remove our temporary override:

  
  

(chroot) > rm /etc/portage/package.use/grub-bootstrap

  
  

Now the profile's normal settings apply again. If the profile wants:

  
  

truetype = ON

  
  

Portage sees that the installed Pillow was built with a different USE configuration. Therefore:

  
  

(chroot) > emerge --ask --newuse dev-python/pillow

  
  

Will rebuild Pillow according to its now-current USE configuration. Indeed, as written before --newuse flag tells Portage to recompile the targeted package if its last use flag(s) did not match with the current one(s).

You can verify that the installation of sys-boot/grub properly worked:

  
  

grub-install --version

  
  

Now, we finally can install grub in the ESP:

  
  

(chroot) > grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Gentoo

  
  

Finally, we generate its configuration:

  
  

(chroot) > grub-mkconfig -o /boot/grub/grub.cfg

  
  

We can check if we see a "Gentoo" entry in the NVRAM with:

  
  

(chroot) > efibootmgr

  
  

And we must see this directory /boot/efi/EFI/Gentoo by recursively ls in /boot/efi/EFI:

  
  

ls -R /boot/efi/EFI

  
  

Or by the method you want (that was just to introduce you to the -R flag in ls lol).

Exiting the target environment

Now, it's a good idea to tell the kernel to flush pending filesystems writes from memory to the storage device.

  
  

(chroot) > sync

  
  

And, we can finally exit:

  
  

(chroot) > exit

  
  

Finally, we recursively unmount all mount points under /mnt/gentoo:

  
  

umount -R /mnt/gentoo

  
  

And then we can finally reboot:

  
  

reboot

  
  

We'll make sure to unplug the USB sticks for the boot.

Congratulations, you finally have installed Gentoo !!!

Conclusion

Hope this article was useful, see you later ;)