I am a Rails developer by heart. My comfort zone is models, controllers and a test suite that goes green. Spending a day inside initramfs hooks, evdev ioctls and device-tree platforms is not where I normally live — which is exactly why this was worth writing down.
Earlier this year I got an external monitor working on Asahi Fedora by building the Fairydust kernel. This time I went further: a clean wipe, Asahi Arch (ALARM) underneath, and Omarchy 4 on top.
The install itself takes an hour. The remaining day went to four bugs that share one shape: an assumption from x86 that does not crash on ARM — it just quietly returns the wrong answer. Those are the interesting ones, so they get the most space here.
Overview
- Installed Asahi Arch (ALARM) from macOS, then Omarchy 4 by following the
omarchy-macfork's own README. - Rebooted into a machine with no Wi-Fi, no Bluetooth, no Touch Bar, no audio — one missing initramfs hook.
- Restored the audio stack, which Omarchy does not install on Apple Silicon.
- Built the Fairydust kernel for USB-C DisplayPort Alt Mode.
- Fixed clamshell mode, screen brightness, and Bluetooth pairing.
Omarchy 4, "Quattro"
Omarchy 4 shipped as Quattro, and DHH presents the release himself in an 81-minute walkthrough. If you are arriving from macOS and want to know what this thing actually is before wiping a disk for it, that is the place to start — it is a tour of the system by the person who designed it, not a feature list.
One stretch of it explains something you will feel directly on Apple Silicon. From 49:50 he covers packaging: after a rough year for the AUR — outages, and some security trouble — Omarchy stopped depending on it at runtime and now builds, verifies and hosts its own packages instead. That repository is x86-only. Everything in section 1 below that takes 40 minutes instead of five traces back to this: on ARM there is nothing prebuilt to install, so the fork compiles the same packages on your machine.
1) Install Asahi Arch, then Omarchy
Follow the guide in the repository itself, not this post. The
omarchy-mac README is the
maintained install document — partitioning, the Asahi Alarm installer, initial
Arch setup, creating your user, and the install itself. It is kept current with
the fork; everything below is only a summary of the path I took, and the rest of
this post starts after the install has finished.
The short version. Run the Asahi Alarm installer from macOS and pick Asahi Arch (ALARM) Minimal:
curl https://asahi-alarm.org/installer-bootstrap.sh | sh
Boot the result, update, and create a regular user — the Omarchy installer
refuses to run as root and calls sudo where it needs to. Then install from the
Apple Silicon fork:
sudo pacman -Syu
git clone --branch quattro https://codeberg.org/malik-na/omarchy-mac.git ~/.local/share/omarchy
cd ~/.local/share/omarchy && bash install.sh
Budget around 40 minutes, almost all of it building AUR packages, because
Omarchy's own package repo has no Apple Silicon builds — the packaging split
described above. A few packages have no
ARM build at all and are reported at the end instead of failing the run — that is
expected. If mirrors fail, bash fix-mirrors.sh from the repository root and
retry.
Two things the README covers that are easy to skip and expensive to skip: back macOS up first, and leave at least 50 GB free (100 GB if you intend to build the kernel in section 4). Check the reported package failures before you reboot.
Let it finish, then reboot. This is where the trouble starts.
2) The bootstrap trap: no Wi-Fi after install
The machine came up with no Wi-Fi, no Bluetooth, no Touch Bar, no ambient light
sensor and no audio. dmesg showed every one of them failing the same way:
brcmfmac 0000:01:00.0: Direct firmware load for brcm/brcmfmac4378b1-pcie.bin failed with error -2
hci_bcm4377 0000:01:00.1: Unable to load firmware; tried 'brcm/brcmbt4378b1-apple,honshu-m.bin' ...
apple-z2 spi0.0: unable to load firmware
iio_aop_als als.0.auto: probe with driver iio_aop_als failed with error -2
Error -2 is ENOENT. Every Apple vendor firmware blob was missing at once,
which points at one thing: the asahi initcpio hook, which unpacks
<ESP>/vendorfw/firmware.cpio into a tmpfs at /lib/firmware/vendor during
early boot.
One command confirms it never ran:
findmnt -t tmpfs /usr/lib/firmware/vendor # silence = the hook did not run
The cause is /etc/mkinitcpio.conf.d/omarchy_hooks.conf, which assigns
HOOKS rather than amending it:
HOOKS=(base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs)
Arch ARM ships /etc/mkinitcpio.conf with HOOKS=(base asahi udev ...), and
drop-ins are sourced after the main config — so asahi is silently discarded.
This is a genuine bootstrap trap: with no Wi-Fi you cannot pacman -S your way
out. /var/log/pacman.log pins the moment exactly:
[13:24:14] [PACMAN] starting full system upgrade <- network fine
[13:35:14] omarchy_hooks.conf written
[13:36:28] ==> Using drop-in configuration file: 'omarchy_hooks.conf'
^- initramfs rebuilt without `asahi`
Fix. A drop-in that sorts after Omarchy's and puts the hook back:
# /etc/mkinitcpio.conf.d/zz-asahi-hook.conf
if [[ -e /usr/lib/initcpio/install/asahi ]] && [[ " ${HOOKS[*]:-} " != *" asahi "* ]]; then
_hooks=(); _added=0
for _h in "${HOOKS[@]}"; do
_hooks+=("$_h")
[[ $_h == base ]] && { _hooks+=(asahi); _added=1; }
done
((_added)) || _hooks=(asahi "${_hooks[@]}")
HOOKS=("${_hooks[@]}")
unset _hooks _h _added
fi
Then rebuild and verify before rebooting, because getting this wrong leaves you offline:
sudo mkinitcpio -P
sudo lsinitcpio /boot/initramfs-linux-asahi.img | grep hooks/asahi
After a reboot, /usr/lib/firmware/vendor is a tmpfs with ~339 files, and
Wi-Fi, Bluetooth, Touch Bar, the light sensor and the camera ISP all come back
at once. Reported as
omarchy-mac#156.
3) Audio
Firmware alone is not enough — Omarchy does not pull in the Apple Silicon audio stack. Both ALSA cards existed but PipeWire had zero sinks:
sudo pacman -S --needed asahi-audio speakersafetyd pipewire-pulse pipewire-alsa
sudo systemctl enable --now speakersafetyd
speakersafetyd is not optional. The UCM profile refuses to enable the speakers
without it, because it is what stops you physically destroying them. Once
running, the default sink becomes MacBook Pro J293 Speakers through a
convolver DSP chain.
While here, set the regulatory domain — mine defaulted to country 00, which
forces passive scan on 5 GHz:
sudo sed -i 's/^#WIRELESS_REGDOM="PL"/WIRELESS_REGDOM="PL"/' /etc/conf.d/wireless-regdom
speakersafetyd running, and the J293 convolver chain that shapes what actually reaches the speakers.4) The Fairydust kernel for the external monitor
Stock linux-asahi still has no USB-C DisplayPort Alt Mode, so the external
monitor needs a patched kernel. I packaged it for pacman rather than building it
by hand as I did on Fedora — that keeps the stock kernel installed as a rescue
entry, and GRUB lists both.
The build is pinned to one exact Asahi release (7.1.6-1 plus a 13-commit
patch). Budget 1–3 hours, keep it plugged in, and expect ~35 GiB of scratch
space. Afterwards:
$ uname -r
7.1.6-1-1-fairydust-ARCH
$ ls /sys/class/drm/ | grep DP
card3-DP-1
The scripts
Three files did the work, and they are on this site if you want them:
-
PKGBUILD— buildslinux-asahi-fairydustpinned to Asahi 7.1.6-1, and sets a distinctlocalversionso it installs beside stocklinux-asahirather than over it:echo "-${_asahirel}-${pkgrel}-fairydust" > localversion.10-pkgrel -
build-and-install.sh— refuses to run unless it is on aarch64, on a J293, on Arch and with 35 GiB free; then installs the build dependencies and the pinned Rust toolchain, runsmakepkg, installs the package, rebuilds the initramfs, updates m1n1 with the patched DTBs and regenerates the GRUB menu. -
verify.sh— the OK/FAIL list in the screenshot above. Run it after the first Fairydust boot.
Two things the package needs are not mine to hand out, so the
README explains how to produce them: the
kernel config (start from zcat /proc/config.gz on a machine running stock
linux-asahi) and fairydust.patch itself, which is the Asahi Linux project's
work on their fairydust branch
and should be generated from their tree:
git diff asahi-7.1.6-1..origin/fairydust > fairydust.patch
updpkgsums # the PKGBUILD pins checksums
Read them before running them. The hardware guard is for my machine, and the version pin is the whole point — see below.
The part worth remembering: this package does not follow linux-asahi
updates. When the stock kernel moves past 7.1.6, Fairydust must be rebuilt
against the new tree and the patch may not apply. Do not uninstall the working
version until the rebuild is tested, or you will boot into a machine with no
external display and no obvious reason why.
5) Clamshell mode — three layers deep
Closing the lid with the monitor attached should blank the internal panel and keep working. It did nothing. This one took three attempts because each fix revealed the next problem.
Layer one. omarchy-hw-laptop-closed reads the lid from ACPI:
for state in /proc/acpi/button/lid/*/state; do ...; done
exit 1
Apple Silicon is a device-tree platform with no ACPI at all. The glob never matches, the loop body never runs, and the script always reports "open".
Layer two. The obvious fix is to read SW_LID from evdev instead. My first
probe suggested the driver did not maintain the switch-state bitmap — but that
turned out to be my own measurement racing the event. A proper probe, reading
state immediately after the event and sanity-checking the ioctl encoding
against a keyboard, showed EVIOCGSW works correctly. No kernel bug. Worth
saying plainly, because I nearly filed one.
Layer three. Dropping a fixed script in /usr/local/bin appeared to work
for exactly one second, then reverted. omarchy-hyprland-monitor-watch re-runs
the clamshell handler 1s, 3s and 7s after any monitor change, and its PATH
starts with /usr/share/omarchy/bin — ahead of /usr/local/bin. The override
was being bypassed by the packaged version.
Fix. Stop fighting the watcher and move the decision into monitors.lua,
which Hyprland re-evaluates on every reload, including the watcher's:
local state_home = os.getenv("XDG_STATE_HOME") or (os.getenv("HOME") .. "/.local/state")
local lid_closed = file_exists(state_home .. "/omarchy/lid-closed")
if lid_closed and external_display_connected() then
hl.monitor({ output = "eDP-1", disabled = true })
else
hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 2 })
end
The lid flag is written by Hyprland's switch bindings, which are reliable:
o.bind("switch:on:Apple SMC power/lid events", nil, "omarchy-lid-clamshell closed", { locked = true })
o.bind("switch:off:Apple SMC power/lid events", nil, "omarchy-lid-clamshell open", { locked = true })
eDP-1 gone while the external 4K keeps running.One nice surprise: systemd-logind gets this right on its own. It counts
connected non-eDP connectors, treats the machine as docked, and applies
HandleLidSwitchDocked=ignore — so it never suspends while the monitor is
attached. That part needed no work at all.
6) Screen brightness was dimming the Touch Bar
The brightness keys "did nothing". Capturing the actual keycodes showed the keys
were fine — both the Touch Bar and my Bluetooth keyboard emit a correct
KEY_BRIGHTNESSDOWN, already bound by Omarchy. The bug was in device selection:
# omarchy-hw-display
device="$(ls -1 /sys/class/backlight | grep -vx appletb_backlight | head -n1)"
First entry alphabetically, then a search for gmux / amdgpu / intel /
acpi_video — all x86-only names. On this machine:
228600000.dsi.0 <- digits sort first, so this wins
apple-panel-bl <- the actual panel, max 509
228600000.dsi.0 is the Touch Bar backlight. The keys had been dimming the
strip above the keyboard the whole time — which is why it looked like they
controlled the keyboard light. Fixing it is a one-line preference for
apple-panel-bl in a /usr/local/bin override.
Note that external monitor brightness is not fixable: ddcutil detect reports
No display adapters with i2c buses appear to exist, so DDC/CI is unavailable
and the Samsung has to be adjusted with its own buttons.
7) Bluetooth: use the right transport
My Magic Trackpad and keyboard paired but never connected. Two separate causes:
The trackpad was plugged in over USB. Apple Magic devices switch to wired
mode and refuse Bluetooth while connected. Unplugging fixed it — and as a bonus,
the magicmouse: unable to request touch data (-32) errors that broke gestures
turn out to happen only over USB. Over Bluetooth it registers cleanly.
The keyboard needed a re-pair, but would not show up in scans. Both devices
are Bluetooth Classic (BR/EDR), not BLE, and a plain scan on misses them:
bluetoothctl
> menu scan
> transport bredr
> back
> scan on
They appear immediately. Pair while the scan is still running — BlueZ drops undiscovered devices from its cache the moment scanning stops, which is what made this look harder than it was.
What still does not work
Worth being honest about the remaining gaps:
- Hardware video decode.
avdwantsapple/avd-fw-v2-t0.bin, which Apple does not ship in the vendor firmware set. Software decode covers it. - External monitor brightness. No I2C buses, so no DDC/CI.
- mpv crashes with the webcam overlay — a SIGSEGV inside
libvulkan_asahiwhile uploading a frame via libplacebo. Recording itself works; the webcam overlay does not. - Wi-Fi Direct / P2P is unsupported by this firmware (
-52on every attempt).
Sources
- Asahi Linux — the project making any of this possible
- Omarchy and the
omarchy-macfork for Apple Silicon - Omarchy Quattro — DHH's walkthrough of the release
- My earlier post on Asahi Fedora — building Fairydust by hand
asahi-scripts, for/usr/lib/initcpio/hooks/asahi— reading it is what explained the firmware failures
Was it worth it?
Yes, and not only for the machine. As a Rails developer, most of my debugging is
one layer deep: read the stack trace, fix the code. Every bug here failed
silently — a glob that matched nothing, a head -n1 that picked the wrong
device, a PATH that put the packaged script first. Nothing crashed. Everything
returned a plausible wrong answer.
The habit that actually worked was refusing to trust my own reasoning. Twice I concluded "this should work now" from reading configuration, and twice I was wrong. What settled it each time was measuring at the moment of the event — probing the lid switch as it closed, timing the monitor watcher at t+0/1/2/3s, capturing the real keycode instead of assuming which one fired.
Note: If you follow this guide, proceed with caution. I don't take responsibility if you damage your system. This involves building and installing a custom kernel and editing boot configuration, which can potentially break things. Keep the stock kernel installed, and verify your initramfs before you reboot.