Skip to content

Commit 0b3d845

Browse files
authored
refactor: make env marker config available through target and flag (#2853)
This factors creation of (most of) the env marker dict into a separate target and provides a label flag to allow customizing the target that provides it. This makes it easier for users to override how env marker values are computed. The `env_marker_setting` rule will still, if necessary, compute values from the toolchain, but existing keys (computed from the env marker config target) have precedence. The `EnvMarkerInfo` provider is the interface for implementing a custom env marker config target; it will be publically exposed in a subsequent PR. Along the way, unify how the env dict and defaults are set. Work towards #2826
1 parent 63555e1 commit 0b3d845

File tree

10 files changed

+300
-123
lines changed

10 files changed

+300
-123
lines changed

docs/api/rules_python/python/config_settings/index.md

+12
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ Values:
159159
:::
160160
::::
161161

162+
::::{bzl:flag} pip_env_marker_config
163+
The target that provides the values for pip env marker evaluation.
164+
165+
Default: `//python/config_settings:_pip_env_marker_default_config`
166+
167+
This flag points to a target providing {obj}`EnvMarkerInfo`, which determines
168+
the values used when environment markers are resolved at build time.
169+
170+
:::{versionadded} VERSION_NEXT_FEATURE
171+
:::
172+
::::
173+
162174
::::{bzl:flag} pip_whl
163175
Set what distributions are used in the `pip` integration.
164176

docs/pypi-dependencies.md

+31-1
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,6 @@ leg of the dependency manually. For instance by making
338338
perhaps `apache-airflow-providers-common-sql`.
339339

340340

341-
(bazel-downloader)=
342341
### Multi-platform support
343342

344343
Multi-platform support of cross-building the wheels can be done in two ways - either
@@ -391,6 +390,31 @@ compatible indexes.
391390
This is only supported on `bzlmd`.
392391
```
393392

393+
<!--
394+
395+
TODO: uncomment this when analysis-phase dependency selection is available
396+
397+
#### Customizing requirements resolution
398+
399+
In Python packaging, packages can express dependencies with conditions
400+
using "environment markers", which represent the Python version, OS, etc.
401+
402+
While the PyPI integration provides reasonable defaults to support most
403+
platforms and environment markers, the values it uses can be customized in case
404+
more esoteric configurations are needed.
405+
406+
To customize the values used, you need to do two things:
407+
1. Define a target that returns {obj}`EnvMarkerInfo`
408+
2. Set the {obj}`//python/config_settings:pip_env_marker_config` flag to
409+
the target defined in (1).
410+
411+
The keys and values should be compatible with the [PyPA dependency specifiers
412+
specification](https://packaging.python.org/en/latest/specifications/dependency-specifiers/).
413+
This is not strictly enforced, however, so you can return a subset of keys or
414+
additional keys, which become available during dependency evalution.
415+
416+
-->
417+
394418
(bazel-downloader)=
395419
### Bazel downloader and multi-platform wheel hub repository.
396420

@@ -487,3 +511,9 @@ Bazel will call this file like `cred_helper.sh get` and use the returned JSON to
487511
into whatever HTTP(S) request it performs against `example.com`.
488512

489513
[rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617
514+
515+
<!--
516+
517+
518+
519+
-->

python/config_settings/BUILD.bazel

+7
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,10 @@ string_flag(
220220
define_pypi_internal_flags(
221221
name = "define_pypi_internal_flags",
222222
)
223+
224+
label_flag(
225+
name = "pip_env_marker_config",
226+
build_setting_default = ":_pip_env_marker_default_config",
227+
# NOTE: Only public because it is used in pip hub repos.
228+
visibility = ["//visibility:public"],
229+
)

python/private/pypi/BUILD.bazel

+19
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ bzl_library(
7171
],
7272
)
7373

74+
bzl_library(
75+
name = "env_marker_info_bzl",
76+
srcs = ["env_marker_info.bzl"],
77+
)
78+
79+
bzl_library(
80+
name = "env_marker_setting_bzl",
81+
srcs = ["env_marker_setting.bzl"],
82+
deps = [
83+
":env_marker_info_bzl",
84+
":pep508_env_bzl",
85+
":pep508_evaluate_bzl",
86+
"//python/private:toolchain_types_bzl",
87+
"@bazel_skylib//rules:common_settings",
88+
],
89+
)
90+
7491
bzl_library(
7592
name = "evaluate_markers_bzl",
7693
srcs = ["evaluate_markers.bzl"],
@@ -111,6 +128,8 @@ bzl_library(
111128
name = "flags_bzl",
112129
srcs = ["flags.bzl"],
113130
deps = [
131+
":env_marker_info.bzl",
132+
":pep508_env_bzl",
114133
"//python/private:enum_bzl",
115134
"@bazel_skylib//rules:common_settings",
116135
],
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Provider for implementing environment marker values."""
2+
3+
EnvMarkerInfo = provider(
4+
doc = """
5+
The values to use during environment marker evaluation.
6+
7+
:::{seealso}
8+
The {obj}`--//python/config_settings:pip_env_marker_config` flag.
9+
:::
10+
11+
:::{versionadded} VERSION_NEXT_FEATURE
12+
""",
13+
fields = {
14+
"env": """
15+
:type: dict[str, str]
16+
17+
The values to use for environment markers when evaluating an expression.
18+
19+
The keys and values should be compatible with the [PyPA dependency specifiers
20+
specification](https://packaging.python.org/en/latest/specifications/dependency-specifiers/)
21+
22+
Missing values will be set to the specification's defaults or computed using
23+
available toolchain information.
24+
""",
25+
},
26+
)

python/private/pypi/env_marker_setting.bzl

+29-75
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,8 @@
22

33
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
44
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-
)
5+
load(":env_marker_info.bzl", "EnvMarkerInfo")
6+
load(":pep508_env.bzl", "create_env", "set_missing_env_defaults")
137
load(":pep508_evaluate.bzl", "evaluate")
148

159
# Use capitals to hint its not an actual boolean type.
@@ -39,72 +33,37 @@ def env_marker_setting(*, name, expression, **kwargs):
3933
_env_marker_setting(
4034
name = name,
4135
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-
}),
5036
**kwargs
5137
)
5238

5339
def _env_marker_setting_impl(ctx):
54-
env = {}
40+
env = create_env()
41+
env.update(
42+
ctx.attr._env_marker_config_flag[EnvMarkerInfo].env,
43+
)
5544

5645
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())
10746

47+
if "python_version" not in env:
48+
if runtime.interpreter_version_info:
49+
version_info = runtime.interpreter_version_info
50+
env["python_version"] = "{major}.{minor}".format(
51+
major = version_info.major,
52+
minor = version_info.minor,
53+
)
54+
full_version = _format_full_version(version_info)
55+
env["python_full_version"] = full_version
56+
env["implementation_version"] = full_version
57+
else:
58+
env["python_version"] = _get_flag(ctx.attr._python_version_major_minor_flag)
59+
full_version = _get_flag(ctx.attr._python_full_version_flag)
60+
env["python_full_version"] = full_version
61+
env["implementation_version"] = full_version
62+
63+
if "implementation_name" not in env and runtime.implementation_name:
64+
env["implementation_name"] = runtime.implementation_name
65+
66+
set_missing_env_defaults(env)
10867
if evaluate(ctx.attr.expression, env = env):
10968
value = _ENV_MARKER_TRUE
11069
else:
@@ -125,14 +84,9 @@ for the specification of behavior.
12584
mandatory = True,
12685
doc = "Environment marker expression to evaluate.",
12786
),
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]],
87+
"_env_marker_config_flag": attr.label(
88+
default = "//python/config_settings:pip_env_marker_config",
89+
providers = [EnvMarkerInfo],
13690
),
13791
"_python_full_version_flag": attr.label(
13892
default = "//python/config_settings:python_version",

python/private/pypi/flags.bzl

+68
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ unnecessary files when all that are needed are flag definitions.
2020

2121
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo", "string_flag")
2222
load("//python/private:enum.bzl", "enum")
23+
load(":env_marker_info.bzl", "EnvMarkerInfo")
24+
load(
25+
":pep508_env.bzl",
26+
"create_env",
27+
"os_name_select_map",
28+
"platform_machine_select_map",
29+
"platform_system_select_map",
30+
"sys_platform_select_map",
31+
)
2332

2433
# Determines if we should use whls for third party
2534
#
@@ -82,6 +91,10 @@ def define_pypi_internal_flags(name):
8291
visibility = ["//visibility:public"],
8392
)
8493

94+
_default_env_marker_config(
95+
name = "_pip_env_marker_default_config",
96+
)
97+
8598
def _allow_wheels_flag_impl(ctx):
8699
input = ctx.attr._setting[BuildSettingInfo].value
87100
value = "yes" if input in ["auto", "only"] else "no"
@@ -97,3 +110,58 @@ This rule allows us to greatly reduce the number of config setting targets at no
97110
if we are duplicating some of the functionality of the `native.config_setting`.
98111
""",
99112
)
113+
114+
def _default_env_marker_config(**kwargs):
115+
_env_marker_config(
116+
os_name = select(os_name_select_map),
117+
sys_platform = select(sys_platform_select_map),
118+
platform_machine = select(platform_machine_select_map),
119+
platform_system = select(platform_system_select_map),
120+
platform_release = select({
121+
"@platforms//os:osx": "USE_OSX_VERSION_FLAG",
122+
"//conditions:default": "",
123+
}),
124+
**kwargs
125+
)
126+
127+
def _env_marker_config_impl(ctx):
128+
env = create_env()
129+
env["os_name"] = ctx.attr.os_name
130+
env["sys_platform"] = ctx.attr.sys_platform
131+
env["platform_machine"] = ctx.attr.platform_machine
132+
133+
# NOTE: Platform release for Android will be Android version:
134+
# https://peps.python.org/pep-0738/#platform
135+
# Similar for iOS:
136+
# https://peps.python.org/pep-0730/#platform
137+
platform_release = ctx.attr.platform_release
138+
if platform_release == "USE_OSX_VERSION_FLAG":
139+
platform_release = _get_flag(ctx.attr._pip_whl_osx_version_flag)
140+
env["platform_release"] = platform_release
141+
env["platform_system"] = ctx.attr.platform_system
142+
143+
# NOTE: We intentionally do not call set_missing_env_defaults() here because
144+
# `env_marker_setting()` computes missing values using the toolchain.
145+
return [EnvMarkerInfo(env = env)]
146+
147+
_env_marker_config = rule(
148+
implementation = _env_marker_config_impl,
149+
attrs = {
150+
"os_name": attr.string(),
151+
"platform_machine": attr.string(),
152+
"platform_release": attr.string(),
153+
"platform_system": attr.string(),
154+
"sys_platform": attr.string(),
155+
"_pip_whl_osx_version_flag": attr.label(
156+
default = "//python/config_settings:pip_whl_osx_version",
157+
providers = [[BuildSettingInfo], [config_common.FeatureFlagInfo]],
158+
),
159+
},
160+
)
161+
162+
def _get_flag(t):
163+
if config_common.FeatureFlagInfo in t:
164+
return t[config_common.FeatureFlagInfo].value
165+
if BuildSettingInfo in t:
166+
return t[BuildSettingInfo].value
167+
fail("Should not occur: {} does not have necessary providers")

0 commit comments

Comments
 (0)