|
| 1 | +# Implementation Plan: Canonical Automatic PyPI Proxy Hub |
| 2 | + |
| 3 | +This document defines the locked, production-ready architectural, Starlark API, |
| 4 | +and testing specifications for implementing dynamic PyPI dependency resolution in |
| 5 | +`rules_python` using the `venv` flag. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1. Architectural Strategy: The Canonical `@pypi` Proxy |
| 10 | + |
| 11 | +The `pip` bzlmod extension will automatically synthesize a canonical `@pypi` |
| 12 | +proxy repository rule that orchestrates routing to underlying concrete hubs. |
| 13 | + |
| 14 | +### Bzlmod-Exclusive Scope |
| 15 | + |
| 16 | +The Unified PyPI Hub Proxy is an **exclusive feature of `bzlmod`**. Legacy |
| 17 | +`WORKSPACE` evaluations using independent `pip_parse` repository macros are not |
| 18 | +supported, as bzlmod's module extension architecture provides the required |
| 19 | +centralized coordination to inspect and interlink cross-module hubs. |
| 20 | + |
| 21 | +### Automatic Proxy Construction & Collision Logic |
| 22 | + |
| 23 | +During the evaluation of the `pip` extension across the dependency graph: |
| 24 | +1. **Unconditional Creation**: The extension will **always** synthesize a |
| 25 | + proxy repository rule with the apparent name `pypi`, even if zero |
| 26 | + `pip.parse` concrete hubs are defined in the dependency graph (in which |
| 27 | + case the proxy is completely valid but empty). |
| 28 | +2. **Collision Prevention**: If a user explicitly defines a concrete hub |
| 29 | + named `pypi` (`pip.parse(hub_name = "pypi")`), the automatic proxy |
| 30 | + synthesis is skipped so the user maintains absolute control over that |
| 31 | + repository name. |
| 32 | + |
| 33 | +In `MODULE.bazel`: |
| 34 | +```starlark |
| 35 | +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") |
| 36 | + |
| 37 | +# Concrete hubs defined for different execution contexts |
| 38 | +pip.parse(hub_name = "pypi_a", ...) |
| 39 | +pip.parse(hub_name = "pypi_b", ...) |
| 40 | + |
| 41 | +# Designate 'pypi_b' as the default hub for the unified '@pypi' repository |
| 42 | +pip.default(default_hub = "pypi_b") |
| 43 | + |
| 44 | +# The canonical proxy is automatically created unconditionally: |
| 45 | +use_repo(pip, "pypi") |
| 46 | +``` |
| 47 | + |
| 48 | +### Unified PyPI Hub |
| 49 | + |
| 50 | +The canonical `@pypi` proxy repository matches exactly how concrete hubs create |
| 51 | +their directory structure: a root package for shared configuration settings, and |
| 52 | +a dedicated subdirectory (subpackage) for each PyPI package. |
| 53 | + |
| 54 | +Here is a complete, representative code example of what the generated files in |
| 55 | +`@pypi` will look like when resolving packages between `pypi_a` and `pypi_b`: |
| 56 | + |
| 57 | +#### 1. `@pypi//BUILD.bazel` (Root Package) |
| 58 | +The root package contains the shared `config_setting` targets following the |
| 59 | +`_is_venv_<name>` private naming convention. Leading underscores are strictly |
| 60 | +applied because these configuration settings are an internal implementation |
| 61 | +detail of the proxy repository and are not intended to be a public API. |
| 62 | + |
| 63 | +```starlark |
| 64 | +package(default_visibility = ["//visibility:public"]) |
| 65 | + |
| 66 | +config_setting( |
| 67 | + name = "_is_venv_pypi_a", |
| 68 | + flag_values = { |
| 69 | + "@rules_python//python/config_settings:venv": "pypi_a", |
| 70 | + }, |
| 71 | +) |
| 72 | + |
| 73 | +config_setting( |
| 74 | + name = "_is_venv_pypi_b", |
| 75 | + flag_values = { |
| 76 | + "@rules_python//python/config_settings:venv": "pypi_b", |
| 77 | + }, |
| 78 | +) |
| 79 | +``` |
| 80 | + |
| 81 | +#### 2. `@pypi//foo/BUILD.bazel` (PyPI Package Subpackage) |
| 82 | +Each PyPI package subpackage defines the standard aliases (`pkg`, `whl`, `data`, |
| 83 | +`dist_info`, `extracted_wheel_files`), plus a complete **union of all custom |
| 84 | +`extra_hub_aliases`** defined across all concrete hubs. |
| 85 | + |
| 86 | +Each alias resolves dynamically to the active concrete hub based on the root |
| 87 | +private configuration settings: |
| 88 | + |
| 89 | +```starlark |
| 90 | +package(default_visibility = ["//visibility:public"]) |
| 91 | + |
| 92 | +alias( |
| 93 | + name = "foo", |
| 94 | + actual = ":pkg", |
| 95 | +) |
| 96 | + |
| 97 | +alias( |
| 98 | + name = "pkg", |
| 99 | + actual = select({ |
| 100 | + "//:_is_venv_pypi_a": "@pypi_a//foo:pkg", |
| 101 | + "//:_is_venv_pypi_b": "@pypi_b//foo:pkg", |
| 102 | + # When venv is "auto" (unset), it defaults to the designated fallback |
| 103 | + # (or first defined concrete hub). |
| 104 | + "//conditions:default": "@pypi_b//foo:pkg", |
| 105 | + }), |
| 106 | +) |
| 107 | + |
| 108 | +alias( |
| 109 | + name = "whl", |
| 110 | + actual = select({ |
| 111 | + "//:_is_venv_pypi_a": "@pypi_a//foo:whl", |
| 112 | + "//:_is_venv_pypi_b": "@pypi_b//foo:whl", |
| 113 | + "//conditions:default": "@pypi_b//foo:whl", |
| 114 | + }), |
| 115 | +) |
| 116 | + |
| 117 | +# ... standard aliases for data, dist_info, extracted_wheel_files ... |
| 118 | + |
| 119 | +# 3. Unionized custom extra alias (defined in pypi_a but missing in pypi_b): |
| 120 | +alias( |
| 121 | + name = "my_custom_tool", |
| 122 | + actual = select({ |
| 123 | + "//:_is_venv_pypi_a": "@pypi_a//foo:my_custom_tool", |
| 124 | + # Unrepresented branch routes to execution failure target: |
| 125 | + "//:_is_venv_pypi_b": "//:_missing_package_error_pypi_b_foo", |
| 126 | + "//conditions:default": "@pypi_a//foo:my_custom_tool", |
| 127 | + }), |
| 128 | +) |
| 129 | +``` |
| 130 | + |
| 131 | +### Disjoint Hub Packages & Execution-Phase Failure |
| 132 | + |
| 133 | +If a package exists in one concrete hub but is missing in another (e.g., `scipy` |
| 134 | +is in `pypi_b` but not `pypi_a`), our proxy synthesizes a package subpackage for |
| 135 | +the union of all packages. |
| 136 | + |
| 137 | +To ensure that `bazel cquery` and `bazel query` successfully analyze over the |
| 138 | +entire transitive build graph without failing, unrepresented select branches |
| 139 | +must route to a dedicated **execution-phase error rule**. |
| 140 | + |
| 141 | +```starlark |
| 142 | +# In @pypi//scipy/BUILD.bazel |
| 143 | +alias( |
| 144 | + name = "pkg", |
| 145 | + actual = select({ |
| 146 | + # Routes to execution-phase action failure target: |
| 147 | + "//:_is_venv_pypi_a": "//:_missing_package_error_pypi_a_scipy", |
| 148 | + "//:_is_venv_pypi_b": "@pypi_b//scipy:pkg", |
| 149 | + "//conditions:default": "@pypi_b//scipy:pkg", |
| 150 | + }), |
| 151 | +) |
| 152 | +``` |
| 153 | + |
| 154 | +The synthesized `//:_missing_package_error_XX` rule in `@pypi//BUILD.bazel` |
| 155 | +returns standard Starlark Python providers so analysis/cquery passes, but |
| 156 | +registers a build action that fails when executed: |
| 157 | + |
| 158 | +``` |
| 159 | +Dependency Error: Third-party package 'scipy' is not available when building under PyPI hub 'pypi_a'. |
| 160 | +``` |
| 161 | + |
| 162 | +### Fallback Hub Precedence (`"auto"`) |
| 163 | + |
| 164 | +When a target depends on `@pypi//foo` and the active build setting is `"auto"`, |
| 165 | +the proxy resolves to a concrete hub using the following precedence: |
| 166 | +1. **Designated Fallback**: If the user has explicitly designated a fallback |
| 167 | + concrete hub via `pip.default(default_hub = "...")` in their root |
| 168 | + `MODULE.bazel`, the proxy routes to it. |
| 169 | +2. **First Defined Hub**: If no fallback is explicitly designated via |
| 170 | + `pip.default()`, the proxy **automatically routes to the first defined |
| 171 | + concrete hub** parsed during extension evaluation (e.g., `pypi_a`). |
| 172 | + |
| 173 | +```starlark |
| 174 | +# Explicitly override the "auto" fallback hub |
| 175 | +pip.default( |
| 176 | + default_hub = "pypi_b", |
| 177 | +) |
| 178 | +``` |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +## 2. Core Rule Integration: `config_settings` Transitions |
| 183 | + |
| 184 | +Users will switch active hubs using the standard, highly generic |
| 185 | +`config_settings` transition attribute on executable targets. |
| 186 | + |
| 187 | +### Build Setting Definition |
| 188 | + |
| 189 | +In `python/config_settings/BUILD.bazel`: |
| 190 | + |
| 191 | +```starlark |
| 192 | +string_flag( |
| 193 | + name = "venv", |
| 194 | + build_setting_default = "auto", # Default value is "auto" |
| 195 | + visibility = ["//visibility:public"], |
| 196 | +) |
| 197 | +``` |
| 198 | + |
| 199 | +In `python/private/common_labels.bzl`: |
| 200 | +```starlark |
| 201 | + VENV = str(Label("//python/config_settings:venv")), |
| 202 | +``` |
| 203 | + |
| 204 | +In `python/private/transition_labels.bzl`: |
| 205 | +```starlark |
| 206 | +_BASE_TRANSITION_LABELS = [ |
| 207 | + # ... existing transition labels ... |
| 208 | + labels.VENV, |
| 209 | +] |
| 210 | +``` |
| 211 | + |
| 212 | +Because `py_binary` and `py_test` implement an incoming transition |
| 213 | +(`_transition_executable_impl`) that automatically processes any |
| 214 | +`config_settings` keys matching `TRANSITION_LABELS`, **this provides complete |
| 215 | +transition capabilities with zero changes to our core rule definitions**. |
| 216 | + |
| 217 | +### Usage in BUILD.bazel |
| 218 | + |
| 219 | +Libraries consume packages through the canonical proxy: |
| 220 | + |
| 221 | +```starlark |
| 222 | +py_library( |
| 223 | + name = "common", |
| 224 | + deps = ["@pypi//foo"], # Apparent proxy repository |
| 225 | +) |
| 226 | +``` |
| 227 | + |
| 228 | +Binaries change the active hub by transitioning the build setting: |
| 229 | + |
| 230 | +```starlark |
| 231 | +# Resolves @pypi -> pypi_b (default hub / designated fallback) |
| 232 | +py_binary( |
| 233 | + name = "bin_default", |
| 234 | + deps = [":common"], |
| 235 | +) |
| 236 | + |
| 237 | +# Resolves @pypi -> pypi_a via transition |
| 238 | +py_binary( |
| 239 | + name = "bin_a", |
| 240 | + deps = [":common"], |
| 241 | + config_settings = { |
| 242 | + "//python/config_settings:venv": "pypi_a", |
| 243 | + }, |
| 244 | +) |
| 245 | +``` |
| 246 | + |
| 247 | +### Analysis Cache & Memory Best Practices |
| 248 | + |
| 249 | +Because transitions fork the Bazel configuration, building targets with highly |
| 250 | +diversified `config_settings` across large build graphs will result in |
| 251 | +re-analysis and re-compilation of shared dependencies. |
| 252 | + |
| 253 | +We will include explicit documentation guidelines advising users to keep their |
| 254 | +`venv` transition configurations localized and minimized to preserve Bazel |
| 255 | +caching and memory efficiency. |
| 256 | + |
| 257 | +--- |
| 258 | + |
| 259 | +## 3. Integration Testing Specification |
| 260 | + |
| 261 | +We will construct a comprehensive Bazel-in-Bazel integration test suite in |
| 262 | +`tests/integration/unified_pypi/` to guarantee correctness and verify |
| 263 | +transitions. |
| 264 | + |
| 265 | +The integration test suite will assert: |
| 266 | +1. **`"auto"` Precedence**: Author a test asserting `bazel run //:bin_default` |
| 267 | + correctly inherits `"auto"` and resolves dependencies from the designated fallback. |
| 268 | +2. **Transitional Resolution**: Author a test asserting two binary targets in |
| 269 | + the same package with different `config_settings` successfully resolve |
| 270 | + dependencies and execute against their respective concrete hubs (`pypi_a` |
| 271 | + vs `pypi_b`). |
| 272 | +3. **Command Line Override**: Author a test asserting |
| 273 | + `bazel run --//python/config_settings:venv=pypi_a //:bin_default` |
| 274 | + successfully forces the executable to run using imports resolved from |
| 275 | + `pypi_a`. |
| 276 | +4. **Disjoint Execution Failure**: Author a test asserting `bazel cquery` over |
| 277 | + a target depending on an unrepresented missing package succeeds, while |
| 278 | + `bazel run` on that target gracefully fails during execution with the exact |
| 279 | + synthesized error message. |
| 280 | +5. **Unionized Extra Hub Aliases**: Author a test asserting that a binary |
| 281 | + successfully runs using a custom `extra_hub_aliases` target resolved |
| 282 | + through the `@pypi proxy`. |
| 283 | + |
| 284 | +--- |
| 285 | + |
| 286 | +## 4. Execution Steps |
| 287 | + |
| 288 | +1. **Phase 1**: Define `venv` `string_flag` and register it in |
| 289 | + `common_labels.bzl` and `transition_labels.bzl`. |
| 290 | +2. **Phase 2**: Update `python/private/pypi/extension.bzl` to synthesize the |
| 291 | + canonical `pypi` proxy repository rule. |
| 292 | +3. **Phase 3**: Implement `missing_package_error` execution failure rule and |
| 293 | + the `proxy_hub_repository` generation logic. |
| 294 | +4. **Phase 4**: Author the Bazel-in-Bazel integration test suite in |
| 295 | + `tests/integration/unified_pypi/`. |
| 296 | +5. **Phase 5**: Run all tests and verify full pass before PR submission. |
0 commit comments