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:
- 62% of Our Packages Are Blocked by torch — The problem statement. Why torch is special and why ignoring it wasn’t an option.
- 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.
- What Does Success Actually Look Like? — The decision framework. Four architecture options, five levels of “done.”
- Three Ways to Smuggle torch into Bazel — Overview of Options A, B, and C before committing to any.
- Option A: The Simplest Option That Wasn’t (published) — Seven phases, a false victory, a CPU index rabbit hole. The full implementation story.
- Option B and the Five Obstacles — The
uv.override_requirementapproach and why transitive deps killed it. - Making Bazel See System Torch Inside Docker — Option C: the one that actually shipped. System Python toolchain in Docker.
- Why Bazel Torch Tests Need Two Python Interpreters (published) — The “wait, why are there two pythons” moment that comes after Option C lands.
- The py_torch_test Pattern: A Complete Technical Guide — Reference doc for the final pattern. How to write torch-dependent BUILD targets.
- 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?
- Migrating mai_config: The First Torch Package — First blood. The package that proved the pattern works.
- 6 Packages, 10 Bumps, 9 Pushes — Batch-migrating the first wave of torch packages. The messy reality of doing it at scale.
- 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:
- 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 whatpip installactually does for a C extension. - 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.
- Stocking the Kitchen: A Hermetic C++ Toolchain — Setting up
hermetic_cc_toolchainfor Python sdist compilation. - 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 topkg-config. - Teaching Bazel to See Non-Python Dependencies — Generalizing beyond CC: system libraries, CUDA, protobuf.
- Native Code in a Python Monorepo: The Bazel Migration’s Hardest Chapter — The synthesis piece. Ties the whole CC arc together.
- Cracking the Biggest Bazel Blocker: lib/bus — The boss fight. 4 Cython files pretending to be a C++ library, blocking 28 packages.
- The Biggest Bazel Blocker Was a Cython Package — The shorter, punchier version of the lib/bus story.
- Migrating a CUDA Package to Bazel Without Bazel Knowing About CUDA — The endgame: precompiled
.sofiles and Bazel’scc_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:
- Your Monorepo Is a Graph — The conceptual foundation. What the dependency graph looks like and why it matters.
- Your Monorepo Has Two Dependency Graphs (and They Disagree) — The discovery that
find_changedand Bazel see different graphs. Both are correct. - The Dep Graph Is the Product (published) — The strategy shift. Using
manualtags to get 100% graph coverage without 100% migration. - What a Full Dep Graph Actually Unlocks (published) — The payoff: scoped pyright, GPU test targeting, drift detection, test selection.
- Dependency Graph Cascade Analysis — The analytical method. How we modeled wave-by-wave unblocking.
- 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.
- I Made Pyright Faster by Removing Environments — Applied dep graph: scoped type checking.
- GPU Test Targeting with the Bazel Dep Graph — Applied dep graph: only running GPU tests that need GPUs.
- 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:
- When Bazel Enters the Chat (published) — The intro: what changes when you add Bazel to a Python monorepo.
- The Full Chain: How Python BUILD Files Get Generated — The 27KB deep dive into the Gazelle → rules_python → BUILD pipeline.
- Gazelle: Reading the Source Code So You Don’t Have To — When Gazelle fails and you need to debug it.
- Gazelle companion posts: Components & Lifecycle, Dependency Management, Invisible Machinery, Manifest Under the Hood
- From pip.parse to uv.lock — Migrating Bazel’s Python dependency backend.
- rules_python vs rules_py: They’re Layers, Not Competitors — Clearing up the most common confusion in the Bazel Python ecosystem.
- Validating a Bazel Migration: The Delete-and-Regenerate Roundtrip — The litmus test: delete BUILD files, regenerate, diff.
- Circular Deps in Bazel: py_binary as Firewall — When your Python package structure creates import cycles that Bazel can’t tolerate.
- When Gazelle Can’t Help — The packages where auto-generation fails and you’re hand-writing BUILD files.
- ‘Ready’ Doesn’t Mean Ready — The four levels of migration readiness. A package passing
bazel buildis not a package passingbazel test. - Bazel Doesn’t Care What Version of numpy You Have — Hermetic resolution: what it means in practice.
- 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:
- Your Bazel Tests Are Passing. None of Them Are Running. (published) — The scariest failure mode:
0 tests runlooks like success. - Four Ways Bazel Breaks Your Python Tests (published) — The taxonomy of sandbox-induced test failures.
- When Your Macro Hides Tests From an Entire Platform — A specific variant:
target_compatible_withsilently skipping tests. - target_compatible_with vs select() — The reference: when to use which.
- Taming Bulky bazel test Output (published) — Practical: making test output readable.
- GPU Tests in Bazel: The Resource Problem — How to model GPU as a Bazel resource constraint.
- When GPU Showed Up in Our Python Bazel Monorepo — The narrative version of the GPU testing problem.
- GPU Test Sharding: Weighted Distribution Across H100 Nodes — Advanced: balanced test distribution across GPU runners.
- How a Monorepo Does Integration Tests in Bazel — When tests need external services.
- The Test Hierarchy Map — The big picture: pytest, Bazel, CI, and how they layer.
- 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.
- When Bazel Enters the Chat — What Bazel is and why it exists
- A Deep Dive into Bazel Ecosystem — Concepts and vocabulary (older but still useful for fundamentals)
- rules_python vs rules_py — The two Python rulesets and how they layer
- Bazel Doesn’t Care What Version of numpy You Have — What hermetic resolution means in practice
- Four Ways Bazel Breaks Your Python Tests — What will break and why
- 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.
- When Homegrown Tools Hit the Ceiling — Why you’re doing this
- ‘Ready’ Doesn’t Mean Ready — The four levels of migration readiness
- The Full Chain: How BUILD Files Get Generated — End-to-end: Gazelle → BUILD → test
- Validating a Bazel Migration: The Delete-and-Regenerate Roundtrip — Your verification method
- Circular Deps in Bazel: py_binary as Firewall — You’ll hit this. Here’s the fix.
- When Gazelle Can’t Help — The packages where automation fails
- 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.
- 62% of Our Packages Are Blocked by torch — The scope of the problem
- What Does Success Actually Look Like? — Your option space
- Making Bazel See System Torch Inside Docker — The solution that shipped
- The py_torch_test Pattern — The implementation reference
- Why Bazel Torch Tests Need Two Pythons — The “ah, that’s why” post
- 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.
- Everything Below
import torch— The conceptual map (long, but worth it) - Why Bazel Can’t Compile Your Python C Extensions — The problem statement
- Stocking the Kitchen: A Hermetic C++ Toolchain — The solution
- I Was Wrong About
use_default_shell_env— When the solution breaks - Cracking the Biggest Bazel Blocker: lib/bus — The war story
- 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.
- The Dep Graph Is the Product — The thesis
- What a Full Dep Graph Actually Unlocks — The catalog of applications
- I Made Pyright Faster by Removing Environments — Applied example: type checking
- GPU Test Targeting with the Bazel Dep Graph — Applied example: CI targeting
- Graduating Test Selection from find_changed to Bazel — Replacing the old system
- 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.