aboutsummaryrefslogtreecommitdiff
path: root/python/pip_install/pip_repository.bzl
diff options
context:
space:
mode:
Diffstat (limited to 'python/pip_install/pip_repository.bzl')
-rw-r--r--python/pip_install/pip_repository.bzl769
1 files changed, 769 insertions, 0 deletions
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
new file mode 100644
index 0000000..99d1fb0
--- /dev/null
+++ b/python/pip_install/pip_repository.bzl
@@ -0,0 +1,769 @@
+# 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", "get_interpreter_dirname", "is_standalone_interpreter")
+load("//python:versions.bzl", "WINDOWS_NAME")
+load("//python/pip_install:repositories.bzl", "all_requirements")
+load("//python/pip_install:requirements_parser.bzl", parse_requirements = "parse")
+load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
+load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
+load("//python/private:normalize_name.bzl", "normalize_name")
+load("//python/private:toolchains_repo.bzl", "get_host_os_arch")
+
+CPPFLAGS = "CPPFLAGS"
+
+COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools"
+
+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.
+ """
+
+ # Get the root directory of these rules
+ rules_root = rctx.path(Label("//:BUILD.bazel")).dirname
+ thirdparty_roots = [
+ # Includes all the external dependencies from repositories.bzl
+ rctx.path(Label("@" + repo + "//:BUILD.bazel")).dirname
+ for repo in all_requirements
+ ]
+ separator = ":" if not "windows" in rctx.os.name.lower() else ";"
+ pypath = separator.join([str(p) for p in [rules_root] + thirdparty_roots])
+ 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: Python interpreter path.
+ """
+ python_interpreter = _get_python_interpreter_attr(rctx)
+
+ if rctx.attr.python_interpreter_target != None:
+ python_interpreter = rctx.path(rctx.attr.python_interpreter_target)
+
+ if BZLMOD_ENABLED:
+ (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:
+ 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
+ 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 []
+
+ # Locate xcode-select
+ xcode_select = rctx.which("xcode-select")
+
+ xcode_sdk_location = rctx.execute([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):
+ """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, rctx.attr.python_interpreter_target):
+ return []
+
+ er = rctx.execute([
+ rctx.path(rctx.attr.python_interpreter_target).realpath,
+ "-c",
+ "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}', end='')",
+ ])
+ if er.return_code != 0:
+ fail("could not get python version from interpreter (status {}): {}".format(er.return_code, er.stderr))
+ _python_version = er.stdout
+ include_path = "{}/include/python{}".format(
+ get_interpreter_dirname(rctx, rctx.attr.python_interpreter_target),
+ _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):
+ """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.
+ Returns: Augmented args list.
+ """
+
+ if use_isolated(rctx, rctx.attr):
+ args.append("--isolated")
+
+ # 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 rctx.attr.extra_pip_args != None:
+ args += [
+ "--extra_pip_args",
+ json.encode(struct(arg = 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):
+ """Create a environment dictionary for processes we spawn with rctx.execute.
+
+ Args:
+ rctx: The repository context.
+ 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))
+
+ 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 locked_requirements_label(ctx, attr):
+ """Get the preferred label for a locked requirements file based on platform.
+
+ Args:
+ ctx: repository or module context
+ attr: attributes for the repo rule or tag extension
+
+ Returns:
+ Label
+ """
+ os = ctx.os.name.lower()
+ requirements_txt = attr.requirements_lock
+ if os.startswith("mac os") and attr.requirements_darwin != None:
+ requirements_txt = attr.requirements_darwin
+ elif os.startswith("linux") and attr.requirements_linux != None:
+ requirements_txt = attr.requirements_linux
+ elif "win" in os and attr.requirements_windows != None:
+ requirements_txt = attr.requirements_windows
+ if not requirements_txt:
+ fail("""\
+A requirements_lock attribute must be specified, or a platform-specific lockfile using one of the requirements_* attributes.
+""")
+ return requirements_txt
+
+def _pkg_aliases(rctx, repo_name, bzl_packages):
+ """Create alias declarations for each python dependency.
+
+ The aliases should be appended to the pip_repository BUILD.bazel file. These aliases
+ allow users to use requirement() without needed a corresponding `use_repo()` for each dep
+ when using bzlmod.
+
+ Args:
+ rctx: the repository context.
+ repo_name: the repository name of the parent that is visible to the users.
+ bzl_packages: the list of packages to setup.
+ """
+ for name in bzl_packages:
+ build_content = """package(default_visibility = ["//visibility:public"])
+
+alias(
+ name = "{name}",
+ actual = "@{repo_name}_{dep}//:pkg",
+)
+
+alias(
+ name = "pkg",
+ actual = "@{repo_name}_{dep}//:pkg",
+)
+
+alias(
+ name = "whl",
+ actual = "@{repo_name}_{dep}//:whl",
+)
+
+alias(
+ name = "data",
+ actual = "@{repo_name}_{dep}//:data",
+)
+
+alias(
+ name = "dist_info",
+ actual = "@{repo_name}_{dep}//:dist_info",
+)
+""".format(
+ name = name,
+ repo_name = repo_name,
+ dep = name,
+ )
+ rctx.file("{}/BUILD.bazel".format(name), build_content)
+
+def _create_pip_repository_bzlmod(rctx, bzl_packages, requirements):
+ repo_name = rctx.attr.repo_name
+ build_contents = _BUILD_FILE_CONTENTS
+ _pkg_aliases(rctx, repo_name, bzl_packages)
+
+ # NOTE: we are using the canonical name with the double '@' in order to
+ # always uniquely identify a repository, as the labels are being passed as
+ # a string and the resolution of the label happens at the call-site of the
+ # `requirement`, et al. macros.
+ macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name)
+
+ rctx.file("BUILD.bazel", build_contents)
+ rctx.template("requirements.bzl", rctx.attr._template, substitutions = {
+ "%%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, p)
+ for p in bzl_packages
+ ]),
+ "%%ALL_WHL_REQUIREMENTS%%": _format_repr_list([
+ macro_tmpl.format(p, "whl")
+ for p in bzl_packages
+ ]),
+ "%%MACRO_TMPL%%": macro_tmpl,
+ "%%NAME%%": rctx.attr.name,
+ "%%REQUIREMENTS_LOCK%%": requirements,
+ })
+
+def _pip_hub_repository_bzlmod_impl(rctx):
+ bzl_packages = rctx.attr.whl_library_alias_names
+ _create_pip_repository_bzlmod(rctx, bzl_packages, "")
+
+pip_hub_repository_bzlmod_attrs = {
+ "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_library_alias_names": attr.string_list(
+ mandatory = True,
+ doc = "The list of whl alias that we use to build aliases and the whl names",
+ ),
+ "_template": attr.label(
+ default = ":pip_hub_repository_requirements_bzlmod.bzl.tmpl",
+ ),
+}
+
+pip_hub_repository_bzlmod = repository_rule(
+ attrs = pip_hub_repository_bzlmod_attrs,
+ doc = """A rule for bzlmod mulitple pip repository creation. PRIVATE USE ONLY.""",
+ implementation = _pip_hub_repository_bzlmod_impl,
+)
+
+def _pip_repository_bzlmod_impl(rctx):
+ requirements_txt = locked_requirements_label(rctx, rctx.attr)
+ content = rctx.read(requirements_txt)
+ parsed_requirements_txt = parse_requirements(content)
+
+ packages = [(normalize_name(name), requirement) for name, requirement in parsed_requirements_txt.requirements]
+
+ bzl_packages = sorted([name for name, _ in packages])
+ _create_pip_repository_bzlmod(rctx, bzl_packages, str(requirements_txt))
+
+pip_repository_bzlmod_attrs = {
+ "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",
+ ),
+ "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'.
+""",
+ ),
+ "requirements_windows": attr.label(
+ allow_single_file = True,
+ doc = "Override the requirements_lock attribute when the host platform is Windows",
+ ),
+ "_template": attr.label(
+ default = ":pip_repository_requirements_bzlmod.bzl.tmpl",
+ ),
+}
+
+pip_repository_bzlmod = repository_rule(
+ attrs = pip_repository_bzlmod_attrs,
+ doc = """A rule for bzlmod pip_repository creation. Intended for private use only.""",
+ implementation = _pip_repository_bzlmod_impl,
+)
+
+def _pip_repository_impl(rctx):
+ requirements_txt = locked_requirements_label(rctx, rctx.attr)
+ content = rctx.read(requirements_txt)
+ parsed_requirements_txt = parse_requirements(content)
+
+ packages = [(normalize_name(name), requirement) for name, requirement in parsed_requirements_txt.requirements]
+
+ bzl_packages = sorted([name for name, _ in packages])
+
+ imports = [
+ 'load("@rules_python//python/pip_install:pip_repository.bzl", "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)
+
+ tokenized_options = []
+ for opt in parsed_requirements_txt.options:
+ for p in opt.split(" "):
+ tokenized_options.append(p)
+
+ options = tokenized_options + rctx.attr.extra_pip_args
+
+ config = {
+ "download_only": rctx.attr.download_only,
+ "enable_implicit_namespace_pkgs": rctx.attr.enable_implicit_namespace_pkgs,
+ "environment": rctx.attr.environment,
+ "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,
+ "repo_prefix": "{}_".format(rctx.attr.name),
+ "timeout": rctx.attr.timeout,
+ }
+
+ if rctx.attr.python_interpreter_target:
+ config["python_interpreter_target"] = str(rctx.attr.python_interpreter_target)
+
+ if rctx.attr.incompatible_generate_aliases:
+ _pkg_aliases(rctx, rctx.attr.name, bzl_packages)
+
+ rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
+ rctx.template("requirements.bzl", rctx.attr._template, substitutions = {
+ "%%ALL_DATA_REQUIREMENTS%%": _format_repr_list([
+ "@{}//{}:data".format(rctx.attr.name, p) if rctx.attr.incompatible_generate_aliases else "@{}_{}//:data".format(rctx.attr.name, p)
+ for p in bzl_packages
+ ]),
+ "%%ALL_REQUIREMENTS%%": _format_repr_list([
+ "@{}//{}".format(rctx.attr.name, p) if rctx.attr.incompatible_generate_aliases else "@{}_{}//:pkg".format(rctx.attr.name, p)
+ for p in bzl_packages
+ ]),
+ "%%ALL_WHL_REQUIREMENTS%%": _format_repr_list([
+ "@{}//{}:whl".format(rctx.attr.name, p) if rctx.attr.incompatible_generate_aliases else "@{}_{}//:whl".format(rctx.attr.name, p)
+ 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(sorted(imports)),
+ "%%NAME%%": rctx.attr.name,
+ "%%PACKAGES%%": _format_repr_list(
+ [
+ ("{}_{}".format(rctx.attr.name, p), r)
+ for p, r in packages
+ ],
+ ),
+ "%%REQUIREMENTS_LOCK%%": str(requirements_txt),
+ })
+
+ return
+
+common_env = [
+ "RULES_PYTHON_PIP_ISOLATED",
+]
+
+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.
+ """,
+ default = {},
+ ),
+ "extra_pip_args": attr.string_list(
+ doc = "Extra arguments to pass on to pip. Must not contain spaces.",
+ ),
+ "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.",
+ ),
+ "repo_prefix": attr.string(
+ doc = """
+Prefix for the generated packages will be of the form `@<prefix><sanitized-package-name>//...`
+""",
+ ),
+ # 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",
+ ),
+ "incompatible_generate_aliases": attr.bool(
+ default = False,
+ doc = "Allow generating aliases '@pip//<pkg>' -> '@pip_<pkg>//:pkg'.",
+ ),
+ "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'.
+""",
+ ),
+ "requirements_windows": attr.label(
+ allow_single_file = True,
+ doc = "Override the requirements_lock attribute when the host platform is Windows",
+ ),
+ "_template": attr.label(
+ default = ":pip_repository_requirements.bzl.tmpl",
+ ),
+}
+
+pip_repository_attrs.update(**common_attrs)
+
+pip_repository = repository_rule(
+ attrs = pip_repository_attrs,
+ doc = """A rule for importing `requirements.txt` dependencies into Bazel.
+
+This rule imports a `requirements.txt` file and generates a new
+`requirements.bzl` file. This is used via the `WORKSPACE` pattern:
+
+```python
+pip_repository(
+ name = "foo",
+ requirements = ":requirements.txt",
+)
+```
+
+You can then reference imported dependencies from your `BUILD` file with:
+
+```python
+load("@foo//:requirements.bzl", "requirement")
+py_library(
+ name = "bar",
+ ...
+ deps = [
+ "//my/other:dep",
+ requirement("requests"),
+ requirement("numpy"),
+ ],
+)
+```
+
+Or alternatively:
+```python
+load("@foo//:requirements.bzl", "all_requirements")
+py_binary(
+ name = "baz",
+ ...
+ deps = [
+ ":foo",
+ ] + all_requirements,
+)
+```
+""",
+ 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,
+ "--repo",
+ rctx.attr.repo,
+ "--repo-prefix",
+ rctx.attr.repo_prefix,
+ ]
+ if rctx.attr.annotation:
+ args.extend([
+ "--annotation",
+ rctx.path(rctx.attr.annotation),
+ ])
+
+ args = _parse_optional_attrs(rctx, args)
+
+ result = rctx.execute(
+ args,
+ # Manually construct the PYTHONPATH since we cannot use the toolchain here
+ environment = _create_repository_execution_environment(rctx),
+ quiet = rctx.attr.quiet,
+ timeout = rctx.attr.timeout,
+ )
+
+ if result.return_code:
+ fail("whl_library %s failed: %s (%s) error code: '%s'" % (rctx.attr.name, result.stdout, result.stderr, result.return_code))
+
+ return
+
+whl_library_attrs = {
+ "annotation": attr.label(
+ doc = (
+ "Optional json encoded file containing annotation to apply to the extracted wheel. " +
+ "See `package_annotation`"
+ ),
+ allow_files = True,
+ ),
+ "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",
+ ),
+}
+
+whl_library_attrs.update(**common_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,
+ ))
+
+# 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()])))