@@ -9,6 +9,10 @@ load("@bazel_skylib//lib:paths.bzl", "paths")
9
9
load ("//py/private:py_semantics.bzl" , _py_semantics = "semantics" )
10
10
load ("//py/private/toolchain:types.bzl" , "PY_TOOLCHAIN" , "VENV_TOOLCHAIN" )
11
11
12
+ VirtualenvInfo = provider (fields = {
13
+ "home" : "Path of the virtualenv" ,
14
+ })
15
+
12
16
def _dict_to_exports (env ):
13
17
return [
14
18
"export %s=\" %s\" " % (k , v )
@@ -19,7 +23,13 @@ def _dict_to_exports(env):
19
23
# be a layer on top of it if we can figure out flowing the data around. This is
20
24
# PoC quality.
21
25
22
- def _py_venv_rule_impl (ctx ):
26
+ def _py_venv_base_impl (ctx ):
27
+
28
+ """
29
+ Common base implementation of taking a PyInfo transitive depset and shoving all that into a "virtualenv" tree.
30
+ Depended on by the implementation of venv building and venv-based binary building.
31
+ """
32
+
23
33
venv_toolchain = ctx .toolchains [VENV_TOOLCHAIN ]
24
34
py_toolchain = _py_semantics .resolve_toolchain (ctx )
25
35
@@ -32,12 +42,14 @@ def _py_venv_rule_impl(ctx):
32
42
pth_lines .set_param_file_format ("multiline" )
33
43
pth_lines .add_all (imports_depset )
34
44
35
- site_packages_pth_file = ctx .actions .declare_file ("{}.venv. pth" .format (ctx .attr .name ))
45
+ site_packages_pth_file = ctx .actions .declare_file ("{}.pth" .format (ctx .attr .name ))
36
46
ctx .actions .write (
37
47
output = site_packages_pth_file ,
38
48
content = pth_lines ,
39
49
)
40
50
51
+ env_file = ctx .actions .declare_file ("{}.env" .format (ctx .attr .name ))
52
+
41
53
default_env = {
42
54
"BAZEL_TARGET" : str (ctx .label ).lstrip ("@" ),
43
55
"BAZEL_WORKSPACE" : ctx .workspace_name ,
@@ -52,6 +64,11 @@ def _py_venv_rule_impl(ctx):
52
64
attribute_name = "env" ,
53
65
)
54
66
67
+ ctx .actions .write (
68
+ output = env_file ,
69
+ content = "\n " .join (_dict_to_exports (default_env )).strip (),
70
+ )
71
+
55
72
srcs_depset = _py_library .make_srcs_depset (ctx )
56
73
57
74
# Use the runfiles computation logic to figure out the files we need to
@@ -68,26 +85,29 @@ def _py_venv_rule_impl(ctx):
68
85
],
69
86
)
70
87
71
- venv_name = ".{}.venv " .format (ctx .attr .name )
88
+ venv_name = ".{}" .format (ctx .attr .name )
72
89
venv_dir = ctx .actions .declare_directory (venv_name )
73
90
74
- # FIXME: Copy in a Python interpreter and symlink it as `./bin/python`
75
- # FIXME: Note that the UV internals we're invoking here do interpreter path canonicalization we don't want
76
91
ctx .actions .run (
77
92
executable = venv_toolchain .bin ,
78
93
arguments = [
79
94
"--location=" + venv_dir .path ,
80
95
"--python=" + ctx .file ._interpreter_shim .path ,
81
96
"--pth-file=" + site_packages_pth_file .path ,
97
+ "--env-file=" + env_file .path ,
82
98
"--bin-dir=" + ctx .bin_dir .path ,
83
99
"--collision-strategy=" + ctx .attr .package_collisions ,
84
100
"--venv-name=" + venv_name ,
85
101
"--mode=static-symlink" ,
86
- "--version={}.{}.{}" .format (py_toolchain .interpreter_version_info .major , py_toolchain .interpreter_version_info .minor , py_toolchain .interpreter_version_info .micro ,), # FIXME: Micro not actually used.
102
+ "--version={}.{}" .format (
103
+ py_toolchain .interpreter_version_info .major ,
104
+ py_toolchain .interpreter_version_info .minor ,
105
+ ),
87
106
],
88
107
inputs = rfs .merge_all ([
89
108
ctx .runfiles (files = [
90
109
site_packages_pth_file ,
110
+ env_file ,
91
111
ctx .file ._interpreter_shim ,
92
112
]),
93
113
venv_toolchain .default_info .default_runfiles ,
@@ -97,48 +117,108 @@ def _py_venv_rule_impl(ctx):
97
117
],
98
118
)
99
119
120
+ return venv_dir , rfs .merge_all ([
121
+ ctx .runfiles (files = [
122
+ venv_dir ,
123
+ ])
124
+ ])
125
+
126
+ def _py_venv_rule_impl (ctx ):
127
+ """
128
+ A virtualenv implementation the binary of which is a proxy to the Python interpreter of the venv.
129
+ """
130
+
131
+ py_toolchain = _py_semantics .resolve_toolchain (ctx )
132
+ venv_dir , rfs = _py_venv_base_impl (ctx )
133
+
100
134
# Now we can generate an entrypoint script wrapping $VENV/bin/python
101
- executable_launcher = ctx .outputs .executable
102
135
ctx .actions .expand_template (
103
136
template = ctx .file ._run_tmpl , # FIXME: Should always be single file
104
- output = executable_launcher ,
137
+ output = ctx . outputs . executable ,
105
138
substitutions = {
106
139
"{{BASH_RLOCATION_FN}}" : BASH_RLOCATION_FUNCTION .strip (),
107
140
"{{INTERPRETER_FLAGS}}" : " " .join (py_toolchain .flags + ctx .attr .interpreter_options ),
108
- "{{ENTRYPOINT}}" : to_rlocation_path (ctx , ctx .file .main ),
109
- "{{PYTHON_ENV}}" : "\n " .join (_dict_to_exports (default_env )).strip (),
141
+ "{{ENTRYPOINT}}" : "${VIRTUAL_ENV}/bin/python" ,
110
142
"{{ARG_VENV}}" : to_rlocation_path (ctx , venv_dir ),
111
143
},
112
144
is_executable = True ,
113
145
)
114
146
115
- instrumented_files_info = _py_library .make_instrumented_files_info (
116
- ctx ,
117
- extra_source_attributes = ["main" ],
118
- )
147
+ # TODO: Zip output group to allow for bypassing filtering et. all
119
148
120
149
return [
121
150
DefaultInfo (
122
151
files = depset ([
123
- executable_launcher ,
152
+ ctx . outputs . executable ,
124
153
venv_dir ,
125
154
]),
126
- executable = executable_launcher ,
155
+ executable = ctx . outputs . executable ,
127
156
runfiles = rfs .merge (ctx .runfiles (files = [
128
157
venv_dir ,
129
158
])),
130
159
),
131
- PyInfo (
132
- imports = imports_depset ,
133
- transitive_sources = srcs_depset ,
134
- has_py2_only_sources = False ,
135
- has_py3_only_sources = True ,
136
- uses_shared_libraries = False ,
160
+ # FIXME: Does not provide PyInfo because venvs are supposed to be terminal artifacts.
161
+ VirtualenvInfo (
162
+ home = venv_dir ,
137
163
),
138
- instrumented_files_info ,
139
- RunEnvironmentInfo (
140
- environment = passed_env ,
141
- inherited_environment = getattr (ctx .attr , "env_inherit" , []),
164
+ ]
165
+
166
+ def _py_venv_binary_impl (ctx ):
167
+ """
168
+ A virtualenv implementation the binary of which is a proxy to the Python interpreter of the venv.
169
+ """
170
+
171
+ py_toolchain = _py_semantics .resolve_toolchain (ctx )
172
+
173
+ # Make runfiles to handle direct srcs and deps which we need to bolt on top
174
+ # of the venv
175
+ srcs_depset = _py_library .make_srcs_depset (ctx )
176
+ virtual_resolution = _py_library .resolve_virtuals (ctx )
177
+
178
+ # Use the runfiles computation logic to figure out the files we need to
179
+ # _build_ the venv. The final venv is these runfiles _plus_ the venv's
180
+ # structures.
181
+ rfs = _py_library .make_merged_runfiles (
182
+ ctx ,
183
+ extra_depsets = [
184
+ py_toolchain .files ,
185
+ srcs_depset ,
186
+ ] + virtual_resolution .srcs + virtual_resolution .runfiles ,
187
+ extra_runfiles_depsets = [
188
+ ctx .attr ._runfiles_lib [DefaultInfo ].default_runfiles ,
189
+ ],
190
+ )
191
+
192
+ if not ctx .attr .venv :
193
+ venv_dir , venv_rfs = _py_venv_base_impl (ctx )
194
+
195
+ else :
196
+ venv_dir = ctx .attr .venv [VirtualenvInfo ].home
197
+ venv_rfs = ctx .attr .venv [DefaultInfo ].default_runfiles
198
+
199
+ rfs = rfs .merge (venv_rfs )
200
+
201
+ # Now we can generate an entrypoint script wrapping $VENV/bin/python
202
+ ctx .actions .expand_template (
203
+ template = ctx .file ._bin_tmpl , # FIXME: Should always be single file
204
+ output = ctx .outputs .executable ,
205
+ substitutions = {
206
+ "{{BASH_RLOCATION_FN}}" : BASH_RLOCATION_FUNCTION .strip (),
207
+ "{{INTERPRETER_FLAGS}}" : " " .join (py_toolchain .flags + ctx .attr .interpreter_options ),
208
+ "{{ENTRYPOINT}}" : to_rlocation_path (ctx , ctx .file .main ),
209
+ "{{ARG_VENV}}" : to_rlocation_path (ctx , venv_dir ),
210
+ "{{RUNFILES_INTERPRETER}}" : str (py_toolchain .runfiles_interpreter ).lower (),
211
+ },
212
+ is_executable = True ,
213
+ )
214
+
215
+ return [
216
+ DefaultInfo (
217
+ files = depset ([
218
+ ctx .outputs .executable ,
219
+ ]),
220
+ executable = ctx .outputs .executable ,
221
+ runfiles = rfs ,
142
222
),
143
223
]
144
224
@@ -147,11 +227,6 @@ _attrs = dict({
147
227
doc = "Environment variables to set when running the binary." ,
148
228
default = {},
149
229
),
150
- "main" : attr .label (
151
- doc = "Script to execute with the Python interpreter." ,
152
- allow_single_file = True ,
153
- mandatory = True ,
154
- ),
155
230
"python_version" : attr .string (
156
231
doc = """Whether to build this target and its transitive deps for a specific python version.""" ,
157
232
),
@@ -194,10 +269,24 @@ A collision can occur when multiple packages providing the same file are install
194
269
195
270
_attrs .update (** _py_library .attrs )
196
271
197
- _venv_attrs = dict ({
272
+ _binary_attrs = dict ({
273
+ "main" : attr .label (
274
+ doc = "Script to execute with the Python interpreter." ,
275
+ allow_single_file = True ,
276
+ mandatory = True ,
277
+ ),
278
+ "venv" : attr .label (
279
+ doc = "A virtualenv; if provided all 3rdparty deps are assumed to come via the venv." ,
280
+ providers = [[VirtualenvInfo ]],
281
+ ),
282
+ "_bin_tmpl" : attr .label (
283
+ allow_single_file = True ,
284
+ default = "//py/private:venv_binary.tmpl.sh" ,
285
+ ),
198
286
})
199
287
200
288
_test_attrs = dict ({
289
+ # FIXME: Where does this come from, do we need to keep it?
201
290
"env_inherit" : attr .string_list (
202
291
doc = "Specifies additional environment variables to inherit from the external environment when the test is executed by bazel test." ,
203
292
default = [],
@@ -226,10 +315,10 @@ _python_version_transition = transition(
226
315
)
227
316
228
317
py_venv_base = struct (
229
- implementation = _py_venv_rule_impl ,
318
+ # implementation = _py_venv_rule_impl,
230
319
attrs = _attrs ,
320
+ binary_attrs = _binary_attrs ,
231
321
test_attrs = _test_attrs ,
232
- venv_attrs = _venv_attrs ,
233
322
toolchains = [
234
323
PY_TOOLCHAIN ,
235
324
VENV_TOOLCHAIN ,
@@ -239,8 +328,8 @@ py_venv_base = struct(
239
328
240
329
py_venv = rule (
241
330
doc = "Build a Python pseudo-virtual environment under Bazel which will execute a shell or console." ,
242
- implementation = py_venv_base . implementation ,
243
- attrs = py_venv_base .attrs | py_venv_base . venv_attrs ,
331
+ implementation = _py_venv_rule_impl ,
332
+ attrs = py_venv_base .attrs ,
244
333
toolchains = py_venv_base .toolchains ,
245
334
executable = True ,
246
335
cfg = py_venv_base .cfg ,
@@ -249,17 +338,17 @@ py_venv = rule(
249
338
250
339
py_venv_binary = rule (
251
340
doc = "Run a Python program under Bazel using a pseudo-virtualenv. Most users should use the [py_binary macro](#py_binary) instead of loading this directly." ,
252
- implementation = py_venv_base . implementation ,
253
- attrs = py_venv_base .attrs ,
341
+ implementation = _py_venv_binary_impl ,
342
+ attrs = py_venv_base .attrs | py_venv_base . binary_attrs ,
254
343
toolchains = py_venv_base .toolchains ,
255
344
executable = True ,
256
345
cfg = py_venv_base .cfg ,
257
346
)
258
347
259
348
py_venv_test = rule (
260
349
doc = "Run a Python program under Bazel using a pseudo-virtualenv. Most users should use the [py_test macro](#py_test) instead of loading this directly." ,
261
- implementation = py_venv_base . implementation ,
262
- attrs = py_venv_base .attrs | py_venv_base .test_attrs ,
350
+ implementation = _py_venv_binary_impl ,
351
+ attrs = py_venv_base .attrs | py_venv_base .binary_attrs | py_venv_base . test_attrs ,
263
352
toolchains = py_venv_base .toolchains ,
264
353
test = True ,
265
354
cfg = py_venv_base .cfg ,
0 commit comments