@@ -21,49 +21,6 @@ load("//python:py_binary.bzl", "py_binary")
2121
2222visibility (["//..." ])
2323
24- def _impl (ctx ):
25- args = ctx .actions .args ()
26-
27- # TODO @aignas 2025-03-02: create an executable file here that is using a
28- # python and uv toolchains.
29-
30- if ctx .files .maybe_out :
31- args .add_all ([
32- "--src-out" ,
33- ctx .files .maybe_out [0 ].path ,
34- ])
35- args .add ("--output-file" , ctx .outputs .out )
36- args .add_all (ctx .files .srcs )
37-
38- ctx .actions .run (
39- executable = ctx .executable .cmd ,
40- mnemonic = "RulesPythonLock" ,
41- inputs = ctx .files .srcs + ctx .files .maybe_out ,
42- outputs = [
43- ctx .outputs .out ,
44- ],
45- arguments = [args ],
46- tools = [ctx .executable .cmd ],
47- progress_message = "Locking requirements using uv" ,
48- env = ctx .attr .env ,
49- )
50-
51- return [DefaultInfo (files = depset ([ctx .outputs .out ]))]
52-
53- _lock = rule (
54- implementation = _impl ,
55- doc = """\
56- """ ,
57- attrs = {
58- "args" : attr .string_list (),
59- "cmd" : attr .label (mandatory = True , executable = True , cfg = "target" ),
60- "env" : attr .string_dict (),
61- "maybe_out" : attr .label (mandatory = False , allow_single_file = True ),
62- "out" : attr .output (mandatory = True ),
63- "srcs" : attr .label_list (mandatory = True , allow_files = True ),
64- },
65- )
66-
6724_uv_toolchain = Label ("//python/uv:uv_toolchain_type" )
6825_py_toolchain = Label ("//python:toolchain_type" )
6926
@@ -75,46 +32,99 @@ _LockInfo = provider(
7532 "srcs" : "" ,
7633 "template" : "" ,
7734 "uv" : "" ,
78- }
35+ },
7936)
8037
8138def _impl2 (ctx ):
8239 args = ctx .attr .args
83- srcs = ctx .attr .srcs
40+ srcs = ctx .files .srcs
41+ existing_output = ctx .files .existing_output
42+ output = ctx .outputs .output
8443
8544 toolchain_info = ctx .toolchains [_uv_toolchain ]
8645 uv = toolchain_info .uv_toolchain_info .uv [DefaultInfo ].files_to_run .executable
8746
88- py_runtime = ctx .toolchains [_py_toolchain ]
89- fail (py_runtime )
90- py = ""
47+ py_runtime = ctx .toolchains [_py_toolchain ].py3_runtime
48+
49+ substitutions = {
50+ " uv " : " {} " .format (uv .path ),
51+ "$bazel_out" : output .path ,
52+ "out=" : "out=" + existing_output [0 ].path ,
53+ }
54+ if existing_output :
55+ substitutions ["# before exec" ] = ""
56+
57+ cmd = ctx .actions .declare_file (ctx .label .name )
58+ ctx .actions .expand_template (
59+ template = ctx .files ._template [0 ],
60+ substitutions = substitutions ,
61+ output = cmd ,
62+ is_executable = True ,
63+ )
64+
65+ run_args = []
66+ lock_args = ctx .actions .args ()
67+ for _args in [
68+ ("--no-python-downloads" ,),
69+ ("--no-cache" ,),
70+ ("--generate-hashes" ,),
71+ ("--no-strip-extras" ,),
72+ (
73+ "--custom-compile-command" ,
74+ # TODO @aignas 2025-03-13:
75+ "'bazel run //{}:{}.update'" .format (ctx .label .package , ctx .attr .update_target ),
76+ ),
77+ ]:
78+ lock_args .add (* _args )
79+ run_args .extend ([a for a in _args ])
80+
81+ lock_args .add_all (args )
82+ lock_args .add_all (srcs )
83+ lock_args .add ("--python" , py_runtime .interpreter )
84+
85+ ctx .actions .run (
86+ executable = cmd ,
87+ mnemonic = "RulesPythonLock" ,
88+ inputs = srcs + ctx .files .existing_output ,
89+ outputs = [output ],
90+ arguments = [lock_args ],
91+ tools = [cmd ],
92+ progress_message = "Locking requirements using uv" ,
93+ env = ctx .attr .env ,
94+ )
9195
9296 return [
93- DefaultInfo (files = depset ([ctx .outputs .out ])),
97+ DefaultInfo (files = depset ([ctx .outputs .output ])),
9498 _LockInfo (
95- args = args ,
99+ args = run_args + args ,
96100 srcs = srcs ,
97101 uv = uv ,
98- py = py ,
99- template = ctx .attr ._template ,
102+ py = py_runtime ,
103+ template = ctx .files ._template [ 0 ] ,
100104 ),
101105 ]
102106
103- _lock2 = rule (
107+ _lock = rule (
104108 implementation = _impl2 ,
105109 doc = """\
106110 """ ,
107111 attrs = {
108112 "args" : attr .string_list (),
109113 "env" : attr .string_dict (),
110114 "existing_output" : attr .label (
111- mandatory = False , allow_single_file = True ,
115+ mandatory = False ,
116+ allow_single_file = True ,
112117 doc = "An already existing output file that is used as a basis for futher modifications and the locking is not done from scratch" ,
113118 ),
114119 "output" : attr .output (mandatory = False ),
115120 "python_version" : attr .string (doc = "TODO: how do I create a transition thing?" ),
116121 "srcs" : attr .label_list (mandatory = True , allow_files = True ),
117- "_template" : attr .label (default = "//python/uv/private:pip_compile_template" ),
122+ "update_target" : attr .string (mandatory = True ),
123+ "_template" : attr .label (
124+ default = "//python/uv/private:pip_compile_template" ,
125+ cfg = "exec" ,
126+ executable = True ,
127+ ),
118128 },
119129 toolchains = [
120130 _uv_toolchain ,
@@ -123,26 +133,60 @@ _lock2 = rule(
123133)
124134
125135def _impl3 (ctx ):
126- fail ()
136+ info = ctx .attr .lock [_LockInfo ]
137+ template = info .template
138+ uv = info .uv
139+ args = info .args
140+ srcs = info .srcs
141+ py_runtime = info .py
142+
143+ args = args + [
144+ src .short_path
145+ for src in srcs
146+ ] + [
147+ "--python" ,
148+ py_runtime .interpreter .short_path ,
149+ ]
150+
151+ substitutions = {
152+ "out=" : "out={}/{}" .format (ctx .label .package , ctx .attr .output ),
153+ "uv " : "{} {} " .format (uv .short_path , " " .join (args )),
154+ }
155+
156+ executable = ctx .actions .declare_file (ctx .label .name )
157+ ctx .actions .expand_template (
158+ template = template ,
159+ substitutions = substitutions ,
160+ output = executable ,
161+ is_executable = True ,
162+ )
163+
164+ runfiles = ctx .runfiles (
165+ transitive_files = depset (
166+ srcs + [uv ],
167+ transitive = [
168+ py_runtime .files ,
169+ ],
170+ ),
171+ )
172+ return [
173+ DefaultInfo (
174+ executable = executable ,
175+ runfiles = runfiles ,
176+ ),
177+ ]
127178
128179_lock_run = rule (
129180 implementation = _impl3 ,
130181 doc = """\
131182 """ ,
132183 attrs = {
133- "lock" : attr .label (
134- doc = "" ,
135- providers = [_LockInfo ],
136- ),
137- "out" : attr .string (),
184+ "lock" : attr .label (providers = [_LockInfo ]),
185+ "output" : attr .string (),
138186 },
139- toolchains = [
140- _uv_toolchain ,
141- _py_toolchain ,
142- ],
187+ executable = True ,
143188)
144189
145-
146190def _maybe_file (path ):
147191 """A small function to return a list of existing outputs.
148192
@@ -183,67 +227,15 @@ def lock(*, name, srcs, out, args = [], **kwargs):
183227 update_target = "{}.update" .format (name )
184228 locker_target = "{}.run" .format (name )
185229
186- # TODO @aignas 2025-03-02: move the following args to a template expansion action
187- user_args = args
188- args = [
189- # FIXME @aignas 2025-03-02: this acts differently in native_binary and the rule
190- "--custom-compile-command='bazel run //{}:{}'" .format (pkg , update_target ),
191- "--generate-hashes" ,
192- "--emit-index-url" ,
193- "--no-strip-extras" ,
194- "--no-python-downloads" ,
195- "--no-cache" ,
196- ]
197- args += user_args
198-
199- run_args = []
200230 maybe_out = _maybe_file (out )
201- if maybe_out :
202- # This means that the output file already exists and it should be used
203- # to create a new file. This will be taken care by the locker tool.
204- #
205- # TODO @aignas 2025-03-02: similarly to sphinx rule, expand the output to short_path
206- run_args += ["--output-file" , "$(rootpath {})" .format (maybe_out )]
207- else :
208- # TODO @aignas 2025-03-02: pass the output as a string
209- run_out = "{}/{}" .format (pkg , out )
210- run_args += ["--output-file" , run_out ]
211-
212- # args just get passed as is
213- run_args += [
214- # TODO @aignas 2025-03-02: get the full source location for these
215- "$(rootpath {})" .format (s )
216- for s in srcs
217- ]
218-
219- expand_template (
220- name = locker_target + "_gen" ,
221- out = locker_target + ".py" ,
222- template = "//python/uv/private:pip_compile.py" ,
223- substitutions = {
224- " args = []" : " args = " + repr (args ),
225- },
226- tags = ["manual" ],
227- )
228-
229- py_binary (
230- name = locker_target ,
231- srcs = [locker_target + ".py" ],
232- data = [
233- "//python/uv:current_toolchain" ,
234- ] + srcs + ([maybe_out ] if maybe_out else []),
235- args = run_args ,
236- tags = ["manual" ],
237- deps = ["//python/runfiles" ],
238- )
239-
240- _lock2 (
231+ _lock (
241232 name = name ,
242233 srcs = srcs ,
243234 # Check if the output file already exists, if yes, first copy it to the
244235 # output file location in order to make `uv` not change the requirements if
245236 # we are just running the command.
246237 existing_output = maybe_out ,
238+ update_target = update_target ,
247239 output = out + ".new" ,
248240 tags = [
249241 "local" ,
@@ -261,6 +253,12 @@ def lock(*, name, srcs, out, args = [], **kwargs):
261253 tags = ["manual" ],
262254 )
263255
256+ _lock_run (
257+ name = locker_target ,
258+ lock = name ,
259+ output = out ,
260+ )
261+
264262 # Write a script that can be used for updating the in-tree version of the
265263 # requirements file
266264 expand_template (
0 commit comments