In a previous post, I reported the procedure for installing Arch Linux. The procedure is basically the one shown in the official Arch Wiki.
After a few manual steps, this post will show my installation script for automatically installing Arch Linux. I took inspiration from https://github.com/ChrisTitusTech/ArchTitus, but, differently from that project, my script is NOT meant to be reusable. The script is heavily tailored to my needs. I describe it in this post in case it might inspire others to follow a similar approach 🙂
The script (which actually consists of several scripts called from the main script) is available here: https://github.com/LorenzoBettini/my-archlinux-install-script.
I’ll describe the script by demonstrating its use for installing Arch Linux on a virtual machine (VirtualBox). However, I use the script for my computers. Also, for real computers, I perform the installation via SSH from another computer for the same reasons I have already explained.
The virtual machine preparation is the same as in my previous post, so I’ll start from the already configured machine.
I start the virtual machine with the Arch ISO mounted:
Inside the live environment, the SSH server is already up and running. However, since we’ll connect with the root account (the only one present), we must give the root account a password. By default, it’s empty, and SSH will not allow you to log in with a blank password. Choose a password. This password is temporary; if you’re in a trusted local network, you can choose an easy one.
Then, I connect to the virtual machine via SSH.
1 |
ssh -p 2522 root@127.0.0.1 |
From now on, I’ll insert all the commands from a local terminal connected to the virtual machine.
Initial manual steps
First, I ensure the system clock is accurate by enabling network synchronization NTP:
1 |
timedatectl set-ntp true |
Then, I partition the disk according to my needs. My script heavily relies on this partitioning scheme consisting of four partitions:
- the one for booting in UEFI mode, formatted as FAT32, 300Mb (it should be enough for UEFI, but if unsure, go on with 512Mb)
- a swap partition, 20Gb (I have 16Gb, and if I want to enable hibernation, i.e., suspend to disk, that should be enough)
- a partition meant to host common data that I want to share among several Linux installations on the same machine (maybe I’ll blog about that in the future), formatted as EXT4, 30Gb
- the root partition, formatted as BTRFS, the rest of the disk
To do that, I’m using cfdisk, a textual partition manager, which I find easy to use. In the virtual machine, the disk is “/dev/sda”:
The partitions must be manually formatted:
1 2 3 4 |
mkfs.fat -F 32 /dev/sda1 mkswap /dev/sda2 mkfs.ext4 /dev/sda3 mkfs.btrfs /dev/sda4 |
Sometimes, I have problems with the keyring, so I first run the following commands that ensure the keyring is up-to-date:
1 2 3 4 |
killall gpg-agent rm -rf /etc/pacman.d/gnupg pacman-key --init pacman-key --populate archlinux |
I’m going to clone the installation script from GitHub, so I need to install “git”:
1 |
pacman -Sy git |
And now, I’m ready to use the installation script.
Running the installation script
First, I clone the installation script from GitHub:
1 2 |
git clone https://github.com/LorenzoBettini/my-archlinux-install-script.git cd my-archlinux-install-script |
The script has no parameter but relies on a few crucial environment variables to set appropriately. The first four variables refer to the partitions I created above. The last one is the name for the machine (in this example, it will be “arch-gnome”):
1 2 3 4 5 |
export EFI_PARTITION=/dev/sda1 export SWAP_PARTITION=/dev/sda2 export DATA_PARTITION=/dev/sda3 export ROOT_PARTITION=/dev/sda4 export INST_HOSTNAME=arch-gnome |
The script will check that all these variables are set. However, it does not check whether the specified partitions are correct, so I always double-check the environment variables.
And now, let’s run it:
1 |
./install.sh |
The script will do all the Arch Linux installation steps. These automatic steps correspond to the ones I showed in my previous post, where I ran them manually.
When the script finishes (it takes a few minutes), I have to perform a few additional manual operations before rebooting. I’ll detail these latter manual operations at the end of the post. In the next section, I’ll describe the script’s parts.
The installation script(s)
As I anticipated, the script actually consists of several scripts.
The main one, install.sh, is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#!/usr/bin/env bash set -x #echo on set -eo pipefail echo -ne " Starting... " ( ./00_check.sh )|& tee 00_check.log ( ./01_mount-partitions.sh )|& tee 01_mount-partitions.log ( ./02_pacstrap.sh )|& tee 02_pacstrap.log ( ./03_prepare-for-arch-chroot.sh )|& tee 03_prepare-for-arch-chroot.log ( arch-chroot /mnt /root/04_configuration.sh )|& tee 04_configuration.log ( arch-chroot /mnt /root/05_bootloader.sh )|& tee 05_bootloader.log ( arch-chroot /mnt /root/06_user.sh )|& tee 06_user.log mkdir -p /mnt/home/bettini/install-logs cp -v *.log /mnt/home/bettini/install-logs/ chown -R 1000:1000 /mnt/home/bettini/install-logs/ echo -ne " ...Finished! " |
Note that the installation logs are saved in the “bettini” user’s home directory (the last run script will create the user). These can be inspected later.
The main script calls the other scripts.
We have the script for checking that all the needed environment variables are set (00_check.sh):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#!/usr/bin/env bash set -x #echo on echo -ne " Check variables " if [[ -z ${EFI_PARTITION+y} ]]; then echo "EFI_PARTITION is not defined" exit 1 else echo "EFI_PARTITION ${EFI_PARTITION}" fi if [[ -z ${SWAP_PARTITION+y} ]]; then echo "SWAP_PARTITION is not defined" exit 1 else echo "SWAP_PARTITION ${SWAP_PARTITION}" fi if [[ -z ${ROOT_PARTITION+y} ]]; then echo "ROOT_PARTITION is not defined" exit 1 else echo "ROOT_PARTITION ${ROOT_PARTITION}" fi if [[ -z ${DATA_PARTITION+y} ]]; then echo "DATA_PARTITION is not defined" exit 1 else echo "DATA_PARTITION ${DATA_PARTITION}" fi if [[ -z ${INST_HOSTNAME+y} ]]; then echo "INST_HOSTNAME is not defined" exit 1 else echo "INST_HOSTNAME ${INST_HOSTNAME}" fi |
The script 01_mount-partitions.sh mounts the partitions and, for the main BTRFS partition, also creates the BTRFS subvolumes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#!/usr/bin/env bash set -x #echo on set -eo pipefail echo -ne " Enabling swap on ${SWAP_PARTITION} " swapon ${SWAP_PARTITION} echo -ne " BTRFS subvolumes on ${ROOT_PARTITION} " mount ${ROOT_PARTITION} /mnt btrfs su cr /mnt/@ btrfs su cr /mnt/@home btrfs su cr /mnt/@root btrfs su cr /mnt/@srv btrfs su cr /mnt/@cache btrfs su cr /mnt/@log btrfs su cr /mnt/@tmp umount /mnt echo -ne " Mounting all partitions " mount -o subvol=/@,defaults,nodiscard,noatime,compress=zstd ${ROOT_PARTITION} /mnt mount -o subvol=/@home,defaults,nodiscard,noatime,compress=zstd -m ${ROOT_PARTITION} /mnt/home mount -o subvol=/@root,defaults,nodiscard,noatime,compress=zstd -m ${ROOT_PARTITION} /mnt/root mount -o subvol=/@srv,defaults,nodiscard,noatime,compress=zstd -m ${ROOT_PARTITION} /mnt/srv mount -o subvol=/@cache,defaults,nodiscard,noatime,compress=zstd -m ${ROOT_PARTITION} /mnt/var/cache mount -o subvol=/@log,defaults,nodiscard,noatime,compress=zstd -m ${ROOT_PARTITION} /mnt/var/log mount -o subvol=/@tmp,defaults,nodiscard,noatime,compress=zstd -m ${ROOT_PARTITION} /mnt/var/tmp mount -o defaults,noatime -m ${EFI_PARTITION} /mnt/boot/efi mount -o defaults,noatime -m ${DATA_PARTITION} /mnt/media/bettini/common # Create /var/lib/machines and /var/lib/portables # So that systemd will not create them as nested subvolumes mkdir -p /mnt/var/lib/machines mkdir -p /mnt/var/lib/portables |
The script 02_pacstrap.sh performs the “pacstrap” (it also sets the mirrors) and generates the /etc/fstab:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#!/usr/bin/env bash set -x #echo on set -eo pipefail # to avoid failures of the shape # signature from "..." is invalid # File ... is corrupted (invalid or corrupted package (PGP signature)) pacman -S --noconfirm archlinux-keyring echo -ne " Setting mirrors " reflector \ --country Italy,Germany \ --age 12 \ --protocol https \ --fastest 5 \ --latest 20 \ --sort rate \ --save /etc/pacman.d/mirrorlist echo -ne " Updating package metadata " pacman -Syy echo -ne " pacstrap " pacstrap /mnt base linux linux-lts linux-firmware nano vim intel-ucode btrfs-progs sof-firmware alsa-firmware echo -ne " Generating fstab " genfstab -U /mnt >> /mnt/etc/fstab # remove subvolid to avoid problems with restoring snapper snapshots sed -i 's/subvolid=.*,//' /mnt/etc/fstab echo -ne " Showing fstab " cat /mnt/etc/fstab |
Then, 03_prepare-for-arch-chroot.sh prepares the script for arch-chroot: it copies all the shell scripts into the /mnt/root:
1 2 3 4 5 6 7 |
#!/usr/bin/env bash set -x #echo on set -eo pipefail cp -a *.sh /mnt/root/ |
In fact, by looking at the main script, you see that further shell scripts are executed using arch-chroot.
The script 04_configuration.sh takes care of all the configuration steps:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#!/usr/bin/env bash set -x #echo on set -eo pipefail # This is meant to be executed with arch-chroot /mnt # and it has to be copied inside /mnt first # example, after copying it to /mnt/root # arch-chroot /mnt /root/04_configuration.sh ln -sf /usr/share/zoneinfo/Europe/Rome /etc/localtime hwclock --systohc echo en_US.UTF-8 UTF-8 >> /etc/locale.gen echo it_IT.UTF-8 UTF-8 >> /etc/locale.gen locale-gen cat >> /etc/locale.conf << EOF LANG=en_US.UTF-8 LC_ADDRESS=it_IT.UTF-8 LC_IDENTIFICATION=it_IT.UTF-8 LC_MEASUREMENT=it_IT.UTF-8 LC_MONETARY=it_IT.UTF-8 LC_NAME=it_IT.UTF-8 LC_NUMERIC=it_IT.UTF-8 LC_PAPER=it_IT.UTF-8 LC_TELEPHONE=it_IT.UTF-8 LC_TIME=it_IT.UTF-8 EOF echo KEYMAP=it >> /etc/vconsole.conf echo ${INST_HOSTNAME} >> /etc/hostname cat >> /etc/hosts << EOF 127.0.0.1 localhost ::1 localhost 127.0.1.1 myhostname.localdomain ${INST_HOSTNAME} EOF |
Note the use of the environment variable INST_HOSTNAME for creating the file /etc/hosts. I’m using en_US.UTF-8 for the language, but other local configurations are for Italy.
The script 05_bootloader.sh configures and installs GRUB. It also configures GRUB for the “mem_sleep_default” parameter (for suspend) and for hibernation; in that respect, it also configures mkinitcpio accordingly (note the “resume” hook):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#!/usr/bin/env bash set -x #echo on set -eo pipefail # This is meant to be executed with arch-chroot /mnt # and it has to be copied inside /mnt first # example, after copying it to /mnt/root # arch-chroot /mnt /root/05_configuration.sh grub_resume_boot_option() { grub_swap_part=$(find /dev/disk/ | grep "$(awk '$3=="swap" {print $1; exit}' /etc/fstab | cut -d= -f 2)") echo "resume=$grub_swap_part" } pacman -S --noconfirm --needed grub efibootmgr base-devel linux-lts-headers networkmanager network-manager-applet sed -i 's/MODULES=(.*)/MODULES=(crc32c-intel btrfs)/' /etc/mkinitcpio.conf sed -i 's/HOOKS=(.*)/HOOKS=(base udev autodetect modconf block filesystems keyboard fsck resume)/' /etc/mkinitcpio.conf mkinitcpio -P sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 mem_sleep_default=deep"/' /etc/default/grub sed -i "/^GRUB_CMDLINE_LINUX_DEFAULT/ s~\"$~ $(grub_resume_boot_option)\"~g" /etc/default/grub grub-install --target=x86_64-efi --bootloader-id=Arch --efi-directory=/boot/efi grub-mkconfig -o /boot/grub/grub.cfg |
Note that it uses the generated /etc/fstab to retrieve the UUID of the swap partition.
Finally, the script 06_user.sh creates my user and configures it so that I can use “sudo”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#!/usr/bin/env bash set -x #echo on set -eo pipefail # This is meant to be executed with arch-chroot /mnt # and it has to be copied inside /mnt first # example, after copying it to /mnt/root # arch-chroot /mnt /root/06_user.sh useradd -m bettini usermod -aG wheel,sys,rfkill bettini sed -i 's/# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoers echo -ne " IMPORTANT: remember to set the password in the end! " chown bettini:bettini /media/bettini/common |
It also sets the right permissions for my user in the mount point where I want the shared partition.
That’s all. The script also prints a message to remind me to set the password for my user.
Final manual steps
I execute a few manual steps to finalize the installation when the script finishes.
First of all, I once again use arch-chroot:
1 |
arch-chroot /mnt |
And I set the password for my user:
1 |
passwd bettini |
Then, I install KDE or GNOME (not both).
For KDE, I would run the following:
1 2 3 4 |
pacman -S --noconfirm pipewire-media-session pipewire-jack xorg plasma plasma-wayland-session konsole dolphin kate firefox systemctl enable sddm.service systemctl enable NetworkManager.service |
For GNOME, I would run the following:
1 2 3 4 |
pacman -S --noconfirm pipewire-media-session pipewire-jack gnome systemctl enable gdm.service systemctl enable NetworkManager.service |
And that ends the installation.
I exit chroot and unmount /mnt:
1 2 |
exit umount -R /mnt |
As you see, most of the steps are performed by the script! 🙂
I can restart the system (in this example, the virtual machine) and enjoy the installed Arch!
That’s another reason why I love Arch Linux so much: the installation can be easily scripted!
It took me some time to finalize all the scripts, but using a virtual machine, especially with snapshots, wasn’t that hard. I encourage you to bake your installation script. It’ll be fun 🙂
By the way, before existing chroot and rebooting, I usually run my Ansible playbook for installing other programs (either KDE or GNOME) and configure the system and user according to my needs. I’ll blog about such a playbook in the future.
I enjoyed your blogs on Archlinux. To me Arch remains a puzzle that I play with but rarely understand why I do somethings. When I compare the Arch installation Wiki to anyone’s blog about their method I see additional packages that are not explained and make me question why they are there and do I need them. It’s the main reason I stick with Endeavous OS for Arch related projects so I can hide my head in the sand and ignore what I don’t see.
I also use EndeavourOS on most of my computers as my daily driver. I have to thank EndeavourOS for getting me in touch with Arch. Once you’re familiar with EndeavourOS, switching to Arch is not that hard 🙂