diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index cfd4efdcda..e6352efcea 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -217,7 +217,15 @@ _other_attrs = { ), "strip_path_prefixes": attr.string_list( default = [], - doc = "path prefixes to strip from files added to the generated package", + doc = """\ +Path prefixes to strip from files added to the generated package. +Prefixes are checked **in order** and only the **first match** will be used. + +For example: ++ `["foo", "foo/bar/baz"]` will strip `"foo/bar/baz/file.py"` to `"bar/baz/file.py"` ++ `["foo/bar/baz", "foo"]` will strip `"foo/bar/baz/file.py"` to `"file.py"` and + `"foo/file2.py"` to `"file2.py"` +""", ), "summary": attr.string( doc = "A one-line summary of what the distribution does", diff --git a/tests/tools/BUILD.bazel b/tests/tools/BUILD.bazel new file mode 100644 index 0000000000..4d163f19f1 --- /dev/null +++ b/tests/tools/BUILD.bazel @@ -0,0 +1,23 @@ +# Copyright 2025 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:py_test.bzl", "py_test") + +licenses(["notice"]) + +py_test( + name = "wheelmaker_test", + size = "small", + srcs = ["wheelmaker_test.py"], + deps = ["//tools:wheelmaker"], +) diff --git a/tests/tools/wheelmaker_test.py b/tests/tools/wheelmaker_test.py new file mode 100644 index 0000000000..0efe1c9fbc --- /dev/null +++ b/tests/tools/wheelmaker_test.py @@ -0,0 +1,38 @@ +import unittest + +import tools.wheelmaker as wheelmaker + + +class ArcNameFromTest(unittest.TestCase): + def test_arcname_from(self) -> None: + # (name, distribution_prefix, strip_path_prefixes, want) tuples + checks = [ + ("a/b/c/file.py", "", [], "a/b/c/file.py"), + ("a/b/c/file.py", "", ["a"], "/b/c/file.py"), + ("a/b/c/file.py", "", ["a/b/"], "c/file.py"), + # only first found is used and it's not cumulative. + ("a/b/c/file.py", "", ["a/", "b/"], "b/c/file.py"), + # Examples from docs + ("foo/bar/baz/file.py", "", ["foo", "foo/bar/baz"], "/bar/baz/file.py"), + ("foo/bar/baz/file.py", "", ["foo/bar/baz", "foo"], "/file.py"), + ("foo/file2.py", "", ["foo/bar/baz", "foo"], "/file2.py"), + # Files under the distribution prefix (eg mylib-1.0.0-dist-info) + # are unmodified + ("mylib-0.0.1-dist-info/WHEEL", "mylib", [], "mylib-0.0.1-dist-info/WHEEL"), + ("mylib/a/b/c/WHEEL", "mylib", ["mylib"], "mylib/a/b/c/WHEEL"), + ] + for name, prefix, strip, want in checks: + with self.subTest( + name=name, + distribution_prefix=prefix, + strip_path_prefixes=strip, + want=want, + ): + got = wheelmaker.arcname_from( + name=name, distribution_prefix=prefix, strip_path_prefixes=strip + ) + self.assertEqual(got, want) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index 8b775e1541..3401c749ed 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -24,6 +24,7 @@ import stat import sys import zipfile +from collections.abc import Iterable from pathlib import Path _ZIP_EPOCH = (1980, 1, 1, 0, 0, 0) @@ -98,6 +99,30 @@ def normalize_pep440(version): return str(packaging.version.Version(f"0+{sanitized}")) +def arcname_from( + name: str, distribution_prefix: str, strip_path_prefixes: Sequence[str] = () +) -> str: + """Return the within-archive name for a given file path name. + + Prefixes to strip are checked in order and only the first match will be used. + + Args: + name: The file path eg 'mylib/a/b/c/file.py' + distribution_prefix: The + strip_path_prefixes: Remove these prefixes from names. + """ + # Always use unix path separators. + normalized_arcname = name.replace(os.path.sep, "/") + # Don't manipulate names filenames in the .distinfo or .data directories. + if distribution_prefix and normalized_arcname.startswith(distribution_prefix): + return normalized_arcname + for prefix in strip_path_prefixes: + if normalized_arcname.startswith(prefix): + return normalized_arcname[len(prefix) :] + + return normalized_arcname + + class _WhlFile(zipfile.ZipFile): def __init__( self, @@ -126,18 +151,6 @@ def data_path(self, basename): def add_file(self, package_filename, real_filename): """Add given file to the distribution.""" - def arcname_from(name): - # Always use unix path separators. - normalized_arcname = name.replace(os.path.sep, "/") - # Don't manipulate names filenames in the .distinfo or .data directories. - if normalized_arcname.startswith(self._distribution_prefix): - return normalized_arcname - for prefix in self._strip_path_prefixes: - if normalized_arcname.startswith(prefix): - return normalized_arcname[len(prefix) :] - - return normalized_arcname - if os.path.isdir(real_filename): directory_contents = os.listdir(real_filename) for file_ in directory_contents: @@ -147,7 +160,11 @@ def arcname_from(name): ) return - arcname = arcname_from(package_filename) + arcname = arcname_from( + package_filename, + distribution_prefix=self._distribution_prefix, + strip_path_prefixes=self._strip_path_prefixes, + ) zinfo = self._zipinfo(arcname) # Write file to the zip archive while computing the hash and length @@ -569,7 +586,9 @@ def get_new_requirement_line(reqs_text, extra): else: return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {req.marker}" else: - return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {extra}".strip(" ;") + return f"Requires-Dist: {req.name}{req_extra_deps}{req.specifier}; {extra}".strip( + " ;" + ) for meta_line in metadata.splitlines(): if not meta_line.startswith("Requires-Dist: "):