From 4b2d3617e92a792fc21e9025587819fdabbbfe87 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:51:46 +0900 Subject: [PATCH 01/11] refactor: creat a seam --- python/private/pypi/whl_library.bzl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 1e75d69c8d..a7be8ea070 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -609,7 +609,7 @@ way to define whl_library and move whl patching to a separate place. INTERNAL US }, **ATTRS) whl_library_attrs.update(AUTH_ATTRS) -whl_library = repository_rule( +_whl_library = repository_rule( attrs = whl_library_attrs, doc = """ Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. @@ -626,3 +626,16 @@ wheel contents without building an `sdist` first. REPO_DEBUG_ENV_VAR, ], ) + +def whl_library(name, **kwargs): + """Create a whl_library + + Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. + Instantiated from pip_repository and inherits config options from there. + + :::{versionchanged} 1.9.0 + The `whl_library` is marked as reproducible if using starlark to extract and parse the + wheel contents without building an `sdist` first. + ::: + """ + return _whl_library(name = name, **kwargs) From 07f57149b503904cf8a714c2f08b8dd709d9d66a Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:07:28 +0900 Subject: [PATCH 02/11] split the whl_library to archive and non --- python/private/pypi/whl_library.bzl | 186 ++++++++++++++++++++++++++-- 1 file changed, 177 insertions(+), 9 deletions(-) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index a7be8ea070..9f30d2daff 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -308,6 +308,125 @@ def _to_purl(*, index, metadata, filename): return "pkg:pypi/{}@{}?{}".format(name, metadata.version, "&".join(["{}={}".format(key, val) for key, val in qualifiers.items()])) +def _whl_archive_impl(rctx): + logger = repo_utils.logger(rctx) + + whl_path = None + if rctx.attr.whl_file: + rctx.watch(rctx.attr.whl_file) + whl_path = rctx.path(rctx.attr.whl_file) + + # Simulate the behaviour where the whl is present in the current directory. + rctx.symlink(whl_path, whl_path.basename) + whl_path = rctx.path(whl_path.basename) + elif rctx.attr.urls and rctx.attr.filename: + filename = rctx.attr.filename + urls = rctx.attr.urls + urls = [ + urllib.absolute_url( + rctx.attr.index_url, + url, + envsubst = rctx.attr.envsubst, + getenv = rctx.getenv, + ) + for url in urls + ] + result = rctx.download( + url = urls, + output = filename, + sha256 = rctx.attr.sha256, + auth = get_auth(rctx, urls), + ) + if not rctx.attr.sha256: + # this is only seen when there is a direct URL reference without sha256 + logger.warn("Please update the requirement line to include the hash:\n{} \\\n --hash=sha256:{}".format( + rctx.attr.requirement, + result.sha256, + )) + + if not result.success: + fail("could not download the '{}' from {}:\n{}".format(filename, urls, result)) + + if filename.endswith(".whl"): + whl_path = rctx.path(filename) + else: + fail("Only wheels are supported") + + if rctx.attr.whl_patches: + patches = {} + for patch_file, json_args in rctx.attr.whl_patches.items(): + patch_dst = struct(**json.decode(json_args)) + if whl_path.basename in patch_dst.whls: + patches[patch_file] = patch_dst.patch_strip + + if patches: + whl_path = patch_whl( + rctx, + whl_path = whl_path, + patches = patches, + ) + + whl_extract(rctx, whl_path = whl_path, logger = logger) + + install_dir_path = whl_path.dirname.get_child("site-packages") + metadata = whl_metadata( + install_dir = install_dir_path, + read_fn = rctx.read, + logger = logger, + ) + namespace_package_files = pypi_repo_utils.find_namespace_package_files(rctx, install_dir_path) + + entry_points = _get_entry_points(rctx, install_dir_path, metadata) + _move_scripts_needing_shebang_rewrite(rctx, entry_points) + + build_file_contents = generate_whl_library_build_bazel( + name = whl_path.basename, + dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format( + rctx.attr.repo_prefix, + ), + config_load = rctx.attr.config_load, + metadata_name = metadata.name, + metadata_version = metadata.version, + requires_dist = metadata.requires_dist, + # TODO @aignas 2025-05-17: maybe have a build flag for this instead + enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs, + # TODO @aignas 2025-04-14: load through the hub: + annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), + data_exclude = rctx.attr.pip_data_exclude, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, + namespace_package_files = namespace_package_files, + extras = requirement(rctx.attr.requirement).extras, + entry_points = entry_points, + purl = _to_purl( + index = rctx.attr.index_url, + metadata = metadata, + filename = whl_path.basename, + ), + ) + + # Delete these in case the wheel had them. They generally don't cause + # a problem, but let's avoid the chance of that happening. + rctx.file("WORKSPACE") + rctx.file("WORKSPACE.bazel") + rctx.file("MODULE.bazel") + rctx.file("REPO.bazel", """\ +repo( + default_package_metadata = [ + "//:package_metadata", + ], +) +""") + + # BUILD files interfere with globbing and Bazel package boundaries. + _remove_files(rctx, "BUILD", "BUILD.bazel") + rctx.file("BUILD.bazel", build_file_contents) + + if hasattr(rctx, "repo_metadata"): + return rctx.repo_metadata(reproducible = True) + + return None + def _whl_library_impl(rctx): logger = repo_utils.logger(rctx) @@ -609,7 +728,7 @@ way to define whl_library and move whl patching to a separate place. INTERNAL US }, **ATTRS) whl_library_attrs.update(AUTH_ATTRS) -_whl_library = repository_rule( +pip_library = repository_rule( attrs = whl_library_attrs, doc = """ Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. @@ -627,15 +746,64 @@ wheel contents without building an `sdist` first. ], ) +whl_archive_attrs = { + k: whl_library_attrs[k] + for k in [ + "annotation", + "config_load", + "dep_template", + "filename", + "group_deps", + "group_name", + "index_url", + "repo", + "repo_prefix", + "requirement", + "sha256", + "urls", + "whl_file", + "whl_patches", + # common attrs + "enable_implicit_namespace_pkgs", + "envsubst", + "experimental_requirement_cycles", + "extra_hub_aliases", + "pip_data_exclude", + ] +} +whl_archive_attrs.update(AUTH_ATTRS) + +whl_archive = repository_rule( + attrs = whl_archive_attrs, + doc = """ +Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. + +Does not depend on any python. +""", + implementation = _whl_archive_impl, + environ = [ + REPO_DEBUG_ENV_VAR, + ], +) + def whl_library(name, **kwargs): - """Create a whl_library + """Create a whl_library. - Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. - Instantiated from pip_repository and inherits config options from there. + This proxies to one of the underlying implementations: + * {obj}`whl_archive` + * {obj}`pip_archive` - :::{versionchanged} 1.9.0 - The `whl_library` is marked as reproducible if using starlark to extract and parse the - wheel contents without building an `sdist` first. - ::: + Args: + name: {type}`str` The name of the repo. + **kwargs: The args passed to the underlying implementation. + + Returns: + the repo metadata. """ - return _whl_library(name = name, **kwargs) + whl_file = kwargs.get("whl_file") + urls = kwargs.get("urls", []) + filename = kwargs.get("filename") + if whl_file or (urls and filename and filename.endswith(".whl")): + return whl_archive(name = name, **kwargs) + + return pip_library(name = name, **kwargs) From b84a03c3f29710f9f513202d2f3e9e80ad6d90f6 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:10:54 +0900 Subject: [PATCH 03/11] wip: leave only pip in the rule --- python/private/pypi/whl_library.bzl | 92 ++++++++++++----------------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 9f30d2daff..85897bccc5 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -430,18 +430,10 @@ repo( def _whl_library_impl(rctx): logger = repo_utils.logger(rctx) - whl_path = None sdist_filename = None extra_pip_args = [] extra_pip_args.extend(rctx.attr.extra_pip_args) - if rctx.attr.whl_file: - rctx.watch(rctx.attr.whl_file) - whl_path = rctx.path(rctx.attr.whl_file) - - # Simulate the behaviour where the whl is present in the current directory. - rctx.symlink(whl_path, whl_path.basename) - whl_path = rctx.path(whl_path.basename) - elif rctx.attr.urls and rctx.attr.filename: + if rctx.attr.urls and rctx.attr.filename: filename = rctx.attr.filename urls = rctx.attr.urls urls = [ @@ -470,7 +462,7 @@ def _whl_library_impl(rctx): fail("could not download the '{}' from {}:\n{}".format(filename, urls, result)) if filename.endswith(".whl"): - whl_path = rctx.path(filename) + fail("Only sdists are supported") else: sdist_filename = filename @@ -483,53 +475,47 @@ def _whl_library_impl(rctx): # When we already have a wheel, Python isn't used, # so there's no need to setup env vars to run Python, unless we need to # build an sdist or resolve a requirement. - if whl_path: - environment = {} - args = [] - python_interpreter = None - else: - python_interpreter = pypi_repo_utils.resolve_python_interpreter( - rctx, - python_interpreter = rctx.attr.python_interpreter, - python_interpreter_target = rctx.attr.python_interpreter_target, - ) - args = [ - "-m", - "python.private.pypi.whl_installer.wheel_installer", - "--requirement", - rctx.attr.requirement, - ] - args = _parse_optional_attrs(rctx, args, extra_pip_args) + python_interpreter = pypi_repo_utils.resolve_python_interpreter( + rctx, + python_interpreter = rctx.attr.python_interpreter, + python_interpreter_target = rctx.attr.python_interpreter_target, + ) + args = [ + "-m", + "python.private.pypi.whl_installer.wheel_installer", + "--requirement", + rctx.attr.requirement, + ] + args = _parse_optional_attrs(rctx, args, extra_pip_args) - # Manually construct the PYTHONPATH since we cannot use the toolchain here - environment = _create_repository_execution_environment(rctx, python_interpreter, logger = logger) + # Manually construct the PYTHONPATH since we cannot use the toolchain here + environment = _create_repository_execution_environment(rctx, python_interpreter, logger = logger) - if not whl_path: - if rctx.attr.urls: - op_tmpl = "whl_library.BuildWheelFromSource({name}, {requirement})" - elif rctx.attr.download_only: - op_tmpl = "whl_library.DownloadWheel({name}, {requirement})" - else: - op_tmpl = "whl_library.ResolveRequirement({name}, {requirement})" + if rctx.attr.urls: + op_tmpl = "whl_library.BuildWheelFromSource({name}, {requirement})" + elif rctx.attr.download_only: + op_tmpl = "whl_library.DownloadWheel({name}, {requirement})" + else: + op_tmpl = "whl_library.ResolveRequirement({name}, {requirement})" - pypi_repo_utils.execute_checked( - rctx, - # truncate the requirement value when logging it / reporting - # progress since it may contain several ' --hash=sha256:... - # --hash=sha256:...' substrings that fill up the console - python = python_interpreter, - op = op_tmpl.format(name = rctx.attr.name, requirement = rctx.attr.requirement.split(" ", 1)[0]), - arguments = args, - environment = environment, - srcs = rctx.attr._python_srcs, - quiet = rctx.attr.quiet, - timeout = rctx.attr.timeout, - logger = logger, - ) + pypi_repo_utils.execute_checked( + rctx, + # truncate the requirement value when logging it / reporting + # progress since it may contain several ' --hash=sha256:... + # --hash=sha256:...' substrings that fill up the console + python = python_interpreter, + op = op_tmpl.format(name = rctx.attr.name, requirement = rctx.attr.requirement.split(" ", 1)[0]), + arguments = args, + environment = environment, + srcs = rctx.attr._python_srcs, + quiet = rctx.attr.quiet, + timeout = rctx.attr.timeout, + logger = logger, + ) - whl_path = rctx.path(json.decode(rctx.read("whl_file.json"))["whl_file"]) - if not rctx.delete("whl_file.json"): - fail("failed to delete the whl_file.json file") + whl_path = rctx.path(json.decode(rctx.read("whl_file.json"))["whl_file"]) + if not rctx.delete("whl_file.json"): + fail("failed to delete the whl_file.json file") if rctx.attr.whl_patches: patches = {} From b15927355b5a9bcde6c1302846ae332657707a06 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:12:30 +0900 Subject: [PATCH 04/11] wip: leave only pip in the rule --- python/private/pypi/whl_library.bzl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 85897bccc5..88a313c381 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -662,9 +662,6 @@ DEPRECATED. Only left for people who vendor requirements.bzl. The list of urls of the whl to be downloaded using bazel downloader. Using this attr makes `extra_pip_args` and `download_only` ignored.""", ), - "whl_file": attr.label( - doc = "The whl file that should be used instead of downloading or building the whl.", - ), "whl_patches": attr.label_keyed_string_dict( doc = """ A label-keyed-string dict with patch files as keys and json-strings as values. @@ -747,7 +744,6 @@ whl_archive_attrs = { "requirement", "sha256", "urls", - "whl_file", "whl_patches", # common attrs "enable_implicit_namespace_pkgs", @@ -757,6 +753,11 @@ whl_archive_attrs = { "pip_data_exclude", ] } +whl_archive_attrs.update({ + "whl_file": attr.label( + doc = "The whl file that should be used instead of downloading or building the whl.", + ), +}) whl_archive_attrs.update(AUTH_ATTRS) whl_archive = repository_rule( From 5bba4a19184753aa7d12e07e524969a55652c8a2 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:13:30 +0900 Subject: [PATCH 05/11] remove one more attr --- python/private/pypi/whl_library.bzl | 1 - 1 file changed, 1 deletion(-) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 88a313c381..506b6fd241 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -748,7 +748,6 @@ whl_archive_attrs = { # common attrs "enable_implicit_namespace_pkgs", "envsubst", - "experimental_requirement_cycles", "extra_hub_aliases", "pip_data_exclude", ] From 483f77b46b474792e21a1e11455befc981aa34fb Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:07:36 +0900 Subject: [PATCH 06/11] condense the code --- python/private/pypi/whl_library.bzl | 171 +++++++++------------------- 1 file changed, 51 insertions(+), 120 deletions(-) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 506b6fd241..635d3e68b7 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -308,50 +308,7 @@ def _to_purl(*, index, metadata, filename): return "pkg:pypi/{}@{}?{}".format(name, metadata.version, "&".join(["{}={}".format(key, val) for key, val in qualifiers.items()])) -def _whl_archive_impl(rctx): - logger = repo_utils.logger(rctx) - - whl_path = None - if rctx.attr.whl_file: - rctx.watch(rctx.attr.whl_file) - whl_path = rctx.path(rctx.attr.whl_file) - - # Simulate the behaviour where the whl is present in the current directory. - rctx.symlink(whl_path, whl_path.basename) - whl_path = rctx.path(whl_path.basename) - elif rctx.attr.urls and rctx.attr.filename: - filename = rctx.attr.filename - urls = rctx.attr.urls - urls = [ - urllib.absolute_url( - rctx.attr.index_url, - url, - envsubst = rctx.attr.envsubst, - getenv = rctx.getenv, - ) - for url in urls - ] - result = rctx.download( - url = urls, - output = filename, - sha256 = rctx.attr.sha256, - auth = get_auth(rctx, urls), - ) - if not rctx.attr.sha256: - # this is only seen when there is a direct URL reference without sha256 - logger.warn("Please update the requirement line to include the hash:\n{} \\\n --hash=sha256:{}".format( - rctx.attr.requirement, - result.sha256, - )) - - if not result.success: - fail("could not download the '{}' from {}:\n{}".format(filename, urls, result)) - - if filename.endswith(".whl"): - whl_path = rctx.path(filename) - else: - fail("Only wheels are supported") - +def _whl_extract(rctx, *, whl_path, logger, sdist_filename = None): if rctx.attr.whl_patches: patches = {} for patch_file, json_args in rctx.attr.whl_patches.items(): @@ -384,6 +341,7 @@ def _whl_archive_impl(rctx): dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format( rctx.attr.repo_prefix, ), + sdist_filename = sdist_filename, config_load = rctx.attr.config_load, metadata_name = metadata.name, metadata_version = metadata.version, @@ -401,7 +359,7 @@ def _whl_archive_impl(rctx): purl = _to_purl( index = rctx.attr.index_url, metadata = metadata, - filename = whl_path.basename, + filename = sdist_filename or whl_path.basename, ), ) @@ -427,6 +385,52 @@ repo( return None +def _whl_archive_impl(rctx): + logger = repo_utils.logger(rctx) + + whl_path = None + if rctx.attr.whl_file: + rctx.watch(rctx.attr.whl_file) + whl_path = rctx.path(rctx.attr.whl_file) + + # Simulate the behaviour where the whl is present in the current directory. + rctx.symlink(whl_path, whl_path.basename) + whl_path = rctx.path(whl_path.basename) + elif rctx.attr.urls and rctx.attr.filename: + filename = rctx.attr.filename + urls = rctx.attr.urls + urls = [ + urllib.absolute_url( + rctx.attr.index_url, + url, + envsubst = rctx.attr.envsubst, + getenv = rctx.getenv, + ) + for url in urls + ] + result = rctx.download( + url = urls, + output = filename, + sha256 = rctx.attr.sha256, + auth = get_auth(rctx, urls), + ) + if not rctx.attr.sha256: + # this is only seen when there is a direct URL reference without sha256 + logger.warn("Please update the requirement line to include the hash:\n{} \\\n --hash=sha256:{}".format( + rctx.attr.requirement, + result.sha256, + )) + + if not result.success: + fail("could not download the '{}' from {}:\n{}".format(filename, urls, result)) + + if filename.endswith(".whl"): + whl_path = rctx.path(filename) + else: + fail("Only wheels are supported") + + return _whl_extract(rctx, whl_path = whl_path, logger = logger) + def _whl_library_impl(rctx): logger = repo_utils.logger(rctx) @@ -517,81 +521,7 @@ def _whl_library_impl(rctx): if not rctx.delete("whl_file.json"): fail("failed to delete the whl_file.json file") - if rctx.attr.whl_patches: - patches = {} - for patch_file, json_args in rctx.attr.whl_patches.items(): - patch_dst = struct(**json.decode(json_args)) - if whl_path.basename in patch_dst.whls: - patches[patch_file] = patch_dst.patch_strip - - if patches: - whl_path = patch_whl( - rctx, - whl_path = whl_path, - patches = patches, - ) - - whl_extract(rctx, whl_path = whl_path, logger = logger) - - install_dir_path = whl_path.dirname.get_child("site-packages") - metadata = whl_metadata( - install_dir = install_dir_path, - read_fn = rctx.read, - logger = logger, - ) - namespace_package_files = pypi_repo_utils.find_namespace_package_files(rctx, install_dir_path) - - entry_points = _get_entry_points(rctx, install_dir_path, metadata) - _move_scripts_needing_shebang_rewrite(rctx, entry_points) - - build_file_contents = generate_whl_library_build_bazel( - name = whl_path.basename, - sdist_filename = sdist_filename, - dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format( - rctx.attr.repo_prefix, - ), - config_load = rctx.attr.config_load, - metadata_name = metadata.name, - metadata_version = metadata.version, - requires_dist = metadata.requires_dist, - # TODO @aignas 2025-05-17: maybe have a build flag for this instead - enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs, - # TODO @aignas 2025-04-14: load through the hub: - annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), - data_exclude = rctx.attr.pip_data_exclude, - group_deps = rctx.attr.group_deps, - group_name = rctx.attr.group_name, - namespace_package_files = namespace_package_files, - extras = requirement(rctx.attr.requirement).extras, - entry_points = entry_points, - purl = _to_purl( - index = rctx.attr.index_url, - metadata = metadata, - filename = sdist_filename or whl_path.basename, - ), - ) - - # Delete these in case the wheel had them. They generally don't cause - # a problem, but let's avoid the chance of that happening. - rctx.file("WORKSPACE") - rctx.file("WORKSPACE.bazel") - rctx.file("MODULE.bazel") - rctx.file("REPO.bazel", """\ -repo( - default_package_metadata = [ - "//:package_metadata", - ], -) -""") - - # BUILD files interfere with globbing and Bazel package boundaries. - _remove_files(rctx, "BUILD", "BUILD.bazel") - rctx.file("BUILD.bazel", build_file_contents) - - if hasattr(rctx, "repo_metadata"): - return rctx.repo_metadata(reproducible = True) - - return None + return _whl_extract(rctx, whl_path = whl_path, logger = logger, sdist_filename = sdist_filename) def _remove_files(rctx, *basenames): paths = list(rctx.path(".").readdir()) @@ -748,6 +678,7 @@ whl_archive_attrs = { # common attrs "enable_implicit_namespace_pkgs", "envsubst", + "experimental_requirement_cycles", "extra_hub_aliases", "pip_data_exclude", ] From 5b0f4be233c499efe17c712941a601d2da911532 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:09:38 +0900 Subject: [PATCH 07/11] add a note --- python/private/pypi/whl_library.bzl | 1 + 1 file changed, 1 insertion(+) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 635d3e68b7..ba118f5b34 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -309,6 +309,7 @@ def _to_purl(*, index, metadata, filename): return "pkg:pypi/{}@{}?{}".format(name, metadata.version, "&".join(["{}={}".format(key, val) for key, val in qualifiers.items()])) def _whl_extract(rctx, *, whl_path, logger, sdist_filename = None): + """Extract the wheel, apply patches and generate BUILD.bazel files.""" if rctx.attr.whl_patches: patches = {} for patch_file, json_args in rctx.attr.whl_patches.items(): From c90bcb62b8a4ca6dcb543592fe8a755bc9ba6ed5 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:11:41 +0900 Subject: [PATCH 08/11] fixup --- python/private/pypi/whl_library.bzl | 70 ++++++++++++++--------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index ba118f5b34..fdc31b8d8b 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -537,7 +537,7 @@ def _remove_files(rctx, *basenames): paths.extend(path.readdir()) # NOTE @aignas 2024-03-21: The usage of dict({}, **common) ensures that all args to `dict` are unique -whl_library_attrs = dict({ +_pip_archive_attrs = dict({ "annotation": attr.label( doc = ( "Optional json encoded file containing annotation to apply to the extracted wheel. " + @@ -640,10 +640,10 @@ way to define whl_library and move whl patching to a separate place. INTERNAL US ), "_rule_name": attr.string(default = "whl_library"), }, **ATTRS) -whl_library_attrs.update(AUTH_ATTRS) +_pip_archive_attrs.update(AUTH_ATTRS) -pip_library = repository_rule( - attrs = whl_library_attrs, +pip_archive = repository_rule( + attrs = _pip_archive_attrs, doc = """ Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. Instantiated from pip_repository and inherits config options from there. @@ -660,39 +660,35 @@ wheel contents without building an `sdist` first. ], ) -whl_archive_attrs = { - k: whl_library_attrs[k] - for k in [ - "annotation", - "config_load", - "dep_template", - "filename", - "group_deps", - "group_name", - "index_url", - "repo", - "repo_prefix", - "requirement", - "sha256", - "urls", - "whl_patches", - # common attrs - "enable_implicit_namespace_pkgs", - "envsubst", - "experimental_requirement_cycles", - "extra_hub_aliases", - "pip_data_exclude", - ] -} -whl_archive_attrs.update({ - "whl_file": attr.label( - doc = "The whl file that should be used instead of downloading or building the whl.", - ), -}) -whl_archive_attrs.update(AUTH_ATTRS) - whl_archive = repository_rule( - attrs = whl_archive_attrs, + attrs = { + k: _pip_archive_attrs[k] + for k in [ + "annotation", + "config_load", + "dep_template", + "filename", + "group_deps", + "group_name", + "index_url", + "repo", + "repo_prefix", + "requirement", + "sha256", + "urls", + "whl_patches", + # common attrs + "enable_implicit_namespace_pkgs", + "envsubst", + "experimental_requirement_cycles", + "extra_hub_aliases", + "pip_data_exclude", + ] + } | { + "whl_file": attr.label( + doc = "The whl file that should be used instead of downloading or building the whl.", + ), + } | AUTH_ATTRS, doc = """ Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. @@ -724,4 +720,4 @@ def whl_library(name, **kwargs): if whl_file or (urls and filename and filename.endswith(".whl")): return whl_archive(name = name, **kwargs) - return pip_library(name = name, **kwargs) + return pip_archive(name = name, **kwargs) From 5f3918dc07ff794012bd4be7918f59db3d1cea26 Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:13:20 +0900 Subject: [PATCH 09/11] add a note to pip_archive --- python/private/pypi/whl_library.bzl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index fdc31b8d8b..30ac4f48a7 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -652,6 +652,11 @@ Instantiated from pip_repository and inherits config options from there. The `whl_library` is marked as reproducible if using starlark to extract and parse the wheel contents without building an `sdist` first. ::: + +:::{versionchanged} VERSION_NEXT_FEATURE +The whl-only pure Starlark operations have been refactored into {obj}`whl_archive` and the +previously named {obj}`whl_library` repository became renamed to `pip_archive`. +::: """, implementation = _whl_library_impl, environ = [ From db4f499eb368b4fbe9e2d5f07819bddf2817621b Mon Sep 17 00:00:00 2001 From: Ignas Anikevicius <240938+aignas@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:16:11 +0900 Subject: [PATCH 10/11] remove the unused args from whl_library --- python/private/pypi/attrs.bzl | 73 -------------------- python/private/pypi/pip_repository_attrs.bzl | 73 ++++++++++++++++++++ python/private/pypi/whl_library.bzl | 2 - 3 files changed, 73 insertions(+), 75 deletions(-) diff --git a/python/private/pypi/attrs.bzl b/python/private/pypi/attrs.bzl index 57bd93f40a..271c17e35c 100644 --- a/python/private/pypi/attrs.bzl +++ b/python/private/pypi/attrs.bzl @@ -64,79 +64,6 @@ here do not cause packages to be re-fetched. Don't fetch different things based on the value of these variables. """, ), - "experimental_requirement_cycles": attr.string_list_dict( - default = {}, - doc = """\ -A mapping of dependency cycle names to a list of requirements which form that cycle. - -Requirements which form cycles will be installed together and taken as -dependencies together in order to ensure that the cycle is always satisified. - -Example: - `sphinx` depends on `sphinxcontrib-serializinghtml` - When listing both as requirements, ala - - ``` - py_binary( - name = "doctool", - ... - deps = [ - "@pypi//sphinx:pkg", - "@pypi//sphinxcontrib_serializinghtml", - ] - ) - ``` - - Will produce a Bazel error such as - - ``` - ERROR: .../external/pypi_sphinxcontrib_serializinghtml/BUILD.bazel:44:6: in alias rule @pypi_sphinxcontrib_serializinghtml//:pkg: cycle in dependency graph: - //:doctool (...) - @pypi//sphinxcontrib_serializinghtml:pkg (...) - .-> @pypi_sphinxcontrib_serializinghtml//:pkg (...) - | @pypi_sphinxcontrib_serializinghtml//:_pkg (...) - | @pypi_sphinx//:pkg (...) - | @pypi_sphinx//:_pkg (...) - `-- @pypi_sphinxcontrib_serializinghtml//:pkg (...) - ``` - - Which we can resolve by configuring these two requirements to be installed together as a cycle - - ``` - pip_parse( - ... - experimental_requirement_cycles = { - "sphinx": [ - "sphinx", - "sphinxcontrib-serializinghtml", - ] - }, - ) - ``` - -Warning: - If a dependency participates in multiple cycles, all of those cycles must be - collapsed down to one. For instance `a <-> b` and `a <-> c` cannot be listed - as two separate cycles. -""", - ), - "extra_hub_aliases": attr.string_list_dict( - doc = """\ -Extra aliases to make for specific wheels in the hub repo. This is useful when -paired with the {attr}`whl_modifications`. - -:::{versionadded} 0.38.0 - -For `pip.parse` with bzlmod -::: - -:::{versionadded} 1.0.0 - -For `pip_parse` with workspace. -::: -""", - mandatory = False, - ), "extra_pip_args": attr.string_list( doc = """Extra arguments to pass on to pip. Must not contain spaces. diff --git a/python/private/pypi/pip_repository_attrs.bzl b/python/private/pypi/pip_repository_attrs.bzl index 23000869e9..4f0b26448b 100644 --- a/python/private/pypi/pip_repository_attrs.bzl +++ b/python/private/pypi/pip_repository_attrs.bzl @@ -21,6 +21,79 @@ repositories.""" load(":attrs.bzl", COMMON_ATTRS = "ATTRS") ATTRS = { + "experimental_requirement_cycles": attr.string_list_dict( + default = {}, + doc = """\ +A mapping of dependency cycle names to a list of requirements which form that cycle. + +Requirements which form cycles will be installed together and taken as +dependencies together in order to ensure that the cycle is always satisified. + +Example: + `sphinx` depends on `sphinxcontrib-serializinghtml` + When listing both as requirements, ala + + ``` + py_binary( + name = "doctool", + ... + deps = [ + "@pypi//sphinx:pkg", + "@pypi//sphinxcontrib_serializinghtml", + ] + ) + ``` + + Will produce a Bazel error such as + + ``` + ERROR: .../external/pypi_sphinxcontrib_serializinghtml/BUILD.bazel:44:6: in alias rule @pypi_sphinxcontrib_serializinghtml//:pkg: cycle in dependency graph: + //:doctool (...) + @pypi//sphinxcontrib_serializinghtml:pkg (...) + .-> @pypi_sphinxcontrib_serializinghtml//:pkg (...) + | @pypi_sphinxcontrib_serializinghtml//:_pkg (...) + | @pypi_sphinx//:pkg (...) + | @pypi_sphinx//:_pkg (...) + `-- @pypi_sphinxcontrib_serializinghtml//:pkg (...) + ``` + + Which we can resolve by configuring these two requirements to be installed together as a cycle + + ``` + pip_parse( + ... + experimental_requirement_cycles = { + "sphinx": [ + "sphinx", + "sphinxcontrib-serializinghtml", + ] + }, + ) + ``` + +Warning: + If a dependency participates in multiple cycles, all of those cycles must be + collapsed down to one. For instance `a <-> b` and `a <-> c` cannot be listed + as two separate cycles. +""", + ), + "extra_hub_aliases": attr.string_list_dict( + doc = """\ +Extra aliases to make for specific wheels in the hub repo. This is useful when +paired with the {attr}`whl_modifications`. + +:::{versionadded} 0.38.0 + +For `pip.parse` with bzlmod +::: + +:::{versionadded} 1.0.0 + +For `pip_parse` with workspace. +::: +""", + mandatory = False, + ), "requirements_by_platform": attr.label_keyed_string_dict( doc = """\ The requirements files and the comma delimited list of target platforms as values. diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 30ac4f48a7..b1d3dc0fe5 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -685,8 +685,6 @@ whl_archive = repository_rule( # common attrs "enable_implicit_namespace_pkgs", "envsubst", - "experimental_requirement_cycles", - "extra_hub_aliases", "pip_data_exclude", ] } | { From 546b87ac28e14df404be8ad4c0047a8df5167f6f Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 24 Jul 2026 16:21:01 +0000 Subject: [PATCH 11/11] refactor(pypi): rename _whl_library_impl to _pip_archive_impl Rename internal repository implementation function _whl_library_impl to _pip_archive_impl to match the pip_archive repository rule name. --- python/private/pypi/whl_library.bzl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index b1d3dc0fe5..b66e502681 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -432,7 +432,7 @@ def _whl_archive_impl(rctx): return _whl_extract(rctx, whl_path = whl_path, logger = logger) -def _whl_library_impl(rctx): +def _pip_archive_impl(rctx): logger = repo_utils.logger(rctx) sdist_filename = None @@ -658,7 +658,7 @@ The whl-only pure Starlark operations have been refactored into {obj}`whl_archiv previously named {obj}`whl_library` repository became renamed to `pip_archive`. ::: """, - implementation = _whl_library_impl, + implementation = _pip_archive_impl, environ = [ "RULES_PYTHON_PIP_ISOLATED", REPO_DEBUG_ENV_VAR,