Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion python/private/py_wheel.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions tests/tools/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"],
)
38 changes: 38 additions & 0 deletions tests/tools/wheelmaker_test.py
Original file line number Diff line number Diff line change
@@ -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()
47 changes: 33 additions & 14 deletions tools/wheelmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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: "):
Expand Down