refactor: move PyPI related extension and repository_rule setup to its own dir (#2003)
This is to ensure that future work is easier and its clear where to add
tests.
Now all of the unit tests can be run by just `bazel test
//tests/pypi/...`.
Refactor summary:
- chore: add aignas to CODEOWNERS
- chore: add a new directory for storing PyPI related code
- move pypi_index_sources.bzl to private/pypi
- chore: move parse_requirements_txt to private/pypi
- move parse_whl_name to private/pypi
- move whl_target_platforms to private/pypi
- move parse_requirements to private/pypi
- move pip_repo_name to private/pypi
- remove unused file
- move pip_config_settings to private/pypi
- move pypi_index to pypi/private and rename
- move labels.bzl to private/pypi
- move generate_build_bazel to private/pypi
- move render_pkg_aliases.bzl to private/pypi
- move patch_whl.bzl to private/pypi
- Move envsubst and render_tests to top level of tests
- move pip_install_srcs to private/pypi
- split and move pip_install/pip_repository.bzl to private/pypi
- move the bzlmod extension to private/pypi
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index cbf2996..29b495b 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -101,7 +101,7 @@
"//python/pip_install:requirements_bzl",
"//python/private:bzlmod_enabled_bzl",
"//python/private:full_version_bzl",
- "//python/private:render_pkg_aliases_bzl",
+ "//python/private/pypi:render_pkg_aliases_bzl",
"//python/private/whl_filegroup:whl_filegroup_bzl",
],
)
diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel
index 1003efb..f6f46db 100644
--- a/python/config_settings/BUILD.bazel
+++ b/python/config_settings/BUILD.bazel
@@ -9,7 +9,7 @@
"PycCollectionFlag",
)
load(
- "//python/private:pip_flags.bzl",
+ "//python/private/pypi:flags.bzl",
"INTERNAL_FLAGS",
"UniversalWhlFlag",
"UseWhlFlag",
diff --git a/python/extensions/BUILD.bazel b/python/extensions/BUILD.bazel
index eb095ab..1bc2a71 100644
--- a/python/extensions/BUILD.bazel
+++ b/python/extensions/BUILD.bazel
@@ -36,8 +36,6 @@
srcs = ["python.bzl"],
visibility = ["//:__subpackages__"],
deps = [
- "//python/private:util_bzl",
- "//python/private/bzlmod:bazel_features_bzl",
"//python/private/bzlmod:python_bzl",
],
)
diff --git a/python/pip.bzl b/python/pip.bzl
index 8cc091d..f1c74dd 100644
--- a/python/pip.bzl
+++ b/python/pip.bzl
@@ -19,12 +19,13 @@
for internal use only.
"""
-load("//python/pip_install:pip_repository.bzl", "pip_repository", _package_annotation = "package_annotation")
load("//python/pip_install:requirements.bzl", _compile_pip_requirements = "compile_pip_requirements")
load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
load("//python/private:full_version.bzl", "full_version")
load("//python/private:normalize_name.bzl", "normalize_name")
-load("//python/private:render_pkg_aliases.bzl", "NO_MATCH_ERROR_MESSAGE_TEMPLATE")
+load("//python/private/pypi:package_annotation.bzl", _package_annotation = "package_annotation")
+load("//python/private/pypi:pip_repository.bzl", "pip_repository")
+load("//python/private/pypi:render_pkg_aliases.bzl", "NO_MATCH_ERROR_MESSAGE_TEMPLATE")
load("//python/private/whl_filegroup:whl_filegroup.bzl", _whl_filegroup = "whl_filegroup")
compile_pip_requirements = _compile_pip_requirements
diff --git a/python/pip_install/BUILD.bazel b/python/pip_install/BUILD.bazel
index 91f2ec7..1894c4d 100644
--- a/python/pip_install/BUILD.bazel
+++ b/python/pip_install/BUILD.bazel
@@ -22,23 +22,10 @@
name = "pip_repository_bzl",
srcs = ["pip_repository.bzl"],
deps = [
- ":repositories_bzl",
- "//python:repositories_bzl",
- "//python:versions_bzl",
- "//python/pip_install/private:generate_group_library_build_bazel_bzl",
- "//python/pip_install/private:generate_whl_library_build_bazel_bzl",
- "//python/pip_install/private:srcs_bzl",
- "//python/private:bzlmod_enabled_bzl",
- "//python/private:envsubst_bzl",
- "//python/private:normalize_name_bzl",
- "//python/private:parse_requirements_bzl",
- "//python/private:parse_whl_name_bzl",
- "//python/private:patch_whl_bzl",
- "//python/private:render_pkg_aliases_bzl",
- "//python/private:repo_utils_bzl",
- "//python/private:toolchains_repo_bzl",
- "//python/private:whl_target_platforms_bzl",
- "@bazel_skylib//lib:sets",
+ "//python/private/pypi:group_library_bzl",
+ "//python/private/pypi:package_annotation_bzl",
+ "//python/private/pypi:pip_repository_bzl",
+ "//python/private/pypi:whl_library_bzl",
],
)
@@ -52,11 +39,6 @@
)
bzl_library(
- name = "requirements_parser_bzl",
- srcs = ["requirements_parser.bzl"],
-)
-
-bzl_library(
name = "repositories_bzl",
srcs = ["repositories.bzl"],
deps = [
@@ -71,7 +53,6 @@
srcs = glob(["*.bzl"]) + [
"BUILD.bazel",
"pip_repository_requirements.bzl.tmpl",
- "//python/pip_install/private:distribution",
"//python/pip_install/tools/dependency_resolver:distribution",
"//python/pip_install/tools/wheel_installer:distribution",
],
@@ -92,23 +73,10 @@
filegroup(
name = "bzl",
- srcs = glob(["*.bzl"]) + [
- "//python/pip_install/private:bzl_srcs",
- ],
+ srcs = glob(["*.bzl"]),
visibility = ["//:__subpackages__"],
)
-filegroup(
- name = "py_srcs",
- srcs = [
- "//python/pip_install/tools/dependency_resolver:py_srcs",
- "//python/pip_install/tools/wheel_installer:py_srcs",
- "//python/private:repack_whl.py",
- "//tools:wheelmaker.py",
- ],
- visibility = ["//python/pip_install/private:__pkg__"],
-)
-
exports_files(
glob(["*.bzl"]),
visibility = ["//docs:__pkg__"],
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index d6c8d91..18deee1 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -14,1099 +14,13 @@
""
-load("@bazel_skylib//lib:sets.bzl", "sets")
-load("//python:repositories.bzl", "is_standalone_interpreter")
-load("//python:versions.bzl", "WINDOWS_NAME")
-load("//python/pip_install:repositories.bzl", "all_requirements")
-load("//python/pip_install/private:generate_group_library_build_bazel.bzl", "generate_group_library_build_bazel")
-load("//python/pip_install/private:generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel")
-load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
-load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth")
-load("//python/private:envsubst.bzl", "envsubst")
-load("//python/private:normalize_name.bzl", "normalize_name")
-load("//python/private:parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement")
-load("//python/private:parse_whl_name.bzl", "parse_whl_name")
-load("//python/private:patch_whl.bzl", "patch_whl")
-load("//python/private:render_pkg_aliases.bzl", "render_pkg_aliases", "whl_alias")
-load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
-load("//python/private:toolchains_repo.bzl", "get_host_os_arch")
-load("//python/private:whl_target_platforms.bzl", "whl_target_platforms")
+load("//python/private/pypi:group_library.bzl", _group_library = "group_library")
+load("//python/private/pypi:package_annotation.bzl", _package_annotation = "package_annotation")
+load("//python/private/pypi:pip_repository.bzl", _pip_repository = "pip_repository")
+load("//python/private/pypi:whl_library.bzl", _whl_library = "whl_library")
-CPPFLAGS = "CPPFLAGS"
-
-COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools"
-
-_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
-
-def _construct_pypath(rctx):
- """Helper function to construct a PYTHONPATH.
-
- Contains entries for code in this repo as well as packages downloaded from //python/pip_install:repositories.bzl.
- This allows us to run python code inside repository rule implementations.
-
- Args:
- rctx: Handle to the repository_context.
-
- Returns: String of the PYTHONPATH.
- """
-
- separator = ":" if not "windows" in rctx.os.name.lower() else ";"
- pypath = separator.join([
- str(rctx.path(entry).dirname)
- for entry in rctx.attr._python_path_entries
- ])
- return pypath
-
-def _get_python_interpreter_attr(rctx):
- """A helper function for getting the `python_interpreter` attribute or it's default
-
- Args:
- rctx (repository_ctx): Handle to the rule repository context.
-
- Returns:
- str: The attribute value or it's default
- """
- if rctx.attr.python_interpreter:
- return rctx.attr.python_interpreter
-
- if "win" in rctx.os.name:
- return "python.exe"
- else:
- return "python3"
-
-def _resolve_python_interpreter(rctx):
- """Helper function to find the python interpreter from the common attributes
-
- Args:
- rctx: Handle to the rule repository context.
-
- Returns:
- `path` object, for the resolved path to the Python interpreter.
- """
- python_interpreter = _get_python_interpreter_attr(rctx)
-
- if rctx.attr.python_interpreter_target != None:
- python_interpreter = rctx.path(rctx.attr.python_interpreter_target)
-
- (os, _) = get_host_os_arch(rctx)
-
- # On Windows, the symlink doesn't work because Windows attempts to find
- # Python DLLs where the symlink is, not where the symlink points.
- if os == WINDOWS_NAME:
- python_interpreter = python_interpreter.realpath
- elif "/" not in python_interpreter:
- # It's a plain command, e.g. "python3", to look up in the environment.
- found_python_interpreter = rctx.which(python_interpreter)
- if not found_python_interpreter:
- fail("python interpreter `{}` not found in PATH".format(python_interpreter))
- python_interpreter = found_python_interpreter
- else:
- python_interpreter = rctx.path(python_interpreter)
- return python_interpreter
-
-def _get_xcode_location_cflags(rctx):
- """Query the xcode sdk location to update cflags
-
- Figure out if this interpreter target comes from rules_python, and patch the xcode sdk location if so.
- Pip won't be able to compile c extensions from sdists with the pre built python distributions from indygreg
- otherwise. See https://github.com/indygreg/python-build-standalone/issues/103
- """
-
- # Only run on MacOS hosts
- if not rctx.os.name.lower().startswith("mac os"):
- return []
-
- xcode_sdk_location = repo_utils.execute_unchecked(
- rctx,
- op = "GetXcodeLocation",
- arguments = [repo_utils.which_checked(rctx, "xcode-select"), "--print-path"],
- )
- if xcode_sdk_location.return_code != 0:
- return []
-
- xcode_root = xcode_sdk_location.stdout.strip()
- if COMMAND_LINE_TOOLS_PATH_SLUG not in xcode_root.lower():
- # This is a full xcode installation somewhere like /Applications/Xcode13.0.app/Contents/Developer
- # so we need to change the path to to the macos specific tools which are in a different relative
- # path than xcode installed command line tools.
- xcode_root = "{}/Platforms/MacOSX.platform/Developer".format(xcode_root)
- return [
- "-isysroot {}/SDKs/MacOSX.sdk".format(xcode_root),
- ]
-
-def _get_toolchain_unix_cflags(rctx, python_interpreter):
- """Gather cflags from a standalone toolchain for unix systems.
-
- Pip won't be able to compile c extensions from sdists with the pre built python distributions from indygreg
- otherwise. See https://github.com/indygreg/python-build-standalone/issues/103
- """
-
- # Only run on Unix systems
- if not rctx.os.name.lower().startswith(("mac os", "linux")):
- return []
-
- # Only update the location when using a standalone toolchain.
- if not is_standalone_interpreter(rctx, python_interpreter):
- return []
-
- stdout = repo_utils.execute_checked_stdout(
- rctx,
- op = "GetPythonVersionForUnixCflags",
- arguments = [
- python_interpreter,
- "-c",
- "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}', end='')",
- ],
- )
- _python_version = stdout
- include_path = "{}/include/python{}".format(
- python_interpreter.dirname,
- _python_version,
- )
-
- return ["-isystem {}".format(include_path)]
-
-def use_isolated(ctx, attr):
- """Determine whether or not to pass the pip `--isolated` flag to the pip invocation.
-
- Args:
- ctx: repository or module context
- attr: attributes for the repo rule or tag extension
-
- Returns:
- True if --isolated should be passed
- """
- use_isolated = attr.isolated
-
- # The environment variable will take precedence over the attribute
- isolated_env = ctx.os.environ.get("RULES_PYTHON_PIP_ISOLATED", None)
- if isolated_env != None:
- if isolated_env.lower() in ("0", "false"):
- use_isolated = False
- else:
- use_isolated = True
-
- return use_isolated
-
-def _parse_optional_attrs(rctx, args, extra_pip_args = None):
- """Helper function to parse common attributes of pip_repository and whl_library repository rules.
-
- This function also serializes the structured arguments as JSON
- so they can be passed on the command line to subprocesses.
-
- Args:
- rctx: Handle to the rule repository context.
- args: A list of parsed args for the rule.
- extra_pip_args: The pip args to pass.
- Returns: Augmented args list.
- """
-
- if use_isolated(rctx, rctx.attr):
- args.append("--isolated")
-
- # Bazel version 7.1.0 and later (and rolling releases from version 8.0.0-pre.20240128.3)
- # support rctx.getenv(name, default): When building incrementally, any change to the value of
- # the variable named by name will cause this repository to be re-fetched.
- if "getenv" in dir(rctx):
- getenv = rctx.getenv
- else:
- getenv = rctx.os.environ.get
-
- # Check for None so we use empty default types from our attrs.
- # Some args want to be list, and some want to be dict.
- if extra_pip_args != None:
- args += [
- "--extra_pip_args",
- json.encode(struct(arg = [
- envsubst(pip_arg, rctx.attr.envsubst, getenv)
- for pip_arg in rctx.attr.extra_pip_args
- ])),
- ]
-
- if rctx.attr.download_only:
- args.append("--download_only")
-
- if rctx.attr.pip_data_exclude != None:
- args += [
- "--pip_data_exclude",
- json.encode(struct(arg = rctx.attr.pip_data_exclude)),
- ]
-
- if rctx.attr.enable_implicit_namespace_pkgs:
- args.append("--enable_implicit_namespace_pkgs")
-
- if rctx.attr.environment != None:
- args += [
- "--environment",
- json.encode(struct(arg = rctx.attr.environment)),
- ]
-
- return args
-
-def _create_repository_execution_environment(rctx, python_interpreter):
- """Create a environment dictionary for processes we spawn with rctx.execute.
-
- Args:
- rctx (repository_ctx): The repository context.
- python_interpreter (path): The resolved python interpreter.
- Returns:
- Dictionary of environment variable suitable to pass to rctx.execute.
- """
-
- # Gather any available CPPFLAGS values
- cppflags = []
- cppflags.extend(_get_xcode_location_cflags(rctx))
- cppflags.extend(_get_toolchain_unix_cflags(rctx, python_interpreter))
-
- env = {
- "PYTHONPATH": _construct_pypath(rctx),
- CPPFLAGS: " ".join(cppflags),
- }
-
- return env
-
-_BUILD_FILE_CONTENTS = """\
-package(default_visibility = ["//visibility:public"])
-
-# Ensure the `requirements.bzl` source can be accessed by stardoc, since users load() from it
-exports_files(["requirements.bzl"])
-"""
-
-def _pip_repository_impl(rctx):
- requirements_by_platform = parse_requirements(
- rctx,
- requirements_by_platform = rctx.attr.requirements_by_platform,
- requirements_linux = rctx.attr.requirements_linux,
- requirements_lock = rctx.attr.requirements_lock,
- requirements_osx = rctx.attr.requirements_darwin,
- requirements_windows = rctx.attr.requirements_windows,
- extra_pip_args = rctx.attr.extra_pip_args,
- )
- selected_requirements = {}
- options = None
- repository_platform = host_platform(rctx.os)
- for name, requirements in requirements_by_platform.items():
- r = select_requirement(
- requirements,
- platform = repository_platform,
- )
- if not r:
- continue
- options = options or r.extra_pip_args
- selected_requirements[name] = r.requirement_line
-
- bzl_packages = sorted(selected_requirements.keys())
-
- # Normalize cycles first
- requirement_cycles = {
- name: sorted(sets.to_list(sets.make(deps)))
- for name, deps in rctx.attr.experimental_requirement_cycles.items()
- }
-
- # Check for conflicts between cycles _before_ we normalize package names so
- # that reported errors use the names the user specified
- for i in range(len(requirement_cycles)):
- left_group = requirement_cycles.keys()[i]
- left_deps = requirement_cycles.values()[i]
- for j in range(len(requirement_cycles) - (i + 1)):
- right_deps = requirement_cycles.values()[1 + i + j]
- right_group = requirement_cycles.keys()[1 + i + j]
- for d in left_deps:
- if d in right_deps:
- fail("Error: Requirement %s cannot be repeated between cycles %s and %s; please merge the cycles." % (d, left_group, right_group))
-
- # And normalize the names as used in the cycle specs
- #
- # NOTE: We must check that a listed dependency is actually in the actual
- # requirements set for the current platform so that we can support cycles in
- # platform-conditional requirements. Otherwise we'll blindly generate a
- # label referencing a package which may not be installed on the current
- # platform.
- requirement_cycles = {
- normalize_name(name): sorted([normalize_name(d) for d in group if normalize_name(d) in bzl_packages])
- for name, group in requirement_cycles.items()
- }
-
- imports = [
- # NOTE: Maintain the order consistent with `buildifier`
- 'load("@rules_python//python:pip.bzl", "pip_utils")',
- 'load("@rules_python//python/pip_install:pip_repository.bzl", "group_library", "whl_library")',
- ]
-
- annotations = {}
- for pkg, annotation in rctx.attr.annotations.items():
- filename = "{}.annotation.json".format(normalize_name(pkg))
- rctx.file(filename, json.encode_indent(json.decode(annotation)))
- annotations[pkg] = "@{name}//:{filename}".format(name = rctx.attr.name, filename = filename)
-
- config = {
- "download_only": rctx.attr.download_only,
- "enable_implicit_namespace_pkgs": rctx.attr.enable_implicit_namespace_pkgs,
- "environment": rctx.attr.environment,
- "envsubst": rctx.attr.envsubst,
- "extra_pip_args": options,
- "isolated": use_isolated(rctx, rctx.attr),
- "pip_data_exclude": rctx.attr.pip_data_exclude,
- "python_interpreter": _get_python_interpreter_attr(rctx),
- "quiet": rctx.attr.quiet,
- "repo": rctx.attr.name,
- "timeout": rctx.attr.timeout,
- }
- if rctx.attr.use_hub_alias_dependencies:
- config["dep_template"] = "@{}//{{name}}:{{target}}".format(rctx.attr.name)
- else:
- config["repo_prefix"] = "{}_".format(rctx.attr.name)
-
- if rctx.attr.python_interpreter_target:
- config["python_interpreter_target"] = str(rctx.attr.python_interpreter_target)
- if rctx.attr.experimental_target_platforms:
- config["experimental_target_platforms"] = rctx.attr.experimental_target_platforms
-
- macro_tmpl = "@%s//{}:{}" % rctx.attr.name
-
- aliases = render_pkg_aliases(
- aliases = {
- pkg: [whl_alias(repo = rctx.attr.name + "_" + pkg)]
- for pkg in bzl_packages or []
- },
- )
- for path, contents in aliases.items():
- rctx.file(path, contents)
-
- rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
- rctx.template("requirements.bzl", rctx.attr._template, substitutions = {
- " # %%GROUP_LIBRARY%%": """\
- group_repo = "{name}__groups"
- group_library(
- name = group_repo,
- repo_prefix = "{name}_",
- groups = all_requirement_groups,
- )""".format(name = rctx.attr.name) if not rctx.attr.use_hub_alias_dependencies else "",
- "%%ALL_DATA_REQUIREMENTS%%": _format_repr_list([
- macro_tmpl.format(p, "data")
- for p in bzl_packages
- ]),
- "%%ALL_REQUIREMENTS%%": _format_repr_list([
- macro_tmpl.format(p, "pkg")
- for p in bzl_packages
- ]),
- "%%ALL_REQUIREMENT_GROUPS%%": _format_dict(_repr_dict(requirement_cycles)),
- "%%ALL_WHL_REQUIREMENTS_BY_PACKAGE%%": _format_dict(_repr_dict({
- p: macro_tmpl.format(p, "whl")
- for p in bzl_packages
- })),
- "%%ANNOTATIONS%%": _format_dict(_repr_dict(annotations)),
- "%%CONFIG%%": _format_dict(_repr_dict(config)),
- "%%EXTRA_PIP_ARGS%%": json.encode(options),
- "%%IMPORTS%%": "\n".join(imports),
- "%%MACRO_TMPL%%": macro_tmpl,
- "%%NAME%%": rctx.attr.name,
- "%%PACKAGES%%": _format_repr_list(
- [
- ("{}_{}".format(rctx.attr.name, p), r)
- for p, r in sorted(selected_requirements.items())
- ],
- ),
- })
-
- return
-
-common_env = [
- "RULES_PYTHON_PIP_ISOLATED",
- REPO_DEBUG_ENV_VAR,
-]
-
-common_attrs = {
- "download_only": attr.bool(
- doc = """
-Whether to use "pip download" instead of "pip wheel". Disables building wheels from source, but allows use of
---platform, --python-version, --implementation, and --abi in --extra_pip_args to download wheels for a different
-platform from the host platform.
- """,
- ),
- "enable_implicit_namespace_pkgs": attr.bool(
- default = False,
- doc = """
-If true, disables conversion of native namespace packages into pkg-util style namespace packages. When set all py_binary
-and py_test targets must specify either `legacy_create_init=False` or the global Bazel option
-`--incompatible_default_to_explicit_init_py` to prevent `__init__.py` being automatically generated in every directory.
-
-This option is required to support some packages which cannot handle the conversion to pkg-util style.
- """,
- ),
- "environment": attr.string_dict(
- doc = """
-Environment variables to set in the pip subprocess.
-Can be used to set common variables such as `http_proxy`, `https_proxy` and `no_proxy`
-Note that pip is run with "--isolated" on the CLI so `PIP_<VAR>_<NAME>`
-style env vars are ignored, but env vars that control requests and urllib3
-can be passed. If you need `PIP_<VAR>_<NAME>`, take a look at `extra_pip_args`
-and `envsubst`.
- """,
- default = {},
- ),
- "envsubst": attr.string_list(
- mandatory = False,
- doc = """\
-A list of environment variables to substitute (e.g. `["PIP_INDEX_URL",
-"PIP_RETRIES"]`). The corresponding variables are expanded in `extra_pip_args`
-using the syntax `$VARNAME` or `${VARNAME}` (expanding to empty string if unset)
-or `${VARNAME:-default}` (expanding to default if the variable is unset or empty
-in the environment). Note: On Bazel 6 and Bazel 7.0 changes to the variables named
-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.
-""",
- ),
- "experimental_target_platforms": attr.string_list(
- default = [],
- doc = """\
-A list of platforms that we will generate the conditional dependency graph for
-cross platform wheels by parsing the wheel metadata. This will generate the
-correct dependencies for packages like `sphinx` or `pylint`, which include
-`colorama` when installed and used on Windows platforms.
-
-An empty list means falling back to the legacy behaviour where the host
-platform is the target platform.
-
-WARNING: It may not work as expected in cases where the python interpreter
-implementation that is being used at runtime is different between different platforms.
-This has been tested for CPython only.
-
-For specific target platforms use values of the form `<os>_<arch>` where `<os>`
-is one of `linux`, `osx`, `windows` and arch is one of `x86_64`, `x86_32`,
-`aarch64`, `s390x` and `ppc64le`.
-
-You can also target a specific Python version by using `cp3<minor_version>_<os>_<arch>`.
-If multiple python versions are specified as target platforms, then select statements
-of the `lib` and `whl` targets will include usage of version aware toolchain config
-settings like `@rules_python//python/config_settings:is_python_3.y`.
-
-Special values: `host` (for generating deps for the host platform only) and
-`<prefix>_*` values. For example, `cp39_*`, `linux_*`, `cp39_linux_*`.
-
-NOTE: this is not for cross-compiling Python wheels but rather for parsing the `whl` METADATA correctly.
-""",
- ),
- "extra_pip_args": attr.string_list(
- doc = """Extra arguments to pass on to pip. Must not contain spaces.
-
-Supports environment variables using the syntax `$VARNAME` or
-`${VARNAME}` (expanding to empty string if unset) or
-`${VARNAME:-default}` (expanding to default if the variable is unset
-or empty in the environment), if `"VARNAME"` is listed in the
-`envsubst` attribute. See also `envsubst`.
-""",
- ),
- "isolated": attr.bool(
- doc = """\
-Whether or not to pass the [--isolated](https://pip.pypa.io/en/stable/cli/pip/#cmdoption-isolated) flag to
-the underlying pip command. Alternatively, the `RULES_PYTHON_PIP_ISOLATED` environment variable can be used
-to control this flag.
-""",
- default = True,
- ),
- "pip_data_exclude": attr.string_list(
- doc = "Additional data exclusion parameters to add to the pip packages BUILD file.",
- ),
- "python_interpreter": attr.string(
- doc = """\
-The python interpreter to use. This can either be an absolute path or the name
-of a binary found on the host's `PATH` environment variable. If no value is set
-`python3` is defaulted for Unix systems and `python.exe` for Windows.
-""",
- # NOTE: This attribute should not have a default. See `_get_python_interpreter_attr`
- # default = "python3"
- ),
- "python_interpreter_target": attr.label(
- allow_single_file = True,
- doc = """
-If you are using a custom python interpreter built by another repository rule,
-use this attribute to specify its BUILD target. This allows pip_repository to invoke
-pip using the same interpreter as your toolchain. If set, takes precedence over
-python_interpreter. An example value: "@python3_x86_64-unknown-linux-gnu//:python".
-""",
- ),
- "quiet": attr.bool(
- default = True,
- doc = """\
-If True, suppress printing stdout and stderr output to the terminal.
-
-If you would like to get more diagnostic output, please use:
-
- RULES_PYTHON_REPO_DEBUG=1
-
-or
-
- RULES_PYTHON_REPO_DEBUG_VERBOSITY=<INFO|DEBUG|TRACE>
-""",
- ),
- "repo_prefix": attr.string(
- doc = """
-Prefix for the generated packages will be of the form `@<prefix><sanitized-package-name>//...`
-
-DEPRECATED. Only left for people who vendor requirements.bzl.
-""",
- ),
- # 600 is documented as default here: https://docs.bazel.build/versions/master/skylark/lib/repository_ctx.html#execute
- "timeout": attr.int(
- default = 600,
- doc = "Timeout (in seconds) on the rule's execution duration.",
- ),
- "_py_srcs": attr.label_list(
- doc = "Python sources used in the repository rule",
- allow_files = True,
- default = PIP_INSTALL_PY_SRCS,
- ),
-}
-
-pip_repository_attrs = {
- "annotations": attr.string_dict(
- doc = "Optional annotations to apply to packages",
- ),
- "requirements_by_platform": attr.label_keyed_string_dict(
- doc = """\
-The requirements files and the comma delimited list of target platforms as values.
-
-The keys are the requirement files and the values are comma-separated platform
-identifiers. For now we only support `<os>_<cpu>` values that are present in
-`@platforms//os` and `@platforms//cpu` packages respectively.
-""",
- ),
- "requirements_darwin": attr.label(
- allow_single_file = True,
- doc = "Override the requirements_lock attribute when the host platform is Mac OS",
- ),
- "requirements_linux": attr.label(
- allow_single_file = True,
- doc = "Override the requirements_lock attribute when the host platform is Linux",
- ),
- "requirements_lock": attr.label(
- allow_single_file = True,
- doc = """\
-A fully resolved 'requirements.txt' pip requirement file containing the
-transitive set of your dependencies. If this file is passed instead of
-'requirements' no resolve will take place and pip_repository will create
-individual repositories for each of your dependencies so that wheels are
-fetched/built only for the targets specified by 'build/run/test'. Note that if
-your lockfile is platform-dependent, you can use the `requirements_[platform]`
-attributes.
-
-Note, that in general requirements files are compiled for a specific platform,
-but sometimes they can work for multiple platforms. `rules_python` right now
-supports requirements files that are created for a particular platform without
-platform markers.
-""",
- ),
- "requirements_windows": attr.label(
- allow_single_file = True,
- doc = "Override the requirements_lock attribute when the host platform is Windows",
- ),
- "use_hub_alias_dependencies": attr.bool(
- default = False,
- doc = """\
-Controls if the hub alias dependencies are used. If set to true, then the
-group_library will be included in the hub repo.
-
-True will become default in a subsequent release.
-""",
- ),
- "_template": attr.label(
- default = ":pip_repository_requirements.bzl.tmpl",
- ),
-}
-
-pip_repository_attrs.update(**common_attrs)
-
-pip_repository = repository_rule(
- attrs = pip_repository_attrs,
- doc = """Accepts a locked/compiled requirements file and installs the dependencies listed within.
-
-Those dependencies become available in a generated `requirements.bzl` file.
-You can instead check this `requirements.bzl` file into your repo, see the "vendoring" section below.
-
-In your WORKSPACE file:
-
-```starlark
-load("@rules_python//python:pip.bzl", "pip_parse")
-
-pip_parse(
- name = "pypi",
- requirements_lock = ":requirements.txt",
-)
-
-load("@pypi//:requirements.bzl", "install_deps")
-
-install_deps()
-```
-
-You can then reference installed dependencies from a `BUILD` file with the alias targets generated in the same repo, for example, for `PyYAML` we would have the following:
-- `@pypi//pyyaml` and `@pypi//pyyaml:pkg` both point to the `py_library`
- created after extracting the `PyYAML` package.
-- `@pypi//pyyaml:data` points to the extra data included in the package.
-- `@pypi//pyyaml:dist_info` points to the `dist-info` files in the package.
-- `@pypi//pyyaml:whl` points to the wheel file that was extracted.
-
-```starlark
-py_library(
- name = "bar",
- ...
- deps = [
- "//my/other:dep",
- "@pypi//numpy",
- "@pypi//requests",
- ],
-)
-```
-
-or
-
-```starlark
-load("@pypi//:requirements.bzl", "requirement")
-
-py_library(
- name = "bar",
- ...
- deps = [
- "//my/other:dep",
- requirement("numpy"),
- requirement("requests"),
- ],
-)
-```
-
-In addition to the `requirement` macro, which is used to access the generated `py_library`
-target generated from a package's wheel, The generated `requirements.bzl` file contains
-functionality for exposing [entry points][whl_ep] as `py_binary` targets as well.
-
-[whl_ep]: https://packaging.python.org/specifications/entry-points/
-
-```starlark
-load("@pypi//:requirements.bzl", "entry_point")
-
-alias(
- name = "pip-compile",
- actual = entry_point(
- pkg = "pip-tools",
- script = "pip-compile",
- ),
-)
-```
-
-Note that for packages whose name and script are the same, only the name of the package
-is needed when calling the `entry_point` macro.
-
-```starlark
-load("@pip//:requirements.bzl", "entry_point")
-
-alias(
- name = "flake8",
- actual = entry_point("flake8"),
-)
-```
-
-### Vendoring the requirements.bzl file
-
-In some cases you may not want to generate the requirements.bzl file as a repository rule
-while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module
-such as a ruleset, you may want to include the requirements.bzl file rather than make your users
-install the WORKSPACE setup to generate it.
-See https://github.com/bazelbuild/rules_python/issues/608
-
-This is the same workflow as Gazelle, which creates `go_repository` rules with
-[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos)
-
-To do this, use the "write to source file" pattern documented in
-https://blog.aspect.dev/bazel-can-write-to-the-source-folder
-to put a copy of the generated requirements.bzl into your project.
-Then load the requirements.bzl file directly rather than from the generated repository.
-See the example in rules_python/examples/pip_parse_vendored.
-""",
- implementation = _pip_repository_impl,
- environ = common_env,
-)
-
-def _whl_library_impl(rctx):
- python_interpreter = _resolve_python_interpreter(rctx)
- args = [
- python_interpreter,
- "-m",
- "python.pip_install.tools.wheel_installer.wheel_installer",
- "--requirement",
- rctx.attr.requirement,
- ]
- extra_pip_args = []
- extra_pip_args.extend(rctx.attr.extra_pip_args)
-
- # Manually construct the PYTHONPATH since we cannot use the toolchain here
- environment = _create_repository_execution_environment(rctx, python_interpreter)
-
- whl_path = None
- if 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:
- filename = rctx.attr.filename
- urls = rctx.attr.urls
- if not filename:
- _, _, filename = urls[0].rpartition("/")
-
- if not (filename.endswith(".whl") or filename.endswith("tar.gz") or filename.endswith(".zip")):
- if rctx.attr.filename:
- msg = "got '{}'".format(filename)
- else:
- msg = "detected '{}' from url:\n{}".format(filename, urls[0])
- fail("Only '.whl', '.tar.gz' or '.zip' files are supported, {}".format(msg))
-
- result = rctx.download(
- url = urls,
- output = filename,
- sha256 = rctx.attr.sha256,
- auth = get_auth(rctx, urls),
- )
-
- if not result.success:
- fail("could not download the '{}' from {}:\n{}".format(filename, urls, result))
-
- if filename.endswith(".whl"):
- whl_path = rctx.path(rctx.attr.filename)
- else:
- # It is an sdist and we need to tell PyPI to use a file in this directory
- # and not use any indexes.
- extra_pip_args.extend(["--no-index", "--find-links", "."])
-
- args = _parse_optional_attrs(rctx, args, extra_pip_args)
-
- if not whl_path:
- repo_utils.execute_checked(
- rctx,
- op = "whl_library.ResolveRequirement({}, {})".format(rctx.attr.name, rctx.attr.requirement),
- arguments = args,
- environment = environment,
- quiet = rctx.attr.quiet,
- timeout = rctx.attr.timeout,
- )
-
- 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 = {}
- 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
-
- whl_path = patch_whl(
- rctx,
- python_interpreter = python_interpreter,
- whl_path = whl_path,
- patches = patches,
- quiet = rctx.attr.quiet,
- timeout = rctx.attr.timeout,
- )
-
- target_platforms = rctx.attr.experimental_target_platforms
- if target_platforms:
- parsed_whl = parse_whl_name(whl_path.basename)
- if parsed_whl.platform_tag != "any":
- # NOTE @aignas 2023-12-04: if the wheel is a platform specific
- # wheel, we only include deps for that target platform
- target_platforms = [
- p.target_platform
- for p in whl_target_platforms(
- platform_tag = parsed_whl.platform_tag,
- abi_tag = parsed_whl.abi_tag,
- )
- ]
-
- repo_utils.execute_checked(
- rctx,
- op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path),
- arguments = args + [
- "--whl-file",
- whl_path,
- ] + ["--platform={}".format(p) for p in target_platforms],
- environment = environment,
- quiet = rctx.attr.quiet,
- timeout = rctx.attr.timeout,
- )
-
- metadata = json.decode(rctx.read("metadata.json"))
- rctx.delete("metadata.json")
-
- entry_points = {}
- for item in metadata["entry_points"]:
- name = item["name"]
- module = item["module"]
- attribute = item["attribute"]
-
- # There is an extreme edge-case with entry_points that end with `.py`
- # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174
- entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name
- entry_point_target_name = (
- _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py
- )
- entry_point_script_name = entry_point_target_name + ".py"
-
- rctx.file(
- entry_point_script_name,
- _generate_entry_point_contents(module, attribute),
- )
- entry_points[entry_point_without_py] = entry_point_script_name
-
- build_file_contents = generate_whl_library_build_bazel(
- dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix),
- whl_name = whl_path.basename,
- dependencies = metadata["deps"],
- dependencies_by_platform = metadata["deps_by_platform"],
- group_name = rctx.attr.group_name,
- group_deps = rctx.attr.group_deps,
- data_exclude = rctx.attr.pip_data_exclude,
- tags = [
- "pypi_name=" + metadata["name"],
- "pypi_version=" + metadata["version"],
- ],
- entry_points = entry_points,
- annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))),
- )
- rctx.file("BUILD.bazel", build_file_contents)
-
- return
-
-def _generate_entry_point_contents(
- module,
- attribute,
- shebang = "#!/usr/bin/env python3"):
- """Generate the contents of an entry point script.
-
- Args:
- module (str): The name of the module to use.
- attribute (str): The name of the attribute to call.
- shebang (str, optional): The shebang to use for the entry point python
- file.
-
- Returns:
- str: A string of python code.
- """
- contents = """\
-{shebang}
-import sys
-from {module} import {attribute}
-if __name__ == "__main__":
- sys.exit({attribute}())
-""".format(
- shebang = shebang,
- module = module,
- attribute = attribute,
- )
- return contents
-
-# NOTE @aignas 2024-03-21: The usage of dict({}, **common) ensures that all args to `dict` are unique
-whl_library_attrs = dict({
- "annotation": attr.label(
- doc = (
- "Optional json encoded file containing annotation to apply to the extracted wheel. " +
- "See `package_annotation`"
- ),
- allow_files = True,
- ),
- "dep_template": attr.string(
- doc = """
-The dep template to use for referencing the dependencies. It should have `{name}`
-and `{target}` tokens that will be replaced with the normalized distribution name
-and the target that we need respectively.
-""",
- ),
- "filename": attr.string(
- doc = "Download the whl file to this filename. Only used when the `urls` is passed. If not specified, will be auto-detected from the `urls`.",
- ),
- "group_deps": attr.string_list(
- doc = "List of dependencies to skip in order to break the cycles within a dependency group.",
- default = [],
- ),
- "group_name": attr.string(
- doc = "Name of the group, if any.",
- ),
- "repo": attr.string(
- mandatory = True,
- doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.",
- ),
- "requirement": attr.string(
- mandatory = True,
- doc = "Python requirement string describing the package to make available, if 'urls' or 'whl_file' is given, then this only needs to include foo[any_extras] as a bare minimum.",
- ),
- "sha256": attr.string(
- doc = "The sha256 of the downloaded whl. Only used when the `urls` is passed.",
- ),
- "urls": attr.string_list(
- doc = """\
-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 that has
- json.encode(struct([whl_file], patch_strip]) as values. This
- is to maintain flexibility and correct bzlmod extension interface
- until we have a better way to define whl_library and move whl
- patching to a separate place. INTERNAL USE ONLY.""",
- ),
- "_python_path_entries": attr.label_list(
- # Get the root directory of these rules and keep them as a default attribute
- # in order to avoid unnecessary repository fetching restarts.
- #
- # This is very similar to what was done in https://github.com/bazelbuild/rules_go/pull/3478
- default = [
- Label("//:BUILD.bazel"),
- ] + [
- # Includes all the external dependencies from repositories.bzl
- Label("@" + repo + "//:BUILD.bazel")
- for repo in all_requirements
- ],
- ),
-}, **common_attrs)
-whl_library_attrs.update(AUTH_ATTRS)
-
-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.
-Instantiated from pip_repository and inherits config options from there.""",
- implementation = _whl_library_impl,
- environ = common_env,
-)
-
-def package_annotation(
- additive_build_content = None,
- copy_files = {},
- copy_executables = {},
- data = [],
- data_exclude_glob = [],
- srcs_exclude_glob = []):
- """Annotations to apply to the BUILD file content from package generated from a `pip_repository` rule.
-
- [cf]: https://github.com/bazelbuild/bazel-skylib/blob/main/docs/copy_file_doc.md
-
- Args:
- additive_build_content (str, optional): Raw text to add to the generated `BUILD` file of a package.
- copy_files (dict, optional): A mapping of `src` and `out` files for [@bazel_skylib//rules:copy_file.bzl][cf]
- copy_executables (dict, optional): A mapping of `src` and `out` files for
- [@bazel_skylib//rules:copy_file.bzl][cf]. Targets generated here will also be flagged as
- executable.
- data (list, optional): A list of labels to add as `data` dependencies to the generated `py_library` target.
- data_exclude_glob (list, optional): A list of exclude glob patterns to add as `data` to the generated
- `py_library` target.
- srcs_exclude_glob (list, optional): A list of labels to add as `srcs` to the generated `py_library` target.
-
- Returns:
- str: A json encoded string of the provided content.
- """
- return json.encode(struct(
- additive_build_content = additive_build_content,
- copy_files = copy_files,
- copy_executables = copy_executables,
- data = data,
- data_exclude_glob = data_exclude_glob,
- srcs_exclude_glob = srcs_exclude_glob,
- ))
-
-def _group_library_impl(rctx):
- build_file_contents = generate_group_library_build_bazel(
- repo_prefix = rctx.attr.repo_prefix,
- groups = rctx.attr.groups,
- )
- rctx.file("BUILD.bazel", build_file_contents)
-
-group_library = repository_rule(
- attrs = {
- "groups": attr.string_list_dict(
- doc = "A mapping of group names to requirements within that group.",
- ),
- "repo_prefix": attr.string(
- doc = "Prefix used for the whl_library created components of each group",
- ),
- },
- implementation = _group_library_impl,
- doc = """
-Create a package containing only wrapper py_library and whl_library rules for implementing dependency groups.
-This is an implementation detail of dependency groups and should not be used alone.
- """,
-)
-
-# pip_repository implementation
-
-def _format_list(items):
- return "[{}]".format(", ".join(items))
-
-def _format_repr_list(strings):
- return _format_list(
- [repr(s) for s in strings],
- )
-
-def _repr_dict(items):
- return {k: repr(v) for k, v in items.items()}
-
-def _format_dict(items):
- return "{{{}}}".format(", ".join(sorted(['"{}": {}'.format(k, v) for k, v in items.items()])))
+# Re-exports for backwards compatibility
+group_library = _group_library
+pip_repository = _pip_repository
+whl_library = _whl_library
+package_annotation = _package_annotation
diff --git a/python/pip_install/private/BUILD.bazel b/python/pip_install/private/BUILD.bazel
deleted file mode 100644
index 887d2d3..0000000
--- a/python/pip_install/private/BUILD.bazel
+++ /dev/null
@@ -1,48 +0,0 @@
-load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
-load(":pip_install_utils.bzl", "srcs_module")
-
-package(default_visibility = ["//:__subpackages__"])
-
-exports_files([
- "srcs.bzl",
-])
-
-filegroup(
- name = "distribution",
- srcs = glob(["*"]),
- visibility = ["//python/pip_install:__subpackages__"],
-)
-
-filegroup(
- name = "bzl_srcs",
- srcs = glob(["*.bzl"]),
-)
-
-srcs_module(
- name = "srcs_module",
- srcs = "//python/pip_install:py_srcs",
- dest = ":srcs.bzl",
-)
-
-bzl_library(
- name = "generate_whl_library_build_bazel_bzl",
- srcs = ["generate_whl_library_build_bazel.bzl"],
- deps = [
- "//python/private:labels_bzl",
- "//python/private:normalize_name_bzl",
- ],
-)
-
-bzl_library(
- name = "generate_group_library_build_bazel_bzl",
- srcs = ["generate_group_library_build_bazel.bzl"],
- deps = [
- "//python/private:labels_bzl",
- "//python/private:normalize_name_bzl",
- ],
-)
-
-bzl_library(
- name = "srcs_bzl",
- srcs = ["srcs.bzl"],
-)
diff --git a/python/pip_install/private/pip_install_utils.bzl b/python/pip_install/private/pip_install_utils.bzl
deleted file mode 100644
index 488583d..0000000
--- a/python/pip_install/private/pip_install_utils.bzl
+++ /dev/null
@@ -1,132 +0,0 @@
-# Copyright 2023 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Utilities for `rules_python` pip rules"""
-
-_SRCS_TEMPLATE = """\
-\"\"\"A generated file containing all source files used for `@rules_python//python/pip_install:pip_repository.bzl` rules
-
-This file is auto-generated from the `@rules_python//python/pip_install/private:srcs_module.update` target. Please
-`bazel run` this target to apply any updates. Note that doing so will discard any local modifications.
-"\"\"
-
-# Each source file is tracked as a target so `pip_repository` rules will know to automatically rebuild if any of the
-# sources changed.
-PIP_INSTALL_PY_SRCS = [
- {srcs}
-]
-"""
-
-def _src_label(file):
- dir_path, file_name = file.short_path.rsplit("/", 1)
-
- return "@rules_python//{}:{}".format(
- dir_path,
- file_name,
- )
-
-def _srcs_module_impl(ctx):
- srcs = [_src_label(src) for src in ctx.files.srcs]
- if not srcs:
- fail("`srcs` cannot be empty")
- output = ctx.actions.declare_file(ctx.label.name)
-
- ctx.actions.write(
- output = output,
- content = _SRCS_TEMPLATE.format(
- srcs = "\n ".join(["\"{}\",".format(src) for src in srcs]),
- ),
- )
-
- return DefaultInfo(
- files = depset([output]),
- )
-
-_srcs_module = rule(
- doc = "A rule for writing a list of sources to a templated file",
- implementation = _srcs_module_impl,
- attrs = {
- "srcs": attr.label(
- doc = "A filegroup of source files",
- allow_files = True,
- ),
- },
-)
-
-_INSTALLER_TEMPLATE = """\
-#!/bin/bash
-set -euo pipefail
-cp -f "{path}" "${{BUILD_WORKSPACE_DIRECTORY}}/{dest}"
-"""
-
-def _srcs_updater_impl(ctx):
- output = ctx.actions.declare_file(ctx.label.name + ".sh")
- target_file = ctx.file.input
- dest = ctx.file.dest.short_path
-
- ctx.actions.write(
- output = output,
- content = _INSTALLER_TEMPLATE.format(
- path = target_file.short_path,
- dest = dest,
- ),
- is_executable = True,
- )
-
- return DefaultInfo(
- files = depset([output]),
- runfiles = ctx.runfiles(files = [target_file]),
- executable = output,
- )
-
-_srcs_updater = rule(
- doc = "A rule for writing a `srcs.bzl` file back to the repository",
- implementation = _srcs_updater_impl,
- attrs = {
- "dest": attr.label(
- doc = "The target file to write the new `input` to.",
- allow_single_file = ["srcs.bzl"],
- mandatory = True,
- ),
- "input": attr.label(
- doc = "The file to write back to the repository",
- allow_single_file = True,
- mandatory = True,
- ),
- },
- executable = True,
-)
-
-def srcs_module(name, dest, **kwargs):
- """A helper rule to ensure `pip_repository` rules are always up to date
-
- Args:
- name (str): The name of the sources module
- dest (str): The filename the module should be written as in the current package.
- **kwargs (dict): Additional keyword arguments
- """
- tags = kwargs.pop("tags", [])
-
- _srcs_module(
- name = name,
- tags = tags,
- **kwargs
- )
-
- _srcs_updater(
- name = name + ".update",
- input = name,
- dest = dest,
- tags = tags,
- )
diff --git a/python/pip_install/private/srcs.bzl b/python/pip_install/private/srcs.bzl
deleted file mode 100644
index e92e49f..0000000
--- a/python/pip_install/private/srcs.bzl
+++ /dev/null
@@ -1,18 +0,0 @@
-"""A generated file containing all source files used for `@rules_python//python/pip_install:pip_repository.bzl` rules
-
-This file is auto-generated from the `@rules_python//python/pip_install/private:srcs_module.update` target. Please
-`bazel run` this target to apply any updates. Note that doing so will discard any local modifications.
-"""
-
-# Each source file is tracked as a target so `pip_repository` rules will know to automatically rebuild if any of the
-# sources changed.
-PIP_INSTALL_PY_SRCS = [
- "@rules_python//python/pip_install/tools/dependency_resolver:__init__.py",
- "@rules_python//python/pip_install/tools/dependency_resolver:dependency_resolver.py",
- "@rules_python//python/pip_install/tools/wheel_installer:arguments.py",
- "@rules_python//python/pip_install/tools/wheel_installer:namespace_pkgs.py",
- "@rules_python//python/pip_install/tools/wheel_installer:wheel.py",
- "@rules_python//python/pip_install/tools/wheel_installer:wheel_installer.py",
- "@rules_python//python/private:repack_whl.py",
- "@rules_python//tools:wheelmaker.py",
-]
diff --git a/python/pip_install/requirements_parser.bzl b/python/pip_install/requirements_parser.bzl
index 3b49fdf..82ec1b9 100644
--- a/python/pip_install/requirements_parser.bzl
+++ b/python/pip_install/requirements_parser.bzl
@@ -14,120 +14,6 @@
"Pip requirements parser for Starlark"
-_STATE = struct(
- # Consume extraneous whitespace
- ConsumeSpace = 0,
- # Consume a comment
- ConsumeComment = 1,
- # Parse the name of a pip package
- ParseDependency = 2,
- # Parse a full requirement line
- ParseRequirement = 3,
- # Parse a pip option
- ParseOption = 4,
-)
+load("//python/private/pypi:parse_requirements_txt.bzl", "parse_requirements_txt")
-EOF = {}
-
-def parse(content):
- """A simplistic (and incomplete) pip requirements lockfile parser.
-
- Parses package names and their full requirement lines, as well pip
- options.
-
- Args:
- content: lockfile content as a string
-
- Returns:
- Struct with fields `requirements` and `options`.
-
- requirements: List of requirements, where each requirement is a 2-element
- tuple containing the package name and the requirement line.
- E.g., [(certifi', 'certifi==2021.10.8 --hash=sha256:7888...'), ...]
-
- options: List of pip option lines
- """
- content = content.replace("\r", "")
-
- result = struct(
- requirements = [],
- options = [],
- )
- state = _STATE.ConsumeSpace
- buffer = ""
-
- inputs = content.elems()[:]
- inputs.append(EOF)
-
- for input in inputs:
- if state == _STATE.ConsumeSpace:
- (state, buffer) = _handleConsumeSpace(input)
- elif state == _STATE.ConsumeComment:
- (state, buffer) = _handleConsumeComment(input, buffer, result)
- elif state == _STATE.ParseDependency:
- (state, buffer) = _handleParseDependency(input, buffer, result)
- elif state == _STATE.ParseOption:
- (state, buffer) = _handleParseOption(input, buffer, result)
- elif state == _STATE.ParseRequirement:
- (state, buffer) = _handleParseRequirement(input, buffer, result)
- else:
- fail("Unknown state %d" % state)
-
- return result
-
-def _handleConsumeSpace(input):
- if input == EOF:
- return (_STATE.ConsumeSpace, "")
- if input.isspace():
- return (_STATE.ConsumeSpace, "")
- elif input == "#":
- return (_STATE.ConsumeComment, "")
- elif input == "-":
- return (_STATE.ParseOption, input)
-
- return (_STATE.ParseDependency, input)
-
-def _handleConsumeComment(input, buffer, result):
- if input == "\n":
- if len(result.requirements) > 0 and len(result.requirements[-1]) == 1:
- result.requirements[-1] = (result.requirements[-1][0], buffer.rstrip(" \n"))
- return (_STATE.ConsumeSpace, "")
- elif len(buffer) > 0:
- result.options.append(buffer.rstrip(" \n"))
- return (_STATE.ConsumeSpace, "")
- return (_STATE.ConsumeSpace, "")
- return (_STATE.ConsumeComment, buffer)
-
-def _handleParseDependency(input, buffer, result):
- if input == EOF:
- fail("Enountered unexpected end of file while parsing requirement")
- elif input.isspace() or input in [">", "<", "~", "=", ";", "["]:
- result.requirements.append((buffer,))
- return (_STATE.ParseRequirement, buffer + input)
-
- return (_STATE.ParseDependency, buffer + input)
-
-def _handleParseOption(input, buffer, result):
- if input == "\n" and buffer.endswith("\\"):
- return (_STATE.ParseOption, buffer[0:-1])
- elif input == " ":
- result.options.append(buffer.rstrip("\n"))
- return (_STATE.ParseOption, "")
- elif input == "\n" or input == EOF:
- result.options.append(buffer.rstrip("\n"))
- return (_STATE.ConsumeSpace, "")
- elif input == "#" and (len(buffer) == 0 or buffer[-1].isspace()):
- return (_STATE.ConsumeComment, buffer)
-
- return (_STATE.ParseOption, buffer + input)
-
-def _handleParseRequirement(input, buffer, result):
- if input == "\n" and buffer.endswith("\\"):
- return (_STATE.ParseRequirement, buffer[0:-1])
- elif input == "\n" or input == EOF:
- result.requirements[-1] = (result.requirements[-1][0], buffer.rstrip(" \n"))
- return (_STATE.ConsumeSpace, "")
- elif input == "#" and (len(buffer) == 0 or buffer[-1].isspace()):
- return (_STATE.ConsumeComment, buffer)
-
- return (_STATE.ParseRequirement, buffer + input)
+parse = parse_requirements_txt
diff --git a/python/pip_install/tools/dependency_resolver/BUILD.bazel b/python/pip_install/tools/dependency_resolver/BUILD.bazel
index c2cfb39..467b009 100644
--- a/python/pip_install/tools/dependency_resolver/BUILD.bazel
+++ b/python/pip_install/tools/dependency_resolver/BUILD.bazel
@@ -15,5 +15,5 @@
include = ["**/*.py"],
exclude = ["**/*_test.py"],
),
- visibility = ["//python/pip_install:__subpackages__"],
+ visibility = ["//:__subpackages__"],
)
diff --git a/python/pip_install/tools/wheel_installer/BUILD.bazel b/python/pip_install/tools/wheel_installer/BUILD.bazel
index a396488..0c24d5a 100644
--- a/python/pip_install/tools/wheel_installer/BUILD.bazel
+++ b/python/pip_install/tools/wheel_installer/BUILD.bazel
@@ -87,5 +87,5 @@
include = ["**/*.py"],
exclude = ["**/*_test.py"],
),
- visibility = ["//python/pip_install:__subpackages__"],
+ visibility = ["//:__subpackages__"],
)
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index cd385e3..ccc6acd 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -31,6 +31,7 @@
"//python/private/bzlmod:distribution",
"//python/private/common:distribution",
"//python/private/proto:distribution",
+ "//python/private/pypi:distribution",
"//python/private/whl_filegroup:distribution",
"//tools/build_defs/python/private:distribution",
],
@@ -126,62 +127,6 @@
)
bzl_library(
- name = "patch_whl_bzl",
- srcs = ["patch_whl.bzl"],
- deps = [":parse_whl_name_bzl"],
-)
-
-bzl_library(
- name = "parse_requirements_bzl",
- srcs = ["parse_requirements.bzl"],
- deps = [
- ":normalize_name_bzl",
- ":pypi_index_sources_bzl",
- ":whl_target_platforms_bzl",
- "//python/pip_install:requirements_parser_bzl",
- ],
-)
-
-bzl_library(
- name = "parse_whl_name_bzl",
- srcs = ["parse_whl_name.bzl"],
-)
-
-bzl_library(
- name = "pip_flags_bzl",
- srcs = ["pip_flags.bzl"],
- deps = [
- ":enum_bzl",
- ],
-)
-
-bzl_library(
- name = "pip_repo_name_bzl",
- srcs = ["pip_repo_name.bzl"],
- deps = [
- ":normalize_name_bzl",
- ":parse_whl_name_bzl",
- ],
-)
-
-bzl_library(
- name = "pypi_index_bzl",
- srcs = ["pypi_index.bzl"],
- deps = [
- ":auth_bzl",
- ":normalize_name_bzl",
- ":text_util_bzl",
- "//python/pip_install:requirements_parser_bzl",
- "//python/private/bzlmod:bazel_features_bzl",
- ],
-)
-
-bzl_library(
- name = "pypi_index_sources_bzl",
- srcs = ["pypi_index_sources.bzl"],
-)
-
-bzl_library(
name = "py_cc_toolchain_bzl",
srcs = [
"py_cc_toolchain_macro.bzl",
@@ -286,17 +231,6 @@
)
bzl_library(
- name = "render_pkg_aliases_bzl",
- srcs = ["render_pkg_aliases.bzl"],
- deps = [
- ":normalize_name_bzl",
- ":text_util_bzl",
- ":version_label_bzl",
- "//python/pip_install/private:generate_group_library_build_bazel_bzl",
- ],
-)
-
-bzl_library(
name = "repo_utils_bzl",
srcs = ["repo_utils.bzl"],
)
@@ -340,20 +274,6 @@
srcs = ["version_label.bzl"],
)
-bzl_library(
- name = "whl_target_platforms_bzl",
- srcs = ["whl_target_platforms.bzl"],
- visibility = ["//:__subpackages__"],
- deps = [
- "parse_whl_name_bzl",
- ],
-)
-
-bzl_library(
- name = "labels_bzl",
- srcs = ["labels.bzl"],
-)
-
# @bazel_tools can't define bzl_library itself, so we just put a wrapper around it.
bzl_library(
name = "bazel_tools_bzl",
diff --git a/python/private/bzlmod/BUILD.bazel b/python/private/bzlmod/BUILD.bazel
index 3362f34..2cb35fc 100644
--- a/python/private/bzlmod/BUILD.bazel
+++ b/python/private/bzlmod/BUILD.bazel
@@ -13,7 +13,6 @@
# limitations under the License.
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
-load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
package(default_visibility = ["//:__subpackages__"])
@@ -29,33 +28,7 @@
name = "pip_bzl",
srcs = ["pip.bzl"],
deps = [
- ":pip_repository_bzl",
- "//python/pip_install:pip_repository_bzl",
- "//python/private:pypi_index_bzl",
- "//python/private:full_version_bzl",
- "//python/private:normalize_name_bzl",
- "//python/private:parse_requirements_bzl",
- "//python/private:parse_whl_name_bzl",
- "//python/private:pip_repo_name_bzl",
- "//python/private:version_label_bzl",
- ":bazel_features_bzl",
- ] + [
- "@pythons_hub//:interpreters_bzl",
- ] if BZLMOD_ENABLED else [],
-)
-
-bzl_library(
- name = "bazel_features_bzl",
- deps = ["@bazel_features//:features"],
-)
-
-bzl_library(
- name = "pip_repository_bzl",
- srcs = ["pip_repository.bzl"],
- visibility = ["//:__subpackages__"],
- deps = [
- "//python/private:render_pkg_aliases_bzl",
- "//python/private:text_util_bzl",
+ "//python/private/pypi:bzlmod_bzl",
],
)
@@ -66,6 +39,8 @@
":pythons_hub_bzl",
"//python:repositories_bzl",
"//python/private:toolchains_repo_bzl",
+ "//python/private:util_bzl",
+ "@bazel_features//:features",
],
)
diff --git a/python/private/bzlmod/pip.bzl b/python/private/bzlmod/pip.bzl
index 2ded3a4..ecf94b6 100644
--- a/python/private/bzlmod/pip.bzl
+++ b/python/private/bzlmod/pip.bzl
@@ -12,817 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"pip module extension for use with bzlmod"
+"pip module extensions for use with bzlmod."
-load("@bazel_features//:features.bzl", "bazel_features")
-load("@pythons_hub//:interpreters.bzl", "DEFAULT_PYTHON_VERSION", "INTERPRETER_LABELS")
-load(
- "//python/pip_install:pip_repository.bzl",
- "pip_repository_attrs",
- "use_isolated",
- "whl_library",
-)
-load("//python/private:auth.bzl", "AUTH_ATTRS")
-load("//python/private:normalize_name.bzl", "normalize_name")
-load("//python/private:parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement")
-load("//python/private:parse_whl_name.bzl", "parse_whl_name")
-load("//python/private:pip_repo_name.bzl", "pip_repo_name")
-load("//python/private:pypi_index.bzl", "simpleapi_download")
-load("//python/private:render_pkg_aliases.bzl", "whl_alias")
-load("//python/private:repo_utils.bzl", "repo_utils")
-load("//python/private:version_label.bzl", "version_label")
-load(":pip_repository.bzl", "pip_repository")
+load("//python/private/pypi:bzlmod.bzl", "pypi", "pypi_internal")
-def _parse_version(version):
- major, _, version = version.partition(".")
- minor, _, version = version.partition(".")
- patch, _, version = version.partition(".")
- build, _, version = version.partition(".")
-
- return struct(
- # use semver vocabulary here
- major = major,
- minor = minor,
- patch = patch, # this is called `micro` in the Python interpreter versioning scheme
- build = build,
- )
-
-def _major_minor_version(version):
- version = _parse_version(version)
- return "{}.{}".format(version.major, version.minor)
-
-def _whl_mods_impl(mctx):
- """Implementation of the pip.whl_mods tag class.
-
- This creates the JSON files used to modify the creation of different wheels.
-"""
- whl_mods_dict = {}
- for mod in mctx.modules:
- for whl_mod_attr in mod.tags.whl_mods:
- if whl_mod_attr.hub_name not in whl_mods_dict.keys():
- whl_mods_dict[whl_mod_attr.hub_name] = {whl_mod_attr.whl_name: whl_mod_attr}
- elif whl_mod_attr.whl_name in whl_mods_dict[whl_mod_attr.hub_name].keys():
- # We cannot have the same wheel name in the same hub, as we
- # will create the same JSON file name.
- fail("""\
-Found same whl_name '{}' in the same hub '{}', please use a different hub_name.""".format(
- whl_mod_attr.whl_name,
- whl_mod_attr.hub_name,
- ))
- else:
- whl_mods_dict[whl_mod_attr.hub_name][whl_mod_attr.whl_name] = whl_mod_attr
-
- for hub_name, whl_maps in whl_mods_dict.items():
- whl_mods = {}
-
- # create a struct that we can pass to the _whl_mods_repo rule
- # to create the different JSON files.
- for whl_name, mods in whl_maps.items():
- build_content = mods.additive_build_content
- if mods.additive_build_content_file != None and mods.additive_build_content != "":
- fail("""\
-You cannot use both the additive_build_content and additive_build_content_file arguments at the same time.
-""")
- elif mods.additive_build_content_file != None:
- build_content = mctx.read(mods.additive_build_content_file)
-
- whl_mods[whl_name] = json.encode(struct(
- additive_build_content = build_content,
- copy_files = mods.copy_files,
- copy_executables = mods.copy_executables,
- data = mods.data,
- data_exclude_glob = mods.data_exclude_glob,
- srcs_exclude_glob = mods.srcs_exclude_glob,
- ))
-
- _whl_mods_repo(
- name = hub_name,
- whl_mods = whl_mods,
- )
-
-def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, simpleapi_cache):
- logger = repo_utils.logger(module_ctx)
- python_interpreter_target = pip_attr.python_interpreter_target
- is_hub_reproducible = True
-
- # if we do not have the python_interpreter set in the attributes
- # we programmatically find it.
- hub_name = pip_attr.hub_name
- if python_interpreter_target == None and not pip_attr.python_interpreter:
- python_name = "python_{}_host".format(
- pip_attr.python_version.replace(".", "_"),
- )
- if python_name not in INTERPRETER_LABELS:
- fail((
- "Unable to find interpreter for pip hub '{hub_name}' for " +
- "python_version={version}: Make sure a corresponding " +
- '`python.toolchain(python_version="{version}")` call exists.' +
- "Expected to find {python_name} among registered versions:\n {labels}"
- ).format(
- hub_name = hub_name,
- version = pip_attr.python_version,
- python_name = python_name,
- labels = " \n".join(INTERPRETER_LABELS),
- ))
- python_interpreter_target = INTERPRETER_LABELS[python_name]
-
- pip_name = "{}_{}".format(
- hub_name,
- version_label(pip_attr.python_version),
- )
- major_minor = _major_minor_version(pip_attr.python_version)
-
- if hub_name not in whl_map:
- whl_map[hub_name] = {}
-
- whl_modifications = {}
- if pip_attr.whl_modifications != None:
- for mod, whl_name in pip_attr.whl_modifications.items():
- whl_modifications[whl_name] = mod
-
- if pip_attr.experimental_requirement_cycles:
- requirement_cycles = {
- name: [normalize_name(whl_name) for whl_name in whls]
- for name, whls in pip_attr.experimental_requirement_cycles.items()
- }
-
- whl_group_mapping = {
- whl_name: group_name
- for group_name, group_whls in requirement_cycles.items()
- for whl_name in group_whls
- }
-
- # TODO @aignas 2024-04-05: how do we support different requirement
- # cycles for different abis/oses? For now we will need the users to
- # assume the same groups across all versions/platforms until we start
- # using an alternative cycle resolution strategy.
- group_map[hub_name] = pip_attr.experimental_requirement_cycles
- else:
- whl_group_mapping = {}
- requirement_cycles = {}
-
- # Create a new wheel library for each of the different whls
-
- get_index_urls = None
- if pip_attr.experimental_index_url:
- if pip_attr.download_only:
- fail("Currently unsupported to use `download_only` and `experimental_index_url`")
-
- get_index_urls = lambda ctx, distributions: simpleapi_download(
- ctx,
- attr = struct(
- index_url = pip_attr.experimental_index_url,
- extra_index_urls = pip_attr.experimental_extra_index_urls or [],
- index_url_overrides = pip_attr.experimental_index_url_overrides or {},
- sources = distributions,
- envsubst = pip_attr.envsubst,
- # Auth related info
- netrc = pip_attr.netrc,
- auth_patterns = pip_attr.auth_patterns,
- ),
- cache = simpleapi_cache,
- parallel_download = pip_attr.parallel_download,
- )
-
- requirements_by_platform = parse_requirements(
- module_ctx,
- requirements_by_platform = pip_attr.requirements_by_platform,
- requirements_linux = pip_attr.requirements_linux,
- requirements_lock = pip_attr.requirements_lock,
- requirements_osx = pip_attr.requirements_darwin,
- requirements_windows = pip_attr.requirements_windows,
- extra_pip_args = pip_attr.extra_pip_args,
- get_index_urls = get_index_urls,
- python_version = major_minor,
- logger = logger,
- )
-
- repository_platform = host_platform(module_ctx.os)
- for whl_name, requirements in requirements_by_platform.items():
- # We are not using the "sanitized name" because the user
- # would need to guess what name we modified the whl name
- # to.
- annotation = whl_modifications.get(whl_name)
- whl_name = normalize_name(whl_name)
-
- group_name = whl_group_mapping.get(whl_name)
- group_deps = requirement_cycles.get(group_name, [])
-
- # Construct args separately so that the lock file can be smaller and does not include unused
- # attrs.
- whl_library_args = dict(
- repo = pip_name,
- dep_template = "@{}//{{name}}:{{target}}".format(hub_name),
- )
- maybe_args = dict(
- # The following values are safe to omit if they have false like values
- annotation = annotation,
- download_only = pip_attr.download_only,
- enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs,
- environment = pip_attr.environment,
- envsubst = pip_attr.envsubst,
- experimental_target_platforms = pip_attr.experimental_target_platforms,
- group_deps = group_deps,
- group_name = group_name,
- pip_data_exclude = pip_attr.pip_data_exclude,
- python_interpreter = pip_attr.python_interpreter,
- python_interpreter_target = python_interpreter_target,
- whl_patches = {
- p: json.encode(args)
- for p, args in whl_overrides.get(whl_name, {}).items()
- },
- )
- whl_library_args.update({k: v for k, v in maybe_args.items() if v})
- maybe_args_with_default = dict(
- # The following values have defaults next to them
- isolated = (use_isolated(module_ctx, pip_attr), True),
- quiet = (pip_attr.quiet, True),
- timeout = (pip_attr.timeout, 600),
- )
- whl_library_args.update({
- k: v
- for k, (v, default) in maybe_args_with_default.items()
- if v != default
- })
-
- if get_index_urls:
- # TODO @aignas 2024-05-26: move to a separate function
- found_something = False
- for requirement in requirements:
- for distribution in requirement.whls + [requirement.sdist]:
- if not distribution:
- # sdist may be None
- continue
-
- found_something = True
- is_hub_reproducible = False
-
- if pip_attr.netrc:
- whl_library_args["netrc"] = pip_attr.netrc
- if pip_attr.auth_patterns:
- whl_library_args["auth_patterns"] = pip_attr.auth_patterns
-
- # pip is not used to download wheels and the python `whl_library` helpers are only extracting things
- whl_library_args.pop("extra_pip_args", None)
-
- # This is no-op because pip is not used to download the wheel.
- whl_library_args.pop("download_only", None)
-
- repo_name = pip_repo_name(pip_name, distribution.filename, distribution.sha256)
- whl_library_args["requirement"] = requirement.srcs.requirement
- whl_library_args["urls"] = [distribution.url]
- whl_library_args["sha256"] = distribution.sha256
- whl_library_args["filename"] = distribution.filename
- whl_library_args["experimental_target_platforms"] = requirement.target_platforms
-
- # Pure python wheels or sdists may need to have a platform here
- target_platforms = None
- if distribution.filename.endswith("-any.whl") or not distribution.filename.endswith(".whl"):
- if len(requirements) > 1:
- target_platforms = requirement.target_platforms
-
- whl_library(name = repo_name, **dict(sorted(whl_library_args.items())))
-
- whl_map[hub_name].setdefault(whl_name, []).append(
- whl_alias(
- repo = repo_name,
- version = major_minor,
- filename = distribution.filename,
- target_platforms = target_platforms,
- ),
- )
-
- if found_something:
- continue
-
- requirement = select_requirement(
- requirements,
- platform = repository_platform,
- )
- if not requirement:
- # Sometimes the package is not present for host platform if there
- # are whls specified only in particular requirements files, in that
- # case just continue, however, if the download_only flag is set up,
- # then the user can also specify the target platform of the wheel
- # packages they want to download, in that case there will be always
- # a requirement here, so we will not be in this code branch.
- continue
- elif get_index_urls:
- logger.warn(lambda: "falling back to pip for installing the right file for {}".format(requirement.requirement_line))
-
- whl_library_args["requirement"] = requirement.requirement_line
- if requirement.extra_pip_args:
- whl_library_args["extra_pip_args"] = requirement.extra_pip_args
-
- # We sort so that the lock-file remains the same no matter the order of how the
- # args are manipulated in the code going before.
- repo_name = "{}_{}".format(pip_name, whl_name)
- whl_library(name = repo_name, **dict(sorted(whl_library_args.items())))
- whl_map[hub_name].setdefault(whl_name, []).append(
- whl_alias(
- repo = repo_name,
- version = major_minor,
- ),
- )
-
- return is_hub_reproducible
-
-def _pip_impl(module_ctx):
- """Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories.
-
- This implementation iterates through all of the `pip.parse` calls and creates
- different pip hub repositories based on the "hub_name". Each of the
- pip calls create spoke repos that uses a specific Python interpreter.
-
- In a MODULES.bazel file we have:
-
- pip.parse(
- hub_name = "pip",
- python_version = 3.9,
- requirements_lock = "//:requirements_lock_3_9.txt",
- requirements_windows = "//:requirements_windows_3_9.txt",
- )
- pip.parse(
- hub_name = "pip",
- python_version = 3.10,
- requirements_lock = "//:requirements_lock_3_10.txt",
- requirements_windows = "//:requirements_windows_3_10.txt",
- )
-
- For instance, we have a hub with the name of "pip".
- A repository named the following is created. It is actually called last when
- all of the pip spokes are collected.
-
- - @@rules_python~override~pip~pip
-
- As shown in the example code above we have the following.
- Two different pip.parse statements exist in MODULE.bazel provide the hub_name "pip".
- These definitions create two different pip spoke repositories that are
- related to the hub "pip".
- One spoke uses Python 3.9 and the other uses Python 3.10. This code automatically
- determines the Python version and the interpreter.
- Both of these pip spokes contain requirements files that includes websocket
- and its dependencies.
-
- We also need repositories for the wheels that the different pip spokes contain.
- For each Python version a different wheel repository is created. In our example
- each pip spoke had a requirements file that contained websockets. We
- then create two different wheel repositories that are named the following.
-
- - @@rules_python~override~pip~pip_39_websockets
- - @@rules_python~override~pip~pip_310_websockets
-
- And if the wheel has any other dependencies subsequent wheels are created in the same fashion.
-
- The hub repository has aliases for `pkg`, `data`, etc, which have a select that resolves to
- a spoke repository depending on the Python version.
-
- Also we may have more than one hub as defined in a MODULES.bazel file. So we could have multiple
- hubs pointing to various different pip spokes.
-
- Some other business rules notes. A hub can only have one spoke per Python version. We cannot
- have a hub named "pip" that has two spokes that use the Python 3.9 interpreter. Second
- we cannot have the same hub name used in sub-modules. The hub name has to be globally
- unique.
-
- This implementation also handles the creation of whl_modification JSON files that are used
- during the creation of wheel libraries. These JSON files used via the annotations argument
- when calling wheel_installer.py.
-
- Args:
- module_ctx: module contents
- """
-
- # Build all of the wheel modifications if the tag class is called.
- _whl_mods_impl(module_ctx)
-
- _overriden_whl_set = {}
- whl_overrides = {}
-
- for module in module_ctx.modules:
- for attr in module.tags.override:
- if not module.is_root:
- fail("overrides are only supported in root modules")
-
- if not attr.file.endswith(".whl"):
- fail("Only whl overrides are supported at this time")
-
- whl_name = normalize_name(parse_whl_name(attr.file).distribution)
-
- if attr.file in _overriden_whl_set:
- fail("Duplicate module overrides for '{}'".format(attr.file))
- _overriden_whl_set[attr.file] = None
-
- for patch in attr.patches:
- if whl_name not in whl_overrides:
- whl_overrides[whl_name] = {}
-
- if patch not in whl_overrides[whl_name]:
- whl_overrides[whl_name][patch] = struct(
- patch_strip = attr.patch_strip,
- whls = [],
- )
-
- whl_overrides[whl_name][patch].whls.append(attr.file)
-
- # Used to track all the different pip hubs and the spoke pip Python
- # versions.
- pip_hub_map = {}
-
- # Keeps track of all the hub's whl repos across the different versions.
- # dict[hub, dict[whl, dict[version, str pip]]]
- # Where hub, whl, and pip are the repo names
- hub_whl_map = {}
- hub_group_map = {}
-
- simpleapi_cache = {}
- is_extension_reproducible = True
-
- for mod in module_ctx.modules:
- for pip_attr in mod.tags.parse:
- hub_name = pip_attr.hub_name
- if hub_name not in pip_hub_map:
- pip_hub_map[pip_attr.hub_name] = struct(
- module_name = mod.name,
- python_versions = [pip_attr.python_version],
- )
- elif pip_hub_map[hub_name].module_name != mod.name:
- # We cannot have two hubs with the same name in different
- # modules.
- fail((
- "Duplicate cross-module pip hub named '{hub}': pip hub " +
- "names must be unique across modules. First defined " +
- "by module '{first_module}', second attempted by " +
- "module '{second_module}'"
- ).format(
- hub = hub_name,
- first_module = pip_hub_map[hub_name].module_name,
- second_module = mod.name,
- ))
-
- elif pip_attr.python_version in pip_hub_map[hub_name].python_versions:
- fail((
- "Duplicate pip python version '{version}' for hub " +
- "'{hub}' in module '{module}': the Python versions " +
- "used for a hub must be unique"
- ).format(
- hub = hub_name,
- module = mod.name,
- version = pip_attr.python_version,
- ))
- else:
- pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version)
-
- is_hub_reproducible = _create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides, hub_group_map, simpleapi_cache)
- is_extension_reproducible = is_extension_reproducible and is_hub_reproducible
-
- for hub_name, whl_map in hub_whl_map.items():
- pip_repository(
- name = hub_name,
- repo_name = hub_name,
- whl_map = {
- key: json.encode(value)
- for key, value in whl_map.items()
- },
- default_version = _major_minor_version(DEFAULT_PYTHON_VERSION),
- groups = hub_group_map.get(hub_name),
- )
-
- if bazel_features.external_deps.extension_metadata_has_reproducible:
- # If we are not using the `experimental_index_url feature, the extension is fully
- # deterministic and we don't need to create a lock entry for it.
- #
- # In order to be able to dogfood the `experimental_index_url` feature before it gets
- # stabilized, we have created the `_pip_non_reproducible` function, that will result
- # in extra entries in the lock file.
- return module_ctx.extension_metadata(reproducible = is_extension_reproducible)
- else:
- return None
-
-def _pip_non_reproducible(module_ctx):
- _pip_impl(module_ctx)
-
- # We default to calling the PyPI index and that will go into the
- # MODULE.bazel.lock file, hence return nothing here.
- return None
-
-def _pip_parse_ext_attrs(**kwargs):
- """Get the attributes for the pip extension.
-
- Args:
- **kwargs: A kwarg for setting defaults for the specific attributes. The
- key is expected to be the same as the attribute key.
-
- Returns:
- A dict of attributes.
- """
- attrs = dict({
- "experimental_extra_index_urls": attr.string_list(
- doc = """\
-The extra index URLs to use for downloading wheels using bazel downloader.
-Each value is going to be subject to `envsubst` substitutions if necessary.
-
-The indexes must support Simple API as described here:
-https://packaging.python.org/en/latest/specifications/simple-repository-api/
-
-This is equivalent to `--extra-index-urls` `pip` option.
-""",
- default = [],
- ),
- "experimental_index_url": attr.string(
- default = kwargs.get("experimental_index_url", ""),
- doc = """\
-The index URL to use for downloading wheels using bazel downloader. This value is going
-to be subject to `envsubst` substitutions if necessary.
-
-The indexes must support Simple API as described here:
-https://packaging.python.org/en/latest/specifications/simple-repository-api/
-
-In the future this could be defaulted to `https://pypi.org` when this feature becomes
-stable.
-
-This is equivalent to `--index-url` `pip` option.
-""",
- ),
- "experimental_index_url_overrides": attr.string_dict(
- doc = """\
-The index URL overrides for each package to use for downloading wheels using
-bazel downloader. This value is going to be subject to `envsubst` substitutions
-if necessary.
-
-The key is the package name (will be normalized before usage) and the value is the
-index URL.
-
-This design pattern has been chosen in order to be fully deterministic about which
-packages come from which source. We want to avoid issues similar to what happened in
-https://pytorch.org/blog/compromised-nightly-dependency/.
-
-The indexes must support Simple API as described here:
-https://packaging.python.org/en/latest/specifications/simple-repository-api/
-""",
- ),
- "hub_name": attr.string(
- mandatory = True,
- doc = """
-The name of the repo pip dependencies will be accessible from.
-
-This name must be unique between modules; unless your module is guaranteed to
-always be the root module, it's highly recommended to include your module name
-in the hub name. Repo mapping, `use_repo(..., pip="my_modules_pip_deps")`, can
-be used for shorter local names within your module.
-
-Within a module, the same `hub_name` can be specified to group different Python
-versions of pip dependencies under one repository name. This allows using a
-Python version-agnostic name when referring to pip dependencies; the
-correct version will be automatically selected.
-
-Typically, a module will only have a single hub of pip dependencies, but this
-is not required. Each hub is a separate resolution of pip dependencies. This
-means if different programs need different versions of some library, separate
-hubs can be created, and each program can use its respective hub's targets.
-Targets from different hubs should not be used together.
-""",
- ),
- "parallel_download": attr.bool(
- doc = """\
-The flag allows to make use of parallel downloading feature in bazel 7.1 and above
-when the bazel downloader is used. This is by default enabled as it improves the
-performance by a lot, but in case the queries to the simple API are very expensive
-or when debugging authentication issues one may want to disable this feature.
-
-NOTE, This will download (potentially duplicate) data for multiple packages if
-there is more than one index available, but in general this should be negligible
-because the simple API calls are very cheap and the user should not notice any
-extra overhead.
-
-If we are in synchronous mode, then we will use the first result that we
-find in case extra indexes are specified.
-""",
- default = True,
- ),
- "python_version": attr.string(
- mandatory = True,
- doc = """
-The Python version the dependencies are targetting, in Major.Minor format
-(e.g., "3.11") or patch level granularity (e.g. "3.11.1").
-
-If an interpreter isn't explicitly provided (using `python_interpreter` or
-`python_interpreter_target`), then the version specified here must have
-a corresponding `python.toolchain()` configured.
-""",
- ),
- "whl_modifications": attr.label_keyed_string_dict(
- mandatory = False,
- doc = """\
-A dict of labels to wheel names that is typically generated by the whl_modifications.
-The labels are JSON config files describing the modifications.
-""",
- ),
- }, **pip_repository_attrs)
- attrs.update(AUTH_ATTRS)
-
- # Like the pip_repository rule, we end up setting this manually so
- # don't allow users to override it.
- attrs.pop("repo_prefix")
-
- # annotations has been replaced with whl_modifications in bzlmod
- attrs.pop("annotations")
-
- return attrs
-
-def _whl_mod_attrs():
- attrs = {
- "additive_build_content": attr.string(
- doc = "(str, optional): Raw text to add to the generated `BUILD` file of a package.",
- ),
- "additive_build_content_file": attr.label(
- doc = """\
-(label, optional): path to a BUILD file to add to the generated
-`BUILD` file of a package. You cannot use both additive_build_content and additive_build_content_file
-arguments at the same time.""",
- ),
- "copy_executables": attr.string_dict(
- doc = """\
-(dict, optional): A mapping of `src` and `out` files for
-[@bazel_skylib//rules:copy_file.bzl][cf]. Targets generated here will also be flagged as
-executable.""",
- ),
- "copy_files": attr.string_dict(
- doc = """\
-(dict, optional): A mapping of `src` and `out` files for
-[@bazel_skylib//rules:copy_file.bzl][cf]""",
- ),
- "data": attr.string_list(
- doc = """\
-(list, optional): A list of labels to add as `data` dependencies to
-the generated `py_library` target.""",
- ),
- "data_exclude_glob": attr.string_list(
- doc = """\
-(list, optional): A list of exclude glob patterns to add as `data` to
-the generated `py_library` target.""",
- ),
- "hub_name": attr.string(
- doc = """\
-Name of the whl modification, hub we use this name to set the modifications for
-pip.parse. If you have different pip hubs you can use a different name,
-otherwise it is best practice to just use one.
-
-You cannot have the same `hub_name` in different modules. You can reuse the same
-name in the same module for different wheels that you put in the same hub, but you
-cannot have a child module that uses the same `hub_name`.
-""",
- mandatory = True,
- ),
- "srcs_exclude_glob": attr.string_list(
- doc = """\
-(list, optional): A list of labels to add as `srcs` to the generated
-`py_library` target.""",
- ),
- "whl_name": attr.string(
- doc = "The whl name that the modifications are used for.",
- mandatory = True,
- ),
- }
- return attrs
-
-# NOTE: the naming of 'override' is taken from the bzlmod native
-# 'archive_override', 'git_override' bzlmod functions.
-_override_tag = tag_class(
- attrs = {
- "file": attr.string(
- doc = """\
-The Python distribution file name which needs to be patched. This will be
-applied to all repositories that setup this distribution via the pip.parse tag
-class.""",
- mandatory = True,
- ),
- "patch_strip": attr.int(
- default = 0,
- doc = """\
-The number of leading path segments to be stripped from the file name in the
-patches.""",
- ),
- "patches": attr.label_list(
- doc = """\
-A list of patches to apply to the repository *after* 'whl_library' is extracted
-and BUILD.bazel file is generated.""",
- mandatory = True,
- ),
- },
- doc = """\
-Apply any overrides (e.g. patches) to a given Python distribution defined by
-other tags in this extension.""",
-)
-
-pip = module_extension(
- doc = """\
-This extension is used to make dependencies from pip available.
-
-pip.parse:
-To use, call `pip.parse()` and specify `hub_name` and your requirements file.
-Dependencies will be downloaded and made available in a repo named after the
-`hub_name` argument.
-
-Each `pip.parse()` call configures a particular Python version. Multiple calls
-can be made to configure different Python versions, and will be grouped by
-the `hub_name` argument. This allows the same logical name, e.g. `@pip//numpy`
-to automatically resolve to different, Python version-specific, libraries.
-
-pip.whl_mods:
-This tag class is used to help create JSON files to describe modifications to
-the BUILD files for wheels.
-""",
- implementation = _pip_impl,
- tag_classes = {
- "override": _override_tag,
- "parse": tag_class(
- attrs = _pip_parse_ext_attrs(),
- doc = """\
-This tag class is used to create a pip hub and all of the spokes that are part of that hub.
-This tag class reuses most of the pip attributes that are found in
-@rules_python//python/pip_install:pip_repository.bzl.
-The exception is it does not use the arg 'repo_prefix'. We set the repository
-prefix for the user and the alias arg is always True in bzlmod.
-""",
- ),
- "whl_mods": tag_class(
- attrs = _whl_mod_attrs(),
- doc = """\
-This tag class is used to create JSON file that are used when calling wheel_builder.py. These
-JSON files contain instructions on how to modify a wheel's project. Each of the attributes
-create different modifications based on the type of attribute. Previously to bzlmod these
-JSON files where referred to as annotations, and were renamed to whl_modifications in this
-extension.
-""",
- ),
- },
-)
-
-pip_internal = module_extension(
- doc = """\
-This extension is used to make dependencies from pypi available.
-
-For now this is intended to be used internally so that usage of the `pip`
-extension in `rules_python` does not affect the evaluations of the extension
-for the consumers.
-
-pip.parse:
-To use, call `pip.parse()` and specify `hub_name` and your requirements file.
-Dependencies will be downloaded and made available in a repo named after the
-`hub_name` argument.
-
-Each `pip.parse()` call configures a particular Python version. Multiple calls
-can be made to configure different Python versions, and will be grouped by
-the `hub_name` argument. This allows the same logical name, e.g. `@pypi//numpy`
-to automatically resolve to different, Python version-specific, libraries.
-
-pip.whl_mods:
-This tag class is used to help create JSON files to describe modifications to
-the BUILD files for wheels.
-""",
- implementation = _pip_non_reproducible,
- tag_classes = {
- "override": _override_tag,
- "parse": tag_class(
- attrs = _pip_parse_ext_attrs(
- experimental_index_url = "https://pypi.org/simple",
- ),
- doc = """\
-This tag class is used to create a pypi hub and all of the spokes that are part of that hub.
-This tag class reuses most of the pypi attributes that are found in
-@rules_python//python/pip_install:pip_repository.bzl.
-The exception is it does not use the arg 'repo_prefix'. We set the repository
-prefix for the user and the alias arg is always True in bzlmod.
-""",
- ),
- "whl_mods": tag_class(
- attrs = _whl_mod_attrs(),
- doc = """\
-This tag class is used to create JSON file that are used when calling wheel_builder.py. These
-JSON files contain instructions on how to modify a wheel's project. Each of the attributes
-create different modifications based on the type of attribute. Previously to bzlmod these
-JSON files where referred to as annotations, and were renamed to whl_modifications in this
-extension.
-""",
- ),
- },
-)
-
-def _whl_mods_repo_impl(rctx):
- rctx.file("BUILD.bazel", "")
- for whl_name, mods in rctx.attr.whl_mods.items():
- rctx.file("{}.json".format(whl_name), mods)
-
-_whl_mods_repo = repository_rule(
- doc = """\
-This rule creates json files based on the whl_mods attribute.
-""",
- implementation = _whl_mods_repo_impl,
- attrs = {
- "whl_mods": attr.string_dict(
- mandatory = True,
- doc = "JSON endcoded string that is provided to wheel_builder.py",
- ),
- },
-)
+pip = pypi
+pip_internal = pypi_internal
diff --git a/python/private/normalize_platform.bzl b/python/private/normalize_platform.bzl
deleted file mode 100644
index 633062f..0000000
--- a/python/private/normalize_platform.bzl
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2024 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
new file mode 100644
index 0000000..1530837
--- /dev/null
+++ b/python/private/pypi/BUILD.bazel
@@ -0,0 +1,238 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
+
+package(default_visibility = ["//:__subpackages__"])
+
+licenses(["notice"])
+
+filegroup(
+ name = "distribution",
+ srcs = glob(["**"]),
+ visibility = ["//python/private:__pkg__"],
+)
+
+# Filegroup of bzl files that can be used by downstream rules for documentation generation
+filegroup(
+ name = "bzl",
+ srcs = glob(["**/*.bzl"]),
+ visibility = ["//python/private:__pkg__"],
+)
+
+# Keep sorted by library name and keep the files named by the main symbol they export
+
+bzl_library(
+ name = "attrs_bzl",
+ srcs = ["attrs.bzl"],
+)
+
+bzl_library(
+ name = "bzlmod_bzl",
+ srcs = ["bzlmod.bzl"],
+ deps = [
+ ":attrs_bzl",
+ ":hub_repository_bzl",
+ ":parse_requirements_bzl",
+ ":parse_whl_name_bzl",
+ ":pip_repository_attrs_bzl",
+ ":simpleapi_download_bzl",
+ ":whl_library_bzl",
+ ":whl_repo_name_bzl",
+ "//python/private:full_version_bzl",
+ "//python/private:normalize_name_bzl",
+ "//python/private:version_label_bzl",
+ "@bazel_features//:features",
+ ] + [
+ "@pythons_hub//:interpreters_bzl",
+ ] if BZLMOD_ENABLED else [],
+)
+
+bzl_library(
+ name = "config_settings_bzl",
+ srcs = ["config_settings.bzl"],
+ deps = ["flags_bzl"],
+)
+
+bzl_library(
+ name = "flags_bzl",
+ srcs = ["flags.bzl"],
+ deps = ["//python/private:enum_bzl"],
+)
+
+bzl_library(
+ name = "generate_whl_library_build_bazel_bzl",
+ srcs = ["generate_whl_library_build_bazel.bzl"],
+ deps = [
+ ":labels_bzl",
+ "//python/private:normalize_name_bzl",
+ ],
+)
+
+bzl_library(
+ name = "generate_group_library_build_bazel_bzl",
+ srcs = ["generate_group_library_build_bazel.bzl"],
+ deps = [
+ ":labels_bzl",
+ "//python/private:normalize_name_bzl",
+ ],
+)
+
+bzl_library(
+ name = "group_library_bzl",
+ srcs = ["group_library.bzl"],
+ deps = [
+ ":generate_group_library_build_bazel_bzl",
+ ],
+)
+
+bzl_library(
+ name = "hub_repository_bzl",
+ srcs = ["hub_repository.bzl"],
+ visibility = ["//:__subpackages__"],
+ deps = [
+ ":render_pkg_aliases_bzl",
+ "//python/private:text_util_bzl",
+ ],
+)
+
+bzl_library(
+ name = "index_sources_bzl",
+ srcs = ["index_sources.bzl"],
+)
+
+bzl_library(
+ name = "labels_bzl",
+ srcs = ["labels.bzl"],
+)
+
+bzl_library(
+ name = "package_annotation_bzl",
+ srcs = ["package_annotation.bzl"],
+)
+
+bzl_library(
+ name = "parse_requirements_bzl",
+ srcs = ["parse_requirements.bzl"],
+ deps = [
+ ":index_sources_bzl",
+ ":parse_requirements_txt_bzl",
+ ":whl_target_platforms_bzl",
+ "//python/private:normalize_name_bzl",
+ ],
+)
+
+bzl_library(
+ name = "parse_requirements_txt_bzl",
+ srcs = ["parse_requirements_txt.bzl"],
+)
+
+bzl_library(
+ name = "parse_simpleapi_html_bzl",
+ srcs = ["parse_simpleapi_html.bzl"],
+)
+
+bzl_library(
+ name = "parse_whl_name_bzl",
+ srcs = ["parse_whl_name.bzl"],
+)
+
+bzl_library(
+ name = "patch_whl_bzl",
+ srcs = ["patch_whl.bzl"],
+ deps = [
+ ":parse_whl_name_bzl",
+ "//python/private:repo_utils_bzl",
+ ],
+)
+
+bzl_library(
+ name = "pip_repository_bzl",
+ srcs = ["pip_repository.bzl"],
+ deps = [
+ ":attrs_bzl",
+ ":parse_requirements_bzl",
+ ":pip_repository_attrs_bzl",
+ ":render_pkg_aliases_bzl",
+ "//python/private:normalize_name_bzl",
+ "//python/private:repo_utils_bzl",
+ "//python/private:text_util_bzl",
+ "@bazel_skylib//lib:sets",
+ ],
+)
+
+bzl_library(
+ name = "pip_repository_attrs_bzl",
+ srcs = ["pip_repository_attrs.bzl"],
+)
+
+bzl_library(
+ name = "render_pkg_aliases_bzl",
+ srcs = ["render_pkg_aliases.bzl"],
+ deps = [
+ ":generate_group_library_build_bazel_bzl",
+ ":labels_bzl",
+ ":parse_whl_name_bzl",
+ ":whl_target_platforms_bzl",
+ "//python/private:normalize_name_bzl",
+ "//python/private:text_util_bzl",
+ ],
+)
+
+bzl_library(
+ name = "simpleapi_download_bzl",
+ srcs = ["simpleapi_download.bzl"],
+ deps = [
+ ":parse_simpleapi_html_bzl",
+ "//python/private:auth_bzl",
+ "//python/private:normalize_name_bzl",
+ "//python/private:text_util_bzl",
+ "@bazel_features//:features",
+ ],
+)
+
+bzl_library(
+ name = "whl_library_bzl",
+ srcs = ["whl_library.bzl"],
+ deps = [
+ ":attrs_bzl",
+ ":generate_whl_library_build_bazel_bzl",
+ ":parse_whl_name_bzl",
+ ":patch_whl_bzl",
+ ":whl_target_platforms_bzl",
+ "//python:repositories_bzl",
+ "//python:versions_bzl",
+ "//python/pip_install:repositories_bzl",
+ "//python/private:auth_bzl",
+ "//python/private:envsubst_bzl",
+ "//python/private:repo_utils_bzl",
+ "//python/private:toolchains_repo_bzl",
+ ],
+)
+
+bzl_library(
+ name = "whl_repo_name_bzl",
+ srcs = ["whl_repo_name.bzl"],
+ deps = [
+ ":parse_whl_name_bzl",
+ "//python/private:normalize_name_bzl",
+ ],
+)
+
+bzl_library(
+ name = "whl_target_platforms_bzl",
+ srcs = ["whl_target_platforms.bzl"],
+ deps = [":parse_whl_name_bzl"],
+)
diff --git a/python/private/pypi/README.md b/python/private/pypi/README.md
new file mode 100644
index 0000000..6be5703
--- /dev/null
+++ b/python/private/pypi/README.md
@@ -0,0 +1,9 @@
+# PyPI integration code
+
+This code is for integrating with PyPI and other compatible indexes. At the
+moment we have code for:
+* Downloading packages using `pip` or `repository_ctx.download`.
+* Interacting with PyPI compatible indexes via [SimpleAPI] spec.
+* Locking a `requirements.in` or [PEP621] compliant `pyproject.toml`.
+
+[PEP621]: https://peps.python.org/pep-0621/
diff --git a/python/private/pypi/attrs.bzl b/python/private/pypi/attrs.bzl
new file mode 100644
index 0000000..79ffea5
--- /dev/null
+++ b/python/private/pypi/attrs.bzl
@@ -0,0 +1,224 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"common attributes for whl_library and pip_repository"
+
+ATTRS = {
+ "download_only": attr.bool(
+ doc = """
+Whether to use "pip download" instead of "pip wheel". Disables building wheels from source, but allows use of
+--platform, --python-version, --implementation, and --abi in --extra_pip_args to download wheels for a different
+platform from the host platform.
+ """,
+ ),
+ "enable_implicit_namespace_pkgs": attr.bool(
+ default = False,
+ doc = """
+If true, disables conversion of native namespace packages into pkg-util style namespace packages. When set all py_binary
+and py_test targets must specify either `legacy_create_init=False` or the global Bazel option
+`--incompatible_default_to_explicit_init_py` to prevent `__init__.py` being automatically generated in every directory.
+
+This option is required to support some packages which cannot handle the conversion to pkg-util style.
+ """,
+ ),
+ "environment": attr.string_dict(
+ doc = """
+Environment variables to set in the pip subprocess.
+Can be used to set common variables such as `http_proxy`, `https_proxy` and `no_proxy`
+Note that pip is run with "--isolated" on the CLI so `PIP_<VAR>_<NAME>`
+style env vars are ignored, but env vars that control requests and urllib3
+can be passed. If you need `PIP_<VAR>_<NAME>`, take a look at `extra_pip_args`
+and `envsubst`.
+ """,
+ default = {},
+ ),
+ "envsubst": attr.string_list(
+ mandatory = False,
+ doc = """\
+A list of environment variables to substitute (e.g. `["PIP_INDEX_URL",
+"PIP_RETRIES"]`). The corresponding variables are expanded in `extra_pip_args`
+using the syntax `$VARNAME` or `${VARNAME}` (expanding to empty string if unset)
+or `${VARNAME:-default}` (expanding to default if the variable is unset or empty
+in the environment). Note: On Bazel 6 and Bazel 7.0 changes to the variables named
+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.
+""",
+ ),
+ "experimental_target_platforms": attr.string_list(
+ default = [],
+ doc = """\
+A list of platforms that we will generate the conditional dependency graph for
+cross platform wheels by parsing the wheel metadata. This will generate the
+correct dependencies for packages like `sphinx` or `pylint`, which include
+`colorama` when installed and used on Windows platforms.
+
+An empty list means falling back to the legacy behaviour where the host
+platform is the target platform.
+
+WARNING: It may not work as expected in cases where the python interpreter
+implementation that is being used at runtime is different between different platforms.
+This has been tested for CPython only.
+
+For specific target platforms use values of the form `<os>_<arch>` where `<os>`
+is one of `linux`, `osx`, `windows` and arch is one of `x86_64`, `x86_32`,
+`aarch64`, `s390x` and `ppc64le`.
+
+You can also target a specific Python version by using `cp3<minor_version>_<os>_<arch>`.
+If multiple python versions are specified as target platforms, then select statements
+of the `lib` and `whl` targets will include usage of version aware toolchain config
+settings like `@rules_python//python/config_settings:is_python_3.y`.
+
+Special values: `host` (for generating deps for the host platform only) and
+`<prefix>_*` values. For example, `cp39_*`, `linux_*`, `cp39_linux_*`.
+
+NOTE: this is not for cross-compiling Python wheels but rather for parsing the `whl` METADATA correctly.
+""",
+ ),
+ "extra_pip_args": attr.string_list(
+ doc = """Extra arguments to pass on to pip. Must not contain spaces.
+
+Supports environment variables using the syntax `$VARNAME` or
+`${VARNAME}` (expanding to empty string if unset) or
+`${VARNAME:-default}` (expanding to default if the variable is unset
+or empty in the environment), if `"VARNAME"` is listed in the
+`envsubst` attribute. See also `envsubst`.
+""",
+ ),
+ "isolated": attr.bool(
+ doc = """\
+Whether or not to pass the [--isolated](https://pip.pypa.io/en/stable/cli/pip/#cmdoption-isolated) flag to
+the underlying pip command. Alternatively, the `RULES_PYTHON_PIP_ISOLATED` environment variable can be used
+to control this flag.
+""",
+ default = True,
+ ),
+ "pip_data_exclude": attr.string_list(
+ doc = "Additional data exclusion parameters to add to the pip packages BUILD file.",
+ ),
+ "python_interpreter": attr.string(
+ doc = """\
+The python interpreter to use. This can either be an absolute path or the name
+of a binary found on the host's `PATH` environment variable. If no value is set
+`python3` is defaulted for Unix systems and `python.exe` for Windows.
+""",
+ # NOTE: This attribute should not have a default. See `_get_python_interpreter_attr`
+ # default = "python3"
+ ),
+ "python_interpreter_target": attr.label(
+ allow_single_file = True,
+ doc = """
+If you are using a custom python interpreter built by another repository rule,
+use this attribute to specify its BUILD target. This allows pip_repository to invoke
+pip using the same interpreter as your toolchain. If set, takes precedence over
+python_interpreter. An example value: "@python3_x86_64-unknown-linux-gnu//:python".
+""",
+ ),
+ "quiet": attr.bool(
+ default = True,
+ doc = """\
+If True, suppress printing stdout and stderr output to the terminal.
+
+If you would like to get more diagnostic output, please use:
+
+ RULES_PYTHON_REPO_DEBUG=1
+
+or
+
+ RULES_PYTHON_REPO_DEBUG_VERBOSITY=<INFO|DEBUG|TRACE>
+""",
+ ),
+ # 600 is documented as default here: https://docs.bazel.build/versions/master/skylark/lib/repository_ctx.html#execute
+ "timeout": attr.int(
+ default = 600,
+ doc = "Timeout (in seconds) on the rule's execution duration.",
+ ),
+}
+
+def use_isolated(ctx, attr):
+ """Determine whether or not to pass the pip `--isolated` flag to the pip invocation.
+
+ Args:
+ ctx: repository or module context
+ attr: attributes for the repo rule or tag extension
+
+ Returns:
+ True if --isolated should be passed
+ """
+ use_isolated = attr.isolated
+
+ # The environment variable will take precedence over the attribute
+ isolated_env = ctx.os.environ.get("RULES_PYTHON_PIP_ISOLATED", None)
+ if isolated_env != None:
+ if isolated_env.lower() in ("0", "false"):
+ use_isolated = False
+ else:
+ use_isolated = True
+
+ return use_isolated
diff --git a/python/private/pypi/bzlmod.bzl b/python/private/pypi/bzlmod.bzl
new file mode 100644
index 0000000..e98208a
--- /dev/null
+++ b/python/private/pypi/bzlmod.bzl
@@ -0,0 +1,818 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"pip module extension for use with bzlmod"
+
+load("@bazel_features//:features.bzl", "bazel_features")
+load("@pythons_hub//:interpreters.bzl", "DEFAULT_PYTHON_VERSION", "INTERPRETER_LABELS")
+load("//python/private:auth.bzl", "AUTH_ATTRS")
+load("//python/private:normalize_name.bzl", "normalize_name")
+load("//python/private:repo_utils.bzl", "repo_utils")
+load("//python/private:version_label.bzl", "version_label")
+load(":attrs.bzl", "use_isolated")
+load(":hub_repository.bzl", "hub_repository")
+load(":parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement")
+load(":parse_whl_name.bzl", "parse_whl_name")
+load(":pip_repository_attrs.bzl", "ATTRS")
+load(":render_pkg_aliases.bzl", "whl_alias")
+load(":simpleapi_download.bzl", "simpleapi_download")
+load(":whl_library.bzl", "whl_library")
+load(":whl_repo_name.bzl", "whl_repo_name")
+
+def _parse_version(version):
+ major, _, version = version.partition(".")
+ minor, _, version = version.partition(".")
+ patch, _, version = version.partition(".")
+ build, _, version = version.partition(".")
+
+ return struct(
+ # use semver vocabulary here
+ major = major,
+ minor = minor,
+ patch = patch, # this is called `micro` in the Python interpreter versioning scheme
+ build = build,
+ )
+
+def _major_minor_version(version):
+ version = _parse_version(version)
+ return "{}.{}".format(version.major, version.minor)
+
+def _whl_mods_impl(mctx):
+ """Implementation of the pip.whl_mods tag class.
+
+ This creates the JSON files used to modify the creation of different wheels.
+"""
+ whl_mods_dict = {}
+ for mod in mctx.modules:
+ for whl_mod_attr in mod.tags.whl_mods:
+ if whl_mod_attr.hub_name not in whl_mods_dict.keys():
+ whl_mods_dict[whl_mod_attr.hub_name] = {whl_mod_attr.whl_name: whl_mod_attr}
+ elif whl_mod_attr.whl_name in whl_mods_dict[whl_mod_attr.hub_name].keys():
+ # We cannot have the same wheel name in the same hub, as we
+ # will create the same JSON file name.
+ fail("""\
+Found same whl_name '{}' in the same hub '{}', please use a different hub_name.""".format(
+ whl_mod_attr.whl_name,
+ whl_mod_attr.hub_name,
+ ))
+ else:
+ whl_mods_dict[whl_mod_attr.hub_name][whl_mod_attr.whl_name] = whl_mod_attr
+
+ for hub_name, whl_maps in whl_mods_dict.items():
+ whl_mods = {}
+
+ # create a struct that we can pass to the _whl_mods_repo rule
+ # to create the different JSON files.
+ for whl_name, mods in whl_maps.items():
+ build_content = mods.additive_build_content
+ if mods.additive_build_content_file != None and mods.additive_build_content != "":
+ fail("""\
+You cannot use both the additive_build_content and additive_build_content_file arguments at the same time.
+""")
+ elif mods.additive_build_content_file != None:
+ build_content = mctx.read(mods.additive_build_content_file)
+
+ whl_mods[whl_name] = json.encode(struct(
+ additive_build_content = build_content,
+ copy_files = mods.copy_files,
+ copy_executables = mods.copy_executables,
+ data = mods.data,
+ data_exclude_glob = mods.data_exclude_glob,
+ srcs_exclude_glob = mods.srcs_exclude_glob,
+ ))
+
+ _whl_mods_repo(
+ name = hub_name,
+ whl_mods = whl_mods,
+ )
+
+def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides, group_map, simpleapi_cache):
+ logger = repo_utils.logger(module_ctx)
+ python_interpreter_target = pip_attr.python_interpreter_target
+ is_hub_reproducible = True
+
+ # if we do not have the python_interpreter set in the attributes
+ # we programmatically find it.
+ hub_name = pip_attr.hub_name
+ if python_interpreter_target == None and not pip_attr.python_interpreter:
+ python_name = "python_{}_host".format(
+ pip_attr.python_version.replace(".", "_"),
+ )
+ if python_name not in INTERPRETER_LABELS:
+ fail((
+ "Unable to find interpreter for pip hub '{hub_name}' for " +
+ "python_version={version}: Make sure a corresponding " +
+ '`python.toolchain(python_version="{version}")` call exists.' +
+ "Expected to find {python_name} among registered versions:\n {labels}"
+ ).format(
+ hub_name = hub_name,
+ version = pip_attr.python_version,
+ python_name = python_name,
+ labels = " \n".join(INTERPRETER_LABELS),
+ ))
+ python_interpreter_target = INTERPRETER_LABELS[python_name]
+
+ pip_name = "{}_{}".format(
+ hub_name,
+ version_label(pip_attr.python_version),
+ )
+ major_minor = _major_minor_version(pip_attr.python_version)
+
+ if hub_name not in whl_map:
+ whl_map[hub_name] = {}
+
+ whl_modifications = {}
+ if pip_attr.whl_modifications != None:
+ for mod, whl_name in pip_attr.whl_modifications.items():
+ whl_modifications[whl_name] = mod
+
+ if pip_attr.experimental_requirement_cycles:
+ requirement_cycles = {
+ name: [normalize_name(whl_name) for whl_name in whls]
+ for name, whls in pip_attr.experimental_requirement_cycles.items()
+ }
+
+ whl_group_mapping = {
+ whl_name: group_name
+ for group_name, group_whls in requirement_cycles.items()
+ for whl_name in group_whls
+ }
+
+ # TODO @aignas 2024-04-05: how do we support different requirement
+ # cycles for different abis/oses? For now we will need the users to
+ # assume the same groups across all versions/platforms until we start
+ # using an alternative cycle resolution strategy.
+ group_map[hub_name] = pip_attr.experimental_requirement_cycles
+ else:
+ whl_group_mapping = {}
+ requirement_cycles = {}
+
+ # Create a new wheel library for each of the different whls
+
+ get_index_urls = None
+ if pip_attr.experimental_index_url:
+ if pip_attr.download_only:
+ fail("Currently unsupported to use `download_only` and `experimental_index_url`")
+
+ get_index_urls = lambda ctx, distributions: simpleapi_download(
+ ctx,
+ attr = struct(
+ index_url = pip_attr.experimental_index_url,
+ extra_index_urls = pip_attr.experimental_extra_index_urls or [],
+ index_url_overrides = pip_attr.experimental_index_url_overrides or {},
+ sources = distributions,
+ envsubst = pip_attr.envsubst,
+ # Auth related info
+ netrc = pip_attr.netrc,
+ auth_patterns = pip_attr.auth_patterns,
+ ),
+ cache = simpleapi_cache,
+ parallel_download = pip_attr.parallel_download,
+ )
+
+ requirements_by_platform = parse_requirements(
+ module_ctx,
+ requirements_by_platform = pip_attr.requirements_by_platform,
+ requirements_linux = pip_attr.requirements_linux,
+ requirements_lock = pip_attr.requirements_lock,
+ requirements_osx = pip_attr.requirements_darwin,
+ requirements_windows = pip_attr.requirements_windows,
+ extra_pip_args = pip_attr.extra_pip_args,
+ get_index_urls = get_index_urls,
+ python_version = major_minor,
+ logger = logger,
+ )
+
+ repository_platform = host_platform(module_ctx.os)
+ for whl_name, requirements in requirements_by_platform.items():
+ # We are not using the "sanitized name" because the user
+ # would need to guess what name we modified the whl name
+ # to.
+ annotation = whl_modifications.get(whl_name)
+ whl_name = normalize_name(whl_name)
+
+ group_name = whl_group_mapping.get(whl_name)
+ group_deps = requirement_cycles.get(group_name, [])
+
+ # Construct args separately so that the lock file can be smaller and does not include unused
+ # attrs.
+ whl_library_args = dict(
+ repo = pip_name,
+ dep_template = "@{}//{{name}}:{{target}}".format(hub_name),
+ )
+ maybe_args = dict(
+ # The following values are safe to omit if they have false like values
+ annotation = annotation,
+ download_only = pip_attr.download_only,
+ enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs,
+ environment = pip_attr.environment,
+ envsubst = pip_attr.envsubst,
+ experimental_target_platforms = pip_attr.experimental_target_platforms,
+ group_deps = group_deps,
+ group_name = group_name,
+ pip_data_exclude = pip_attr.pip_data_exclude,
+ python_interpreter = pip_attr.python_interpreter,
+ python_interpreter_target = python_interpreter_target,
+ whl_patches = {
+ p: json.encode(args)
+ for p, args in whl_overrides.get(whl_name, {}).items()
+ },
+ )
+ whl_library_args.update({k: v for k, v in maybe_args.items() if v})
+ maybe_args_with_default = dict(
+ # The following values have defaults next to them
+ isolated = (use_isolated(module_ctx, pip_attr), True),
+ quiet = (pip_attr.quiet, True),
+ timeout = (pip_attr.timeout, 600),
+ )
+ whl_library_args.update({
+ k: v
+ for k, (v, default) in maybe_args_with_default.items()
+ if v != default
+ })
+
+ if get_index_urls:
+ # TODO @aignas 2024-05-26: move to a separate function
+ found_something = False
+ for requirement in requirements:
+ for distribution in requirement.whls + [requirement.sdist]:
+ if not distribution:
+ # sdist may be None
+ continue
+
+ found_something = True
+ is_hub_reproducible = False
+
+ if pip_attr.netrc:
+ whl_library_args["netrc"] = pip_attr.netrc
+ if pip_attr.auth_patterns:
+ whl_library_args["auth_patterns"] = pip_attr.auth_patterns
+
+ # pip is not used to download wheels and the python `whl_library` helpers are only extracting things
+ whl_library_args.pop("extra_pip_args", None)
+
+ # This is no-op because pip is not used to download the wheel.
+ whl_library_args.pop("download_only", None)
+
+ repo_name = whl_repo_name(pip_name, distribution.filename, distribution.sha256)
+ whl_library_args["requirement"] = requirement.srcs.requirement
+ whl_library_args["urls"] = [distribution.url]
+ whl_library_args["sha256"] = distribution.sha256
+ whl_library_args["filename"] = distribution.filename
+ whl_library_args["experimental_target_platforms"] = requirement.target_platforms
+
+ # Pure python wheels or sdists may need to have a platform here
+ target_platforms = None
+ if distribution.filename.endswith("-any.whl") or not distribution.filename.endswith(".whl"):
+ if len(requirements) > 1:
+ target_platforms = requirement.target_platforms
+
+ whl_library(name = repo_name, **dict(sorted(whl_library_args.items())))
+
+ whl_map[hub_name].setdefault(whl_name, []).append(
+ whl_alias(
+ repo = repo_name,
+ version = major_minor,
+ filename = distribution.filename,
+ target_platforms = target_platforms,
+ ),
+ )
+
+ if found_something:
+ continue
+
+ requirement = select_requirement(
+ requirements,
+ platform = repository_platform,
+ )
+ if not requirement:
+ # Sometimes the package is not present for host platform if there
+ # are whls specified only in particular requirements files, in that
+ # case just continue, however, if the download_only flag is set up,
+ # then the user can also specify the target platform of the wheel
+ # packages they want to download, in that case there will be always
+ # a requirement here, so we will not be in this code branch.
+ continue
+ elif get_index_urls:
+ logger.warn(lambda: "falling back to pip for installing the right file for {}".format(requirement.requirement_line))
+
+ whl_library_args["requirement"] = requirement.requirement_line
+ if requirement.extra_pip_args:
+ whl_library_args["extra_pip_args"] = requirement.extra_pip_args
+
+ # We sort so that the lock-file remains the same no matter the order of how the
+ # args are manipulated in the code going before.
+ repo_name = "{}_{}".format(pip_name, whl_name)
+ whl_library(name = repo_name, **dict(sorted(whl_library_args.items())))
+ whl_map[hub_name].setdefault(whl_name, []).append(
+ whl_alias(
+ repo = repo_name,
+ version = major_minor,
+ ),
+ )
+
+ return is_hub_reproducible
+
+def _pip_impl(module_ctx):
+ """Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories.
+
+ This implementation iterates through all of the `pip.parse` calls and creates
+ different pip hub repositories based on the "hub_name". Each of the
+ pip calls create spoke repos that uses a specific Python interpreter.
+
+ In a MODULES.bazel file we have:
+
+ pip.parse(
+ hub_name = "pip",
+ python_version = 3.9,
+ requirements_lock = "//:requirements_lock_3_9.txt",
+ requirements_windows = "//:requirements_windows_3_9.txt",
+ )
+ pip.parse(
+ hub_name = "pip",
+ python_version = 3.10,
+ requirements_lock = "//:requirements_lock_3_10.txt",
+ requirements_windows = "//:requirements_windows_3_10.txt",
+ )
+
+ For instance, we have a hub with the name of "pip".
+ A repository named the following is created. It is actually called last when
+ all of the pip spokes are collected.
+
+ - @@rules_python~override~pip~pip
+
+ As shown in the example code above we have the following.
+ Two different pip.parse statements exist in MODULE.bazel provide the hub_name "pip".
+ These definitions create two different pip spoke repositories that are
+ related to the hub "pip".
+ One spoke uses Python 3.9 and the other uses Python 3.10. This code automatically
+ determines the Python version and the interpreter.
+ Both of these pip spokes contain requirements files that includes websocket
+ and its dependencies.
+
+ We also need repositories for the wheels that the different pip spokes contain.
+ For each Python version a different wheel repository is created. In our example
+ each pip spoke had a requirements file that contained websockets. We
+ then create two different wheel repositories that are named the following.
+
+ - @@rules_python~override~pip~pip_39_websockets
+ - @@rules_python~override~pip~pip_310_websockets
+
+ And if the wheel has any other dependencies subsequent wheels are created in the same fashion.
+
+ The hub repository has aliases for `pkg`, `data`, etc, which have a select that resolves to
+ a spoke repository depending on the Python version.
+
+ Also we may have more than one hub as defined in a MODULES.bazel file. So we could have multiple
+ hubs pointing to various different pip spokes.
+
+ Some other business rules notes. A hub can only have one spoke per Python version. We cannot
+ have a hub named "pip" that has two spokes that use the Python 3.9 interpreter. Second
+ we cannot have the same hub name used in sub-modules. The hub name has to be globally
+ unique.
+
+ This implementation also handles the creation of whl_modification JSON files that are used
+ during the creation of wheel libraries. These JSON files used via the annotations argument
+ when calling wheel_installer.py.
+
+ Args:
+ module_ctx: module contents
+ """
+
+ # Build all of the wheel modifications if the tag class is called.
+ _whl_mods_impl(module_ctx)
+
+ _overriden_whl_set = {}
+ whl_overrides = {}
+
+ for module in module_ctx.modules:
+ for attr in module.tags.override:
+ if not module.is_root:
+ fail("overrides are only supported in root modules")
+
+ if not attr.file.endswith(".whl"):
+ fail("Only whl overrides are supported at this time")
+
+ whl_name = normalize_name(parse_whl_name(attr.file).distribution)
+
+ if attr.file in _overriden_whl_set:
+ fail("Duplicate module overrides for '{}'".format(attr.file))
+ _overriden_whl_set[attr.file] = None
+
+ for patch in attr.patches:
+ if whl_name not in whl_overrides:
+ whl_overrides[whl_name] = {}
+
+ if patch not in whl_overrides[whl_name]:
+ whl_overrides[whl_name][patch] = struct(
+ patch_strip = attr.patch_strip,
+ whls = [],
+ )
+
+ whl_overrides[whl_name][patch].whls.append(attr.file)
+
+ # Used to track all the different pip hubs and the spoke pip Python
+ # versions.
+ pip_hub_map = {}
+
+ # Keeps track of all the hub's whl repos across the different versions.
+ # dict[hub, dict[whl, dict[version, str pip]]]
+ # Where hub, whl, and pip are the repo names
+ hub_whl_map = {}
+ hub_group_map = {}
+
+ simpleapi_cache = {}
+ is_extension_reproducible = True
+
+ for mod in module_ctx.modules:
+ for pip_attr in mod.tags.parse:
+ hub_name = pip_attr.hub_name
+ if hub_name not in pip_hub_map:
+ pip_hub_map[pip_attr.hub_name] = struct(
+ module_name = mod.name,
+ python_versions = [pip_attr.python_version],
+ )
+ elif pip_hub_map[hub_name].module_name != mod.name:
+ # We cannot have two hubs with the same name in different
+ # modules.
+ fail((
+ "Duplicate cross-module pip hub named '{hub}': pip hub " +
+ "names must be unique across modules. First defined " +
+ "by module '{first_module}', second attempted by " +
+ "module '{second_module}'"
+ ).format(
+ hub = hub_name,
+ first_module = pip_hub_map[hub_name].module_name,
+ second_module = mod.name,
+ ))
+
+ elif pip_attr.python_version in pip_hub_map[hub_name].python_versions:
+ fail((
+ "Duplicate pip python version '{version}' for hub " +
+ "'{hub}' in module '{module}': the Python versions " +
+ "used for a hub must be unique"
+ ).format(
+ hub = hub_name,
+ module = mod.name,
+ version = pip_attr.python_version,
+ ))
+ else:
+ pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version)
+
+ is_hub_reproducible = _create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides, hub_group_map, simpleapi_cache)
+ is_extension_reproducible = is_extension_reproducible and is_hub_reproducible
+
+ for hub_name, whl_map in hub_whl_map.items():
+ hub_repository(
+ name = hub_name,
+ repo_name = hub_name,
+ whl_map = {
+ key: json.encode(value)
+ for key, value in whl_map.items()
+ },
+ default_version = _major_minor_version(DEFAULT_PYTHON_VERSION),
+ groups = hub_group_map.get(hub_name),
+ )
+
+ if bazel_features.external_deps.extension_metadata_has_reproducible:
+ # If we are not using the `experimental_index_url feature, the extension is fully
+ # deterministic and we don't need to create a lock entry for it.
+ #
+ # In order to be able to dogfood the `experimental_index_url` feature before it gets
+ # stabilized, we have created the `_pip_non_reproducible` function, that will result
+ # in extra entries in the lock file.
+ return module_ctx.extension_metadata(reproducible = is_extension_reproducible)
+ else:
+ return None
+
+def _pip_non_reproducible(module_ctx):
+ _pip_impl(module_ctx)
+
+ # We default to calling the PyPI index and that will go into the
+ # MODULE.bazel.lock file, hence return nothing here.
+ return None
+
+def _pip_parse_ext_attrs(**kwargs):
+ """Get the attributes for the pip extension.
+
+ Args:
+ **kwargs: A kwarg for setting defaults for the specific attributes. The
+ key is expected to be the same as the attribute key.
+
+ Returns:
+ A dict of attributes.
+ """
+ attrs = dict({
+ "experimental_extra_index_urls": attr.string_list(
+ doc = """\
+The extra index URLs to use for downloading wheels using bazel downloader.
+Each value is going to be subject to `envsubst` substitutions if necessary.
+
+The indexes must support Simple API as described here:
+https://packaging.python.org/en/latest/specifications/simple-repository-api/
+
+This is equivalent to `--extra-index-urls` `pip` option.
+""",
+ default = [],
+ ),
+ "experimental_index_url": attr.string(
+ default = kwargs.get("experimental_index_url", ""),
+ doc = """\
+The index URL to use for downloading wheels using bazel downloader. This value is going
+to be subject to `envsubst` substitutions if necessary.
+
+The indexes must support Simple API as described here:
+https://packaging.python.org/en/latest/specifications/simple-repository-api/
+
+In the future this could be defaulted to `https://pypi.org` when this feature becomes
+stable.
+
+This is equivalent to `--index-url` `pip` option.
+""",
+ ),
+ "experimental_index_url_overrides": attr.string_dict(
+ doc = """\
+The index URL overrides for each package to use for downloading wheels using
+bazel downloader. This value is going to be subject to `envsubst` substitutions
+if necessary.
+
+The key is the package name (will be normalized before usage) and the value is the
+index URL.
+
+This design pattern has been chosen in order to be fully deterministic about which
+packages come from which source. We want to avoid issues similar to what happened in
+https://pytorch.org/blog/compromised-nightly-dependency/.
+
+The indexes must support Simple API as described here:
+https://packaging.python.org/en/latest/specifications/simple-repository-api/
+""",
+ ),
+ "hub_name": attr.string(
+ mandatory = True,
+ doc = """
+The name of the repo pip dependencies will be accessible from.
+
+This name must be unique between modules; unless your module is guaranteed to
+always be the root module, it's highly recommended to include your module name
+in the hub name. Repo mapping, `use_repo(..., pip="my_modules_pip_deps")`, can
+be used for shorter local names within your module.
+
+Within a module, the same `hub_name` can be specified to group different Python
+versions of pip dependencies under one repository name. This allows using a
+Python version-agnostic name when referring to pip dependencies; the
+correct version will be automatically selected.
+
+Typically, a module will only have a single hub of pip dependencies, but this
+is not required. Each hub is a separate resolution of pip dependencies. This
+means if different programs need different versions of some library, separate
+hubs can be created, and each program can use its respective hub's targets.
+Targets from different hubs should not be used together.
+""",
+ ),
+ "parallel_download": attr.bool(
+ doc = """\
+The flag allows to make use of parallel downloading feature in bazel 7.1 and above
+when the bazel downloader is used. This is by default enabled as it improves the
+performance by a lot, but in case the queries to the simple API are very expensive
+or when debugging authentication issues one may want to disable this feature.
+
+NOTE, This will download (potentially duplicate) data for multiple packages if
+there is more than one index available, but in general this should be negligible
+because the simple API calls are very cheap and the user should not notice any
+extra overhead.
+
+If we are in synchronous mode, then we will use the first result that we
+find in case extra indexes are specified.
+""",
+ default = True,
+ ),
+ "python_version": attr.string(
+ mandatory = True,
+ doc = """
+The Python version the dependencies are targetting, in Major.Minor format
+(e.g., "3.11") or patch level granularity (e.g. "3.11.1").
+
+If an interpreter isn't explicitly provided (using `python_interpreter` or
+`python_interpreter_target`), then the version specified here must have
+a corresponding `python.toolchain()` configured.
+""",
+ ),
+ "whl_modifications": attr.label_keyed_string_dict(
+ mandatory = False,
+ doc = """\
+A dict of labels to wheel names that is typically generated by the whl_modifications.
+The labels are JSON config files describing the modifications.
+""",
+ ),
+ }, **ATTRS)
+ attrs.update(AUTH_ATTRS)
+
+ return attrs
+
+def _whl_mod_attrs():
+ attrs = {
+ "additive_build_content": attr.string(
+ doc = "(str, optional): Raw text to add to the generated `BUILD` file of a package.",
+ ),
+ "additive_build_content_file": attr.label(
+ doc = """\
+(label, optional): path to a BUILD file to add to the generated
+`BUILD` file of a package. You cannot use both additive_build_content and additive_build_content_file
+arguments at the same time.""",
+ ),
+ "copy_executables": attr.string_dict(
+ doc = """\
+(dict, optional): A mapping of `src` and `out` files for
+[@bazel_skylib//rules:copy_file.bzl][cf]. Targets generated here will also be flagged as
+executable.""",
+ ),
+ "copy_files": attr.string_dict(
+ doc = """\
+(dict, optional): A mapping of `src` and `out` files for
+[@bazel_skylib//rules:copy_file.bzl][cf]""",
+ ),
+ "data": attr.string_list(
+ doc = """\
+(list, optional): A list of labels to add as `data` dependencies to
+the generated `py_library` target.""",
+ ),
+ "data_exclude_glob": attr.string_list(
+ doc = """\
+(list, optional): A list of exclude glob patterns to add as `data` to
+the generated `py_library` target.""",
+ ),
+ "hub_name": attr.string(
+ doc = """\
+Name of the whl modification, hub we use this name to set the modifications for
+pip.parse. If you have different pip hubs you can use a different name,
+otherwise it is best practice to just use one.
+
+You cannot have the same `hub_name` in different modules. You can reuse the same
+name in the same module for different wheels that you put in the same hub, but you
+cannot have a child module that uses the same `hub_name`.
+""",
+ mandatory = True,
+ ),
+ "srcs_exclude_glob": attr.string_list(
+ doc = """\
+(list, optional): A list of labels to add as `srcs` to the generated
+`py_library` target.""",
+ ),
+ "whl_name": attr.string(
+ doc = "The whl name that the modifications are used for.",
+ mandatory = True,
+ ),
+ }
+ return attrs
+
+# NOTE: the naming of 'override' is taken from the bzlmod native
+# 'archive_override', 'git_override' bzlmod functions.
+_override_tag = tag_class(
+ attrs = {
+ "file": attr.string(
+ doc = """\
+The Python distribution file name which needs to be patched. This will be
+applied to all repositories that setup this distribution via the pip.parse tag
+class.""",
+ mandatory = True,
+ ),
+ "patch_strip": attr.int(
+ default = 0,
+ doc = """\
+The number of leading path segments to be stripped from the file name in the
+patches.""",
+ ),
+ "patches": attr.label_list(
+ doc = """\
+A list of patches to apply to the repository *after* 'whl_library' is extracted
+and BUILD.bazel file is generated.""",
+ mandatory = True,
+ ),
+ },
+ doc = """\
+Apply any overrides (e.g. patches) to a given Python distribution defined by
+other tags in this extension.""",
+)
+
+pypi = module_extension(
+ doc = """\
+This extension is used to make dependencies from pip available.
+
+pip.parse:
+To use, call `pip.parse()` and specify `hub_name` and your requirements file.
+Dependencies will be downloaded and made available in a repo named after the
+`hub_name` argument.
+
+Each `pip.parse()` call configures a particular Python version. Multiple calls
+can be made to configure different Python versions, and will be grouped by
+the `hub_name` argument. This allows the same logical name, e.g. `@pip//numpy`
+to automatically resolve to different, Python version-specific, libraries.
+
+pip.whl_mods:
+This tag class is used to help create JSON files to describe modifications to
+the BUILD files for wheels.
+""",
+ implementation = _pip_impl,
+ tag_classes = {
+ "override": _override_tag,
+ "parse": tag_class(
+ attrs = _pip_parse_ext_attrs(),
+ doc = """\
+This tag class is used to create a pip hub and all of the spokes that are part of that hub.
+This tag class reuses most of the pip attributes that are found in
+@rules_python//python/pip_install:pip_repository.bzl.
+The exception is it does not use the arg 'repo_prefix'. We set the repository
+prefix for the user and the alias arg is always True in bzlmod.
+""",
+ ),
+ "whl_mods": tag_class(
+ attrs = _whl_mod_attrs(),
+ doc = """\
+This tag class is used to create JSON file that are used when calling wheel_builder.py. These
+JSON files contain instructions on how to modify a wheel's project. Each of the attributes
+create different modifications based on the type of attribute. Previously to bzlmod these
+JSON files where referred to as annotations, and were renamed to whl_modifications in this
+extension.
+""",
+ ),
+ },
+)
+
+pypi_internal = module_extension(
+ doc = """\
+This extension is used to make dependencies from pypi available.
+
+For now this is intended to be used internally so that usage of the `pip`
+extension in `rules_python` does not affect the evaluations of the extension
+for the consumers.
+
+pip.parse:
+To use, call `pip.parse()` and specify `hub_name` and your requirements file.
+Dependencies will be downloaded and made available in a repo named after the
+`hub_name` argument.
+
+Each `pip.parse()` call configures a particular Python version. Multiple calls
+can be made to configure different Python versions, and will be grouped by
+the `hub_name` argument. This allows the same logical name, e.g. `@pypi//numpy`
+to automatically resolve to different, Python version-specific, libraries.
+
+pip.whl_mods:
+This tag class is used to help create JSON files to describe modifications to
+the BUILD files for wheels.
+""",
+ implementation = _pip_non_reproducible,
+ tag_classes = {
+ "override": _override_tag,
+ "parse": tag_class(
+ attrs = _pip_parse_ext_attrs(
+ experimental_index_url = "https://pypi.org/simple",
+ ),
+ doc = """\
+This tag class is used to create a pypi hub and all of the spokes that are part of that hub.
+This tag class reuses most of the pypi attributes that are found in
+@rules_python//python/pip_install:pip_repository.bzl.
+The exception is it does not use the arg 'repo_prefix'. We set the repository
+prefix for the user and the alias arg is always True in bzlmod.
+""",
+ ),
+ "whl_mods": tag_class(
+ attrs = _whl_mod_attrs(),
+ doc = """\
+This tag class is used to create JSON file that are used when calling wheel_builder.py. These
+JSON files contain instructions on how to modify a wheel's project. Each of the attributes
+create different modifications based on the type of attribute. Previously to bzlmod these
+JSON files where referred to as annotations, and were renamed to whl_modifications in this
+extension.
+""",
+ ),
+ },
+)
+
+def _whl_mods_repo_impl(rctx):
+ rctx.file("BUILD.bazel", "")
+ for whl_name, mods in rctx.attr.whl_mods.items():
+ rctx.file("{}.json".format(whl_name), mods)
+
+_whl_mods_repo = repository_rule(
+ doc = """\
+This rule creates json files based on the whl_mods attribute.
+""",
+ implementation = _whl_mods_repo_impl,
+ attrs = {
+ "whl_mods": attr.string_dict(
+ mandatory = True,
+ doc = "JSON endcoded string that is provided to wheel_builder.py",
+ ),
+ },
+)
diff --git a/python/private/pip_config_settings.bzl b/python/private/pypi/config_settings.bzl
similarity index 98%
rename from python/private/pip_config_settings.bzl
rename to python/private/pypi/config_settings.bzl
index b13b39b..9741217 100644
--- a/python/private/pip_config_settings.bzl
+++ b/python/private/pypi/config_settings.bzl
@@ -39,12 +39,7 @@
order to ensure that the matching fails if the user requests for `musl` and we don't have it or vice versa.
"""
-load(
- ":pip_flags.bzl",
- "INTERNAL_FLAGS",
- "UniversalWhlFlag",
- "WhlLibcFlag",
-)
+load(":flags.bzl", "INTERNAL_FLAGS", "UniversalWhlFlag", "WhlLibcFlag")
FLAGS = struct(
**{
@@ -73,7 +68,7 @@
}
)
-def pip_config_settings(
+def config_settings(
*,
python_versions = [],
glibc_versions = [],
diff --git a/python/private/pip_flags.bzl b/python/private/pypi/flags.bzl
similarity index 97%
rename from python/private/pip_flags.bzl
rename to python/private/pypi/flags.bzl
index 1d271c7..d834be8 100644
--- a/python/private/pip_flags.bzl
+++ b/python/private/pypi/flags.bzl
@@ -18,7 +18,7 @@
unnecessary files when all that are needed are flag definitions.
"""
-load(":enum.bzl", "enum")
+load("//python/private:enum.bzl", "enum")
# Determines if we should use whls for third party
#
diff --git a/python/pip_install/private/generate_group_library_build_bazel.bzl b/python/private/pypi/generate_group_library_build_bazel.bzl
similarity index 98%
rename from python/pip_install/private/generate_group_library_build_bazel.bzl
rename to python/private/pypi/generate_group_library_build_bazel.bzl
index 5fa93e2..54da066 100644
--- a/python/pip_install/private/generate_group_library_build_bazel.bzl
+++ b/python/private/pypi/generate_group_library_build_bazel.bzl
@@ -14,15 +14,15 @@
"""Generate the BUILD.bazel contents for a repo defined by a group_library."""
+load("//python/private:normalize_name.bzl", "normalize_name")
+load("//python/private:text_util.bzl", "render")
load(
- "//python/private:labels.bzl",
+ ":labels.bzl",
"PY_LIBRARY_IMPL_LABEL",
"PY_LIBRARY_PUBLIC_LABEL",
"WHEEL_FILE_IMPL_LABEL",
"WHEEL_FILE_PUBLIC_LABEL",
)
-load("//python/private:normalize_name.bzl", "normalize_name")
-load("//python/private:text_util.bzl", "render")
_PRELUDE = """\
load("@rules_python//python:defs.bzl", "py_library")
diff --git a/python/pip_install/private/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl
similarity index 99%
rename from python/pip_install/private/generate_whl_library_build_bazel.bzl
rename to python/private/pypi/generate_whl_library_build_bazel.bzl
index f3ddd3b..d25f73a 100644
--- a/python/pip_install/private/generate_whl_library_build_bazel.bzl
+++ b/python/private/pypi/generate_whl_library_build_bazel.bzl
@@ -14,8 +14,10 @@
"""Generate the BUILD.bazel contents for a repo defined by a whl_library."""
+load("//python/private:normalize_name.bzl", "normalize_name")
+load("//python/private:text_util.bzl", "render")
load(
- "//python/private:labels.bzl",
+ ":labels.bzl",
"DATA_LABEL",
"DIST_INFO_LABEL",
"PY_LIBRARY_IMPL_LABEL",
@@ -24,8 +26,6 @@
"WHEEL_FILE_IMPL_LABEL",
"WHEEL_FILE_PUBLIC_LABEL",
)
-load("//python/private:normalize_name.bzl", "normalize_name")
-load("//python/private:text_util.bzl", "render")
_COPY_FILE_TEMPLATE = """\
copy_file(
diff --git a/python/private/pypi/group_library.bzl b/python/private/pypi/group_library.bzl
new file mode 100644
index 0000000..ff800e2
--- /dev/null
+++ b/python/private/pypi/group_library.bzl
@@ -0,0 +1,40 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""group_library implementation for WORKSPACE setups."""
+
+load(":generate_group_library_build_bazel.bzl", "generate_group_library_build_bazel")
+
+def _group_library_impl(rctx):
+ build_file_contents = generate_group_library_build_bazel(
+ repo_prefix = rctx.attr.repo_prefix,
+ groups = rctx.attr.groups,
+ )
+ rctx.file("BUILD.bazel", build_file_contents)
+
+group_library = repository_rule(
+ attrs = {
+ "groups": attr.string_list_dict(
+ doc = "A mapping of group names to requirements within that group.",
+ ),
+ "repo_prefix": attr.string(
+ doc = "Prefix used for the whl_library created components of each group",
+ ),
+ },
+ implementation = _group_library_impl,
+ doc = """
+Create a package containing only wrapper py_library and whl_library rules for implementing dependency groups.
+This is an implementation detail of dependency groups and should not be used alone.
+ """,
+)
diff --git a/python/private/bzlmod/pip_repository.bzl b/python/private/pypi/hub_repository.bzl
similarity index 78%
rename from python/private/bzlmod/pip_repository.bzl
rename to python/private/pypi/hub_repository.bzl
index d42eb8b..5e209d6 100644
--- a/python/private/bzlmod/pip_repository.bzl
+++ b/python/private/pypi/hub_repository.bzl
@@ -14,12 +14,12 @@
""
+load("//python/private:text_util.bzl", "render")
load(
- "//python/private:render_pkg_aliases.bzl",
+ ":render_pkg_aliases.bzl",
"render_multiplatform_pkg_aliases",
"whl_alias",
)
-load("//python/private:text_util.bzl", "render")
_BUILD_FILE_CONTENTS = """\
package(default_visibility = ["//visibility:public"])
@@ -28,7 +28,7 @@
exports_files(["requirements.bzl"])
"""
-def _pip_repository_impl(rctx):
+def _impl(rctx):
bzl_packages = rctx.attr.whl_map.keys()
aliases = render_multiplatform_pkg_aliases(
aliases = {
@@ -66,35 +66,33 @@
"%%NAME%%": rctx.attr.repo_name,
})
-pip_repository_attrs = {
- "default_version": attr.string(
- mandatory = True,
- doc = """\
+hub_repository = repository_rule(
+ attrs = {
+ "default_version": attr.string(
+ mandatory = True,
+ doc = """\
This is the default python version in the format of X.Y. This should match
what is setup by the 'python' extension using the 'is_default = True'
setting.""",
- ),
- "groups": attr.string_list_dict(
- mandatory = False,
- ),
- "repo_name": attr.string(
- mandatory = True,
- doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name.",
- ),
- "whl_map": attr.string_dict(
- mandatory = True,
- doc = """\
+ ),
+ "groups": attr.string_list_dict(
+ mandatory = False,
+ ),
+ "repo_name": attr.string(
+ mandatory = True,
+ doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name.",
+ ),
+ "whl_map": attr.string_dict(
+ mandatory = True,
+ doc = """\
The wheel map where values are json.encoded strings of the whl_map constructed
in the pip.parse tag class.
""",
- ),
- "_template": attr.label(
- default = ":requirements.bzl.tmpl",
- ),
-}
-
-pip_repository = repository_rule(
- attrs = pip_repository_attrs,
+ ),
+ "_template": attr.label(
+ default = ":requirements.bzl.tmpl.bzlmod",
+ ),
+ },
doc = """A rule for bzlmod mulitple pip repository creation. PRIVATE USE ONLY.""",
- implementation = _pip_repository_impl,
+ implementation = _impl,
)
diff --git a/python/private/pypi_index_sources.bzl b/python/private/pypi/index_sources.bzl
similarity index 97%
rename from python/private/pypi_index_sources.bzl
rename to python/private/pypi/index_sources.bzl
index 470a8c9..2166014 100644
--- a/python/private/pypi_index_sources.bzl
+++ b/python/private/pypi/index_sources.bzl
@@ -16,7 +16,7 @@
A file that houses private functions used in the `bzlmod` extension with the same name.
"""
-def get_simpleapi_sources(line):
+def index_sources(line):
"""Get PyPI sources from a requirements.txt line.
We interpret the spec described in
diff --git a/python/private/labels.bzl b/python/private/pypi/labels.bzl
similarity index 100%
rename from python/private/labels.bzl
rename to python/private/pypi/labels.bzl
diff --git a/python/private/pypi/package_annotation.bzl b/python/private/pypi/package_annotation.bzl
new file mode 100644
index 0000000..4a54703
--- /dev/null
+++ b/python/private/pypi/package_annotation.bzl
@@ -0,0 +1,49 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Package annotation API for WORKSPACE setups."""
+
+def package_annotation(
+ additive_build_content = None,
+ copy_files = {},
+ copy_executables = {},
+ data = [],
+ data_exclude_glob = [],
+ srcs_exclude_glob = []):
+ """Annotations to apply to the BUILD file content from package generated from a `pip_repository` rule.
+
+ [cf]: https://github.com/bazelbuild/bazel-skylib/blob/main/docs/copy_file_doc.md
+
+ Args:
+ additive_build_content (str, optional): Raw text to add to the generated `BUILD` file of a package.
+ copy_files (dict, optional): A mapping of `src` and `out` files for [@bazel_skylib//rules:copy_file.bzl][cf]
+ copy_executables (dict, optional): A mapping of `src` and `out` files for
+ [@bazel_skylib//rules:copy_file.bzl][cf]. Targets generated here will also be flagged as
+ executable.
+ data (list, optional): A list of labels to add as `data` dependencies to the generated `py_library` target.
+ data_exclude_glob (list, optional): A list of exclude glob patterns to add as `data` to the generated
+ `py_library` target.
+ srcs_exclude_glob (list, optional): A list of labels to add as `srcs` to the generated `py_library` target.
+
+ Returns:
+ str: A json encoded string of the provided content.
+ """
+ return json.encode(struct(
+ additive_build_content = additive_build_content,
+ copy_files = copy_files,
+ copy_executables = copy_executables,
+ data = data,
+ data_exclude_glob = data_exclude_glob,
+ srcs_exclude_glob = srcs_exclude_glob,
+ ))
diff --git a/python/private/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl
similarity index 98%
rename from python/private/parse_requirements.bzl
rename to python/private/pypi/parse_requirements.bzl
index 21e132b..22a6f0a 100644
--- a/python/private/parse_requirements.bzl
+++ b/python/private/pypi/parse_requirements.bzl
@@ -26,9 +26,9 @@
behavior.
"""
-load("//python/pip_install:requirements_parser.bzl", "parse")
-load(":normalize_name.bzl", "normalize_name")
-load(":pypi_index_sources.bzl", "get_simpleapi_sources")
+load("//python/private:normalize_name.bzl", "normalize_name")
+load(":index_sources.bzl", "index_sources")
+load(":parse_requirements_txt.bzl", "parse_requirements_txt")
load(":whl_target_platforms.bzl", "select_whls", "whl_target_platforms")
# This includes the vendored _translate_cpu and _translate_os from
@@ -271,7 +271,7 @@
# Parse the requirements file directly in starlark to get the information
# needed for the whl_library declarations later.
- parse_result = parse(contents)
+ parse_result = parse_requirements_txt(contents)
# Replicate a surprising behavior that WORKSPACE builds allowed:
# Defining a repo with the same name multiple times, but only the last
@@ -317,7 +317,7 @@
(requirement_line, ",".join(extra_pip_args)),
struct(
distribution = distribution,
- srcs = get_simpleapi_sources(requirement_line),
+ srcs = index_sources(requirement_line),
requirement_line = requirement_line,
target_platforms = [],
extra_pip_args = extra_pip_args,
diff --git a/python/private/pypi/parse_requirements_txt.bzl b/python/private/pypi/parse_requirements_txt.bzl
new file mode 100644
index 0000000..6f51d03
--- /dev/null
+++ b/python/private/pypi/parse_requirements_txt.bzl
@@ -0,0 +1,133 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Pip requirements parser for Starlark."""
+
+_STATE = struct(
+ # Consume extraneous whitespace
+ ConsumeSpace = 0,
+ # Consume a comment
+ ConsumeComment = 1,
+ # Parse the name of a pip package
+ ParseDependency = 2,
+ # Parse a full requirement line
+ ParseRequirement = 3,
+ # Parse a pip option
+ ParseOption = 4,
+)
+
+EOF = {}
+
+def parse_requirements_txt(content):
+ """A simplistic (and incomplete) pip requirements lockfile parser.
+
+ Parses package names and their full requirement lines, as well pip
+ options.
+
+ Args:
+ content: lockfile content as a string
+
+ Returns:
+ Struct with fields `requirements` and `options`.
+
+ requirements: List of requirements, where each requirement is a 2-element
+ tuple containing the package name and the requirement line.
+ E.g., [(certifi', 'certifi==2021.10.8 --hash=sha256:7888...'), ...]
+
+ options: List of pip option lines
+ """
+ content = content.replace("\r", "")
+
+ result = struct(
+ requirements = [],
+ options = [],
+ )
+ state = _STATE.ConsumeSpace
+ buffer = ""
+
+ inputs = content.elems()[:]
+ inputs.append(EOF)
+
+ for input in inputs:
+ if state == _STATE.ConsumeSpace:
+ (state, buffer) = _handleConsumeSpace(input)
+ elif state == _STATE.ConsumeComment:
+ (state, buffer) = _handleConsumeComment(input, buffer, result)
+ elif state == _STATE.ParseDependency:
+ (state, buffer) = _handleParseDependency(input, buffer, result)
+ elif state == _STATE.ParseOption:
+ (state, buffer) = _handleParseOption(input, buffer, result)
+ elif state == _STATE.ParseRequirement:
+ (state, buffer) = _handleParseRequirement(input, buffer, result)
+ else:
+ fail("Unknown state %d" % state)
+
+ return result
+
+def _handleConsumeSpace(input):
+ if input == EOF:
+ return (_STATE.ConsumeSpace, "")
+ if input.isspace():
+ return (_STATE.ConsumeSpace, "")
+ elif input == "#":
+ return (_STATE.ConsumeComment, "")
+ elif input == "-":
+ return (_STATE.ParseOption, input)
+
+ return (_STATE.ParseDependency, input)
+
+def _handleConsumeComment(input, buffer, result):
+ if input == "\n":
+ if len(result.requirements) > 0 and len(result.requirements[-1]) == 1:
+ result.requirements[-1] = (result.requirements[-1][0], buffer.rstrip(" \n"))
+ return (_STATE.ConsumeSpace, "")
+ elif len(buffer) > 0:
+ result.options.append(buffer.rstrip(" \n"))
+ return (_STATE.ConsumeSpace, "")
+ return (_STATE.ConsumeSpace, "")
+ return (_STATE.ConsumeComment, buffer)
+
+def _handleParseDependency(input, buffer, result):
+ if input == EOF:
+ fail("Enountered unexpected end of file while parsing requirement")
+ elif input.isspace() or input in [">", "<", "~", "=", ";", "["]:
+ result.requirements.append((buffer,))
+ return (_STATE.ParseRequirement, buffer + input)
+
+ return (_STATE.ParseDependency, buffer + input)
+
+def _handleParseOption(input, buffer, result):
+ if input == "\n" and buffer.endswith("\\"):
+ return (_STATE.ParseOption, buffer[0:-1])
+ elif input == " ":
+ result.options.append(buffer.rstrip("\n"))
+ return (_STATE.ParseOption, "")
+ elif input == "\n" or input == EOF:
+ result.options.append(buffer.rstrip("\n"))
+ return (_STATE.ConsumeSpace, "")
+ elif input == "#" and (len(buffer) == 0 or buffer[-1].isspace()):
+ return (_STATE.ConsumeComment, buffer)
+
+ return (_STATE.ParseOption, buffer + input)
+
+def _handleParseRequirement(input, buffer, result):
+ if input == "\n" and buffer.endswith("\\"):
+ return (_STATE.ParseRequirement, buffer[0:-1])
+ elif input == "\n" or input == EOF:
+ result.requirements[-1] = (result.requirements[-1][0], buffer.rstrip(" \n"))
+ return (_STATE.ConsumeSpace, "")
+ elif input == "#" and (len(buffer) == 0 or buffer[-1].isspace()):
+ return (_STATE.ConsumeComment, buffer)
+
+ return (_STATE.ParseRequirement, buffer + input)
diff --git a/python/private/pypi/parse_simpleapi_html.bzl b/python/private/pypi/parse_simpleapi_html.bzl
new file mode 100644
index 0000000..f7cd032
--- /dev/null
+++ b/python/private/pypi/parse_simpleapi_html.bzl
@@ -0,0 +1,106 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Parse SimpleAPI HTML in Starlark.
+"""
+
+def parse_simpleapi_html(*, url, content):
+ """Get the package URLs for given shas by parsing the Simple API HTML.
+
+ Args:
+ url(str): The URL that the HTML content can be downloaded from.
+ content(str): The Simple API HTML content.
+
+ Returns:
+ A list of structs with:
+ * filename: The filename of the artifact.
+ * url: The URL to download the artifact.
+ * sha256: The sha256 of the artifact.
+ * metadata_sha256: The whl METADATA sha256 if we can download it. If this is
+ present, then the 'metadata_url' is also present. Defaults to "".
+ * metadata_url: The URL for the METADATA if we can download it. Defaults to "".
+ """
+ sdists = {}
+ whls = {}
+ lines = content.split("<a href=\"")
+
+ _, _, api_version = lines[0].partition("name=\"pypi:repository-version\" content=\"")
+ api_version, _, _ = api_version.partition("\"")
+
+ # We must assume the 1.0 if it is not present
+ # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#clients
+ api_version = api_version or "1.0"
+ api_version = tuple([int(i) for i in api_version.split(".")])
+
+ if api_version >= (2, 0):
+ # We don't expect to have version 2.0 here, but have this check in place just in case.
+ # https://packaging.python.org/en/latest/specifications/simple-repository-api/#versioning-pypi-s-simple-api
+ fail("Unsupported API version: {}".format(api_version))
+
+ for line in lines[1:]:
+ dist_url, _, tail = line.partition("#sha256=")
+ sha256, _, tail = tail.partition("\"")
+
+ # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api
+ yanked = "data-yanked" in line
+
+ maybe_metadata, _, tail = tail.partition(">")
+ filename, _, tail = tail.partition("<")
+
+ metadata_sha256 = ""
+ metadata_url = ""
+ for metadata_marker in ["data-core-metadata", "data-dist-info-metadata"]:
+ metadata_marker = metadata_marker + "=\"sha256="
+ if metadata_marker in maybe_metadata:
+ # Implement https://peps.python.org/pep-0714/
+ _, _, tail = maybe_metadata.partition(metadata_marker)
+ metadata_sha256, _, _ = tail.partition("\"")
+ metadata_url = dist_url + ".metadata"
+ break
+
+ if filename.endswith(".whl"):
+ whls[sha256] = struct(
+ filename = filename,
+ url = _absolute_url(url, dist_url),
+ sha256 = sha256,
+ metadata_sha256 = metadata_sha256,
+ metadata_url = _absolute_url(url, metadata_url),
+ yanked = yanked,
+ )
+ else:
+ sdists[sha256] = struct(
+ filename = filename,
+ url = _absolute_url(url, dist_url),
+ sha256 = sha256,
+ metadata_sha256 = "",
+ metadata_url = "",
+ yanked = yanked,
+ )
+
+ return struct(
+ sdists = sdists,
+ whls = whls,
+ )
+
+def _absolute_url(index_url, candidate):
+ if not candidate.startswith(".."):
+ return candidate
+
+ candidate_parts = candidate.split("..")
+ last = candidate_parts[-1]
+ for _ in range(len(candidate_parts) - 1):
+ index_url, _, _ = index_url.rstrip("/").rpartition("/")
+
+ return "{}/{}".format(index_url, last.strip("/"))
diff --git a/python/private/parse_whl_name.bzl b/python/private/pypi/parse_whl_name.bzl
similarity index 100%
rename from python/private/parse_whl_name.bzl
rename to python/private/pypi/parse_whl_name.bzl
diff --git a/python/private/patch_whl.bzl b/python/private/pypi/patch_whl.bzl
similarity index 87%
rename from python/private/patch_whl.bzl
rename to python/private/pypi/patch_whl.bzl
index 9e3119f..c2c633d 100644
--- a/python/private/patch_whl.bzl
+++ b/python/private/pypi/patch_whl.bzl
@@ -27,7 +27,8 @@
within the wheel.
"""
-load("//python/private:parse_whl_name.bzl", "parse_whl_name")
+load("//python/private:repo_utils.bzl", "repo_utils")
+load(":parse_whl_name.bzl", "parse_whl_name")
_rules_python_root = Label("//:BUILD.bazel")
@@ -40,7 +41,7 @@
whl_path: The whl file name to be patched.
patches: a label-keyed-int dict that has the patch files as keys and
the patch_strip as the value.
- **kwargs: extras passed to rctx.execute.
+ **kwargs: extras passed to repo_utils.execute_checked.
Returns:
value of the repackaging action.
@@ -75,11 +76,12 @@
record_patch = rctx.path("RECORD.patch")
- result = rctx.execute(
- [
+ repo_utils.execute_checked(
+ rctx,
+ arguments = [
python_interpreter,
"-m",
- "python.private.repack_whl",
+ "python.private.pypi.repack_whl",
"--record-patch",
record_patch,
whl_input,
@@ -91,16 +93,6 @@
**kwargs
)
- if result.return_code:
- fail(
- "repackaging .whl {whl} failed: with exit code '{return_code}':\n{stdout}\n\nstderr:\n{stderr}".format(
- whl = whl_input.basename,
- stdout = result.stdout,
- stderr = result.stderr,
- return_code = result.return_code,
- ),
- )
-
if record_patch.exists:
record_patch_contents = rctx.read(record_patch)
warning_msg = """WARNING: the resultant RECORD file of the patch wheel is different
diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl
new file mode 100644
index 0000000..a22f4d9
--- /dev/null
+++ b/python/private/pypi/pip_repository.bzl
@@ -0,0 +1,327 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""
+
+load("@bazel_skylib//lib:sets.bzl", "sets")
+load("//python/private:normalize_name.bzl", "normalize_name")
+load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR")
+load("//python/private:text_util.bzl", "render")
+load(":parse_requirements.bzl", "host_platform", "parse_requirements", "select_requirement")
+load(":pip_repository_attrs.bzl", "ATTRS")
+load(":render_pkg_aliases.bzl", "render_pkg_aliases", "whl_alias")
+
+def _get_python_interpreter_attr(rctx):
+ """A helper function for getting the `python_interpreter` attribute or it's default
+
+ Args:
+ rctx (repository_ctx): Handle to the rule repository context.
+
+ Returns:
+ str: The attribute value or it's default
+ """
+ if rctx.attr.python_interpreter:
+ return rctx.attr.python_interpreter
+
+ if "win" in rctx.os.name:
+ return "python.exe"
+ else:
+ return "python3"
+
+def use_isolated(ctx, attr):
+ """Determine whether or not to pass the pip `--isolated` flag to the pip invocation.
+
+ Args:
+ ctx: repository or module context
+ attr: attributes for the repo rule or tag extension
+
+ Returns:
+ True if --isolated should be passed
+ """
+ use_isolated = attr.isolated
+
+ # The environment variable will take precedence over the attribute
+ isolated_env = ctx.os.environ.get("RULES_PYTHON_PIP_ISOLATED", None)
+ if isolated_env != None:
+ if isolated_env.lower() in ("0", "false"):
+ use_isolated = False
+ else:
+ use_isolated = True
+
+ return use_isolated
+
+_BUILD_FILE_CONTENTS = """\
+package(default_visibility = ["//visibility:public"])
+
+# Ensure the `requirements.bzl` source can be accessed by stardoc, since users load() from it
+exports_files(["requirements.bzl"])
+"""
+
+def _pip_repository_impl(rctx):
+ requirements_by_platform = parse_requirements(
+ rctx,
+ requirements_by_platform = rctx.attr.requirements_by_platform,
+ requirements_linux = rctx.attr.requirements_linux,
+ requirements_lock = rctx.attr.requirements_lock,
+ requirements_osx = rctx.attr.requirements_darwin,
+ requirements_windows = rctx.attr.requirements_windows,
+ extra_pip_args = rctx.attr.extra_pip_args,
+ )
+ selected_requirements = {}
+ options = None
+ repository_platform = host_platform(rctx.os)
+ for name, requirements in requirements_by_platform.items():
+ r = select_requirement(
+ requirements,
+ platform = repository_platform,
+ )
+ if not r:
+ continue
+ options = options or r.extra_pip_args
+ selected_requirements[name] = r.requirement_line
+
+ bzl_packages = sorted(selected_requirements.keys())
+
+ # Normalize cycles first
+ requirement_cycles = {
+ name: sorted(sets.to_list(sets.make(deps)))
+ for name, deps in rctx.attr.experimental_requirement_cycles.items()
+ }
+
+ # Check for conflicts between cycles _before_ we normalize package names so
+ # that reported errors use the names the user specified
+ for i in range(len(requirement_cycles)):
+ left_group = requirement_cycles.keys()[i]
+ left_deps = requirement_cycles.values()[i]
+ for j in range(len(requirement_cycles) - (i + 1)):
+ right_deps = requirement_cycles.values()[1 + i + j]
+ right_group = requirement_cycles.keys()[1 + i + j]
+ for d in left_deps:
+ if d in right_deps:
+ fail("Error: Requirement %s cannot be repeated between cycles %s and %s; please merge the cycles." % (d, left_group, right_group))
+
+ # And normalize the names as used in the cycle specs
+ #
+ # NOTE: We must check that a listed dependency is actually in the actual
+ # requirements set for the current platform so that we can support cycles in
+ # platform-conditional requirements. Otherwise we'll blindly generate a
+ # label referencing a package which may not be installed on the current
+ # platform.
+ requirement_cycles = {
+ normalize_name(name): sorted([normalize_name(d) for d in group if normalize_name(d) in bzl_packages])
+ for name, group in requirement_cycles.items()
+ }
+
+ imports = [
+ # NOTE: Maintain the order consistent with `buildifier`
+ 'load("@rules_python//python:pip.bzl", "pip_utils")',
+ 'load("@rules_python//python/pip_install:pip_repository.bzl", "group_library", "whl_library")',
+ ]
+
+ annotations = {}
+ for pkg, annotation in rctx.attr.annotations.items():
+ filename = "{}.annotation.json".format(normalize_name(pkg))
+ rctx.file(filename, json.encode_indent(json.decode(annotation)))
+ annotations[pkg] = "@{name}//:{filename}".format(name = rctx.attr.name, filename = filename)
+
+ config = {
+ "download_only": rctx.attr.download_only,
+ "enable_implicit_namespace_pkgs": rctx.attr.enable_implicit_namespace_pkgs,
+ "environment": rctx.attr.environment,
+ "envsubst": rctx.attr.envsubst,
+ "extra_pip_args": options,
+ "isolated": use_isolated(rctx, rctx.attr),
+ "pip_data_exclude": rctx.attr.pip_data_exclude,
+ "python_interpreter": _get_python_interpreter_attr(rctx),
+ "quiet": rctx.attr.quiet,
+ "repo": rctx.attr.name,
+ "timeout": rctx.attr.timeout,
+ }
+ if rctx.attr.use_hub_alias_dependencies:
+ config["dep_template"] = "@{}//{{name}}:{{target}}".format(rctx.attr.name)
+ else:
+ config["repo_prefix"] = "{}_".format(rctx.attr.name)
+
+ if rctx.attr.python_interpreter_target:
+ config["python_interpreter_target"] = str(rctx.attr.python_interpreter_target)
+ if rctx.attr.experimental_target_platforms:
+ config["experimental_target_platforms"] = rctx.attr.experimental_target_platforms
+
+ macro_tmpl = "@%s//{}:{}" % rctx.attr.name
+
+ aliases = render_pkg_aliases(
+ aliases = {
+ pkg: [whl_alias(repo = rctx.attr.name + "_" + pkg)]
+ for pkg in bzl_packages or []
+ },
+ )
+ for path, contents in aliases.items():
+ rctx.file(path, contents)
+
+ rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
+ rctx.template("requirements.bzl", rctx.attr._template, substitutions = {
+ " # %%GROUP_LIBRARY%%": """\
+ group_repo = "{name}__groups"
+ group_library(
+ name = group_repo,
+ repo_prefix = "{name}_",
+ groups = all_requirement_groups,
+ )""".format(name = rctx.attr.name) if not rctx.attr.use_hub_alias_dependencies else "",
+ "%%ALL_DATA_REQUIREMENTS%%": render.list([
+ macro_tmpl.format(p, "data")
+ for p in bzl_packages
+ ]),
+ "%%ALL_REQUIREMENTS%%": render.list([
+ macro_tmpl.format(p, "pkg")
+ for p in bzl_packages
+ ]),
+ "%%ALL_REQUIREMENT_GROUPS%%": render.dict(requirement_cycles),
+ "%%ALL_WHL_REQUIREMENTS_BY_PACKAGE%%": render.dict({
+ p: macro_tmpl.format(p, "whl")
+ for p in bzl_packages
+ }),
+ "%%ANNOTATIONS%%": render.dict(dict(sorted(annotations.items()))),
+ "%%CONFIG%%": render.dict(dict(sorted(config.items()))),
+ "%%EXTRA_PIP_ARGS%%": json.encode(options),
+ "%%IMPORTS%%": "\n".join(imports),
+ "%%MACRO_TMPL%%": macro_tmpl,
+ "%%NAME%%": rctx.attr.name,
+ "%%PACKAGES%%": render.list(
+ [
+ ("{}_{}".format(rctx.attr.name, p), r)
+ for p, r in sorted(selected_requirements.items())
+ ],
+ ),
+ })
+
+ return
+
+pip_repository = repository_rule(
+ attrs = dict(
+ annotations = attr.string_dict(
+ doc = "Optional annotations to apply to packages",
+ ),
+ _template = attr.label(
+ default = ":requirements.bzl.tmpl.workspace",
+ ),
+ **ATTRS
+ ),
+ doc = """Accepts a locked/compiled requirements file and installs the dependencies listed within.
+
+Those dependencies become available in a generated `requirements.bzl` file.
+You can instead check this `requirements.bzl` file into your repo, see the "vendoring" section below.
+
+In your WORKSPACE file:
+
+```starlark
+load("@rules_python//python:pip.bzl", "pip_parse")
+
+pip_parse(
+ name = "pypi",
+ requirements_lock = ":requirements.txt",
+)
+
+load("@pypi//:requirements.bzl", "install_deps")
+
+install_deps()
+```
+
+You can then reference installed dependencies from a `BUILD` file with the alias targets generated in the same repo, for example, for `PyYAML` we would have the following:
+- `@pypi//pyyaml` and `@pypi//pyyaml:pkg` both point to the `py_library`
+ created after extracting the `PyYAML` package.
+- `@pypi//pyyaml:data` points to the extra data included in the package.
+- `@pypi//pyyaml:dist_info` points to the `dist-info` files in the package.
+- `@pypi//pyyaml:whl` points to the wheel file that was extracted.
+
+```starlark
+py_library(
+ name = "bar",
+ ...
+ deps = [
+ "//my/other:dep",
+ "@pypi//numpy",
+ "@pypi//requests",
+ ],
+)
+```
+
+or
+
+```starlark
+load("@pypi//:requirements.bzl", "requirement")
+
+py_library(
+ name = "bar",
+ ...
+ deps = [
+ "//my/other:dep",
+ requirement("numpy"),
+ requirement("requests"),
+ ],
+)
+```
+
+In addition to the `requirement` macro, which is used to access the generated `py_library`
+target generated from a package's wheel, The generated `requirements.bzl` file contains
+functionality for exposing [entry points][whl_ep] as `py_binary` targets as well.
+
+[whl_ep]: https://packaging.python.org/specifications/entry-points/
+
+```starlark
+load("@pypi//:requirements.bzl", "entry_point")
+
+alias(
+ name = "pip-compile",
+ actual = entry_point(
+ pkg = "pip-tools",
+ script = "pip-compile",
+ ),
+)
+```
+
+Note that for packages whose name and script are the same, only the name of the package
+is needed when calling the `entry_point` macro.
+
+```starlark
+load("@pip//:requirements.bzl", "entry_point")
+
+alias(
+ name = "flake8",
+ actual = entry_point("flake8"),
+)
+```
+
+### Vendoring the requirements.bzl file
+
+In some cases you may not want to generate the requirements.bzl file as a repository rule
+while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module
+such as a ruleset, you may want to include the requirements.bzl file rather than make your users
+install the WORKSPACE setup to generate it.
+See https://github.com/bazelbuild/rules_python/issues/608
+
+This is the same workflow as Gazelle, which creates `go_repository` rules with
+[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos)
+
+To do this, use the "write to source file" pattern documented in
+https://blog.aspect.dev/bazel-can-write-to-the-source-folder
+to put a copy of the generated requirements.bzl into your project.
+Then load the requirements.bzl file directly rather than from the generated repository.
+See the example in rules_python/examples/pip_parse_vendored.
+""",
+ implementation = _pip_repository_impl,
+ environ = [
+ "RULES_PYTHON_PIP_ISOLATED",
+ REPO_DEBUG_ENV_VAR,
+ ],
+)
diff --git a/python/private/pypi/pip_repository_attrs.bzl b/python/private/pypi/pip_repository_attrs.bzl
new file mode 100644
index 0000000..2300086
--- /dev/null
+++ b/python/private/pypi/pip_repository_attrs.bzl
@@ -0,0 +1,73 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Common attributes between bzlmod pip.parse and workspace pip_parse.
+
+A common attributes shared between bzlmod and workspace implementations
+stored in a separate file to avoid unnecessary refetching of the
+repositories."""
+
+load(":attrs.bzl", COMMON_ATTRS = "ATTRS")
+
+ATTRS = {
+ "requirements_by_platform": attr.label_keyed_string_dict(
+ doc = """\
+The requirements files and the comma delimited list of target platforms as values.
+
+The keys are the requirement files and the values are comma-separated platform
+identifiers. For now we only support `<os>_<cpu>` values that are present in
+`@platforms//os` and `@platforms//cpu` packages respectively.
+""",
+ ),
+ "requirements_darwin": attr.label(
+ allow_single_file = True,
+ doc = "Override the requirements_lock attribute when the host platform is Mac OS",
+ ),
+ "requirements_linux": attr.label(
+ allow_single_file = True,
+ doc = "Override the requirements_lock attribute when the host platform is Linux",
+ ),
+ "requirements_lock": attr.label(
+ allow_single_file = True,
+ doc = """\
+A fully resolved 'requirements.txt' pip requirement file containing the
+transitive set of your dependencies. If this file is passed instead of
+'requirements' no resolve will take place and pip_repository will create
+individual repositories for each of your dependencies so that wheels are
+fetched/built only for the targets specified by 'build/run/test'. Note that if
+your lockfile is platform-dependent, you can use the `requirements_[platform]`
+attributes.
+
+Note, that in general requirements files are compiled for a specific platform,
+but sometimes they can work for multiple platforms. `rules_python` right now
+supports requirements files that are created for a particular platform without
+platform markers.
+""",
+ ),
+ "requirements_windows": attr.label(
+ allow_single_file = True,
+ doc = "Override the requirements_lock attribute when the host platform is Windows",
+ ),
+ "use_hub_alias_dependencies": attr.bool(
+ default = False,
+ doc = """\
+Controls if the hub alias dependencies are used. If set to true, then the
+group_library will be included in the hub repo.
+
+True will become default in a subsequent release.
+""",
+ ),
+}
+
+ATTRS.update(**COMMON_ATTRS)
diff --git a/python/private/render_pkg_aliases.bzl b/python/private/pypi/render_pkg_aliases.bzl
similarity index 97%
rename from python/private/render_pkg_aliases.bzl
rename to python/private/pypi/render_pkg_aliases.bzl
index 82ac764..eb907fe 100644
--- a/python/private/render_pkg_aliases.bzl
+++ b/python/private/pypi/render_pkg_aliases.bzl
@@ -16,8 +16,10 @@
This is used in bzlmod and non-bzlmod setups."""
+load("//python/private:normalize_name.bzl", "normalize_name")
+load("//python/private:text_util.bzl", "render")
load(
- "//python/pip_install/private:generate_group_library_build_bazel.bzl",
+ ":generate_group_library_build_bazel.bzl",
"generate_group_library_build_bazel",
) # buildifier: disable=bzl-visibility
load(
@@ -29,9 +31,7 @@
"WHEEL_FILE_IMPL_LABEL",
"WHEEL_FILE_PUBLIC_LABEL",
)
-load(":normalize_name.bzl", "normalize_name")
load(":parse_whl_name.bzl", "parse_whl_name")
-load(":text_util.bzl", "render")
load(":whl_target_platforms.bzl", "whl_target_platforms")
NO_MATCH_ERROR_MESSAGE_TEMPLATE = """\
@@ -309,7 +309,7 @@
aliases = config_setting_aliases,
**kwargs
)
- contents["_config/BUILD.bazel"] = _render_pip_config_settings(**flag_versions)
+ contents["_config/BUILD.bazel"] = _render_config_settings(**flag_versions)
return contents
def multiplatform_whl_aliases(*, aliases, default_version = None, **kwargs):
@@ -398,12 +398,12 @@
ret.extend(versioned.values())
return ret
-def _render_pip_config_settings(python_versions = [], target_platforms = [], osx_versions = [], glibc_versions = [], muslc_versions = []):
+def _render_config_settings(python_versions = [], target_platforms = [], osx_versions = [], glibc_versions = [], muslc_versions = []):
return """\
-load("@rules_python//python/private:pip_config_settings.bzl", "pip_config_settings")
+load("@rules_python//python/private/pypi:config_settings.bzl", "config_settings")
-pip_config_settings(
- name = "pip_config_settings",
+config_settings(
+ name = "config_settings",
glibc_versions = {glibc_versions},
muslc_versions = {muslc_versions},
osx_versions = {osx_versions},
diff --git a/python/private/repack_whl.py b/python/private/pypi/repack_whl.py
similarity index 100%
rename from python/private/repack_whl.py
rename to python/private/pypi/repack_whl.py
diff --git a/python/private/bzlmod/requirements.bzl.tmpl b/python/private/pypi/requirements.bzl.tmpl.bzlmod
similarity index 100%
rename from python/private/bzlmod/requirements.bzl.tmpl
rename to python/private/pypi/requirements.bzl.tmpl.bzlmod
diff --git a/python/private/pypi/requirements.bzl.tmpl.workspace b/python/private/pypi/requirements.bzl.tmpl.workspace
new file mode 100644
index 0000000..2f4bcd6
--- /dev/null
+++ b/python/private/pypi/requirements.bzl.tmpl.workspace
@@ -0,0 +1,72 @@
+"""Starlark representation of locked requirements.
+
+@generated by rules_python pip_parse repository rule.
+"""
+
+%%IMPORTS%%
+
+all_requirements = %%ALL_REQUIREMENTS%%
+
+all_whl_requirements_by_package = %%ALL_WHL_REQUIREMENTS_BY_PACKAGE%%
+
+all_whl_requirements = all_whl_requirements_by_package.values()
+
+all_data_requirements = %%ALL_DATA_REQUIREMENTS%%
+
+_packages = %%PACKAGES%%
+_config = %%CONFIG%%
+_annotations = %%ANNOTATIONS%%
+
+def requirement(name):
+ return "%%MACRO_TMPL%%".format(pip_utils.normalize_name(name), "pkg")
+
+def whl_requirement(name):
+ return "%%MACRO_TMPL%%".format(pip_utils.normalize_name(name), "whl")
+
+def data_requirement(name):
+ return "%%MACRO_TMPL%%".format(pip_utils.normalize_name(name), "data")
+
+def dist_info_requirement(name):
+ return "%%MACRO_TMPL%%".format(pip_utils.normalize_name(name), "dist_info")
+
+def _get_annotation(requirement):
+ # This expects to parse `setuptools==58.2.0 --hash=sha256:2551203ae6955b9876741a26ab3e767bb3242dafe86a32a749ea0d78b6792f11`
+ # down to `setuptools`.
+ name = requirement.split(" ")[0].split("=")[0].split("[")[0]
+ return _annotations.get(name)
+
+def install_deps(**whl_library_kwargs):
+ """Repository rule macro. Install dependencies from `pip_parse`.
+
+ Args:
+ **whl_library_kwargs: Additional arguments which will flow to underlying
+ `whl_library` calls. See pip_repository.bzl for details.
+ """
+
+ # Set up the requirement groups
+ all_requirement_groups = %%ALL_REQUIREMENT_GROUPS%%
+
+ requirement_group_mapping = {
+ requirement: group_name
+ for group_name, group_requirements in all_requirement_groups.items()
+ for requirement in group_requirements
+ }
+
+ # %%GROUP_LIBRARY%%
+
+ # Install wheels which may be participants in a group
+ whl_config = dict(_config)
+ whl_config.update(whl_library_kwargs)
+
+ for name, requirement in _packages:
+ group_name = requirement_group_mapping.get(name.replace("%%NAME%%_", ""))
+ group_deps = all_requirement_groups.get(group_name, [])
+
+ whl_library(
+ name = name,
+ requirement = requirement,
+ group_name = group_name,
+ group_deps = group_deps,
+ annotation = _get_annotation(requirement),
+ **whl_config
+ )
diff --git a/python/private/pypi_index.bzl b/python/private/pypi/simpleapi_download.bzl
similarity index 67%
rename from python/private/pypi_index.bzl
rename to python/private/pypi/simpleapi_download.bzl
index 64d908e..b258fef 100644
--- a/python/private/pypi_index.bzl
+++ b/python/private/pypi/simpleapi_download.bzl
@@ -17,9 +17,10 @@
"""
load("@bazel_features//:features.bzl", "bazel_features")
-load(":auth.bzl", "get_auth")
-load(":envsubst.bzl", "envsubst")
-load(":normalize_name.bzl", "normalize_name")
+load("//python/private:auth.bzl", "get_auth")
+load("//python/private:envsubst.bzl", "envsubst")
+load("//python/private:normalize_name.bzl", "normalize_name")
+load(":parse_simpleapi_html.bzl", "parse_simpleapi_html")
def simpleapi_download(ctx, *, attr, cache, parallel_download = True):
"""Download Simple API HTML.
@@ -71,7 +72,7 @@
success = False
for index_url in index_urls:
- result = read_simple_api(
+ result = _read_simpleapi(
ctx = ctx,
url = "{}/{}/".format(
index_url_overrides.get(pkg_normalized, index_url).rstrip("/"),
@@ -122,7 +123,7 @@
return contents
-def read_simple_api(ctx, url, attr, cache, **download_kwargs):
+def _read_simpleapi(ctx, url, attr, cache, **download_kwargs):
"""Read SimpleAPI.
Args:
@@ -195,98 +196,9 @@
content = ctx.read(output)
- output = parse_simple_api_html(url = url, content = content)
+ output = parse_simpleapi_html(url = url, content = content)
if output:
cache.setdefault(cache_key, output)
return struct(success = True, output = output, cache_key = cache_key)
else:
return struct(success = False)
-
-def parse_simple_api_html(*, url, content):
- """Get the package URLs for given shas by parsing the Simple API HTML.
-
- Args:
- url(str): The URL that the HTML content can be downloaded from.
- content(str): The Simple API HTML content.
-
- Returns:
- A list of structs with:
- * filename: The filename of the artifact.
- * url: The URL to download the artifact.
- * sha256: The sha256 of the artifact.
- * metadata_sha256: The whl METADATA sha256 if we can download it. If this is
- present, then the 'metadata_url' is also present. Defaults to "".
- * metadata_url: The URL for the METADATA if we can download it. Defaults to "".
- """
- sdists = {}
- whls = {}
- lines = content.split("<a href=\"")
-
- _, _, api_version = lines[0].partition("name=\"pypi:repository-version\" content=\"")
- api_version, _, _ = api_version.partition("\"")
-
- # We must assume the 1.0 if it is not present
- # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#clients
- api_version = api_version or "1.0"
- api_version = tuple([int(i) for i in api_version.split(".")])
-
- if api_version >= (2, 0):
- # We don't expect to have version 2.0 here, but have this check in place just in case.
- # https://packaging.python.org/en/latest/specifications/simple-repository-api/#versioning-pypi-s-simple-api
- fail("Unsupported API version: {}".format(api_version))
-
- for line in lines[1:]:
- dist_url, _, tail = line.partition("#sha256=")
- sha256, _, tail = tail.partition("\"")
-
- # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api
- yanked = "data-yanked" in line
-
- maybe_metadata, _, tail = tail.partition(">")
- filename, _, tail = tail.partition("<")
-
- metadata_sha256 = ""
- metadata_url = ""
- for metadata_marker in ["data-core-metadata", "data-dist-info-metadata"]:
- metadata_marker = metadata_marker + "=\"sha256="
- if metadata_marker in maybe_metadata:
- # Implement https://peps.python.org/pep-0714/
- _, _, tail = maybe_metadata.partition(metadata_marker)
- metadata_sha256, _, _ = tail.partition("\"")
- metadata_url = dist_url + ".metadata"
- break
-
- if filename.endswith(".whl"):
- whls[sha256] = struct(
- filename = filename,
- url = _absolute_url(url, dist_url),
- sha256 = sha256,
- metadata_sha256 = metadata_sha256,
- metadata_url = _absolute_url(url, metadata_url),
- yanked = yanked,
- )
- else:
- sdists[sha256] = struct(
- filename = filename,
- url = _absolute_url(url, dist_url),
- sha256 = sha256,
- metadata_sha256 = "",
- metadata_url = "",
- yanked = yanked,
- )
-
- return struct(
- sdists = sdists,
- whls = whls,
- )
-
-def _absolute_url(index_url, candidate):
- if not candidate.startswith(".."):
- return candidate
-
- candidate_parts = candidate.split("..")
- last = candidate_parts[-1]
- for _ in range(len(candidate_parts) - 1):
- index_url, _, _ = index_url.rstrip("/").rpartition("/")
-
- return "{}/{}".format(index_url, last.strip("/"))
diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl
new file mode 100644
index 0000000..cae0db3
--- /dev/null
+++ b/python/private/pypi/whl_library.bzl
@@ -0,0 +1,509 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""
+
+load("//python:repositories.bzl", "is_standalone_interpreter")
+load("//python:versions.bzl", "WINDOWS_NAME")
+load("//python/pip_install:repositories.bzl", "all_requirements")
+load("//python/private:auth.bzl", "AUTH_ATTRS", "get_auth")
+load("//python/private:envsubst.bzl", "envsubst")
+load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
+load("//python/private:toolchains_repo.bzl", "get_host_os_arch")
+load(":attrs.bzl", "ATTRS", "use_isolated")
+load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel")
+load(":parse_whl_name.bzl", "parse_whl_name")
+load(":patch_whl.bzl", "patch_whl")
+load(":whl_target_platforms.bzl", "whl_target_platforms")
+
+_CPPFLAGS = "CPPFLAGS"
+_COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools"
+_WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point"
+
+def _construct_pypath(rctx):
+ """Helper function to construct a PYTHONPATH.
+
+ Contains entries for code in this repo as well as packages downloaded from //python/pip_install:repositories.bzl.
+ This allows us to run python code inside repository rule implementations.
+
+ Args:
+ rctx: Handle to the repository_context.
+
+ Returns: String of the PYTHONPATH.
+ """
+
+ separator = ":" if not "windows" in rctx.os.name.lower() else ";"
+ pypath = separator.join([
+ str(rctx.path(entry).dirname)
+ for entry in rctx.attr._python_path_entries
+ ])
+ return pypath
+
+def _get_python_interpreter_attr(rctx):
+ """A helper function for getting the `python_interpreter` attribute or it's default
+
+ Args:
+ rctx (repository_ctx): Handle to the rule repository context.
+
+ Returns:
+ str: The attribute value or it's default
+ """
+ if rctx.attr.python_interpreter:
+ return rctx.attr.python_interpreter
+
+ if "win" in rctx.os.name:
+ return "python.exe"
+ else:
+ return "python3"
+
+def _resolve_python_interpreter(rctx):
+ """Helper function to find the python interpreter from the common attributes
+
+ Args:
+ rctx: Handle to the rule repository context.
+
+ Returns:
+ `path` object, for the resolved path to the Python interpreter.
+ """
+ python_interpreter = _get_python_interpreter_attr(rctx)
+
+ if rctx.attr.python_interpreter_target != None:
+ python_interpreter = rctx.path(rctx.attr.python_interpreter_target)
+
+ (os, _) = get_host_os_arch(rctx)
+
+ # On Windows, the symlink doesn't work because Windows attempts to find
+ # Python DLLs where the symlink is, not where the symlink points.
+ if os == WINDOWS_NAME:
+ python_interpreter = python_interpreter.realpath
+ elif "/" not in python_interpreter:
+ # It's a plain command, e.g. "python3", to look up in the environment.
+ found_python_interpreter = rctx.which(python_interpreter)
+ if not found_python_interpreter:
+ fail("python interpreter `{}` not found in PATH".format(python_interpreter))
+ python_interpreter = found_python_interpreter
+ else:
+ python_interpreter = rctx.path(python_interpreter)
+ return python_interpreter
+
+def _get_xcode_location_cflags(rctx):
+ """Query the xcode sdk location to update cflags
+
+ Figure out if this interpreter target comes from rules_python, and patch the xcode sdk location if so.
+ Pip won't be able to compile c extensions from sdists with the pre built python distributions from indygreg
+ otherwise. See https://github.com/indygreg/python-build-standalone/issues/103
+ """
+
+ # Only run on MacOS hosts
+ if not rctx.os.name.lower().startswith("mac os"):
+ return []
+
+ xcode_sdk_location = repo_utils.execute_unchecked(
+ rctx,
+ op = "GetXcodeLocation",
+ arguments = [repo_utils.which_checked(rctx, "xcode-select"), "--print-path"],
+ )
+ if xcode_sdk_location.return_code != 0:
+ return []
+
+ xcode_root = xcode_sdk_location.stdout.strip()
+ if _COMMAND_LINE_TOOLS_PATH_SLUG not in xcode_root.lower():
+ # This is a full xcode installation somewhere like /Applications/Xcode13.0.app/Contents/Developer
+ # so we need to change the path to to the macos specific tools which are in a different relative
+ # path than xcode installed command line tools.
+ xcode_root = "{}/Platforms/MacOSX.platform/Developer".format(xcode_root)
+ return [
+ "-isysroot {}/SDKs/MacOSX.sdk".format(xcode_root),
+ ]
+
+def _get_toolchain_unix_cflags(rctx, python_interpreter):
+ """Gather cflags from a standalone toolchain for unix systems.
+
+ Pip won't be able to compile c extensions from sdists with the pre built python distributions from indygreg
+ otherwise. See https://github.com/indygreg/python-build-standalone/issues/103
+ """
+
+ # Only run on Unix systems
+ if not rctx.os.name.lower().startswith(("mac os", "linux")):
+ return []
+
+ # Only update the location when using a standalone toolchain.
+ if not is_standalone_interpreter(rctx, python_interpreter):
+ return []
+
+ stdout = repo_utils.execute_checked_stdout(
+ rctx,
+ op = "GetPythonVersionForUnixCflags",
+ arguments = [
+ python_interpreter,
+ "-c",
+ "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}', end='')",
+ ],
+ )
+ _python_version = stdout
+ include_path = "{}/include/python{}".format(
+ python_interpreter.dirname,
+ _python_version,
+ )
+
+ return ["-isystem {}".format(include_path)]
+
+def _parse_optional_attrs(rctx, args, extra_pip_args = None):
+ """Helper function to parse common attributes of pip_repository and whl_library repository rules.
+
+ This function also serializes the structured arguments as JSON
+ so they can be passed on the command line to subprocesses.
+
+ Args:
+ rctx: Handle to the rule repository context.
+ args: A list of parsed args for the rule.
+ extra_pip_args: The pip args to pass.
+ Returns: Augmented args list.
+ """
+
+ if use_isolated(rctx, rctx.attr):
+ args.append("--isolated")
+
+ # Bazel version 7.1.0 and later (and rolling releases from version 8.0.0-pre.20240128.3)
+ # support rctx.getenv(name, default): When building incrementally, any change to the value of
+ # the variable named by name will cause this repository to be re-fetched.
+ if "getenv" in dir(rctx):
+ getenv = rctx.getenv
+ else:
+ getenv = rctx.os.environ.get
+
+ # Check for None so we use empty default types from our attrs.
+ # Some args want to be list, and some want to be dict.
+ if extra_pip_args != None:
+ args += [
+ "--extra_pip_args",
+ json.encode(struct(arg = [
+ envsubst(pip_arg, rctx.attr.envsubst, getenv)
+ for pip_arg in rctx.attr.extra_pip_args
+ ])),
+ ]
+
+ if rctx.attr.download_only:
+ args.append("--download_only")
+
+ if rctx.attr.pip_data_exclude != None:
+ args += [
+ "--pip_data_exclude",
+ json.encode(struct(arg = rctx.attr.pip_data_exclude)),
+ ]
+
+ if rctx.attr.enable_implicit_namespace_pkgs:
+ args.append("--enable_implicit_namespace_pkgs")
+
+ if rctx.attr.environment != None:
+ args += [
+ "--environment",
+ json.encode(struct(arg = rctx.attr.environment)),
+ ]
+
+ return args
+
+def _create_repository_execution_environment(rctx, python_interpreter):
+ """Create a environment dictionary for processes we spawn with rctx.execute.
+
+ Args:
+ rctx (repository_ctx): The repository context.
+ python_interpreter (path): The resolved python interpreter.
+ Returns:
+ Dictionary of environment variable suitable to pass to rctx.execute.
+ """
+
+ # Gather any available CPPFLAGS values
+ cppflags = []
+ cppflags.extend(_get_xcode_location_cflags(rctx))
+ cppflags.extend(_get_toolchain_unix_cflags(rctx, python_interpreter))
+
+ env = {
+ "PYTHONPATH": _construct_pypath(rctx),
+ _CPPFLAGS: " ".join(cppflags),
+ }
+
+ return env
+
+def _whl_library_impl(rctx):
+ python_interpreter = _resolve_python_interpreter(rctx)
+ args = [
+ python_interpreter,
+ "-m",
+ "python.pip_install.tools.wheel_installer.wheel_installer",
+ "--requirement",
+ rctx.attr.requirement,
+ ]
+ extra_pip_args = []
+ extra_pip_args.extend(rctx.attr.extra_pip_args)
+
+ # Manually construct the PYTHONPATH since we cannot use the toolchain here
+ environment = _create_repository_execution_environment(rctx, python_interpreter)
+
+ whl_path = None
+ if 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:
+ filename = rctx.attr.filename
+ urls = rctx.attr.urls
+ if not filename:
+ _, _, filename = urls[0].rpartition("/")
+
+ if not (filename.endswith(".whl") or filename.endswith("tar.gz") or filename.endswith(".zip")):
+ if rctx.attr.filename:
+ msg = "got '{}'".format(filename)
+ else:
+ msg = "detected '{}' from url:\n{}".format(filename, urls[0])
+ fail("Only '.whl', '.tar.gz' or '.zip' files are supported, {}".format(msg))
+
+ result = rctx.download(
+ url = urls,
+ output = filename,
+ sha256 = rctx.attr.sha256,
+ auth = get_auth(rctx, urls),
+ )
+
+ if not result.success:
+ fail("could not download the '{}' from {}:\n{}".format(filename, urls, result))
+
+ if filename.endswith(".whl"):
+ whl_path = rctx.path(rctx.attr.filename)
+ else:
+ # It is an sdist and we need to tell PyPI to use a file in this directory
+ # and not use any indexes.
+ extra_pip_args.extend(["--no-index", "--find-links", "."])
+
+ args = _parse_optional_attrs(rctx, args, extra_pip_args)
+
+ if not whl_path:
+ repo_utils.execute_checked(
+ rctx,
+ op = "whl_library.ResolveRequirement({}, {})".format(rctx.attr.name, rctx.attr.requirement),
+ arguments = args,
+ environment = environment,
+ quiet = rctx.attr.quiet,
+ timeout = rctx.attr.timeout,
+ )
+
+ 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 = {}
+ 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
+
+ whl_path = patch_whl(
+ rctx,
+ op = "whl_library.PatchWhl({}, {})".format(rctx.attr.name, rctx.attr.requirement),
+ python_interpreter = python_interpreter,
+ whl_path = whl_path,
+ patches = patches,
+ quiet = rctx.attr.quiet,
+ timeout = rctx.attr.timeout,
+ )
+
+ target_platforms = rctx.attr.experimental_target_platforms
+ if target_platforms:
+ parsed_whl = parse_whl_name(whl_path.basename)
+ if parsed_whl.platform_tag != "any":
+ # NOTE @aignas 2023-12-04: if the wheel is a platform specific
+ # wheel, we only include deps for that target platform
+ target_platforms = [
+ p.target_platform
+ for p in whl_target_platforms(
+ platform_tag = parsed_whl.platform_tag,
+ abi_tag = parsed_whl.abi_tag,
+ )
+ ]
+
+ repo_utils.execute_checked(
+ rctx,
+ op = "whl_library.ExtractWheel({}, {})".format(rctx.attr.name, whl_path),
+ arguments = args + [
+ "--whl-file",
+ whl_path,
+ ] + ["--platform={}".format(p) for p in target_platforms],
+ environment = environment,
+ quiet = rctx.attr.quiet,
+ timeout = rctx.attr.timeout,
+ )
+
+ metadata = json.decode(rctx.read("metadata.json"))
+ rctx.delete("metadata.json")
+
+ # NOTE @aignas 2024-06-22: this has to live on until we stop supporting
+ # passing `twine` as a `:pkg` library via the `WORKSPACE` builds.
+ #
+ # See ../../packaging.bzl line 190
+ entry_points = {}
+ for item in metadata["entry_points"]:
+ name = item["name"]
+ module = item["module"]
+ attribute = item["attribute"]
+
+ # There is an extreme edge-case with entry_points that end with `.py`
+ # See: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/java/com/google/devtools/build/lib/rules/python/PyBinaryConfiguredTargetTest.java#L174
+ entry_point_without_py = name[:-3] + "_py" if name.endswith(".py") else name
+ entry_point_target_name = (
+ _WHEEL_ENTRY_POINT_PREFIX + "_" + entry_point_without_py
+ )
+ entry_point_script_name = entry_point_target_name + ".py"
+
+ rctx.file(
+ entry_point_script_name,
+ _generate_entry_point_contents(module, attribute),
+ )
+ entry_points[entry_point_without_py] = entry_point_script_name
+
+ build_file_contents = generate_whl_library_build_bazel(
+ dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format(rctx.attr.repo_prefix),
+ whl_name = whl_path.basename,
+ dependencies = metadata["deps"],
+ dependencies_by_platform = metadata["deps_by_platform"],
+ group_name = rctx.attr.group_name,
+ group_deps = rctx.attr.group_deps,
+ data_exclude = rctx.attr.pip_data_exclude,
+ tags = [
+ "pypi_name=" + metadata["name"],
+ "pypi_version=" + metadata["version"],
+ ],
+ entry_points = entry_points,
+ annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))),
+ )
+ rctx.file("BUILD.bazel", build_file_contents)
+
+ return
+
+def _generate_entry_point_contents(
+ module,
+ attribute,
+ shebang = "#!/usr/bin/env python3"):
+ """Generate the contents of an entry point script.
+
+ Args:
+ module (str): The name of the module to use.
+ attribute (str): The name of the attribute to call.
+ shebang (str, optional): The shebang to use for the entry point python
+ file.
+
+ Returns:
+ str: A string of python code.
+ """
+ contents = """\
+{shebang}
+import sys
+from {module} import {attribute}
+if __name__ == "__main__":
+ sys.exit({attribute}())
+""".format(
+ shebang = shebang,
+ module = module,
+ attribute = attribute,
+ )
+ return contents
+
+# NOTE @aignas 2024-03-21: The usage of dict({}, **common) ensures that all args to `dict` are unique
+whl_library_attrs = dict({
+ "annotation": attr.label(
+ doc = (
+ "Optional json encoded file containing annotation to apply to the extracted wheel. " +
+ "See `package_annotation`"
+ ),
+ allow_files = True,
+ ),
+ "dep_template": attr.string(
+ doc = """
+The dep template to use for referencing the dependencies. It should have `{name}`
+and `{target}` tokens that will be replaced with the normalized distribution name
+and the target that we need respectively.
+""",
+ ),
+ "filename": attr.string(
+ doc = "Download the whl file to this filename. Only used when the `urls` is passed. If not specified, will be auto-detected from the `urls`.",
+ ),
+ "group_deps": attr.string_list(
+ doc = "List of dependencies to skip in order to break the cycles within a dependency group.",
+ default = [],
+ ),
+ "group_name": attr.string(
+ doc = "Name of the group, if any.",
+ ),
+ "repo": attr.string(
+ mandatory = True,
+ doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.",
+ ),
+ "repo_prefix": attr.string(
+ doc = """
+Prefix for the generated packages will be of the form `@<prefix><sanitized-package-name>//...`
+
+DEPRECATED. Only left for people who vendor requirements.bzl.
+""",
+ ),
+ "requirement": attr.string(
+ mandatory = True,
+ doc = "Python requirement string describing the package to make available, if 'urls' or 'whl_file' is given, then this only needs to include foo[any_extras] as a bare minimum.",
+ ),
+ "sha256": attr.string(
+ doc = "The sha256 of the downloaded whl. Only used when the `urls` is passed.",
+ ),
+ "urls": attr.string_list(
+ doc = """\
+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 that has
+ json.encode(struct([whl_file], patch_strip]) as values. This
+ is to maintain flexibility and correct bzlmod extension interface
+ until we have a better way to define whl_library and move whl
+ patching to a separate place. INTERNAL USE ONLY.""",
+ ),
+ "_python_path_entries": attr.label_list(
+ # Get the root directory of these rules and keep them as a default attribute
+ # in order to avoid unnecessary repository fetching restarts.
+ #
+ # This is very similar to what was done in https://github.com/bazelbuild/rules_go/pull/3478
+ default = [
+ Label("//:BUILD.bazel"),
+ ] + [
+ # Includes all the external dependencies from repositories.bzl
+ Label("@" + repo + "//:BUILD.bazel")
+ for repo in all_requirements
+ ],
+ ),
+}, **ATTRS)
+whl_library_attrs.update(AUTH_ATTRS)
+
+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.
+Instantiated from pip_repository and inherits config options from there.""",
+ implementation = _whl_library_impl,
+ environ = [
+ "RULES_PYTHON_PIP_ISOLATED",
+ REPO_DEBUG_ENV_VAR,
+ ],
+)
diff --git a/python/private/pip_repo_name.bzl b/python/private/pypi/whl_repo_name.bzl
similarity index 93%
rename from python/private/pip_repo_name.bzl
rename to python/private/pypi/whl_repo_name.bzl
index bef4304..295f5a4 100644
--- a/python/private/pip_repo_name.bzl
+++ b/python/private/pypi/whl_repo_name.bzl
@@ -15,10 +15,10 @@
"""A function to convert a dist name to a valid bazel repo name.
"""
-load(":normalize_name.bzl", "normalize_name")
+load("//python/private:normalize_name.bzl", "normalize_name")
load(":parse_whl_name.bzl", "parse_whl_name")
-def pip_repo_name(prefix, filename, sha256):
+def whl_repo_name(prefix, filename, sha256):
"""Return a valid whl_library repo name given a distribution filename.
Args:
diff --git a/python/private/whl_target_platforms.bzl b/python/private/pypi/whl_target_platforms.bzl
similarity index 100%
rename from python/private/whl_target_platforms.bzl
rename to python/private/pypi/whl_target_platforms.bzl
diff --git a/python/private/text_util.bzl b/python/private/text_util.bzl
index dade9cb..702a08e 100644
--- a/python/private/text_util.bzl
+++ b/python/private/text_util.bzl
@@ -36,6 +36,9 @@
])
def _render_dict(d, *, key_repr = repr, value_repr = repr):
+ if not d:
+ return "{}"
+
return "\n".join([
"{",
_indent("\n".join([