1616"""
1717
1818load ("@bazel_skylib//lib:shell.bzl" , "shell" )
19- load ("@bazel_skylib//rules:expand_template .bzl" , "expand_template " )
19+ load ("@pythons_hub//:versions .bzl" , "DEFAULT_PYTHON_VERSION" , "MINOR_MAPPING " )
2020load ("//python:py_binary.bzl" , "py_binary" )
2121load ("//python/private:bzlmod_enabled.bzl" , "BZLMOD_ENABLED" ) # buildifier: disable=bzl-visibility
22+ load ("//python/private:full_version.bzl" , "full_version" )
2223load ("//python/private:toolchain_types.bzl" , "TARGET_TOOLCHAIN_TYPE" ) # buildifier: disable=bzl-visibility
2324load (":toolchain_types.bzl" , "UV_TOOLCHAIN_TYPE" )
2425
2526visibility (["//..." ])
2627
28+ _PYTHON_VERSION_FLAG = "//python/config_settings:python_version"
29+
2730_RunLockInfo = provider (
2831 doc = "" ,
2932 fields = {
@@ -72,7 +75,10 @@ def _args(ctx):
7275
7376def _lock_impl (ctx ):
7477 srcs = ctx .files .srcs
75- output = ctx .actions .declare_file (ctx .label .name + ".out" )
78+ output = ctx .actions .declare_file ("{}.{}.out" .format (
79+ ctx .label .name ,
80+ ctx .attr .python_version .replace ("." , "_" ),
81+ ))
7682
7783 toolchain_info = ctx .toolchains [UV_TOOLCHAIN_TYPE ]
7884 uv = toolchain_info .uv_toolchain_info .uv [DefaultInfo ].files_to_run .executable
@@ -87,10 +93,9 @@ def _lock_impl(ctx):
8793 "--no-python-downloads" ,
8894 "--no-cache" ,
8995 ])
90- args .add ("--custom-compile-command" , "bazel run //{}:{}" .format (
91- ctx .label .package ,
92- ctx .attr .update_target ,
93- ))
96+ pkg = ctx .label .package
97+ update_target = ctx .attr .update_target
98+ args .add ("--custom-compile-command" , "bazel run //{}:{}" .format (pkg , update_target ))
9499 if ctx .attr .generate_hashes :
95100 args .add ("--generate-hashes" )
96101 if not ctx .attr .strip_extras :
@@ -151,6 +156,28 @@ def _lock_impl(ctx):
151156 ),
152157 ]
153158
159+ def _transition_impl (input_settings , attr ):
160+ settings = {
161+ _PYTHON_VERSION_FLAG : input_settings [_PYTHON_VERSION_FLAG ],
162+ }
163+ if attr .python_version :
164+ # FIXME @aignas 2025-03-20: using `full_version` is a workaround for a bug in
165+ # how we order toolchains in bazel. If I set the `python_version` flag
166+ # to `3.12`, I would expect the latest version to be selected, i.e. the
167+ # one that is in MINOR_MAPPING, but it seems that 3.12.0 is selected,
168+ # because of how the targets are ordered.
169+ settings [_PYTHON_VERSION_FLAG ] = full_version (
170+ version = attr .python_version ,
171+ minor_mapping = MINOR_MAPPING ,
172+ )
173+ return settings
174+
175+ _python_version_transition = transition (
176+ implementation = _transition_impl ,
177+ inputs = [_PYTHON_VERSION_FLAG ],
178+ outputs = [_PYTHON_VERSION_FLAG ],
179+ )
180+
154181_lock = rule (
155182 implementation = _lock_impl ,
156183 doc = """\
@@ -182,8 +209,16 @@ modifications and the locking is not done from scratch.
182209 doc = "Public, see the docs in the macro." ,
183210 default = True ,
184211 ),
212+ "output" : attr .string (
213+ doc = "Public, see the docs in the macro." ,
214+ mandatory = True ,
215+ ),
185216 "python_version" : attr .string (
186217 doc = "Public, see the docs in the macro." ,
218+ default = full_version (
219+ version = DEFAULT_PYTHON_VERSION ,
220+ minor_mapping = MINOR_MAPPING ,
221+ ),
187222 ),
188223 "srcs" : attr .label_list (
189224 mandatory = True ,
@@ -200,12 +235,16 @@ modifications and the locking is not done from scratch.
200235 The string to input for the 'uv pip compile'.
201236""" ,
202237 ),
238+ "_allowlist_function_transition" : attr .label (
239+ default = "@bazel_tools//tools/allowlists/function_transition_allowlist" ,
240+ ),
203241 },
204242 toolchains = [
205243 UV_TOOLCHAIN_TYPE ,
206244 # FIXME @aignas 2025-03-17: should this be instead EXEC_TOOLCHAIN_TYPE?
207245 TARGET_TOOLCHAIN_TYPE ,
208246 ],
247+ cfg = _python_version_transition ,
209248)
210249
211250def _lock_run_impl (ctx ):
@@ -293,17 +332,45 @@ def _maybe_file(path):
293332
294333 return None
295334
335+ def _expand_template_impl (ctx ):
336+ pkg = ctx .label .package
337+ update_src = ctx .actions .declare_file (ctx .attr .update_target + ".py" )
338+ ctx .actions .expand_template (
339+ template = ctx .files ._template [0 ],
340+ substitutions = {
341+ "{{dst}}" : "{}/{}" .format (pkg , ctx .attr .output ),
342+ "{{src}}" : "{}" .format (ctx .files .src [0 ].short_path ),
343+ "{{update_target}}" : "//{}:{}" .format (pkg , ctx .attr .update_target ),
344+ },
345+ output = update_src ,
346+ )
347+ return DefaultInfo (files = depset ([update_src ]))
348+
349+ _expand_template = rule (
350+ implementation = _expand_template_impl ,
351+ attrs = {
352+ "output" : attr .string (mandatory = True ),
353+ "src" : attr .label (mandatory = True ),
354+ "update_target" : attr .string (mandatory = True ),
355+ "_template" : attr .label (
356+ default = "//python/uv/private:lock_copier.py" ,
357+ allow_single_file = True ,
358+ ),
359+ },
360+ doc = "Expand the template for the update script allowing us to use `select` statements in the {attr}`output` attribute." ,
361+ )
362+
296363def lock (
297364 * ,
298365 name ,
366+ srcs ,
367+ out ,
299368 args = [],
300369 build_constraints = [],
301370 constraints = [],
302371 env = None ,
303372 generate_hashes = True ,
304- out ,
305373 python_version = None ,
306- srcs ,
307374 strip_extras = False ,
308375 ** kwargs ):
309376 """Pin the requirements based on the src files.
@@ -344,7 +411,6 @@ def lock(
344411 """
345412 update_target = "{}.update" .format (name )
346413 locker_target = "{}.run" .format (name )
347- template_target = "_{}.update_template" .format (name )
348414
349415 # Check if the output file already exists, if yes, first copy it to the
350416 # output file location in order to make `uv` not change the requirements if
@@ -372,6 +438,7 @@ def lock(
372438 strip_extras = strip_extras ,
373439 target_compatible_with = target_compatible_with ,
374440 update_target = update_target ,
441+ output = out ,
375442 tags = [
376443 "local" ,
377444 "manual" ,
@@ -393,28 +460,19 @@ def lock(
393460 tags = ["manual" ],
394461 )
395462
396- # Write a script that can be used for updating the in-tree version of the
397- # requirements file
398- pkg = native .package_name ()
399- expand_template (
400- name = template_target ,
401- out = update_target + ".py" ,
402- template = "//python/uv/private:lock_copier.py" ,
403- substitutions = {
404- "{{dst}}" : "{}/{}" .format (pkg , out ),
405- "{{src}}" : "{}/{}.out" .format (pkg , name ),
406- "{{update_target}}" : "//{}:{}" .format (pkg , update_target ),
407- },
408- tags = ["manual" ],
463+ # FIXME @aignas 2025-03-20: is it possible to extend `py_binary` so that the
464+ # srcs are generated before `py_binary` is run? I found that
465+ # `ctx.files.srcs` usage in the base implementation is making it difficult.
466+ _expand_template (
467+ name = name + "_cp" ,
468+ src = name ,
469+ output = out ,
470+ update_target = update_target ,
409471 )
410472
411- # TODO @aignas 2025-03-17: use a custom `py_binary` rule and
412- # `ctx.actions.expand_template` instead of the bazel skylib rule. This
413- # would make it possible to avoid the extra target.
414473 py_binary (
415474 name = update_target ,
416- srcs = [update_target + ".py" ],
417- data = [name ] + ([] if not maybe_out else [maybe_out ]),
418- python_version = python_version ,
419- ** kwargs
475+ srcs = [name + "_cp" ],
476+ data = [name ] + ([maybe_out ] if maybe_out else []),
477+ tags = ["manual" ],
420478 )
0 commit comments