Installing Arch Linux on a Framework Laptop // Installation

Feb 22, 2022

This guide's intent is to be a comprehensive, command-by-command guide to get a complete web development environment up and running with Arch Linux on a Framework laptop. This is not meant to replace the generally excellent Arch Linux Installation Guide, or to serve as a general install guide for any non-Framework laptop. I'd encourage you to give the install guide a read through as it's quite short and serves as the bones of much of the information found in this article.

This guide will also attempt to service the "why" of the decisions I've made along the way via the explanation of command operands, as well as the addition of notes regarding specific design choices. This is mainly born out of my frustration with some guides detailing how to do things, without providing any additional context as to how they've arrived at those decisions.

This guide will be split into two pieces. This article - "Installation" - should be generic enough that it can be used as a springboard to get to your first bootstrapped command prompt. The second article - "Setup" - will be far more opinionated. It will present a set of packages and config files to provision an environment the way I like to work. It should be flexible enough for you to modify as you see fit - but ultimately it's goal is to be a reminder to myself how to get everything working after a fresh reformat.

Context

Locale

I'm an American currently living in Australia. As such, this guide will be US-centric, and since many defaults (e.g. keyboards, languages) are also US, I won't have been exposed to the same challenges as others while getting an environment up and running. That being said, I've attempted to add sections on where these decisions may need to be made, but as always the Arch Linux Installation Guide will be your best bet for further exploration on those topics.

Names

In the commands detailed in this article, you'll notice many references to kaishiro. This is my username, and as such should obviously be swapped out for your own where necessary.

Hardware

For reference, the Framework laptop that I'm targeting for this guide has the following hardware:

Processor
Intel® Core™ i7-1165G7 Processor
Wi-Fi
Intel® Wi-Fi 6E AX210 No vPro®
Storage
1TB WD_BLACK™ SN850 NVMe™
RAM
32GB (1 x 32GB) DDR4-3200

Additionally, I ordered a number of expansion cards, but the most relevant one to mention is the 250GB Expansion Card. I originally ordered this without a clear intention in mind, but my co-worker, Nathan, had the idea to install the Arch Linux ISO onto this directly. This meant that I always have a bootable ISO "on system" available for recovery, data carving, and general testing. It also makes it trivial to update to the latest monthly ISO release. It is admittedly a waste of the drive's capacity, but having used it several times already I'm happy with the functionality of having it available.


Create Bootable USB Drive

Download Arch Linux ISO

You can download the Arch Linux ISO from one of the official mirrors. I tend to pull these down via wget with a -c flag in case something goes wrong. At the time of writing (Mar 02, 2021), the most recent version is the 2022.03.01 release.

wget \
  -c \
  -P ~/Downloads \
  http://mirror.rackspace.com/archlinux/iso/2022.03.01/archlinux-2022.03.01-x86_64.iso
Operands
-c
Continue downloading a partial file - allows resuming an interrupted download
-P ~/Downloads
Download the iso to the current user's Downloads directory
http://mirror.rackspace.com/archlinux/iso/2022.03.01/archlinux-2022.03.01-x86_64.iso
This is the Rackspace global mirror - a country specific target is usually faster.

Verify ISO File

Once downloaded, we should quickly verify the integrity of the file using a SHA checker.

On Linux or macOS, you most likely already have the sha256sum command available for just such an occasion. On Windows, you can use the certutil command via Command Prompt.

sha256sum ~/Downloads/archlinux-2022.03.01-x86_64.iso

> 64070acf60ac342d7aaddddfa0448f5900c4a0a5  archlinux-2022.03.01-x86_64.iso
sha256sum ~/Downloads/archlinux-2022.03.01-x86_64.iso

> 64070acf60ac342d7aaddddfa0448f5900c4a0a5  archlinux-2022.03.01-x86_64.iso
certutil -hashfile %HOMEPATH%\Downloads\archlinux-2022.03.01-x86_64.iso

> SHA1 hash of \Users\kaishiro\Downloads\archlinux-2022.03.01-x86_64.iso:
> 64070acf60ac342d7aaddddfa0448f5900c4a0a5
> Certutil: -hashfile command completed successfully.

As you can see, this has produced a SHA sum of 64070acf60ac342d7aaddddfa0448f5900c4a0a5. We'll need to compare this sum to a trusted source to verify the integrity of our download.

The first option is to just check the Official Arch Linux Downloads page. Scroll down until you see the heading titled "Checksums", and find the sum labeled "SHA1". If this is identical to the sum generated above, then we're all set.

Alternatively, all mirrors should have a sha1sums.txt sitting alongside the ISO file that you downloaded above. We can curl this file to quickly check the valid sum as we already known the rest of the URL - we just need to swap out the filename.

curl http://mirror.rackspace.com/archlinux/iso/2022.03.01/sha1sums.txt

> 64070acf60ac342d7aaddddfa0448f5900c4a0a5  archlinux-2022.03.01-x86_64.iso
> 85b03756aeebd08b0cd1b61c6d950491c5aabcea  archlinux-bootstrap-2022.03.01-x86_64.tar.gz

Looks good. Obviously, if you find you have a mismatch you'll want to re-download the ISO as you most likely have a partial or corrupted download.

Identify USB Drive

With a verified ISO on hand, we're now almost ready to write the file to our USB drive. Before we can do that though, we'll need to identify our USB drive so that we know the target of our write command.

Linux

On Linux, I like to use the fdisk command, as it provides more information than an lsblk.

fdisk -l

> Disk /dev/nvme0n1: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors
> Disk model: WDS100T1X0E-00AFY0
> Units: sectors of 1 * 512 = 512 bytes
> Sector size (logical/physical): 512 bytes / 512 bytes
> I/O size (minimum/optimal): 512 bytes / 512 bytes
> Disklabel type: gpt
> Disk identifier: 664302B4-3B3C-4C6F-86A5-EE1DAC312885
> 
> Device             Start        End    Sectors  Size Type
> /dev/nvme0n1p1      2048    2099199    2097152    1G EFI System
> /dev/nvme0n1p2   2099200  136316927  134217728   64G Linux swap
> /dev/nvme0n1p3 136316928  346032127  209715200  100G Linux filesystem
> /dev/nvme0n1p4 346032128 1918896127 1572864000  750G Linux filesystem
> 
> Disk /dev/sda: 232.89 GiB, 250059350016 bytes, 488397168 sectors
> Disk model: USB DISK 3.2
> Units: sectors of 1 * 512 = 512 bytes
> Sector size (logical/physical): 512 bytes / 512 bytes
> I/O size (minimum/optimal): 4096 bytes / 33553920 bytes
> Disklabel type: dos
> Disk identifier: 0x6219625c
> 
> Disk /dev/sdb: 7.47 GiB, 8017412096 bytes, 15659008 sectors
> Disk model: USB Flash Drive
> Units: sectors of 1 * 512 = 512 bytes
> Sector size (logical/physical): 512 bytes / 512 bytes
> I/O size (minimum/optimal): 512 bytes / 512 bytes
> Disklabel type: dos
> Disk identifier: 0xd547386e
> 
> Device     Boot Start      End  Sectors  Size Id Type
> /dev/sdb1        2048 15659007 15656960  7.5G  b W95 FAT32

As you can see, we have both /dev/sda and /dev/sdb available.

/dev/sda is the 250GB Expansion Card that was shipped with my Framework.

/dev/sdb is what a fairly typical USB drive would look like. This one happens to be an 8GB drive, which makes it a fine candidate for an Arch Linux ISO.

macOS

On macOS I would use the diskutil command to find all of my available drives.

diskutil list

> /dev/disk0 (internal, physical):
>    #:                       TYPE NAME                    SIZE       IDENTIFIER
>    0:      GUID_partition_scheme                        *500.3 GB   disk0
>    1:                        EFI ⁨EFI⁩                     209.7 MB   disk0s1
>    2:                 Apple_APFS ⁨Container disk1⁩         500.1 GB   disk0s2
> 
> /dev/disk1 (synthesized):
>    #:                       TYPE NAME                    SIZE       IDENTIFIER
>    0:      APFS Container Scheme -                      +500.1 GB   disk1
>                                  Physical Store disk0s2
>    1:                APFS Volume ⁨Macintosh HD — Data⁩     274.8 GB   disk1s1
>    2:                APFS Volume ⁨Preboot⁩                 313.1 MB   disk1s2
>    3:                APFS Volume ⁨Recovery⁩                626.1 MB   disk1s3
>    4:                APFS Volume ⁨VM⁩                      3.2 GB     disk1s4
>    5:                APFS Volume ⁨Macintosh HD⁩            20.2 GB    disk1s5
>    6:              APFS Snapshot ⁨com.apple.os.update-...⁩ 20.2 GB    disk1s5s1
> 
> /dev/disk2 (external, physical):
>    #:                       TYPE NAME                    SIZE       IDENTIFIER
>    0:     FDisk_partition_scheme                        *8.4 GB     disk2
>    1:                 DOS_FAT_32 ⁨NO NAME⁩                 8.4 GB     disk2s1

From the above, you can see our USB drive is available as /dev/disk2.

Windows

I've never provisioned an Arch Linux bootable USB from Windows before, but the Arch Linux wiki's USB flash installation medium page has a great deal of information if that's where you're starting from.

Write ISO to USB Drive

With our USB drive identified, we can now begin the process of writing our ISO.

Linux

On Linux, if the drive is mounted, you'll first want to unmount it. If you're unsure of the mountpoint, now is a good time for a quick lsblk.

NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda           8:0    0 232.9G  0 disk
├─sda1        8:1    0   735M  0 part
└─sda2        8:2    0    77M  0 part /run/media/kaishiro/Archive
sdb           8:16   1   7.5G  0 disk
├─sdb1        8:17   1   735M  0 part /run/media/kaishiro/USB
└─sdb2        8:18   1    77M  0 part
nvme0n1     259:0    0 931.5G  0 disk
├─nvme0n1p1 259:1    0     1G  0 part /boot
├─nvme0n1p2 259:2    0    64G  0 part [SWAP]
├─nvme0n1p3 259:3    0   100G  0 part /
└─nvme0n1p4 259:4    0   750G  0 part /home

As you can see above, our target drive - sdb - is mounted to /run/media/kaishiro/USB. We'll want to unmount that now via the umount command.

umount /run/media/kaishiro/USB

Now we can run our dd command to write our ISO to the USB drive.

dd \
  bs=4M \
  conv=fsync \
  if=/home/kaishiro/Downloads/archlinux-2022.03.01-x86_64.iso \
  of=/dev/sdb \
  oflag=direct \
  status=progress

> (663 MB, 632 MiB) copied, 2 s, 330 MB/s
> 203+1 records in
> 203+1 records out
> 851783680 bytes (852 MB, 812 MiB) copied, 2.57875 s, 330 MB/s
Operands
bs=4M
Block size - The maximum size of each read/write block, in this case 4 megabytes
conv=fsync
Synchronizes the data and metadata once at the completion of the dd command.
if=/home/kaishiro/Downloads/archlinux-2022.03.01-x86_64.iso
Input file - the file we're reading from
of=/dev/sdb
Output file - the file or drive we're writing to
oflag=direct
Per the documentation, this should bypass the kernel cache and write directly to the file. See note below.
status=progress
Periodically show transfer progress

A note on oflag=direct

Many examples, including the one from the Arch Linux wiki's USB flash installation medium page, utilize the oflag=direct operand along with the dd command. That being said, in my personal testing I've found that adding this operand actually slows down the write process. However, I'm admittedly not an expert here, so will defer to the experts and continue adding this operand until otherwise instructed.

macOS

On macOS, we'll first need to unmount the target drive via diskutil.

diskutil unmountDisk /dev/disk2

> Unmount of all volumes on disk2 was successful

We can then proceed to write out our ISO using the dd command. The default dd command on macOS is generally older and doesn't support all of the operands that we provide in the Linux version above. Most notably, a visual status of the progress with status=progress is unsupported. Also to note is the lowercase m in the bs parameter. This is required and the command will most likely fail with a capital M.

One other thing to note is the use of /dev/rdisk2 instead of /dev/disk2. This isn't a typo, but rather the raw disk accessor for the drive. Read/write speeds to an rdisk designation are generally significantly faster than their buffered alternative.

A note on block sizes

As mentioned above, the macOS dd command's bs operand requires a lowercase m when specifying the block size in megabytes. This is apparently a quirk of the macOS dd having it's roots in BSD[1], while the dd command in most Linux distros would be using the GNU variant[2].

One exception to this is if you've installed coreutils - most typically via Homebrew. If, after running the following command you are presented with the following error...

dd: invalid number: '4m'

...than you most likely have overridden your default macOS dd command at some point with the coreutils alternative. If this is the case, simply switch the bs operand from 4m to 4M.

sudo dd
  bs=4m
  if=/Users/kaishiro/Downloads/archlinux-2022.03.01-x86_64.iso \
  of=/dev/rdisk2

> 203+1 records in
> 203+1 records out
> 851783680 bytes transferred in 106.642399 secs (7987289 bytes/sec)
Operands
bs
Block size - The maximum size of each read/write block, in this case 4 megabytes
if
Input file @TODO
of
Output file @TODO

The dd command, while running, will provide no feedback at all. However, you can poll for progress by sending a SIGINFO signal. On macOS, this is Ctrl+t by default.

Windows

Again, the "In Windows" section of the Arch Linux wiki's USB flash installation medium page presents several options here, namely win32diskimager, USBwriter, and Rufus.


Disable Secure Boot

Now that we have our bootable USB drive, we'll need to disable Secure Boot on the Framework laptop to ensure that we can succesfully boot from this drive. It will also allow us to add our own manual EFI boot entries via efibootmgr further along in the process.

  1. On boot, hit F2 until the BIOS menu appears
  2. Go to Security > Secure Boot
  3. Switch "Enforce Secure Boot" to "Disabled"

Boot to USB Drive

With Secure Boot disabled and our newly minted bootable USB drive plugged into the system, reboot your Framework laptop while hitting F12. This should bring up the BIOS Boot Manager, from which you can select the USB drive. For me, it was called "EFI USB Device (Lexar USB Flash Drive)"

This should immediately launch you into the EFI boot manager. Select "Arch Linux install medium (x86_64, UEFI)" and then hit Enter.

You should now be at a root command line prompt:

Arch Linux 5.16.11-arch1-1 (tty1)

archiso login: root (automatic login)

To install Arch Linux follow the installation guide:
https://wiki.archlinux.org/title/Installation_guide

For Wi-Fi, authenticate to the wireless network using the iwctl utility.
For mobile broadband (WWAN) modems, connect with the mmcli utility.
Ethernet, WLAN and WWAN interfaces using DHCP should work automatically.

After connecting to the internet, the installation guide can be accessed 
via the convenience script Installation_guide.

root@archiso ~ #

Prepare Hard Drive

First, we need to determine which drive we're installing Arch Linux on to. The Framework ships with an NVMe drive, so using fdisk we should be able to find our drive.

fdisk -l

> Disk /dev/nvme0n1: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors
> Disk model: WDS100T1X0E-00AFY0
> Units: sectors of 1 * 512 = 512 bytes
> Sector size (logical/physical): 512 bytes / 512 bytes
> I/O size (minimum/optimal): 512 bytes / 512 bytes
> Disklabel type: gpt
> Disk identifier: 664302B4-3B3C-4C6F-86A5-EE1DAC312885

I built my system with one of their 1TB drives, but your particular drive make and size will obviously vary if you've chosen a smaller drive or installed a non-Framework m2 drive. In either case, the following procedure should remain the same - you'll just need to modify the /home partition size according to your available space.

The most important thing to take from the above is the disk identifier - /dev/nvme0n1. This will be our target for all subsequent partition commands.

In terms of our particular partitioning schema, since we have 1TB available to us the plan is to utilize four separate disk partitions:

A note about partition sizes

Unless you're an old hat at Linux, it's possible you may find the above schema outdated. Traditional wisdom used to state that maintaining a separate root and home partition would allow one to reinstall their OS while maintaining an untouched home directory. Additionally, it also used to dictate that a swap partition should be roughly twice the size of your available RAM. From recent reading, it feels like drives are now fast enough that swap files are perfectly reasonable alternatives to formal partitions, and that a swap file of this size is a waste of space. I'll be the first to admit that I'm no expert here, but I have plenty of space to play with, so I've decided on a traditional partitioning schema until someone smarter comes along and tells me I'm wrong.

Clean up Current Partition Table

First, we'll need to remove any existing partitions - if any - from the target drive. My favorite way to do this is to just zero out the drive using the dd command.

dd \
  bs=4M \
  count=1 \
  if=/dev/zero \
  of=/dev/nvme0n1

> 1+0 records in
> 1+0 records out
> 4194304 bytes (4.2 MB, 4.0 MiB) copied, 0.0140505 s, 299 MB/s
Operands
bs
Block size - The maximum size of each read/write block, in this case 4 megabytes
count
The number of blocks to copy - since we're inputting from /dev/zero it would continue forever without a count
if
Input file - in this case, /dev/zero - a special file that just returns null characters
of
Output file - our NVMe drive

Now that any existing partitions have been cleaned up, we can start our fdisk session.

fdisk /dev/nvme0n1

> Welcome to fdisk (util-linux 2.37.2).
> Changes will remain in memory only, until you decide to write them.
> Be careful before using the write command.

> Device does not contain a recognized partition table.
> Created a new DOS disklabel with disk identifier 0xab743ee3.

Command (m for help):

Create GPT Partition Table

We need to convert the drive to use a GPT partition table instead of MBR. You can read more about the decision to use one over the other on the Arch Linux Partitioning page, but the TL;DR is that GPT is a newer standard that's been slowly replacing MBR.

Command (m for help): g

> Created a new GPT disklabel (GUID: F28E0003-A324-E842-9E28-48616CD5A625).

Setup Boot Partition

Next we can create our boot partition.

Command (m for help): n

Partition number (1-128, default 1): <Enter>

First sector (2048-1953525134, default 2048): <Enter>

Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-1953525134, default 1953525134): +1G

> Created a new partition 1 of type 'Linux filesystem` and of size 1 GiB.

A note on existing signatures

Regardless of whether you zeroed out your drive using dd or instead manually deleted your partitions within fdisk, upon creating your partitions you may at some point receive the following warning in scary red letters:

> Partition #1 contains a vfat signature.

Do you want to remove the signature? [Y]es/[N]o: Y

You should safely be able to respond either Y or N to this query - we'll be formatting all of these new partitions anyway which should override any existing format signatures. I typically just respond Y.

We now have our new boot partition - however, there is one problem. The default partition type is Linux filesystem. This is fine for our root and home partitions, however our boot partition needs a special type in order to be made bootable - EFI System.

Since we're still in fdisk, we can use the t command to switch the type of a partition. The EFI System partition's type identifier is 1 (you can verify this by listing all of the types with L on the third line of the following code block.

Command (m for help): t
Selected partition 1
Partition type or alias (type L to list all): 1

Changed type of partition 'Linux filesystem' to 'EFI System'.

Setup Swap Partition

With our boot partition created, we can move on to the rest of the partition schema. These will follow roughly the same process as above.

First, we create the swap partition. As noted above, this will be twice the size of our system's available RAM (32GB) - so 64GB. If space is at a premium for your drive, matching your system's available RAM is also a perfectly acceptable alternative.

Command (m for help): n

Partition number (2-128, default 2): <Enter>

First sector (2099200-1953525134, default 2099200): <Enter>

Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-1953525134, default 1953525134): +64G

> Created a new partition 2 of type 'Linux filesystem` and of size 64 GiB.

And just like the boot partition, the swap partition also requires a different type, Linux swap. The type code for Linux swap is 19.

Command (m for help): t
Partition number (1,2, default 2): 2
Partition type or alias (type L to list all): 19

Changed type of partition 'Linux filesystem' to 'Linux swap'.

Setup Root Partition

Our third partition will be the root partition. This will hold the entirety of our Arch Linux install - minus the /home directory containing the user's files. The root partition will be 100GB - this should be more than adequate to hold our non-home directory files.

Command (m for help): n
Partition number (3-128, default 3): <Enter>
First sector (136316928-1953525134, default 136316928): <Enter>
Last sector, +/-sectors or +/-size{K,M,G,T,P} (136316928-1953525134, default 1953525134): +100G

Created a new partition 3 of type 'Linux filesystem` and of size 100 GiB.

The Linux filesystem type is correct for this partition, so no type change is necessary.

Setup Home Partition

Finally, we have our home partition. This will house the entirety of the /home directory - containing all of our user's files. This includes configuration, downloads, images, videos, site/project files, code repos, etc. We'll be giving the bulk of our leftover drive space over to this partition - 750GB.

Command (m for help): n
Partition number (4-128, default 4): <Enter>
First sector (346032128-1953525134, default 346032128): <Enter>
Last sector, +/-sectors or +/-size{K,M,G,T,P} (346032128-1953525134, default 1953525134): +750G

Created a new partition 4 of type 'Linux filesystem` and of size 750 GiB.

Once again, the default Linux filesystem partition type is correct for this partition, so no other type change is necessary.

A note on reserved space

You may have noticed that the above math doesn't exactly check out. Adding up our various partition sizes - 1 for boot, 64 for swap, 100 for root, and 750 for home, leaves us with a total of only 915GB being partitioned in this schema. Traditionally, leaving some available, unpartitioned space was good practice in case you needed to extend an existing partition (e.g. your /usr/bin fills up). While not necessary, I generally like to continue this practice if possible.

Save Partitions

Now that all of our partitions have been created and our types have been set accordingly, we can review our partition table and confirm that everything is in order.

Command (m for help): p

> Disk /dev/nvme0n1: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors
> Disk model: WDS100T1X0E-00AFY0
> Units: sectors of 1 * 512 = 512 bytes
> Sector size (logical/physical): 512 bytes / 512 bytes
> I/O size (minimum/optimal): 512 bytes / 512 bytes
> Disklabel type: gpt
> Disk identifier: 664302B4-3B3C-4C6F-86A5-EE1DAC312885
> 
> Device             Start        End    Sectors  Size Type
> /dev/nvme0n1p1      2048    2099199    2097152    1G EFI System
> /dev/nvme0n1p2   2099200  136316927  134217728   64G Linux swap
> /dev/nvme0n1p3 136316928  346032127  209715200  100G Linux filesystem
> /dev/nvme0n1p4 346032128 1918896127 1572864000  750G Linux filesystem

Looks great - four partitions: boot, swap, root, and home. 1GB, 64GB, 100GB, and 750GB, respectively, along with correct types for each partition. Let's go ahead and save all of these changes by writing our partition table to disk.

Command (m for help): w

> The partition table has been altered.
> Calling ioctl() to re-read partition table.
> Syncing disks.

Upon writing these changes, this will drop us out of fdisk and back to our root command prompt.

Format Partitions

Now that we have our partitions all written to disk, we can complete our drive prep by formatting each of these partitions with the relevant filesystem. From here on out, we'll be referencing our drive partitions using the device identifier listed in the partition table we printed above /dev/nvme0n1p1 for the boot partition, /dev/nvme0n1p2 for the swap partition, etc.

The boot partition will be formatted to FAT32 using the mkfs.fat command.

mkfs.fat -F 32 /dev/nvme0n1p1

> mkfs.fat 4.2 (2021-01-31)

The swap partition will be made a swap area using the mkswap command.

mkswap /dev/nvme0n1p2

> Setting up swapspace version 1, size = 64 GiB (68719472640 bytes)
> no label, UUID=fc4a8958-370f-43f6-966f-12db2b5add76

Finally, the root and home partitions will be formatted to ext4 using the mkfs.ext4 command.

A note on filesystems

I've chosen to use the ext4 filesystem due to it's familiarity and stability, however there are a wealth of other choices available. I wouldn't be surprised if there are better choices specifically for NVMe drives like the ones that ship with the Framework. I'd encourage you to read more about file systems on the Arch Linux wiki's File system page. That being said, ext4 should "just work", and that's good enough for me at the moment.

mkfs.ext4 /dev/nvme0n1p3

> mke2fs 1.46.4 (18-Aug-2021)
> Discarding device blocks: done
> Creating filesystem with 26214400 4k blocks and 6553600 inodes
> Filesystem UUID: bcebb6fe-4e4f-4efa-a85a-d67fb1f3bd66
> Superblock backups stored on blocks:
>   32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654200,
>   4096000, 7962624, 11239424, 20480000, 23887872
>
> Allocation group tables: done
> Writing inode tables: done
> Creating journal (131072 blocks): done
> Writing superblocks and filesystem accounting information: done
mkfs.ext4 /dev/nvme0n1p4

> mke2fs 1.46.4 (18-Aug-2021)
> Discarding device blocks: done
> Creating filesystem with 196608000 4k blocks and 49152000 inodes
> Filesystem UUID: bcf7729a-eb1c-41fd-ab77-76b5db387219
> Superblock backups stored on blocks:
>   32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654200,
>   4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
>   102400000
>
> Allocation group tables: done
> Writing inode tables: done
> Creating journal (262144 blocks): done
> Writing superblocks and filesystem accounting information: done

Install Arch Linux

With a fully prepared drive, we're now ready to begin the installation process. But first, we'll need an internet connection.

Connect to the Internet via Wi-Fi

Since the Framework doesn't currently ship with an Ethernet port - the presumption is that you are connecting to the internet via Wi-Fi. To do so, we'll use the iwctl utility. I personally don't like running iwctl in it's CLI mode - but instead prefer to just issue commands to it via the terminal.

# This should show you wlan0 if you've properly installed
# the physical wi-fi adapter. To interact with your wifi
# module at all, you'll be using wlan0 as the "station"
# identifier for all subsequent commands
iwctl device list

# This will scan for all available networks (@TODO no results)
iwctl station wlan0 scan

# This will list all networks found via the scan (@TODO show results)
iwctl station wlan0 get-networks

# "Toguchi" is the name of my local wifi router. This should
# prompt you to enter the wifi passphrase.
iwctl station wlan0 connect Toguchi

At this point, you should now be connected to the internet. To confirm, I like to ping example.com. Keep in mind it may take several seconds to asynchronously connect, so if your initial ping fails, it's worth trying again.

ping example.com

> PING example.com (93.184.216.34) 56(84) bytes of data.
> 64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=1 ttl=56 time=283 ms
> 64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=2 ttl=56 time=309 ms
> 64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=3 ttl=56 time=216 ms
Success
ping example.com

> ping: example.com Temporary failure in name resolution
Failure

Update System Clock

Now that we have an active internet connection we can update the system clock. The reason we frontload the updating of the system clock via NTP is because if our system clock is wrong than the signing keys required for installation of packages via pacstrap could fail.

timedatectl set-ntp true

@TODO add explanation

timedatectl status

Prepare /mnt Directory

For anyone who hasn't installed Linux from scratch before - this process is fairly similar across distributions (Arch gives us a few distro-specific commands - namely pacstrap and arch-chroot which will help expedite the process). We first need to mount our partitions (currently located in the /dev directory) to the /mnt directory. We'll then install some required packages to this newly mounted directory root so that they'll be available for the next step.

You see - up until now we've been running all of these commands in the context of the bootable USB drive. However, once we change root into our newly created /mnt directory to complete the installation - none of these tools will be available anymore. This is because the /usr/bin directory that all of this tooling is located in is different from the /mnt/usr/bin that will be available to us once we've chroot'ed into the new drive.

Mount Prepared Volumes

The order in which these partitions are mounted matters. We'll need to start with the root partition so that the child boot and home directories can be properly mounted underneath it. We'll also need to ensure that the mount points (directories) have been created inside the new root directory before attempting to mount the sub-directories.

mount /dev/nvme0n1p3 /mnt
Mount root
mkdir /mnt/boot
mount /dev/nvme0n1p1 /mnt/boot
Mount boot
mkdir /mnt/home
mount /dev/nvme0n1p4 /mnt/home
Mount home

Enable swap partition

Finally, the swap partition isn't so much "mounted" as it is enabled. We achieve this with the swapon command.

swapon /dev/nvme0n1p2

Construct Mirrorlist

Before we begin installing all of our packages, we'll want to reconstruct a mirrorlist specific to our current location. This should help ensure the fastest possible download speeds. We can use the already available reflector command to achieve this.

reflector \
  --country Australia \
  --save /etc/pacman.d/mirrorlist \
  --sort rate
Operands
--country Australia
The country to pull the available mirrors from
--save /etc/pacman.d/mirrorlist
The location to save our sorted results to - we'll be saving this directly to /etc/pacman.d/mirrorlist so that pacstrap can find it
--sort rate
Sort our mirrors by the fastest transfer rate

Install Packages

We're now ready to install all of the necessary packages to finish bootstrapping the system. We'll be using the Arch-specific pacstrap command for this. It's vaguely similar to the pacman package manager for Arch, with the primary difference being that you also pass in a specific installation directory - in this case our newly provisioned root directory - /mnt. I generally try and keep this list as tight as possible - installing any userland programs after first boot as a local user instead of as our current root.

pacstrap \
  /mnt \
  base \
  base-devel \
  efibootmgr \
  intel-ucode \
  iwd \
  linux \
  linux-firmware \
  man-db \
  vim
Operands
/mnt
Install all packages using /mnt as the install root
base
Base dependencies required by Arch Linux
base-devel
Additional development utilities - sed, sudo, which, etc
efibootmgr
Tooling to modify the EFI Boot Manager
intel-ucode
Microcode for Intel processors
iwd
Wi-fi connection management and daemon
linux
Linux kernel
linux-firmware
Additional Linux firmware
man-db
Utility to read man pages
vim
Vim - the only text editor you'll ever need

Generate fstab

The final step before switching into our new root directory is to generate an fstab file. You can read more about fstab here, but essentially it acts as a config file for Linux so that it knows what partitions to mount for a particular system. Generating this file is trivial with the genfstab command, and we'll be outputting the results of this command directly to our newly created root directory's /etc/fstab file.

genfstab -U /mnt >> /mnt/etc/fstab
Operands
-U
Use UUIDs as identifiers for the drives
/mnt >> /mnt/etc/fstab
Generate the fstab file from /mnt, and output it to /mnt/etc/fstab

Change Root

We've now completed all the prep necessary before changing root into our new /mnt directory. As mentioned above, once we've changed our root directory, all of the tooling currently available will no longer be accessible, as it's mostly living in our current /usr/bin - not /mnt/usr/bin. However, due to our pacstrap command above, we should have the necessary commands to finish provisioning our system.

Normally, changing your root directory in Linux during an install process requires a bit more housekeeping - however Arch Linux gives us the handy arch-chroot command that handles all of that for us. So, all that we have to do is run the following.

arch-chroot /mnt

Create Boot Entry

Now that we're in our new root directory, the next step is to add a boot entry so that upon restarting, we'll be able to boot directly to our new Arch Linux install. This involves capturing our partition's unique identifiers (UUIDs), and then using those to insert an entry into the UEFI boot manager.

Get Partition UUIDs

First, we'll get our partition's UUIDs using the lsblk command. The only two we care about are the swap partition (nvme0n1p2), and the root partition (nvme0n1p3).

lsblk -f

> NAME      FSTYPE FSVER LABEL  UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
> nvme0n1
> ├─nvme0n1p1
> │         vfat   FAT32        9393-2809                             966.6M     5% /boot
> ├─nvme0n1p2
> │         swap   1            6e3cbf29-0b1f-4f7f-b1b6-a20499d45185                [SWAP]
> ├─nvme0n1p3
> │         ext4   1.0          4d142d07-e5dc-4f92-89c6-06e6c944bc1f   91.1G     2% /
> └─nvme0n1p4
>           ext4   1.0          924f16ce-63eb-4ab6-85e0-9a61eaf0e846  699.6G     0% /home

Insert Boot Entry

With the UUID's made available via the lsblk command, we can now write our command to insert the boot entry via efibootmgr. This is a fairly complex command, and it's important that you get everything correct otherwise the system won't be able to boot into our newly provisioned Arch system. However, if this does happen - it's fine. We can fix it by simply booting back into our USB drive and trying again.

efibootmgr \
  --create \
  --disk /dev/nvme0n1 \
  --part 1 \
  --loader /vmlinuz-linux \
  --unicode 'root=UUID=4d142d07-e5dc-4f92-89c6-06e6c944bc1f rw resume=UUID=6e3cbf29-0b1f-4f7f-b1b6-a20499d45185 mem_sleep_default=deep initrd=\intel-ucode.img initrd=\initramfs-linux.img' \
  --verbose
Operands
--create
Flags that we are inserting a new entry
--disk /dev/nvme0n1
Identifies the disk we're booting from
--part 1
Identifies the boot partition
--loader /vmlinuz-linux
The Linux kernel, relative to our boot partition (/boot)
--unicode
See below
--verbose
Prints out the modified boot order once insert is complete

The only flag we didn't explain above is also the most complicated one --unicode. This is used to send additional arguments (kernel parameters) along with our boot entry. The best reference for these that I've found is via kernel.org's kernel parameters page. However, I've done my best to break down the parameters that we're using below.

root=UUID=4d142d07-e5dc-4f92-89c6-06e6c944bc1f
This identifies the root partition via the UUID that we received from lsblk above.
rw
Mounts the root device in read/write mode
resume=UUID=6e3cbf29-0b1f-4f7f-b1b6-a20499d45185
This identifies the swap partition (where memory will be dumped to when sleeping) via the UUID that we received from lsblk above.
mem_sleep_default=deep
Sets the system's suspend mode. By default, this would be s2idle, however after reading reports[3] that deep was more efficient I've settled on this as a default. Subsequently, my own testing showed a booted, but sleeping system with nothing running discharging from 100% to ~72% over the course of 12 hours using s2idle. The same test using deep discharged from 100% to ~86%.
initrd=\intel-ucode.img
The initrd parameter specifies the "initial ramdisk" loaded via the boot loader. This is typically an .img file and the path is absolute from the /boot directory. Since the Framework laptop's currently only ship with the with either Intel i5 or i7 chipsets, we'll need to load the intel-ucode microcode. As directed via the Arch Linux wiki's Microcode page, this chipset microcode must be the first initrd operand in the bootloader config.
initrd=\initramfs-linux.img
Specifies the location of the default initial ramdisk (absolute from the /boot directory).

A note about initrd paths

The backslashes in the initrd parameters may look strange, but they are correct. The only note on the Arch Linux wiki I've been able to find related to this anomaly is from the REFind configuration page. It states "Use backslashes (\) as path separators in the initrd parameter, otherwise the kernel may fail to find the initramfs image(s)".

This still doesn't explain the "why" for me, but I can only assume it has something to do with the UEFI specification. Maybe someone more well versed in UEFI could let me know.


Configuration

Set Timezone

To list all available timezones, we can use the timedatectl package.

timedatectl list-timezones

If you're looking for a specific region (or city), you can pass the output through grep.

timedatectl list-timezones | grep America

And if you're still seeing too many results even with a grep, you can pipe these results through less.

timedatectl list-timezones | grep America | less

Once you've found the region/city combination most applicable to your location, we'll need to manually create the symlink to our /etc/localtime file. You just need to append the timezone to /user/share/zoneinfo/ to create the link.

I'm currently located in Melbourne, Australia - so that's the time zone I'll be setting for myself.

ln -sf /usr/share/zoneinfo/Australia/Melbourne /etc/localtime

Create /etc/adjtime

Since we previously synchronized our system clock via NTP, we know this to be accurate. We can now set the hardware clock from the current system clock to account for any drift.

hwclock --systohc

Setup Localization

I only need the US locale setup, so I'll just append this to the existing /etc/locale.gen file. If you need a different locale, or multiple locales, then you can edit the /etc/locale.gen file directly and just uncomment the relevant locales.

echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen

Once the file is correct, we can generate the locales with the locale-gen command.

locale-gen
> Generating locales...
>   en_US.UTF-8... done
> Generation complete.

Now that the locale files have been generated, we can set the locale by creating an /etc/locale.conf file.

echo 'LANG=en_US.UTF-8' > /etc/locale.conf

Set Hostname

Your hostname is how your machine will be identified on a network - it's a name for your laptop. Just echo whatever name you'd like to /etc/hostname.

echo 'gladius' > /etc/hostname

Set Root Password

passwd

Permiss Wheel Group

echo '%wheel ALL=(ALL) ALL' >> /etc/sudoers

Add Local User

useradd \
  -m \
  -G wheel,video \
  kaishiro
Operands
-m
Create a home directory for the user
-G wheel,video
Add the user to the following groups
kaishiro
The name of the user - in my case, kaishiro

We can then set the local user's default password.

passwd kaishiro

A note about the root user

The base-devel package we added in the pacstrap command above brings down the sudo package, and anyone in the wheel group can now use sudo to elevate themselves to run any command. Our newly added user is part of the wheel group - so once we've completed provisioning the /mnt directory we should largely be done with the root user.

Enable Services

The last thing to do before our first official reboot into the new system is to enable two key services - iwd and systemd-resolved. These will allow us to have an active internet connection upon restart.

iwd

Let's add a configuration file for iwd. We'll first need to make the /etc/iwd directory, which is where the main.conf file lives.

mkdir /etc/iwd

With that done, we can create the main.conf file and add the following:

[General]
EnableNetworkConfiguration=true

[Network]
EnableIPv6=true
NameResolvingService=systemd
/etc/iwd/main.conf
Rules
EnableNetworkConiguration=true
Enables the built-in network configuration of iwd
EnableIPv6=true
Enables IPv6 support
NameResolvingService=systemd
Set's the default name resolution service - in this case systemd-resolved, which we'll be enabling in a moment

With the configration file in place, we can now go ahead and enable the iwd service.

systemctl enable iwd

systemd-resolved

The systemd-resolved service "provides network name resolution to local applications via a D-Bus interface". A rather simplistic explanation is that it converts domain names (e.g. example.com) to IP addresses (e.g. 93.184.216.34). It's necessary to have some sort of resolver, and the systemd-resolved service is already available to us so we'll go head and use that.

systemctl enable systemd-resolved.service

Cleanup & Reboot

With that, we're just about ready to boot into our new system.

First, let's drop out of our chroot, back to the USB drive.

exit

Then, there is one more optional step to make our lives a little easier. We've already connected to our current router, and those connection details are available via iwd - so we can actually migrate them over to the new system to ensure we have an immediate connection upon reboot. All we have to do is create the /mnt/var/lib/iwd directory and then copy the router specific configuration over.

mkdir /mnt/var/lib/iwd
cp /var/lib/iwd/Toguchi.psk /mnt/var/lib/iwd

@TODO cp over mirrorlist

Then, let's unmount our drive - most likely an unnecessary precaution.

umount -l /mnt

Finally, let's go ahead and reboot the system. The EFI boot entry we entered above via efibootmgr should take precedence over the USB drive, so it shouldn't matter if it remains plugged in or not (you can always choose to boot back into it by hitting F12 upon boot).

systemctl reboot

Upon starting - if everything has gone according to plan - you should see the Framework splash screen, immediately followed by our terminal login.

Arch Linux 5.16.11-arch1-2 (tty1)

gladius login:

The boot should be quite fast, and you may notice there was no intermediary boot manager such as GRUB or rEFInd asking you to select an OS. This is because we're booting directly from the motherboard using UEFI (via the efibootmgr entry above) - pretty neat.


  1. https://ss64.com/osx/dd.html ↩︎

  2. https://man7.org/linux/man-pages/man1/dd.1.html ↩︎

  3. https://community.frame.work/t/linux-deep-sleep/2491 ↩︎