feat(pypi): incrementally build platform configuration (#3112)
Before this PR the configuration for platforms would be built
non-incrementally, making it harder for users to override particular
attributes of the already configured ones.
With this PR the new features introduced in #3058 will be easier to
override.
Work towards #2747
diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl
index 08e1af4..59e77d1 100644
--- a/python/private/pypi/extension.bzl
+++ b/python/private/pypi/extension.bzl
@@ -377,26 +377,80 @@
),
)
-def _configure(config, *, platform, os_name, arch_name, config_settings, env = {}, override = False):
- """Set the value in the config if the value is provided"""
- config.setdefault("platforms", {})
- if platform:
- if not override and config.get("platforms", {}).get(platform):
- return
+def _plat(*, name, arch_name, os_name, config_settings = [], env = {}):
+ return struct(
+ name = name,
+ arch_name = arch_name,
+ os_name = os_name,
+ config_settings = config_settings,
+ env = env,
+ )
+def _configure(config, *, override = False, **kwargs):
+ """Set the value in the config if the value is provided"""
+ env = kwargs.get("env")
+ if env:
for key in env:
if key not in _SUPPORTED_PEP508_KEYS:
fail("Unsupported key in the PEP508 environment: {}".format(key))
- config["platforms"][platform] = struct(
- name = platform.replace("-", "_").lower(),
- os_name = os_name,
- arch_name = arch_name,
- config_settings = config_settings,
- env = env,
- )
- else:
- config["platforms"].pop(platform)
+ for key, value in kwargs.items():
+ if value and (override or key not in config):
+ config[key] = value
+
+def build_config(
+ *,
+ module_ctx,
+ enable_pipstar):
+ """Parse 'configure' and 'default' extension tags
+
+ Args:
+ module_ctx: {type}`module_ctx` module context.
+ enable_pipstar: {type}`bool` a flag to enable dropping Python dependency for
+ evaluation of the extension.
+
+ Returns:
+ A struct with the configuration.
+ """
+ defaults = {
+ "platforms": {},
+ }
+ for mod in module_ctx.modules:
+ if not (mod.is_root or mod.name == "rules_python"):
+ continue
+
+ for tag in mod.tags.default:
+ platform = tag.platform
+ if platform:
+ specific_config = defaults["platforms"].setdefault(platform, {})
+ _configure(
+ specific_config,
+ arch_name = tag.arch_name,
+ config_settings = tag.config_settings,
+ env = tag.env,
+ os_name = tag.os_name,
+ name = platform.replace("-", "_").lower(),
+ override = mod.is_root,
+ )
+
+ if platform and not (tag.arch_name or tag.config_settings or tag.env or tag.os_name):
+ defaults["platforms"].pop(platform)
+
+ # TODO @aignas 2025-05-19: add more attr groups:
+ # * for AUTH - the default `netrc` usage could be configured through a common
+ # attribute.
+ # * for index/downloader config. This includes all of those attributes for
+ # overrides, etc. Index overrides per platform could be also used here.
+ # * for whl selection - selecting preferences of which `platform_tag`s we should use
+ # for what. We could also model the `cp313t` freethreaded as separate platforms.
+
+ return struct(
+ platforms = {
+ name: _plat(**values)
+ for name, values in defaults["platforms"].items()
+ },
+ enable_pipstar = enable_pipstar,
+ )
def parse_modules(
module_ctx,
@@ -448,33 +502,7 @@
srcs_exclude_glob = whl_mod.srcs_exclude_glob,
)
- defaults = {
- "enable_pipstar": enable_pipstar,
- "platforms": {},
- }
- for mod in module_ctx.modules:
- if not (mod.is_root or mod.name == "rules_python"):
- continue
-
- for tag in mod.tags.default:
- _configure(
- defaults,
- arch_name = tag.arch_name,
- config_settings = tag.config_settings,
- env = tag.env,
- os_name = tag.os_name,
- platform = tag.platform,
- override = mod.is_root,
- # TODO @aignas 2025-05-19: add more attr groups:
- # * for AUTH - the default `netrc` usage could be configured through a common
- # attribute.
- # * for index/downloader config. This includes all of those attributes for
- # overrides, etc. Index overrides per platform could be also used here.
- # * for whl selection - selecting preferences of which `platform_tag`s we should use
- # for what. We could also model the `cp313t` freethreaded as separate platforms.
- )
-
- config = struct(**defaults)
+ config = build_config(module_ctx = module_ctx, enable_pipstar = enable_pipstar)
# TODO @aignas 2025-06-03: Merge override API with the builder?
_overriden_whl_set = {}
@@ -659,6 +687,7 @@
k: dict(sorted(args.items()))
for k, args in sorted(whl_libraries.items())
},
+ config = config,
)
def _pip_impl(module_ctx):
diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl
index 4949c0d..d115546 100644
--- a/tests/pypi/extension/extension_tests.bzl
+++ b/tests/pypi/extension/extension_tests.bzl
@@ -16,7 +16,7 @@
load("@rules_testing//lib:test_suite.bzl", "test_suite")
load("@rules_testing//lib:truth.bzl", "subjects")
-load("//python/private/pypi:extension.bzl", "parse_modules") # buildifier: disable=bzl-visibility
+load("//python/private/pypi:extension.bzl", "build_config", "parse_modules") # buildifier: disable=bzl-visibility
load("//python/private/pypi:parse_simpleapi_html.bzl", "parse_simpleapi_html") # buildifier: disable=bzl-visibility
load("//python/private/pypi:whl_config_setting.bzl", "whl_config_setting") # buildifier: disable=bzl-visibility
@@ -92,6 +92,18 @@
),
)
+def _build_config(env, enable_pipstar = 0, **kwargs):
+ return env.expect.that_struct(
+ build_config(
+ enable_pipstar = enable_pipstar,
+ **kwargs
+ ),
+ attrs = dict(
+ platforms = subjects.dict,
+ enable_pipstar = subjects.bool,
+ ),
+ )
+
def _default(
arch_name = None,
config_settings = None,
@@ -1206,6 +1218,54 @@
_tests.append(_test_pipstar_platforms)
+def _test_build_pipstar_platform(env):
+ config = _build_config(
+ env,
+ module_ctx = _mock_mctx(
+ _mod(
+ name = "rules_python",
+ default = [
+ _default(
+ platform = "myplat",
+ os_name = "linux",
+ arch_name = "x86_64",
+ config_settings = [
+ "@platforms//os:linux",
+ "@platforms//cpu:x86_64",
+ ],
+ ),
+ _default(),
+ _default(
+ platform = "myplat2",
+ os_name = "linux",
+ arch_name = "x86_64",
+ config_settings = [
+ "@platforms//os:linux",
+ "@platforms//cpu:x86_64",
+ ],
+ ),
+ _default(platform = "myplat2"),
+ ],
+ ),
+ ),
+ enable_pipstar = True,
+ )
+ config.enable_pipstar().equals(True)
+ config.platforms().contains_exactly({
+ "myplat": struct(
+ name = "myplat",
+ os_name = "linux",
+ arch_name = "x86_64",
+ config_settings = [
+ "@platforms//os:linux",
+ "@platforms//cpu:x86_64",
+ ],
+ env = {},
+ ),
+ })
+
+_tests.append(_test_build_pipstar_platform)
+
def extension_test_suite(name):
"""Create the test suite.