Repository: bazelbuild/rules_python (language: python)

Issue:
Heads up: `setuptools` contain files with whitespaces in their names
Hello, I would like to thank the authors, maintainers and all contributors for making such a cool project 😄 

The reason why I was brought here is that recently we received a issue in the setuptools repository (https://github.com/pypa/setuptools/issues/3072) that seems related to the fact that `bazel` does not support whitespaces in file names, and this might generate problems for people using `rules_python`.

As noted [in of the issue's comment](https://github.com/pypa/setuptools/issues/3072#issuecomment-1028474077), even before the recent changes, `setuptools` already had files containing whitespaces.

My main objective here is just to give you guys a heads up, so you are aware about potential problems that might arise. (I understand that it might be a very complicated problem to solve and that it will take a long time...)

For the time being `setuptools`' lead developer have kindly provided a patch with a workaround, but it is difficult to guarantee that in the future other files containing whitespace in their names will not be added (specially via vendored dependencies).




Discussion / hints:
Thanks for reaching out @abravalheri. In rules python we ignore files from installed packages that have spaces in them, which is what caused the runtime error with the Lorem file. We are aware of the issue but there ins't much we can do without upstream support for input files with spaces in bazel https://github.com/bazelbuild/bazel/issues/4327
> Thanks for reaching out @abravalheri. In rules python we ignore files from installed packages that have spaces in them, which is what caused the runtime error with the Lorem file. We are aware of the issue but there ins't much we can do without upstream support for input files with spaces in bazel [bazelbuild/bazel#4327](https://github.com/bazelbuild/bazel/issues/4327)

@hrfuller out of curiosity, where does rules_python ignore files with spaces in them. I'm noticing an issue where there's a directory of an installed package with spaces in it and that is causing a failure.

Actually, I found it here: https://github.com/bazelbuild/rules_python/blob/0aa5ea48cbecd8d73d62f77abac6b2f4aafbf64e/python/repositories.bzl#L209

Seems like we'd want to glob exclude directories as well
Cross referencing https://github.com/pypa/setuptools/issues/4605.

As previously noted the workaround introduced by setuptools was not bulletproof and there was no guarantee that similar problems would not happen again due the limitation in `bazel` itself (it is not really a bug in setuptools codebase).
Is there any workaround we can apply to be able to upgrade setuptools?

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a1a2edc13..c43fb91e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,8 @@ A brief description of the categories of changes:
 * (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 c41380c6b..d36ec9fa0 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 d3b9bf4aa..558966a8d 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -179,6 +179,12 @@ bzl_library(
     srcs = ["full_version.bzl"],
 )
 
+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"],
diff --git a/python/private/glob_excludes.bzl b/python/private/glob_excludes.bzl
new file mode 100644
index 000000000..c98afe0ae
--- /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 4b5a3c681..cf9a5a6b1 100644
--- a/python/private/hermetic_runtime_repo_setup.bzl
+++ b/python/private/hermetic_runtime_repo_setup.bzl
@@ -17,6 +17,7 @@ load("@rules_cc//cc:defs.bzl", "cc_import", "cc_library")
 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 @@ def define_hermetic_runtime_toolchain_impl(
             # 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 @@ def define_hermetic_runtime_toolchain_impl(
                 "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 9be355c0c..20afe7077 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -86,6 +86,7 @@ bzl_library(
     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 e07d9aa8d..8949ed4ab 100644
--- a/python/private/pypi/deps.bzl
+++ b/python/private/pypi/deps.bzl
@@ -101,6 +101,7 @@ _GENERIC_WHEEL = """\
 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_library(
         "**/*.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 1798b9d77..a303bdcd9 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 @@ def whl_library_targets(
 
     if hasattr(rules, "py_library"):
         _data_exclude = [
-            "**/* *",
             "**/*.py",
             "**/*.pyc",
             "**/*.pyc.*",  # During pyc creation, temp files named *.pyc.NNNN are created
@@ -230,7 +230,7 @@ def whl_library_targets(
             # 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 033920d3b..33261befa 100644
--- a/python/private/util.bzl
+++ b/python/private/util.bzl
@@ -99,6 +99,8 @@ def define_bazel_6_provider(doc, fields, **kwargs):
         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


Test patch:
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 9694eeec4..e69eb0f0e 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 @@ def _test_whl_and_library_deps(env):
             ),
             "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 @@ def _test_group(env):
             "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({