diff --git a/.bazelrc b/.bazelrc index e668a68822..b900d7ddb6 100644 --- a/.bazelrc +++ b/.bazelrc @@ -3,8 +3,8 @@ # This lets us glob() up all the files inside the examples to make them inputs to tests # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh -build --deleted_packages=examples/build_file_generation,examples/pip_install,examples/pip_parse,examples/py_import,examples/relative_requirements -query --deleted_packages=examples/build_file_generation,examples/pip_install,examples/pip_parse,examples/py_import,examples/relative_requirements +build --deleted_packages=examples/build_file_generation,examples/pip_install,examples/pip_parse,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements +query --deleted_packages=examples/build_file_generation,examples/pip_install,examples/pip_parse,examples/pip_repository_annotations,examples/py_import,examples/relative_requirements test --test_output=errors diff --git a/BUILD b/BUILD index 6ba643c4fd..cd19d368b5 100644 --- a/BUILD +++ b/BUILD @@ -32,6 +32,8 @@ filegroup( "//python:distribution", "//python/pip_install:distribution", "//third_party/github.com/bazelbuild/bazel-skylib/lib:distribution", + "//third_party/github.com/bazelbuild/bazel-skylib/rules:distribution", + "//third_party/github.com/bazelbuild/bazel-skylib/rules/private:distribution", "//tools:distribution", ], visibility = ["//examples:__pkg__"], diff --git a/docs/pip.md b/docs/pip.md index ce865a6980..baf7003b26 100644 --- a/docs/pip.md +++ b/docs/pip.md @@ -35,6 +35,33 @@ It also generates two targets for running pip-compile: | kwargs | other bazel attributes passed to the "_test" rule | none | + + +## package_annotation + +
+package_annotation(build_content, 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 + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :-------------: | :-------------: | :-------------: | +| build_content | Raw text to add to the generated
BUILD
file of a package. | None
|
+| copy_files | A mapping of src
and out
files for [@bazel_skylib//rules:copy_file.bzl][cf] | {}
|
+| copy_executables | 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 | A list of labels to add as data
dependencies to the generated py_library
target. | []
|
+| data_exclude_glob | A list of exclude glob patterns to add as data
to the generated py_library
target. | []
|
+| srcs_exclude_glob | A list of labels to add as srcs
to the generated py_library
target. | []
|
+
+
## pip_install
diff --git a/examples/BUILD b/examples/BUILD
index 8188ca7c81..44147e5c4a 100644
--- a/examples/BUILD
+++ b/examples/BUILD
@@ -27,6 +27,11 @@ bazel_integration_test(
timeout = "long",
)
+bazel_integration_test(
+ name = "pip_repository_annotations_example",
+ timeout = "long",
+)
+
bazel_integration_test(
name = "py_import_example",
timeout = "long",
diff --git a/examples/pip_repository_annotations/.bazelrc b/examples/pip_repository_annotations/.bazelrc
new file mode 100644
index 0000000000..9e7ef37327
--- /dev/null
+++ b/examples/pip_repository_annotations/.bazelrc
@@ -0,0 +1,2 @@
+# https://docs.bazel.build/versions/main/best-practices.html#using-the-bazelrc-file
+try-import %workspace%/user.bazelrc
diff --git a/examples/pip_repository_annotations/BUILD b/examples/pip_repository_annotations/BUILD
new file mode 100644
index 0000000000..a5a0561a37
--- /dev/null
+++ b/examples/pip_repository_annotations/BUILD
@@ -0,0 +1,30 @@
+load("@pip_installed//:requirements.bzl", "requirement")
+load("@rules_python//python:defs.bzl", "py_test")
+load("@rules_python//python:pip.bzl", "compile_pip_requirements")
+
+exports_files(
+ glob(["data/**"]),
+ visibility = ["//visibility:public"],
+)
+
+# This rule adds a convenient way to update the requirements file.
+compile_pip_requirements(
+ name = "requirements",
+ extra_args = ["--allow-unsafe"],
+)
+
+py_test(
+ name = "pip_parse_annotations_test",
+ srcs = ["pip_repository_annotations_test.py"],
+ env = {"WHEEL_PKG_DIR": "pip_parsed_wheel"},
+ main = "pip_repository_annotations_test.py",
+ deps = ["@pip_parsed_wheel//:pkg"],
+)
+
+py_test(
+ name = "pip_install_annotations_test",
+ srcs = ["pip_repository_annotations_test.py"],
+ env = {"WHEEL_PKG_DIR": "pip_installed/pypi__wheel"},
+ main = "pip_repository_annotations_test.py",
+ deps = [requirement("wheel")],
+)
diff --git a/examples/pip_repository_annotations/WORKSPACE b/examples/pip_repository_annotations/WORKSPACE
new file mode 100644
index 0000000000..ad16e67f15
--- /dev/null
+++ b/examples/pip_repository_annotations/WORKSPACE
@@ -0,0 +1,58 @@
+workspace(name = "pip_repository_annotations_example")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+ name = "rules_python",
+ sha256 = "cd6730ed53a002c56ce4e2f396ba3b3be262fd7cb68339f0377a45e8227fe332",
+ url = "https://github.com/bazelbuild/rules_python/releases/download/0.5.0/rules_python-0.5.0.tar.gz",
+)
+
+http_archive(
+ name = "bazel_skylib",
+ sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
+ urls = [
+ "https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+ "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
+ ],
+)
+
+load("@rules_python//python:pip.bzl", "package_annotation", "pip_install", "pip_parse")
+
+# Here we can see an example of annotations being applied to an arbitrary
+# package. For details on `package_annotation` and it's uses, see the
+# docs at @rules_python//docs:pip.md`.
+ANNOTATIONS = {
+ "wheel": package_annotation(
+ build_content = """\
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+write_file(
+ name = "generated_file",
+ out = "generated_file.txt",
+ content = ["Hello world from build content file"],
+)
+""",
+ copy_executables = {"@pip_repository_annotations_example//:data/copy_executable.py": "copied_content/executable.py"},
+ copy_files = {"@pip_repository_annotations_example//:data/copy_file.txt": "copied_content/file.txt"},
+ data = [":generated_file"],
+ data_exclude_glob = ["*.dist-info/RECORD"],
+ ),
+}
+
+# For a more thorough example of `pip_parse`. See `@rules_python//examples/pip_parse`
+pip_parse(
+ name = "pip_parsed",
+ annotations = ANNOTATIONS,
+ requirements_lock = "//:requirements.txt",
+)
+
+load("@pip_parsed//:requirements.bzl", "install_deps")
+
+install_deps()
+
+# For a more thorough example of `pip_install`. See `@rules_python//examples/pip_install`
+pip_install(
+ name = "pip_installed",
+ annotations = ANNOTATIONS,
+ requirements = "//:requirements.txt",
+)
diff --git a/examples/pip_repository_annotations/data/copy_executable.py b/examples/pip_repository_annotations/data/copy_executable.py
new file mode 100755
index 0000000000..20c6651e5b
--- /dev/null
+++ b/examples/pip_repository_annotations/data/copy_executable.py
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+
+if __name__ == "__main__":
+ print("Hello world from copied executable")
diff --git a/examples/pip_repository_annotations/data/copy_file.txt b/examples/pip_repository_annotations/data/copy_file.txt
new file mode 100644
index 0000000000..b1020f7b95
--- /dev/null
+++ b/examples/pip_repository_annotations/data/copy_file.txt
@@ -0,0 +1 @@
+Hello world from copied file
diff --git a/examples/pip_repository_annotations/pip_repository_annotations_test.py b/examples/pip_repository_annotations/pip_repository_annotations_test.py
new file mode 100644
index 0000000000..a8f0863c9e
--- /dev/null
+++ b/examples/pip_repository_annotations/pip_repository_annotations_test.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+import os
+import subprocess
+import unittest
+from glob import glob
+from pathlib import Path
+
+
+class PipRepositoryAnnotationsTest(unittest.TestCase):
+ maxDiff = None
+
+ def wheel_pkg_dir(self) -> str:
+ env = os.environ.get("WHEEL_PKG_DIR")
+ self.assertIsNotNone(env)
+ return env
+
+ def test_build_content_and_data(self):
+ generated_file = (
+ Path.cwd() / "external" / self.wheel_pkg_dir() / "generated_file.txt"
+ )
+ self.assertTrue(generated_file.exists())
+
+ content = generated_file.read_text().rstrip()
+ self.assertEqual(content, "Hello world from build content file")
+
+ def test_copy_files(self):
+ copied_file = (
+ Path.cwd() / "external" / self.wheel_pkg_dir() / "copied_content/file.txt"
+ )
+ self.assertTrue(copied_file.exists())
+
+ content = copied_file.read_text().rstrip()
+ self.assertEqual(content, "Hello world from copied file")
+
+ def test_copy_executables(self):
+ executable = (
+ Path.cwd()
+ / "external"
+ / self.wheel_pkg_dir()
+ / "copied_content/executable.py"
+ )
+ self.assertTrue(executable.exists())
+
+ proc = subprocess.run(
+ [executable], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+ stdout = proc.stdout.decode("utf-8").strip()
+ self.assertEqual(stdout, "Hello world from copied executable")
+
+ def test_data_exclude_glob(self):
+ files = glob("external/" + self.wheel_pkg_dir() + "/wheel-*.dist-info/*")
+ basenames = [Path(path).name for path in files]
+ self.assertIn("WHEEL", basenames)
+ self.assertNotIn("RECORD", basenames)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/examples/pip_repository_annotations/requirements.in b/examples/pip_repository_annotations/requirements.in
new file mode 100644
index 0000000000..2309722a93
--- /dev/null
+++ b/examples/pip_repository_annotations/requirements.in
@@ -0,0 +1 @@
+wheel
diff --git a/examples/pip_repository_annotations/requirements.txt b/examples/pip_repository_annotations/requirements.txt
new file mode 100644
index 0000000000..51d1dfc8e2
--- /dev/null
+++ b/examples/pip_repository_annotations/requirements.txt
@@ -0,0 +1,10 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# bazel run //:requirements.update
+#
+wheel==0.37.1 \
+ --hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \
+ --hash=sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4
+ # via -r requirements.in
diff --git a/python/pip.bzl b/python/pip.bzl
index 7776efec28..0530a2c9f1 100644
--- a/python/pip.bzl
+++ b/python/pip.bzl
@@ -13,11 +13,12 @@
# limitations under the License.
"""Import pip requirements into Bazel."""
-load("//python/pip_install:pip_repository.bzl", "pip_repository")
+load("//python/pip_install:pip_repository.bzl", "pip_repository", _package_annotation = "package_annotation")
load("//python/pip_install:repositories.bzl", "pip_install_dependencies")
load("//python/pip_install:requirements.bzl", _compile_pip_requirements = "compile_pip_requirements")
compile_pip_requirements = _compile_pip_requirements
+package_annotation = _package_annotation
def pip_install(requirements, name = "pip", **kwargs):
"""Accepts a `requirements.txt` file and installs the dependencies listed within.
diff --git a/python/pip_install/extract_wheels/__init__.py b/python/pip_install/extract_wheels/__init__.py
index f1b72543d2..cda6d541b0 100644
--- a/python/pip_install/extract_wheels/__init__.py
+++ b/python/pip_install/extract_wheels/__init__.py
@@ -12,7 +12,13 @@
import subprocess
import sys
-from python.pip_install.extract_wheels.lib import arguments, bazel, requirements
+from python.pip_install.extract_wheels.lib import (
+ annotation,
+ arguments,
+ bazel,
+ requirements,
+ wheel,
+)
def configure_reproducible_wheels() -> None:
@@ -58,6 +64,11 @@ def main() -> None:
required=True,
help="Path to requirements.txt from where to install dependencies",
)
+ parser.add_argument(
+ "--annotations",
+ type=annotation.annotations_map_from_str_path,
+ help="A json encoded file containing annotations for rendered packages.",
+ )
arguments.parse_common_args(parser)
args = parser.parse_args()
deserialized_args = dict(vars(args))
@@ -89,18 +100,26 @@ def main() -> None:
repo_label = "@%s" % args.repo
+ # Locate all wheels
+ wheels = [whl for whl in glob.glob("*.whl")]
+
+ # Collect all annotations
+ reqs = {whl: wheel.Wheel(whl).name for whl in wheels}
+ annotations = args.annotations.collect(reqs.values())
+
targets = [
'"{}{}"'.format(
repo_label,
bazel.extract_wheel(
- whl,
- extras,
- deserialized_args["pip_data_exclude"],
- args.enable_implicit_namespace_pkgs,
- args.repo_prefix,
+ wheel_file=whl,
+ extras=extras,
+ pip_data_exclude=deserialized_args["pip_data_exclude"],
+ enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs,
+ repo_prefix=args.repo_prefix,
+ annotation=annotations.get(name),
),
)
- for whl in glob.glob("*.whl")
+ for whl, name in reqs.items()
]
with open("requirements.bzl", "w") as requirement_file:
diff --git a/python/pip_install/extract_wheels/lib/BUILD b/python/pip_install/extract_wheels/lib/BUILD
index 2b0a91fa3d..1df1bed3aa 100644
--- a/python/pip_install/extract_wheels/lib/BUILD
+++ b/python/pip_install/extract_wheels/lib/BUILD
@@ -1,9 +1,11 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")
load("//python/pip_install:repositories.bzl", "requirement")
+load(":annotations_test_helpers.bzl", "package_annotation", "package_annotations_file")
py_library(
name = "lib",
srcs = [
+ "annotation.py",
"arguments.py",
"bazel.py",
"namespace_pkgs.py",
@@ -21,6 +23,43 @@ py_library(
],
)
+package_annotations_file(
+ name = "mock_annotations",
+ annotations = {
+ "pkg_a": package_annotation(),
+ "pkg_b": package_annotation(
+ data_exclude_glob = [
+ "*.foo",
+ "*.bar",
+ ],
+ ),
+ "pkg_c": package_annotation(
+ build_content = """\
+cc_library(
+ name = "my_target",
+ hdrs = glob(["**/*.h"]),
+ srcs = glob(["**/*.cc"]),
+)
+""",
+ data = [":my_target"],
+ ),
+ "pkg_d": package_annotation(
+ srcs_exclude_glob = ["pkg_d/tests/**"],
+ ),
+ },
+ tags = ["manual"],
+)
+
+py_test(
+ name = "annotations_test",
+ size = "small",
+ srcs = ["annotations_test.py"],
+ data = [":mock_annotations"],
+ env = {"MOCK_ANNOTATIONS": "$(rootpath :mock_annotations)"},
+ tags = ["unit"],
+ deps = [":lib"],
+)
+
py_test(
name = "bazel_test",
size = "small",
diff --git a/python/pip_install/extract_wheels/lib/annotation.py b/python/pip_install/extract_wheels/lib/annotation.py
new file mode 100644
index 0000000000..7ae2b348c3
--- /dev/null
+++ b/python/pip_install/extract_wheels/lib/annotation.py
@@ -0,0 +1,112 @@
+import json
+import logging
+from collections import OrderedDict
+from pathlib import Path
+from typing import Any, Dict, List
+
+
+class Annotation(OrderedDict):
+ """A python representation of `@rules_python//python:pip.bzl%package_annotation`"""
+
+ def __init__(self, content: Dict[str, Any]) -> None:
+
+ missing = []
+ ordered_content = OrderedDict()
+ for field in (
+ "build_content",
+ "copy_executables",
+ "copy_files",
+ "data",
+ "data_exclude_glob",
+ "srcs_exclude_glob",
+ ):
+ if field not in content:
+ missing.append(field)
+ continue
+ ordered_content.update({field: content.pop(field)})
+
+ if missing:
+ raise ValueError("Data missing from initial annotation: {}".format(missing))
+
+ if content:
+ raise ValueError(
+ "Unexpected data passed to annotations: {}".format(
+ sorted(list(content.keys()))
+ )
+ )
+
+ return OrderedDict.__init__(self, ordered_content)
+
+ @property
+ def build_content(self) -> str:
+ return self["build_content"]
+
+ @property
+ def copy_executables(self) -> Dict[str, str]:
+ return self["copy_executables"]
+
+ @property
+ def copy_files(self) -> Dict[str, str]:
+ return self["copy_files"]
+
+ @property
+ def data(self) -> List[str]:
+ return self["data"]
+
+ @property
+ def data_exclude_glob(self) -> List[str]:
+ return self["data_exclude_glob"]
+
+ @property
+ def srcs_exclude_glob(self) -> List[str]:
+ return self["srcs_exclude_glob"]
+
+
+class AnnotationsMap:
+ """A mapping of python package names to [Annotation]"""
+
+ def __init__(self, json_file: Path):
+ content = json.loads(json_file.read_text())
+
+ self._annotations = {pkg: Annotation(data) for (pkg, data) in content.items()}
+
+ @property
+ def annotations(self) -> Dict[str, Annotation]:
+ return self._annotations
+
+ def collect(self, requirements: List[str]) -> Dict[str, Annotation]:
+ unused = self.annotations
+ collection = {}
+ for pkg in requirements:
+ if pkg in unused:
+ collection.update({pkg: unused.pop(pkg)})
+
+ logging.warning("Unused annotations: {}".format(sorted(list(unused.keys()))))
+
+ return collection
+
+
+def annotation_from_str_path(path: str) -> Annotation:
+ """Load an annotation from a json encoded file
+
+ Args:
+ path (str): The path to a json encoded file
+
+ Returns:
+ Annotation: The deserialized annotations
+ """
+ json_file = Path(path)
+ content = json.loads(json_file.read_text())
+ return Annotation(content)
+
+
+def annotations_map_from_str_path(path: str) -> AnnotationsMap:
+ """Load an annotations map from a json encoded file
+
+ Args:
+ path (str): The path to a json encoded file
+
+ Returns:
+ AnnotationsMap: The deserialized annotations map
+ """
+ return AnnotationsMap(Path(path))
diff --git a/python/pip_install/extract_wheels/lib/annotations_test.py b/python/pip_install/extract_wheels/lib/annotations_test.py
new file mode 100644
index 0000000000..5b32d3cbb6
--- /dev/null
+++ b/python/pip_install/extract_wheels/lib/annotations_test.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+
+import os
+import textwrap
+import unittest
+from pathlib import Path
+
+from python.pip_install.extract_wheels.lib.annotation import Annotation, AnnotationsMap
+
+
+class AnnotationsTestCase(unittest.TestCase):
+
+ maxDiff = None
+
+ def test_annotations_constructor(self) -> None:
+ annotations_env = os.environ.get("MOCK_ANNOTATIONS")
+ self.assertIsNotNone(annotations_env)
+
+ annotations_path = Path.cwd() / annotations_env
+ self.assertTrue(annotations_path.exists())
+
+ annotations_map = AnnotationsMap(annotations_path)
+ self.assertListEqual(
+ list(annotations_map.annotations.keys()),
+ ["pkg_a", "pkg_b", "pkg_c", "pkg_d"],
+ )
+
+ collection = annotations_map.collect(["pkg_a", "pkg_b", "pkg_c", "pkg_d"])
+
+ self.assertEqual(
+ collection["pkg_a"],
+ Annotation(
+ {
+ "build_content": None,
+ "copy_executables": {},
+ "copy_files": {},
+ "data": [],
+ "data_exclude_glob": [],
+ "srcs_exclude_glob": [],
+ }
+ ),
+ )
+
+ self.assertEqual(
+ collection["pkg_b"],
+ Annotation(
+ {
+ "build_content": None,
+ "copy_executables": {},
+ "copy_files": {},
+ "data": [],
+ "data_exclude_glob": ["*.foo", "*.bar"],
+ "srcs_exclude_glob": [],
+ }
+ ),
+ )
+
+ self.assertEqual(
+ collection["pkg_c"],
+ Annotation(
+ {
+ "build_content": textwrap.dedent(
+ """\
+ cc_library(
+ name = "my_target",
+ hdrs = glob(["**/*.h"]),
+ srcs = glob(["**/*.cc"]),
+ )
+ """
+ ),
+ "copy_executables": {},
+ "copy_files": {},
+ "data": [":my_target"],
+ "data_exclude_glob": [],
+ "srcs_exclude_glob": [],
+ }
+ ),
+ )
+
+ self.assertEqual(
+ collection["pkg_d"],
+ Annotation(
+ {
+ "build_content": None,
+ "copy_executables": {},
+ "copy_files": {},
+ "data": [],
+ "data_exclude_glob": [],
+ "srcs_exclude_glob": ["pkg_d/tests/**"],
+ }
+ ),
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/python/pip_install/extract_wheels/lib/annotations_test_helpers.bzl b/python/pip_install/extract_wheels/lib/annotations_test_helpers.bzl
new file mode 100644
index 0000000000..dbd1124670
--- /dev/null
+++ b/python/pip_install/extract_wheels/lib/annotations_test_helpers.bzl
@@ -0,0 +1,33 @@
+"""Helper macros and rules for testing the `annotations` module of `extract_wheels`"""
+
+load("//python:pip.bzl", _package_annotation = "package_annotation")
+
+package_annotation = _package_annotation
+
+def _package_annotations_file_impl(ctx):
+ output = ctx.actions.declare_file(ctx.label.name + ".annotations.json")
+
+ annotations = {package: json.decode(data) for (package, data) in ctx.attr.annotations.items()}
+ ctx.actions.write(
+ output = output,
+ content = json.encode_indent(annotations, indent = " " * 4),
+ )
+
+ return DefaultInfo(
+ files = depset([output]),
+ runfiles = ctx.runfiles(files = [output]),
+ )
+
+package_annotations_file = rule(
+ implementation = _package_annotations_file_impl,
+ doc = (
+ "Consumes `package_annotation` definitions in the same way " +
+ "`pip_repository` rules do to produce an annotations file."
+ ),
+ attrs = {
+ "annotations": attr.string_dict(
+ doc = "See `@rules_python//python:pip.bzl%package_annotation",
+ mandatory = True,
+ ),
+ },
+)
diff --git a/python/pip_install/extract_wheels/lib/bazel.py b/python/pip_install/extract_wheels/lib/bazel.py
index e880c20381..f1ab8ba219 100644
--- a/python/pip_install/extract_wheels/lib/bazel.py
+++ b/python/pip_install/extract_wheels/lib/bazel.py
@@ -6,7 +6,12 @@
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Set
-from python.pip_install.extract_wheels.lib import namespace_pkgs, purelib, wheel
+from python.pip_install.extract_wheels.lib import (
+ annotation,
+ namespace_pkgs,
+ purelib,
+ wheel,
+)
WHEEL_FILE_LABEL = "whl"
PY_LIBRARY_LABEL = "pkg"
@@ -77,13 +82,45 @@ def generate_entry_point_rule(script: str, pkg: str) -> str:
)
+def generate_copy_commands(src, dest, is_executable=False) -> str:
+ """Generate a [@bazel_skylib//rules:copy_file.bzl%copy_file][cf] target
+
+ [cf]: https://github.com/bazelbuild/bazel-skylib/blob/1.1.1/docs/copy_file_doc.md
+
+ Args:
+ src (str): The label for the `src` attribute of [copy_file][cf]
+ dest (str): The label for the `out` attribute of [copy_file][cf]
+ is_executable (bool, optional): Whether or not the file being copied is executable.
+ sets `is_executable` for [copy_file][cf]
+
+ Returns:
+ str: A `copy_file` instantiation.
+ """
+ return textwrap.dedent(
+ """\
+ copy_file(
+ name = "{dest}.copy",
+ src = "{src}",
+ out = "{dest}",
+ is_executable = {is_executable},
+ )
+ """.format(
+ src=src,
+ dest=dest,
+ is_executable=is_executable,
+ )
+ )
+
+
def generate_build_file_contents(
name: str,
dependencies: List[str],
whl_file_deps: List[str],
- pip_data_exclude: List[str],
+ data_exclude: List[str],
tags: List[str],
- additional_targets: List[str] = [],
+ srcs_exclude: List[str] = [],
+ data: List[str] = [],
+ additional_content: List[str] = [],
) -> str:
"""Generate a BUILD file for an unzipped Wheel
@@ -91,9 +128,9 @@ def generate_build_file_contents(
name: the target name of the py_library
dependencies: a list of Bazel labels pointing to dependencies of the library
whl_file_deps: a list of Bazel labels pointing to wheel file dependencies of this wheel.
- pip_data_exclude: more patterns to exclude from the data attribute of generated py_library rules.
+ data_exclude: more patterns to exclude from the data attribute of generated py_library rules.
tags: list of tags to apply to generated py_library rules.
- additional_targets: A list of additional targets to append to the BUILD file contents.
+ additional_content: A list of additional content to append to the BUILD file.
Returns:
A complete BUILD file as a string
@@ -102,22 +139,28 @@ def generate_build_file_contents(
there may be no Python sources whatsoever (e.g. packages written in Cython: like `pymssql`).
"""
- data_exclude = [
- "*.whl",
- "**/__pycache__/**",
- "**/*.py",
- "**/*.pyc",
- f"{WHEEL_ENTRY_POINT_PREFIX}*.py",
- "**/* *",
- "BUILD.bazel",
- "WORKSPACE",
- ] + pip_data_exclude
+ data_exclude = list(
+ set(
+ [
+ "*.whl",
+ "**/__pycache__/**",
+ "**/* *",
+ "**/*.py",
+ "**/*.pyc",
+ "BUILD.bazel",
+ "WORKSPACE",
+ f"{WHEEL_ENTRY_POINT_PREFIX}*.py",
+ ]
+ + data_exclude
+ )
+ )
return "\n".join(
[
textwrap.dedent(
"""\
load("@rules_python//python:defs.bzl", "py_library", "py_binary")
+ load("@rules_python//third_party/github.com/bazelbuild/bazel-skylib/rules:copy_file.bzl", "copy_file")
package(default_visibility = ["//visibility:public"])
@@ -139,8 +182,8 @@ def generate_build_file_contents(
py_library(
name = "{name}",
- srcs = glob(["**/*.py"], exclude=["{entry_point_prefix}*.py", "**/__pycache__/**"], allow_empty = True),
- data = glob(["**/*"], exclude={data_exclude}),
+ srcs = glob(["**/*.py"], exclude={srcs_exclude}, allow_empty = True),
+ data = {data} + glob(["**/*"], exclude={data_exclude}),
# This makes this directory a top-level in the python import
# search path for anything that depends on this.
imports = ["."],
@@ -149,18 +192,20 @@ def generate_build_file_contents(
)
""".format(
name=name,
- dependencies=",".join(dependencies),
- data_exclude=json.dumps(data_exclude),
+ dependencies=",".join(sorted(dependencies)),
+ data_exclude=json.dumps(sorted(data_exclude)),
whl_file_label=WHEEL_FILE_LABEL,
- whl_file_deps=",".join(whl_file_deps),
- tags=",".join(['"%s"' % t for t in tags]),
+ whl_file_deps=",".join(sorted(whl_file_deps)),
+ tags=",".join(sorted(['"%s"' % t for t in tags])),
data_label=DATA_LABEL,
dist_info_label=DIST_INFO_LABEL,
entry_point_prefix=WHEEL_ENTRY_POINT_PREFIX,
+ srcs_exclude=json.dumps(sorted(srcs_exclude)),
+ data=json.dumps(sorted(data)),
)
)
]
- + additional_targets
+ + additional_content
)
@@ -297,6 +342,7 @@ def extract_wheel(
repo_prefix: str,
incremental: bool = False,
incremental_dir: Path = Path("."),
+ annotation: Optional[annotation.Annotation] = None,
) -> Optional[str]:
"""Extracts wheel into given directory and creates py_library and filegroup targets.
@@ -308,6 +354,7 @@ def extract_wheel(
incremental: If true the extract the wheel in a format suitable for an external repository. This
effects the names of libraries and their dependencies, which point to other external repositories.
incremental_dir: An optional override for the working directory of incremental builds.
+ annotation: An optional set of annotations to apply to the BUILD contents of the wheel.
Returns:
The Bazel label for the extracted wheel, in the form '//path/to/wheel'.
@@ -367,13 +414,36 @@ def extract_wheel(
)
with open(os.path.join(directory, "BUILD.bazel"), "w") as build_file:
+ additional_content = entry_points
+ data = []
+ data_exclude = pip_data_exclude
+ srcs_exclude = []
+ if annotation:
+ for src, dest in annotation.copy_files.items():
+ data.append(dest)
+ additional_content.append(generate_copy_commands(src, dest))
+ for src, dest in annotation.copy_executables.items():
+ data.append(dest)
+ additional_content.append(
+ generate_copy_commands(src, dest, is_executable=True)
+ )
+ data.extend(annotation.data)
+ data_exclude.extend(annotation.data_exclude_glob)
+ srcs_exclude.extend(annotation.srcs_exclude_glob)
+ if annotation.build_content:
+ additional_content.append(annotation.build_content)
+
contents = generate_build_file_contents(
- PY_LIBRARY_LABEL if incremental else sanitise_name(whl.name, repo_prefix),
- sanitised_dependencies,
- sanitised_wheel_file_dependencies,
- pip_data_exclude,
- ["pypi_name=" + whl.name, "pypi_version=" + whl.metadata.version],
- entry_points,
+ name=PY_LIBRARY_LABEL
+ if incremental
+ else sanitise_name(whl.name, repo_prefix),
+ dependencies=sanitised_dependencies,
+ whl_file_deps=sanitised_wheel_file_dependencies,
+ data_exclude=data_exclude,
+ data=data,
+ srcs_exclude=srcs_exclude,
+ tags=["pypi_name=" + whl.name, "pypi_version=" + whl.metadata.version],
+ additional_content=additional_content,
)
build_file.write(contents)
diff --git a/python/pip_install/parse_requirements_to_bzl/__init__.py b/python/pip_install/parse_requirements_to_bzl/__init__.py
index 1e1261de7c..7a2338442c 100644
--- a/python/pip_install/parse_requirements_to_bzl/__init__.py
+++ b/python/pip_install/parse_requirements_to_bzl/__init__.py
@@ -3,20 +3,20 @@
import shlex
import sys
import textwrap
-from typing import List, Tuple
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple
from pip._internal.network.session import PipSession
-from pip._internal.req import constructors, parse_requirements
+from pip._internal.req import constructors
from pip._internal.req.req_file import (
RequirementsFileParser,
get_file_content,
get_line_parser,
- handle_line,
preprocess,
)
from pip._internal.req.req_install import InstallRequirement
-from python.pip_install.extract_wheels.lib import arguments, bazel
+from python.pip_install.extract_wheels.lib import annotation, arguments, bazel
def parse_install_requirements(
@@ -56,7 +56,25 @@ def repo_names_and_requirements(
]
-def generate_parsed_requirements_contents(all_args: argparse.Namespace) -> str:
+def parse_whl_library_args(args: argparse.Namespace) -> Dict[str, Any]:
+ whl_library_args = dict(vars(args))
+ whl_library_args = arguments.deserialize_structured_args(whl_library_args)
+ whl_library_args.setdefault("python_interpreter", sys.executable)
+
+ # These arguments are not used by `whl_library`
+ for arg in ("requirements_lock", "annotations"):
+ if arg in whl_library_args:
+ whl_library_args.pop(arg)
+
+ return whl_library_args
+
+
+def generate_parsed_requirements_contents(
+ requirements_lock: Path,
+ repo_prefix: str,
+ whl_library_args: Dict[str, Any],
+ annotations: Dict[str, str] = dict(),
+) -> str:
"""
Parse each requirement from the requirements_lock file, and prepare arguments for each
repository rule, which will represent the individual requirements.
@@ -64,28 +82,21 @@ def generate_parsed_requirements_contents(all_args: argparse.Namespace) -> str:
Generates a requirements.bzl file containing a macro (install_deps()) which instantiates
a repository rule for each requirment in the lock file.
"""
-
- args = dict(vars(all_args))
- args = arguments.deserialize_structured_args(args)
- args.setdefault("python_interpreter", sys.executable)
- # Pop this off because it wont be used as a config argument to the whl_library rule.
- requirements_lock = args.pop("requirements_lock")
-
install_req_and_lines = parse_install_requirements(
- requirements_lock, args["extra_pip_args"]
+ requirements_lock, whl_library_args["extra_pip_args"]
)
repo_names_and_reqs = repo_names_and_requirements(
- install_req_and_lines, args["repo_prefix"]
+ install_req_and_lines, repo_prefix
)
all_requirements = ", ".join(
[
- bazel.sanitised_repo_library_label(ir.name, repo_prefix=args["repo_prefix"])
+ bazel.sanitised_repo_library_label(ir.name, repo_prefix=repo_prefix)
for ir, _ in install_req_and_lines
]
)
all_whl_requirements = ", ".join(
[
- bazel.sanitised_repo_file_label(ir.name, repo_prefix=args["repo_prefix"])
+ bazel.sanitised_repo_file_label(ir.name, repo_prefix=repo_prefix)
for ir, _ in install_req_and_lines
]
)
@@ -99,6 +110,7 @@ def generate_parsed_requirements_contents(all_args: argparse.Namespace) -> str:
_packages = {repo_names_and_reqs}
_config = {args}
+ _annotations = {annotations}
def _clean_name(name):
return name.replace("-", "_").replace(".", "_").lower()
@@ -120,24 +132,32 @@ def entry_point(pkg, script = None):
script = pkg
return "@{repo_prefix}" + _clean_name(pkg) + "//:{entry_point_prefix}_" + script
+ def _get_annotation(requirement):
+ # This expects to parse `setuptools==58.2.0 --hash=sha256:2551203ae6955b9876741a26ab3e767bb3242dafe86a32a749ea0d78b6792f11`
+ # down wo `setuptools`.
+ name = requirement.split(" ")[0].split("=")[0]
+ return _annotations.get(name)
+
def install_deps():
for name, requirement in _packages:
whl_library(
name = name,
requirement = requirement,
+ annotation = _get_annotation(requirement),
**_config,
)
""".format(
all_requirements=all_requirements,
all_whl_requirements=all_whl_requirements,
- repo_names_and_reqs=repo_names_and_reqs,
- args=args,
- repo_prefix=args["repo_prefix"],
- py_library_label=bazel.PY_LIBRARY_LABEL,
- wheel_file_label=bazel.WHEEL_FILE_LABEL,
+ annotations=json.dumps(annotations),
+ args=whl_library_args,
data_label=bazel.DATA_LABEL,
dist_info_label=bazel.DIST_INFO_LABEL,
entry_point_prefix=bazel.WHEEL_ENTRY_POINT_PREFIX,
+ py_library_label=bazel.PY_LIBRARY_LABEL,
+ repo_names_and_reqs=repo_names_and_reqs,
+ repo_prefix=repo_prefix,
+ wheel_file_label=bazel.WHEEL_FILE_LABEL,
)
)
@@ -181,8 +201,42 @@ def main() -> None:
required=True,
help="timeout to use for pip operation.",
)
+ parser.add_argument(
+ "--annotations",
+ type=annotation.annotations_map_from_str_path,
+ help="A json encoded file containing annotations for rendered packages.",
+ )
arguments.parse_common_args(parser)
args = parser.parse_args()
+ # Check for any annotations which match packages in the locked requirements file
+ install_requirements = parse_install_requirements(
+ args.requirements_lock, args.extra_pip_args
+ )
+ req_names = sorted([req.name for req, _ in install_requirements])
+ annotations = args.annotations.collect(req_names)
+
+ # Write all rendered annotation files and generate a list of the labels to write to the requirements file
+ annotated_requirements = dict()
+ for name, content in annotations.items():
+ annotation_path = Path(name + ".annotation.json")
+ annotation_path.write_text(json.dumps(content, indent=4))
+ annotated_requirements.update(
+ {
+ name: "@{}//:{}.annotation.json".format(
+ args.repo_prefix.rstrip("_"), name
+ )
+ }
+ )
+
with open("requirements.bzl", "w") as requirement_file:
- requirement_file.write(generate_parsed_requirements_contents(args))
+ whl_library_args = parse_whl_library_args(args)
+
+ requirement_file.write(
+ generate_parsed_requirements_contents(
+ requirements_lock=args.requirements_lock,
+ repo_prefix=args.repo_prefix,
+ whl_library_args=whl_library_args,
+ annotations=annotated_requirements,
+ )
+ )
diff --git a/python/pip_install/parse_requirements_to_bzl/extract_single_wheel/__init__.py b/python/pip_install/parse_requirements_to_bzl/extract_single_wheel/__init__.py
index 2c03ff3c04..198aefae83 100644
--- a/python/pip_install/parse_requirements_to_bzl/extract_single_wheel/__init__.py
+++ b/python/pip_install/parse_requirements_to_bzl/extract_single_wheel/__init__.py
@@ -8,6 +8,7 @@
from python.pip_install.extract_wheels import configure_reproducible_wheels
from python.pip_install.extract_wheels.lib import arguments, bazel, requirements
+from python.pip_install.extract_wheels.lib.annotation import annotation_from_str_path
def main() -> None:
@@ -20,6 +21,11 @@ def main() -> None:
required=True,
help="A single PEP508 requirement specifier string.",
)
+ parser.add_argument(
+ "--annotation",
+ type=annotation_from_str_path,
+ help="A json encoded file containing annotations for rendered packages.",
+ )
arguments.parse_common_args(parser)
args = parser.parse_args()
deserialized_args = dict(vars(args))
@@ -61,10 +67,11 @@ def main() -> None:
whl = next(iter(glob.glob("*.whl")))
bazel.extract_wheel(
- whl,
- extras,
- deserialized_args["pip_data_exclude"],
- args.enable_implicit_namespace_pkgs,
+ wheel_file=whl,
+ extras=extras,
+ pip_data_exclude=deserialized_args["pip_data_exclude"],
+ enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs,
incremental=True,
repo_prefix=args.repo_prefix,
+ annotation=args.annotation,
)
diff --git a/python/pip_install/parse_requirements_to_bzl/parse_requirements_to_bzl_test.py b/python/pip_install/parse_requirements_to_bzl/parse_requirements_to_bzl_test.py
index 9619af5f0b..c0608bf499 100644
--- a/python/pip_install/parse_requirements_to_bzl/parse_requirements_to_bzl_test.py
+++ b/python/pip_install/parse_requirements_to_bzl/parse_requirements_to_bzl_test.py
@@ -6,6 +6,7 @@
from python.pip_install.parse_requirements_to_bzl import (
generate_parsed_requirements_contents,
+ parse_whl_library_args,
)
@@ -28,7 +29,12 @@ def test_generated_requirements_bzl(self) -> None:
args.python_interpreter = "/custom/python3"
args.python_interpreter_target = "@custom_python//:exec"
args.environment = json.dumps({"arg": {}})
- contents = generate_parsed_requirements_contents(args)
+ whl_library_args = parse_whl_library_args(args)
+ contents = generate_parsed_requirements_contents(
+ requirements_lock=args.requirements_lock,
+ repo_prefix=args.repo_prefix,
+ whl_library_args=whl_library_args,
+ )
library_target = "@pip_parsed_deps_pypi__foo//:pkg"
whl_target = "@pip_parsed_deps_pypi__foo//:whl"
all_requirements = 'all_requirements = ["{library_target}"]'.format(
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index 1dc49c7240..4a3acf5b10 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -127,6 +127,11 @@ def _pip_repository_impl(rctx):
# We need a BUILD file to load the generated requirements.bzl
rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
+ # Write the annotations file to pass to the wheel maker
+ annotations = {package: json.decode(data) for (package, data) in rctx.attr.annotations.items()}
+ annotations_file = rctx.path("annotations.json")
+ rctx.file(annotations_file, json.encode_indent(annotations, indent = " " * 4))
+
pypath = _construct_pypath(rctx)
if rctx.attr.incremental:
@@ -141,6 +146,8 @@ def _pip_repository_impl(rctx):
str(rctx.attr.quiet),
"--timeout",
str(rctx.attr.timeout),
+ "--annotations",
+ annotations_file,
]
args += ["--python_interpreter", _get_python_interpreter_attr(rctx)]
@@ -154,6 +161,8 @@ def _pip_repository_impl(rctx):
"python.pip_install.extract_wheels",
"--requirements",
rctx.path(rctx.attr.requirements),
+ "--annotations",
+ annotations_file,
]
args += ["--repo", rctx.attr.name, "--repo-prefix", rctx.attr.repo_prefix]
@@ -253,6 +262,9 @@ For incremental mode the packages will be of the form
}
pip_repository_attrs = {
+ "annotations": attr.string_dict(
+ doc = "Optional annotations to apply to packages",
+ ),
"incremental": attr.bool(
default = False,
doc = "Create the repository in incremental mode.",
@@ -318,7 +330,7 @@ py_binary(
environ = common_env,
)
-def _impl_whl_library(rctx):
+def _whl_library_impl(rctx):
python_interpreter = _resolve_python_interpreter(rctx)
# pointer to parent repo so these rules rerun if the definitions in requirements.bzl change.
@@ -334,6 +346,12 @@ def _impl_whl_library(rctx):
"--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(
@@ -350,6 +368,13 @@ def _impl_whl_library(rctx):
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.",
@@ -367,6 +392,40 @@ whl_library = repository_rule(
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 = _impl_whl_library,
+ implementation = _whl_library_impl,
environ = common_env,
)
+
+def package_annotation(
+ 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:
+ 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(
+ build_content = 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/third_party/github.com/bazelbuild/bazel-skylib/rules/BUILD b/third_party/github.com/bazelbuild/bazel-skylib/rules/BUILD
new file mode 100644
index 0000000000..6857449878
--- /dev/null
+++ b/third_party/github.com/bazelbuild/bazel-skylib/rules/BUILD
@@ -0,0 +1,36 @@
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+licenses(["notice"])
+
+package(default_visibility = ["//visibility:public"])
+
+bzl_library(
+ name = "copy_file",
+ srcs = ["copy_file.bzl"],
+ deps = ["//third_party/github.com/bazelbuild/bazel-skylib/rules/private:copy_file_private"],
+)
+
+filegroup(
+ name = "test_deps",
+ testonly = True,
+ srcs = [
+ "BUILD",
+ ] + glob(["*.bzl"]),
+)
+
+# The files needed for distribution
+filegroup(
+ name = "distribution",
+ srcs = [
+ "BUILD",
+ ] + glob(["*.bzl"]),
+ visibility = [
+ "//:__pkg__",
+ ],
+)
+
+# export bzl files for the documentation
+exports_files(
+ glob(["*.bzl"]),
+ visibility = ["//:__subpackages__"],
+)
diff --git a/third_party/github.com/bazelbuild/bazel-skylib/rules/copy_file.bzl b/third_party/github.com/bazelbuild/bazel-skylib/rules/copy_file.bzl
new file mode 100644
index 0000000000..2908fa6e85
--- /dev/null
+++ b/third_party/github.com/bazelbuild/bazel-skylib/rules/copy_file.bzl
@@ -0,0 +1,29 @@
+# Copyright 2019 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.
+
+"""A rule that copies a file to another place.
+
+native.genrule() is sometimes used to copy files (often wishing to rename them).
+The 'copy_file' rule does this with a simpler interface than genrule.
+
+The rule uses a Bash command on Linux/macOS/non-Windows, and a cmd.exe command
+on Windows (no Bash is required).
+"""
+
+load(
+ "@rules_python//third_party/github.com/bazelbuild/bazel-skylib/rules/private:copy_file_private.bzl",
+ _copy_file = "copy_file",
+)
+
+copy_file = _copy_file
diff --git a/third_party/github.com/bazelbuild/bazel-skylib/rules/private/BUILD b/third_party/github.com/bazelbuild/bazel-skylib/rules/private/BUILD
new file mode 100644
index 0000000000..a1aeb39914
--- /dev/null
+++ b/third_party/github.com/bazelbuild/bazel-skylib/rules/private/BUILD
@@ -0,0 +1,18 @@
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+licenses(["notice"])
+
+bzl_library(
+ name = "copy_file_private",
+ srcs = ["copy_file_private.bzl"],
+ visibility = ["//third_party/github.com/bazelbuild/bazel-skylib/rules:__pkg__"],
+)
+
+# The files needed for distribution
+filegroup(
+ name = "distribution",
+ srcs = glob(["*"]),
+ visibility = [
+ "//:__subpackages__",
+ ],
+)
diff --git a/third_party/github.com/bazelbuild/bazel-skylib/rules/private/copy_file_private.bzl b/third_party/github.com/bazelbuild/bazel-skylib/rules/private/copy_file_private.bzl
new file mode 100644
index 0000000000..d044c9767e
--- /dev/null
+++ b/third_party/github.com/bazelbuild/bazel-skylib/rules/private/copy_file_private.bzl
@@ -0,0 +1,141 @@
+# Copyright 2019 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.
+
+"""Implementation of copy_file macro and underlying rules.
+
+These rules copy a file to another location using Bash (on Linux/macOS) or
+cmd.exe (on Windows). '_copy_xfile' marks the resulting file executable,
+'_copy_file' does not.
+"""
+
+def copy_cmd(ctx, src, dst):
+ # Most Windows binaries built with MSVC use a certain argument quoting
+ # scheme. Bazel uses that scheme too to quote arguments. However,
+ # cmd.exe uses different semantics, so Bazel's quoting is wrong here.
+ # To fix that we write the command to a .bat file so no command line
+ # quoting or escaping is required.
+ bat = ctx.actions.declare_file(ctx.label.name + "-cmd.bat")
+ ctx.actions.write(
+ output = bat,
+ # Do not use lib/shell.bzl's shell.quote() method, because that uses
+ # Bash quoting syntax, which is different from cmd.exe's syntax.
+ content = "@copy /Y \"%s\" \"%s\" >NUL" % (
+ src.path.replace("/", "\\"),
+ dst.path.replace("/", "\\"),
+ ),
+ is_executable = True,
+ )
+ ctx.actions.run(
+ inputs = [src],
+ tools = [bat],
+ outputs = [dst],
+ executable = "cmd.exe",
+ arguments = ["/C", bat.path.replace("/", "\\")],
+ mnemonic = "CopyFile",
+ progress_message = "Copying files",
+ use_default_shell_env = True,
+ )
+
+def copy_bash(ctx, src, dst):
+ ctx.actions.run_shell(
+ tools = [src],
+ outputs = [dst],
+ command = "cp -f \"$1\" \"$2\"",
+ arguments = [src.path, dst.path],
+ mnemonic = "CopyFile",
+ progress_message = "Copying files",
+ use_default_shell_env = True,
+ )
+
+def _copy_file_impl(ctx):
+ if ctx.attr.allow_symlink:
+ ctx.actions.symlink(
+ output = ctx.outputs.out,
+ target_file = ctx.file.src,
+ is_executable = ctx.attr.is_executable,
+ )
+ elif ctx.attr.is_windows:
+ copy_cmd(ctx, ctx.file.src, ctx.outputs.out)
+ else:
+ copy_bash(ctx, ctx.file.src, ctx.outputs.out)
+
+ files = depset(direct = [ctx.outputs.out])
+ runfiles = ctx.runfiles(files = [ctx.outputs.out])
+ if ctx.attr.is_executable:
+ return [DefaultInfo(files = files, runfiles = runfiles, executable = ctx.outputs.out)]
+ else:
+ return [DefaultInfo(files = files, runfiles = runfiles)]
+
+_ATTRS = {
+ "allow_symlink": attr.bool(mandatory = True),
+ "is_executable": attr.bool(mandatory = True),
+ "is_windows": attr.bool(mandatory = True),
+ "out": attr.output(mandatory = True),
+ "src": attr.label(mandatory = True, allow_single_file = True),
+}
+
+_copy_file = rule(
+ implementation = _copy_file_impl,
+ provides = [DefaultInfo],
+ attrs = _ATTRS,
+)
+
+_copy_xfile = rule(
+ implementation = _copy_file_impl,
+ executable = True,
+ provides = [DefaultInfo],
+ attrs = _ATTRS,
+)
+
+def copy_file(name, src, out, is_executable = False, allow_symlink = False, **kwargs):
+ """Copies a file to another location.
+
+ `native.genrule()` is sometimes used to copy files (often wishing to rename them). The 'copy_file' rule does this with a simpler interface than genrule.
+
+ This rule uses a Bash command on Linux/macOS/non-Windows, and a cmd.exe command on Windows (no Bash is required).
+
+ Args:
+ name: Name of the rule.
+ src: A Label. The file to make a copy of. (Can also be the label of a rule
+ that generates a file.)
+ out: Path of the output file, relative to this package.
+ is_executable: A boolean. Whether to make the output file executable. When
+ True, the rule's output can be executed using `bazel run` and can be
+ in the srcs of binary and test rules that require executable sources.
+ WARNING: If `allow_symlink` is True, `src` must also be executable.
+ allow_symlink: A boolean. Whether to allow symlinking instead of copying.
+ When False, the output is always a hard copy. When True, the output
+ *can* be a symlink, but there is no guarantee that a symlink is
+ created (i.e., at the time of writing, we don't create symlinks on
+ Windows). Set this to True if you need fast copying and your tools can
+ handle symlinks (which most UNIX tools can).
+ **kwargs: further keyword arguments, e.g. `visibility`
+ """
+
+ copy_file_impl = _copy_file
+ if is_executable:
+ copy_file_impl = _copy_xfile
+
+ copy_file_impl(
+ name = name,
+ src = src,
+ out = out,
+ is_windows = select({
+ "@bazel_tools//src/conditions:host_windows": True,
+ "//conditions:default": False,
+ }),
+ is_executable = is_executable,
+ allow_symlink = allow_symlink,
+ **kwargs
+ )