Continuation — chroot
In the huge previous post, Breakdown — my Arch installation script pt. 1 (archinstall.sh), I explained the entire first stage of my Arch installation script: from gathering information and preparing the disk to installing the base system and entering the chroot environment. This article continues right from that point, detailing the final system configuration before the first boot.
It's worth highlighting an important point from the previous post: this script is not run manually; archinstall.sh is responsible for calling it automatically. At this point, it's already running inside the chroot, meaning every command executed here directly modifies the system that will be booted afterward. It's at this stage that how the system will actually behave is defined: bootloader, locale, users, services, and so on.
An interesting curiosity is that, in this environment, there is no init system running — that is, there is no PID 1 process managing services. This means commands like systemctl start wouldn't work.
Again, the link to the repository containing the code discussed in this post:
Shebang, set -euo, source and logs
Nothing new here: these are the same lines present in archinstall.sh, already explained in the previous post.
#!/usr/bin/env bash
# Stage 2 — executed inside arch-chroot by archinstall.sh. Not meant to be run standalone.
set -euo pipefail
cd /root
source ./config.sh
source ./.archinstall_secrets
log() { echo -e "\n==> $*"; }
err() { echo "ERROR: $*" >&2; }
First, we declare the shebang so the script is executed with Bash. Next, we enable options that make it safer — for example, terminating its execution if any command fails. Then, we enter the /root directory, where the required files are located, import the configurations from config.sh and .archinstall_secrets, and finally define two simple functions to log messages and errors.
In practice, this is exactly the same structure as archinstall.sh, so there's not much new to explore here.
enable_locale
Besides log() and err(), enable_locale() is the only other function present in this script.
enable_locale() {
local locale="$1"
if grep -qE "^${locale} UTF-8" /etc/locale.gen; then
return
elif grep -qE "^#${locale} UTF-8" /etc/locale.gen; then
sed -i "s/^#${locale} UTF-8/${locale} UTF-8/" /etc/locale.gen
else
echo "${locale} UTF-8" >> /etc/locale.gen
fi
}
Locale is the setting that tells the system in which language and regional format to display information such as dates, numbers, currency, among others. The /etc/locale.gen file lists all the locales the system can generate. By default, practically all of them come commented out, meaning disabled.
This function handles the three possible states of this file:
- The locale is already enabled.
if grep -qE "^${locale} UTF-8" /etc/locale.gen; then return→greplooks for a line that matches the content of$localeexactly. If found, it means the locale is already enabled, so the function simply returns.
- The locale exists but is commented out.
elif ... sed -i "s/^#${locale} UTF-8/${locale} UTF-8/" /etc/locale.gen→greplooks for the same pattern as in the previous case, but with#at the start of the line, indicating that the locale is disabled. Then,sed -iremoves the#, enabling the locale.
- The locale doesn't even appear in the file.
else echo "${locale} UTF-8" >> /etc/locale.gen→ If none of the previous checks find the locale, it means it doesn't exist in/etc/locale.gen. In that case,>>appends a new line to the end of the file containing the locale.
As a complement, grep is a command used to search for lines in a file that match a specified pattern. sed, in turn, is a non-interactive text editor; the -i flag modifies the file itself (in-place), instead of just printing the result to the terminal.
The sed substitution pattern is similar to vim's: s/pattern/replacement/, where s stands for substitute.
timezone, hwclock and locale-gen
log "Setting timezone"
ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime
hwclock --systohc
log "Generating locale"
enable_locale "$LOCALE_LANG"
[[ "$LOCALE_REGIONAL" != "$LOCALE_LANG" ]] && enable_locale "$LOCALE_REGIONAL"
locale-gen
cat > /etc/locale.conf <<EOF
LANG=${LOCALE_LANG}
LC_TIME=${LOCALE_REGIONAL}
LC_MONETARY=${LOCALE_REGIONAL}
LC_PAPER=${LOCALE_REGIONAL}
LC_MEASUREMENT=${LOCALE_REGIONAL}
LC_NAME=${LOCALE_REGIONAL}
LC_ADDRESS=${LOCALE_REGIONAL}
LC_TELEPHONE=${LOCALE_REGIONAL}
EOF
ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime → Creates a symbolic link at /etc/localtime pointing to the timezone defined in config.sh. The timezone tells the system how to interpret and display local time, defining rules such as the offset relative to UTC (for example, UTC-3), daylight saving time, and historical timezone changes.
hwclock --systohc → Computers have two clocks: the RTC (Real-Time Clock) and the System Clock. The RTC is a small chip on the motherboard, powered by the battery, which keeps counting time even while the computer is off. The System Clock, in turn, is maintained by the kernel while the system is running. During boot, Linux initializes the System Clock from the RTC, and from that point on, it starts using only the system's clock. The hwclock --systohc command does the reverse: it writes the current System Clock time to the RTC. This ensures that, after adjusting the system time (for example, via NTP) or configuring the timezone, the hardware clock also stays synchronized for the next boot.
enable_locale "$LOCALE_LANG" and [[ "$LOCALE_REGIONAL" != "$LOCALE_LANG" ]] && enable_locale "$LOCALE_REGIONAL" → In the first line, enable_locale ensures that the locale defined in LOCALE_LANG is enabled in /etc/locale.gen. The second line compares LOCALE_REGIONAL and LOCALE_LANG; if they differ, the script runs enable_locale again, this time passing the regional locale. This is necessary because it's common to use a combination where the system's main language differs from the regional settings. For example, someone might prefer the system interface in English, but use Brazilian format for currency, dates, measurements, and other regional standards.
locale-gen → As explained earlier, locale tells the system how to interpret information such as currency, dates, numbers, among others. The /etc/locale.gen file only specifies which locales should be generated; the locale-gen command is the one actually responsible for generating them.
cat > /etc/locale.conf <<EOF ... → A natural question then arises: if we just generated the locales, why do we still need a locale.conf? The answer is that locale-gen only makes those locales available to the system. It's still necessary to choose which one will be used by default. That's exactly the role of /etc/locale.conf: to define the system's default locale (LANG) and, if desired, override specific settings through the LC_* variables, such as date format, currency, and units of measurement.
- And as I didn't mention earlier,
<<EOFmakes it possible to write the content in multiline form directly into the file. This feature is called a heredoc (here document). Everything between<<EOFand the finalEOFwill be interpreted as input for the previous command. In this case, the>operator redirects that content to/etc/locale.conf, creating or overwriting the file.
vconsole.conf, hostname, localhost, NetworkManager and root password
In the following section, the keyboard is configured for the TTYs (for example, when we use CTRL+ALT+F1, F2, etc. to open a virtual terminal). Next, network information and system identification are configured, such as the hostname and local resolution via /etc/hosts. Then, the NetworkManager service is enabled in systemd, allowing it to manage network connections during boot. Finally, the root user's password is changed.
log "Setting console keymap"
echo "KEYMAP=${KEYMAP_CONSOLE}" > /etc/vconsole.conf
log "Setting hostname"
echo "$HOSTNAME" > /etc/hostname
cat > /etc/hosts <<EOF
127.0.0.1 localhost
::1 localhost
127.0.1.1 ${HOSTNAME}.localdomain ${HOSTNAME}
EOF
log "Enabling NetworkManager"
systemctl enable NetworkManager
log "Setting root password"
echo "root:${ROOT_PASSWORD}" | chpasswd
echo "KEYMAP=${KEYMAP_CONSOLE}" > /etc/vconsole.conf → Writes to /etc/vconsole.conf the setting responsible for mapping the correct keyboard layout in the TTY, using the value of KEYMAP_CONSOLE declared in config.sh.
echo "$HOSTNAME" > /etc/hostname → Sets the machine's hostname, using the value declared in config.sh.
cat > /etc/hosts <<EOF ... → Configures the /etc/hosts file, responsible for mapping machine names to IP addresses locally. In this case, it associates the machine's hostname with the local address, allowing the system itself to resolve its own name without depending on a DNS server.
systemctl enable NetworkManager → Enables the NetworkManager service to start automatically along with the system. NetworkManager is responsible for managing network connections.
echo "root:${ROOT_PASSWORD}" | chpasswd → Changes the root user's password using the chpasswd command, which receives the new password through standard input and applies the change without needing interactive confirmation.
useradd, sudoers, wheel
This section refers to creating a user, including the sudoers.d directory (if not already included in the sudoers file), and enabling Arch's wheel group to allow users in this group to use the sudo command (running commands with root privileges).
wheel on Arch is a user group. Groups in Linux serve to apply permissions collectively. In the case of wheel, it traditionally means that users in this group have administrative privileges.
log "Creating user $USERNAME"
useradd -m -G "$USER_GROUPS" -s /bin/bash "$USERNAME"
echo "${USERNAME}:${USER_PASSWORD}" | chpasswd
log "Configuring sudo for wheel group"
if ! grep -qE '^[#@]includedir /etc/sudoers.d' /etc/sudoers; then
echo "@includedir /etc/sudoers.d" >> /etc/sudoers
fi
echo "%wheel ALL=(ALL:ALL) ALL" > /etc/sudoers.d/99-wheel
chmod 440 /etc/sudoers.d/99-wheel
if ! visudo -cf /etc/sudoers.d/99-wheel; then
err "Generated sudoers drop-in failed validation; removing it."
rm -f /etc/sudoers.d/99-wheel
exit 1
fi
useradd -m -G "$USER_GROUPS" -s /bin/bash "$USERNAME" → Creates a new user on the system.
-m→ Creates thehomedirectory-G→ Sets the groups the user will belong to (configured inconfig.sh).-s→ Sets the user's defaultshellas/bin/bash.
if ! grep -qE '^[#@]includedir /etc/sudoers.d' /etc/sudoers; then and echo "@includedir /etc/sudoers.d" >> /etc/sudoers → Checks whether the main sudo configuration file, /etc/sudoers, already has the reference to the /etc/sudoers.d/ directory. If this setting doesn't exist, it's added to the end of the file. This directive allows sudo to load additional configuration files present inside the /etc/sudoers.d/ directory, such as the 99-wheel file created later by the script.
!→ Inverts the result ofgrep, making the block run if the pattern is not found.^[#@]includedir /etc/sudoers.d→ Pattern searched for:^indicates the start of the line,[#@]indicates it can start with#or@, and the rest is the searched text.
echo "%wheel ALL=(ALL:ALL) ALL" > /etc/sudoers.d/99-wheel → Creates the 99-wheel file inside the /etc/sudoers.d/ directory containing a rule for the wheel group. The % symbol indicates that wheel is a group, and the rule allows any user belonging to this group to run any command using sudo, with the privileges of any user and group (including root).
%wheel→ All users belonging to thewheelgroup.ALL=...→ On any machine (relevant in environments with shared settings).(ALL:ALL)→ Can run commands as any user and group.ALL→ Can run any command.
chmod 440 /etc/sudoers.d/99-wheel → Grants read permission only to the file owner and group members. Other users have no access to the file. This is an important security directive, as it prevents unprivileged users from altering the sudo configuration and granting themselves administrative permissions.
if ! visudo -cf /etc/sudoers.d/99-wheel; then and rm -f /etc/sudoers.d/99-wheel → Validates whether the 99-wheel file has correct syntax. If validation fails, the file is removed and the script terminates, avoiding leaving an invalid sudo configuration.
visudo -cf→ Tool used to edit and validatesudoconfiguration files.-cchecks the syntax, without editing the file.-fspecifies the file to be validated.rm -f→ Removes the file without asking for confirmation (if the configuration is invalid).
GRUB
GRUB (GRand Unified Bootloader) is a bootloader, meaning its only function is to load the operating system.
GRUB is extremely important, as it bridges the machine's firmware (BIOS or UEFI) and the Linux kernel.
The typical flow when a computer turns on is as follows: Computer powers on → BIOS/UEFI → GRUB → Linux kernel → systemd (PID 1) → Operating system.
log "Installing GRUB ($BOOT_MODE)"
if [[ "$BOOT_MODE" == "uefi" ]]; then
if ! grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB; then
log "Standard GRUB install failed, retrying with --removable (NVRAM likely unavailable, e.g. some VM firmwares)"
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB --removable
fi
else
grub-install --target=i386-pc "$DISK"
fi
grub-mkconfig -o /boot/grub/grub.cfg
The script checks the value of BOOT_MODE. If it's uefi, it installs GRUB for UEFI systems, informing where the EFI partition is mounted (/boot). If this installation fails (something common on some virtual machines, where it's not possible to register a boot entry in NVRAM), the script retries using the --removable flag, which installs the bootloader at a standard path recognized by UEFI firmware.
If BOOT_MODE is not uefi, the script assumes the system was started in legacy BIOS mode (Legacy BIOS) and installs the corresponding version of GRUB directly onto the disk specified by DISK.
Finally, grub-mkconfig -o /boot/grub/grub.cfg generates the GRUB configuration file (grub.cfg), responsible for defining the boot menu entries and telling GRUB which kernel and initramfs should be loaded during boot.
Cinnamon LightDM, xorg keyboard
In this section, the script checks whether the chosen graphical environment was Cinnamon. If so, it enables LightDM (graphical login manager) to start automatically along with the system. Then, it creates the Xorg configuration directory (if it doesn't already exist) and writes a file containing the keyboard layout and model, using the values defined in config.sh.
if [[ "$DESKTOP_ENV" == "cinnamon" ]]; then
log "Enabling LightDM"
systemctl enable lightdm
mkdir -p /etc/X11/xorg.conf.d
cat > /etc/X11/xorg.conf.d/00-keyboard.conf <<EOF
Section "InputClass"
Identifier "system-keyboard"
MatchIsKeyboard "on"
Option "XkbLayout" "${KEYMAP_X11_LAYOUT}"
Option "XkbModel" "${KEYMAP_X11_MODEL}"
EndSection
EOF
fi
DETECTED_VIRT
Finally, the script checks whether it's running inside a virtual machine. Depending on the detected environment, it automatically enables the corresponding service: VirtualBox Guest Services for VirtualBox or QEMU Guest Agent for QEMU/KVM virtual machines. These services are responsible for enabling specific features of the detected virtualization platforms, ensuring the system works properly in that environment.
case "$DETECTED_VIRT" in
oracle)
log "Enabling VirtualBox guest services"
systemctl enable vboxservice
;;
kvm|qemu)
log "Enabling QEMU guest agent"
systemctl enable qemu-guest-agent
;;
esac
log "Chroot setup finished"
At the end, the script prints the message Chroot setup finished, indicating that all the configuration performed inside the chroot has been completed and the system is ready to be booted.
Final thoughts
These were definitely two huge posts, full of content. Although I had already manually installed Arch Linux dozens of times, the process of automating this installation through a script showed me several new layers and edge cases that need to be handled correctly.
Furthermore, understanding this entire process gave me a much deeper understanding of the fundamentals of operating systems and scripting. More than just learning to install a Linux distribution, this knowledge developed a skill transferable to several other areas of computing.
Now, a change I might make to this script soon is the possibility of creating home as a separate partition — that way I could switch distributions or reinstall the base system if needed without having to touch my data.
Anyway, the script gives me a very portable and efficient way to replicate my Arch environment on any computer, having to deal less with machines with different configurations, or having to frequently look up commands such as installing GRUB for UEFI or BIOS, or even the need to configure locale, wheel, etc.