22 minute read

This article sums up why and how I run Arch Linux on my new Framework Laptop 13, which I received on the 3rd of this month.

I’ve been busy getting up+running and getting to know the device. TL;DR; I’m extremely impressed and very happy with this laptop - it is by far one of the best devices I’ve owned in a long time. It is near-silent, It uses ~3watts at idle, it’s fast, it’s sturdy and it looks good!

This is going to be quite a long article, so here’s a Table of Contents:

Why Arch Linux

Prior to my Framework Laptop adventures, I’ve been planning to move to Arch Linux for a while. I’m a long-time Linux desktop user. I started out with those Red Hat CD-ROMs you’d buy at your local bookshop (this was around ‘96). I fooled around with SCO UnixWare, had a whole period of SGI IRIX after that and then some distro-hopping to Debian, Fedora, Arch Linux (in 2007), Gentoo and Void Linux, more-or-less in that order.

The last two-ish years I’ve been running on Void Linux, which I still strongly recommend and love - it’s a really good distribution with a nice balance between stability and simplicity.

I used the OS package manager for the essentials, like Gnome, Firefox, the terminal, Wayland and X11, and used /opt like a sort of Program Files or Applications directory where I had my own programs like IntelliJ, Postman, PostgreSQL, etc.

This works relatively well: you don’t have a lot of demands on package availability in the distro itself and you keep things stable - only update your own stuff when you feel like it and/or when you need to.

At a certain point I had about 70 applications in there which became a burden to keep up to date, so I set out to find a distribution that packages as much as possible of the software I use, and where packaging it yourself is as simple as possible.

I definitely prefer a rolling-release distro. I checked out Nix, Gentoo and Arch Linux. Nix I found really appealing (hard declarative) but I hated the syntax (maybe Guix one day? I like Scheme better).

Gentoo did have a comparable set of packaged software available, and it is one of my favorite distributions, but I had too many issues on my test-build virtual machine which blocked me from experimenting prior to running it on my daily driver.

So why move to Arch? Well, in one word: mind-share. The wiki, the amount of packaged software, sane package standards:

  • The Arch wiki has essentially become the de-facto standard Linux wiki
  • Really huge package collection, fantastic community participation with AUR
  • Upstream stable usually means an upstream update is in repos within minutes
  • Does not have separate dev/devel packages for headers

The above three things means a lot of convenience to a Linux desktop user. On Arch Linux, 99.9% of the software I ever used on Linux is packaged, most of it in the official repositories and a few in AUR. Next to that, AUR and PKGBUILD are so easy to get into, so you can easily package things yourself and share it with the community.

On most other distros the gap between what’s packaged and what isn’t is quite a bit larger which means you need alternative ways to get that software and keep it up-to-date, which in practice becomes error-prone and time-consuming.

On Arch Linux I made the choice to run everything packaged with pacman. If I need something that isn’t packaged yet (currently the case for 5 packages of which I packaged one already), I package it and publish it to AUR.

The rest of this article is essentially a how-to with notes about what I run and install. Feel free to peruse and/or replay!

BIOS settings

You can find the BIOS guide on the Framework community website. The only settings I changed were:

  • CPU Configuration -> Boot Performance mode: MAX BATTERY
  • CPU Configuration -> Intel Turbo Boost Max Technology 3.0: DISABLED
  • Secure Boot -> Enforce Secure Boot: DISABLED

The above causes the laptop to effectively run in a lower TDP setting. I think it goes from 28watts to 22watts. This has a dramatic effect on the battery life and thermals of the device. I don’t want a wild beast that blasts the fans as soon as I type ls, so these settings are great for me. Did I mention the laptop performs really great with these settings?

With the above and the settings I configure in TLP I get ~3watts idle with screen on normal brightness, Wi-Fi and Bluetooth enabled, and about ~45 celsius core temperatures.

Basic installation

Basically, download the ISO and follow the installation guide. I opted to use systemd-boot as boot manager and NetworkManager as my network configuration tooling.

Additional kernel parameters I use

Whichever boot manager you use, you might want to set a few extra kernel parameters. I found the following additionals handle a bunch of stuff nicely on my Framework Laptop 13:

net.ifnames=0 libata.allow_tpm=1 module_blacklist=cros_ec_lpcs,hid_sensor_hub acpi_osi="!Windows 2020" tpm_tis.interrupts=0 nvme.noacpi=1 mem_sleep_default=s2idle

Packages I install

Next to the default core and extra repositories, I enable multilib in pacman.conf before installing. Additionally, I blacklist pam_systemd_home.so because I don’t use systemd-homed and it spams the journal on any login or auth action:

--- pacman.conf.orig    2023-08-06 12:09:44.849855338 +0300
+++ pacman.conf 2023-08-06 12:01:30.261984035 +0300
@@ -26,7 +26,7 @@
 #IgnoreGroup =

 #NoUpgrade   =
-#NoExtract   =
+NoExtract    = usr/lib/security/pam_systemd_home.so

 # Misc options
@@ -87,12 +87,11 @@
 #Include = /etc/pacman.d/mirrorlist

-#Include = /etc/pacman.d/mirrorlist
+Include = /etc/pacman.d/mirrorlist

You can install the following packages right after your system comes up first boot, or you could do it during install with pacstrap (it doesn’t really matter, but in any case, make sure core, contrib and multilib are enabled in /etc/pacman.conf first):

pacman -S --needed acpi acpi_call-dkms acpid alsa-utils ansible-language-server ant ardour audacious autoconf automake aws-cli bash bash-language-server bc bind bison blender bookworm btop bubblewrap cabal-install calibre cameractrls carla cdemu-daemon cdemu-client cheese cifs-utils clang cmake corkscrew cpio cpupower cue cups curl dagger dash dcraw deno desmume devtools discord distrobox dive dmidecode dmraid dnsmasq docker docker-buildx docker-compose dos2unix dosfstools dotnet-sdk efibootmgr elixir emacs-wayland eog erlang ethtool evince extra-cmake-modules exfatprogs fakeroot fd file file-roller firefox flex foomatic-db-engine foomatic-db-nonfree-ppds foomatic-db-ppds fop fractal fs-uae fs-uae-launcher furnace fwupd fzf gamemode gcc gdb gdm ghc ghidra gimp git glab gnome-backgrounds gnome-browser-connector gnome-calculator gnome-characters gnome-control-center gnome-session gnome-settings-daemon gnome-shell gnome-shell-extensions gnome-system-monitor gnome-tweaks gnupg go gopls gparted gradle groovy guile gvfs gvfs-gphoto2 gvfs-mtp gvfs-nfs gvfs-smb gvim harfbuzz-cairo haskell-language-server hdparm helm hplip htop i2c-tools ifuse inkscape iperf3 iptables-nft irssi jdk17-openjdk jdk8-openjdk jfsutils jq k9s kafka kubectl libcroco libindicator-gtk3 libreoffice-still libretro-beetle-pce libretro-beetle-psx-hw libretro-core-info libretro-desmume libretro-dolphin libretro-duckstation libretro-flycast libretro-mame libretro-mgba libretro-mupen64plus-next libretro-nestopia libretro-pcsx2 libretro-picodrive libretro-ppsspp libretro-sameboy libretro-scummvm libretro-snes9x libretro-yabause libva-mesa-driver libva-utils libvirt libvisual libxcrypt-compat linux-headers lldb lm_sensors lm_sensors lshw lsof ltrace lua-language-server make mame mame-tools man-db man-pages mattermost-desktop maven mbedtls2 mednafen mesa-utils mesa-vdpau mgba-qt minikube mitmproxy mono mono-msbuild moreutils mplayer mpv mtools multipath-tools mupen64plus mupdf-tools mutter nasm nautilus neofetch netbeans network-manager-applet networkmanager networkmanager-openvpn nfs-utils ninja nmap nodejs npm ntfs-3g nuget nvchecker nvme-cli nvtop openbsd-netcat openldap openssh openvpn p7zip pacutils pandoc-cli patch pavucontrol pciutils perl perl-net-dbus perl-x11-protocol pinentry piper pipewire pipewire-alsa pipewire-jack pipewire-pulse pipewire-v4l2 pkgconf postgresql powertop ppsspp pyright python python-kubernetes python-ldap python-opengl python-pip python-pycryptodomex python-pyopenssl python-setuptools python-websockets python-wheel qbittorrent qemu-full qjackctl qmc2 qpwgraph qt5-declarative qt5-tools qt5-wayland qt5-webchannel qt5-webengine qt5ct qt6-multimedia-ffmpeg qt6-tools qt6-wayland qt6ct quodlibet rabbitmq racket retroarch retroarch-assets-ozone retroarch-assets-glui ripgrep rsync ruby ruby-rake-compiler rust s-tui samba sane sbt scons screen scummvm sdl2_mixer signal-desktop smartmontools smbclient snes9x speedtest-cli squashfs-tools stack steam step-ca step-cli stern strace sudo syncthing tar texlive-bin texlive-core the_silver_searcher thunderbird tlp tmux traceroute tracker3-miners tree ttf-joypixels typescript typescript-language-server udftools unixodbc unzip urlwatch usbutils util-linux v4l-utils valgrind vdpauinfo vhba-module-dkms virt-manager vlc vulkan-tools wake wgetpaste wireless_tools wireplumber wireshark-cli wireshark-qt wmctrl wol xchm xclip xdg-desktop-portal-gnome xdg-user-dirs-gtk xdg-utils xdotool xfsprogs xorg-font-util xorg-fonts-100dpi xorg-mkfontscale xorg-server xorg-server-devel xorg-xauth xorg-xdpyinfo xorg-xdriinfo xorg-xev xorg-xfontsel xorg-xhost xorg-xinit xorg-xinput xorg-xkill xorg-xprop xorg-xrandr xorg-xrdb xorg-xset xorg-xsetroot xorg-xvinfo xorg-xwayland xorg-xwininfo xsane xsane-gimp xterm yarn yasm yq yt-dlp yuzu zig zip zls zstd

Additionally install when you want to use dracut instead of mkinitcpio

I use dracut on my desktop computer, because I need working LVM RAID mirrors there. I couldn’t get that to work with mkinitcpio based initrd.

If you want to use dracut instead of mkinitcpio to generate initrd images, you need to install it and remove the default initrd tooling:

pacman -S --needed dracut
pacman -R mkinitcpio mkinitcpio-busybox

Note that there are no hooks by default for rebuilding dracut-based initrd images, so you’d need to do that manually after linux kernel package updates. For example:

# Kernel version in package contains dot, on-disk it contains dash, hence the sed.
export KERNEL_VERSION=$(pacman -Q linux | awk '{print $2}' | sed 's|\.arch|-arch|g')
cp /lib/modules/$KERNEL_VERSION/vmlinuz /boot/vmlinuz-linux
dracut --kver $KERNEL_VERSION --force /boot/initramfs-linux-fallback.img
dracut --hostonly --no-hostonly-cmdline --kver $KERNEL_VERSION --force /boot/initramfs-linux.img

Additionally install when you use X11 instead of Wayland and want gestures

Wayland provides working gestures on Gnome out of the box, and Wayland is the default on Arch Linux. If, however, you have some reason to use X11 instead of Wayland by default and you want gestures to work, you can install Touchégg:

pacman -S --needed touchegg

Make sure you enable the service:

systemctl enable --now touchegg

And also install the required Gnome extension.

Additionally install on devices with fingerprint reader

Devices like the Framework Laptop 13 have a fingerprint reader. If you want to use it, make sure to install the following packages:

pacman -S --needed libfprint fprintd

And make sure you enable the service:

systemctl enable --now fprintd

Note that the latest Goodix MOC fingerprint readers which the Framework Laptop 13 uses, use a firmware that is not compatible with Linux. For more information, see here.

Apparently, when one installs the Windows drivers for these things, the Windows driver actually downgrades the firmware to a version that works with Linux..

Installing Windows 10 in a virtual machine and passing through the Fingerprint USB device and then installing the Windows drivers, downgrades the firwmware, after which the device is usable in Linux.

If your fingerprint reader is working, you can continue to follow the steps here to set it up for usage.

Additionally install on devices with Intel graphics

My Framework Laptop 13 is intel-based, so I install these packages additionally:

pacman -S --needed intel-gpu-tools vulkan-intel intel-media-driver libvdpau-va-gl

Additionally install on devices with AMD graphics

If you have an AMD device instead, you might want these ones:

pacman -S --needed radeontop vulkan-radeon

Additionally install on devices with NVidia graphics

If you have an NVidia device instead, you might want these ones:

pacman -S --needed nvidia-utils nvidia-dkms

Installing lib32 package equivalents (optional)

I like to install the lib32-* package equivalents of packages I installed. There isn’t an easy way to do this and it is a bit messy, but here’s how I do it:

# Clean beginnings.
mkdir -p ~/Desktop
rm ~/Desktop/lib32-candidates ~/Desktop/lib32-notfound

# Append a list of possible lib32-* package names by prepending to package name.
for p in `pacman -Qq | grep -v lib32`; do echo lib32-$p >> ~/Desktop/lib32-candidates; done

# Append a list of possible lib32-* package names which consist of lib32-firstnamepart.
for p in `pacman -Qq | grep -v lib32 | cut -d- -f1 | sort -u`; do echo lib32-$p >> ~/Desktop/lib32-candidates; done

# Remove known-not-working elements and make sure the output file is sorted and unique.
cat ~/Desktop/lib32-candidates | grep -v rustup | grep -v openssl-1.1 | grep -v mesa-amber | grep -v mesa-demos | sort -u -o ~/Desktop/lib32-candidates

# Abuse pacman -S to obtain invalid package names.
pacman -S --needed `cat ~/Desktop/lib32-candidates | sort -u` 2>&1 | grep 'error: target not found' | awk '{print $5}' > ~/Desktop/lib32-notfound

# Make sure that's sorted and unique too.
sort -o ~/Desktop/lib32-notfound ~/Desktop/lib32-notfound

# Use comm to diff the lists and only feed valid package names to pacman -S.
pacman -S --needed `comm -23 ~/Desktop/lib32-candidates ~/Desktop/lib32-notfound`

Service configuration

I use and customized a bunch of services on my device. So I don’t forget what I customized and why, let me document it.


I don’t want any service auto-configuring stuff on my system, especially things like printers for example. Therefore I disable the Avahi zeroconf service:

systemctl mask avahi-daemon.service
systemctl mask avahi-daemon.socket
systemctl mask avahi-dnsconfd.service


Enable bluetooth with:

systemctl enable --now bluetooth.service

Bluetooth mostly just works out of the box, except for my XBox Series X|S Wireless Game Controller. To get this to run, I use the xpadneo-dkms AUR package. Additionally, I need to configure a few settings in /etc/bluetooth/main.conf (add or set these settings yourself, or use patch to apply the settings to your main.conf):

--- main.conf.orig  2023-07-31 19:01:29.473651656 +0300
+++ main.conf 2023-08-01 23:39:23.294653784 +0300
@@ -49,7 +49,7 @@
 # Restricts all controllers to the specified transport. Default value
 # is "dual", i.e. both BR/EDR and LE enabled (when supported by the HW).
 # Possible values: "dual", "bredr", "le"
-#ControllerMode = dual
+ControllerMode = dual

 # Maximum number of controllers allowed to be exposed to the system.
 # Default=0 (unlimited)
@@ -100,7 +100,7 @@
 # Specify the policy to the JUST-WORKS repairing initiated by peer
 # Possible values: "never", "confirm", "always"
 # Defaults to "never"
-#JustWorksRepairing = never
+JustWorksRepairing = confirm

 # How long to keep temporary devices around
 # The value is in seconds. Default is 30.
@@ -212,9 +212,9 @@

 # LE default connection parameters.  These values are superceeded by any
 # specific values provided via the Load Connection Parameters interface

@@ -318,7 +318,7 @@
 # AutoEnable defines option to enable all controllers when they are found.
 # This includes adapters present on start as well as adapters that are plugged
 # in later on. Defaults to 'true'.

 # Audio devices that were disconnected due to suspend will be reconnected on
 # resume. ResumeDelay determines the delay between when the controller

After changes, restart the service:

systemctl restart bluetooth.service


Enable gdm with:

systemctl enable --now gdm.service

I prefer auto-login on some of my devices (no on laptop, yes on desktop). Add the two lines or apply the below diff to /etc/gdm/custom.conf using patch if you would like your user to allow GDM to automatically login (don’t forget to replace $USER with your user name):

--- custom.conf.orig  2023-07-31 19:08:35.307832755 +0300
+++ custom.conf 2023-07-31 19:08:25.741219958 +0300
@@ -1,6 +1,8 @@
 # GDM configuration storage

 # Uncomment the line below to force the login screen to use Xorg

Reboot for the above to take effect.


Enable libvirtd with:

systemctl enable --now libvirtd.service

I run libvirtd mostly stock. I do set unix_sock_group to libvirt and add myself to the libvirt group. I then set unix_sock_ro_perms, unix_sock_rw_perms and unix_sock_admin_perms to 0770 (Meaning, the owner and group can read, write and execute, everybody else can do nothing).

unix_sock_group = "libvirt"
unix_sock_ro_perms = "0770"
unix_sock_rw_perms = "0770"
unix_sock_admin_perms = "0770"

You need to change this for a whole lot of files under /etc/libvirt. Not doing this causes problems when connecting with your user instead of root using the virsh or virt-manager clients. don’t forget to restart the service after changes:

systemctl restart libvirtd.service

Furthermore, I configure the virt0 interface of the default NAT-enabled network to have a specific IP address ( and range (note: needs to be run after starting/restarting libvirtd):

export UUID=$(uuidgen)
cat <<EOF > "/tmp/net-default.xml"
  <forward mode='nat'>
      <port start='1024' end='65535'/>
  <bridge name='virt0' stp='off' delay='0'/>
  <ip address='' netmask=''>
      <range start='' end=''/>

virsh net-destroy default
virsh net-define /tmp/net-default.xml
virsh net start default
rm -qf /tmp/net-default.xml


Enable NetworkManager with:

systemctl enable --now NetworkManager.service

NetworkManager works mostly out of the box, normally no special settings needed.

However I did run into an issue connecting with older VPN environments related to OpenSSL 3.x disabling various legacy encapsulation and connection modes by default. The error you would see in such a case is:

Jul 31 20:26:38 FRAME nm-openvpn[58956]: OpenSSL: error:11800071:PKCS12 routines::mac verify failure
Jul 31 20:26:38 FRAME nm-openvpn[58956]: OpenSSL: error:0308010C:digital envelope routines::unsupported
Jul 31 20:26:38 FRAME nm-openvpn[58956]: Decoding PKCS12 failed. Probably wrong password or unsupported/legacy encryption

Furthermore, when you are behind corporate proxies, you might also have difficulties passing through the corporate proxy without the settings UnsafeLegacyRenegotiation and UnsafeLegacyServerConnect (which were allowed by default on OpenSSL 1.x).

To work-around these issues, I place a custom /etc/ssl/openssl.cnf:

cp -n "/etc/ssl/openssl.cnf" "/etc/ssl/openssl.cnf.orig"
cat <<EOF > "/etc/ssl/openssl.cnf"
HOME = .
openssl_conf = openssl_init

providers = provider_sect
ssl_conf = ssl_sect

default = default_sect
legacy = legacy_sect

activate = 1

activate = 1

system_default = system_default_sect

Options = UnsafeLegacyRenegotiation,UnsafeLegacyServerConnect

Note that I would only do the above if you need to interact with some old legacy VPN stuff, or if you’re behind moron-grade SSL-terminating proxies.


Enable nfsv4 with:

systemctl enable --now nfsv4-server.service

Quick note: the above start may fail if you updated the linux package but did not reboot yet, you’ll see an error about a dependency failure.

I use NFS only on internal interfaces, specifically the virt0 interface of the default network (remember that ip address This allows me to work with shared storage on other operating systems that I fool around with on Qemu/KVM (Note that you have much better options for modern Linux systems - there you can use enable shared memory and a virtiofs device to essentially loop-mount a memory block device which is a directory on the host).

Since we’re only doing NFSv4, and we’re not interested in user/group ID mapping, let’s stop and mask a couple of RPC services first:

systemctl stop rpcbind.service
systemctl mask rpcbind.service
systemctl stop nfs-blkmap
systemctl mask nfs-blkmap
systemctl stop nfs-idmapd
systemctl mask nfs-idmapd
systemctl stop nfs-mountd
systemctl mask nfs-mountd

To make NFSv4 only listen on a specific interface, and to disable version 3 of the protocol explicitly, we patch /etc/nfs.conf (use patch or add the host=, vers3= and vers4= elements by hand under [nfsd]:

--- nfs.conf.orig       2023-07-31 21:16:20.438028044 +0300
+++ nfs.conf    2023-08-03 09:01:05.586457300 +0300
@@ -67,13 +67,14 @@
 # debug=0
 # threads=8
-# host=
 # port=0
 # grace-time=90
 # lease-time=90
 # udp=n
 # tcp=y
-# vers3=y
-# vers4=y
 # vers4.0=y
 # vers4.1=y
 # vers4.2=y

Create an exports for /home:

cp -n "/etc/exports.d/home.exports" "/etc/exports.d/home.exports.orig"
cat <<EOF > "/etc/exports.d/home.exports"

After the above changes, restart the service:

systemctl restart nfsv4-server.service


Enable smbd and nmbd with:

systemctl enable --now nmb.service smb.service

I use Samba for the same reasons as I use NFS, that is to have shared storage on various older virtual machines (like Windows NT 4.0). Let’s create an smb.conf file:

cat <<EOF > "/etc/samba/smb.conf"
   workgroup = WORKGROUP
   netbios name = $(hostname | tr 'a-z' 'A-Z' | cut -d. -f1)
   server string =
   server role = standalone server
   server min protocol = NT1
   ntlm auth = yes
   lanman auth = yes
   hosts allow = 10.10.11.
   log file = /var/log/samba/log.smbd
   max log size = 100
   interfaces =
   bind interfaces only = yes
   dns proxy = yes
   wins proxy = yes
   wins support = yes
   local master = yes
   domain master = yes
   preferred master = yes
   os level = 33

   comment = Home Directories
   acl allow execute always = True
   browsable = yes
   writable = yes
   valid users = %U
   create mask = 0644
   directory mask = 0755

After creating or changing smb.conf, restart the services:

systemctl restart nmb.service smb.service


Enable tlp with:

systemctl enable --now tlp.service

TLP is used to manage power-saving modes of various hardware. It is usually configured to enable power-saving when not connected to AC, and to disable it when connected to AC. It does that for all kinds of things, like Wi-Fi, USB, PCIe, Bluetooth, the CPU scheduler, etc.

I’ve made a custom TLP configuration for my Framework Laptop 13 which you can install as follows:

cat <<EOF > "/etc/tlp.d/01-custom.conf"







After the above, you need to restart the service:

systemctl restart tlp.service


Enable cups with:

systemctl enable --now cups.socket

I also don’t want cups via cups-browsed to be able to auto-add printers, so I patch /etc/cups/cups-browsed.conf as follows:

--- cups-browsed.conf.default	2023-08-29 09:57:47.157250003 +0200
+++ cups-browsed.conf	2023-08-29 09:58:45.054108764 +0200
@@ -53,7 +53,7 @@
 # BrowseLocalProtocols.
 # Can use DNSSD and/or CUPS and/or LDAP, or 'none' for neither.

-# BrowseProtocols none
+BrowseProtocols none

 # Only browse remote printers (via DNS-SD or CUPS browsing) from


Enable bluetooth with:

systemctl enable --now docker.socket

I use docker without any adjustments.


I use cdemu and related vhba kernel module to emulate optical drives for use with emulation stuff. If you’d like to run CDemu, make sure the required modules are loaded at boot:

# Load modules at boot.
cat <<EOF > "/etc/modules-load.d/cdemu.conf"

# Do it now too.
modprobe -a sg sr_mod vhba

Enabling user services

I use the following user-level services (do as logged in user):

for u in syncthing.service wireplumber.service pipewire.socket pipewire-pulse.socket; do systemctl enable --now --user $u; done

How I use AUR

All the hip young things are running yay these days, but I like to do it the bare-hands way. That way I have more feeling with what’s going on with AUR packages.

Setting up your own custom local repository

First, let’s create a custom repository source for pacman. Note that I sign my packages using my GnuPG key, so the following assumes that. If you don’t want to sign your own packages, you need to change the SigLevel setting in /etc/pacman.conf for your custom repository and you need to tell makepkg not to sign your built package.

Also note that my package build root location is specific to my needs; feel free to change it to anything you like.

# Set package build root location.
export PKG_ROOT="$HOME/Syncthing/Packaging/Arch"

# Create a directory structure for Arch packaging.
mkdir -p "$PKG_ROOT"
cd "$PKG_ROOT"
install -d Build Packages 'Source Packages' Sources -o $USER

# Create a repository database.
cd "$PKG_ROOT/Packages"
repo-add custom.db.tar.gz

# Add entry to /etc/pacman.conf.
cat <<EOF >> "/etc/pacman.conf"
SigLevel = Required DatabaseRequired TrustedOnly
Server = file://$PKG_ROOT/Packages

# Add your public key to pacman keychain and set trust (assuming key matches $USER).
gpg --export --armor $USER > your.key
pacman-key --add your.key
pacman-key --lsign-key $USER
rm -qf your.key

# Run update for db and files, you should see custom being referenced.
pacman -Syu
pacman -Fy

# Unset variables.
unset PKG_ROOT

Configuring for package building

Now we configure makepkg defaults. Make sure you configure the PKG_ROOT, GPG_PUBKEY and PACKAGER environment variables to your specific likings:

# Set package build root location, gpgpubkey-id and packager string.
export PKG_ROOT="$HOME/Syncthing/Packaging/Arch"
export GPG_PUBKEY='89E5EB2541BC6668A9C165D424BD51CD12534CE6'
export PACKAGER='Rubin Simons <me@rubin55.org>'

cat <<EOF > "$HOME/.makepkg.conf"
# ~/.makepkg.conf.
BUILDENV=(!distcc color !ccache check sign)
SRCPKGDEST="$PKG_ROOT/Source Packages"

# Unset variables.

Example interactions using AUR

Here are a few example interactions with package fetching, building, repository updating and package removal to get you started:

# Set package build root location.
export PKG_ROOT="$HOME/Syncthing/Packaging/Arch"

# Get an AUR package.
cd "$PKG_ROOT/Build"
git clone https://aur.archlinux.org/aurutils.git

# Build an AUR package.
cd aurutils
makepkg -cCs
ls ../../Packages/aurutils*

# Show information about a built package.
cd "$PKG_ROOT/Packages"
pacman -Qpi aurutils-*-any.pkg.tar.zst

# Build a source package.
cd aurutils
makepkg -cCsS
ls ../../Source\ Packages

# Update local repository (adds new packages, removes older ones).
cd "$PKG_ROOT/Packages"
repo-add -n -R -s custom.db.tar.gz *.zst

# Remove a specific AUR package from local repository
cd "$PKG_ROOT/Packages"
repo-remove -s custom.db.tar.gz aurutils

AUR packages I build and install

So now to fill that $PKG_ROOT/Build directory with packages from AUR so we can build some stuff and put it in our own repository:

# Set package build root location.
export PKG_ROOT="$HOME/Syncthing/Packaging/Arch"

# Git clone them all.
cd "$PKG_ROOT/Build"
for p in akku ares-emu attract-git aurutils authy azure-cli chez-scheme conan cubeb dolphin-emu-git dosbox-x duckstation-git earthly eclipse-java elixir elixir-ls erlang_ls exercism ff flycast godot-mono-bin google-cloud-cli groovy-language-server-git ibmcloud-cli icaclient imhex irccloud-bin jdk17-graalvm-bin jdk17-jetbrains-bin jdk17-openj9-bin jdk21-jetbrains-bin jdtls jetbrains-toolbox krew kubelogin lagrange libretro-beetle-lynx-git libretro-beetle-pcfx-git libretro-bluemsx-git libretro-dosbox-pure-git libretro-fsuae-git libspng license-wtfpl m64py mathematica mednaffe metals moonlight-qt ms-sys ncurses5-compat-libs nestopia openmsx openshift-client-bin openshift-developer-bin openshift-pipelines-bin rcu-bin scheme-chez-symlink parsec-bin passmark-performancetest-bin pcsx2-git pegasus-frontend-git postman-bin powershell-bin protonmail-bridge-bin ps3-disc-dumper-bin python-patch-ng python-pluginbase python-pysdl2 rabtap rebar3 remark-language-server roomeqwizard rpcs3-git ruby-backport ruby-e2mmap ruby-jaro_winkler ruby-reverse_markdown ruby-solargraph ryujinx-git sameboy scala-dotty sedutil skyscraper-git soapui sublime-text-4 sunshine tla-toolbox townsemu-git ums ungoogled-chromium-bin visual-studio-code-bin vi-vim-symlink vmware-horizon-client vmware-keymaps xpadneo-dkms zeal-git zlib-ng zoom; do git clone https://aur.archlinux.org/$p.git; done

# Build all (I wouldn't do this, I would initially enter one-by-one and do
# git log ; makepkg -cCs manually). Will result in packages under $PKG_ROOT/Packages.
cd "$PKG_ROOT/Build"
for p in *; do cd $p; makepkg -cCs; cd -; done

# Update custom repository.
cd "$PKG_ROOT/Packages"
repo-add -n -R custom.db.tar.gz *.zst

# Update pacman databases.
pacman -Syu

# List all packages pacman sees in custom repository.
pacman -Sl custom

# Install all not-installed packages from custom repository.
pacman -S --needed $(pacman -Sl custom | grep -v installed | awk '{print $2}')

No AUR packages available (yet)

I maintain these AUR packages:

Here are a few more things I plan to create AUR packages for:

Final thoughts

After you’ve done (most of) the above, a reboot is in order; the system should come up cleanly, without errors or stalls.

I’ve be been using this setup for the last month and it has been pretty great. I get really good battery life, The fan almost never comes on, sleep and resume work reliably, bluetooth works with mouse, gamepad and headphones, Libvirt has been amazing with a bunch of interesting virtual machines (Guix, Windows NT, RHEL6 with Softimage).

I hope to be using this machine and operating system for a long time!