Nix Adventures part 1!

1. What is this?

I recently (re?)read some posts from Ian Henry’s own adventures with Nix. I find the series both amusing (because watching someone suffer as I have suffered can be amusing if done well), and because it’s actually encouraging to see someone actually struggling with this. Nix feels a lot like Rust to me. In many ways it represents the future of computing. In others, I see a select few people with deep understanding of the topic spouting off what is the equivalent of total nonsense to me. I am glad these people are helping, and I am positive they are 100% correct in their statements. I just don’t understand any of it. There’s some knowledge I was supposed to acquire while skimming reading through a colossal set of documentation.

Now, before this goes further, I want to make it really clear here: I fully understand and appreciate for most people working on these projects that it’s a passion project and open source burnout is very real. Posts like Ian’s and now mine walk a dangerous line of contributing to that burnout. I will make immense effort to keep my observations tasteful and respectful, while still capturing a degree of frustration so that maybe someone will have an experience similar to mine as I read Ian’s posts.

I tried finding the origin to this, but Google really has been letting me down lately. Likely paraphrased, it goes like this:

There are two kinds of things in the world: Things people complain about, and things nobody has heard of.

That Nix now has at least two blog series on this topic speaks well of Nix, its promise, and its progress.

2. Introduction

As is stamped all over this blog, I am Logan Barnett. I like working with computers more than working with humans, and I’ve been doing it for a while now. There are significant gaps in my knowledge and I am comfortable with that. I have a penchant for functional programming, documentation of all kinds, and overall I want the computer to work for me, not the other way around. I have a resume if you want to get into more specifics of my technical background.

3. Nix Thus Far

I have been using Nix for well over a year at time of writing. Since then I have moved to using Nix with Home-manager, and now some combination of Home-manager and Flakes that still puzzles me. I use it extensively for managing my machine’s local configuration (historically “dotfiles”). I also use it for managing repository dependencies (a generalized version of tfswitch, rbenv, pyenv, nodenv, etc). I always have to look up how to make a derivation or even the simplest overlay.

I’ve gotten to the point where installing my settings on a new machine requires very little adjusting. A vast majority of my local software is governed by Nix. The only thing I’m truly missing is Homebrew’s cask functionality, and that’s mostly because Alfred.app refuses to follow symlinks as a design decision (I plan to rectify that eventually).

I am getting to the point where I can make my own derivations that simply follow build-from-source instructions on a README.

4. Today’s Project: Working on a Raspberry Pi

I have some work I want to do on a Raspberry Pi involving Wireguard. I have some prior experience with Wireguard and also with working on Raspberry Pis. I have a complication: I have no displays nor display adapters (micro-HDMI) with which I could use to interface with the Pi. I have no extra keyboard or mouse at the moment. For a system I should be able build an image for, this kind of hardware requirement really seems like overkill.

sshd is disabled by default on a Raspberry Pi, so I cannot ssh to it, even if I could find it on a local link (this would take hours because nmap is pretty slow at this).

So my next best activity is to either build the image in the state I want it in, or edit the files on the SD card whose state is setup by a pre-built image. I have a pre-built image already, and I’m on macOS. Being on macOS prevents me from building new Linux images of any kind without a Docker image (which needs a VM) or setting up a Linux VM (which needs a VM too, ha!). My bandwidth is low, as is my disk space. I can solve the disk space issue, but that’s a project for another day.

In comes a tool called macfuse, previously known as osxfuse. They followed the rebrand that Apple did, apparently. I’ve learned recently that this tool no longer requires disabling the SIP on macOS, which I have found to be a deal-breaker for my standards. It’s just a user-land kernel extension. I don’t know who to thank for that, but I am grateful for it.

5. Somehow Write to the Pi SD card

Whether I have some way to bake an image in the desired state (such as with a fixed IP for link-local and sshd enabled), or I just edit the SD card directly doesn’t strictly matter to me. Because I wanted to keep things “simple”, I elected to just go for the edits directly on what I believe is an already functional SD card, and save the NixOS-on-Pi activity for another day.

5.1. Empower Editing SD Card Files from macOS Directly

First, getting macfuse setup on my system via Nix isn’t something directly supported. I had to go into the Nix derivation for e2fsprogs and remove the guards against running on darwin. Additionally I found e2fsprogs doesn’t even compile. setxattr and getxattr can’t be assigned to because the function signatures don’t match. So I make them match with best guess by adding some parameters and declaring a couple of others as unsigned (link will come to the commit by the time I publish). I believe this is because osxfuse has some differences in the API, or perhaps there are version incompatibilities. It builds after that. I struggle with nix-flakes for a bit but eventually I notice that fuse-ext2 is indeed on my PATH - but I’m not certain which of my incantations brought it about. I’m still very new to nix-flakes and much of it was forced upon me from other things I desired. I basically jumped in the cockpit seat of the nix-flakes plane and grabbed the stick, hoping for the best. I haven’t collided with the planet Earth yet but there’s still time for that.

I try to mount my disk with invocations such as these:

sudo fuse-ext2 /dev/disk4s1 /Volumes/4s1
sudo fuse-ext2 /dev/disk4s2 /Volumes/4s2

Because I don’t know which one is which and there are only two partitions for disk4.

I don’t have the exact message or dialog screenshots unfortunately, but this is where I find out that I indeed need to disable SIP (System Integrity Protection). This involves rebooting the system into recovery mode, firing up the terminal there, and running csrutil disable. uptime reports I’m at 70 days - a lot of days earned via hard won knowledge on battery draining activities on my laptop (such as bluetooth). My pride suffers a blow, but I sally forth. Then I spend about 7 reboots or so trying to get my laptop to actually boot into recovery mode.

macOS has changed their reboot key bindings more than they’ve changed their power adapter connections, and that’s saying a lot. Naturally I searched for how to do this, and was happy with the holding of Cmd+R during the boot-up. It doesn’t work, which in my experience means the timing is wrong, so I try it several more time. The technical timing is important so I try it more than once, which is how we wind up with me going to 7. The 7th attempt was me finding out those instructions were for Intel Macs and I’m on Apple Silicon. Right - glad we have that distinguishment. My mind immediately demands “Why? Why make these different?” but answers I can come up with are depressing in a very literal way.

I do the csrutil disable which prompts me but works anyways. Knowing I have done the nasty successfully and my work here is done, I reboot to go back to my normal runtime. I attempt to mount again with:

sudo fuse-ext2 /dev/disk4s2 /Volumes/4s2

But again am prompted - my Mac tells me it has blocked the kernel extension, and I must reboot to bless it. Great. Couldn’t we have just done that earlier? Or maybe just let me queue up here at least so I don’t need to go into a mode where I can’t call upon my lifelines for help. Alas, more reboots ahoy. At least this time they give me really vague instructions. This probably would’ve confused me into doing more reboots and searching, but I’ve already been here. Now that I’ve blessed this kernel extension from Benjamin (who is that?), I go back to try it again. Still blocked. The System Settings section tells me there’s a kernel extension that needs to be blessed - it doesn’t give me indication that I’ve blessed anything already. Ugh. Why is this such a miserable experience? Again I reboot into recovery mode, bring up the Security Utility, and bless the kernel extension. This time it goes away. I was operating in the suspicion that there might be more than one kernel extension involved, and all of them must be blessed individually.

Now I try again. I have some vague memory that mount processes don’t typically create the actual mount point directory in Linux, so I assume it’s the same here.

sudo mkdir /Volumes/4s2
sudo fuse-ext2 /dev/disk4s2 /Volumes/4s2

Nothing happens. The directory is empty and I have exit code 253 from fuse-ext2 (having your prompt show the last exit code is so incredibly useful). What’s 253 mean? I check the man page - nothing. From searching on other people trying things out, I add a -o debug to the invocation. No output, same code. I add -v to the front of the from/to arguments. No change in output/code. I worry this is resulting from my hack earlier to e2fsprogs. But I would expect to see something here. There is mention of a /var/log/fuse-ext2_util.log in some tickets I see, but I don’t have that.

I try:

sudo mount -t fuse-ext2 /dev/disk4s2 /Volumes/4s2
mount: exec /Library/Filesystems/fuse-ext2.fs/Contents/Resources/mount_fuse-ext2 for /Volumes/4s2: No such file or directory
mount: /Volumes/4s2 failed with 72

The No such file or directory portion can’t be correct on the latter path - /Volumes/4s2 exists. I check the /Library/Filesystems/... path and it is indeed non-existent. Curious, looking in /Library/Filesystems directly shows me something of relevance:

ls /Library/Filesystems
NetFSPlugins
macfuse.fs

So I try this modified mount:

sudo mount -t macfuse.fs /dev/disk4s2 /Volumes/4s2
macFUSE mount version 4.5.0

This program is not meant to be called directly. The macFUSE library calls it.

Available mount options:
    -o allow_other         allow access to others besides the user who mounted
                           the file system
    -o allow_recursion     allow a mount point that itself resides on a macFUSE
                           volume (by default, such mounting is disallowed)
    -o allow_root          allow access to root (can't be used with allow_other)
    -o auto_xattr          handle extended attributes entirely through ._ files
    -o blocksize=<size>    specify block size in bytes of "storage"
    -o daemon_timeout=<s>  timeout in seconds for kernel calls to daemon
    -o debug               turn on debug information printing
    -o default_permissions let the kernel handle permission checks locally
    -o defer_permissions   defer permission checks to file operations themselves
    -o direct_io           use alternative (direct) path for kernel-user I/O
    -o extended_security   turn on macOS extended security (ACLs)
    -o fsid=<fsid>         set the second 32-bit component of the fsid
    -o fsname=<name>       set the file system's name
    -o fssubtype=<num>     set the file system's fssubtype identifier
    -o fstypename=<name>   set the file system's type name
    -o iosize=<size>       specify maximum I/O size in bytes
    -o jail_symlinks       contain symbolic links within the mount
    -o local               mark the volume as "local" (default is "nonlocal")
    -o negative_vncache    enable vnode name caching of non-existent objects
    -o sparse              enable support for sparse files
    -o volname=<name>      set the file system's volume name

Available negative mount options:
    -o noalerts            disable all graphical alerts (if any) in macFUSE Core
    -o noappledouble       ignore Apple Double (._) and .DS_Store files entirely
    -o noapplexattr        ignore all "com.apple.*" extended attributes
    -o nobrowse            mark the volume as non-browsable by the Finder
    -o nolocalcaches       meta option equivalent to noreadahead,noubc,novncache
    -o noreadahead         disable I/O read-ahead behavior for this file system
    -o nosynconclose       disable sync-on-close behavior (enabled by default)
    -o nosyncwrites        disable synchronous-writes behavior (dangerous)
    -o noubc               disable the unified buffer cache for this file system
    -o novncache           disable the vnode name cache for this file system
mount: /Volumes/4s2 failed with 64

So mount is looking less promising. This appears to be mount just spewing the usage information of macfuse because it is giving macfuse an erroneous invocation. Is this intended to ever work? I found this invocation from other lost souls who were experiencing no luck here as well, so it might not mean much.

Following some other tickets, I want to just confirm that everything I have is in order. Oftentimes errors come from the user, not the software. I find sudo diskutil list gives some good answers.

<snip>
/dev/disk6 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *127.9 GB   disk6
   1:                 DOS_FAT_32 FIRMWARE                31.5 MB    disk6s1
   2:                      Linux                         127.8 GB   disk6s2

I have been operating with the notion that I was using disk4, but between reboots and swapping the physical card out to stow my laptop between attempts, it must have moved. Sigh. Let’s do this again. At least I know with confidence that I want to be mounting the “2” partition.

sudo fuse-ext2 /dev/disk6s2 /Volumes/4s2

I didn’t rename the directory to a matching 6s2 because I just want to try something. Results are promising and with a delay:

Mounting /dev/disk6s2 Read-Only.
Use 'force' or 'rw+' options to enable Read-Write mode

And then:

ls /Volumes/4s2

Wow something worked! I’m going to treat myself.

Now let’s clean up the directory name, and make it read+write.

sudo umount /Volumes/4s2
sudo mv /Volumes/{4,6}s2
sudo fuse-ext2 -o rw+ /dev/disk6s2 /Volumes/4s2

Actually just trying to scroll through some shell history wound up making the machine unusable, and it eventually necessitated a reboot. I’ve lost any home-made directories under /Volume and so I can just make them the way I want them. I am a little worried that maybe my hacks I made to g2fsprogs have introduced some critical instability to my kernel, but I fail to see how the xattrs stuff would have anything to do with my shell history. Perhaps it’s due to some zsh magic?

The commands again, and note that now we’re back to disk4:

sudo mkdir /Volumes/picard
sudo fuse-ext2 -o rw+ /dev/disk4s2 /Volumes/picard

And that worked! Now to make edits to the card.

5.2. Making Edits to the SD Card

5.2.1. sshd

I need to “enable” sshd, assuming it comes installed. If it does not come installed, this will have been a very wasteful effort.

Searching on this topic, I find an article with this:

There are also mechanisms to preconfigure an image without using Imager. To set up a user on first boot and bypass the wizard completely, create a file called userconf or userconf.txt in the boot partition of the SD card; this is the part of the SD card which can be seen when it is mounted in a Windows or MacOS computer.

This section doesn’t detail how to enable sshd, but the original post that lead me to this article has some additional instructions:

The empty ’ssh’ (or ssh.txt) file should act as a toggle (turn SSH on, and it will stay on unless you explicitly disable it later). If that’s not how it’s working for you, then something else that was done to the system has messed that up.

Right. So I didn’t need to do any of the prior section at all? Let’s look and be sure. I happen to recall that the default name of the SD card is FIRMWARE for whatever reason.

Okay, I’m going to try really hard not to swear off computers forever.

touch /Volumes/FIRMWARE/ssh
echo "$USER:$(echo 'lolno' | openssl passwd -stdin)" > /Volumes/FIRMWARE/userconf

The instructions use a -6 argument, but that doesn’t work for my openssl:

openssl version
LibreSSL 3.3.6

Which I believe is the stock version running on my MacBook, based on:

which openssl
/usr/bin/openssl

Otherwise it would be a deep path under /nix.

A quick check of the files shows they are there and in the desired state:

ls /Volumes/FIRMWARE/ssh
cat /Volumes/FIRMWARE/userconf
/Volumes/FIRMWARE/ssh
logan:oKYG/fePTExgs

I have no worries about the password here - it will be changed before this device connects to anything outside of my laptop.

Okay let’s give it a go. I eject the mounts (picard needs a sudo umount /Volumes/picard but works without a hitch).

An import caveat: I found from How the Raspberry Pi enables SSH when /boot/ssh.txt is present that these files are removed on boot - so we only get one shot, and if it fails we have to run it again. This is one of the benefits of blogging as I do this - the command history is at my fingertips.

The files are listed the configuration boot section documentation.

5.3. Connecting to the Pi (USB-C fail)

I did forget about networking, which isn’t covered in the boot section linked above. I had worked on this earlier but the 169.254.0.0/16 address space is way too large to nmap over. Instead I read in the boot section there’s an “OTG” (On The Go) USB setting that is enabled by default in config.txt (at least in my case, being it’s a Pi 4B). I don’t want to just plug it in and find out, and I actually did try raspberrypi.local earlier, I thought, and that didn’t work. I found a ping in my history to it in fact, so I know this was attempted and failed. There may be something else involved. This answer specifically talks about macOS and OTG. It does mention to use Bonjour to find the Pi, which apparently it advertises on.

The command link invocation is provided in this answer:

dns-sd -Gv4v6 raspberrypi.local

That doesn’t work. I do recall some chatter in the OTG+MacBook answer that talked about some problems with OTG and certain cables. It links to an article about an issue with USB-C on the Pi that is probably causing me problems. Essentially it means that the Pi isn’t powered properly if the USB-C cable is “e-marked” (meaning it can identify/query additional features).

So how do I know if the Pi got sufficient power or not? I can check the SD card to see if the configuration files were removed on-boot.

ls /Volumes/FIRMWARE | grep ssh
ls /Volumes/FIRMWARE | grep userconf
ssh
userconf

Sure enough, they are both present. So the Pi didn’t boot properly. I did have a solid red light, with an occasionally blinking green light. I don’t know what that means yet, but perhaps I should learn. I have read it is programmable and may have changed over time, so I don’t want to go too far down that path. In any case, we didn’t make it. For what it’s worth, the Pi was connected for a solid 5-10 minutes.

I only glanced at the USB-C diagrams in the article linked prior. I also skimmed through parts of the article itself. I would need a specifically bad USB-C cable - it would need to transmit data but not be “e-marked”. I suspect that would be hard to find.

I did learn somewhat recently that USB cables (even USB-C) sometimes are hard-wired to actually be a true “charging cable” in that all they do is provide power, and all of the data pins are grounded or shared. That’s not part of the spec, and it explains why a lot of people get cables that “work” but sometimes don’t (via reviews or one’s own personal experience). It’s a real shame we don’t have standard, human-readable markings for these differences. This video was really enlightening on the topic, and also shows off some fascinating circuitry in the thunderbolt cables which share the USB-C form factor.

So I need to plug this in via a battery or wall outlet, and I need to run an Ethernet cable between the Pi and my laptop. I do have the equipment on hand for all of this.

5.4. Imaging the Pi Correctly

I have blinking Ethernet lights. The Pi has a solid red light, and the green light is doing a dot-dot kind of pattern, repeatedly.

I’ve done a little looking into for the light patterns. The Pi 4B uses a new set of patterns. However my 2 pattern (two of anything but nothing else) doesn’t show up on the list. Checking my command history from earlier, I can see that I’m using a NixOS image. Perhaps that is related. I got that from this instruction available on the official NixOS documentation. It does assume a 4B model, but might be demanding more RAM than what I gave the Pi. My purchase order for the Pi shows it as 2 GB, but the documentation says it should be 4GB. I don’t know how hard that limitation is. Also, and perhaps most critically, this cannot work because it doesn’t use the Pi’s normal configuration. Okay now I think we’re onto something. I did want to do this via NixOS but I think I might be best off saving that for another day. This post has gotten enormous from all of the research and attempts already. I’ve also noticed the documentation tails behind the current NixOS image version. Ugh. I just need to do this myself - later.

6. Abandon Nix and Just Get it Doneprivate

As much as I wanted this to be something about Nix, it turned into a series of other hardships. NixOS introduced other, undocumented complications to the process. I still lack the ability to manage the Pi’s packages, let alone via the “Nix way”.

I used:

wget https://downloads.raspberrypi.com/raspios_arm64/images/raspios_arm64-2023-12-06/2023-12-05-raspios-bookworm-arm64.img.xz\?_gl\=1\*qvoa7r\*_ga\*MjYzMjA4NTA3LjE3MDMyMzk1Nzc.\*_ga_22FD70LWDS\*MTcwMzMzMTYzMi4yLjAuMTcwMzMzMTYzMi4wLjAuMA..
mv \
  '2023-12-05-raspios-bookworm-arm64.img.xz?_gl=1*qvoa7r*_ga*MjYzMjA4NTA3LjE3MDMyMzk1Nzc.*_ga_22FD70LWDS*MTcwMzMzMTYzMi4yLjAuMTcwMzMzMTYzMi4wLjAuMA..' \
  2023-12-05-raspios-bookworm-arm64.img.xz
unxz 2023-12-05-raspios-bookworm-arm64.img.xz

Oh oh, I did get to use Nix a little! I had to install the xz package to get unxz.

And then flashed the card with:

sudo dd if=2023-12-05-raspios-bookworm-arm64.img of=/dev/disk4 bs=1M status=progress conv=fsync

7. Building the Image

Okay, let’s build our own NixOS image for the Pi.

7.1. Prior Art

I came across Headless Raspberry Pi on NixOS Discourse and user Sweenu states they have a working version of NixOS being deployed to a Raspberry Pi. The source is all posted to https://github.com/sweenu/nixfiles. From some brief research, it does use a tool called digga that states that it is no longer recommended for use, which is expanded upon in issue#503 for the repository. In essence, digga seems to have some customization / scaling difficulties because it focused on ergonomics first. There isn’t any real alternatives recommended that are purpose built for what digga does (which, to be honest, I’m not entirely sure what it does). I’d prefer to keep my research to shiny new things to a minimum, so I hope I can adlib what should be already working for someone else, but to fit my needs. Primarily I want image generation that gives me a working NixOS based Raspberry Pi image, but also have it configured in such a way that I don’t have to re-flash the SD card every time I want to make a change. In other words, I want to be able to build an image but also be able to manage the host configuration via a standard Nix configuration.

In the Discourse, Sweenu states the Pi can be updated thusly:

nix flake update
deploy ".#grunfeld"

Where grunfeld is the name of the Raspberry Pi.

In the repository’s README, it states the image can be created and copied like so:

nixos-generate --flake '.#grunfeld' --format sd-aarch64 --system aarch64-linux
unzstd -d {the output path from the command above} -o nixos-sd-image.img
sudo dd if=nixos-sd-image.img of=/dev/sda bs=64K status=progress

This is all very promising. I’ve done some copying/adlibing of the repository in my proton-nix repository, which I plan on using to declare my local network configuration. I do have some security concerns about open sourcing it, especially because both public and private keys seem to be present in the repository (even if the private keys are encrypted at rest). I would like a way to share this, without the private bits hanging out for all to see. This has been something I’ve looked into before with Nix and nix-flakes in particular because flakes really wants everything to be self contained, lest it be marked as “impure”. From reading Ian’s blog I mentioned earlier, it does seem like one can reference a file using a git repository link. If flakes is happy with this, then I think I could be very happy with it as well.

But first, we need a detour. I don’t have a means of actually using NixOS because I’m on macOS. As I understand, this does require work with a container or VM.

7.2. Doing Real Nix Things on macOS

On this current MacBook, I’ve successfully avoided running Docker. I’ve come to liken Docker to bloatware (whether that’s fair or not). On my home network, I’ve had a degree of success with Podman. However there’s hitches with it, as there always is. I also have a very small amount of space available on my MacBook, and containerd containers aren’t known for their small size - unless they use Alpine, I guess. Either way it will need an entire VM to work - because macOS doesn’t have cgroups et. al. There appears to be some promising work done to make containers work on macOS but it’s way too immature (as of [2023-12-24 Sun]) for the kind of work we’re doing here.

My detour needs an additional detour: I have to clean up some space. I presently have < 30GB free and I would love to be running with closer to 100GB to feel like I can quickly iterate on things without constantly needing to do a nix gc or docker system prune --force, and also thereby increasing my build iteration times significantly (those caches are present for good reason).

Begin space cleaning montage!

Alright I’ve deleted a bunch of things I had no business keeping, and things I probably won’t remember that I deleted later. ncdu was instrumental there!

In a vacuum I’d prefer podman to avoid my perceived bloatware of Docker proper. I don’t recall any current perils, and there does appear to be some support for running podman on aarch64-darwin (my specific setup), so let’s give it a shot.

I did find nixpkgs#169118 which outlines some issues with podman on macOS, but they seem to be resolved and nobody feels confident enough to declare it done. This commit that refers to the ticket just installs qemu and podman together (but with no direct relationship - I don’t know how that’s supposed to work). qemu is a significant part of working with the VM, I’ve gathered.

I found Just, Nix Shell And Podman Are A Killer Combo and learned that Just is a thing. The post has some great incantations in it that I was happy to leverage into this with-podman.sh script:

#!/usr/bin/env bash
set -euo pipefail
container_name="nix-run"
script="$@"

podman machine init --cpus 12 --memory 8192 --disk-size 50 \
      --volume $HOME:$HOME || true
podman machine start || true
podman container ls -a | grep $container_name > /dev/null || \
        podman create -t --name $container_name -w /workdir \
            -v $PWD:/workdir nixos/nix
container_id=$(podman start $container_name)
echo "$container_id"
podman exec $container_id $script
podman stop $container_name || true
podman machine stop

Which basically sets up the Podman VM, starts the container nixos/nix container (named nix-run), executes the script (whatever I pass in for args), and then cleans it all up. It’s very expensive but it’s also something I can use thoughtlessly and I really like that.

Here’s an example run:

@ ./with-podman.sh ls -al
Error: podman-machine-default: VM already exists
Error: cannot start VM podman-machine-default: VM already running or starting
a34354199be6cca047309f9d29ecefeeb1c4521d776536cc6305bad8380874b6
nix-run
total 12
drwxr-xr-x 11 root nobody  352 Dec 24 13:17 .
dr-xr-xr-x  1 root root     77 Dec 24 13:26 ..
drwxr-xr-x 10 root nobody  320 Dec 24 13:23 .git
-rw-r--r--  1 root nobody  283 Dec 22 00:50 README.org
-rw-r--r--  1 root nobody 3809 Dec 24 00:29 flake.nix
drwxr-xr-x  3 root nobody   96 Dec 24 00:30 hosts
drwxr-xr-x  4 root nobody  128 Dec 24 00:40 modules
drwxr-xr-x  3 root nobody   96 Dec 24 00:42 pkgs
drwxr-xr-x  3 root nobody   96 Dec 24 00:52 profiles
drwxr-xr-x  4 root nobody  128 Dec 24 00:41 shell
-rwxr-xr-x  1 root nobody  529 Dec 24 13:26 with-podman.sh
nix-run
Waiting for VM to exit...
Machine "podman-machine-default" stopped successfully

~/dev/proton-nix logan@scandium 0 [22:27:20] 75s
$

Note it took 75 seconds to run on my system just to do ls but it works without me doing anything fancy, I’m running NixOS, and the repository I’m working in is properly volume mounted. I’m not really worried about speed here, especially because these invocations I’m about to do will generate disk images - an already lengthy affair I expect to take on the order of minutes. Perhaps I will find short cuts to speed up iteration as I experience pain with building the image improperly, but I want to reserve my grease until the wheels begin to squeak.

A couple of asides:

  1. I looked at Just since this is my first exposure to it. From some quick reading around, it seems to be a marginally better make. make is portable, standard, and yes it has aged but I believe it has aged moderately well. I would not drop portability and familiarity (due to it being fairly standard) just for a marginal improvement. Plus, if the file starts to get too squirrelly, I’d rather just break out the complex bits into standalone scripts. If I really could wave a wand and have anything I wanted in terms of love to make, it would be to give me something like rake -T where I can simply print all of the tasks available to me. Reading the code is not documentation, even if it is a Makefile.
  2. I tend to go down the rabbit hole on a lot of these ventures, but I’m not posting all of it as I go. Indeed, I just closed a tab pointing to r/programminganimememes, which was serving as nothing other than pure distraction. I’d followed link to it from the same article linked above.

Now that I have a real Nix system available to me, I need to try to run my paired down repository from sweenu and see if I can build an image from it.

7.3. Building the NixOS Pi image

7.3.1. Trying to Make it Work with digga

From sweenu’s earlier invocations, I have adapted it to be:

./with-podman.sh "
nixos-generate --flake '.#iron' --format sd-aarch64 --system aarch64-linux;
"

Which outputs:

Error: crun: executable file `nixos-generate` not found in $PATH: No such file or directory: OCI runtime attempted to invoke a command that was not found
Error: read unixpacket @->/proc/self/fd/13/attach: read: connection reset by peer

I hope that unixpacket error is just some weirdness with error handling - an error of an error. I’ll test again with ls -al… and the results are the same successful results I had before. I even tried with "ls -al" to make sure there isn’t some weird quoting issues, but there isn’t - all is well.

I tried a nix invocation I know to work:

./with-podman.sh nix-channel --list

And I get:

nixpkgs https://nixos.org/channels/nixpkgs-unstable

Which is a very reasonable/expected output. So some nix tools are available, but why can’t it find nixos-genereate? This is literally my first time using any command prefixed with nixos, so I’m assuming that this command actually exists under normal circumstances and that might be too heavy an assumption. Some searching reveals… nothing? Not at all what I expected. I expected something, but just not on my path for whatever reason. I double check and realize I’m searching for nix-generate instead of nixos-generate, whoops.

nixos-genereate is supplied by nixos-generators and it requires installation - it’s not something that ships with nixos.

./with-podman.sh "
nix run github:nix-community/nixos-generators -- \
  --flake '.#iron' --format sd-aarch64 --system aarch64-linux;
"

And I get:

error: experimental Nix feature 'nix-command' is disabled; add '--extra-experimental-features nix-command' to enable it

Ah. I’ve seen these before. This just makes our invocation a little more tricky with:

./with-podman.sh "
nix --extra-experimental-features nix-command \
  run github:nix-community/nixos-generators -- \
    --flake '.#iron' --format sd-aarch64 --system aarch64-linux;
"

The complexity and nesting here is starting to make me a little nervous. Prior trauma experience tells me that I will soon be running into shell quoting issues.

Now I see:

error: experimental Nix feature 'flakes' is disabled; add '--extra-experimental-features flakes' to enable it

Sigh. Nix, you couldn’t have told me about both of them at once? Furthermore, it would’ve told me the proper thing to do for enabling multiple experimental features at once. So I wing it:

./with-podman.sh "
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  run github:nix-community/nixos-generators -- \
    --flake '.#iron' --format sd-aarch64 --system aarch64-linux;
"

I see a lot of stuff going on with the nix store - a sign of progress!

But we end with:

warning: Ignoring setting 'auto-allocate-uids' because experimental feature 'auto-allocate-uids' is not enabled
warning: Ignoring setting 'impure-env' because experimental feature 'configurable-impure-env' is not enabled
building '/nix/store/531cw9n4lxqx3w830ijackc5xffgz27k-nixos-generate.drv'...
/nix/store/7nkr6ysd859f6pwsn0k32qr8w368rmcz-nixos-generate/bin/.nixos-generate-wrapped: line 72: awk: command not found

You’d think nixos-generators would just include awk. Maybe I can submit a PR…? Let’s cook a new invocation:

./with-podman.sh "
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  run awk github:nix-community/nixos-generators -- \
    --flake '.#iron' --format sd-aarch64 --system aarch64-linux;
"

I get:

error: cannot find flake 'flake:awk' in the flake registries

It should be noted the pkg1 pkg2... notation I used is taken from the explanation in this Discourse about nix shell vs nix run but the help/man page for nix run gives no indication that this can be done. If anything it indicates that this cannot be done because the invocation demands installable as a single argument and other arguments have flags.

More reading in that same Discourse shows that nix run might not be suitable for this, or is otherwise a work in progress. Okay fine. I find some suggestions on nix shell that do what I want in the same Discourse once again. Let’s try it out.

Actually I try several things out, and eventually land on:

./with-podman.sh "
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  shell awk github:nix-community/nixos-generators -- \
    --flake '.#iron' --format sd-aarch64 --system aarch64-linux;
"

I ran into:

path '/workdir/--flake' does not contain a 'flake.nix', searching up
error: getting status of '/workdir/--flake': No such file or directory

So I just remove --flake and use:

./with-podman.sh "
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  run 'nixpkgs#awk' github:LoganBarnett/nixos-generators#with-awk -- \
    --flake '.#iron' --format sd-aarch64 --system aarch64-linux;
"

To get:

./with-podman.sh "
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  shell \
      -c \
         "'"'"nixos-generate \
                             --flake '.#iron' \
                             --format sd-aarch64 \
                             --system aarch64-linux \
         "'"'"
"

Ugh I really went off the tracks trying things out. I have an incantation that finally exercises the bits I want, but the actual nix code is busted somehow. I suspect the version of digga that sweenu uses and my own differ. I need to look into some more things. I wonder if I can just use nixos-generate by itself, without all of this extra stuff? My other two paths are to either pin my repo to the same version that sweenu has in the flake.lock or follow another example (https://github.com/divnix/digga/pull/501 for a working(?) one).

./with-podman.sh " \
ls -al . ;
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron' \
"

This invocation helps a lot, and keeps it so I don’t need to keep running directly on the VM while still working out silly issues.

nix flake check --show-trace

This is the point where I just tried a bunch of different things. Many a straw were grasped. I tried following obscure error messages. The sight of broken “magic” is not new to me, and this section of stuff really felt like it. So I abandoned digga entirely.

7.3.2. Just Use nix-generators

Now I have the following configuration:

The flake.nix:

{
  description = ''iron.proton imaging and configuration.
iron.proton is a cross-region routing device.
'';
  inputs = {
    nixpkgs.url = "nixpkgs/nixos-unstable";
    nixos-generators = {
      url = "github:nix-community/nixos-generators";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
  outputs = { self, nixpkgs, nixos-generators, ... }: {
    packages.aarch64-linux = {
      iron = nixos-generators.nixosGenerate {
        format = "sd-aarch64";
        modules = [
          ./iron-configuration.nix
        ];
        system = "aarch64-linux";
      };
    };
  };
}

The linked iron-configuration.nix is this:

# This is the NixOS configuration for iron.proton.  It is drawn from the example
# here:
# https://github.com/Misterio77/nix-starter-configs/blob/main/minimal/nixos/configuration.nix
{
  inputs,
  lib,
  config,
  pkgs,
  ...
}: {
  imports = [
    ./hardware-configuration.nix
  ];

  nixpkgs = {
    overlays = [
    ];
    config = {
      allowUnfree = true;
    };
  };
  nix.nixPath = ["/etc/nix/path"];
  environment.etc =
    lib.mapAttrs'
    (name: value: {
      name = "nix/path/${name}";
      value.source = value.flake;
    })
    config.nix.registry;
  nix.settings = {
    experimental-features = "nix-command flakes";
    auto-optimise-store = true;
  };
  networking.hostName = "iron";
  # TODO: This is just an example, be sure to use whatever bootloader you prefer
  # I guess grub or whatever is used implicitly somewhere?  Keeping this (which
  # comes from the example verbatim) results in:
  #   The option `system.build.installBootLoader' is defined multiple times
  #   while it's expected to be unique.
  # boot.loader.systemd-boot.enable = true;
  users.users = {
    logan = {
      # TODO: You can set an initial password for your user.
      # If you do, you can skip setting a root password by passing
      # '--no-root-passwd' to nixos-install.
      # Be sure to change it (using passwd) after rebooting!
      initialPassword = "correcthorsebatterystaple";
      isNormalUser = true;
      openssh.authorizedKeys.keys = [
        "my public key"
      ];
      extraGroups = ["wheel"];
    };
  };
  services.openssh = {
    enable = true;
    settings = {
      # Forbid root login through SSH.
      PermitRootLogin = "no";
      # Use keys only. Remove if you want to SSH using password (not
      # recommended).
      PasswordAuthentication = false;
    };
  };
  system.stateVersion = "23.05";
}

And then hardware-configuration.nix is this:

{
  fileSystems."/" = {
    # Must match what sd-image expects exactly.  This is found by trying to run
    # anything and then encountering an error.
    device = "/dev/disk/by-label/NIXOS_SD";
    fsType = "ext4";
  };
  nixpkgs.hostPlatform = "aarch64-linux";
}

If you’re comparing from the example mentioned in the comments, this does without the nix.registry setting because that was causing problems. I also removed the boot loader declaration, because that seems to be implicit or configured in some other way that is not obvious to me.

The error for the boot loader is this:

error: The option `system.build.installBootLoader' is defined multiple times while it's expected to be unique.
       Only one bootloader can be enabled at a time. This requirement has not
       been checked until NixOS 22.05. Earlier versions defaulted to the last
       definition. Change your configuration to enable only one bootloader.

Unfortunately I don’t have an error capture for nix.registry.

The nix.registry error:

error:
       … while calling the 'derivationStrict' builtin

         at /derivation-internal.nix:9:12:

            8|
            9|   strict = derivationStrict drvAttrs;
             |            ^
           10|

       … while evaluating derivation 'nixos-sd-image-24.05.20231222.6df37dc-aarch64-linux.img'
         whose name attribute is located at /nix/store/55ql4j8d47xjvfa8xggjddgfwny4n9j7-source/pkgs/stdenv/generic/make-derivation.nix:348:7

       … while evaluating attribute 'buildCommand' of derivation 'nixos-sd-image-24.05.20231222.6df37dc-aarch64-linux.img'

         at /nix/store/55ql4j8d47xjvfa8xggjddgfwny4n9j7-source/nixos/modules/installer/sd-card/sd-image.nix:182:7:

          181|
          182|       buildCommand = ''
             |       ^
          183|         mkdir -p $out/nix-support $out/sd-image

       (stack trace truncated; use '--show-trace' to show the full trace)

       error: attribute 'inputs' missing

       at /nix/store/55ql4j8d47xjvfa8xggjddgfwny4n9j7-source/lib/modules.nix:508:28:

          507|         builtins.addErrorContext (context name)
          508|           (args.${name} or config._module.args.${name})
             |                            ^
          509|       ) (lib.functionArgs f);

Divining the fix was mostly a guess, but based on:

  1. The inputs missing. The nix.registry line has inputs in there.
  2. The nix.registry line has a comment saying “To make nix3 commands consistent with your flake” and this looks like speculative future-proofing to me. The future is likely now, and the speculation proven false.
  3. I have inputs very clearly in the flake. Everything else looks “tidy”.

Okay so this all generates an image by running:

./with-podman.sh " \
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron' \
"

I get no meaningful output, but no news is good news in the Unix world. I see that my local directory gets an result file. It’s a symlink.

result -> /nix/store/dv21jmin4aqq4m0zvlbfbw2d6c1d47sr-nixos-sd-image-24.05.20231222.6df37dc-aarch64-linux.img

7.3.3. Extracting the Image

I quickly discovered that the symlink is orphaned.

ls -al $(readlink result) 2>&1
ls: cannot access '/nix/store/dv21jmin4aqq4m0zvlbfbw2d6c1d47sr-nixos-sd-image-24.05.20231222.6df37dc-aarch64-linux.img': No such file or directory

I bet there is some containerd trickery going on here. Here’s what I think is happening:

  1. I know from my script in with-podman.sh, the container is volume-mounting $HOME. It is not volume mounting /nix.
  2. The file is actually written to /nix in the container. This is inaccessible to me once the container is shut down.
  3. The symlink emitted isn’t translated to whatever temporary file that podman is actually writing to.
  4. Thus I am left with a dead symlink.

I think the way we can fix this is to copy the image and then remove symlink (to keep things clean, it’s not strictly necessary).

./with-podman.sh " \
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron'
cp \$(readlink result) /workdir/nixos-pi.img
rm result
"

This doesn’t work because the symlink points a directory, and not the image file itself. I did some digging and found I can do this:

./with-podman.sh " \
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron'
cp \$(readlink result)/sd-image/* /workdir/nixos-pi.img.zst
rm result
"

Note the added .zst. Also there is another directory adjacent to sd-image called nix-support. I’ll take a look at that as I do more runs.

I do need to add the ability to decompress this image in preparation for dd. Let’s yank our earlier invocation and place it in this command chain. If I wanted to muck around with the container image itself, I could use the unzstd invocation to specify an output directory, which means we avoid a cp of the entire image. I don’t want to muck around with the container image as much as I can though.

./with-podman.sh " \
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron'
ls -al \$(readlink result)/nix-support
cp \$(readlink result)/sd-image/* /workdir/nixos-pi-sd-image.img.zst
rm result
"
unzstd -d nixos-pi-sd-image.img.zst -o nixos-pi-sd-image.img

One of the things I really like about this journaled approach to exploration is that it slows me down enough to ask questions that make me avoid pitfalls. As I am writing, I am thinking “How do I know the result from unzstd is actually a valid image without having to push it through dd and then slotting the card into the Pi?

Some quick searching reveals I can use hdiutil imageinfo <file> to get this information.

Let’s see if it tells us that it is not looking at a valid image:

hdiutil imageinfo nixos-pi-sd-image.img.zst 2>&1
echo $?
hdiutil: imageinfo failed - image not recognized
1

Excellent! And the positive case?

hdiutil imageinfo nixos-pi-sd-image.img 2>&1
echo $?
Class Name: CRawDiskImage
Size Information:
	Total Bytes: 2829352960
	Compressed Ratio: 1
	Sector Count: 5526080
	Total Non-Empty Bytes: 2829352960
	Compressed Bytes: 2829352960
	Total Empty Bytes: 0
Checksum Type: none
Format: UDRW
partitions:
	partition-scheme: fdisk
	block-size: 512
	partitions:
		0:
			partition-name: Master Boot Record
			partition-start: 0
			partition-synthesized: true
			partition-length: 1
			partition-hint: MBR
			boot-code: 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004E6978210000
		1:
			partition-name:
			partition-start: 1
			partition-synthesized: true
			partition-length: 16383
			partition-hint: Apple_Free
		2:
			partition-start: 16384
			partition-number: 1
			partition-length: 61440
			partition-hint: DOS_FAT_32
			partition-filesystems:
				FAT16: FIRMWARE
		3:
			partition-start: 77824
			partition-number: 2
			partition-length: 5448256
			partition-hint: Linux_Ext2FS
	burnable: false
Format Description: raw read/write
Checksum Value:
Properties:
	Encrypted: false
	Kernel Compatible: true
	Checksummed: false
	Software License Agreement: false
	Partitioned: false
	Compressed: no
Segments:
	0: /Users/logan/dev/proton-nix/nixos-pi-sd-image.img
Backing Store Information:
	URL: file:///Users/logan/dev/proton-nix/nixos-pi-sd-image.img
	Name: nixos-pi-sd-image.img
	Class Name: CBSDBackingStore
Resize limits (per hdiutil resize -limits):
5526080	5526080	181452016
0

Wonderful!

We can incorporate that into our eventual script we’ll be creating, just to check on things.

./with-podman.sh " \
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron'
ls -al \$(readlink result)/nix-support
cp \$(readlink result)/sd-image/* /workdir/nixos-pi-sd-image.img.zst
rm result
"
unzstd -d nixos-pi-sd-image.img.zst -o nixos-pi-sd-image.img
rm nixos-pi-sd-image.img.zst
hdiutil imageinfo nixos-pi-sd-image.img

And then we can incorporate the dd invocation into it:

./with-podman.sh " \
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron'
ls -al \$(readlink result)/nix-support
cp \$(readlink result)/sd-image/* /workdir/nixos-pi-sd-image.img.zst
rm result
"
unzstd -d nixos-pi-sd-image.img.zst -o nixos-pi-sd-image.img
rm nixos-pi-sd-image.img.zst
hdiutil imageinfo nixos-pi-sd-image.img
sudo dd if=nixos-pi-sd-image.img of=/dev/disk4 bs=1M status=progress conv=fsync

Let’s not run that yet, because just blindly doing dd on things under /dev is inherently dangerous. Is there a way we can query for a detachable disk?

There is for macOS, but it’s not portable. I have seen some mention that lsusb works for non-USB storage devices (and by extension, cyme would as well), but I have not observed that on my system. Still, learning of cyme is useful and it’s been added to my toolbox for later.

This is what I came up with:

system_profiler SPStorageDataType -json \
  | jq -r \
      '.SPStorageDataType.[]
        | select(.physical_drive.protocol == "Secure Digital")
        | .bsd_name' \
  | sed -E 's/s[0-9]+//'

Note that this might not work if using a USB attached SD card. This could be the case if you’re using a docking station setup on your Mac/MacBook, instead of preferring the on-board SD card reader.

Okay so now we have the disk, dynamically. We can just idiotically bail out if more than one is found. We should only find one and only one result.

Ugh. I really wish we could actually replace Bash with a stronger scripting language but with the same level of ubiquitousness. This is what I was able to put together for giving me a one-and-only-one search:

#!/usr/bin/env bash

# Can't use -u here because empty arrays are "unset" according to some kind of
# Bashism, and figuring out the incantation to work around it has not been
# fruitful.  See
# https://stackoverflow.com/questions/7577052/bash-empty-array-expansion-with-set-u
# for some of those details.
set -eo pipefail

IFS=$'\n'
result=( $(system_profiler SPStorageDataType -json \
  | jq -r \
      '.SPStorageDataType.[]
        | select(
           .physical_drive.protocol == "Secure Digital" or
           .physical_drive.protocol == "USB"
        )
        | .bsd_name' \
  | sed -E 's/s[0-9]+//'
) )
unset IFS
count="${#result[*]}"
if [[ $count != '1' || ${result[0]} == '' ]]; then
  echo "Error: '$result' has $count results but we expect exactly 1 non-empty \
result." 1>&2
  exit 1
else
  echo -n $result
fi

Update: I thought I could safely add set -euo pipefail to polish up my script and no further testing was needing, but it turned out it needed some updating. This is the final result I was able to come to. Bash seems to only really thrive with the most trivial of scripts, and it takes so little to start pulling out some nasty stuff.

I tested with 1 device and 4 devices, and got the results I wanted.

This leaves us with a final script of:

#!/usr/bin/env bash

./with-podman.sh " \
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron'
ls -al \$(readlink result)/nix-support
cp \$(readlink result)/sd-image/* /workdir/nixos-pi-sd-image.img.zst
rm result
"
unzstd -d nixos-pi-sd-image.img.zst -o nixos-pi-sd-image.img
rm nixos-pi-sd-image.img.zst
hdiutil imageinfo nixos-pi-sd-image.img
sudo dd \
     if=nixos-pi-sd-image.img \
     of=/dev/$(./sd-card-dev-find.sh) \
     bs=1M \
     status=progress \
     conv=fsync

I found as part of futzing with this that SPStorageDataType only works for mounted devices but not available devices. However I need things to be unmounted to use dd on them. This does not make it easy to do ephemeral runs. If I wanted to always use SPStorageDataType, then I’d need to know what to mount, which is what I’m trying to determine in the first place. Additionally, the disk might be totally fresh or using some unknown filesystem - both of which would render the device unmountable. With some checking around from system_profiler -listDataTypes I found SPCardReaderDataType which looks promising:

system_profiler SPCardReaderDataType

And now the JSON form:

system_profiler SPCardReaderDataType -json

That gives me a jq query of:

system_profiler SPCardReaderDataType -json \
  | jq -r \
      '.SPCardReaderDataType[]._items[].bsd_name'

This leaves me with a final script for sd-card-dev-find.sh of:

#!/usr/bin/env bash
##
# Finds the device file for the attached SD card (eg. /dev/disk4) or USB drive.
# It will assume /dev as a relative directory to make consumption easier.
#
# This has a non-zero exit code if more than one disk is found, or no disks are
# found.
##

set -euo pipefail

while true; do
  case "${1:-}" in
    -h | --help)
      cat <<EOH
Usage: $0

Finds the device file for the attached SD card (eg. /dev/disk4) or USB drive.
It will assume /dev as a relative directory to make consumption easier.
EOH
      exit
      ;;
    * ) break ;;
  esac
done

IFS=$'\n'
sd_results=$(
  system_profiler SPCardReaderDataType -json \
    | jq -r \
      '.SPCardReaderDataType[]._items[].bsd_name'
)
usb_results=$(
  system_profiler SPUSBDataType -json \
    | jq -r \
      '.. | select(.bsd_name? and .volumes?).bsd_name'
)
unset IFS
array=( "${sd_results[@]}""${usb_results[@]}" )
count="${#array[@]}"
if [[ $count != 1 ]]; then
  echo "Error: Query result '${array[@]}' has $count results but we expect exactly \
1." 1>&2
  exit 1
else
  echo -n "${array[@]}"
fi

Update: I added the capability to also check for USB drives.

Which also includes -h / --help and a comment describing how it works. It might seem obvious, and it is, but that’s because it’s fresh. I’ve made life easier for future-me.

Here’s the same treatment for image-create.sh:

#!/usr/bin/env bash
##
# Creates a bootable Raspberry Pi image with NixOS.  This image should contain
# the entirety of the configuration, sans password updates (until I can get
# secret management going).
#
# The image file name is printed upon success.
##

set -euo pipefail

while true; do
  case "${1:-}" in
    -h | --help)
      cat <<EOH
Usage: $0

Creates a bootable Raspberry Pi image with NixOS.  This image should contain
the entirety of the configuration, sans password updates (until I can get
secret management going).

The image file name is printed upon success.
EOH
      exit
      ;;
    * ) break ;;
  esac
done

image_name='nixos-pi-sd-image.img'
./with-podman.sh " \
nix \
  --extra-experimental-features nix-command \
  --extra-experimental-features flakes \
  build '.#iron'
ls -al \$(readlink result)/nix-support
cp \$(readlink result)/sd-image/* /workdir/$image_name.zst
rm result
"
unzstd \
  --force \
  --decompress $image_name.zst \
  -o $image_name
rm --force $image_name.zst
hdiutil imageinfo $image_name

echo 'Image built successfully.' 1>&2
# Print the name used so we can use it elsewhere, and scripts needn't be tightly
# coupled.
echo $image_name

And I further broke things apart, such that image-deploy.sh actually installs it. I have done this because building the image is its own ordeal, and something I might want to test separately. Note that image-create.sh prints the file name of the image so image-deploy.sh can use it, and doesn’t need to have intimate knowledge of the file name. I have one place I can change the image file name (or parameterize it), and nothing else needs to be updated.

7.4. No Joy on Boot

The Pi seems to “boot”, but results in the double-green flashes I had before I went down the NixOS image approach. I happen to have a second Pi with me, and have tried it there, as well as a second card with the same image. I don’t feel like this has ruled out hardware problems.

My card is from SanDisk, and is the SanDisk Ultra 128GB microSDXC UHS-I Card (SDSQUNC-128G-GN6MA) variant. https://elinux.org/RPi_SD_cards indicates that this should work. Many of the recent SanDisk cards are reported as working. The Amazon reviews say that they specifically work for Raspberry Pis as well. Even though I got them from the same batch (as well as the Pis), I suspect this is not a hardware issue, but I haven’t ruled it out.

I need a display to see if there’s some other critical information before I can continue.

I did install f3 utilities to test it (with f3read and f3write) but I haven’t taken them for a spin yet.

7.5. Back, and Display Ports

My travels are now complete, and I’m able to inspect things with my home network. A curious thing that came up was the displays ports. The new (circa 2023) Raspberry Pi’s come with micro-HDMI connections for display. I did some searching around and found that HDMI actually requires royalties are paid (perhaps the Pis are exempt?) at the cost of $0.15 USD or $0.05 (or lower) if the HDMI logo is displayed per Wikipedia’s HDMI article. The Pi has the logo printed on PCB. The capabilities between HDMI. are on par with each other, from what various posts I could find. Apologists have reported that HDMI is more universal, and thus more in line with the goals of the Pi to make cheaply accessible computers. I still had to buy an adapter to connect it to any of my displays made in the last 10 years. Granted, the adapter wasn’t terribly expensive. It did halt my work though. Now I have one, and a display to connect it to. The results were surprising!

7.6. Actually Joy on Boot

I can see the Pi has actually booted up, and it works! It successfully boots, and the flashing green light must just be “activity” that is on some regular interval. The Bluetooth or WiFi radio, perhaps? Either way, I have a working Pi! Let’s see if I can log in.

$ ssh iron.proton
The authenticity of host 'iron.proton (192.168.254.102)' can't be established.
ED25519 key fingerprint is SHA256:E3b/T0lQiqItcIKbh2n6Wb/sOEV9nFbrVojJXlKmHrQ.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'iron.proton' (ED25519) to the list of known hosts.

[logan@iron:~]$

Wow! I’d forgotten I’d added one of my authorized keys to the list. I have to admit, I’m pretty giddy. I do have a password to update. A quick passwd invocation shows the initialPassword field is good, but you won’t see anything interesting here:

[logan@iron:~]$ passwd
Changing password for logan.
Current password:
New password:
Retype new password:
passwd: password updated successfully

And then to see if I can use sudo:

[logan@iron:~]$ sudo ls

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:

    #1) Respect the privacy of others.
    #2) Think before you type.
    #3) With great power comes great responsibility.

For security reasons, the password you type will not be visible.

[sudo] password for logan:

[logan@iron:~]$

So this ticks all of my boxes for getting a Pi configured on the baseline, but lets go through the list to be sure:

  • [X] The Pi has the desired hostname.
  • [X] The Pi has my user.
  • [X] My user can be logged into via SSH.
  • [X] My user is a sudoer.

That’s everything!

8. Conclusion

I’ll wrap this up here. The next part of this adventure will be to get Wireguard configured on the Pi. That’s a bit beyond the scope of what I feel safe sharing here.

I have noticed during my searching about that there are these age secrets that I intend to read up on, though I find it hard to believe these secrets are safely shared on public repositories. Offline brute force attacks are highly effective, from what I understand.