Repository: pypa/setuptools

Issue:
[BUG] plat_name attribute is None with setuptools 64.0.1 and editable install
### setuptools version

64.0.1

### Python version

3.10.6

### OS

macOS

### Additional environment information

_No response_

### Description

While setting up a local virtual environment with `pip install -e .` I received an error I had not previously seen.

### Expected behavior

The package installs properly.

### How to Reproduce

1. `git clone https://github.com/pyca/cryptography`
2. `cd cryptography`
3. `python3 -m venv .venv`
4. `source .venv/bin/activate`
5. `CRYPTOGRAPHY_SUPPRESS_LINK_FLAGS='1' LDFLAGS='/opt/homebrew/opt/openssl@3/lib/libcrypto.a /opt/homebrew/opt/openssl@3/lib/libssl.a' CFLAGS='-I/opt/homebrew/opt/openssl@3/include' pip install -r dev-requirements.txt` (environment variables are specific to macOS)


### Output

```console
[...]
Building wheels for collected packages: cryptography
  Building editable for cryptography (pyproject.toml) ... error
  error: subprocess-exited-with-error
  
  × Building editable for cryptography (pyproject.toml) did not run successfully.
  │ exit code: 1
  ╰─> [66 lines of output]
      running editable_wheel
      creating /private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography.egg-info
      writing /private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography.egg-info/PKG-INFO
      writing dependency_links to /private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography.egg-info/dependency_links.txt
      writing requirements to /private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography.egg-info/requires.txt
      writing top-level names to /private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography.egg-info/top_level.txt
      writing manifest file '/private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography.egg-info/SOURCES.txt'
      reading manifest file '/private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography.egg-info/SOURCES.txt'
      reading manifest template 'MANIFEST.in'
      warning: no previously-included files found matching 'vectors'
      warning: no previously-included files matching '*' found under directory 'vectors'
      warning: no previously-included files matching '*' found under directory '.github'
      warning: no previously-included files found matching 'release.py'
      warning: no previously-included files found matching '.readthedocs.yml'
      warning: no previously-included files found matching 'dev-requirements.txt'
      warning: no previously-included files found matching 'tox.ini'
      warning: no previously-included files found matching 'mypy.ini'
      warning: no previously-included files matching '*' found under directory '.circleci'
      adding license file 'LICENSE'
      adding license file 'LICENSE.APACHE'
      adding license file 'LICENSE.BSD'
      adding license file 'LICENSE.PSF'
      writing manifest file '/private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography.egg-info/SOURCES.txt'
      creating '/private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-wheel-mpo_h63u/tmp2d0kyx1o/cryptography-38.0.0.dev1.dist-info'
      adding license file "LICENSE" (matched pattern "LICEN[CS]E*")
      adding license file "LICENSE.PSF" (matched pattern "LICEN[CS]E*")
      adding license file "LICENSE.APACHE" (matched pattern "LICEN[CS]E*")
      adding license file "LICENSE.BSD" (matched pattern "LICEN[CS]E*")
      Traceback (most recent call last):
        File "/private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-build-env-k9m_30lt/overlay/lib/python3.10/site-packages/setuptools/command/editable_wheel.py", line 136, in run
          bdist_wheel.write_wheelfile(self.dist_info_dir)
        File "/private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-build-env-k9m_30lt/overlay/lib/python3.10/site-packages/wheel/bdist_wheel.py", line 389, in write_wheelfile
          impl_tag, abi_tag, plat_tag = self.get_tag()
        File "/private/var/folders/tc/9_n6gvsx251b2j70g7bny1lm0000gn/T/pip-build-env-k9m_30lt/overlay/lib/python3.10/site-packages/setuptools_rust/setuptools_ext.py", line 254, in get_tag
          if self.plat_name.startswith("macosx-") and arch_flags:
      AttributeError: 'NoneType' object has no attribute 'startswith'
      
          =============================DEBUG ASSISTANCE=============================
          If you are seeing a compilation error please try the following steps to
          successfully install cryptography:
          1) Upgrade to the latest pip and try again. This will fix errors for most
             users. See: https://pip.pypa.io/en/stable/installing/#upgrading-pip
          2) Read https://cryptography.io/en/latest/installation/ for specific
             instructions for your platform.
          3) Check our frequently asked questions for more information:
             https://cryptography.io/en/latest/faq/
          4) Ensure you have a recent Rust toolchain installed:
             https://cryptography.io/en/latest/installation/#rust
      
          Python: 3.10.6
          platform: macOS-12.5-arm64-arm-64bit
          pip: n/a
          setuptools: 64.0.1
          setuptools_rust: 1.5.0
          rustc: 1.63.0 (4b91a6ea7 2022-08-08)
          =============================DEBUG ASSISTANCE=============================
      
      error: Support for editable installs via PEP 660 was recently introduced
      in `setuptools`. If you are seeing this error, please report to:
      
      https://github.com/pypa/setuptools/issues
      
      Meanwhile you can try the legacy behavior by setting an
      environment variable and trying to install again:
      
      SETUPTOOLS_ENABLE_FEATURES="legacy-editable"
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building editable for cryptography
Failed to build cryptography
ERROR: Could not build wheels for cryptography, which is required to install pyproject.toml-based projects

```



Discussion:

Resolving patch:
diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py
--- a/setuptools/command/build_py.py
+++ b/setuptools/command/build_py.py
@@ -184,7 +184,7 @@ def analyze_manifest(self):
             files = ei_cmd.filelist.files
 
         check = _IncludePackageDataAbuse()
-        for path in _filter_absolute_egg_info(files, egg_info_dir):
+        for path in self._filter_build_files(files, egg_info_dir):
             d, f = os.path.split(assert_relative(path))
             prev = None
             oldf = f
@@ -202,6 +202,25 @@ def analyze_manifest(self):
                         check.warn(importable)
                 mf.setdefault(src_dirs[d], []).append(path)
 
+    def _filter_build_files(self, files: Iterable[str], egg_info: str) -> Iterator[str]:
+        """
+        ``build_meta`` may try to create egg_info outside of the project directory,
+        and this can be problematic for certain plugins (reported in issue #3500).
+
+        Extensions might also include between their sources files created on the
+        ``build_lib`` and ``build_temp`` directories.
+
+        This function should filter this case of invalid files out.
+        """
+        build = self.get_finalized_command("build")
+        build_dirs = (egg_info, self.build_lib, build.build_temp, build.build_base)
+        norm_dirs = [os.path.normpath(p) for p in build_dirs if p]
+
+        for file in files:
+            norm_path = os.path.normpath(file)
+            if not os.path.isabs(file) or all(d not in norm_path for d in norm_dirs):
+                yield file
+
     def get_data_files(self):
         pass  # Lazily compute data files in _get_data_files() function.
 
@@ -347,15 +366,3 @@ def warn(self, importable):
             msg = textwrap.dedent(self.MESSAGE).format(importable=importable)
             warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
             self._already_warned.add(importable)
-
-
-def _filter_absolute_egg_info(files: Iterable[str], egg_info: str) -> Iterator[str]:
-    """
-    ``build_meta`` may try to create egg_info outside of the project directory,
-    and this can be problematic for certain plugins (reported in issue #3500).
-    This function should filter this case of invalid files out.
-    """
-    egg_info_name = Path(egg_info).name
-    for file in files:
-        if not (egg_info_name in file and os.path.isabs(file)):
-            yield file
diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py
--- a/setuptools/command/editable_wheel.py
+++ b/setuptools/command/editable_wheel.py
@@ -133,7 +133,8 @@ def run(self):
             self._ensure_dist_info()
 
             # Add missing dist_info files
-            bdist_wheel = self.reinitialize_command("bdist_wheel")
+            self.reinitialize_command("bdist_wheel")
+            bdist_wheel = self.get_finalized_command("bdist_wheel")
             bdist_wheel.write_wheelfile(self.dist_info_dir)
 
             self._create_wheel_file(bdist_wheel)
@@ -156,7 +157,7 @@ def _ensure_dist_info(self):
         if self.dist_info_dir is None:
             dist_info = self.reinitialize_command("dist_info")
             dist_info.output_dir = self.dist_dir
-            dist_info.finalize_options()
+            dist_info.ensure_finalized()
             dist_info.run()
             self.dist_info_dir = dist_info.dist_info_dir
         else:
@@ -278,7 +279,7 @@ def _run_build_subcommands(self):
         #       Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.
         build: Command = self.get_finalized_command("build")
         for name in build.get_sub_commands():
-            cmd = self.distribution.get_command_obj(name)
+            cmd = self.get_finalized_command(name)
             if name == "build_py" and type(cmd) != build_py_cls:
                 self._safely_run(name)
             else:


Test patch:
diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
--- a/setuptools/tests/test_editable_install.py
+++ b/setuptools/tests/test_editable_install.py
@@ -25,6 +25,7 @@
     _find_namespaces,
     _find_package_roots,
     _finder_template,
+    editable_wheel,
 )
 from setuptools.dist import Distribution
 
@@ -846,6 +847,36 @@ def test_safeguarded_from_errors(self, tmp_path, venv):
         assert b"42" in out
 
 
+class TestCustomBuildWheel:
+    def install_custom_build_wheel(self, dist):
+        bdist_wheel_cls = dist.get_command_class("bdist_wheel")
+
+        class MyBdistWheel(bdist_wheel_cls):
+            def get_tag(self):
+                # In issue #3513, we can see that some extensions may try to access
+                # the `plat_name` property in bdist_wheel
+                if self.plat_name.startswith("macosx-"):
+                    _ = "macOS platform"
+                return super().get_tag()
+
+        dist.cmdclass["bdist_wheel"] = MyBdistWheel
+
+    def test_access_plat_name(self, tmpdir_cwd):
+        # Even when a custom bdist_wheel tries to access plat_name the build should
+        # be successful
+        jaraco.path.build({"module.py": "x = 42"})
+        dist = Distribution()
+        dist.script_name = "setup.py"
+        dist.set_defaults()
+        self.install_custom_build_wheel(dist)
+        cmd = editable_wheel(dist)
+        cmd.ensure_finalized()
+        cmd.run()
+        wheel_file = str(next(Path().glob('dist/*')))
+        assert "editable" in wheel_file
+        assert wheel_file.endswith(".whl")
+
+
 def install_project(name, venv, tmp_path, files, *opts):
     project = tmp_path / name
     project.mkdir()