From cec5210a19e5c3f4073fba21ae1f272124d303b7 Mon Sep 17 00:00:00 2001 From: Alex Trotta Date: Wed, 3 Sep 2025 00:54:27 -0400 Subject: [PATCH 1/5] Allow files in wheels to be installed to directories When specifying `data_files` in `py_wheel`, allow just the directory to be specified (with a trailing slash), in which case it will use the file's name. This avoids duplicating (potentially platform-specific) names. --- python/private/py_wheel.bzl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index 1d98d21a65..25b0645004 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -520,10 +520,13 @@ def _py_wheel_impl(ctx): filename, ), ) + + final_filename = filename + target_files[0].basename if filename.endswith("/") else filename + other_inputs.extend(target_files) args.add( "--data_files", - filename + ";" + target_files[0].path, + final_filename + ";" + target_files[0].path, ) ctx.actions.run( From 5683ee99d5ed55f5562aeb4ad2fc46e2c9155307 Mon Sep 17 00:00:00 2001 From: Ahajha Date: Wed, 19 Nov 2025 03:09:07 -0500 Subject: [PATCH 2/5] Allow files or groups of files to be installed to directories --- python/private/py_wheel.bzl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index 25b0645004..a0f553355a 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -506,9 +506,9 @@ def _py_wheel_impl(ctx): for target, filename in ctx.attr.data_files.items(): target_files = target[DefaultInfo].files.to_list() - if len(target_files) != 1: + if len(target_files) != 1 and not filename.endswith("/"): fail( - "Multi-file target listed in data_files %s", + "Multi-file target listed in data_files %s, this is only supported when specifying a folder path (i.e. a path ending in '/')", filename, ) @@ -521,13 +521,14 @@ def _py_wheel_impl(ctx): ), ) - final_filename = filename + target_files[0].basename if filename.endswith("/") else filename + for file in target_files: + final_filename = filename + file.basename if filename.endswith("/") else filename - other_inputs.extend(target_files) - args.add( - "--data_files", - final_filename + ";" + target_files[0].path, - ) + other_inputs.extend(target_files) + args.add( + "--data_files", + final_filename + ";" + file.path, + ) ctx.actions.run( mnemonic = "PyWheel", From 9858340ee7e3b91c89aa6d5bb5c2b9fe708151b5 Mon Sep 17 00:00:00 2001 From: Alex Trotta Date: Sat, 14 Mar 2026 23:21:10 -0400 Subject: [PATCH 3/5] Docs, tests, changelog --- CHANGELOG.md | 2 ++ examples/wheel/BUILD.bazel | 24 ++++++++++++++++++++++++ examples/wheel/wheel_test.py | 19 +++++++++++++++++++ python/private/py_wheel.bzl | 19 +++++++++++++++++-- 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d908992c9c..e5748704b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,8 @@ Other changes: {obj}`experimental_index_url` which should speed up consecutive initializations and should no longer require the network access if the cache is hydrated. Implements [#2731](https://github.com/bazel-contrib/rules_python/issues/2731). +* (wheel) Specifying a path ending in `/` as a destination in `data_files` + will now install file(s) to a folder, preserving their basename. {#v1-9-0} ## [1.9.0] - 2026-02-21 diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index e52e0fc3a3..3cf6e9f350 100644 --- a/examples/wheel/BUILD.bazel +++ b/examples/wheel/BUILD.bazel @@ -401,6 +401,29 @@ py_wheel( version = "0.0.1", ) +filegroup( + name = "data_files_test_group", + # Re-using some files already checked into the repo. + srcs = [ + "README.md", + "//examples/wheel:NOTICE", + ], +) + +py_wheel( + name = "data_files_installed_in_folder", + testonly = True, # Set this to verify the generated .dist target doesn't break things + # Re-using some files already checked into the repo. + data_files = { + # Single file + "//examples/wheel:NOTICE": "scripts/", + # Filegroup + ":data_files_test_group": "data/", + }, + distribution = "data_files_installed_in_folder", + version = "0.0.1", +) + py_test( name = "wheel_test", srcs = ["wheel_test.py"], @@ -409,6 +432,7 @@ py_test( ":custom_package_root_multi_prefix", ":custom_package_root_multi_prefix_reverse_order", ":customized", + ":data_files_installed_in_folder", ":empty_requires_files", ":extra_requires", ":filename_escaping", diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 7f19ecd9f9..9ed2b842e5 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -615,6 +615,25 @@ def test_requires_dist_depends_on_extras_file(self): requires, ) + def test_data_files_installed_in_folder(self): + filename = self._get_path( + "data_files_installed_in_folder-0.0.1-py3-none-any.whl" + ) + + with zipfile.ZipFile(filename) as zf: + self.assertAllEntriesHasReproducibleMetadata(zf) + self.assertEqual( + zf.namelist(), + [ + "data_files_installed_in_folder-0.0.1.dist-info/WHEEL", + "data_files_installed_in_folder-0.0.1.dist-info/METADATA", + "data_files_installed_in_folder-0.0.1.data/data/NOTICE", + "data_files_installed_in_folder-0.0.1.data/data/README.md", + "data_files_installed_in_folder-0.0.1.data/scripts/NOTICE", + "data_files_installed_in_folder-0.0.1.dist-info/RECORD", + ], + ) + if __name__ == "__main__": unittest.main() diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index a0f553355a..0d0478db2e 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -182,8 +182,23 @@ _other_attrs = { doc = "A list of strings describing the categories for the package. For valid classifiers see https://pypi.org/classifiers", ), "data_files": attr.label_keyed_string_dict( - doc = ("Any file that is not normally installed inside site-packages goes into the .data directory, named " + - "as the .dist-info directory but with the .data/ extension. Allowed paths: {prefixes}".format(prefixes = ALLOWED_DATA_FILE_PREFIX)), + doc = (""" +Any file that is not normally installed inside site-packages goes into the .data directory, named +as the .dist-info directory but with the .data/ extension. If the destination of a file or group of files ends +in a `/`, the destination is a folder and files are placed with their existing basenames under that folder. + +For example: +` +":file1.txt": "data/file1.txt", # Destination: .data/data/file1.txt +":file1.txt": "data/", # Destination: .data/data/file1.txt +":file1.txt": "data/special.txt", # Destination: .data/data/special.txt + +filegroup(name = "files", srcs = [":file1.txt", ":file2.txt"]) +":files": "data/", # Destinations: .data/data/file1.txt, .data/data/file2.txt +` + +Allowed paths: {prefixes} +""".format(prefixes = ALLOWED_DATA_FILE_PREFIX)), allow_files = True, ), "description_content_type": attr.string( From 04401f4f4e129d2a8f8c17e4992e761e5951b1fc Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 14 Mar 2026 20:39:25 -0700 Subject: [PATCH 4/5] add versionchanged doc, copyedit attribte doc --- python/private/py_wheel.bzl | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index 0d0478db2e..cbc1ceb419 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -183,21 +183,33 @@ _other_attrs = { ), "data_files": attr.label_keyed_string_dict( doc = (""" -Any file that is not normally installed inside site-packages goes into the .data directory, named -as the .dist-info directory but with the .data/ extension. If the destination of a file or group of files ends -in a `/`, the destination is a folder and files are placed with their existing basenames under that folder. +Mapping of data files to go into the wheel. + +The keys are targets of files to include, and the values are the `.data`-relative +path to use. + +Any file that is not normally installed inside site-packages goes into the .data +directory, named as the .dist-info directory but with the .data/ extension. If +the destination of a file or group of files ends in a `/`, the destination is a +folder and files are placed with their existing basenames under that folder. For example: -` + +``` ":file1.txt": "data/file1.txt", # Destination: .data/data/file1.txt ":file1.txt": "data/", # Destination: .data/data/file1.txt ":file1.txt": "data/special.txt", # Destination: .data/data/special.txt filegroup(name = "files", srcs = [":file1.txt", ":file2.txt"]) ":files": "data/", # Destinations: .data/data/file1.txt, .data/data/file2.txt -` +``` Allowed paths: {prefixes} + +:::{versionchanged} VERSION_NEXT_FEATURE +Values can end in slash (`/`) to indicate that all files of the target should +be moved under that directory. +::: """.format(prefixes = ALLOWED_DATA_FILE_PREFIX)), allow_files = True, ), From 82b1faa6e3e0bc3f540e0a7b8dd03be81e32a489 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 14 Mar 2026 20:41:31 -0700 Subject: [PATCH 5/5] fix escaping of directive --- python/private/py_wheel.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index cbc1ceb419..e6a9925a15 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -206,7 +206,7 @@ filegroup(name = "files", srcs = [":file1.txt", ":file2.txt"]) Allowed paths: {prefixes} -:::{versionchanged} VERSION_NEXT_FEATURE +:::{{versionchanged}} VERSION_NEXT_FEATURE Values can end in slash (`/`) to indicate that all files of the target should be moved under that directory. :::