Featured image of post The repo that downloaded 170 gigabytes

The repo that downloaded 170 gigabytes

An hour to compile and 170GB of artefacts to clean up afterwards. Rust has a built-in answer to that, and I only used half of it.

Everything I own goes through one self-hosted runner. Three concurrent slots, one machine, sat in the corner doing the work for every repository I have.

And for about an hour at a stretch, a single Rust project could have all three of them.

One repo, three hobs, everybody waiting

The Rust toolkit was in a good state. It did what I wanted, the tests passed, and I was fond of it (in the way you’re fond of a car that’s always in the garage). It was also enormous: a round of compilation tests would pull down north of 170 gigabytes of build artefacts, which I then got to clean out by hand, with rm -rf and a sigh, so the machine didn’t fill up, and a full compile could run past the hour.

Size alone I could have lived with, because everybody’s Rust project is slow to build. The problem was the queue behind it. A push to rust-tool-base didn’t just make me wait, it made everything else wait, and the Go modules and infra and the blog and the lot all sat behind one crate graph recompiling itself for the fourth time that morning.

One cache key per lockfile filled my runner’s disk is that same runner seen from the CI side. This is the architectural half of the same complaint.

This is where it parts company with the Go story, because when I broke go-tool-base into modules the argument was about other people’s dependency graphs: import one package, inherit the whole module’s requirements, so splitting it protected consumers. This one wasn’t about consumers at all. This one was me, waiting, watching a disk fill up, at ten to six in the morning.

Same operation, completely different justification. I suspect the second is the one more people feel.

But Rust already solves this

Any Rust developer reading the above has an objection ready, and it’s a good one. Cargo has workspaces: split the monolith into crates inside one repository and you get incremental compilation per crate, a shared lock file, a shared target directory, and a build that only rebuilds what changed. It is the built-in answer to the problem I’ve described, and it doesn’t cost you twelve repositories to look after.

So why on earth go to separate repos?

I did keep one, as it happens

Because I didn’t choose repos over workspaces. I used both, for different jobs, and the line between them is the interesting bit.

rust-tool-base still has a workspace, sat in the Cargo.toml today, holding crates/rtb, crates/rtb-cli-bin and an example. That’s the distributable, the thing that becomes the rtb binary someone installs, and its parts genuinely do move together: one version, one release, one cadence. A workspace is right for that and breaking it up would have been daft.

What went out to separate repositories was the reusable library crates: config, errors, the TUI, credentials, telemetry. The things another project might want without wanting a CLI framework bolted to them, and the reason is the one thing a workspace is least willing to give you.

Independent release cadence.

What the version numbers give away

A workspace nudges you hard toward one version across everything in it, which is lovely when the parts ship together and a straitjacket when they don’t. You end up cutting a release of nine crates because one of them changed, and every downstream consumer sees nine bumps and gets to work out for themselves which one mattered.

Here’s where the eleven library crates sit today:

CrateVersion
assets, config, credentials, error, redact, tui0.6.3
forge0.7.1
chat0.7.2
telemetry0.7.3
app, cli0.9.0

Five different version numbers across eleven crates. tui hasn’t needed anything since 0.6.3 and hasn’t been dragged along for the ride, while app and cli have moved twice as far because that’s where the work has been. That spread is the argument, and a workspace simply can’t express it: each of those crates would be sat at 0.9.0 whether it had changed or not, and the version number would have stopped telling anybody anything at all.

It isn’t repo-per-crate either. The cli repo holds four crates, rtb-cli, rtb-docs, rtb-mcp and rtb-update, all tagged together at v0.9.0, because those genuinely do move as a set. So the rule that fell out is less tidy than “one crate, one repo” and rather more useful: a workspace where things move together, a repository where they don’t.

The bill for all this

Splitting the repo did not make the Rust compiler faster, not by a nanosecond. What it did was make the unit of work smaller, so a change to the error crate compiles the error crate instead of compiling the world, which is a real win and a different one from the win people usually claim.

And it’s bought with a cost I’m paying every week: propagation. A change at the bottom has to be released, picked up, then released again, all the way up. Twelve repositories is twelve pipelines, twelve Renovate configs, twelve sets of branch protection, and a bump that used to be one commit is now a small procession of merge requests. I knew that going in, because I’d already done it to the Go side and had the invoice to prove it.

Still the right call. But “we split the monolith and everything got better” is not how this one goes, and I’d be a tad suspicious of anyone telling you it is.

Where it landed

Twelve repositories, eleven versioned crates, five different version numbers, and one workspace still sat in the middle holding the bit that ships as a single thing.

Three slots on that runner, same as they always were. These days someone else usually gets one.

Built with Hugo · Theme Stack designed by Jimmy