Skip to content

Commit 535be2f

Browse files
committed
fold per-target python version into base rule
1 parent 2136215 commit 535be2f

File tree

4 files changed

+63
-271
lines changed

4 files changed

+63
-271
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ Unreleased changes template.
5858
tested by CI, so functionality cannot be guaranteed.
5959
* ({bzl:obj}`pip.parse`) Only query SimpleAPI for packages that have
6060
sha values in the `requirements.txt` file.
61+
* (rules) The version-aware rules have been folded into the base rules and
62+
the version-aware rules are now simply aliases for the base rules. The
63+
`python_version` attribute is still used to specify the Python version.
64+
65+
{#v0-0-0-deprecations}
66+
#### Deprecations
67+
* `//python/config_settings:transitions.bzl` and its `py_binary` and `py_test`
68+
wrappers are deprecated. Use the regular rules instead.
6169

6270
{#v0-0-0-fixed}
6371
### Fixed

python/config_settings/transition.bzl

Lines changed: 7 additions & 258 deletions
Original file line numberDiff line numberDiff line change
@@ -14,266 +14,15 @@
1414

1515
"""The transition module contains the rule definitions to wrap py_binary and py_test and transition
1616
them to the desired target platform.
17+
18+
:::{versionchanged} VERSION_NEXT_PATCH
19+
The `py_binary` and `py_test` symbols are aliases to the regular rules. Usages
20+
of them should be changed to load the regular rules directly.
21+
:::
1722
"""
1823

19-
load("@bazel_skylib//lib:dicts.bzl", "dicts")
2024
load("//python:py_binary.bzl", _py_binary = "py_binary")
21-
load("//python:py_info.bzl", "PyInfo")
22-
load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
2325
load("//python:py_test.bzl", _py_test = "py_test")
24-
load("//python/config_settings/private:py_args.bzl", "py_args")
25-
load("//python/private:reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
26-
27-
def _transition_python_version_impl(_, attr):
28-
return {"//python/config_settings:python_version": str(attr.python_version)}
29-
30-
_transition_python_version = transition(
31-
implementation = _transition_python_version_impl,
32-
inputs = [],
33-
outputs = ["//python/config_settings:python_version"],
34-
)
35-
36-
def _transition_py_impl(ctx):
37-
target = ctx.attr.target
38-
windows_constraint = ctx.attr._windows_constraint[platform_common.ConstraintValueInfo]
39-
target_is_windows = ctx.target_platform_has_constraint(windows_constraint)
40-
executable = ctx.actions.declare_file(ctx.attr.name + (".exe" if target_is_windows else ""))
41-
ctx.actions.symlink(
42-
is_executable = True,
43-
output = executable,
44-
target_file = target[DefaultInfo].files_to_run.executable,
45-
)
46-
default_outputs = []
47-
if target_is_windows:
48-
# NOTE: Bazel 6 + host=linux + target=windows results in the .exe extension missing
49-
inner_bootstrap_path = _strip_suffix(target[DefaultInfo].files_to_run.executable.short_path, ".exe")
50-
inner_bootstrap = None
51-
inner_zip_file_path = inner_bootstrap_path + ".zip"
52-
inner_zip_file = None
53-
for file in target[DefaultInfo].files.to_list():
54-
if file.short_path == inner_bootstrap_path:
55-
inner_bootstrap = file
56-
elif file.short_path == inner_zip_file_path:
57-
inner_zip_file = file
58-
59-
# TODO: Use `fragments.py.build_python_zip` once Bazel 6 support is dropped.
60-
# Which file the Windows .exe looks for depends on the --build_python_zip file.
61-
# Bazel 7+ has APIs to know the effective value of that flag, but not Bazel 6.
62-
# To work around this, we treat the existence of a .zip in the default outputs
63-
# to mean --build_python_zip=true.
64-
if inner_zip_file:
65-
suffix = ".zip"
66-
underlying_launched_file = inner_zip_file
67-
else:
68-
suffix = ""
69-
underlying_launched_file = inner_bootstrap
70-
71-
if underlying_launched_file:
72-
launched_file_symlink = ctx.actions.declare_file(ctx.attr.name + suffix)
73-
ctx.actions.symlink(
74-
is_executable = True,
75-
output = launched_file_symlink,
76-
target_file = underlying_launched_file,
77-
)
78-
default_outputs.append(launched_file_symlink)
79-
80-
env = {}
81-
for k, v in ctx.attr.env.items():
82-
env[k] = ctx.expand_location(v)
83-
84-
providers = [
85-
DefaultInfo(
86-
executable = executable,
87-
files = depset(default_outputs, transitive = [target[DefaultInfo].files]),
88-
runfiles = ctx.runfiles(default_outputs).merge(target[DefaultInfo].default_runfiles),
89-
),
90-
# Ensure that the binary we're wrapping is included in code coverage.
91-
coverage_common.instrumented_files_info(
92-
ctx,
93-
dependency_attributes = ["target"],
94-
),
95-
target[OutputGroupInfo],
96-
# TODO(f0rmiga): testing.TestEnvironment is deprecated in favour of RunEnvironmentInfo but
97-
# RunEnvironmentInfo is not exposed in Bazel < 5.3.
98-
# https://github.com/bazelbuild/rules_python/issues/901
99-
# https://github.com/bazelbuild/bazel/commit/dbdfa07e92f99497be9c14265611ad2920161483
100-
testing.TestEnvironment(env),
101-
]
102-
if PyInfo in target:
103-
providers.append(target[PyInfo])
104-
if BuiltinPyInfo != None and BuiltinPyInfo in target and PyInfo != BuiltinPyInfo:
105-
providers.append(target[BuiltinPyInfo])
106-
107-
if PyRuntimeInfo in target:
108-
providers.append(target[PyRuntimeInfo])
109-
if BuiltinPyRuntimeInfo != None and BuiltinPyRuntimeInfo in target and PyRuntimeInfo != BuiltinPyRuntimeInfo:
110-
providers.append(target[BuiltinPyRuntimeInfo])
111-
return providers
112-
113-
_COMMON_ATTRS = {
114-
"deps": attr.label_list(
115-
mandatory = False,
116-
),
117-
"env": attr.string_dict(
118-
mandatory = False,
119-
),
120-
"python_version": attr.string(
121-
mandatory = True,
122-
),
123-
"srcs": attr.label_list(
124-
allow_files = True,
125-
mandatory = False,
126-
),
127-
"target": attr.label(
128-
executable = True,
129-
cfg = "target",
130-
mandatory = True,
131-
providers = [PyInfo],
132-
),
133-
# "tools" is a hack here. It should be "data" but "data" is not included by default in the
134-
# location expansion in the same way it is in the native Python rules. The difference on how
135-
# the Bazel deals with those special attributes differ on the LocationExpander, e.g.:
136-
# https://github.com/bazelbuild/bazel/blob/ce611646/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java#L415-L429
137-
#
138-
# Since the default LocationExpander used by ctx.expand_location is not the same as the native
139-
# rules (it doesn't set "allowDataAttributeEntriesInLabel"), we use "tools" temporarily while a
140-
# proper fix in Bazel happens.
141-
#
142-
# A fix for this was proposed in https://github.com/bazelbuild/bazel/pull/16381.
143-
"tools": attr.label_list(
144-
allow_files = True,
145-
mandatory = False,
146-
),
147-
# Required to Opt-in to the transitions feature.
148-
"_allowlist_function_transition": attr.label(
149-
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
150-
),
151-
"_windows_constraint": attr.label(
152-
default = "@platforms//os:windows",
153-
),
154-
}
155-
156-
_PY_TEST_ATTRS = {
157-
# Magic attribute to help C++ coverage work. There's no
158-
# docs about this; see TestActionBuilder.java
159-
"_collect_cc_coverage": attr.label(
160-
default = "@bazel_tools//tools/test:collect_cc_coverage",
161-
executable = True,
162-
cfg = "exec",
163-
),
164-
# Magic attribute to make coverage work. There's no
165-
# docs about this; see TestActionBuilder.java
166-
"_lcov_merger": attr.label(
167-
default = configuration_field(fragment = "coverage", name = "output_generator"),
168-
executable = True,
169-
cfg = "exec",
170-
),
171-
}
172-
173-
_transition_py_binary = rule(
174-
_transition_py_impl,
175-
attrs = _COMMON_ATTRS | _PY_TEST_ATTRS,
176-
cfg = _transition_python_version,
177-
executable = True,
178-
fragments = ["py"],
179-
)
180-
181-
_transition_py_test = rule(
182-
_transition_py_impl,
183-
attrs = _COMMON_ATTRS | _PY_TEST_ATTRS,
184-
cfg = _transition_python_version,
185-
test = True,
186-
fragments = ["py"],
187-
)
188-
189-
def _py_rule(rule_impl, transition_rule, name, python_version, **kwargs):
190-
pyargs = py_args(name, kwargs)
191-
args = pyargs["args"]
192-
data = pyargs["data"]
193-
env = pyargs["env"]
194-
srcs = pyargs["srcs"]
195-
deps = pyargs["deps"]
196-
main = pyargs["main"]
197-
198-
# Attributes common to all build rules.
199-
# https://bazel.build/reference/be/common-definitions#common-attributes
200-
compatible_with = kwargs.pop("compatible_with", None)
201-
deprecation = kwargs.pop("deprecation", None)
202-
exec_compatible_with = kwargs.pop("exec_compatible_with", None)
203-
exec_properties = kwargs.pop("exec_properties", None)
204-
features = kwargs.pop("features", None)
205-
restricted_to = kwargs.pop("restricted_to", None)
206-
tags = kwargs.pop("tags", None)
207-
target_compatible_with = kwargs.pop("target_compatible_with", None)
208-
testonly = kwargs.pop("testonly", None)
209-
toolchains = kwargs.pop("toolchains", None)
210-
visibility = kwargs.pop("visibility", None)
211-
212-
common_attrs = {
213-
"compatible_with": compatible_with,
214-
"deprecation": deprecation,
215-
"exec_compatible_with": exec_compatible_with,
216-
"exec_properties": exec_properties,
217-
"features": features,
218-
"restricted_to": restricted_to,
219-
"target_compatible_with": target_compatible_with,
220-
"testonly": testonly,
221-
"toolchains": toolchains,
222-
}
223-
224-
# Test-specific extra attributes.
225-
if "env_inherit" in kwargs:
226-
common_attrs["env_inherit"] = kwargs.pop("env_inherit")
227-
if "size" in kwargs:
228-
common_attrs["size"] = kwargs.pop("size")
229-
if "timeout" in kwargs:
230-
common_attrs["timeout"] = kwargs.pop("timeout")
231-
if "flaky" in kwargs:
232-
common_attrs["flaky"] = kwargs.pop("flaky")
233-
if "shard_count" in kwargs:
234-
common_attrs["shard_count"] = kwargs.pop("shard_count")
235-
if "local" in kwargs:
236-
common_attrs["local"] = kwargs.pop("local")
237-
238-
# Binary-specific extra attributes.
239-
if "output_licenses" in kwargs:
240-
common_attrs["output_licenses"] = kwargs.pop("output_licenses")
241-
242-
rule_impl(
243-
name = "_" + name,
244-
args = args,
245-
data = data,
246-
deps = deps,
247-
env = env,
248-
srcs = srcs,
249-
main = main,
250-
tags = ["manual"] + (tags if tags else []),
251-
visibility = ["//visibility:private"],
252-
**dicts.add(common_attrs, kwargs)
253-
)
254-
255-
return transition_rule(
256-
name = name,
257-
args = args,
258-
deps = deps,
259-
env = env,
260-
python_version = python_version,
261-
srcs = srcs,
262-
tags = tags,
263-
target = ":_" + name,
264-
tools = data,
265-
visibility = visibility,
266-
**common_attrs
267-
)
268-
269-
def py_binary(name, python_version, **kwargs):
270-
return _py_rule(_py_binary, _transition_py_binary, name, python_version, **kwargs)
271-
272-
def py_test(name, python_version, **kwargs):
273-
return _py_rule(_py_test, _transition_py_test, name, python_version, **kwargs)
27426

275-
def _strip_suffix(s, suffix):
276-
if s.endswith(suffix):
277-
return s[:-len(suffix)]
278-
else:
279-
return s
27+
py_binary = _py_binary
28+
py_test = _py_test

python/private/py_executable.bzl

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ load(
7676
_py_builtins = py_internal
7777
_EXTERNAL_PATH_PREFIX = "external"
7878
_ZIP_RUNFILES_DIRECTORY_NAME = "runfiles"
79+
_PYTHON_VERSION_FLAG = str(Label("//python/config_settings:python_version"))
7980

8081
# Bazel 5.4 doesn't have config_common.toolchain_type
8182
_CC_TOOLCHAINS = [config_common.toolchain_type(
@@ -132,16 +133,34 @@ Valid values are:
132133
target level.
133134
""",
134135
),
135-
# TODO(b/203567235): In Google, this attribute is deprecated, and can
136-
# only effectively be PY3. Externally, with Bazel, this attribute has
137-
# a separate story.
138136
"python_version": attr.string(
139137
# TODO(b/203567235): In the Java impl, the default comes from
140138
# --python_version. Not clear what the Starlark equivalent is.
141-
default = "PY3",
142-
# NOTE: Some tests care about the order of these values.
143-
values = ["PY2", "PY3"],
144-
doc = "Defunct, unused, does nothing.",
139+
doc = """
140+
The Python version this target should use.
141+
142+
The value should be in `X.Y` or `X.Y.Z` (or compatible) format. If empty or
143+
unspecified, the incoming configuration's {obj}`--python_version` flag is
144+
inherited. For backwards compatibility, the values `PY2` and `PY3` are
145+
accepted, but treated as an empty/unspecified value.
146+
147+
:::{note}
148+
In order for the requested version to be used, there must be a
149+
toolchain configured to match the Python version. If there isn't, then it
150+
may be silently ignored, or an error may occur, depending on the toolchain
151+
configuration.
152+
:::
153+
154+
:::{versionchanged} VERSION_NEXT_PATCH
155+
156+
This attribute was changed from only accepting `PY2` and `PY3` values to
157+
accepting arbitrary Python versions.
158+
:::
159+
""",
160+
),
161+
# Required to opt-in to the transition feature.
162+
"_allowlist_function_transition": attr.label(
163+
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
145164
),
146165
"_bootstrap_impl_flag": attr.label(
147166
default = "//python/config_settings:bootstrap_impl",
@@ -1008,7 +1027,7 @@ def _get_build_info(ctx, cc_toolchain):
10081027
return build_info_files.redacted_build_info_files.to_list()
10091028

10101029
def _validate_executable(ctx):
1011-
if ctx.attr.python_version != "PY3":
1030+
if ctx.attr.python_version == "PY2":
10121031
fail("It is not allowed to use Python 2")
10131032

10141033
def _declare_executable_file(ctx):
@@ -1691,9 +1710,26 @@ def _create_run_environment_info(ctx, inherited_environment):
16911710
inherited_environment = inherited_environment,
16921711
)
16931712

1713+
def _transition_executable_impl(input_settings, attr):
1714+
settings = {
1715+
_PYTHON_VERSION_FLAG: input_settings[_PYTHON_VERSION_FLAG],
1716+
}
1717+
if attr.python_version and attr.python_version not in ("PY2", "PY3"):
1718+
settings[_PYTHON_VERSION_FLAG] = attr.python_version
1719+
return settings
1720+
1721+
_transition_executable = transition(
1722+
implementation = _transition_executable_impl,
1723+
inputs = [
1724+
_PYTHON_VERSION_FLAG,
1725+
],
1726+
outputs = [
1727+
_PYTHON_VERSION_FLAG,
1728+
],
1729+
)
1730+
16941731
def create_executable_rule(*, attrs, **kwargs):
16951732
return create_base_executable_rule(
1696-
##attrs = dicts.add(EXECUTABLE_ATTRS, attrs),
16971733
attrs = attrs,
16981734
fragments = ["py", "bazel_py"],
16991735
**kwargs
@@ -1715,6 +1751,7 @@ def create_base_executable_rule(*, attrs, fragments = [], **kwargs):
17151751
fragments = fragments + ["py"]
17161752
kwargs.setdefault("provides", []).append(PyExecutableInfo)
17171753
kwargs["exec_groups"] = REQUIRED_EXEC_GROUPS | (kwargs.get("exec_groups") or {})
1754+
kwargs.setdefault("cfg", _transition_executable)
17181755
return rule(
17191756
# TODO: add ability to remove attrs, i.e. for imports attr
17201757
attrs = dicts.add(EXECUTABLE_ATTRS, attrs),

0 commit comments

Comments
 (0)