Honey, I Built a Linux Distribution
Or, what happens when a small ARM SoC, a quiet weekend, and a 45-year-old Disney movie collide on your desk
Disclosure: This post reflects independent personal experimentation and my own hands-on work on personal open-source projects. It reflects only my personal views, is not professional advice, and does not represent any organization, employer, or official position.
I want to tell you about the thing I just made, because I think it might actually be useful to people, and because the alternative — putting it up on GitHub and saying nothing — felt like a strange way to treat something I've been wrestling with for weeks.
So, I built a Linux distribution.
The name is NCZ 26.5 "Reinhardt". The first time it booted on real hardware was at 23:45 EST on Sunday, May 4, 2026, and I genuinely don't think anyone else has done what it does on this particular SoC yet. The rest of this post is me trying to explain why I think that matters and why you might want to play with it.
A small box you've probably never heard of
The hardware is a Minisforum MS-R1 — about the size of a Mac mini, with a small fan so quiet you have to lean in to confirm it's running. Inside is a Cix Technology Sky1 (the CP8180, 12-core Arm Cortex-A720/X4, Mali-G720 GPU, a 45 TOPS Zhouyi NPU, soldered LPDDR5). At the time I'm writing this, the Minisforum US store has the 32 GB barebones (BYO NVMe) at $463 and the 64 GB barebones (BYO NVMe) at $559. Two M.2 slots on the board — take whichever capacity SSD you have lying around. Pricing moves; check the Minisforum store page for current numbers. Both shipped this spring. It's an interesting little box because it's a real Arm desktop-class chip with serious AI capability — not a Raspberry Pi, not a phone SoC, not Apple Silicon — at a price you can, frankly, justify on a credit card without a meeting.

I bought one because I wanted a small, cool-running box with an NPU that I could run AI agent workloads on, and that wasn't either (a) a giant rack-mountable thing, (b) a Mac, or (c) a Pi pretending to be more than it is. The MS-R1 ticks all those boxes. There's just one wrinkle: the Linux story for Sky1 is, charitably, still cooking.
The vendor ships a reference Debian image. The Sky1-Linux community (Daniel-Constantin Mierla and others) maintains an excellent set of kernel patches and a Debian-sid spin. Both are great. Neither is what I actually wanted to run, which was Ubuntu LTS userspace on top of a sane, deterministic, Yocto-built kernel.
So I built one.
What NCZ is, in three sentences
It's the first Yocto-built Ubuntu derivative for Cix Sky1. It comes as a real installable ISO — boot the thing from a USB stick, get a graphical Debian-installer flow, partition the NVMe, set a username, pick a default kernel, and ten minutes later you have a working desktop. And it ships the lightest possible desktop — XFCE plus Openbox plus Window Maker, no GNOME, no KDE — so the Sky1 NPU, the Mali-G720, and your 32 or 64 GB of RAM stay available for the actual reason you bought the box.
There. That's the elevator pitch. The rest is footnotes and feelings.
Why "lightest possible desktop" is the load-bearing decision
Here's the math that drove everything else.
Ubuntu Desktop with GNOME idles at around 2 GB of RAM. Fedora Workstation is similar. Kubuntu's slightly less. NCZ with XFCE idles at 250–400 MB. With Openbox plus Window Maker, closer to 150 MB.
On a 32 GB Sky1 box, that's the difference between 30 GB available for your workload and 31.7 GB available for your workload — and on this hardware, that 1.7 GB matters a lot more than GNOME's animated workspace switcher does. On the 64 GB config, it's even more lopsided. If you bought a Sky1 box, you bought it for the NPU, the GPU, and the cores. The desktop should be a polite seat near the door, not the centerpiece of the room.
I'm aware this isn't a popular position. "Just ship GNOME, every other distro does" is the path of least resistance. But every other distro is also not optimizing for the workload-first audience on resource-constrained Arm SoCs. NCZ is.
So I framed it as a feature, not a tradeoff: deliberate resource budgeting. The cosmic-destruction aesthetic is the visible identity. The resource budget is the load-bearing decision. Workloads. Not wallpapers.

About the cosmic-destruction aesthetic
Yes. About that.
Ubuntu names its releases after cute animals. Quirky Quokka. Friendly Falcon. I think this is great branding for Ubuntu, and an absolutely terrible fit for what I was building, which is more "thing that should exist on the desk of someone running heavy compute" and less "thing my wife would name her stuffed manatee."
So, NCZ names releases after the most destructive forces in the universe. Black holes. Magnetars. Vacuum decay. Quasars. Gamma-ray bursts. Things that, if they got close to your house, would resolve every other problem you have, instantly and forever.

The first codename is Reinhardt, after Dr. Hans Reinhardt — the obsessed scientist played by Maximilian Schell in the 1979 Disney film The Black Hole, the one who flies the Cygnus into the maw of the singularity. The boot splash opens with "Dr. Reinhardt has gone into the Black Hole." The trash icon is a black hole. There's a 10-minute wallpaper rotator that cycles through six FLUX-generated cosmic motifs (Gargantua's accretion disk, M87 from the Event Horizon Telescope, magnetar jets, Cygnus's vacuum-decay event horizon, etc.). The amber-phosphor color palette is meant to evoke late-1970s CRT monitors decaying gracefully toward heat death.
Is it serious? Yes. Is it also a little silly? Also yes. I think those things go together more often than they get credit for.
There is, additionally, a meta-joke buried in the codename that took me a few weeks to notice. The Black Hole is what I had been staring at for the entire development cycle. Every failed install boot ended with a black screen. Every time the kernel panicked, I got a black screen. Every time the Mali GPU couldn't bring up a display, every time systemd-boot failed to find a root partition, every time the wrong DRM card got picked, every time an Xorg startup tripped over a missing icon theme — black screen.
The number of hours I spent glaring into a dark monitor across the room, willing it to print anything, is genuinely not small. By the time I sat down to pick a brand, the connection wasn't "let's name this after destructive cosmic phenomena, that sounds cool"; it was "I have spent six weeks watching the literal Black Hole of Death, and I refuse to let that experience go un-monetized for irony purposes." Hence Reinhardt. Hence the tagline.
Reinhardt goes in. He chooses to. And Reinhardt flew into the black hole is the load-bearing image of this whole project, because what you're reading right now is the artifact of someone repeatedly flying his cixmini build pipeline into the black-screen-of-death and refusing to come back until the screen lit up. The cosmic dread is fully load-bearing because the cosmic dread is real. The branding is a memorial.
The technical bit, since some of you are here for that
The unusual thing about NCZ, technically, is the build-pipeline split.
Most ARM SoC distros are one of two shapes:
- A flashable Yocto image — built by the vendor or a community using BitBake- comes out as a
.wicfile youddto an SD card or eMMC. Works great for embedded. Doesn't really have an "install flow." You can't add yourself as a user during boot. It's just here;- take it or leave it. - A vendor reference Debian image — Cix's own Debian build, or Sky1-Linux's Debian sid spin. Closer to a normal desktop install, but you're stuck on the vendor's userspace choices, on the vendor's kernel cadence, on the vendor's update story.
NCZ is neither. It's a custom Debian Installer fork (cix-installer, codenamed V.I.N.CENT after the floating robot from The Black Hole — keep up) that runs the standard d-i graphical install flow on Sky1 hardware. The kernels are built by Yocto from the Sky1-Linux community patch set (currently 6.18.26-cix-sky1-lts as production default, 7.0.3-cix-sky1-next as a beta slot). The userspace is Ubuntu 25.10 questing.
To make the Yocto-kernel-feeding-Ubuntu-rootfs combination work, the installer pre-stages the Ubuntu rootfs as a .tar.zst and extracts it to /target during the partman late_command, which causes bootstrap-base to detect a populated /target/etc/os-release and exit "skip-success." Then a stack of post-install hooks installs the desktop, the agent stack, the icon themes, the wallpaper rotator, the NCZ brand assets, and roughly forty other small decisions I made.
The result is a 3.7 GB thin ISO (or an 8.8 GB max ISO for offline installs) that boots from any USB stick you can find, walks you through partitioning your NVMe, sets up systemd-boot with both kernels as selectable entries, and lands you at a LightDM greeter ten minutes later. You log in. The desktop is there. The wallpaper is rotating. The cosmic dread is settling in nicely.
Sixty-one tries
Since some of you will ask, it took me 61 build iterations to get to this point. The last several of those were a real education in how distros actually fail, and I'll come back to that in its own section.
I should be honest about that, because it might be the single most important thing for you to know if you're considering doing something like this yourself. I had never built a Linux distribution before, full stop. Some of you may have me filed mentally with OpenZaurus twenty-some years ago — that's because, briefly in late 2002 and early 2003, I was Software Developer Liaison for Sharp Electronics' Zaurus in the US, working with the community of developers porting Linux to the Zaurus PDA line. That tenure is documented in an article I wrote later about the experience, where the short version was: Sharp's US-side developer-relations team was understaffed, the developers couldn't get timely beta ROMs or kernel source from Japan (everything was funneled through one English-speaking liaison there, with a twelve-hour communication lag), and the Qtopia SDK cost thousands of dollars per seat. So the community went rogue. They built OpenZaurus, OPIE, and ports of Familiar, and they did the work that Sharp would not.
The actual person who built OpenZaurus was Chris Larson, one of those rogue community developers I worked with from the Sharp side. Chris is also the person whose architectural work on the OpenZaurus build system became OpenEmbedded and ultimately Yocto. Chris built the thing. I had the much smaller job of trying, with the cards Sharp dealt me, to be a useful bridge from inside the vendor. So when I say I was "involved with OpenZaurus," what I mean is I was the corporate guy answering developers' emails and trying to get them what they needed faster than the existing pipeline allowed. I never sat in the engineering chair.
So to be clear: NCZ is the first Linux distribution I have ever actually built, sitting in the engineering chair. It is a from-the-ground-up integration of three different distro paradigms — a Yocto kernel build pipeline, an Ubuntu userspace rootfs, and a fork of Debian-installer's busybox initrd substrate — that have never been glued together in this combination. There were no shoulders to stand on. The shoulders were the whole job.
The 61 builds were not all dumb churn. Build 1–7 were learning what a working d-i flow even looks like on Arm. Build 8–22 were figuring out the kernel chain (Yocto recipe wrangling, Sky1-Linux patch series rebases, the moment I discovered scarthgap doesn't ship kernel-headers udebs by default). Build 23–36 were the GLIBC-version mismatch saga (bookworm runtime needing trixie userspace tools, Path-A graft architecture, whose binary calls whose). Build 37 was the historic first-boot. Build 38–54 were the slow grind from "boots to a black screen" to "boots to a polished login screen."
Build 55 was the one I'd actually hand to a stranger. Build 56 was sixty-plus user-facing strings rewritten in a single Python pass mid-build because I didn't like how the old name looked on the boot splash. Build 57 was the first one with a working NPU stack, and that part has a story I want to tell properly in its own section. Builds 58–60 were the cascade of post-install bugs you can't see until you actually install on real hardware — also a section. Build 61 is the one I'd download today.
Most of the builds failed in ways that took an hour to diagnose and ninety seconds to fix. A few took eight hours to diagnose and required rewriting an entire stage of the build pipeline. One of them — the V.I.N.CENT busybox graft (build 26-ish) — was a piece of work I'm genuinely proud of, where I had to combine bookworm's static-linked initrd binaries with trixie's userspace shell scripts to make the libc-version difference irrelevant. That's the kind of weird hybrid solution that doesn't appear in any tutorial because it's exactly the kind of thing tutorials don't teach.
I don't say this to brag. I say it because if anyone reading this is in week one of "I want to build a Linux distribution from scratch" and is on build 4 and is wondering whether 61 sounds like a lot or a little, the answer is: it's exactly however many it takes, and the ones that don't work are the ones that teach you the most. None of those 60 failures were wasted. The current build is the way it is because the previous 60 weren't.
The other thing I'd say is that AI tooling helped a lot. I was writing release notes, debugging xorriso boot-block boundaries, regex-replacing across post-install hooks, drafting outreach emails, and explaining my own design decisions back to myself — all with a Claude session as the second pair of hands. That doesn't mean Claude built the distribution; it didn't, I did, the design choices and the understanding live with me. But it did mean I could move at "weekend project + paid AI tools" velocity instead of "this is the only thing I'm doing for six weeks" velocity, and the difference between those two velocities is what makes a thing like this feasible for one person now in a way it absolutely wasn't a few years ago.
The bug that finally got us out
There's one specific bug worth describing in detail because it determined the entire installer architecture and explains why I made what looks like a deeply weird choice about which Debian-installer base to fork.
The first instinct is to use the most recent thing available. Ubuntu's own ARM64 installer is current and well-tested. Debian trixie (the current stable) ships a modern d-i 1.0.141 with all the trimmings. Either should work, right? Pick the new one. Move on.
I tried the new one. From build 17 to build 24, I tried every variant of Ubuntu's casper-based installer that could plausibly boot on Sky1 hardware: standard Ubuntu Server live, Ubuntu Desktop live, Ubuntu Cloud, the minimal-install variants. All of them kernel-panicked at boot, regardless of bootloader (rEFInd, GRUB, systemd-boot — every combination). The panic looked different each time, in the way only an early-boot panic on undocumented Arm SoC firmware can, but the trigger was always the same: casper's initrd init script makes assumptions about the boot environment that aren't true on this hardware. Casper was not going to be the answer.
Fine. Switch to Debian-installer. trixie 1.0.141 is the current stable; it's a clean lineage, the busybox initrd is much simpler than casper, fewer assumptions to break. I built it. It almost worked. Then it died at the base-installer with /lib/aarch64-linux-gnu/libc.so.6: version 'GLIBC_2.38' not found.
The reason: trixie d-i was built against trixie libraries. Trixie ships glibc 2.42. The host runtime in the d-i busybox initrd had only glibc 2.36 because the static-linked bootstrap binaries that the d-i kernel boots into (specifically bootstrap-base 1.213 from bookworm — Debian 12, the previous stable) were compiled against bookworm's older glibc. Mixing trixie's d-i 1.0.141 with bookworm's static bootstrap binaries produces a chimera that asks the host glibc for symbols it doesn't have, and the result is base-installer dying instantly with a cryptic linker message that's technically describing the situation correctly but is functionally useless to read at midnight.
I tried this for several days. The answers were all bad:
- Use only trixie: requires rebuilding bootstrap-base from source against trixie. Source-builds of d-i base packages are a known-difficult engineering task because the build-deps reference specific versions of libdebian-installer4-dev that cross-reference half the rest of the d-i ecosystem. Multi-week project.
- Use only bookworm: bookworm's d-i is too old to handle Ubuntu questing's
.zst-compressed control.tar.zst / data.tar.zst .deb files. The bookworm d-i debootstrap doesn't know what zstd is. So bookworm-only can't actually install questing. - Use Ubuntu's installer: see paragraph above. Casper doesn't boot on this hardware.
The breakthrough — let's call it build 26, though the exact number is a haze — was a hybrid I named V.I.N.CENT, after the floating robot from The Black Hole. The architecture is:
- Keep bookworm
bootstrap-base 1.213— itsrun-debootstrapstatic binary only needs glibc 2.17/2.34 symbols, which are present everywhere. This is the host-glibc-safe layer. - Graft trixie
debootstrap-udeb 1.0.141pluslibzstd1-udebplusliblzma5-udebon top. These are mostly shell scripts plus a couple of arm64 shared libraries. The shell knows how to handle.zst-compressed Ubuntu .debs because it's a 2025-era debootstrap. - Inject a static arm64
zstdbinary into the busybox initrd as a separate cpio member, because zstd's binary tooling is the one piece neither side ships. - Pre-extract the Ubuntu rootfs as a
.tar.zstin the ISO at/cixmini/rootfs.tar.zst, and decompress it into/targetfrom the partman late_command using the static zstd. This populates/target/etc/os-releasebefore bootstrap-base runs, which causes bootstrap-base to detect a populated target and exit "skip-success" without ever calling debootstrap on a live network.
The result: bookworm's static bootstrap binaries handle the parts that need glibc-safe code. Trixie's debootstrap shell handles the parts that need to understand questing-era .deb formats. They never overlap, never argue about glibc symbols, and the rootfs gets to /target via a third path entirely — a tarball extraction — that bypasses the whole debate.
This is, to be clear, not a thing tutorials recommend. It is not in any of the cookbooks I read while building this. It's a hybrid of three distros' early-boot infrastructure, glued together because all three obvious paths were broken in different ways. And it works, reliably, on every install I've run from build 26 onward.
So when you read the release notes and they say "d-i base: bookworm 12 busybox initrd + trixie udeb graft" and your reaction is "why on earth would you do that, just use the latest thing" — that's why. The latest thing didn't boot. The previous thing couldn't read the new package format. The only thing that worked was an unholy graft, which turned out reliable enough to ship.
The codename V.I.N.CENT, again, is the floating robot from The Black Hole. You can guess which character he's loyal to. You can guess what they do at the end of the film together.
The FyrbyAdditive moment
There's a story I owe you about the NPU.
The framing I shipped in r56's release notes, and the one I'd carried in my head for days, was: the NPU is the one community-open gap. Everything else works; the NPU doesn't, and getting it to work would be a months-long reverse-engineering project against vendor-controlled binaries. I had a reason for that framing. Back in r51, I went looking for the kernel driver, found cix_opensource__npu_driver/npu.mk, opened it, saw a Soong-formatted Android build file with BUILD_PLATFORM_SKY1_ANDROID baked in, closed it, and concluded the source was AOSP-only.
This morning, on a hunch, I went back. I went one directory deeper than npu.mk and found driver/Makefile — a textbook out-of-tree Linux Kbuild module, GPL-2.0, with BUILD_PLATFORM_SKY1 (no _ANDROID suffix) already wired in. The ArmChina-authored driver was upstream-Linux-friendly all along. The AOSP layer was just Cix's own packaging on top.
I'd read the wrong file. Five months ago. First lesson, the small one: when an open-source package has two build systems, read both before declaring it unportable.
A few hours of patches and I had armchina_npu.ko building clean against both NCZ kernels — five small fixes, all generic kernel-version ABI drift (platform_driver.remove returning void in 6.11+, pm_runtime_put returning void in 7.x, MODULE_IMPORT_NS taking string literals in 6.13+, devfreq SCMI helpers we don't ship, the broken obj-m Kbuild pattern). Loaded it on MS-R1. Watched it auto-bind to ACPI path \_SB_.NPU0, set up the IOMMU, advertise zhouyi-v3, create /dev/aipu at 10:263. Zero kernel oops. It looked done.
It wasn't done.
When cix-noe-umd 4.0.0's libnoe.so.3.1.0 tried to talk to the chardev, it rejected with create_device_adapter: Unsupported device type before ever opening /dev/aipu. strace confirmed it never touched the device. The kernel module worked. The userspace runtime refused to acknowledge it. I wrote that up, framed it as "the actual community ask, refined" — give us libnoe source or the ioctl spec and we'll close the gap — and started thinking about how to package the framing.
A few hours later, on the second hunch of the day, I went looking for prior art on MS-R1 specifically. Not Orange Pi 6 Plus, not Radxa Orion, not any of the other Sky1 boards. Just MS-R1. And I found FyrbyAdditive/ms-r1-npu-hack, BSD-2-Clause-Patent licensed, dated April 30, 2026.
One person had already solved it. Cleanly. Four bugs, all real, none of which my morning's port had caught:
- Missing
_HIDon the NPU cores in MS-R1's BIOS DSDT. Minisforum's shipped firmware declaresDevice(\_SB.NPU0)with HIDCIXH4000plus three children with_ADR=Zeroand no_HID. The kernel doesn't enumerate them as platform devices; the driver getsNULLfrombus_find_device_by_fwnode;pm_runtime_enable(NULL)oopses. Cix's own upstream EDK2 source has it correct (_HID = "CIXH4010"per core); Minisforum's flash dropped it. Fix: a 222-byte SSDT override loaded via initramfs CPIO prepend. Fully reversible. Doesn't touch firmware. - UMD↔KMD ABI mismatch.
cix-noe-umd 2.0.4uses k6.6 struct layouts; the cixtech mainline driver had been partially rolled forward without keeping userspace compat. The fix adds v0-compat ioctl handlers (REQ_BUF_V0,FREE_BUF_V0,SCHEDULE_JOB_V0) that translate the older layouts in-kernel, plus widensaipu_cap.asid_basefrom[4]to[32]so theQUERY_CAPioctl size matches what UMD sends. This is what my morning's "Unsupported device type" actually was — not a deep architectural mismatch. A translation layer that hadn't been published. - NPU is 32-bit; mainline arm64 SMMU isn't constrained. The cixtech driver has CIX-specific SMMU constraint code under
#if defined(CONFIG_ARCH_CIX), which mainline arm64 doesn't define. Without that, the IOMMU subsystem hands out 35-bit IOVAs starting at0x700000000; the NPU's address bus only carries 32 bits, truncates, faults on every memory access. Forcebus_dma_limit = 0xc0000000anddma_mask = 32for V3 NPUs. IOVAs land in the right range. NPU reads them. MODULE_IMPORT_NS(DMA_BUF)→"DMA_BUF"— the same string-literal namespace ABI change my morning's port had already hit on an unrelated path. Confirmation that we were independently chasing the same kernel-version drift.
I took FyrbyAdditive's patched DKMS source, built it against both NCZ kernels (780 KB on 6.18.26-LTS, 807 KB on 7.0.3-NEXT), staged the patched modules and the SSDT CPIO override into the post-install hooks. Built r57. Flashed. Installed on MS-R1. Built npu_smoketest.c, linked against libnoe.so.0.6.0 from cix-noe-umd_2.0.2_arm64.deb. Ran it:
noe_init_context OK ctx=0xaaaad4b1e300 (handle=4294967295)
target: X2_1204MP3
partition_count: 1
partition 0: 1 clusters
cluster 0: 3 cores
OK
Translation: kernel module probes, SSDT override injects three cores, libnoe opens /dev/aipu, the v0-compat bridge translates the ABI gap, kernel reports three cores × four TECs, userspace round-trips clean. The "Unsupported device type" wall is gone.
Second lesson, the big one: before you frame a problem as the gap waiting for help, search for the person who already filled it. The right repo had been on GitHub for four days. I didn't think to look for prior art on MS-R1 specifically — I searched for "Sky1," "Cix NPU," "Zhouyi," and "ArmChina" and got back the upstream sources, the Orange Pi work, the Radxa work, and during installation, so users don't have to plenty of partial answers. I didn't search for "MS-R1 NPU." If I had, I'd have found FyrbyAdditive's repo on the first page.
What NCZ adds, given that FyrbyAdditive already did the hard part:
- A Yocto-built kernel pipeline that handles both LTS and NEXT slots cleanly, so the patched module rebuilds against either kernel without DKMS at install-time.
- An installer that stages the SSDT override into the initramfs CPIO chain during installation, so users don't have to apply ASL patches by hand.
- Pre-installed
cix-noe-umd 2.0.2(the v0-compat-friendly version),cix-noe-onnxruntime, andlibnoePython bindings, so the ONNX inference stack works end-to-end on first boot. - A documented status report at
/usr/share/doc/ncz/NPU-STATUS.mdthat names what works, what doesn't, and who solved which piece.
FyrbyAdditive made it work. NCZ makes it install. Credit goes upstream.
What's running by the time you log in
By default, after a fresh install, NCZ has:
- Three AI agent runtimes — zeroclaw, openclaw, and hermes-agent — running as Podman containers managed by systemd quadlets, with an
ncz agentCLI for managing them (ncz agent list,ncz agent show openclaw, etc.) and host wrappers in/usr/local/bin/. - claude-code — Anthropic's terminal CLI, host-installed.
- Container management via Portainer CE on
:9000/, Pods (a native GTK4 Podman GUI from Flathub), and Cockpit + cockpit-podman on:9090/if you prefer system-admin-style. - Three browsers — Vivaldi as default (a real Chromium-fork .deb, ARM64 native), Falkon (Qt WebEngine), Epiphany (WebKit), plus real Chromium 147 via Flatpak. Firefox is purged and held because the Mozilla snap is unusable on Mali and the .deb stub on Ubuntu is a snap-redirect.
- xscreensaver with a curated NCZ theme set —
xanalogtv(CRT static, perfect NeXTSTEP feel),galaxy,glmatrix,glslideshowcycling through the cosmic wallpapers,flyingtoastersbecause of course. - NoMachine 9.4 ARM64 on
:4000/for remote desktop (the NX protocol handles Mali GPU graceful-fallback better than xrdp), plus xrdp on:3389/as a fallback. - Llama.cpp + Ollama for CPU-side LLM inference.
- NPU stack — the patched
armchina_npumodule auto-loads against either kernel; the SSDT override is staged into initramfs to fix MS-R1's missing-_HIDBIOS bug;/dev/aipuis present;cix-noe-umd 2.0.2(v0-compat-friendly) is pre-installed with both C and Python bindings;cix-noe-onnxruntimeis ready. Smoketest passes end-to-end on MS-R1 — three cores × four TECs detected, libnoe round-trips clean, ONNX models compile and run on the NPU. Credit upstream — see The FyrbyAdditive moment above.
It is, in other words, an actually-set-up workstation, not a "now you spend two days configuring it" base image. You could plausibly do real work on it five minutes after the install completes.
Why distros take time
I want to spend a section on something I learned about post-install observability, because it's the thing that turned the last four builds (r58, r59, r60, the in-progress r61) into a multi-day saga rather than a one-evening wrap-up after the FyrbyAdditive integration landed.
Post-install hooks in a Debian-installer pipeline are shell scripts. They run inside the chrooted target rootfs, executed by in-target run-all.sh from the d-i busybox initrd, after the rootfs has been unpacked and base-installer has done its work. They don't have a TTY. They don't have a debugger. They don't have a way to pause and inspect state. You build the ISO, flash it to a USB stick, plug it into MS-R1, boot, walk the d-i flow for ten minutes, log in, and only then do you find out which line of which post-install hook silently failed.
Each one of r58 through r60 was kicked by a different bug that was invisible until that point.
r58 — paths in 80-npu.sh were prefixed with /target/, which is the path the d-i kernel sees. But in-target already chroots into /target before running the hooks, so the in-chroot path is just /. Every cp ./assets/npu/armchina_npu.ko /target/lib/modules/... was writing to /target/target/lib/modules/..., which doesn't exist in the booted system. The NPU module never got deployed. Took one full install cycle to find. Ten-line fix.
r59 — a set -euo pipefail at the top of 50-brand.sh, combined with a find /usr/share/icons/NCZ \| head -1 over a directory that didn't exist yet, killed the script three lines later, right before the LightDM greeter section ran. Booted system shows the leftover Xubuntu Greybird theme. Same script's downstream sibling 30-agents.sh died trying to write /etc/skel/Desktop/Agents/Portainer.desktop into a directory that hadn't been created yet, taking out three-quarters of the agent stack's post-install work. Both fixes are one line each. Both took a full ISO build + flash + install to surface.
r60 — brand fixes from r59 didn't fully bake; an old /usr/share/backgrounds/ncx reference still in the script. Cosmetic, but visible at boot. One-line fix, one full install cycle to confirm.
The pattern: set -euo pipefail is a footgun in post-install hooks where you can't debug-step interactively. Each bug is ten lines to fix, but each takes a full ISO build cycle (8–10 minutes of wall clock plus a USB flash plus a real install on real hardware) to surface, and each one masks every subsequent bug in the same script because set -e truncates the visible failure to the first symptom. You can read a script statically and not see any of these. They show up only on real hardware, only on real installs.
So the honest answer to "why does it take sixty-something ISO revisions to ship a one-person Linux distribution" isn't that I'm slow. It isn't even that the work is hard, exactly. It's that hardware-target post-install hooks have zero observability until the target boots, and the truncation properties of set -e mean each bug gets discovered serially, one ISO at a time. I learned more about set -e + pipefail interactions in two days than I had in twenty years of writing shell scripts. That's a craft observation, not a complaint. It is the single largest delta between "I have something that works on my desk" and "I have something I'd hand to a stranger." Anyone shipping installers for new hardware should expect this gap. Plan the build cycle around it.
What's still open
So if the kernel module works, the userspace round-trips, and ONNX inference runs end-to-end, what's the actual community ask now?
Three things, in order of difficulty:
One: LLM on Zhouyi v3. This is the real frontier. Cix's model hub on ModelScope ships the vision-and-audio surface — YOLOv8n, CLIP text and visual, Whisper tiny/small/medium, SDXL-Turbo, ResNet50, MobileNet, depth estimation, OCR — all of which compile to .cix graphs and run on the NPU today, given a working libnoe. But the hub's LLM/ directory ships Phi-3-mini, Llama-3.2, Qwen2.5, and gemma-2-2b-it, and every one of them lists Device: CPU in the performance tables. The reason: Compass_Unified_Parser, the Cix-shipped graph compiler that turns ONNX into .cix, is static-graph only. Transformer KV cache and variable sequence lengths don't compile to NPU-runnable graphs. Two paths exist — extend the parser to handle dynamic shapes, or write a static-graph LLM decomposition that emits NPU-runnable subgraphs and falls back to CPU for the rest. Either is a research-grade contribution. Either would be the first time anyone — including Cix — ran an LLM on Zhouyi v3 silicon. The 45 TOPS at low watts would justify the work. If you're a compiler person looking for a real problem, here it is.
Two: MS-R1 BIOS fix upstream. The 222-byte SSDT override that makes the NPU enumerate is a workaround for what is, fundamentally, a Minisforum BIOS bug. The shipped DSDT declares the NPU device but drops _HID on the per-core children; Cix's own upstream EDK2 source has it correct. The override is reversible, doesn't touch firmware, and it works — but every MS-R1 NCZ install carries it forever, or until Minisforum ships a UEFI update with the corrected DSDT. The right fix is upstream. If you have a contact at Minisforum's Linux engineering, this is the ticket: publish a firmware update with the upstream-correct DSDT and we delete a hundred-and-some lines of post-install plumbing. We'd be delighted.
Three: UMD compat for cix-noe-umd 4.0.0. The v0-compat ioctl shims FyrbyAdditive added work cleanly with cix-noe-umd 2.0.2 (the version shipping today on Sky1-Linux). The newer cix-noe-umd 4.0.0 ships libnoe.so.3.1.0 and uses a different ioctl layout — the v0-compat shims don't translate. Bridging that gap is somewhere between fifty and two hundred lines of patches, in either the kernel driver (extend the compat layer to v3) or libnoe (if Cix open-sources it). Without 4.0.0 compat, NCZ stays on the 2.0.2 stack indefinitely, which works but means we don't pick up newer Cix model-format support. Open ask to Cix and Arm China both: a published libnoe source, or even just the v3 ioctl spec, makes this a weekend project.
So: vision and audio inference work. The frontier is LLMs. The gap is the OEM firmware. The friction is the userspace ABI cadence. None of those is the same problem. All three are tractable. If any of them looks tractable to you, jperlow@gmail.com is the inbox. Patches, hardware loaners, or beer are all welcome.
(Updated post-publish:) While I was finishing this post, I went and verified two things on a fresh r74 install. First, NCZ already ships a libnoe Python wheel at /usr/share/cix/pypi/, which means you can drive /dev/aipu from Python directly — no C glue required, no SDK install dance, just from utils.NOE_Engine import EngineInfer and you're submitting .cix graphs to three Zhouyi cores. Second, the Cix AI Model Hub (https://github.com/cixtech/ai_model_hub, full LFS artifacts on https://www.modelscope.cn/cix/ai_model_hub_25_Q3.git) is much richer than I'd realized. It ships pre-compiled .cix files for the YOLO catalog, MobileNet, ResNet, EfficientNet, Whisper tiny / small / medium, CLIP / SigLIP, SDXL-Turbo (text encoder + UNet + VAE decoder), PP-OCRv4, hand and pose models, and most interestingly for me, bge-small-zh-v1.5 as a 256-token sentence-embedding model quantized for the NPU. That last one is exactly what I need to wire MNEMOS — my AI memory project to run vector search at NPU latency on the local device. r75 is going to be ncz install mnemos, with the NPU as the embedding. The model's already on disk; the integration is next week's work.
Why I built it instead of just waiting
A reasonable question is: why did I, a person whose last serious involvement with the world of Linux distribution-building was as Sharp's developer-relations contact for the OpenZaurus / Linux-on-the-Zaurus community twenty-something years ago, decide to actually sit down and build one now? It is not lost on me that the build system Chris Larson designed for OpenZaurus is the one that became OpenEmbedded and then Yocto, and that I'm now using Yocto to build a distribution that didn't exist when Chris was writing the recipes I'm distantly descended from. There's a recursive quality to it I didn't plan for. The kid you championed at the company twenty-some years ago grows up to be the toolchain you use to build your own first distro at fifty. Hi, Chris.
Three reasons. They're all kind of the same reason.
One: I wanted the thing to exist, and it didn't, and I had the skills to make it exist. The MS-R1 is genuinely interesting hardware. The user experience around running real Linux on it is genuinely thin. That gap won't close on its own. Someone has to be the first community distro person to land. I figured it might as well be me. (Also: doing infrastructure work for fun, on hardware I picked, with no ticketing system in sight, turns out to be exactly as restorative as you remember.)
Two: the distro-building tools have gotten enormously better. Yocto is a pleasure to work with now in a way it absolutely wasn't in 2003. Custom Debian-installer forks are tractable. systemd-boot replaces all the GRUB foot-cannons. Podman quadlets give you Docker-Compose-style orchestration as systemd units. None of the pieces are heroic; the integration is the work, and the tooling makes the integration approachable. It's a good time for an individual to create a Linux distribution.
Three: I wanted to put a thing into the world with a coherent point of view. There are something like a thousand Linux distributions. Most are derivatives of the big four (Debian, Ubuntu, Fedora, Arch) and don't really stake a position beyond "the old one but with our colors." NCZ has a position: lightest possible desktop, workloads get the cycles, cosmic dread for everyone. You can disagree with that position. I'd be delighted to argue about it. But it's a position.
The pattern, twenty-three years later, with a twist
I want to flag something that took me until late tonight to see clearly: NCZ on Sky1 is a structural mirror of OpenZaurus on the Zaurus, except for the part that isn't.
Twenty-three years ago, a Japanese hardware vendor (Sharp) shipped Linux-capable hardware to Western developers but kept the source code gated, the SDK expensive, and the developer pipeline so thin that the Western community went rogue and built its own distributions. From inside Sharp, watching it happen, I could see exactly why: the corporate machinery wasn't designed to engage at developer-relations speed, and the developers, being developers, did not wait. Chris Larson built OpenZaurus on the cards that Sharp wouldn't deal him. The work was load-bearing. The work happened anyway.
Today, a Chinese hardware vendor (Cix Technology) ships Linux-capable hardware to Western developers, and Cix has actually published source. The EDK2 firmware. The ArmChina NPU driver. The userspace runtime headers. Pre-compiled .cix model graphs on ModelScope. Not perfectly — libnoe.so itself is closed; the LLM compiler path is static-graph-only; and the documentation that reaches into Western community channels is thin. But the load-bearing pieces are open. The gap that needed filling was between what Cix published and what shipped on the OEM hardware: Minisforum's BIOS dropped the _HID declarations, the prebuilt UMD divergence created an ABI mismatch, the upstream arm64 kernel doesn't define the SMMU constraints the NPU needs. Each fix is a couple hundred bytes of patches. Each one needed someone to find it.
That someone, on April 30 of this year, goes by FyrbyAdditive on GitHub. They published 230 lines of cumulative driver patches, a 47-line ASL override, and step-by-step DKMS install instructions for getting a working NPU on MS-R1 hardware. The repository is BSD-2-Clause-Patent licensed, free to integrate. They did the load-bearing work the same way Chris Larson did it on the Zaurus, except they had three days where Chris had a year, because Cix had published the source Larson didn't have.
So the recursion now has three layers. Sharp didn't publish; Larson built OpenZaurus by reverse-engineering. Cix did publish; FyrbyAdditive built the NPU stack by reading what was published and patching what wasn't. NCZ wraps that whole chain into a Debian installer that takes ten minutes from USB stick to GUI login with the NPU loaded and /dev/aipu ready for inference. Three steps. Each one shorter than the last, because the upstream got more open at each turn.
What's different about this side of the dynamic, compared to where I sat at Sharp in 2003: the corporate machinery actually engaged this time. Cix Technology publishes meaningfully more source code than Sharp did, faster, in more places. ArmChina's Compass parser, kernel-side bits, userspace headers, model-hub artifacts. The community-side work is still real and still load-bearing — FyrbyAdditive's three-day port is the proof — but the community didn't have to invent the toolchain the way Larson did. They had to patch and integrate what was already there.
If the lesson at Sharp twenty-three years ago was "the community will fill the vendor gap whether you help or not, so you might as well help," the lesson at Cix today is "the community will fill the OEM gap whether the vendor helps or not, so the vendor publishing source is the highest-leverage thing they can do — and Cix has done it."
I am, to my own surprise, more optimistic about this story's ending than I was about the last one's.

Try it
The r61 ISO is at gitlab.com/nclawzero/cix-installer/-/releases (3.7 GB thin / 8.8 GB max). Source at github.com/nclawzero/distro. Issues, PRs, and "you idiot, here's how the NPU actually works" emails to jperlow@gmail.com.
It's only been tested on MS-R1. It should work on Radxa Orion O6 too — that's next on my list. If you have other Sky1 hardware and want to be a guinea pig, get in touch.
If you only got this far because you came for the title pun and stayed for the cosmic-destruction motifs: same. Welcome.
Jason Perlow is a former IBM consultant, ex-Microsoft, longtime ZDNET contributing editor, and Linux Foundation Editorial Director. He has been making computers do unreasonable things since the late 1980s and apologizes to nobody.