diff --git a/third_party/rules_pycross/LICENSE b/third_party/rules_pycross/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/third_party/rules_pycross/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/rules_pycross/pycross/private/providers.bzl b/third_party/rules_pycross/pycross/private/providers.bzl new file mode 100644 index 000000000..f55e98693 --- /dev/null +++ b/third_party/rules_pycross/pycross/private/providers.bzl @@ -0,0 +1,32 @@ +# Copyright 2023 Jeremy Volkman. All rights reserved. +# 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. + +"""Pycross providers.""" + +PycrossWheelInfo = provider( + doc = "Information about a Python wheel.", + fields = { + "name_file": "File: A file containing the canonical name of the wheel.", + "wheel_file": "File: The wheel file itself.", + }, +) + +PycrossTargetEnvironmentInfo = provider( + doc = "A target environment description.", + fields = { + "file": "The JSON file containing target environment information.", + "python_compatible_with": "A list of constraints used to select this platform.", + }, +) diff --git a/third_party/rules_pycross/pycross/private/tools/BUILD.bazel b/third_party/rules_pycross/pycross/private/tools/BUILD.bazel new file mode 100644 index 000000000..867b771aa --- /dev/null +++ b/third_party/rules_pycross/pycross/private/tools/BUILD.bazel @@ -0,0 +1,53 @@ +# Copyright 2023 Jeremy Volkman. All rights reserved. +# 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("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") + +package(default_visibility = ["//visibility:private"]) + +py_library( + name = "namespace_pkgs", + srcs = [ + "namespace_pkgs.py", + ], +) + +py_test( + name = "namespace_pkgs_test", + size = "small", + srcs = [ + "namespace_pkgs_test.py", + ], + tags = [ + "unit", + # TODO(philsc): Make this work. + "manual", + ], + deps = [ + ":namespace_pkgs", + ], +) + +py_binary( + name = "wheel_installer", + srcs = ["wheel_installer.py"], + visibility = ["//visibility:public"], + deps = [ + ":namespace_pkgs", + # TODO(philsc): Make this work with what's available in rules_python. + #"@rules_pycross_pypi_deps_absl_py//:pkg", + #"@rules_pycross_pypi_deps_installer//:pkg", + ], +) diff --git a/third_party/rules_pycross/pycross/private/tools/namespace_pkgs.py b/third_party/rules_pycross/pycross/private/tools/namespace_pkgs.py new file mode 100644 index 000000000..59300ffcd --- /dev/null +++ b/third_party/rules_pycross/pycross/private/tools/namespace_pkgs.py @@ -0,0 +1,109 @@ +"""Utility functions to discover python package types""" +import os +import textwrap +from pathlib import Path # supported in >= 3.4 +from typing import List +from typing import Optional +from typing import Set + + +def implicit_namespace_packages( + directory: str, ignored_dirnames: Optional[List[str]] = None +) -> Set[Path]: + """Discovers namespace packages implemented using the 'native namespace packages' method. + + AKA 'implicit namespace packages', which has been supported since Python 3.3. + See: https://packaging.python.org/guides/packaging-namespace-packages/#native-namespace-packages + + Args: + directory: The root directory to recursively find packages in. + ignored_dirnames: A list of directories to exclude from the search + + Returns: + The set of directories found under root to be packages using the native namespace method. + """ + namespace_pkg_dirs: Set[Path] = set() + standard_pkg_dirs: Set[Path] = set() + directory_path = Path(directory) + ignored_dirname_paths: List[Path] = [Path(p) for p in ignored_dirnames or ()] + # Traverse bottom-up because a directory can be a namespace pkg because its child contains module files. + for dirpath, dirnames, filenames in map( + lambda t: (Path(t[0]), *t[1:]), os.walk(directory_path, topdown=False) + ): + if "__init__.py" in filenames: + standard_pkg_dirs.add(dirpath) + continue + elif ignored_dirname_paths: + is_ignored_dir = dirpath in ignored_dirname_paths + child_of_ignored_dir = any( + d in dirpath.parents for d in ignored_dirname_paths + ) + if is_ignored_dir or child_of_ignored_dir: + continue + + dir_includes_py_modules = _includes_python_modules(filenames) + parent_of_namespace_pkg = any( + Path(dirpath, d) in namespace_pkg_dirs for d in dirnames + ) + parent_of_standard_pkg = any( + Path(dirpath, d) in standard_pkg_dirs for d in dirnames + ) + parent_of_pkg = parent_of_namespace_pkg or parent_of_standard_pkg + if ( + (dir_includes_py_modules or parent_of_pkg) + and + # The root of the directory should never be an implicit namespace + dirpath != directory_path + ): + namespace_pkg_dirs.add(dirpath) + return namespace_pkg_dirs + + +def add_pkgutil_style_namespace_pkg_init(dir_path: Path) -> None: + """Adds 'pkgutil-style namespace packages' init file to the given directory + + See: https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages + + Args: + dir_path: The directory to create an __init__.py for. + + Raises: + ValueError: If the directory already contains an __init__.py file + """ + ns_pkg_init_filepath = os.path.join(dir_path, "__init__.py") + + if os.path.isfile(ns_pkg_init_filepath): + raise ValueError("%s already contains an __init__.py file." % dir_path) + + with open(ns_pkg_init_filepath, "w") as ns_pkg_init_f: + # See https://packaging.python.org/guides/packaging-namespace-packages/#pkgutil-style-namespace-packages + ns_pkg_init_f.write( + textwrap.dedent( + """\ + # __path__ manipulation added by bazelbuild/rules_python to support namespace pkgs. + __path__ = __import__('pkgutil').extend_path(__path__, __name__) + """ + ) + ) + + +def _includes_python_modules(files: List[str]) -> bool: + """ + In order to only transform directories that Python actually considers namespace pkgs + we need to detect if a directory includes Python modules. + + Which files are loadable as modules is extension based, and the particular set of extensions + varies by platform. + + See: + 1. https://github.com/python/cpython/blob/7d9d25dbedfffce61fc76bc7ccbfa9ae901bf56f/Lib/importlib/machinery.py#L19 + 2. PEP 420 -- Implicit Namespace Packages, Specification - https://www.python.org/dev/peps/pep-0420/#specification + 3. dynload_shlib.c and dynload_win.c in python/cpython. + """ + module_suffixes = { + ".py", # Source modules + ".pyc", # Compiled bytecode modules + ".so", # Unix extension modules + ".pyd", # https://docs.python.org/3/faq/windows.html#is-a-pyd-file-the-same-as-a-dll + } + return any(Path(f).suffix in module_suffixes for f in files) diff --git a/third_party/rules_pycross/pycross/private/tools/namespace_pkgs_test.py b/third_party/rules_pycross/pycross/private/tools/namespace_pkgs_test.py new file mode 100644 index 000000000..49945f9b8 --- /dev/null +++ b/third_party/rules_pycross/pycross/private/tools/namespace_pkgs_test.py @@ -0,0 +1,179 @@ +import os +import pathlib +import shutil +import tempfile +import unittest +from typing import Optional +from typing import Set + +from pycross.private.tools import namespace_pkgs + + +class TempDir: + def __init__(self) -> None: + self.dir = tempfile.mkdtemp() + + def root(self) -> str: + return self.dir + + def add_dir(self, rel_path: str) -> None: + d = pathlib.Path(self.dir, rel_path) + d.mkdir(parents=True) + + def add_file(self, rel_path: str, contents: Optional[str] = None) -> None: + f = pathlib.Path(self.dir, rel_path) + f.parent.mkdir(parents=True, exist_ok=True) + if contents: + with open(str(f), "w") as writeable_f: + writeable_f.write(contents) + else: + f.touch() + + def remove(self) -> None: + shutil.rmtree(self.dir) + + +class TestImplicitNamespacePackages(unittest.TestCase): + def assertPathsEqual(self, actual: Set[pathlib.Path], expected: Set[str]) -> None: + self.assertEqual(actual, {pathlib.Path(p) for p in expected}) + + def test_in_current_directory(self) -> None: + directory = TempDir() + directory.add_file("foo/bar/biz.py") + directory.add_file("foo/bee/boo.py") + directory.add_file("foo/buu/__init__.py") + directory.add_file("foo/buu/bii.py") + cwd = os.getcwd() + os.chdir(directory.root()) + expected = { + "foo", + "foo/bar", + "foo/bee", + } + try: + actual = namespace_pkgs.implicit_namespace_packages(".") + self.assertPathsEqual(actual, expected) + finally: + os.chdir(cwd) + directory.remove() + + def test_finds_correct_namespace_packages(self) -> None: + directory = TempDir() + directory.add_file("foo/bar/biz.py") + directory.add_file("foo/bee/boo.py") + directory.add_file("foo/buu/__init__.py") + directory.add_file("foo/buu/bii.py") + + expected = { + directory.root() + "/foo", + directory.root() + "/foo/bar", + directory.root() + "/foo/bee", + } + actual = namespace_pkgs.implicit_namespace_packages(directory.root()) + self.assertPathsEqual(actual, expected) + + def test_ignores_empty_directories(self) -> None: + directory = TempDir() + directory.add_file("foo/bar/biz.py") + directory.add_dir("foo/cat") + + expected = { + directory.root() + "/foo", + directory.root() + "/foo/bar", + } + actual = namespace_pkgs.implicit_namespace_packages(directory.root()) + self.assertPathsEqual(actual, expected) + + def test_empty_case(self) -> None: + directory = TempDir() + directory.add_file("foo/__init__.py") + directory.add_file("foo/bar/__init__.py") + directory.add_file("foo/bar/biz.py") + + actual = namespace_pkgs.implicit_namespace_packages(directory.root()) + self.assertEqual(actual, set()) + + def test_ignores_non_module_files_in_directories(self) -> None: + directory = TempDir() + directory.add_file("foo/__init__.pyi") + directory.add_file("foo/py.typed") + + actual = namespace_pkgs.implicit_namespace_packages(directory.root()) + self.assertEqual(actual, set()) + + def test_parent_child_relationship_of_namespace_pkgs(self): + directory = TempDir() + directory.add_file("foo/bar/biff/my_module.py") + directory.add_file("foo/bar/biff/another_module.py") + + expected = { + directory.root() + "/foo", + directory.root() + "/foo/bar", + directory.root() + "/foo/bar/biff", + } + actual = namespace_pkgs.implicit_namespace_packages(directory.root()) + self.assertPathsEqual(actual, expected) + + def test_parent_child_relationship_of_namespace_and_standard_pkgs(self): + directory = TempDir() + directory.add_file("foo/bar/biff/__init__.py") + directory.add_file("foo/bar/biff/another_module.py") + + expected = { + directory.root() + "/foo", + directory.root() + "/foo/bar", + } + actual = namespace_pkgs.implicit_namespace_packages(directory.root()) + self.assertPathsEqual(actual, expected) + + def test_parent_child_relationship_of_namespace_and_nested_standard_pkgs(self): + directory = TempDir() + directory.add_file("foo/bar/__init__.py") + directory.add_file("foo/bar/biff/another_module.py") + directory.add_file("foo/bar/biff/__init__.py") + directory.add_file("foo/bar/boof/big_module.py") + directory.add_file("foo/bar/boof/__init__.py") + directory.add_file("fim/in_a_ns_pkg.py") + + expected = { + directory.root() + "/foo", + directory.root() + "/fim", + } + actual = namespace_pkgs.implicit_namespace_packages(directory.root()) + self.assertPathsEqual(actual, expected) + + def test_recognized_all_nonstandard_module_types(self): + directory = TempDir() + directory.add_file("ayy/my_module.pyc") + directory.add_file("bee/ccc/dee/eee.so") + directory.add_file("eff/jee/aych.pyd") + + expected = { + directory.root() + "/ayy", + directory.root() + "/bee", + directory.root() + "/bee/ccc", + directory.root() + "/bee/ccc/dee", + directory.root() + "/eff", + directory.root() + "/eff/jee", + } + actual = namespace_pkgs.implicit_namespace_packages(directory.root()) + self.assertPathsEqual(actual, expected) + + def test_skips_ignored_directories(self): + directory = TempDir() + directory.add_file("foo/boo/my_module.py") + directory.add_file("foo/bar/another_module.py") + + expected = { + directory.root() + "/foo", + directory.root() + "/foo/bar", + } + actual = namespace_pkgs.implicit_namespace_packages( + directory.root(), + ignored_dirnames=[directory.root() + "/foo/boo"], + ) + self.assertPathsEqual(actual, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/third_party/rules_pycross/pycross/private/tools/wheel_installer.py b/third_party/rules_pycross/pycross/private/tools/wheel_installer.py new file mode 100644 index 000000000..6d3673669 --- /dev/null +++ b/third_party/rules_pycross/pycross/private/tools/wheel_installer.py @@ -0,0 +1,122 @@ +""" +A tool that invokes pypa/build to build the given sdist tarball. +""" + +import os +import shutil +import tempfile +from pathlib import Path +from typing import Any + +from absl import app +from absl.flags import argparse_flags +from installer import install +from installer.destinations import SchemeDictionaryDestination +from installer.sources import WheelFile +from pycross.private.tools import namespace_pkgs + + +def setup_namespace_pkg_compatibility(wheel_dir: Path) -> None: + """Converts native namespace packages to pkgutil-style packages + + Namespace packages can be created in one of three ways. They are detailed here: + https://packaging.python.org/guides/packaging-namespace-packages/#creating-a-namespace-package + + 'pkgutil-style namespace packages' (2) and 'pkg_resources-style namespace packages' (3) works in Bazel, but + 'native namespace packages' (1) do not. + + We ensure compatibility with Bazel of method 1 by converting them into method 2. + + Args: + wheel_dir: the directory of the wheel to convert + """ + + namespace_pkg_dirs = namespace_pkgs.implicit_namespace_packages( + str(wheel_dir), + ignored_dirnames=["%s/bin" % wheel_dir], + ) + + for ns_pkg_dir in namespace_pkg_dirs: + namespace_pkgs.add_pkgutil_style_namespace_pkg_init(ns_pkg_dir) + + +def main(args: Any) -> None: + dest_dir = args.directory + lib_dir = dest_dir / "site-packages" + destination = SchemeDictionaryDestination( + scheme_dict={ + "platlib": str(lib_dir), + "purelib": str(lib_dir), + "headers": str(dest_dir / "include"), + "scripts": str(dest_dir / "bin"), + "data": str(dest_dir / "data"), + }, + interpreter="/usr/bin/env python3", # Generic; it's not feasible to run these scripts directly. + script_kind="posix", + bytecode_optimization_levels=[0, 1], + ) + + link_dir = Path(tempfile.mkdtemp()) + if args.wheel_name_file: + with open(args.wheel_name_file, "r") as f: + wheel_name = f.read().strip() + else: + wheel_name = os.path.basename(args.wheel) + + link_path = link_dir / wheel_name + os.symlink(os.path.join(os.getcwd(), args.wheel), link_path) + + try: + with WheelFile.open(link_path) as source: + install( + source=source, + destination=destination, + # Additional metadata that is generated by the installation tool. + additional_metadata={ + "INSTALLER": b"https://github.com/jvolkman/rules_pycross", + }, + ) + finally: + shutil.rmtree(link_dir, ignore_errors=True) + + setup_namespace_pkg_compatibility(lib_dir) + + +def parse_flags(argv) -> Any: + parser = argparse_flags.ArgumentParser(description="Extract a Python wheel.") + + parser.add_argument( + "--wheel", + type=Path, + required=True, + help="The wheel file path.", + ) + + parser.add_argument( + "--wheel-name-file", + type=Path, + required=False, + help="A file containing the canonical name of the wheel.", + ) + + parser.add_argument( + "--enable-implicit-namespace-pkgs", + action="store_true", + help="If true, disables conversion of implicit namespace packages and will unzip as-is.", + ) + + parser.add_argument( + "--directory", + type=Path, + help="The output path.", + ) + + return parser.parse_args(argv[1:]) + + +if __name__ == "__main__": + # When under `bazel run`, change to the actual working dir. + if "BUILD_WORKING_DIRECTORY" in os.environ: + os.chdir(os.environ["BUILD_WORKING_DIRECTORY"]) + + app.run(main, flags_parser=parse_flags) diff --git a/third_party/rules_pycross/pycross/private/wheel_library.bzl b/third_party/rules_pycross/pycross/private/wheel_library.bzl new file mode 100644 index 000000000..25a2497ab --- /dev/null +++ b/third_party/rules_pycross/pycross/private/wheel_library.bzl @@ -0,0 +1,137 @@ +# Copyright 2023 Jeremy Volkman. All rights reserved. +# 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. + +"""Implementation of the pycross_wheel_library rule.""" + +load("@bazel_skylib//lib:paths.bzl", "paths") +load("@rules_python//python:defs.bzl", "PyInfo") +load(":providers.bzl", "PycrossWheelInfo") + +def _pycross_wheel_library_impl(ctx): + out = ctx.actions.declare_directory(ctx.attr.name) + + wheel_target = ctx.attr.wheel + if PycrossWheelInfo in wheel_target: + wheel_file = wheel_target[PycrossWheelInfo].wheel_file + name_file = wheel_target[PycrossWheelInfo].name_file + else: + wheel_file = ctx.file.wheel + name_file = None + + args = ctx.actions.args().use_param_file("--flagfile=%s") + args.add("--wheel", wheel_file) + args.add("--directory", out.path) + + inputs = [wheel_file] + if name_file: + inputs.append(name_file) + args.add("--wheel-name-file", name_file) + + if ctx.attr.enable_implicit_namespace_pkgs: + args.add("--enable-implicit-namespace-pkgs") + + ctx.actions.run( + inputs = inputs, + outputs = [out], + executable = ctx.executable._tool, + arguments = [args], + # Set environment variables to make generated .pyc files reproducible. + env = { + "PYTHONHASHSEED": "0", + "SOURCE_DATE_EPOCH": "315532800", + }, + mnemonic = "WheelInstall", + progress_message = "Installing %s" % ctx.file.wheel.basename, + ) + + has_py2_only_sources = ctx.attr.python_version == "PY2" + has_py3_only_sources = ctx.attr.python_version == "PY3" + if not has_py2_only_sources: + for d in ctx.attr.deps: + if d[PyInfo].has_py2_only_sources: + has_py2_only_sources = True + break + if not has_py3_only_sources: + for d in ctx.attr.deps: + if d[PyInfo].has_py3_only_sources: + has_py3_only_sources = True + break + + # TODO: Is there a more correct way to get this runfiles-relative import path? + imp = paths.join( + ctx.label.workspace_name or ctx.workspace_name, # Default to the local workspace. + ctx.label.package, + ctx.label.name, + "site-packages", # we put lib files in this subdirectory. + ) + + imports = depset( + direct = [imp], + transitive = [d[PyInfo].imports for d in ctx.attr.deps], + ) + transitive_sources = depset( + direct = [out], + transitive = [dep[PyInfo].transitive_sources for dep in ctx.attr.deps if PyInfo in dep], + ) + runfiles = ctx.runfiles(files = [out]) + for d in ctx.attr.deps: + runfiles = runfiles.merge(d[DefaultInfo].default_runfiles) + + return [ + DefaultInfo( + files = depset(direct = [out]), + runfiles = runfiles, + ), + PyInfo( + has_py2_only_sources = has_py2_only_sources, + has_py3_only_sources = has_py3_only_sources, + imports = imports, + transitive_sources = transitive_sources, + uses_shared_libraries = True, # Docs say this is unused + ), + ] + +pycross_wheel_library = rule( + implementation = _pycross_wheel_library_impl, + attrs = { + "deps": attr.label_list( + doc = "A list of this wheel's Python library dependencies.", + providers = [DefaultInfo, PyInfo], + ), + "enable_implicit_namespace_pkgs": attr.bool( + default = True, + 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. + """, + ), + "python_version": attr.string( + doc = "The python version required for this wheel ('PY2' or 'PY3')", + values = ["PY2", "PY3", ""], + ), + "wheel": attr.label( + doc = "The wheel file.", + allow_single_file = [".whl"], + mandatory = True, + ), + "_tool": attr.label( + default = Label("//pycross/private/tools:wheel_installer"), + cfg = "exec", + executable = True, + ), + }, +)