Summary · Background · Why it happens · How Omnix fixes it · Install · Repo layout · Testing · Get started
NixOS can't run most software built for "normal" Linux. A program downloaded from a vendor, a pip wheel, an npm
package with a native binary, or an AppImage all expect to find their shared libraries in /lib and /usr/lib.
On NixOS those directories don't exist, so the program fails before its first line of code runs.
This is not a bug. It's a deliberate trade-off at the centre of NixOS's design, and it buys the reproducibility and safe rollbacks that people choose NixOS for. But the cost is real: every NixOS user eventually hits it, the tools that work around it have to be found and wired up by hand, and whole distributions like Omarchy can't run on NixOS at all.
Omnix makes NixOS FHS-compliant. It's a small base distribution that adds the standard /lib64 loader, /usr/lib,
/bin, and library cache to NixOS, so prebuilt Linux software just runs. It never moves or patches the Nix store, so
it can use the same prebuilt packages as NixOS and compile when necessary, while preserving rollbacks. On top of
the base, you pick a flavor, a complete desktop such as an Omarchy port or a KDE Plasma setup.
Why now. There's been a lot of debate about whether the NixOS project would ever work with Omarchy, given the politics around its creator, DHH. For Omnix, it turns out not to matter. NixOS's real advantage is the public binary cache at cache.nixos.org. Omnix builds on that cache without changing a single package, using the same prebuilt packages as NixOS when available and compiling when necessary. The installer ISO is hosted on GitHub Releases, and available prebuilt packages come from the NixOS cache.
Almost every program on Linux is dynamically linked: the executable file holds only the program's own code, and it borrows common code (the C library, the C++ runtime, compression, graphics, TLS) from shared libraries that are installed once and used by everything.
When you start such a program, two lookups happen before main() runs:
- Find the loader. The executable names a dynamic linker to start it. On x86-64 this path is fixed in the file
at build time:
/lib64/ld-linux-x86-64.so.2. - Find the libraries. The loader reads the list of libraries the program needs (
libc.so.6,libstdc++.so.6,libz.so.1, …) and searches for each one in a standard order: paths baked into the binary,LD_LIBRARY_PATH, the cache in/etc/ld.so.cache, then the default directories/liband/usr/lib.
The Filesystem Hierarchy Standard (FHS) is the
agreement that makes this work everywhere. Debian, Fedora, Arch, and Ubuntu all put the loader and libraries in the
same places, so one binary built on any of them runs on all of them. Software vendors rely on this. So does nearly
every shell script, which starts with #!/bin/bash and expects tools in /usr/bin.
NixOS deliberately does not follow the FHS.
Instead of installing libraries into shared directories, Nix puts every package in its own directory in the Nix store, named by a hash of everything used to build it:
/nix/store/q4kd9…-glibc-2.40/lib/libc.so.6
/nix/store/8vbm2…-gcc-14.2.0-lib/lib/libstdc++.so.6
/nix/store/zr4c1…-zlib-1.3.1/lib/libz.so.1
This is the core of what makes NixOS good:
- Many versions coexist. Two programs can use two different versions of the same library without conflict.
- Nothing is overwritten. An upgrade adds new store paths instead of replacing files, so a rollback is just switching back to the old set.
- Builds are pure. A package can only see the dependencies it declared, so builds are reproducible.
Software that Nix builds itself works because Nix rewrites each binary at build time to point at exact store paths
for its loader and libraries. Software built anywhere else hasn't been rewritten. It still asks for
/lib64/ld-linux-x86-64.so.2 and searches /usr/lib, and on NixOS those paths are empty or missing. There is no
single "the" libstdc++.so.6 for the loader to find, only a dozen hash-named copies in the store, and nothing tells
it which one to use.
The rule is add, never relocate. Omnix leaves /nix/store untouched and adds the standard paths on top of it,
using tools nixpkgs already ships:
/lib64/ld-linux-x86-64.so.2is provided by nix-ld, a small shim that starts the real glibc loader with Omnix's library path. Unmodified binaries start, including from systemd services, cron, andssh host cmd./usr/liband/libpoint at a declared set of common libraries: the C/C++ runtime, compression, crypto, SQLite, ICU, and more. An optional desktop preset adds GTK, Qt, WebKitGTK, Mesa, Vulkan, X11, and audio libraries for Electron apps, Playwright browsers, and AppImages./etc/ld.so.cacheand/sbin/ldconfigare regenerated on every switch, so code that searches for libraries itself, like Python'sctypes.util.find_library, finds them./binand/usr/binare provided by envfs, so#!/bin/bash,#!/usr/bin/python3, and hard-coded tool paths resolve to whatever is onPATH.
All of it is generated from your configuration and points through /run/current-system, so rolling back a NixOS
generation rolls back the library layout with it. Nix's own builds still can't see /usr/lib, so they stay pure.
Installing Omnix takes five choices and one download. The installer is a text menu: arrow keys to move, Enter to pick. Nothing is written to disk until you confirm at the end of step 4.
Download the ISO from Releases, write it to a USB stick, and boot from it. It's the stock NixOS minimal installer plus the Omnix base, so it runs on anything NixOS supports.
Then run sudo omnix-install. The first thing it does is get online, because everything after this step is
downloaded. A wired connection is picked up automatically. On Wi-Fi, it opens nmtui so you can choose a network.
Install to which disk? (it will be ERASED)
▸ /dev/nvme0n1 1.8T Samsung SSD 990 PRO
/dev/sda 465G Crucial MX500
Filesystem
▸ ext4
btrfs
xfs
| Filesystem | Choose it if… |
|---|---|
| ext4 | You want the simple, proven default |
| btrfs | You want filesystem snapshots on top of NixOS's own rollbacks |
| xfs | You build a lot of code: copy-on-write cloning makes copy-heavy builds such as Rust fast |
Omnix uses the whole disk. Installing next to another operating system is planned for a later version.
Encrypt the disk (LUKS)?
Yes ▸ No
Choose Yes for full-disk encryption with LUKS. You'll type a passphrase at every boot, and the disk is unreadable without it. Recommended for laptops.
What kind of system do you want?
▸ Autarchy (stable) — Omarchy-style keyboard-driven Hyprland, pinned to an Omarchy release
Autarchy (latest) — Omarchy-style keyboard-driven Hyprland, tracking Omarchy main
Atrium — KDE Plasma desktop: polished windows, mouse-first
Minimal — The Omnix base alone: console, FHS layer, nothing else
|
Omarchy, ported to Omnix. The keyboard-driven Omarchy desktop on Hyprland: tiling windows, Omarchy's keybindings, themes, and tools, rebuilt as declarative NixOS modules.
|
Lots of windows, done well. A polished, mouse-first KDE Plasma 6 desktop for people who like a taskbar and overlapping windows.
|
More styles are coming. The menu is read live from flavors.json,
so new flavors appear in the installer without a new ISO. Minimal installs just the base, for servers or for
building your own setup.
After choosing, you set a username, password, hostname, and timezone, then confirm one last time before the disk is erased.
The installer partitions and formats the disk, writes your machine flake, and installs. Available packages are downloaded prebuilt from cache.nixos.org, the official NixOS binary cache. Software is compiled when necessary, using the package versions pinned by the ISO.
It also detects your hardware: an NVIDIA GPU gets the NVIDIA driver automatically.
When it finishes, reboot into your new system.
Tip
Changed your mind later? Your flavor is a single input in /etc/nixos/flake.nix. Change it and run
nixos-rebuild switch to switch from Atrium to Autarchy, or the other way round. The previous system stays in
the boot menu, so you can always go back.
Omnix is deliberately small. The base only provides FHS compatibility. Everything opinionated lives in a flavor, and each flavor is its own repository.
Every install produces a flake that you own, in /etc/nixos. It stacks four layers, and dependencies only point down:
| Layer | What it is | Lives in |
|---|---|---|
| Machine | Your user, hardware, disk, and local tweaks | Your /etc/nixos flake |
| Flavor | A complete system: desktop, apps, settings | A flavor repo |
| Base | NixOS plus the FHS layer | Omnix |
| nixpkgs | nixos-unstable, unmodified |
cache.nixos.org |
| Repository | Role |
|---|---|
🧊 Omnix |
The base and the installer. The FHS module, the installer ISO, the flavor registry (flavors.json), and the design docs |
🟣 Autarchy |
Flavor: Omarchy, ported. A keyboard-driven Hyprland desktop, in stable (a pinned Omarchy release) and latest (Omarchy's main branch) variants |
🪟 Atrium |
Flavor: KDE Plasma. A polished, mouse-first Plasma 6 desktop, fully declarative |
⚙️ .github |
This org profile |
A Minimal option, the base alone, is built into the installer.
- The base boots in a NixOS VM under
nix flake checkand runs a collection of real downloaded binaries against the FHS layer. - The installer is tested end to end:
tests/install-qemu.pyinstalls the ISO unattended in QEMU and boots the result. - Each flavor has its own VM test that boots its desktop session.
- Every flavor must be cache-clean: a dry-run build of a reference machine must show nothing to compile, so installing never builds software locally.
Autarchy doubles as the hardest test of all. Omarchy is a script-driven install that assumes Arch, a standard Linux layout, and prebuilt binaries. If it runs on Omnix, the FHS layer works.
Fresh install: follow the install walkthrough.
Existing NixOS (unstable) machine: add the base to your flake.
inputs.omnix = { url = "github:Omnix-Linux/Omnix"; inputs.nixpkgs.follows = "nixpkgs"; };
# modules = [ omnix.nixosModules.default ... ];- 🐛 Found a prebuilt binary that won't run? Open an issue on
Omnixwith the program and the error. Don't diagnose withldd: it bypasses the loader shim and reports working binaries as broken. - 🎨 Want to build a flavor? Read the flavor contract in the
design doc, then open a pull request that adds it to
flavors.json.