|  | # 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/pip_install:requirements_parser.bzl", parse_requirements = "parse") | 
|  | 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:bzlmod_enabled.bzl", "BZLMOD_ENABLED") | 
|  | load("//python/private:normalize_name.bzl", "normalize_name") | 
|  | load("//python/private:patch_whl.bzl", "patch_whl") | 
|  | load("//python/private:render_pkg_aliases.bzl", "render_pkg_aliases") | 
|  | load("//python/private:toolchains_repo.bzl", "get_host_os_arch") | 
|  | load("//python/private:which.bzl", "which_with_fail") | 
|  | load("//python/private/bzlmod:pip_repository.bzl", _pip_hub_repository_bzlmod = "pip_repository") | 
|  |  | 
|  | CPPFLAGS = "CPPFLAGS" | 
|  |  | 
|  | COMMAND_LINE_TOOLS_PATH_SLUG = "commandlinetools" | 
|  |  | 
|  | _WHEEL_ENTRY_POINT_PREFIX = "rules_python_wheel_entry_point" | 
|  |  | 
|  | # Kept for not creating merge conflicts with PR#1476, can be removed later. | 
|  | pip_hub_repository_bzlmod = _pip_hub_repository_bzlmod | 
|  |  | 
|  | 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: 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 [] | 
|  |  | 
|  | xcode_sdk_location = rctx.execute([which_with_fail("xcode-select", rctx), "--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 [] | 
|  |  | 
|  | er = rctx.execute([ | 
|  | python_interpreter, | 
|  | "-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( | 
|  | 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): | 
|  | """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, 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 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 _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: | 
|  | macro_tmpl = "@%s//{}:{}" % rctx.attr.name | 
|  | aliases = render_pkg_aliases(repo_name = rctx.attr.name, bzl_packages = bzl_packages) | 
|  | for path, contents in aliases.items(): | 
|  | rctx.file(path, contents) | 
|  | else: | 
|  | macro_tmpl = "@%s_{}//:{}" % rctx.attr.name | 
|  |  | 
|  | rctx.file("BUILD.bazel", _BUILD_FILE_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, "pkg") | 
|  | for p in bzl_packages | 
|  | ]), | 
|  | "%%ALL_WHL_REQUIREMENTS%%": _format_repr_list([ | 
|  | 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(sorted(imports)), | 
|  | "%%MACRO_TMPL%%": macro_tmpl, | 
|  | "%%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 = True, | 
|  | doc = """\ | 
|  | If true, extra aliases will be created in the main `hub` repo - i.e. the repo | 
|  | where the `requirements.bzl` is located. This means that for a Python package | 
|  | `PyYAML` initialized within a `pip` `hub_repo` there will be the following | 
|  | aliases generated: | 
|  | - `@pip//pyyaml` will point to `@pip_pyyaml//:pkg` | 
|  | - `@pip//pyyaml:data` will point to `@pip_pyyaml//:data` | 
|  | - `@pip//pyyaml:dist_info` will point to `@pip_pyyaml//:dist_info` | 
|  | - `@pip//pyyaml:pkg` will point to `@pip_pyyaml//:pkg` | 
|  | - `@pip//pyyaml:whl` will point to `@pip_pyyaml//:whl` | 
|  |  | 
|  | This is to keep the dependencies coming from PyPI to have more ergonomic label | 
|  | names and support smooth transition to `bzlmod`. | 
|  | """, | 
|  | ), | 
|  | "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. | 
|  | """, | 
|  | ), | 
|  | "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 = """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. | 
|  |  | 
|  | This macro wraps the [`pip_repository`](./pip_repository.md) rule that invokes `pip`. | 
|  | In your WORKSPACE file: | 
|  |  | 
|  | ```starlark | 
|  | load("@rules_python//python:pip.bzl", "pip_parse") | 
|  |  | 
|  | pip_parse( | 
|  | name = "pip_deps", | 
|  | requirements_lock = ":requirements.txt", | 
|  | ) | 
|  |  | 
|  | load("@pip_deps//:requirements.bzl", "install_deps") | 
|  |  | 
|  | install_deps() | 
|  | ``` | 
|  |  | 
|  | You can then reference installed dependencies from a `BUILD` file with: | 
|  |  | 
|  | ```starlark | 
|  | load("@pip_deps//:requirements.bzl", "requirement") | 
|  |  | 
|  | py_library( | 
|  | name = "bar", | 
|  | ... | 
|  | deps = [ | 
|  | "//my/other:dep", | 
|  | requirement("requests"), | 
|  | requirement("numpy"), | 
|  | ], | 
|  | ) | 
|  | ``` | 
|  |  | 
|  | 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("@pip_deps//: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_deps//: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, | 
|  | ] | 
|  |  | 
|  | args = _parse_optional_attrs(rctx, args) | 
|  |  | 
|  | # Manually construct the PYTHONPATH since we cannot use the toolchain here | 
|  | environment = _create_repository_execution_environment(rctx, python_interpreter) | 
|  |  | 
|  | result = rctx.execute( | 
|  | args, | 
|  | environment = environment, | 
|  | 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)) | 
|  |  | 
|  | 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 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, | 
|  | ) | 
|  |  | 
|  | result = rctx.execute( | 
|  | args + ["--whl-file", whl_path], | 
|  | environment = environment, | 
|  | 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)) | 
|  |  | 
|  | 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( | 
|  | repo_prefix = rctx.attr.repo_prefix, | 
|  | whl_name = whl_path.basename, | 
|  | dependencies = metadata["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 | 
|  |  | 
|  | 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_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 | 
|  | ], | 
|  | ), | 
|  | } | 
|  |  | 
|  | 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()]))) |