fix: allow spaces in whl_librarys (#2334)

Fixes #617.

Modern `setuptools` versions contain files critical to setuptools
functionality loaded proactively on module load that contain spaces.
Bazel 7.4.0+ now supports files with spaces in their names.

---------

Co-authored-by: Richard Levasseur <richardlev@gmail.com>
Co-authored-by: Richard Levasseur <rlevasseur@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e85cfb4..028f0ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,8 @@
 * (bzlmod) Generate `config_setting` values for all available toolchains instead
   of only the registered toolchains, which restores the previous behaviour that
   `bzlmod` users would have observed.
+* (pypi) (Bazel 7.4+) Allow spaces in filenames included in `whl_library`s
+  ([617](https://github.com/bazelbuild/rules_python/issues/617)).
 
 {#v0-0-0-added}
 ### Added
diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock
index c41380c..d36ec9f 100644
--- a/examples/bzlmod/MODULE.bazel.lock
+++ b/examples/bzlmod/MODULE.bazel.lock
@@ -1392,13 +1392,13 @@
     },
     "@@rules_python~//python/extensions:pip.bzl%pip": {
       "general": {
-        "bzlTransitiveDigest": "g9NnJTZcM2BjPelxHHLy0ZyhFd+8XAb86u9OvNIOhFo=",
+        "bzlTransitiveDigest": "ovGr2x1QDHBffYRqtz5fRgBCvBIG9xO/6Lk2UEnqj48=",
         "usagesDigest": "MChlcSw99EuW3K7OOoMcXQIdcJnEh6YmfyjJm+9mxIg=",
         "recordedFileInputs": {
           "@@other_module~//requirements_lock_3_11.txt": "a7d0061366569043d5efcf80e34a32c732679367cb3c831c4cdc606adc36d314",
           "@@rules_python~//python/private/pypi/whl_installer/platform.py": "b944b908b25a2f97d6d9f491504ad5d2507402d7e37c802ee878783f87f2aa11",
           "@@//requirements_lock_3_10.txt": "5e7083982a7e60f34998579a0ae83b520d46ab8f2552cc51337217f024e6def5",
-          "@@rules_python~~internal_deps~pypi__packaging//BUILD.bazel": "8d36246aeefaab4b26fb9c1175cfaf13df5b6f1587e6753f1e78b132bad74795",
+          "@@rules_python~~internal_deps~pypi__packaging//BUILD.bazel": "16cf02cdc6cd989d8a92b551d406abea3fe597b1524ba5fa88f0410010671d7f",
           "@@//whl_mods/appended_build_content.BUILD": "87745b00382c66e5efbd7cb44a08fc3edbf7fd5099cf593f87599188f1557a9e",
           "@@//requirements_lock_3_9.txt": "6a4990586366467d1e7d56d9f2ec9bafdd7e17fb29dc959aa5a6b0395c22eac7",
           "@@rules_python~~internal_deps~pypi__packaging//packaging-24.0.dist-info/RECORD": "be1aea790359b4c2c9ea83d153c1a57c407742a35b95ee36d00723509f5ed5dd",
@@ -6299,7 +6299,7 @@
     },
     "@@rules_python~//python/private/pypi:pip.bzl%pip_internal": {
       "general": {
-        "bzlTransitiveDigest": "ctc7nzMsQfNG16wSXLqbix2k99rf614qJRwcd/2RxGI=",
+        "bzlTransitiveDigest": "OP8sZohIIGi+NNlfo7dAnU3yGo3Ea4xAU6TgzbPbCgw=",
         "usagesDigest": "LYtSAPzhPjmfD9vF39mCED1UQSvHEo2Hv+aK5Z4ZWWc=",
         "recordedFileInputs": {
           "@@rules_python~//tools/publish/requirements_linux.txt": "8175b4c8df50ae2f22d1706961884beeb54e7da27bd2447018314a175981997d",
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 7741e55..7399b10 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -174,6 +174,12 @@
 )
 
 bzl_library(
+    name = "glob_excludes_bzl",
+    srcs = ["glob_excludes.bzl"],
+    deps = [":util_bzl"],
+)
+
+bzl_library(
     name = "internal_config_repo_bzl",
     srcs = ["internal_config_repo.bzl"],
     deps = [":bzlmod_enabled_bzl"],
diff --git a/python/private/glob_excludes.bzl b/python/private/glob_excludes.bzl
new file mode 100644
index 0000000..c98afe0
--- /dev/null
+++ b/python/private/glob_excludes.bzl
@@ -0,0 +1,32 @@
+# Copyright 2024 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.
+
+"Utilities for glob exclusions."
+
+load(":util.bzl", "IS_BAZEL_7_4_OR_HIGHER")
+
+def _version_dependent_exclusions():
+    """Returns glob exclusions that are sensitive to Bazel version.
+
+    Returns:
+        a list of glob exclusion patterns
+    """
+    if IS_BAZEL_7_4_OR_HIGHER:
+        return []
+    else:
+        return ["**/* *"]
+
+glob_excludes = struct(
+    version_dependent_exclusions = _version_dependent_exclusions,
+)
diff --git a/python/private/hermetic_runtime_repo_setup.bzl b/python/private/hermetic_runtime_repo_setup.bzl
index 4b5a3c6..cf9a5a6 100644
--- a/python/private/hermetic_runtime_repo_setup.bzl
+++ b/python/private/hermetic_runtime_repo_setup.bzl
@@ -17,6 +17,7 @@
 load("//python:py_runtime.bzl", "py_runtime")
 load("//python:py_runtime_pair.bzl", "py_runtime_pair")
 load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain")
+load(":glob_excludes.bzl", "glob_excludes")
 load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain")
 load(":semver.bzl", "semver")
 
@@ -64,7 +65,6 @@
             # Platform-agnostic filegroup can't match on all patterns.
             allow_empty = True,
             exclude = [
-                "**/* *",  # Bazel does not support spaces in file names.
                 # Unused shared libraries. `python` executable and the `:libpython` target
                 # depend on `libpython{python_version}.so.1.0`.
                 "lib/libpython{major}.{minor}.so".format(**version_dict),
@@ -74,7 +74,7 @@
                 "lib/python{major}.{minor}/**/test/**".format(**version_dict),
                 "lib/python{major}.{minor}/**/tests/**".format(**version_dict),
                 "**/__pycache__/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNN are created
-            ] + extra_files_glob_exclude,
+            ] + glob_excludes.version_dependent_exclusions() + extra_files_glob_exclude,
         ),
     )
     cc_import(
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index 9be355c..20afe70 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -86,6 +86,7 @@
     srcs = ["deps.bzl"],
     deps = [
         "//python/private:bazel_tools_bzl",
+        "//python/private:glob_excludes_bzl",
     ],
 )
 
diff --git a/python/private/pypi/deps.bzl b/python/private/pypi/deps.bzl
index e07d9aa..8949ed4 100644
--- a/python/private/pypi/deps.bzl
+++ b/python/private/pypi/deps.bzl
@@ -101,6 +101,7 @@
 package(default_visibility = ["//visibility:public"])
 
 load("@rules_python//python:defs.bzl", "py_library")
+load("@rules_python//python/private:glob_excludes.bzl", "glob_excludes")
 
 py_library(
     name = "lib",
@@ -111,11 +112,10 @@
         "**/*.py",
         "**/*.pyc",
         "**/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNN are created
-        "**/* *",
         "**/*.dist-info/RECORD",
         "BUILD",
         "WORKSPACE",
-    ]),
+    ] + glob_excludes.version_dependent_exclusions()),
     # This makes this directory a top-level in the python import
     # search path for anything that depends on this.
     imports = ["."],
diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl
index 1798b9d..a303bdc 100644
--- a/python/private/pypi/whl_library_targets.bzl
+++ b/python/private/pypi/whl_library_targets.bzl
@@ -17,6 +17,7 @@
 load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
 load("//python:py_binary.bzl", "py_binary")
 load("//python:py_library.bzl", "py_library")
+load("//python/private:glob_excludes.bzl", "glob_excludes")
 load("//python/private:normalize_name.bzl", "normalize_name")
 load(
     ":labels.bzl",
@@ -222,7 +223,6 @@
 
     if hasattr(rules, "py_library"):
         _data_exclude = [
-            "**/* *",
             "**/*.py",
             "**/*.pyc",
             "**/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNNN are created
@@ -230,7 +230,7 @@
             # of generated files produced when wheels are installed. The file is ignored to avoid
             # Bazel caching issues.
             "**/*.dist-info/RECORD",
-        ]
+        ] + glob_excludes.version_dependent_exclusions()
         for item in data_exclude:
             if item not in _data_exclude:
                 _data_exclude.append(item)
diff --git a/python/private/util.bzl b/python/private/util.bzl
index 033920d..33261be 100644
--- a/python/private/util.bzl
+++ b/python/private/util.bzl
@@ -99,6 +99,8 @@
         return provider("Stub, not used", fields = []), None
     return provider(doc = doc, fields = fields, **kwargs)
 
+IS_BAZEL_7_4_OR_HIGHER = hasattr(native, "legacy_globals")
+
 IS_BAZEL_7_OR_HIGHER = hasattr(native, "starlark_doc_extract")
 
 # Bazel 5.4 has a bug where every access of testing.ExecutionInfo is a
diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl
index 9694eee..e69eb0f 100644
--- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl
+++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl
@@ -15,6 +15,7 @@
 ""
 
 load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("//python/private:glob_excludes.bzl", "glob_excludes")  # buildifier: disable=bzl-visibility
 load("//python/private/pypi:whl_library_targets.bzl", "whl_library_targets")  # buildifier: disable=bzl-visibility
 
 _tests = []
@@ -246,7 +247,12 @@
             ),
             "data": [] + _glob(
                 ["site-packages/**/*"],
-                exclude = ["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
+                exclude = [
+                    "**/*.py",
+                    "**/*.pyc",
+                    "**/*.pyc.*",
+                    "**/*.dist-info/RECORD",
+                ] + glob_excludes.version_dependent_exclusions(),
             ),
             "imports": ["site-packages"],
             "deps": [
@@ -312,7 +318,12 @@
             "srcs": _glob(["site-packages/**/*.py"], exclude = [], allow_empty = True),
             "data": [] + _glob(
                 ["site-packages/**/*"],
-                exclude = ["**/* *", "**/*.py", "**/*.pyc", "**/*.pyc.*", "**/*.dist-info/RECORD"],
+                exclude = [
+                    "**/*.py",
+                    "**/*.pyc",
+                    "**/*.pyc.*",
+                    "**/*.dist-info/RECORD",
+                ] + glob_excludes.version_dependent_exclusions(),
             ),
             "imports": ["site-packages"],
             "deps": ["@pypi_bar_baz//:pkg"] + _select({