I Rewrote Portage in Go and Lost My Mind Doing It (Worth It)

I've been a Gentoo guy for a long time. And for almost as long, I've had this thought in the back of my head that just wouldn't go away:

What if Portage was written in some faster, native programming language?

Not in a "ugh Python bad" way. More like - I'm lying awake at 2am and I can't stop thinking about dependency graphs and why the hell merge has to block on resolution when sync just happened. You know that feeling when a technical problem just gets under your skin and won't let go? That's what this was.

So eventually I stopped fantasizing about it and just started building it.

The stupid ambitious goal

I wanted Gentoo running in VirtualBox in 15 minutes. Start to usable desktop, 15 minutes.

If you've ever installed Gentoo you know what I'm asking. The handbook alone takes longer than that. Hours of compiling. Partitioning by hand. Configuring USE flags while googling three things at once. It's a whole thing - and that's on purpose, that's the Gentoo way. But I wanted to see if I could make it stupid fast without sacrificing what makes Gentoo worth using.

That obsession is what became portage_go.

portage_go_cmds.png

The thing that actually kept me up

Here's the real idea that started all of this. Not "Go is faster than Python" (it is, massively, but that's almost a side effect).

When you do emerge --sync and then emerge -uDN @world, Portage fetches the entire ebuild tree during sync... and then does nothing with it. When you actually ask for a merge, it starts walking the dependency graph from scratch, right then, blocking you. All that work you just fetched - just sitting there.

What if --sync built the full dependency graph and stored it in SQLite right then? So when you run emerge <package>, the answer is already there. You're querying a pre-built index, not re-walking the entire tree cold.

That's the bet. Build the world during sync. Merge starts instantly.

portage_go_emerge_world.png

It's not a complicated idea. It's one of those ideas that once you see it you can't unsee it, and then you're writing Go at 1am because what else are you going to do.

What I actually built

portage_go - the core

The meat of it: ebuild parsing, USE flag evaluation, dependency resolution, all the core package manager operations - rewritten in Go. The dependency graph goes into SQLite via modernc.org/sqlite (pure Go, no CGO, no native deps) and gets rebuilt incrementally on sync.

The key constraint I set for myself: 100% behavioral compatibility with Python Portage. The Python implementation is the spec. The Go implementation has to produce identical results, including for the weird edge cases that accumulated over 20 years that nobody documented but everyone's system quietly depends on.

portage_go_monorepo_view.png

I found bugs in the Python version during this process. Real ones, producing wrong behavior. Some I fixed in Go. Some I kept as-is because they're de-facto spec at this point - if Portage has been doing it "wrong" for 15 years and systems are built around it, that's the behavior. I documented every divergence.

explode2lxc - how I test without losing my mind

You can't unit test a package manager into correctness. You need a real system to throw packages at.

portage_go_explode2lxc.png

explode2lxc spins up a Gentoo environment in an LXC container and wires it so portage_go can run against it. Every change gets validated against real live Gentoo behavior. Claude Code runs autonomously in this loop - build something, try it, fail, fix it, try again. I get an SMS when it's done or stuck.

setup - the installer

The other half of the 15-minute target. An automated installer that goes into the install-amd64-minimum.iso and handles hardware detection, partitioning, the full Gentoo handbook setup sequence, kernel compilation with autodetection. Boot the ISO, walk away, come back to a system.

15 minutes clock starts at boot.

purpled - the daemon

Here's the fun part. Binary packages get you to 15 minutes but using binary packages on Gentoo feels philosophically wrong. Like buying a baguette from a supermarket.

purpled is the daemon that fixes this. Install from binaries - fast, done, no waiting. Then purpled watches for system idle time and uses those idle periods to rebuild packages from source with your specific USE flags and CPU optimizations. Over time the system converges from "binary Gentoo" to "fully compiled for your hardware Gentoo" by itself, in the background, while you're doing other things.

Your computer gets faster every day you use it. Automatically. No action required.

The stack (because I know you want to know)

Go, only Go. No Python tooling, no Node, no CGO where I can avoid it. Bazel as the single build authority - build, test, coverage, everything goes through Bazel. SQLite for the dependency graph. Hexagonal architecture because it's the only way I know how to build things that stay sane over time. 25-line function hard limit across the whole codebase. SonarQube gates on every change. The whole monorepo lives under Dropbox for cross-machine sync (I did so many git push in my career).

portage_go_emerge_version.png

The CI loop is autonomous. Claude Code runs against the VM, I get SMS updates. It's been running basically unattended for days at a time.

What "100% compatibility" actually costs

This part is where the real hours went and I want to be honest about it because it's not glamorous.

Portage has 20+ years of edge case handling. Slot conflicts with weird inheritance chains. USE flag propagation behavior that's technically wrong but everything depends on. Circular dependency resolution quirks. Things that aren't in any doc because they never needed to be - they just work, and everything is built around them working that way.

Every time my Go implementation diverged from Python Portage I had a decision to make: is this Python being wrong, or is this Python being de-facto spec? A lot of them are the latter. I found real bugs in Portage during this project. Some I fixed. Some I documented and matched because breaking them would be worse than keeping them.

This is the work that doesn't show up in metrics. No test covers it. It's just hours of running things and diffing outputs and thinking hard about whether a difference is a bug or a feature that nobody wrote down.

And then we started fuzzing

Unit tests get you so far. At some point you just have to throw real packages at it and see what breaks.

portage_go_bazel_status.png

The idea was simple: work up a ladder of increasingly complex emerge targets inside explode2lxc. Start with small leaf packages. Then things with real dependency trees. Then metapackages that pull in half the world. Use the real Gentoo package tree as the fuzzer - not random bytes, just 20 years of accumulated real-world complexity running against the dependency engine.

The target I had in my head from the beginning was xfce4-meta. Full XFCE desktop metapackage, Wayland support, Mesa, the whole graphics stack. If portage_go could chew through that correctly - right resolution order, USE flags propagated properly through the entire chain, no slot conflicts, packages built - then I'd know the engine was real.

Getting there took a 36-hour Claude Code session.

That's not me being dramatic. I set the goal, kicked off the session, and let it run. For 36 hours Claude Code worked through the build ladder autonomously - trying packages, hitting failures, diagnosing what went wrong, fixing it in portage_go, rebuilding. Every failure was signal. A USE flag edge case that the unit tests hadn't modeled. A circular dep the resolver wasn't handling right. A slot conflict workaround that Python Portage had baked in and my implementation hadn't replicated yet.

The failures were the whole point. Each one tightened the compatibility envelope in ways I couldn't have hand-written tests for.

At the end of 36 hours, xfce4-meta built. Full desktop stack, Wayland, Mesa, dependency tree resolved correctly, packages in the right order, USE flags right all the way down.

That was the moment this went from "interesting experiment" to "this is actually real."

The part about AI tooling

I'll be direct about this because it's become part of how I build everything.

The development loop here isn't write-test-commit. It's: I write the architecture, set the constraints, describe what correct looks like - and Claude Code executes against the real system, autonomously, for as long as it takes. The 25-line function limit and hexagonal architecture aren't just code quality preferences, they're what makes autonomous AI execution trustworthy. Bounded functions are auditable. Clean architecture means a wrong decision in one adapter can't silently corrupt everything else. Mandatory interfaces mean failures surface in tests before they reach the container.

portage_go_skill_file.png

I've been saying for a while that a senior developer with the right AI tools and the right architectural discipline can outcompete a team. This project is what that looks like in practice.

Where it's at

86.1% build system coverage. USE flag propagation working correctly. Binary packages generating. xfce4-meta building clean. The explode2lxc loop running.

portage_go_sonar_portage_go.png

The 15-minute target is close.

If you've been a Gentoo user for years and you've had the same nagging thought I had - yeah. I know. I'm building it.