The Argo OS Journey - Part 4: The Hybrid Vision
December 7, 2025
Optimization is an addiction. Once you start compiling your kernel to strip out drivers for hardware you don’t own, it’s hard to stop.
You start with -march=native. Then you’re tuning CFLAGS. Then you’re removing kernel modules for SCSI tape drives because you don’t own a tape drive. Then you’re spending 45 minutes deciding whether -O3 is actually faster than -O2 for your specific workload.
At some point, you realize you’ve crossed a line. But you don’t go back. You just keep optimizing.
The problem? Optimization has a cost: Brittleness.
The Reproducibility Problem
By early December, I had an incredible system. Custom kernel. Binary packages from my own binhost. Snapshots protecting me from bad updates. Cloud backups protecting me from house fires.
But I had a nagging fear.
If I buy a new laptop in 2 years, can I recreate this exact environment?
Let’s be honest about what “this environment” actually means:
- 1,571 packages with specific USE flags
- A custom
/etc/portage/make.confwith 47 lines of carefully tuned settings - 23 files in
/etc/portage/package.use/ - 12 files in
/etc/portage/package.accept_keywords/ - 8 files in
/etc/portage/package.mask/ - A custom kernel config with 4,000+ lines
- Dotfiles scattered across
/home/commander/in 50+ locations
With Gentoo? Maybe. If I kept meticulous backups of /etc/portage, never missed a config file, documented every kernel option, and wrote down why I made each decision.
With Nix? One file. flake.nix. Done.
I realized I didn’t want to choose.
Two Philosophies
Gentoo: The Ultimate Customization
Gentoo’s philosophy: You control everything.
Want to compile Firefox with PGO and disable WebRTC? You can. Want to build KDE without Bluetooth support because you don’t use Bluetooth? You can. Want to strip out every kernel driver except the ones for your exact hardware? You can.
The result is a system that’s perfectly optimized for your specific machine. Nothing is wasted. Every binary is tuned.
The cost: Configuration is imperative, not declarative.
You do things to the system. emerge this. echo that >> /etc/portage/package.use/custom. eselect profile set. Each action changes the system state. To recreate it, you’d need to replay every action in the exact same order.
Nix: The Ultimate Reproducibility
Nix’s philosophy: Describe what you want. Let the system figure out how to get there.
{ pkgs, ... }:
{
home.packages = with pkgs; [
neovim
ripgrep
git
];
}
That’s it. Run home-manager switch. You now have neovim, ripgrep, and git. Run it on another machine. Exact same versions. Exact same configuration. Byte-for-byte identical.
The result is a system that’s perfectly reproducible. You can hand your flake.nix to someone else and they’ll get exactly what you have.
The cost: Everything is sandboxed.
Nix packages don’t use system libraries. They bundle their own. A Nix binary of alacritty doesn’t link against your system’s OpenGL. It links against the OpenGL in /nix/store/. Which means it doesn’t see your GPU drivers.
The Hybrid Theory
December 7th, 2025. 11:43 PM. I’m staring at my terminal, thinking about this problem.
Gentoo for hardware. Nix for configuration.
Why not both?
Layer 1: The Core (Gentoo)
This is the hardware abstraction layer. The part of the system that talks to the metal.
What belongs here:
- The kernel (custom built,
-march=native, stripped to exactly my hardware) - GPU drivers (NVIDIA proprietary, because I need CUDA)
- The display server (Wayland/KWin)
- The desktop environment (KDE Plasma)
- System daemons (elogind, D-Bus, polkit)
- The filesystem (Btrfs with Snapper)
Why Gentoo for this layer:
- I want my window manager to respond in milliseconds
- I want my GPU to work with zero configuration
- I want my boot time to be under 10 seconds
- These components interact with hardware. Optimization matters.
The make.conf for this layer:
# /etc/portage/make.conf - Layer 1 (Hardware)
COMMON_FLAGS="-O2 -pipe -march=native"
CFLAGS="${COMMON_FLAGS}"
CXXFLAGS="${COMMON_FLAGS}"
# System-level packages only
USE="X wayland kde plasma qt6 vulkan nvidia"
USE="${USE} -systemd elogind"
# GPU
VIDEO_CARDS="nvidia"
# Binary packages for speed
FEATURES="buildpkg binpkg-multi-instance getbinpkg"
PORTAGE_BINHOST="ssh://[email protected]/var/cache/binpkgs/stable"
Layer 2: The Userland (Nix)
This is where I actually live. The tools I use every day.
What belongs here:
- Text editors (Neovim)
- Shell and prompt (Zsh + Starship)
- CLI tools (ripgrep, fzf, jq, bat, fd, eza)
- Development tools (git, lazygit, gh)
- Language toolchains (Rust, Go, Python)
Why Nix for this layer:
- Does it matter if
ripgrepis compiled with-O3? Not really. It’s already instant. - Does it matter if
ripgrepis configured exactly the same way on every machine I own? Yes.
I SSH into 5 different machines regularly. I want the same keybindings, the same aliases, the same prompt everywhere. I don’t want to maintain 5 separate .zshrc files.
The Implementation
Installing Nix on Gentoo
Nix can run on any Linux distribution. It doesn’t replace the package manager; it supplements it.
# Single-user installation (simpler, what I use)
sh <(curl -L https://nixos.org/nix/install) --no-daemon
This creates:
/nix/store/— Where all Nix packages live~/.nix-profile/— Symlinks to your active packages~/.nix-channels— Package sources
Home Manager: The Magic
Home Manager is what makes Nix useful for non-NixOS systems. It manages your home directory declaratively.
# Install Home Manager
nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
nix-channel --update
nix-shell '<home-manager>' -A install
Now I can create ~/.config/home-manager/home.nix:
{ config, pkgs, ... }:
{
# Let Home Manager manage itself
programs.home-manager.enable = true;
# Basic info
home.username = "commander";
home.homeDirectory = "/home/commander";
home.stateVersion = "23.11";
# Packages to install
home.packages = with pkgs; [
# CLI essentials
ripgrep
fd
fzf
bat
eza
jq
yq
htop
btop
# Development
lazygit
gh
delta # Better git diffs
# Fun
neofetch
cowsay
figlet
];
# Git configuration
programs.git = {
enable = true;
userName = "commander";
userEmail = "[email protected]";
extraConfig = {
init.defaultBranch = "main";
pull.rebase = true;
push.autoSetupRemote = true;
core.pager = "delta";
interactive.diffFilter = "delta --color-only";
delta = {
navigate = true;
side-by-side = true;
line-numbers = true;
};
};
aliases = {
st = "status";
co = "checkout";
br = "branch";
ci = "commit";
lg = "log --oneline --graph --all";
};
};
# Zsh configuration
programs.zsh = {
enable = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
shellAliases = {
ls = "eza --icons";
ll = "eza -la --icons";
lt = "eza --tree --icons";
cat = "bat";
grep = "rg";
find = "fd";
# Gentoo shortcuts
emu = "sudo emerge --update --deep --newuse @world";
emc = "sudo emerge --depclean";
ems = "emerge --search";
};
initExtra = ''
# Custom prompt additions
export PATH="$HOME/.local/bin:$PATH"
# FZF integration
source ${pkgs.fzf}/share/fzf/key-bindings.zsh
source ${pkgs.fzf}/share/fzf/completion.zsh
'';
};
# Starship prompt
programs.starship = {
enable = true;
settings = {
add_newline = false;
character = {
success_symbol = "[➜](bold green)";
error_symbol = "[✗](bold red)";
};
directory = {
truncation_length = 3;
truncate_to_repo = true;
};
git_branch = {
symbol = " ";
};
hostname = {
ssh_only = true;
format = "[$hostname](bold blue) ";
};
};
};
# Neovim
programs.neovim = {
enable = true;
defaultEditor = true;
viAlias = true;
vimAlias = true;
extraPackages = with pkgs; [
# LSP servers
lua-language-server
nil # Nix LSP
rust-analyzer
gopls
nodePackages.typescript-language-server
# Formatters
stylua
nixfmt
rustfmt
gofumpt
];
};
}
The Magic Moment
home-manager switch
Output:
Starting home-manager activation
Activating checkFilesChanged
Activating checkLinkTargets
Activating writeBoundary
Activating installPackages
installing 'home-manager-path'
Activating linkGeneration
Creating profile generation 1
Creating home file links in /home/commander
Activating onFilesChange
Activating setupLaunchd
Boom. My entire environment appeared.
Then I went to my server. Installed Nix. Copied my home.nix. Ran home-manager switch.
Boom. Identical environment. Same prompt. Same aliases. Same keybindings.
This is the power of declarative configuration.
The Conflict: OpenGL
It wasn’t all sunshine.
The Problem
I wanted to use Alacritty (a GPU-accelerated terminal) installed via Nix.
nix-env -iA nixpkgs.alacritty
alacritty
Output:
Error: WinitEventLoop(Os(OsError { line: 80, file: "src/platform_impl/linux/x11/window.rs", error: XError { description: "GLXBadFBConfig", error_code: 183, request_code: 152, minor_code: 34 } }))
Or sometimes:
Error: EGL not available
Why This Happens
Nix packages are self-contained. They don’t link against system libraries. This is what makes them reproducible—they bundle everything they need.
The Nix binary of Alacritty is linked against:
/nix/store/abc123-mesa-23.1.0/lib/libGL.so/nix/store/def456-libdrm-2.4.115/lib/libdrm.so
My NVIDIA drivers are at:
/usr/lib64/libGL.so/usr/lib64/libEGL.so/usr/lib64/libnvidia-glcore.so
The Nix Alacritty doesn’t see my GPU. It sees a software-rendered Mesa. Which doesn’t work.
The Solution: nixGL
There’s a wrapper called nixGL that injects the host’s OpenGL drivers into the Nix environment at runtime.
# Add the nixGL channel
nix-channel --add https://github.com/guibou/nixGL/archive/main.tar.gz nixgl
nix-channel --update
# Install nixGL for NVIDIA
nix-env -iA nixgl.nixGLNvidia
# Now run Alacritty with the wrapper
nixGLNvidia alacritty
Works perfectly.
Automating the Wrapper
I don’t want to type nixGLNvidia every time. So I created wrapper scripts:
# ~/.local/bin/alacritty
#!/bin/bash
exec nixGLNvidia /home/commander/.nix-profile/bin/alacritty "$@"
And added this to my home.nix:
home.file.".local/bin/alacritty" = {
executable = true;
text = ''
#!/bin/bash
exec nixGLNvidia /home/commander/.nix-profile/bin/alacritty "$@"
'';
};
Now alacritty just works. The wrapper is managed by Home Manager, so it’s reproduced on every machine.
The Binhost Integration
One issue: my binhost compiles packages for all my machines. If a package can be installed from a binary, I want it to come from the binhost.
But now I have two package managers:
- Gentoo’s Portage (for system packages)
- Nix (for userland tools)
The Rule
If it touches hardware, use Gentoo. If it’s a CLI tool, use Nix.
Examples:
| Package | Layer | Manager | Why |
|---|---|---|---|
| Linux kernel | Core | Gentoo | Hardware interaction |
| NVIDIA drivers | Core | Gentoo | GPU support |
| KDE Plasma | Core | Gentoo | Desktop performance |
| Mesa/Vulkan | Core | Gentoo | Graphics stack |
| ripgrep | Userland | Nix | CLI tool, reproducibility |
| neovim | Userland | Nix | Editor, configuration |
| Docker | Gray zone | Gentoo | System daemon |
| kubectl | Gray zone | Nix | CLI tool |
The Gray Zone
Some packages are ambiguous. Docker is a system daemon, but it’s also a development tool.
My rule: If it has a system service, use Gentoo. Docker runs as a daemon. It needs to start at boot. It interacts with the kernel (cgroups, namespaces). That’s Gentoo territory.
Kubectl, on the other hand, is a pure CLI tool. It doesn’t run as a daemon. It doesn’t touch the kernel. That’s Nix territory.
The Python Conflict
While we’re here: I finally fixed the Python target conflict.
The Problem
My binhost was compiling packages with PYTHON_TARGETS="python3_11 python3_12".
My desktop had PYTHON_TARGETS="python3_12".
When I pulled a binary package that depended on Python, it would sometimes pull in Python 3.11 libraries I didn’t need.
The Solution
Standardize. Everywhere.
# On ALL machines (binhost, desktop, laptop)
# /etc/portage/make.conf
PYTHON_TARGETS="python3_12"
PYTHON_SINGLE_TARGET="python3_12"
Then rebuild everything that depended on Python:
emerge --update --deep --newuse @world
emerge --depclean
This removed all Python 3.11 packages from my system. Clean.
The Final Architecture
┌─────────────────────────────────────────────────────────────┐
│ ARGO OS ARCHITECTURE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ LAYER 2: USERLAND (Nix) │ │
│ │ │ │
│ │ home.nix → Home Manager → ~/.nix-profile/ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ neovim │ │ ripgrep │ │ starship │ │ lazygit │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ Reproducible across all machines via flake.nix │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ nixGL wrapper │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ LAYER 1: CORE (Gentoo) │ │
│ │ │ │
│ │ make.conf → Portage → /usr/, /lib64/, /etc/ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ kernel │ │ nvidia │ │ kde │ │ elogind │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ Optimized with -march=native, binary packages │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ LAYER 0: HARDWARE │ │
│ │ │ │
│ │ i7-4790K • RTX 4070 Ti • NVMe • Btrfs + Snapper │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
The Statistics
After implementing the hybrid system:
Gentoo Layer (Portage):
- Installed packages: 1,247
- Binary packages pulled: 95% of installs
- Compile time saved: ~40 hours/month
Nix Layer (Home Manager):
- Declared packages: 47
- Generation size: 1.2 GB
- Switch time: 12 seconds
Reproducibility:
- Time to rebuild Gentoo layer from scratch: ~4 hours (with binhost)
- Time to reproduce Nix layer from scratch: ~2 minutes (download)
- Time to restore full environment on new machine: ~5 hours
What I track in git:
/etc/portage/→ Gentoo configuration~/.config/home-manager/→ Nix configuration~/dotfiles/→ Miscellaneous configs
What I Learned
Use The Right Tool For Each Layer
Gentoo and Nix aren’t competitors. They solve different problems.
Gentoo excels at:
- Hardware-specific optimization
- System-level packages
- Kernel and drivers
- Things that need to be fast
Nix excels at:
- Configuration reproducibility
- Userland tools
- Dotfiles management
- Things that need to be consistent
Using both gives you the best of both worlds. Optimized core. Reproducible environment.
Declarative > Imperative
After 15 years of maintaining dotfiles with symlinks and shell scripts, Home Manager was a revelation.
Old way:
ln -sf ~/dotfiles/.zshrc ~/.zshrc
ln -sf ~/dotfiles/.vimrc ~/.vimrc
# Repeat for 50 files
# Hope you didn't forget any
# Hope the target system has the same structure
New way:
programs.zsh.enable = true;
programs.neovim.enable = true;
home-manager switch
The second approach is declarative. I don’t describe how to get my environment. I describe what my environment should look like. The system figures out the rest.
Complexity Has A Cost
Is this system complex? Yes.
Am I maintaining two package managers? Yes.
Is it over-engineered? Absolutely.
But the complexity is bounded. I understand both systems. Each has a clear purpose. And the result is a machine that’s both fast AND reproducible.
Sometimes over-engineering is just… engineering the system you actually need.
The 12-Year Upgrade Path
My i7-4790K is 12 years old. I’ve upgraded the RAM (16GB → 32GB), the storage (SATA SSD → NVMe), and the GPU (GTX 970 → RTX 4070 Ti). The CPU is the only original component.
When I finally replace this machine, I won’t start from scratch. I’ll:
- Install Gentoo (binhost makes this fast)
- Copy
/etc/portage/ - Install Nix
- Copy
~/.config/home-manager/ - Run
home-manager switch
Four hours, maybe. And I’ll have exactly my environment. Not “similar to.” Exactly.
That’s the hybrid vision.
The End State
Argo OS is now a distribution that defies categorization.
It boots like Gentoo. It snapshots like openSUSE. It configures user tools like NixOS.
Is it weird? Yes. Is it sustainable? Three months in, yes. Is it mine? Absolutely.
And for the first time in 15 years of using Linux, I feel like I have a system that is both incredibly fast AND impossible to break AND reproducible.
That was always the goal.
What’s Next
This concludes the “Journey” series. 46 days in, from first emerge to hybrid architecture.
But the work continues. The binhost needs maintenance. The build swarm could use more drones. The documentation needs updating.
For the specific configurations:
If you’ve read this far, you’re probably either:
- Considering building your own custom Linux distribution
- Procrastinating on something important
- Both
Either way, thanks for coming along on this journey. It’s been… an experience.
46 days. 1 custom distribution. 2 package managers. 1,571 packages. 2,146 lines of shell script. Countless cups of coffee.
Worth it? 100%. And the journey continues.