Skip to content

Commit 5f3a0d8

Browse files
committed
vibe coded impl
1 parent 7050b3d commit 5f3a0d8

File tree

8 files changed

+270
-7
lines changed

8 files changed

+270
-7
lines changed

plan.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Plan for adding uv.lock support to pip.parse
2+
3+
## Part 1: Analyze and Plan (Done)
4+
- Analyzed `python/extensions/pip.bzl`, `python/private/pypi/hub_builder.bzl`, `python/private/pypi/uv_lock.bzl`.
5+
- Confirmed `uv.lock` structure contains wheel URLs and resolution markers.
6+
- Identified need to modify `pip_repository_attrs.bzl` and `hub_builder.bzl`.
7+
8+
## Part 2: Basic Implementation
9+
1. **Modify `python/private/pypi/pip_repository_attrs.bzl`**:
10+
- Add `uv_lock` attribute (label, allow_single_file=True).
11+
- Add `_toml2json` attribute (label, default pointing to a tool). Note: Need to verify if `_toml2json` is already available or needs to be added. The `uv_lock.bzl` helper uses `attr._toml2json`, so it must be present on the calling rule/tag.
12+
13+
2. **Modify `python/private/pypi/hub_builder.bzl`**:
14+
- In `_pip_parse`:
15+
- Check if `pip_attr.uv_lock` is set.
16+
- If set, call `convert_uv_lock_to_json`.
17+
- Parse the JSON result.
18+
- Iterate over packages in the JSON.
19+
- Group packages by name.
20+
- For this step, select the highest version for each package name.
21+
- Select the first wheel URL for that version.
22+
- Create `whl_library` repositories for these wheels.
23+
- Ensure these `whl_library` calls are integrated into `self._whl_libraries`.
24+
25+
3. **Verify**:
26+
- Run `bazel run //tests/uv_pypi:bin`.
27+
28+
## Part 3: Advanced Implementation (Multiple Versions)
29+
1. **Handle Resolution Markers**:
30+
- Parse `resolution-markers` from `uv.lock` packages.
31+
- Instead of picking one version, keep all versions that have distinct resolution markers.
32+
33+
2. **Use `wheel_tags_settings`**:
34+
- In `hub_builder.bzl`, when constructing the hub repository content (via `hub_repository`), we need to pass information about these multiple versions.
35+
- The `hub_repository` rule (or the macros creating it) needs to generate `define_wheel_tag_settings` in the `BUILD.bazel` of the hub.
36+
- Generate `alias` targets using `select()` based on the defined settings.
37+
38+
3. **Refactor Hub Generation**:
39+
- Update `hub_repository.bzl` (or the template it uses) to support this new "multi-version via select" pattern, if it doesn't already. The prompt suggests modifying the "hub build file for a package".
40+
41+
4. **Verify**:
42+
- Run the test again. It should correctly pick `absl-py` 2.3.1 or 2.4.0 based on the environment/platform.

python/private/pypi/extension.bzl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
272272
exposed_packages = {}
273273
extra_aliases = {}
274274
whl_libraries = {}
275+
uv_selectors = {}
275276
for hub in pip_hub_map.values():
276277
out = hub.build()
277278

@@ -285,6 +286,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
285286
extra_aliases[hub.name] = out.extra_aliases
286287
hub_group_map[hub.name] = out.group_map
287288
hub_whl_map[hub.name] = out.whl_map
289+
uv_selectors[hub.name] = out.uv_selectors
288290

289291
return struct(
290292
config = config,
@@ -294,6 +296,7 @@ You cannot use both the additive_build_content and additive_build_content_file a
294296
hub_whl_map = hub_whl_map,
295297
whl_libraries = whl_libraries,
296298
whl_mods = whl_mods,
299+
uv_selectors = uv_selectors,
297300
platform_config_settings = {
298301
hub_name: {
299302
platform_name: sorted([str(Label(cv)) for cv in p.config_settings])
@@ -386,6 +389,7 @@ def _pip_impl(module_ctx):
386389
key: whl_config_settings_to_json(values)
387390
for key, values in whl_map.items()
388391
},
392+
uv_selectors = mods.uv_selectors.get(hub_name, {}),
389393
packages = mods.exposed_packages.get(hub_name, []),
390394
platform_config_settings = mods.platform_config_settings.get(hub_name, {}),
391395
groups = mods.hub_group_map.get(hub_name),

python/private/pypi/hub_builder.bzl

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ load(":evaluate_markers.bzl", "evaluate_markers_py", evaluate_markers_star = "ev
1111
load(":parse_requirements.bzl", "parse_requirements")
1212
load(":pep508_env.bzl", "env")
1313
load(":pep508_evaluate.bzl", "evaluate")
14+
load(":pypi_repo_utils.bzl", "pypi_repo_utils")
1415
load(":python_tag.bzl", "python_tag")
1516
load(":requirements_files_by_platform.bzl", "requirements_files_by_platform")
17+
load(":uv_lock.bzl", "convert_uv_lock_to_json")
1618
load(":whl_config_setting.bzl", "whl_config_setting")
1719
load(":whl_repo_name.bzl", "pypi_repo_name", "whl_repo_name")
1820

@@ -65,6 +67,7 @@ def hub_builder(
6567
_group_map = {}, # modified by _add_group_map
6668
_whl_libraries = {}, # modified by _add_whl_library
6769
_whl_map = {}, # modified by _add_whl_library
70+
_uv_selectors = {}, # modified by _process_uv_lock
6871
# internal
6972
_platforms = {},
7073
_group_name_by_whl = {},
@@ -111,6 +114,7 @@ def _build(self):
111114
},
112115
exposed_packages = sorted(self._exposed_packages),
113116
whl_libraries = self._whl_libraries,
117+
uv_selectors = self._uv_selectors,
114118
)
115119

116120
def _pip_parse(self, module_ctx, pip_attr):
@@ -157,6 +161,11 @@ def _pip_parse(self, module_ctx, pip_attr):
157161
# to `{os}_{arch}`.
158162
target_platforms = pip_attr.target_platforms or ([] if default_cross_setup else ["{os}_{arch}"]),
159163
)
164+
165+
if pip_attr.uv_lock:
166+
_process_uv_lock(self, module_ctx, pip_attr)
167+
return
168+
160169
_add_group_map(self, pip_attr.experimental_requirement_cycles)
161170
_add_extra_aliases(self, pip_attr.extra_hub_aliases)
162171
_create_whl_repos(
@@ -167,6 +176,97 @@ def _pip_parse(self, module_ctx, pip_attr):
167176
enable_pipstar_extract = self._config.enable_pipstar_extract or self._get_index_urls.get(pip_attr.python_version),
168177
)
169178

179+
def _process_uv_lock(self, module_ctx, pip_attr):
180+
interpreter = _detect_interpreter(self, pip_attr)
181+
182+
real_interpreter = pypi_repo_utils.resolve_python_interpreter(
183+
module_ctx,
184+
python_interpreter = interpreter.path,
185+
python_interpreter_target = interpreter.target,
186+
)
187+
188+
json_str = convert_uv_lock_to_json(module_ctx, pip_attr, self._logger, python_interpreter = real_interpreter)
189+
lock_data = json.decode(json_str)
190+
191+
packages = lock_data.get("package", [])
192+
packages_by_name = {}
193+
for pkg in packages:
194+
name = normalize_name(pkg["name"])
195+
packages_by_name.setdefault(name, []).append(pkg)
196+
197+
# Common args logic
198+
common_args = _common_args(
199+
self,
200+
module_ctx,
201+
pip_attr = pip_attr,
202+
enable_pipstar = False, # TODO: Support pipstar
203+
)
204+
205+
for name, pkgs in packages_by_name.items():
206+
package_settings = []
207+
for pkg in pkgs:
208+
version = pkg["version"]
209+
wheels = pkg.get("wheels", [])
210+
if not wheels:
211+
continue
212+
213+
# Use first wheel
214+
wheel = wheels[0]
215+
wheel_url = wheel["url"]
216+
wheel_hash = wheel.get("hash")
217+
218+
# Create repo
219+
src = struct(
220+
requirement_line = "{}=={}".format(name, version),
221+
filename = wheel_url.split("/")[-1],
222+
url = wheel_url,
223+
sha256 = wheel_hash.replace("sha256:", "") if wheel_hash else None,
224+
distribution = name,
225+
target_platforms = [],
226+
extra_pip_args = [],
227+
is_multiple_versions = False,
228+
)
229+
230+
whl_library_args = dict(common_args)
231+
# Add extra args if needed
232+
whl_library_args.update(dict(
233+
dep_template = "@{}//{{name}}:{{target}}".format(self.name),
234+
))
235+
236+
repo = _whl_repo(
237+
src = src,
238+
whl_library_args = whl_library_args,
239+
download_only = pip_attr.download_only,
240+
netrc = self._config.netrc or pip_attr.netrc,
241+
auth_patterns = self._config.auth_patterns or pip_attr.auth_patterns,
242+
python_version = _major_minor_version(pip_attr.python_version),
243+
is_multiple_versions = False,
244+
use_downloader = True,
245+
interpreter = interpreter,
246+
enable_pipstar = False,
247+
enable_pipstar_extract = False,
248+
)
249+
250+
repo_name = repo.repo_name
251+
if repo_name not in self._whl_libraries:
252+
self._whl_libraries[repo_name] = repo.args
253+
254+
markers = pkg.get("resolution-markers", [])
255+
if not markers:
256+
package_settings.append((repo_name, None))
257+
else:
258+
for m in markers:
259+
package_settings.append((repo_name, m))
260+
261+
if package_settings:
262+
self._uv_selectors[name] = json.encode(package_settings)
263+
264+
# We also need to add exposed packages for the hub
265+
_add_exposed_packages(self, {
266+
name: None
267+
for name in packages_by_name.keys()
268+
})
269+
170270
### end of PUBLIC methods
171271
### setters for build outputs
172272

python/private/pypi/hub_repository.bzl

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,68 @@ def _impl(rctx):
4545
# `requirement`, et al. macros.
4646
macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name)
4747

48+
if rctx.attr.uv_selectors:
49+
for pkg_name, selectors_json in rctx.attr.uv_selectors.items():
50+
settings = json.decode(selectors_json)
51+
# settings is list of [repo, marker]
52+
53+
select_dict = {}
54+
for i, entry in enumerate(settings):
55+
repo = entry[0]
56+
marker = entry[1]
57+
if marker:
58+
select_dict[":pick_{}".format(i)] = "@" + repo
59+
else:
60+
select_dict["//conditions:default"] = "@" + repo
61+
62+
# We create aliases for pkg, whl, data, dist_info
63+
# repo points to the whl_library which has these targets.
64+
# So actual should be repo + "//:pkg" etc.
65+
66+
def make_select(suffix):
67+
d = {}
68+
for k, v in select_dict.items():
69+
d[k] = v + suffix
70+
return render.dict(d)
71+
72+
content = """
73+
load("@rules_python//python/private/pypi:uv_lock_targets.bzl", "define_wheel_tag_settings")
74+
75+
package(default_visibility = ["//visibility:public"])
76+
77+
define_wheel_tag_settings({settings})
78+
79+
alias(
80+
name = "{pkg_name}",
81+
actual = ":pkg",
82+
)
83+
alias(
84+
name = "pkg",
85+
actual = select({select_pkg}),
86+
)
87+
alias(
88+
name = "whl",
89+
actual = select({select_whl}),
90+
)
91+
alias(
92+
name = "data",
93+
actual = select({select_data}),
94+
)
95+
alias(
96+
name = "dist_info",
97+
actual = select({select_dist_info}),
98+
)
99+
""".format(
100+
pkg_name = pkg_name,
101+
settings = render.list(settings),
102+
select_pkg = make_select("//:pkg"),
103+
select_whl = make_select("//:whl"),
104+
select_data = make_select("//:data"),
105+
select_dist_info = make_select("//:dist_info"),
106+
)
107+
rctx.file("{}/BUILD.bazel".format(pkg_name), content)
108+
109+
48110
rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
49111
rctx.template(
50112
"config.bzl",
@@ -99,6 +161,10 @@ The wheel map where values are json.encoded strings of the whl_map constructed
99161
in the pip.parse tag class.
100162
""",
101163
),
164+
"uv_selectors": attr.string_dict(
165+
mandatory = False,
166+
doc = "Map of package name to JSON list of (repo, marker) for uv.lock support",
167+
),
102168
"_config_template": attr.label(
103169
default = ":config.bzl.tmpl",
104170
),

python/private/pypi/pip_repository_attrs.bzl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,20 @@ True will become default in a subsequent release.
7070
),
7171
}
7272

73+
UV_ATTRS = {
74+
"uv_lock": attr.label(
75+
allow_single_file = True,
76+
doc = """\
77+
The uv.lock file to use for resolving dependencies.
78+
""",
79+
),
80+
"_toml2json": attr.label(
81+
default = Label("//tools/toml2json:toml2json.py"),
82+
allow_single_file = True,
83+
cfg = "exec",
84+
executable = True,
85+
),
86+
}
87+
88+
ATTRS.update(**UV_ATTRS)
7389
ATTRS.update(**COMMON_ATTRS)

python/private/pypi/uv_lock.bzl

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
load("//python/private/pypi:pypi_repo_utils.bzl", "pypi_repo_utils")
22

3-
def convert_uv_lock_to_json(mrctx, attr, logger):
3+
def convert_uv_lock_to_json(mrctx, attr, logger, python_interpreter = None):
44
"""Converts a uv.lock file to json.
55
66
Args:
@@ -9,16 +9,25 @@ def convert_uv_lock_to_json(mrctx, attr, logger):
99
`_python_interpreter_target` attribute of the interpreter
1010
to use.
1111
logger: a logger object to use
12+
python_interpreter: (optional) The resolved python interpreter object.
1213
1314
Returns:
1415
The command output, which is a json string.
1516
"""
16-
python_interpreter = pypi_repo_utils.resolve_python_interpreter(
17-
mrctx,
18-
python_interpreter_target = attr._python_interpreter_target,
19-
)
17+
if not python_interpreter:
18+
python_interpreter_target = getattr(attr, "_python_interpreter_target", None)
19+
if not python_interpreter_target:
20+
python_interpreter_target = getattr(attr, "python_interpreter_target", None)
21+
22+
python_interpreter = pypi_repo_utils.resolve_python_interpreter(
23+
mrctx,
24+
python_interpreter_target = python_interpreter_target,
25+
)
2026
toml2json = mrctx.path(attr._toml2json)
21-
src_path = mrctx.path(attr.srcs[0])
27+
if hasattr(attr, "uv_lock") and attr.uv_lock:
28+
src_path = mrctx.path(attr.uv_lock)
29+
else:
30+
src_path = mrctx.path(attr.srcs[0])
2231

2332
stdout = pypi_repo_utils.execute_checked_stdout(
2433
mrctx,

python/private/pypi/uv_lock_targets.bzl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,29 @@ def define_targets(name, selectors):
4141
name = name,
4242
actual = select(select_map),
4343
)
44+
45+
def define_wheel_tag_settings(settings):
46+
"""Defines the wheel tag settings and config settings.
47+
48+
Args:
49+
settings: list of (repo_name, marker_expression).
50+
"""
51+
for i, (repo, marker) in enumerate(settings):
52+
# name for the marker rule
53+
marker_name = "marker_{}".format(i)
54+
# name for the config setting (used in select keys)
55+
config_name = "pick_{}".format(i)
56+
57+
if marker:
58+
env_marker_setting(
59+
name = marker_name,
60+
expression = marker,
61+
)
62+
native.config_setting(
63+
name = config_name,
64+
flag_values = { ":" + marker_name : "TRUE" }
65+
)
66+
else:
67+
# If no marker, we can't create a config setting that matches "everything" easily
68+
# without a flag. But maybe we don't need to if we use //conditions:default in the select.
69+
pass

python/private/pypi/wheel_tags_setting.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def _wheel_tags_setting_impl(ctx):
7373
if platform_tag == "any":
7474
platform_is_compatible = True
7575
else:
76-
libc = "musl" if musl in platform_tag else "glibc"
76+
libc = "musl" if "musl" in platform_tag else "glibc"
7777
if "linux" in platform_tag:
7878
os = "linux"
7979
else:

0 commit comments

Comments
 (0)