The Argo OS Journey - Part 4: The Hybrid Vision

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.conf with 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 ripgrep is compiled with -O3? Not really. It’s already instant.
  • Does it matter if ripgrep is 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:

PackageLayerManagerWhy
Linux kernelCoreGentooHardware interaction
NVIDIA driversCoreGentooGPU support
KDE PlasmaCoreGentooDesktop performance
Mesa/VulkanCoreGentooGraphics stack
ripgrepUserlandNixCLI tool, reproducibility
neovimUserlandNixEditor, configuration
DockerGray zoneGentooSystem daemon
kubectlGray zoneNixCLI 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:

  1. Install Gentoo (binhost makes this fast)
  2. Copy /etc/portage/
  3. Install Nix
  4. Copy ~/.config/home-manager/
  5. 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:

  1. Considering building your own custom Linux distribution
  2. Procrastinating on something important
  3. 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.