Repository: pypa/pip

Issue:
pip-19.x: `pip wheel` does not pick a whl on PEP-517 project
**Environment**
* pip version: 19.0, 19.0.1
* Python version: 3.6.6, 3.4.3
* OS: ubuntu, macosx

**Description**
It seems that pep517's build_wheel passes existing distfiles.
However, pip uses `os.listdir()`'s first item.
https://github.com/pypa/pip/blob/master/src/pip/_internal/wheel.py#L751

When `os.listdir(temp_dir.path)` returns `['somepkg-1.0.tar.bz2', 'somepkg-1.0-py2.py3-none-any.whl']`, `somepkg-1.0.tar.bz2` is copied to wheelhouse directory.

**How to Reproduce**
1. create a project with pyproject.toml
2. run `python setup.py sdist`
3. run `pip wheel -f wheelhouse -w wheelhouse .`

**Workarounds**
- clean `dist` directory before run `pip wheel`
- use `pip wheel --no-use-pep517`
- use pip-18.1



Discussion:
a similar case is reproduced in `pip install`:
1. create a project with pyproject.toml
2. run `python setup.py sdist`
3. create venv and cd to it. (`python3 -m venv /path/to/env && cd /path/to/env && bin/pip install -U pip`)
4. `bin/pip install /path/to/project`

Stacktrace
```
Exception:
Traceback (most recent call last):
  File "/PATH/TO/VENV/lib/python3.6/site-packages/pip/_internal/cli/base_command.py", line 176, in main
    status = self.run(options, args)
  File "/PATH/TO/VENV/lib/python3.6/site-packages/pip/_internal/commands/install.py", line 346, in run
    session=session, autobuilding=True
  File "//PATH/TO/VENV/lib/python3.6/site-packages/pip/_internal/wheel.py", line 941, in build
    assert req.link.is_wheel
AssertionError
```

Hi @gjo! Thanks for filing this!

```sh-session
$ python3 -m venv /tmp/pip-6196-tester
$ cd /tmp/pip-6196-tester
$ bin/pip install -U pip
$ mkpkg trial
$ touch trial/pyproject.toml
$ cd trial
$ python setup.py sdist
$ ../bin/pip wheel -f wheelhouse -w wheelhouse .
Looking in links: wheelhouse
Processing /private/tmp/pip-6196-tester/trial
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Building wheels for collected packages: trial
  Building wheel for trial (PEP 517) ... done
  Stored in directory: /private/tmp/pip-6196-tester/trial/wheelhouse
Successfully built trial
$ tree wheelhouse
wheelhouse
└── trial-0.1.0.tar.gz

0 directories, 1 file
```

So... I can reproduce this issue! Thanks for a detailed report! :)

---

The install case seems to be working fine for me. I tried this locally:

```sh-session
$ python3 -m venv /tmp/pip-6196-tester
$ cd /tmp/pip-6196-tester
$ bin/pip install -U pip --quiet
$ mkpkg trial  # https://github.com/pradyunsg/dotfiles/blob/master/src/python/aliases.zsh
$ touch trial/pyproject.toml
$ tree trial
trial
├── setup.py
├── pyproject.toml
└── trial
    └── __init__.py

1 directory, 3 files
$ bin/pip install ./trial
Processing ./trial
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Building wheels for collected packages: trial
  Building wheel for trial (PEP 517) ... done
  Stored in directory: /private/var/folders/4d/bt0_xfx56bjfmmt2bv3r5_qh0000gn/T/pip-ephem-wheel-cache-qo7imeut/wheels/d9/26/8a/63a8ca08ceb31ad86bf93bd6f9913bf64a86363730b6359887
Successfully built trial
Installing collected packages: trial
Successfully installed trial-0.1.0
```

Okay, so PEP 517 backends return a basename. I'll make changes so that pip will use _that_ backend name itself, when using PEP 517 to building wheels using it.
And... I have trouble reproducing this in the test suite. Turns out `os.listdir` order is arbitrary and thus this issue doesn't surface in a deterministic manner for me. Making a PR without the test for this until then.

@cjerdonek @pfmoore Any suggestions for how to test this?

<details>

<summary>My latest attempt at writing the test</summary>

```py
def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data):
    """Check correct wheels are copied. (#6196)
    """
    pkg_to_wheel = data.src / 'withpyproject'
    (pkg_to_wheel / 'dist').makedirs()
    (pkg_to_wheel / 'dist' / 'withpyproject-0.0.1.tar.gz').write("hello")

    result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path)
    assert "Installing build dependencies" in result.stdout, result.stdout

    wheel_file_name = 'withpyproject-0.0.1-py%s-none-any.whl' % pyversion[0]
    wheel_file_path = script.scratch_path / wheel_file_name
    assert wheel_file_path in result.files_created, result.stdout
```

</details>
For starters, I would remove the indeterminism from pip’s code base by making it sorted(os.listdir(...)) and then go from there.
Cool. I'll do that then. :)

Resolving patch:
diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py
--- a/src/pip/_internal/wheel.py
+++ b/src/pip/_internal/wheel.py
@@ -818,15 +818,14 @@ def _build_one_inside_env(self, req, output_dir, python_tag=None):
                 builder = self._build_one_pep517
             else:
                 builder = self._build_one_legacy
-            if builder(req, temp_dir.path, python_tag=python_tag):
+            wheel_path = builder(req, temp_dir.path, python_tag=python_tag)
+            if wheel_path is not None:
+                wheel_name = os.path.basename(wheel_path)
+                dest_path = os.path.join(output_dir, wheel_name)
                 try:
-                    wheel_name = os.listdir(temp_dir.path)[0]
-                    wheel_path = os.path.join(output_dir, wheel_name)
-                    shutil.move(
-                        os.path.join(temp_dir.path, wheel_name), wheel_path
-                    )
+                    shutil.move(wheel_path, dest_path)
                     logger.info('Stored in directory: %s', output_dir)
-                    return wheel_path
+                    return dest_path
                 except Exception:
                     pass
             # Ignore return, we can't do anything else useful.
@@ -844,11 +843,15 @@ def _base_setup_args(self, req):
         ] + list(self.global_options)
 
     def _build_one_pep517(self, req, tempd, python_tag=None):
+        """Build one InstallRequirement using the PEP 517 build process.
+
+        Returns path to wheel if successfully built. Otherwise, returns None.
+        """
         assert req.metadata_directory is not None
         try:
             req.spin_message = 'Building wheel for %s (PEP 517)' % (req.name,)
             logger.debug('Destination directory: %s', tempd)
-            wheelname = req.pep517_backend.build_wheel(
+            wheel_name = req.pep517_backend.build_wheel(
                 tempd,
                 metadata_directory=req.metadata_directory
             )
@@ -856,17 +859,23 @@ def _build_one_pep517(self, req, tempd, python_tag=None):
                 # General PEP 517 backends don't necessarily support
                 # a "--python-tag" option, so we rename the wheel
                 # file directly.
-                newname = replace_python_tag(wheelname, python_tag)
+                new_name = replace_python_tag(wheel_name, python_tag)
                 os.rename(
-                    os.path.join(tempd, wheelname),
-                    os.path.join(tempd, newname)
+                    os.path.join(tempd, wheel_name),
+                    os.path.join(tempd, new_name)
                 )
-            return True
+                # Reassign to simplify the return at the end of function
+                wheel_name = new_name
         except Exception:
             logger.error('Failed building wheel for %s', req.name)
-            return False
+            return None
+        return os.path.join(tempd, wheel_name)
 
     def _build_one_legacy(self, req, tempd, python_tag=None):
+        """Build one InstallRequirement using the "legacy" build process.
+
+        Returns path to wheel if successfully built. Otherwise, returns None.
+        """
         base_args = self._base_setup_args(req)
 
         spin_message = 'Building wheel for %s (setup.py)' % (req.name,)
@@ -881,11 +890,12 @@ def _build_one_legacy(self, req, tempd, python_tag=None):
             try:
                 call_subprocess(wheel_args, cwd=req.setup_py_dir,
                                 show_stdout=False, spinner=spinner)
-                return True
             except Exception:
                 spinner.finish("error")
                 logger.error('Failed building wheel for %s', req.name)
-                return False
+                return None
+            # listdir's return value is sorted to be deterministic
+            return os.path.join(tempd, sorted(os.listdir(tempd))[0])
 
     def _clean_one(self, req):
         base_args = self._base_setup_args(req)


Test patch:
diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py
--- a/tests/functional/test_wheel.py
+++ b/tests/functional/test_wheel.py
@@ -14,6 +14,13 @@ def auto_with_wheel(with_wheel):
     pass
 
 
+def add_files_to_dist_directory(folder):
+    (folder / 'dist').makedirs()
+    (folder / 'dist' / 'a_name-0.0.1.tar.gz').write("hello")
+    # Not adding a wheel file since that confuses setuptools' backend.
+    # (folder / 'dist' / 'a_name-0.0.1-py2.py3-none-any.whl').write("hello")
+
+
 def test_wheel_exit_status_code_when_no_requirements(script):
     """
     Test wheel exit status code when no requirements specified
@@ -210,3 +217,31 @@ def test_pip_wheel_with_user_set_in_config(script, data, common_wheels):
         '--no-index', '-f', common_wheels
     )
     assert "Successfully built withpyproject" in result.stdout, result.stdout
+
+
+def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data):
+    """Check correct wheels are copied. (#6196)
+    """
+    pkg_to_wheel = data.src / 'withpyproject'
+    add_files_to_dist_directory(pkg_to_wheel)
+
+    result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path)
+    assert "Installing build dependencies" in result.stdout, result.stdout
+
+    wheel_file_name = 'withpyproject-0.0.1-py%s-none-any.whl' % pyversion[0]
+    wheel_file_path = script.scratch / wheel_file_name
+    assert wheel_file_path in result.files_created, result.stdout
+
+
+def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data):
+    """Check correct wheels are copied. (#6196)
+    """
+    pkg_to_wheel = data.src / 'simplewheel-1.0'
+    add_files_to_dist_directory(pkg_to_wheel)
+
+    result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path)
+    assert "Installing build dependencies" not in result.stdout, result.stdout
+
+    wheel_file_name = 'simplewheel-1.0-py%s-none-any.whl' % pyversion[0]
+    wheel_file_path = script.scratch / wheel_file_name
+    assert wheel_file_path in result.files_created, result.stdout