| # Copyright 2017 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. |
| """Import pip requirements into Bazel.""" |
| |
| load("//python/pip_install:pip_repository.bzl", "pip_repository") |
| load("//python/pip_install:repositories.bzl", "pip_install_dependencies") |
| |
| def _pip_import_impl(repository_ctx): |
| """Core implementation of pip_import.""" |
| |
| # Add an empty top-level BUILD file. |
| # This is because Bazel requires BUILD files along all paths accessed |
| # via //this/sort/of:path and we wouldn't be able to load our generated |
| # requirements.bzl without it. |
| repository_ctx.file("BUILD", "") |
| |
| interpreter_path = repository_ctx.attr.python_interpreter |
| if repository_ctx.attr.python_interpreter_target != None: |
| target = repository_ctx.attr.python_interpreter_target |
| interpreter_path = repository_ctx.path(target) |
| |
| args = [ |
| interpreter_path, |
| repository_ctx.path(repository_ctx.attr._script), |
| "--python_interpreter", |
| interpreter_path, |
| "--name", |
| repository_ctx.attr.name, |
| "--input", |
| repository_ctx.path(repository_ctx.attr.requirements), |
| "--output", |
| repository_ctx.path("requirements.bzl"), |
| "--directory", |
| repository_ctx.path(""), |
| ] |
| if repository_ctx.attr.extra_pip_args: |
| args += [ |
| "--extra_pip_args", |
| "\"" + " ".join(repository_ctx.attr.extra_pip_args) + "\"", |
| ] |
| |
| # To see the output, pass: quiet=False |
| result = repository_ctx.execute(args, timeout=repository_ctx.attr.timeout) |
| |
| if result.return_code: |
| fail("pip_import failed: %s (%s)" % (result.stdout, result.stderr)) |
| |
| pip_import = repository_rule( |
| attrs = { |
| "extra_pip_args": attr.string_list( |
| doc = "Extra arguments to pass on to pip. Must not contain spaces.", |
| ), |
| "python_interpreter": attr.string(default = "python", doc = """ |
| The command to run the Python interpreter used to invoke pip and unpack the |
| wheels. |
| """), |
| "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_import to invoke |
| pip using the same interpreter as your toolchain. If set, takes precedence over |
| python_interpreter. |
| """), |
| "requirements": attr.label( |
| mandatory = True, |
| allow_single_file = True, |
| doc = "The label of the requirements.txt file.", |
| ), |
| "timeout": attr.int( |
| default = 600, |
| doc = "Timeout (in seconds) for repository fetch." |
| ), |
| "_script": attr.label( |
| executable = True, |
| default = Label("//tools:piptool.par"), |
| cfg = "host", |
| ), |
| }, |
| implementation = _pip_import_impl, |
| 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_import( |
| name = "foo", |
| requirements = ":requirements.txt", |
| ) |
| load("@foo//:requirements.bzl", "pip_install") |
| pip_install() |
| ``` |
| |
| 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("futures"), |
| requirement("mock"), |
| ], |
| ) |
| ``` |
| |
| Or alternatively: |
| ```python |
| load("@foo//:requirements.bzl", "all_requirements") |
| py_binary( |
| name = "baz", |
| ... |
| deps = [ |
| ":foo", |
| ] + all_requirements, |
| ) |
| ``` |
| """, |
| ) |
| |
| # We don't provide a `pip2_import` that would use the `python2` system command |
| # because this command does not exist on all platforms. On most (but not all) |
| # systems, `python` means Python 2 anyway. See also #258. |
| |
| def pip3_import(**kwargs): |
| """A wrapper around pip_import that uses the `python3` system command. |
| |
| Use this for requirements of PY3 programs. |
| """ |
| pip_import(python_interpreter = "python3", **kwargs) |
| |
| def pip_repositories(): |
| """Pull in dependencies needed to use the packaging rules.""" |
| |
| # At the moment this is a placeholder, in that it does not actually pull in |
| # any dependencies. However, it does do some validation checking. |
| # |
| # As a side effect of migrating our canonical workspace name from |
| # "@io_bazel_rules_python" to "@rules_python" (#203), users who still |
| # imported us by the old name would get a confusing error about a |
| # repository dependency cycle in their workspace. (The cycle is likely |
| # related to the fact that our repo name is hardcoded into the template |
| # in piptool.py.) |
| # |
| # To produce a more informative error message in this situation, we |
| # fail-fast here if we detect that we're not being imported by the new |
| # name. (I believe we have always had the requirement that we're imported |
| # by the canonical name, because of the aforementioned hardcoding.) |
| # |
| # Users who, against best practice, do not call pip_repositories() in their |
| # workspace will not benefit from this check. |
| if "rules_python" not in native.existing_rules(): |
| message = "=" * 79 + """\n\ |
| It appears that you are trying to import rules_python without using its |
| canonical name, "@rules_python". This does not work. Please change your |
| WORKSPACE file to import this repo with `name = "rules_python"` instead. |
| """ |
| if "io_bazel_rules_python" in native.existing_rules(): |
| message += """\n\ |
| Note that the previous name of "@io_bazel_rules_python" is no longer used. |
| See https://github.com/bazelbuild/rules_python/issues/203 for context. |
| """ |
| message += "=" * 79 |
| fail(message) |
| |
| def pip_install(requirements, name = "pip", **kwargs): |
| """Imports a `requirements.txt` file and generates a new `requirements.bzl` file. |
| |
| This is used via the `WORKSPACE` pattern: |
| |
| ```python |
| pip_install( |
| requirements = ":requirements.txt", |
| ) |
| ``` |
| |
| You can then reference imported dependencies from your `BUILD` file with: |
| |
| ```python |
| load("@pip//:requirements.bzl", "requirement") |
| py_library( |
| name = "bar", |
| ... |
| deps = [ |
| "//my/other:dep", |
| requirement("requests"), |
| requirement("numpy"), |
| ], |
| ) |
| ``` |
| |
| Args: |
| requirements: A 'requirements.txt' pip requirements file. |
| name: A unique name for the created external repository (default 'pip'). |
| **kwargs: Keyword arguments passed directly to the `pip_repository` repository rule. |
| """ |
| # Just in case our dependencies weren't already fetched |
| pip_install_dependencies() |
| |
| pip_repository( |
| name = name, |
| requirements = requirements, |
| **kwargs |
| ) |