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 1d98d21a65..e6a9925a15 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -182,8 +182,35 @@ _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 = (""" +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, ), "description_content_type": attr.string( @@ -506,9 +533,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, ) @@ -520,11 +547,15 @@ def _py_wheel_impl(ctx): filename, ), ) - other_inputs.extend(target_files) - args.add( - "--data_files", - filename + ";" + target_files[0].path, - ) + + 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 + ";" + file.path, + ) ctx.actions.run( mnemonic = "PyWheel",