Skip to content

Commit 1909e8f

Browse files
committed
[NO TESTS] WIP
1 parent 9fd1187 commit 1909e8f

File tree

12 files changed

+250
-53
lines changed

12 files changed

+250
-53
lines changed

examples/py_venv/BUILD.bazel

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,29 @@ py_venv(
2020
)
2121

2222
py_venv_binary(
23-
name = "py_venv",
23+
name = "external_venv",
2424
srcs = [
2525
":stamped"
2626
],
27+
# FIXME: Allow use of venv from a separate target?
2728
venv = ":venv",
2829
main = ":stamped",
2930
imports = [
3031
"."
3132
],
3233
)
34+
35+
py_venv_binary(
36+
name = "internal_venv",
37+
srcs = [
38+
":stamped"
39+
],
40+
main = ":stamped",
41+
deps = [
42+
"@pypi_cowsay//:pkg",
43+
],
44+
imports = [
45+
"."
46+
],
47+
48+
)

py/private/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ exports_files(
99
[
1010
"run.tmpl.sh",
1111
"venv.tmpl.sh",
12+
"venv_binary.tmpl.sh",
1213
"pytest.py.tmpl",
1314
],
1415
visibility = ["//visibility:public"],

py/private/py_venv.bzl

Lines changed: 129 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ load("@bazel_skylib//lib:paths.bzl", "paths")
99
load("//py/private:py_semantics.bzl", _py_semantics = "semantics")
1010
load("//py/private/toolchain:types.bzl", "PY_TOOLCHAIN", "VENV_TOOLCHAIN")
1111

12+
VirtualenvInfo = provider(fields = {
13+
"home": "Path of the virtualenv",
14+
})
15+
1216
def _dict_to_exports(env):
1317
return [
1418
"export %s=\"%s\"" % (k, v)
@@ -19,7 +23,13 @@ def _dict_to_exports(env):
1923
# be a layer on top of it if we can figure out flowing the data around. This is
2024
# PoC quality.
2125

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+
2333
venv_toolchain = ctx.toolchains[VENV_TOOLCHAIN]
2434
py_toolchain = _py_semantics.resolve_toolchain(ctx)
2535

@@ -32,12 +42,14 @@ def _py_venv_rule_impl(ctx):
3242
pth_lines.set_param_file_format("multiline")
3343
pth_lines.add_all(imports_depset)
3444

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))
3646
ctx.actions.write(
3747
output = site_packages_pth_file,
3848
content = pth_lines,
3949
)
4050

51+
env_file = ctx.actions.declare_file("{}.env".format(ctx.attr.name))
52+
4153
default_env = {
4254
"BAZEL_TARGET": str(ctx.label).lstrip("@"),
4355
"BAZEL_WORKSPACE": ctx.workspace_name,
@@ -52,6 +64,11 @@ def _py_venv_rule_impl(ctx):
5264
attribute_name = "env",
5365
)
5466

67+
ctx.actions.write(
68+
output = env_file,
69+
content = "\n".join(_dict_to_exports(default_env)).strip(),
70+
)
71+
5572
srcs_depset = _py_library.make_srcs_depset(ctx)
5673

5774
# Use the runfiles computation logic to figure out the files we need to
@@ -68,26 +85,29 @@ def _py_venv_rule_impl(ctx):
6885
],
6986
)
7087

71-
venv_name = ".{}.venv".format(ctx.attr.name)
88+
venv_name = ".{}".format(ctx.attr.name)
7289
venv_dir = ctx.actions.declare_directory(venv_name)
7390

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
7691
ctx.actions.run(
7792
executable = venv_toolchain.bin,
7893
arguments = [
7994
"--location=" + venv_dir.path,
8095
"--python=" + ctx.file._interpreter_shim.path,
8196
"--pth-file=" + site_packages_pth_file.path,
97+
"--env-file=" + env_file.path,
8298
"--bin-dir=" + ctx.bin_dir.path,
8399
"--collision-strategy=" + ctx.attr.package_collisions,
84100
"--venv-name=" + venv_name,
85101
"--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+
),
87106
],
88107
inputs = rfs.merge_all([
89108
ctx.runfiles(files=[
90109
site_packages_pth_file,
110+
env_file,
91111
ctx.file._interpreter_shim,
92112
]),
93113
venv_toolchain.default_info.default_runfiles,
@@ -97,48 +117,108 @@ def _py_venv_rule_impl(ctx):
97117
],
98118
)
99119

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+
100134
# Now we can generate an entrypoint script wrapping $VENV/bin/python
101-
executable_launcher = ctx.outputs.executable
102135
ctx.actions.expand_template(
103136
template = ctx.file._run_tmpl, # FIXME: Should always be single file
104-
output = executable_launcher,
137+
output = ctx.outputs.executable,
105138
substitutions = {
106139
"{{BASH_RLOCATION_FN}}": BASH_RLOCATION_FUNCTION.strip(),
107140
"{{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",
110142
"{{ARG_VENV}}": to_rlocation_path(ctx, venv_dir),
111143
},
112144
is_executable = True,
113145
)
114146

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
119148

120149
return [
121150
DefaultInfo(
122151
files = depset([
123-
executable_launcher,
152+
ctx.outputs.executable,
124153
venv_dir,
125154
]),
126-
executable = executable_launcher,
155+
executable = ctx.outputs.executable,
127156
runfiles = rfs.merge(ctx.runfiles(files=[
128157
venv_dir,
129158
])),
130159
),
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,
137163
),
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,
142222
),
143223
]
144224

@@ -147,11 +227,6 @@ _attrs = dict({
147227
doc = "Environment variables to set when running the binary.",
148228
default = {},
149229
),
150-
"main": attr.label(
151-
doc = "Script to execute with the Python interpreter.",
152-
allow_single_file = True,
153-
mandatory = True,
154-
),
155230
"python_version": attr.string(
156231
doc = """Whether to build this target and its transitive deps for a specific python version.""",
157232
),
@@ -194,10 +269,24 @@ A collision can occur when multiple packages providing the same file are install
194269

195270
_attrs.update(**_py_library.attrs)
196271

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+
),
198286
})
199287

200288
_test_attrs = dict({
289+
# FIXME: Where does this come from, do we need to keep it?
201290
"env_inherit": attr.string_list(
202291
doc = "Specifies additional environment variables to inherit from the external environment when the test is executed by bazel test.",
203292
default = [],
@@ -226,10 +315,10 @@ _python_version_transition = transition(
226315
)
227316

228317
py_venv_base = struct(
229-
implementation = _py_venv_rule_impl,
318+
# implementation = _py_venv_rule_impl,
230319
attrs = _attrs,
320+
binary_attrs = _binary_attrs,
231321
test_attrs = _test_attrs,
232-
venv_attrs = _venv_attrs,
233322
toolchains = [
234323
PY_TOOLCHAIN,
235324
VENV_TOOLCHAIN,
@@ -239,8 +328,8 @@ py_venv_base = struct(
239328

240329
py_venv = rule(
241330
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,
244333
toolchains = py_venv_base.toolchains,
245334
executable = True,
246335
cfg = py_venv_base.cfg,
@@ -249,17 +338,17 @@ py_venv = rule(
249338

250339
py_venv_binary = rule(
251340
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,
254343
toolchains = py_venv_base.toolchains,
255344
executable = True,
256345
cfg = py_venv_base.cfg,
257346
)
258347

259348
py_venv_test = rule(
260349
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,
263352
toolchains = py_venv_base.toolchains,
264353
test = True,
265354
cfg = py_venv_base.cfg,

py/private/venv.tmpl.sh

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@ runfiles_export_envvars
66

77
set -o errexit -o nounset -o pipefail
88

9-
# Psuedo-activate; would be better if we could just source "the" activate scripts
10-
VIRTUAL_ENV="$(rlocation "{{ARG_VENV}}")"
11-
export VIRTUAL_ENV
9+
source "$(rlocation "{{ARG_VENV}}")"/bin/activate
1210

13-
PATH="${VIRTUAL_ENV}/bin:${PATH}"
14-
export PATH
15-
16-
{{PYTHON_ENV}}
17-
18-
# And punt to the virtualenv's "interpreter" which will be a link to the Python toolchain
19-
exec "${VIRTUAL_ENV}/bin/python" {{INTERPRETER_FLAGS}} "$(rlocation {{ENTRYPOINT}})" "$@"
11+
exec "$(rlocation "{{ARG_VENV}}")"/bin/python {{INTERPRETER_FLAGS}} "$@"

py/private/venv_binary.tmpl.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
3+
{{BASH_RLOCATION_FN}}
4+
5+
runfiles_export_envvars
6+
7+
set -o errexit -o nounset -o pipefail
8+
9+
# Psuedo-activate; would be better if we could just source "the" activate scripts
10+
source "$(rlocation "{{ARG_VENV}}")"/bin/activate
11+
12+
# And punt to the virtualenv's "interpreter" which will be a link to the Python toolchain
13+
# FIXME: Assumes that the entrypoint isn't relocated into the site-packages tree
14+
exec "$(rlocation "{{ARG_VENV}}")"/bin/python {{INTERPRETER_FLAGS}} "$(rlocation {{ENTRYPOINT}})" "$@"

py/tests/py-external-venv/BUILD.bazel

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
load("//py/private:py_venv.bzl", "py_venv", "py_venv_test")
2+
3+
py_venv(
4+
name = "venv",
5+
deps = [
6+
"@pypi_cowsay//:pkg",
7+
],
8+
)
9+
10+
py_venv_test(
11+
name = "py-external-venv",
12+
srcs = [
13+
"test.py"
14+
],
15+
venv = ":venv",
16+
main = "test.py",
17+
imports = [
18+
"."
19+
],
20+
)

0 commit comments

Comments
 (0)