Skip to content

Commit 8e76bd4

Browse files
rickeylevaignas
andauthored
refactor: add rule to do analysis time evaluation of environment markers (#2832)
wip/prototype to help bootstrap the impl of an analysis-time flag that evaluates the pep508 dep specs Creating a PR to make collab easier (maintainers can directly edit) TODO: * Remove the todo markers after discussion Work towards #2826 --------- Co-authored-by: Ignas Anikevicius <[email protected]>
1 parent 76b221e commit 8e76bd4

File tree

4 files changed

+351
-3
lines changed

4 files changed

+351
-3
lines changed
+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""Implement a flag for matching the dependency specifiers at analysis time."""
2+
3+
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
4+
load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
5+
load(
6+
":pep508_env.bzl",
7+
"env_aliases",
8+
"os_name_select_map",
9+
"platform_machine_select_map",
10+
"platform_system_select_map",
11+
"sys_platform_select_map",
12+
)
13+
load(":pep508_evaluate.bzl", "evaluate")
14+
15+
# Use capitals to hint its not an actual boolean type.
16+
_ENV_MARKER_TRUE = "TRUE"
17+
_ENV_MARKER_FALSE = "FALSE"
18+
19+
def env_marker_setting(*, name, expression, **kwargs):
20+
"""Creates an env_marker setting.
21+
22+
Generated targets:
23+
24+
* `is_{name}_true`: config_setting that matches when the expression is true.
25+
* `{name}`: env marker target that evalutes the expression.
26+
27+
Args:
28+
name: {type}`str` target name
29+
expression: {type}`str` the environment marker string to evaluate
30+
**kwargs: {type}`dict` additional common kwargs.
31+
"""
32+
native.config_setting(
33+
name = "is_{}_true".format(name),
34+
flag_values = {
35+
":{}".format(name): _ENV_MARKER_TRUE,
36+
},
37+
**kwargs
38+
)
39+
_env_marker_setting(
40+
name = name,
41+
expression = expression,
42+
os_name = select(os_name_select_map),
43+
sys_platform = select(sys_platform_select_map),
44+
platform_machine = select(platform_machine_select_map),
45+
platform_system = select(platform_system_select_map),
46+
platform_release = select({
47+
"@platforms//os:osx": "USE_OSX_VERSION_FLAG",
48+
"//conditions:default": "",
49+
}),
50+
**kwargs
51+
)
52+
53+
def _env_marker_setting_impl(ctx):
54+
env = {}
55+
56+
runtime = ctx.toolchains[TARGET_TOOLCHAIN_TYPE].py3_runtime
57+
if runtime.interpreter_version_info:
58+
version_info = runtime.interpreter_version_info
59+
env["python_version"] = "{major}.{minor}".format(
60+
major = version_info.major,
61+
minor = version_info.minor,
62+
)
63+
full_version = _format_full_version(version_info)
64+
env["python_full_version"] = full_version
65+
env["implementation_version"] = full_version
66+
else:
67+
env["python_version"] = _get_flag(ctx.attr._python_version_major_minor_flag)
68+
full_version = _get_flag(ctx.attr._python_full_version_flag)
69+
env["python_full_version"] = full_version
70+
env["implementation_version"] = full_version
71+
72+
# We assume cpython if the toolchain doesn't specify because it's most
73+
# likely to be true.
74+
env["implementation_name"] = runtime.implementation_name or "cpython"
75+
env["os_name"] = ctx.attr.os_name
76+
env["sys_platform"] = ctx.attr.sys_platform
77+
env["platform_machine"] = ctx.attr.platform_machine
78+
79+
# The `platform_python_implementation` marker value is supposed to come
80+
# from `platform.python_implementation()`, however, PEP 421 introduced
81+
# `sys.implementation.name` and the `implementation_name` env marker to
82+
# replace it. Per the platform.python_implementation docs, there's now
83+
# essentially just two possible "registered" values: CPython or PyPy.
84+
# Rather than add a field to the toolchain, we just special case the value
85+
# from `sys.implementation.name` to handle the two documented values.
86+
platform_python_impl = runtime.implementation_name
87+
if platform_python_impl == "cpython":
88+
platform_python_impl = "CPython"
89+
elif platform_python_impl == "pypy":
90+
platform_python_impl = "PyPy"
91+
env["platform_python_implementation"] = platform_python_impl
92+
93+
# NOTE: Platform release for Android will be Android version:
94+
# https://peps.python.org/pep-0738/#platform
95+
# Similar for iOS:
96+
# https://peps.python.org/pep-0730/#platform
97+
platform_release = ctx.attr.platform_release
98+
if platform_release == "USE_OSX_VERSION_FLAG":
99+
platform_release = _get_flag(ctx.attr._pip_whl_osx_version_flag)
100+
env["platform_release"] = platform_release
101+
env["platform_system"] = ctx.attr.platform_system
102+
103+
# For lack of a better option, just use an empty string for now.
104+
env["platform_version"] = ""
105+
106+
env.update(env_aliases())
107+
108+
if evaluate(ctx.attr.expression, env = env):
109+
value = _ENV_MARKER_TRUE
110+
else:
111+
value = _ENV_MARKER_FALSE
112+
return [config_common.FeatureFlagInfo(value = value)]
113+
114+
_env_marker_setting = rule(
115+
doc = """
116+
Evaluates an environment marker expression using target configuration info.
117+
118+
See
119+
https://packaging.python.org/en/latest/specifications/dependency-specifiers
120+
for the specification of behavior.
121+
""",
122+
implementation = _env_marker_setting_impl,
123+
attrs = {
124+
"expression": attr.string(
125+
mandatory = True,
126+
doc = "Environment marker expression to evaluate.",
127+
),
128+
"os_name": attr.string(),
129+
"platform_machine": attr.string(),
130+
"platform_release": attr.string(),
131+
"platform_system": attr.string(),
132+
"sys_platform": attr.string(),
133+
"_pip_whl_osx_version_flag": attr.label(
134+
default = "//python/config_settings:pip_whl_osx_version",
135+
providers = [[BuildSettingInfo], [config_common.FeatureFlagInfo]],
136+
),
137+
"_python_full_version_flag": attr.label(
138+
default = "//python/config_settings:python_version",
139+
providers = [config_common.FeatureFlagInfo],
140+
),
141+
"_python_version_major_minor_flag": attr.label(
142+
default = "//python/config_settings:python_version_major_minor",
143+
providers = [config_common.FeatureFlagInfo],
144+
),
145+
},
146+
provides = [config_common.FeatureFlagInfo],
147+
toolchains = [
148+
TARGET_TOOLCHAIN_TYPE,
149+
],
150+
)
151+
152+
def _format_full_version(info):
153+
"""Format the full python interpreter version.
154+
155+
Adapted from spec code at:
156+
https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers
157+
158+
Args:
159+
info: The provider from the Python runtime.
160+
161+
Returns:
162+
a {type}`str` with the version
163+
"""
164+
kind = info.releaselevel
165+
if kind == "final":
166+
kind = ""
167+
serial = ""
168+
else:
169+
kind = kind[0] if kind else ""
170+
serial = str(info.serial) if info.serial else ""
171+
172+
return "{major}.{minor}.{micro}{kind}{serial}".format(
173+
v = info,
174+
major = info.major,
175+
minor = info.minor,
176+
micro = info.micro,
177+
kind = kind,
178+
serial = serial,
179+
)
180+
181+
def _get_flag(t):
182+
if config_common.FeatureFlagInfo in t:
183+
return t[config_common.FeatureFlagInfo].value
184+
if BuildSettingInfo in t:
185+
return t[BuildSettingInfo].value
186+
fail("Should not occur: {} does not have necessary providers")

python/private/pypi/pep508_env.bzl

+91-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
load(":pep508_platform.bzl", "platform_from_str")
1919

2020
# See https://stackoverflow.com/a/45125525
21-
_platform_machine_aliases = {
21+
platform_machine_aliases = {
2222
# These pairs mean the same hardware, but different values may be used
2323
# on different host platforms.
2424
"amd64": "x86_64",
@@ -27,13 +27,65 @@ _platform_machine_aliases = {
2727
"i686": "x86_32",
2828
}
2929

30+
# NOTE: There are many cpus, and unfortunately, the value isn't directly
31+
# accessible to Starlark. Using CcToolchain.cpu might work, though.
32+
platform_machine_select_map = {
33+
"@platforms//cpu:aarch32": "aarch32",
34+
"@platforms//cpu:aarch64": "aarch64",
35+
"@platforms//cpu:arm": "arm",
36+
"@platforms//cpu:arm64": "arm64",
37+
"@platforms//cpu:arm64_32": "arm64_32",
38+
"@platforms//cpu:arm64e": "arm64e",
39+
"@platforms//cpu:armv6-m": "armv6-m",
40+
"@platforms//cpu:armv7": "armv7",
41+
"@platforms//cpu:armv7-m": "armv7-m",
42+
"@platforms//cpu:armv7e-m": "armv7e-m",
43+
"@platforms//cpu:armv7e-mf": "armv7e-mf",
44+
"@platforms//cpu:armv7k": "armv7k",
45+
"@platforms//cpu:armv8-m": "armv8-m",
46+
"@platforms//cpu:cortex-r52": "cortex-r52",
47+
"@platforms//cpu:cortex-r82": "cortex-r82",
48+
"@platforms//cpu:i386": "i386",
49+
"@platforms//cpu:mips64": "mips64",
50+
"@platforms//cpu:ppc": "ppc",
51+
"@platforms//cpu:ppc32": "ppc32",
52+
"@platforms//cpu:ppc64le": "ppc64le",
53+
"@platforms//cpu:riscv32": "riscv32",
54+
"@platforms//cpu:riscv64": "riscv64",
55+
"@platforms//cpu:s390x": "s390x",
56+
"@platforms//cpu:wasm32": "wasm32",
57+
"@platforms//cpu:wasm64": "wasm64",
58+
"@platforms//cpu:x86_32": "x86_32",
59+
"@platforms//cpu:x86_64": "x86_64",
60+
# The value is empty string if it cannot be determined:
61+
# https://docs.python.org/3/library/platform.html#platform.machine
62+
"//conditions:default": "",
63+
}
64+
3065
# Platform system returns results from the `uname` call.
3166
_platform_system_values = {
3267
"linux": "Linux",
3368
"osx": "Darwin",
3469
"windows": "Windows",
3570
}
3671

72+
platform_system_select_map = {
73+
# See https://peps.python.org/pep-0738/#platform
74+
"@platforms//os:android": "Android",
75+
"@platforms//os:freebsd": "FreeBSD",
76+
# See https://peps.python.org/pep-0730/#platform
77+
# NOTE: Per Pep 730, "iPadOS" is also an acceptable value
78+
"@platforms//os:ios": "iOS",
79+
"@platforms//os:linux": "Linux",
80+
"@platforms//os:netbsd": "NetBSD",
81+
"@platforms//os:openbsd": "OpenBSD",
82+
"@platforms//os:osx": "Darwin",
83+
"@platforms//os:windows": "Windows",
84+
# The value is empty string if it cannot be determined:
85+
# https://docs.python.org/3/library/platform.html#platform.machine
86+
"//conditions:default": "",
87+
}
88+
3789
# The copy of SO [answer](https://stackoverflow.com/a/13874620) containing
3890
# all of the platforms:
3991
# ┍━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━┑
@@ -64,12 +116,45 @@ _sys_platform_values = {
64116
"osx": "darwin",
65117
"windows": "win32",
66118
}
119+
120+
# Taken from
121+
# https://docs.python.org/3/library/sys.html#sys.platform
122+
sys_platform_select_map = {
123+
# These values are decided by the sys.platform docs.
124+
"@platforms//os:android": "android",
125+
"@platforms//os:emscripten": "emscripten",
126+
# NOTE: The below values are approximations. The sys.platform() docs
127+
# don't have documented values for these OSes. Per docs, the
128+
# sys.platform() value reflects the OS at the time Python was *built*
129+
# instead of the runtime (target) OS value.
130+
"@platforms//os:freebsd": "freebsd",
131+
"@platforms//os:ios": "ios",
132+
"@platforms//os:linux": "linux",
133+
"@platforms//os:openbsd": "openbsd",
134+
"@platforms//os:osx": "darwin",
135+
"@platforms//os:wasi": "wasi",
136+
"@platforms//os:windows": "win32",
137+
# For lack of a better option, use empty string. No standard doc/spec
138+
# about sys_platform value.
139+
"//conditions:default": "",
140+
}
141+
67142
_os_name_values = {
68143
"linux": "posix",
69144
"osx": "posix",
70145
"windows": "nt",
71146
}
72147

148+
os_name_select_map = {
149+
# The "java" value is documented, but with Jython defunct,
150+
# shouldn't occur in practice.
151+
# The os.name value is technically a property of the runtime, not the
152+
# targetted runtime OS, but the distinction shouldn't matter if
153+
# things are properly configured.
154+
"@platforms//os:windows": "nt",
155+
"//conditions:default": "posix",
156+
}
157+
73158
def env(target_platform, *, extra = None):
74159
"""Return an env target platform
75160
@@ -113,8 +198,11 @@ def env(target_platform, *, extra = None):
113198
}
114199

115200
# This is split by topic
116-
return env | {
201+
return env | env_aliases()
202+
203+
def env_aliases():
204+
return {
117205
"_aliases": {
118-
"platform_machine": _platform_machine_aliases,
206+
"platform_machine": platform_machine_aliases,
119207
},
120208
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
load(":env_marker_setting_tests.bzl", "env_marker_setting_test_suite")
2+
3+
env_marker_setting_test_suite(
4+
name = "env_marker_setting_tests",
5+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""env_marker_setting tests."""
2+
3+
load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
4+
load("@rules_testing//lib:test_suite.bzl", "test_suite")
5+
load("@rules_testing//lib:util.bzl", "TestingAspectInfo")
6+
load("//python/private/pypi:env_marker_setting.bzl", "env_marker_setting") # buildifier: disable=bzl-visibility
7+
load("//tests/support:support.bzl", "PYTHON_VERSION")
8+
9+
_tests = []
10+
11+
def _test_expr(name):
12+
def impl(env, target):
13+
env.expect.where(
14+
expression = target[TestingAspectInfo].attrs.expression,
15+
).that_str(
16+
target[config_common.FeatureFlagInfo].value,
17+
).equals(
18+
env.ctx.attr.expected,
19+
)
20+
21+
cases = {
22+
"python_full_version_lt_negative": {
23+
"config_settings": {
24+
PYTHON_VERSION: "3.12.0",
25+
},
26+
"expected": "FALSE",
27+
"expression": "python_full_version < '3.8'",
28+
},
29+
"python_version_gte": {
30+
"config_settings": {
31+
PYTHON_VERSION: "3.12.0",
32+
},
33+
"expected": "TRUE",
34+
"expression": "python_version >= '3.12.0'",
35+
},
36+
}
37+
38+
tests = []
39+
for case_name, case in cases.items():
40+
test_name = name + "_" + case_name
41+
tests.append(test_name)
42+
env_marker_setting(
43+
name = test_name + "_subject",
44+
expression = case["expression"],
45+
)
46+
analysis_test(
47+
name = test_name,
48+
impl = impl,
49+
target = test_name + "_subject",
50+
config_settings = case["config_settings"],
51+
attr_values = {
52+
"expected": case["expected"],
53+
},
54+
attrs = {
55+
"expected": attr.string(),
56+
},
57+
)
58+
native.test_suite(
59+
name = name,
60+
tests = tests,
61+
)
62+
63+
_tests.append(_test_expr)
64+
65+
def env_marker_setting_test_suite(name):
66+
test_suite(
67+
name = name,
68+
tests = _tests,
69+
)

0 commit comments

Comments
 (0)