I Wrote 90 Posts About Bazel. Here’s the Map.

2026/03/12

BUILDbazelmetamigrationmonorepo

I apparently process migration trauma through blog posts. Over the past three months I wrote about ninety of them — deep dives, field notes, dead-end postmortems, stubs I started at 2am and forgot about. The series sprawled the way monorepo dependencies sprawl: organically, with hidden cycles, and with at least four things that turned out to be the same thing wearing different hats.

This post is the map. If you’ve been following along, it tells you what to skip and what connects to what. If you’re arriving fresh, it gives you a reading order that won’t make you feel like you walked into season 4 of a show with no recap.


The Five Story Arcs

The ninety posts aren’t ninety topics. They’re five stories told in parallel, with shared characters.

Arc 1: The Torch Migration Saga

The longest arc. Our monorepo had torch listed under gazelle:resolve ignore — which meant 62% of packages couldn’t migrate to Bazel at all. Getting torch into Bazel’s hermetic sandbox was a four-month odyssey through three architecture options, each with its own blog-post-generating failure modes.

The thread:

  1. 62% of Our Packages Are Blocked by torch — The problem statement. Why torch is special and why ignoring it wasn’t an option.
  2. How Bazel Resolves Python Deps (and Why torch Breaks Everything) — The technical root cause: Bazel’s hermetic resolution model vs. torch’s 800MB platform-specific wheels.
  3. What Does Success Actually Look Like? — The decision framework. Four architecture options, five levels of “done.”
  4. Three Ways to Smuggle torch into Bazel — Overview of Options A, B, and C before committing to any.
  5. Option A: The Simplest Option That Wasn’t (published) — Seven phases, a false victory, a CPU index rabbit hole. The full implementation story.
  6. Option B and the Five Obstacles — The uv.override_requirement approach and why transitive deps killed it.
  7. Making Bazel See System Torch Inside Docker — Option C: the one that actually shipped. System Python toolchain in Docker.
  8. Why Bazel Torch Tests Need Two Python Interpreters (published) — The “wait, why are there two pythons” moment that comes after Option C lands.
  9. The py_torch_test Pattern: A Complete Technical Guide — Reference doc for the final pattern. How to write torch-dependent BUILD targets.
  10. Test Classification: The Real Torch CI Problem — The follow-up problem: now that torch tests can run, how do you sort them into the right CI job?
  11. Migrating mai_config: The First Torch Package — First blood. The package that proved the pattern works.
  12. 6 Packages, 10 Bumps, 9 Pushes — Batch-migrating the first wave of torch packages. The messy reality of doing it at scale.
  13. Migrating Torch Packages to Bazel: The Complete Decision Tree — The final playbook. Every branch, every edge case.

Recommended reading: Start at #3 (decision framework), then #5 or #7 (whichever option interests you), then #9 (the final pattern). Skip the rest unless you’re debugging a similar migration.

Arc 2: Native Code and the CC Toolchain

The second-hardest problem. Python packages that wrap C++, Cython, or CUDA code can’t just be pip installed inside Bazel’s sandbox — they need a compiler, linker flags, and system libraries that Bazel’s hermetic model deliberately excludes. This arc traces the path from “why won’t this wheel build” to a working hermetic C++ toolchain.

The thread:

  1. Everything Below import torch — The 38KB explainer. Maps the full native build stack from a Python dev’s perspective. Start here if you’ve never thought about what pip install actually does for a C extension.
  2. Why Bazel Can’t Compile Your Python C Extensions — The problem: sdist wheels need a CC toolchain and Bazel doesn’t provide one by default.
  3. Stocking the Kitchen: A Hermetic C++ Toolchain — Setting up hermetic_cc_toolchain for Python sdist compilation.
  4. I Was Wrong About use_default_shell_env — The sequel. What happens when your hermetic toolchain assumption meets a real-world sdist that shells out to pkg-config.
  5. Teaching Bazel to See Non-Python Dependencies — Generalizing beyond CC: system libraries, CUDA, protobuf.
  6. Native Code in a Python Monorepo: The Bazel Migration’s Hardest Chapter — The synthesis piece. Ties the whole CC arc together.
  7. Cracking the Biggest Bazel Blocker: lib/bus — The boss fight. 4 Cython files pretending to be a C++ library, blocking 28 packages.
  8. The Biggest Bazel Blocker Was a Cython Package — The shorter, punchier version of the lib/bus story.
  9. Migrating a CUDA Package to Bazel Without Bazel Knowing About CUDA — The endgame: precompiled .so files and Bazel’s cc_import.

Recommended reading: #1 for background, #6 for the narrative, #7 for the war story.

Arc 3: The Dependency Graph as Product

The pivot point. Somewhere around month two, the migration stopped being about “get BUILD files passing” and started being about “the dep graph itself is the deliverable.” This arc is about what the graph unlocks once you have it.

The thread:

  1. Your Monorepo Is a Graph — The conceptual foundation. What the dependency graph looks like and why it matters.
  2. Your Monorepo Has Two Dependency Graphs (and They Disagree) — The discovery that find_changed and Bazel see different graphs. Both are correct.
  3. The Dep Graph Is the Product (published) — The strategy shift. Using manual tags to get 100% graph coverage without 100% migration.
  4. What a Full Dep Graph Actually Unlocks (published) — The payoff: scoped pyright, GPU test targeting, drift detection, test selection.
  5. Dependency Graph Cascade Analysis — The analytical method. How we modeled wave-by-wave unblocking.
  6. I Built a Tool to Prove Bazel Was Better. The Data Said It’s Complicated. — The honest reckoning: find_changed vs Bazel head-to-head comparison.
  7. I Made Pyright Faster by Removing Environments — Applied dep graph: scoped type checking.
  8. GPU Test Targeting with the Bazel Dep Graph — Applied dep graph: only running GPU tests that need GPUs.
  9. Your BUILD Files Are Lying to You — The dark side: when the graph’s data is wrong.

Recommended reading: #3 then #4. If you want the full arc, #1 → #3 → #4 → #6.

Arc 4: Migration Mechanics

The nuts-and-bolts arc. How BUILD files get generated, how Gazelle works, how to validate a migration, what breaks and how to fix it. The least narratively satisfying arc but the most practically useful one.

The thread:

  1. When Bazel Enters the Chat (published) — The intro: what changes when you add Bazel to a Python monorepo.
  2. The Full Chain: How Python BUILD Files Get Generated — The 27KB deep dive into the Gazelle → rules_python → BUILD pipeline.
  3. Gazelle: Reading the Source Code So You Don’t Have To — When Gazelle fails and you need to debug it.
  4. Gazelle companion posts: Components & Lifecycle, Dependency Management, Invisible Machinery, Manifest Under the Hood
  5. From pip.parse to uv.lock — Migrating Bazel’s Python dependency backend.
  6. rules_python vs rules_py: They’re Layers, Not Competitors — Clearing up the most common confusion in the Bazel Python ecosystem.
  7. Validating a Bazel Migration: The Delete-and-Regenerate Roundtrip — The litmus test: delete BUILD files, regenerate, diff.
  8. Circular Deps in Bazel: py_binary as Firewall — When your Python package structure creates import cycles that Bazel can’t tolerate.
  9. When Gazelle Can’t Help — The packages where auto-generation fails and you’re hand-writing BUILD files.
  10. ‘Ready’ Doesn’t Mean Ready — The four levels of migration readiness. A package passing bazel build is not a package passing bazel test.
  11. Bazel Doesn’t Care What Version of numpy You Have — Hermetic resolution: what it means in practice.
  12. Bazel Marker Files: How Bazel Knows When to Re-Download — Deep internals: the marker file mechanism.

Recommended reading: #1, #6, #10, then whatever matches your specific problem.

Arc 5: Testing in Bazel

Tests break differently in Bazel. This arc covers the taxonomy of failures, the GPU-specific problems, and the hierarchy of test infrastructure from pytest to CI.

The thread:

  1. Your Bazel Tests Are Passing. None of Them Are Running. (published) — The scariest failure mode: 0 tests run looks like success.
  2. Four Ways Bazel Breaks Your Python Tests (published) — The taxonomy of sandbox-induced test failures.
  3. When Your Macro Hides Tests From an Entire Platform — A specific variant: target_compatible_with silently skipping tests.
  4. target_compatible_with vs select() — The reference: when to use which.
  5. Taming Bulky bazel test Output (published) — Practical: making test output readable.
  6. GPU Tests in Bazel: The Resource Problem — How to model GPU as a Bazel resource constraint.
  7. When GPU Showed Up in Our Python Bazel Monorepo — The narrative version of the GPU testing problem.
  8. GPU Test Sharding: Weighted Distribution Across H100 Nodes — Advanced: balanced test distribution across GPU runners.
  9. How a Monorepo Does Integration Tests in Bazel — When tests need external services.
  10. The Test Hierarchy Map — The big picture: pytest, Bazel, CI, and how they layer.
  11. Graduating Test Selection from find_changed to Bazel — The migration of test selection itself.

Recommended reading: #1 and #2 first (everyone hits these). Then #6 if you have GPUs, #10 if you want the full picture.


The Supporting Cast

Posts that don’t fit neatly into one arc but provide essential context.

Bazel Fundamentals

Post What It Covers
A Deep Dive into Bazel Ecosystem High-level overview of Bazel concepts (published, 2024)
Bazel: Cache Mechanisms Demystified Local + remote cache, how invalidation works (published)
Bazel CI Cache: Why GitHub Actions Cache Won’t Save You CI-specific caching: the gap between local and remote
Bazel: Starlark and Skyframe The evaluation model under the hood
Bazel’s Analysis Phase and In-Memory Cache How Bazel decides what to rebuild (migrated)
query vs cquery When to use which query command
The Bazel Query Commands I Actually Use Practical query recipes
Bazel: Its Parallel Is Imparallel How Bazel parallelizes execution
Go Compiler and Linker Flags in Bazel The three-layer model (if you touch Go targets)

Docker + Bazel Intersection

Post What It Covers
Why Bazel Won’t Speed Up Your Docker Build The myth vs. reality of Bazel + Docker (published)
Docker Build vs Bazel rules_oci Same image, two philosophies
5 Bazel Image Build Patterns Patterns found tracing through a monorepo
Getting ccache Into Docker Builds TIL post — build acceleration trick (published)

Migration Strategy

Post What It Covers
When Homegrown Tools Hit the Ceiling The moment that triggered the Bazel commitment
Studying a Migration Playbook Reverse-engineering a teammate’s migration methodology
Reading Migration Status Like a Map How find_bazel_ready_packages.py works and what it reveals
From PR to Test Runner How CI decides what to test in a monorepo
Presenting PR #22703 to the Team A team presentation write-up — meta, but useful for migration communication

Conference Notes

Post What It Covers
BazelCon 2023: Field Notes from Munich Community talks, ecosystem state (migrated)
BazelCon 2024 Field Notes Updates, what shipped (migrated)
ibazel — Bazel’s File Watcher Live reload tooling (migrated)
Batch-Migrating Seven Go Services Earlier migration, different language, same patterns (migrated)

The Cleanup Report

Here’s the honest audit. Some posts should be merged, some should be deleted, and some are doing the same job at different altitudes.

Delete (Stubs with no salvageable content)

These are empty outlines, single-bullet placeholders, or YouTube links masquerading as posts. Nothing will be lost.

File Title Reason
bazel-and-bazelisk.md Bazel and Bazelisk 189B stub — just bullet points
bazel-benchmarking-ci-artifact.md Benchmarking on CI 244B empty outline
bazel-migration-go-ci-alignment.md Migration Alignment with Go CI 238B HTML comment only
bazel-cluster-disk-cache-pvc.md Cluster Disk Cache and PVC 212B HTML comment only
bazel-flags-categorization.md Flags and Categorization 315B — just a YouTube link
bazel-plugin-clouddev-debug.md Plugin with CloudDev Debug 225B HTML comment only
bazel-terminal-output-and-logs.md Terminal Output and Logs 400B — same YouTube link as flags
bazel-tips-and-quick-actions.md Tips and Quick Actions 236B empty
bazel-remote-execution-dynamic-systems.md Remote Execution 459B outline only

Count: 9 posts. Combined size: ~2.5KB. No content to salvage.

Merge (Overlapping content that should become one post)

Cluster Keep Merge Into It Why
lib/bus blocker cracking-the-biggest-bazel-blocker-lib-bus.md (20KB) the-biggest-bazel-blocker-was-a-cython-package.md (7.7KB) Same story; the shorter one is a condensed version. Fold any unique content into the longer piece.
CC Toolchain overview what-cc-stands-for-in-bazel.md (17.4KB) bazel-cc-toolchain-sdist-wheels.md (16.2KB) The “what CC stands for” post is the synthesis; the sdist-wheels post covers the same ground at a lower level. Absorb the unique technical detail.
Torch migration playbook migrating-torch-packages-to-bazel.md (26.4KB) bazel-migration-torch-packages.md (19.7KB) The “decision tree” post is the canonical reference; the “6 packages” post is the narrative of doing it. They overlap ~60%. Keep the narrative bits from the shorter one as a section in the longer one.
Dep graph concept bazel-dep-graph-is-the-product.md (18.8KB) monorepo-dep-graph.md (11.3KB) “Your Monorepo Is a Graph” sets up the concept; “The Dep Graph Is the Product” delivers the payoff. Merge the setup into the payoff, or make “Your Monorepo Is a Graph” a clear Part 1 with explicit link.
GPU tests gpu-tests-in-bazel-the-resource-problem.md (16.5KB) gpu-meets-bazel-monorepo.md (9.6KB) “When GPU showed up” tells the story; “The Resource Problem” is the technical deep dive. Merge the story into the deep dive as an intro.
Caching bazel-ci-cache-strategies.md (10.3KB) bazel-cloud-disk-cache.md (787B) The cloud disk cache stub has no real content; any concept there belongs in the CI cache post.
Query bazel-query-survival-guide.md (2.9KB) bazel-query-vs-cquery.md (2.1KB) Both are small. Combine into one “Bazel Query: The Commands I Actually Use” post.

Deprecate (Superseded by later posts)

File Title Superseded By Action
bazel-cc-toolchain-sdist-wheels.md Why Bazel Can’t Compile Your C Extensions what-cc-stands-for-in-bazel.md + bazel-cc-toolchain-for-python-sdists.md After merge, add deprecation note pointing to the canonical posts
three-ways-to-smuggle-torch-into-bazel.md Three Ways to Smuggle torch torch-in-bazel-what-does-success-look-like.md The “success” post covers all three options more completely. Add a redirect note.
bazel-migration-gpu-blockers.md 62% of Packages Are Blocked bazel-migration-cascade-analysis.md + Option C landing The blocker count is now wrong (we unblocked 30 packages). Either update with current numbers or deprecate in favor of the cascade analysis.
a-deep-dive-into-bazel.md A Deep Dive into Bazel Ecosystem when-bazel-enters-the-chat.md The 2024 overview is superseded by the 2026 intro post. Keep as historical artifact with a note.

Keep As-Is (Strong standalone posts)

These are the heavy hitters. Unique content, clear scope, no significant overlap.

Post Why It’s Good
Option A: The Simplest Option That Wasn’t (25.6KB) Complete implementation saga with a narrative arc. The longest and most detailed torch post. Published.
Four Ways Bazel Breaks Your Python Tests (8.7KB) Clean taxonomy, immediately useful. Published. High search value.
Your Bazel Tests Are Passing. None Are Running. (3.5KB) Short, scary, unforgettable. Published.
The Dep Graph Is the Product (18.8KB) The strategic thesis of the whole migration. Published.
What a Full Dep Graph Actually Unlocks (14.5KB) The concrete payoff. Published.
Why Bazel Torch Tests Need Two Pythons (10.2KB) Unique topic, no overlap. Published.
The Full Chain: How BUILD Files Get Generated (26.6KB) The definitive Gazelle → BUILD pipeline reference.
Everything Below import torch (37.9KB) The most ambitious post. Maps the entire native build stack.
I Was Wrong About use_default_shell_env (16.8KB) Great narrative — the “I was wrong” framing is honest and instructive.
rules_python vs rules_py (9.8KB) Answers the #1 FAQ in Bazel Python land.
Migrating a CUDA Package Without Bazel Knowing About CUDA (20.7KB) Unique problem, unique solution, great title.
From pip.parse to uv.lock (9.7KB) Practical migration guide for a common step.
The Test Hierarchy Map (17KB) The big-picture testing post.
I Built a Tool to Prove Bazel Was Better (11.8KB) Honest data analysis. Rare.

Reading Paths

Path 1: “I’ve Never Used Bazel”

You’re a Python dev. Someone said “we’re migrating to Bazel.” You typed bazel into Google and got scared.

  1. When Bazel Enters the Chat — What Bazel is and why it exists
  2. A Deep Dive into Bazel Ecosystem — Concepts and vocabulary (older but still useful for fundamentals)
  3. rules_python vs rules_py — The two Python rulesets and how they layer
  4. Bazel Doesn’t Care What Version of numpy You Have — What hermetic resolution means in practice
  5. Four Ways Bazel Breaks Your Python Tests — What will break and why
  6. Bazel: Cache Mechanisms Demystified — The thing that makes Bazel worth the pain

Path 2: “I’m Migrating a Python Monorepo”

You’ve decided to migrate. You need the how.

  1. When Homegrown Tools Hit the Ceiling — Why you’re doing this
  2. ‘Ready’ Doesn’t Mean Ready — The four levels of migration readiness
  3. The Full Chain: How BUILD Files Get Generated — End-to-end: Gazelle → BUILD → test
  4. Validating a Bazel Migration: The Delete-and-Regenerate Roundtrip — Your verification method
  5. Circular Deps in Bazel: py_binary as Firewall — You’ll hit this. Here’s the fix.
  6. When Gazelle Can’t Help — The packages where automation fails
  7. Batch-Migrating Seven Go Services — Same patterns, different language (migrated, but the methodology transfers)

Path 3: “torch Won’t Cooperate”

You have GPU-dependent packages and Bazel doesn’t know what to do with them.

  1. 62% of Our Packages Are Blocked by torch — The scope of the problem
  2. What Does Success Actually Look Like? — Your option space
  3. Making Bazel See System Torch Inside Docker — The solution that shipped
  4. The py_torch_test Pattern — The implementation reference
  5. Why Bazel Torch Tests Need Two Pythons — The “ah, that’s why” post
  6. Migrating Torch Packages to Bazel: The Complete Decision Tree — Your playbook

Path 4: “I Have C++/CUDA/Cython in My Python Repo”

The native dependency problem. Possibly the hardest part of any Python Bazel migration.

  1. Everything Below import torch — The conceptual map (long, but worth it)
  2. Why Bazel Can’t Compile Your Python C Extensions — The problem statement
  3. Stocking the Kitchen: A Hermetic C++ Toolchain — The solution
  4. I Was Wrong About use_default_shell_env — When the solution breaks
  5. Cracking the Biggest Bazel Blocker: lib/bus — The war story
  6. Migrating a CUDA Package Without Bazel Knowing About CUDA — The CUDA-specific endgame

Path 5: “I Want the Dep Graph to Do Work for Me”

You’ve migrated enough packages. Now you want the graph to pay dividends.

  1. The Dep Graph Is the Product — The thesis
  2. What a Full Dep Graph Actually Unlocks — The catalog of applications
  3. I Made Pyright Faster by Removing Environments — Applied example: type checking
  4. GPU Test Targeting with the Bazel Dep Graph — Applied example: CI targeting
  5. Graduating Test Selection from find_changed to Bazel — Replacing the old system
  6. Your BUILD Files Are Lying to You — The thing that can go wrong

By the Numbers

Because I am apparently the kind of person who needs to see the stats.

Category Count Combined Size
Published 12 ~125KB
Substantial drafts (>5KB) 45 ~630KB
Small drafts (1.5-5KB) 11 ~27KB
Stubs (<1.5KB) 9 ~2.5KB
Migrated 5 ~60KB
Adjacent (Gazelle, Docker, etc.) ~10 ~80KB
Total ~92 ~925KB

After the proposed cleanup:

Action Count Net Change
Delete stubs 9 -9 posts
Merge pairs 7 clusters -7 posts (absorbed into keeper)
Deprecate 4 0 (stay as redirects)
Post-cleanup total ~76 Still a lot. I know.

The Narrative, if You Read Nothing Else

Here’s the three-sentence version of the whole series:

We had a 100-package Python monorepo with a homegrown CI tool that couldn’t tell us what our changes affected. We migrated to Bazel, which required solving torch (62% of packages blocked), native code (C++/Cython/CUDA in a hermetic sandbox), and test classification (GPU vs CPU, real vs phantom). The dep graph that emerged from the migration turned out to be more valuable than the build system itself.

That’s the through-line. Every post is a chapter in that story, even the ones that felt like tangents at the time.


This map will age. I’ll keep writing posts — apparently I can’t stop — and the clusters will shift. But if you’re standing at the entrance to this series wondering where to start, pick a reading path and go. The map isn’t the territory, but it beats wandering.