6

My script for automated Arch Linux installation

 1 year ago
source link: https://www.lorenzobettini.it/2023/05/my-script-for-automated-arch-linux-installation/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

My script for automated Arch Linux installation

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:

arch-installation-script.png?resize=625%2C525&ssl=1

arch-installation-script-2.png?resize=625%2C525&ssl=1

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.

ssh -p 2522 [email protected]

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:

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”:

arch-installation-script-3.png?resize=625%2C441&ssl=1

The partitions must be manually formatted:

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:

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”:

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:

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”):

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:

./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:

#!/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):

#!/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
    echo "EFI_PARTITION  ${EFI_PARTITION}"
if [[ -z ${SWAP_PARTITION+y} ]]; then
    echo "SWAP_PARTITION is not defined"
    exit 1
    echo "SWAP_PARTITION ${SWAP_PARTITION}"
if [[ -z ${ROOT_PARTITION+y} ]]; then
    echo "ROOT_PARTITION is not defined"
    exit 1
    echo "ROOT_PARTITION ${ROOT_PARTITION}"
if [[ -z ${DATA_PARTITION+y} ]]; then
    echo "DATA_PARTITION is not defined"
    exit 1
    echo "DATA_PARTITION ${DATA_PARTITION}"
if [[ -z ${INST_HOSTNAME+y} ]]; then
    echo "INST_HOSTNAME is not defined"
    exit 1
    echo "INST_HOSTNAME ${INST_HOSTNAME}"

The script 01_mount-partitions.sh mounts the partitions and, for the main BTRFS partition, also creates the BTRFS subvolumes:

#!/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:

#!/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:

#!/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:

#!/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
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}

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):

#!/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”:

#!/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:

arch-chroot /mnt

And I set the password for my user:

passwd bettini

Then, I install KDE or GNOME (not both).

For KDE, I would run the following:

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:

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:

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.

Like this:

Loading...

Related


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK