Omarchy is x86-64 only. There is no Pi image, no ISO, nothing to download for ARM. An unofficial port existed on paper, but nobody had proven it boots on real hardware rather than in a virtual machine. This is what it took to get it running on a Raspberry Pi 5 — every fault, including the ones caused by my own confident wrong answers.
After this page you’ll know: how the system works, what is genuinely proven on hardware, and what still does not work.
Alpha · unofficial · one of ours
Verified on real Pi 5 hardware
Not yet something you can download
Last updated
First
Confirmed boot of this port on real Pi 5 hardware
6
Root-cause faults found and fixed
5
Upstream bugs identified for the maintainers
428
Boot files verified byte-for-byte against the image
The setup — three things
What this actually is
Everything below is kernel logs. None of it means anything without these three.
Omarchy
An opinionated Arch Linux desktop
A complete, pre-configured Linux desktop built on Arch and the Hyprland window manager, put together by David Heinemeier Hansson — the creator of Ruby on Rails. It is not a theme or a config repo: it is a full system with its own package set, installer and defaults. It runs on x86-64 only.
The Raspberry Pi 5
A £60 ARM computer the size of a credit card
A 64-bit ARM board — a different processor architecture entirely from the Intel and AMD chips Omarchy targets. Software compiled for one does not run on the other. Everything Omarchy ships has to exist as an aarch64 build before any of it can start.
The gap
There is no image to download
Arch’s official support does not extend to aarch64, and ARM support for Omarchy is an open feature request rather than a shipped thing. No official Pi image, no ISO, nothing to flash. The only route is an unofficial port that you build yourself from source — and when this started, nobody had proven it boots on a real Pi rather than in a virtual machine.
The port — 06 phases
Where the port stands
Phase 04 is the one this project moved.
01aarch64 build environmentDocker under WSL2, emulating arm64 on an x86-64 machine.Done
02Rebuild the x86-only packages19 rebuilt from PKGBUILDs, 4 built separately, 6 dropped.Done
03Bootable VM image and installer on ARMProven in a virtual machine before any hardware was involved.Done
04Verify on real Pi 5 hardwareThis project. Nobody had done it when this started; it is done now.Done
05Automated image releases via CIScaffolded upstream, not yet producing downloadable images.In progress
06Host an aarch64 pacman repoNot started. This is the one that blocks updates — see below.Not started
The build
What it runs on, and what had to be rebuilt
Assembly avoids loop devices entirely, so it runs unprivileged under Docker on Windows.
Target board
Raspberry Pi 5, 8GB
Storage
128GB microSD
Power supply
Official Raspberry Pi 27W (5.1V / 5A)
Build machine
Windows 11 + WSL2 + Docker Desktop
Build architecture
x86-64, emulating arm64
Omarchy version
4.0.1, 148 packages
The build runs in Docker under WSL2 on a Windows machine, emulating arm64 on x86-64. Three sibling repositories are required: the port itself, a package repo carrying the rebuilt PKGBUILDs, and a Pi 5 branch of Omarchy holding the ARM patches.
Image assembly deliberately avoids loop devices and mounting, so it needs no elevated privileges — the ext4 filesystem is populated with mke2fs -d and the FAT partition with mtools. That is the whole reason this works under Docker Desktop on Windows at all.
122
Packages installed straight from Arch Linux ARM
19
Rebuilt from PKGBUILDs, mostly a one-line arch= addition
4
Built separately from source
6
Dropped as x86-only — none needed for the desktop
The potholes — 06 faults
Six things stood between me and a working desktop
Pick one to open it. Four of the six had a confident wrong answer first — those are written up too.
№The faultWhat it looked like
Choose a fault above — the detail opens hereOne at a time
Fault 01of 06
The image would not boot at all
Firmware read the card and rejected the partition table
What it printed
GPT: No bootable partitions
The answer that was wrong
The obvious suspicion was a malformed partition table, and there was an open upstream bug — rpi-eeprom #585 — about the Pi mishandling a GPT partition array that is not exactly at LBA 2. It looked like a match. It was not. Reading the raw GPT header bytes off the image showed the array exactly where it should be, both CRCs valid, the backup header present, and sgdisk -v reporting "No problems found". The image was textbook-valid GPT. And #585 had been fixed in October 2024, nineteen months before this Pi’s firmware. Two independent reasons it could not be the cause.
What was actually going on
build-image.sh branches on $VARIANT for the kernel and the bootloader — but not for the partition table. Both variants got GPT plus an EFI System Partition. That is correct for the VM variant, which boots systemd-boot over UEFI. It is simply the wrong layout for a Pi, which boots from its own firmware off a FAT partition with no UEFI involved anywhere. The GPT was a carry-over from the VM path, not a decision. The clue had been sitting in the notes for days: an Arch Linux ARM probe card had booted fine on this exact board a week earlier — and it was MBR.
The fix
MBR with a type 0x0c boot partition, matching what pi-gen — the official Raspberry Pi OS image builder — produces. Plus mkfs.vfat -F32 -s 1 to match pi-gen’s cluster size, since the Pi bootloader’s FAT parser has a documented history (rpi-eeprom #558) of misdetecting FAT type on ESP partitions.
build-image.sh — the partition table now branches with the variant
if [ "$VARIANT" = "vm" ]; then
sfdisk --quiet --label gpt "$OUT" <<EOF
${ESP_START_MB}MiB,${ESP_MB}MiB,U,*
,,L
EOF
else
sfdisk --quiet --label dos "$OUT" <<EOF
${ESP_START_MB}MiB,${ESP_MB}MiB,c,*
,,83
EOF
fi
A published upstream bug that matches your symptom is still a hypothesis. Checking its fix date took two minutes and ruled it out entirely.
Fault 02of 06
Two flashing tools that lied
A false "the card does not match the image" verdict
What it printed
Error 433: a device which does not exist
Verification failed at sector 2048
The answer that was wrong
Sector 2048 is where the boot partition starts, so a verify failure there looked genuinely alarming — as though the flash had corrupted the one region that has to be perfect.
What was actually going on
Both were false alarms. Error 433 was Win32 Disk Imager enumerating empty card-reader slots and tripping over drive letters with no media in them. The verify failure was worse than a false alarm: raw physical-drive reads need Administrator on Windows, and an unelevated read does not fail loudly — it returns all-zero buffers. Disk Imager reported a read failure as a data mismatch. A second attempt returned Windows error 5, Access Denied, which is at least honest.
The fix
Verify through the filesystem instead of the raw device: mount the boot partition and hash all 428 files against the same files in the image. It needs no elevation, and it is the better check anyway — it compares what the Pi will actually read. All 428 matched.
An unelevated raw disk read on Windows fails silently by returning zeros. That produced one completely false result before it was caught.
Fault 03of 06
The display pipeline — the one that actually worried me
GPU and both HDMI outputs stuck on a clock that never arrives
What it printed
platform 107c706400.hdmi: deferred probe pending: supplier firmware:rpi-firmware:clocks not ready
platform 1002000000.gpu: deferred probe pending: supplier firmware:rpi-firmware:clocks not ready
platform 107c580000.hvs: deferred probe pending: supplier firmware:rpi-firmware:clocks not ready
What was actually going on
An earlier Arch Linux ARM probe card booted to a login prompt and rendered nothing at all. Both HDMI outputs, the GPU and the hardware video scaler were stuck forever on a firmware clock interface that never becomes ready — no display driver, therefore no DRM device, therefore no Hyprland. That probe used the generic linux-aarch64 kernel. The prediction was that linux-rpi — the kernel Raspberry Pi OS itself uses, and what the real image ships — would not have the problem, because it carries the Pi-specific firmware clock driver the generic kernel lacks.
The fix
Nothing to fix, as it turned out — but this was the biggest unknown in the project and it had to be answered on hardware rather than reasoned about. It came back clean on the first real boot: every component bound, and the Pi 5’s VideoCore VII works under this port. Hyprland has a real GPU.
The riskiest unknown was identified before the build started and answered by the cheapest test that could answer it. It is the only one of the six that came back clean.
Fault 04of 06
The twelve-hour hang
A filesystem resize that never finished, and no error anywhere
What it printed
[ *** ] (1 of 2) A start job is running for Rebuild Dynamic Linker Cache (9s / no limit)
EXT4-fs (mmcblk0p2): resizing filesystem from 3308032 to 32636672 blocks
EXT4-fs (mmcblk0p2): resized to 15204352 blocks
The answer that was wrong
That last line reads like "stopped halfway", and I said so. Wrong: it is a ten-second progress heartbeat emitted from inside the resize loop, and the completion message is a different string entirely ("resized filesystem to %llu", with no "blocks"). So it was still working. I also told myself the SD card’s activity LED flashing every ten seconds was that same heartbeat and therefore proof of progress. Also wrong — the heartbeat is only a printk and does no disk I/O at all. That flash was ordinary background writeback on an idle system. On the strength of those two mistakes I left it running overnight. Twelve hours later: identical screen.
What was actually going on
The real diagnosis came from reading fs/ext4/resize.c properly. The heartbeat check sits INSIDE the growth loop, so if the resize were merely slow there would be a new line every ten seconds — dozens of them. There was exactly one. The loop had not completed a single iteration in twelve hours: it was blocked inside one call, waiting on I/O that never returned. An audit of the filesystem confirmed the work itself was trivial — 896 new block groups, about 1.8 GiB of inode tables, minutes of work rather than hours. Ruled out at source level along the way: orphan_file, metadata_csum_seed, discard/TRIM (the resize path issues none), synchronous inode-table zeroing (deferred to a lazy-init thread) and every known upstream ext4 resize regression. Most likely cause: SD Command Queueing. raspberrypi/linux #6561 is an open Pi 5 defect — CQE is new to the Pi 5, enabled by default on A2 cards, and produces exactly this signature: I/O stalls with no errors logged and the kernel otherwise alive.
The fix
Belt and braces. dtparam=sd_cqe=off in config.txt, and — the better fix — stop resizing on the Pi entirely. The filesystem is now grown offline at build time, in a container, where there is no CQE, no journal barriers and no SD card. The same operation that hung for twelve hours completed in seconds. omarchy-pi-expand-root.service is masked to /dev/null so it can never run.
fs/ext4/resize.c — the heartbeat is inside the loop, which is what proves the stall
while (ext4_setup_next_flex_gd(sb, flex_gd, n_blocks_count)) {
if (time_is_before_jiffies(last_update_time + HZ * 10)) {
ext4_msg(sb, KERN_INFO, "resized to %llu blocks", ...);
}
...
err = ext4_flex_group_add(sb, resize_inode, flex_gd);
Twelve hours were lost to two confident readings of the evidence, neither checked against the source that produced it. Reading the loop took twenty minutes and settled it.
Fault 05of 06
The WiFi interface that was not called wlan0
A NetworkManager profile that silently matched nothing
What it printed
brcmfmac mmc1:0001:1 wld0: renamed from wlan0
What was actually going on
wld0 is genuine, not corruption. systemd naming scheme v259 added devicetree-alias naming for WLAN devices — "wl" plus "d" plus the alias index — in udev-builtin-net_id.c. NetworkManager’s documentation is explicit that interface-name restricts a connection to that device, so a profile pinned to a name that no longer exists matches nothing and never activates. No error: it simply does not come up.
The fix
Delete the interface-name line entirely. Unset means "any interface of the appropriate type", which is what was wanted in the first place. As an immediate workaround, nmcli device wifi connect bypasses the saved profile and its pinning altogether.
systemd udev-builtin-net_id.c — where wld0 comes from
if (snprintf_ok(str, sizeof str, "%sd%u", prefix, i))
udev_builtin_add_property(event, "ID_NET_NAME_ONBOARD", str);
Config that pins a name is config that breaks when the name changes. The failure mode is silence, which is the expensive kind.
Fault 06of 06
Bluetooth was never enabled
bluetoothctl hanging on "Waiting to connect to bluetoothd"
What was actually going on
The service simply was not enabled in the image — almost certainly the same installer stage that also failed to enable SDDM, the display manager, which had been found and fixed earlier in the project. One class of miss, two symptoms.
The fix
sudo systemctl enable --now bluetooth. The keyboard then paired normally. One catch worth recording: when bluetoothctl prints a passkey you have to type it on the Bluetooth keyboard and press Enter there. Miss the window and you get org.bluez.Error.AuthenticationFailed, which looks like a real failure but is just a timeout.
The cheapest fault of the six, and it was found last — because it was behind five that stopped the machine from getting far enough to notice.
Evidence
What is actually proven on hardware
Every line here was read off a photograph of the HDMI console. There was no serial console.
✓Firmware accepts the card
No "GPT: No bootable partitions". The MBR fix works on hardware.
✓linux-rpi boots on a Pi 5
Kernel, initramfs and hooks all ran.
✓Root mounts read-write
EXT4-fs (mmcblk0p2): mounted filesystem … r/w
✓No kernel panic
The earlier move_module/finit_module panic did not reproduce — it was an emulation artefact, as predicted.
✓The vc4 display pipeline binds
Both HDMI outputs, the scaler and both pixelvalves bound; DRM initialised.
✓USB works through the RP1 chip
Keyboard enumerated via xhci-hcd on 1f00300000.usb
✓WiFi firmware loads
brcmf_c_preinit_dcmds: Firmware: BCM4345/6 wl0
✓Bluetooth radio present
BCM43455 37.4MHz Raspberry Pi 3+-0190
✓pacman and the AUR are healthy
13,317 packages available across core, extra, alarm, aur and omarchy-pi. Keyring populated, mirror reachable.
✓A real desktop application runs natively
VSCodium installed from the AUR and self-reports arm64 — the first desktop-class app proven end to end.
Software
Can you actually install things on it
Four tiers, now empirically distinct rather than theoretical.
Arch Linux ARM core and extrabtop, and most of the 13,317Works directly, no caveatsDone
AUR — prebuilt binary, aarch64 in its arch linevscodium-binConfirmed working on hardwareDone
AUR — compiled from source, x86-pinned or arch-genericUntestedA real failure rate is expected hereIn progress
Omarchy’s own packages, via omarchy-updatepkgs.omarchy.orgDoes not work and cannot yet — x86-64 only, no aarch64 repo existsNot started
The one check worth running before installing anything from the AUR
yay -G <pkg> && grep '^arch=' <pkg>/PKGBUILD
13,317 packages are available across core, extra, alarm, aur and the port’s own repo, and VSCodium — installed from the AUR — self-reports arm64, which is the binary telling you its own architecture rather than package metadata being taken on trust. What does not work is omarchy-update: Omarchy’s own packages come from a repository that is x86-64 only, and no aarch64 equivalent exists anywhere yet.
Findings — 05 for upstream
Bugs that belong to the port, not to this build
All five reproducible. Filing them is item 02 on the roadmap below.
01
build-image.sh uses the VM variant’s GPT/ESP layout when VARIANT=pi
Affects the one phase the author had not tested, which is exactly why it survived. Two-line fix.
Both are DefaultDependencies=no and Before=sysinit.target with no ordering between them, so systemd runs them concurrently — a multi-gigabyte resize starving a disk-walking ldconfig on the slowest device in the system. That is the literal "(1 of 2)" on screen.
03
The expand script calls sgdisk --move-second-header on what is now an MBR disk
sgdisk silently converts MBR to GPT and writes immediately, unlike interactive gdisk. Latent since the service is now masked, but real.
04
The image is not built with lazy_itable_init
So first-boot expansion zeroes inode tables synchronously rather than deferring the work to a background thread.
05
bluetooth.service is not enabled
The same class of miss as the systemctl enable sddm gap found earlier in the project.
The plan
Where this goes next
“Not started” means unsolved, not scheduled. Nothing here gets a date it has not earned.
01
A serial console
In progress
A USB-TTL adapter is on the way, and the image is already configured for it — enable_uart=1 and console=serial0,115200 are set. Every hard moment in this project would have been minutes rather than hours with one, because the only window into the Pi for most of it was photographing the HDMI output. (The Pi’s UART is 3.3V and not 5V-tolerant: GND to pin 6, adapter TX to pin 10, adapter RX to pin 8, VCC left disconnected.)
02
File the five upstream findings
In progress
All five are reproducible and written up. They belong with the port’s maintainer rather than sitting in a private build log — the whole point of finding them in a phase nobody had reached is that the next person should not have to.
03
Answer how this system stays current
Not started
This is the genuinely open one. Arch packages update normally, and the AUR works. Omarchy’s own packages do not: omarchy-update pulls from pkgs.omarchy.org, which is x86-64 only, and the port’s phase 6 — hosting an aarch64 pacman repo — has not started. The only path today is rebuilding packages in WSL and dropping them into the local repo cache, which is intact and ready to receive them but is not a thing you would ask anyone else to do. Until phase 6 exists, "how do you update it" has no good answer, and saying so is more useful than pretending otherwise.
04
Thermals and real-world performance
Not started
Now there is finally something to measure. Nothing has been benchmarked yet, so there are no numbers on this page — an active cooler, which the port’s own target hardware includes, goes on before any of them would mean anything.
05
The harder half of the AUR
Not started
Prebuilt binary packages are proven. Packages that compile from source under whatever toolchain ships on the image are not, and a real failure rate is expected. One small compiled package, start to finish, is the next test — it gives an honest read on build time and on how often the answer is simply no.
06
A desktop someone else could install
Not started
The ambition, and the thing the four items above are in service of. Right now this is one working card in one house, reproducible only by someone willing to build the image themselves. Between here and a thing you download there is CI (the port’s phase 5, scaffolded), a package repo (phase 6, unstarted), and a great deal more hardware than one board. Worth being plain: that is not close, and this page will say so until it is.
Method
Why any of this is on a studio website
The evidence is above. This is the part that is about hiring us.
Debugging without a serial console was most of the difficulty. For most of this project the only window into the machine was a photograph of the HDMI output — no scrollback, no copy and paste, no way to ask it a question. That forces a particular discipline: read the actual source, compute what the numbers should be, and check them against the image on disk rather than guessing from symptoms.
Nearly every fix on this page came from reading kernel or e2fsprogs source, systemd’s udev rules or upstream release notes — not from a forum post. Four of the six faults had a confident, plausible, wrong explanation first, and each one cost real time: a plausible wrong answer costs a rebuild-and-reflash cycle, and at 13GB per flash that is an hour gone. The discipline was not cleverness, it was refusing to act on a theory that had not been checked against a primary source.
That is the same way client work gets done here. A production bug in someone’s business is the same shape as this: an unhelpful symptom, several plausible causes, and a real cost to guessing wrong. The difference between a fix that holds and one that comes back is almost always whether anybody read the thing that produced the error.
Every hard moment in this project came from guessing instead of reading the source. Once we stopped doing that, every problem turned out to be understandable.
Credit
None of this is mine alone
vincenth19/omarchy-piThe aarch64 port everything here is built on. Alpha, honestly labelled, and genuinely good work — every bug above is the ordinary consequence of a phase nobody had reached yet, not a criticism.