Being able to rebuild my Linux desktop from scratch quickly has been a big boost for me. I’ve found it easier to try out new things knowing that, worst case, I can always get back to a fresh, familiar installation within minutes.

NOTE: Everything here pertains specifically to my Macbook Pro Retina.

Check out the repository on Github.

Archiso

The archiso package contains scripts for creating live, bootable Arch Linux images. Install it:

sudo pacman -S archiso

Archiso gives you two options for starting points: releng and baseline. I suggest starting with baseline. Make a copy of it and use it to initialize your repository:

cp /usr/share/archiso/configs/releng ~/arch-installer
cd ~/arch-installer
git init
git commit -am "initial commit"

You’ll also want to add the build artifact directories to your .gitignore:

/work
/out

We’ll need git in our live environment, so add it to the packages.both file:

git

To test it out, run the ./build.sh script as root:

sudo ./build.sh -v

This will create an .iso in the /out subdirectory of the repo. Use dd to write this to a USB disk. For instance:

dd if=out/archlinux-2015.01.30-dual.iso of=/dev/sd_ bs=2M

Boot up with the resulting disk and you’ll be automatically dumped in to a zsh shell, as root, with git installed. Success!

Root Filesystem Overlay: /airootfs

The /airootfs (“Archiso root filesystem”) directory is overlayed on top of the root filesystem in the live environment. In other words, if you create a file /airootfs/root/test.txt, that file will appear as /root/test.txt upon boot.

Automatically Running a Script

Take a look at the /airootfs/root/.automated_script.sh file. When the live system boots, the root user is automatically logged on, and this script is automatically executed. The script looks at the options passed to the kernel (/proc/cmdline) and executes whatever is passed as the “script” parameter.

Add this parameter by modifying the last line in /efiboot/loader/entries/archiso-x86_64-cd.conf. It should look like this:

options archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% script=autorun.sh

Then create the autorun.sh script in the /airootfs/root directory:

#!/bin/bash
echo "hello, world"

Now, if you rebuild and boot from the ISO, it will automatically run the autorun.sh script and you’ll see “hello, world” output.

NOTE: you’ll need to clear out the /work directory before rebuilding.

Sensitive Files and Cached Packages

There are a some types of files that you’ll want to keep out of your repository:

  • Cached package files
  • SSH keys
  • PGP keys / trust database
  • Network profiles and wifi passwords

The approach I’ve found to work best is to copy these files into /airootfs and keep them out of the repo with a .gitignore. The downside is that you’ll need to create a new ISO whenever those files need to change, so it’s a good idea to script it.

My script does the following:

  1. Copies cached package files in /airootfs/root/packages to be copied to the pacmac package cache prior to installation.
  2. Copies sensitive files in /airootfs/root/rootfs-private to be overlayed on top of the installation filesystem.
  3. Copies the network profile and wireless firmware in place so that the wireless network can be brought up on boot.
  4. Copies hashed passwords from the shadowed password file to restore during installation.

Create a script called prepare_and_build.sh and commit it to the repository:

#!/bin/bash -x

cd `dirname $0`

cp -r /var/cache/pacman/pkg ./airootfs/root/

mkdir -p ./airootfs/root/rootfs-private

for i in \
  /home/chendry/.ssh \
  /home/chendry/.gnupg \
  /etc/postfix/sasl_passwd.db \
  /usr/lib/firmware/b43 \
  /etc/netctl/wlp4s0b1-Chad
do
  cp --parents -r $i ./airootfs/root/rootfs-private
done

cp --parents /etc/netctl/wlp4s0b1-Chad ./airootfs
cp --parents -r /usr/lib/firmware/b43 ./airootfs

sudo cat /etc/shadow | \
  grep -E '^(root|chendry):' | \
  awk -F: '{ print $1 ":" $2 }' > ./airootfs/root/passwords

sudo rm -rf ./work
sudo ./build.sh -v

Be sure to add the following to your .gitignore to make sure nothing private gets into your repo:

/airootfs/root/packages
/airootfs/root/rootfs-private
/airootfs/root/passwords
/airootfs/etc/netctl
/airootfs/usr/lib/firmware/b43

Finally, to have the network come up on boot, use systemctl to enable it at the end of /airootfs/root/customize_airootfs.sh:

netctl enable wlp4s0b1-Chad

Cloning and Running the Installation Scripts

The git repository actually serves two separate purposes:

  1. It builds the bootable ISO containing the live environment and private files, and
  2. It contains installation scripts and “public” files.

Once the live environment boots, it clones the repository to get the latest and greatest and runs the installation scripts contained within. This means that you can often simply make changes to the repository without having to rebuild and write the image.

Below is my autorun.sh. It waits for the network to become available, puts the SSH key needed to clone the repository in place, clones the repository, and runs all of the installation scripts in sequence:

#!/bin/bash

# wait for the network to come up
while true
do
  ping -c1 bitbucket.org &> /dev/null && break
done

# copy the ssh key used to authenticate to the repo
cp -r ./rootfs-private/home/chendry/.ssh .
chmod 600 .ssh/*

# clone the repository
git clone git@bitbucket.org:chendry/arch-installer.git

# run each installation script in sequence, logging the results
mkdir logs
cd arch-install/install-scripts
for i in *.sh
do
  echo $i
  bash -x $i &>> ~/logs/${i%.sh}.out
done

Installation Scripts

And finally, the installation scripts. These scripts do the bulk of the installation work. I currently have 15 scripts, each described below.

010-console.sh

This simply sets the keyboard layout and screen font so I can comfortably work in the live environment:

loadkeys dvorak
setfont sun12x22

020-partitions.sh

This creates and mounts the filesystem. I haven’t found a great way to automatically identify the device, and am currently using blkid to find the device file by the filesystem label. I’ve also tried looking it up by size using a combination of lsblk --raw, grep, and awk.

device=$( blkid -L Arch )
yes | mkfs.ext4 -L Arch $device
mount $device /mnt

030-packages.sh

Copy the cached package files in ~/root/packages in place and run pacstrap.

mkdir -p /mnt/var/cache/pacman/pkg
cp ~/packages/* /mnt/var/cache/pacman/pkg

rm -rf /var/cache/pacman/pkg
ln -s /mnt/var/cache/pacman/pkg /var/cache/pacman/pkg

pacstrap -c /mnt base base-devel \
  vim \
  dialog \
  wpa_supplicant \
  openssh \
  nettle \
  nvidia-libgl \
  xorg-server \
  xorg-xinit \
  xorg-xsetroot \
  xf86-video-nouveau \
  mesa \
  i3-wm \
  i3status \
  dmenu \
  rxvt-unicode \
  xorg-xrandr \
  xorg-xrdb \
  ttf-dejavu \
  opera \
  xcursor-themes \
  git \
  wget \
  pass \
  postgresql \
  postfix \
  archiso

040-bootloader.sh

Run mkinitcpio and install the bootloader. Again, we identify the EFI partition by label using blkid. Also, /etc/mkinitcpio.conf is updated to ensure that the nouveau driver is loader early so that the font loaded as part of vconsole.conf isn’t “overwritten” mid-boot.

cat <<-END | arch-chroot /mnt
sed -i 's/^MODULES=.*/MODULES="nouveau"/' /etc/mkinitcpio.conf
mkinitcpio -p linux
END

mkdir /efi
mount $( blkid -L EFI ) /efi

cp -v /mnt/boot/* /efi

gummiboot install --path=/efi

cat <<-END > /efi/loader/entries/arch.conf
title Arch Linux
linux /vmlinuz-linux
initrd /initramfs-linux.img
options root=PARTUUID=$( blkid -s PARTUUID -o value $( blkid -L Arch ) )
END

050-fstab.sh

Add the root partition to the target filesystem’s /etc/fstab:

genfstab -p /mnt >> /mnt/etc/fstab

060-timezone.sh

Set up the local timezone:

arch-chroot /mnt ln -sf /usr/share/zoneinfo/America/Chicago /etc/localtime

070-sshd

Enable the sshd service:

arch-chroot /mnt systemctl enable sshd.service

080-users

Add users and restore passwords using the passwords file created by prepare_and_build.sh:

arch-chroot /mnt useradd -m -G wheel,input -s /bin/bash chendry
cat ~/passwords | arch-chroot /mnt chpasswd -e

090-rootfs.sh

Copy both the public (added to repository) and private (existing only on installation media) filesystems to the target filesystem and fix permissions:

cp -Rv ~/rootfs-private/* /mnt
cp -Rv ../rootfs-public/* /mnt

cat <<-'END' | arch-chroot /mnt
chown -R chendry:chendry /home/chendry
chmod 600 /home/chendry/.ssh/*
chmod 644 /home/chendry/.ssh/*.pub
END

100-locale.sh

Generate locales. This uses the /etc/locale.gen file assumed to be copied on to the target filesystem as during the rootfs script:

arch-chroot /mnt locale-gen

110-chendry.sh

This does the work of setting up my personal account. It clones repositories for all of my projects, (/home/chendry/clone.sh comes from the private rootfs,) installs rbenv, pyenv, and nvm, unpacks ruby versions for rbenv, uses homesick to put my dotfiles in place, and clones my pass password database:

arch-chroot /mnt su -l chendry ./code/clone.sh

cat <<-'END' | arch-chroot /mnt su -l chendry
cd
git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
git clone https://github.com/yyuu/pyenv.git ~/.pyenv

export PATH=$HOME/.rbenv/bin:$PATH
eval "$(rbenv init -)"

curl https://s3.amazonaws.com/chendry/rubies/arch/2.0.0-p247.tar.bz | tar -jxf - -C ~/.rbenv/versions
curl https://s3.amazonaws.com/chendry/rubies/arch/2.2.0.tar.bz | tar -jxf - -C ~/.rbenv/versions
rbenv global 2.2.0

gem install homesick --no-rdoc --no-ri
rbenv rehash

homesick clone git@bitbucket.org:chendry/dotfiles-arch.git
homesick link dotfiles-arch --force

git clone https://github.com/creationix/nvm.git ~/.nvm
cd ~/.nvm
git checkout `git describe --abbrev=0 --tags`
END

cat <<-'END' | arch-chroot /mnt su -l chendry
git clone git@bitbucket.org:chendry/pass ~/.password-store
END

120-xf86-input-mtrack.sh

Download, compile, and install the xf86-input-mtrack trackpad driver and dispad (disable trackpad while typing) from the AUR:

install_aur() {
  filename=$( basename $1 )
  dirname=${filename%.tar.gz}

  cat <<-END | arch-chroot /mnt su -l chendry
    wget $1
    tar -zxvf $filename
    cd $dirname
    makepkg -s --noconfirm
END

  cat <<-END | arch-chroot /mnt
    pacman --noconfirm -U ~chendry/${dirname}/*.tar.xz
    rm -rf ~chendry/${dirname}*
END
}

install_aur 'https://aur.archlinux.org/packages/xf/xf86-input-mtrack-git/xf86-input-mtrack-git.tar.gz'
install_aur 'https://aur.archlinux.org/packages/di/dispad-git/dispad-git.tar.gz

130-postgres.sh

Install postgres:

cat <<-END | arch-chroot /mnt
su -l postgres -c "initdb --locale en_US.UTF-8 -D '/var/lib/postgres/data'"
systemctl enable postgresql
END

140-postfix.sh

Install and configure postfix:

cat <<-END >> /mnt/etc/postfix/main.cf
relayhost = email-smtp.us-east-1.amazonaws.com:587
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_note_starttls_offer = yes
END

chmod 600 /mnt/etc/postfix/sasl_passwd.db

sudo arch-chroot /mnt systemctl enable postfix

150-wifi.sh

And finally copy over the wireless firmware and enable my wireless network:

cp -r /lib/firmware/b43 /mnt/lib/firmware
arch-chroot /mnt netctl enable wlp4s0b1-Chad