Repository: pypa/setuptools

Issue:
`build_meta.build_sdist` should work if the destination directory already contains a tar.gz
The issue is similar to #1671, see #1745 for how the issue can be resolved.


Discussion:
@benoit-pierre Do you have a reproducer? I tried using my [original reproducer](https://github.com/pypa/setuptools/issues/1671#issue-406412822) but using `python -m pep517.build --source` in place of `pip wheel`, and it had no problem creating two sdists. Not entirely sure why it wasn't a problem, though.
With an updated version? I suspect it works fine when re-creating the exact same source dist.
Also, I don't know how `pep517.build` is implemented.
> With an updated version? I suspect it works fine when re-creating the exact same source dist.

Yeah: 

```
$ ls /tmp/demo_dist_517/dist
foo-0.0.1.tar.gz  foo-0.0.2.tar.gz
```

> Also, I don't know how pep517.build is implemented.

Yeah. I suppose the best reproducer would be an `xfail`-ing test like the one from #1726 that hits this bug in our minimal PEP 517 build frontend. For now, the only PEP 517 frontends that I know of that build an sdist are `tox` and `pep517.build`. If for some reason neither of those hit it that makes this a less important bug to fix, but if it's *possible* for a valid PEP 517 frontend to hit it, then we should fix it anyway.
This fail as expected for the sdist case:
```diff
 setuptools/tests/test_build_meta.py | 33 ++++++++++++++++++---------------
 1 file changed, 18 insertions(+), 15 deletions(-)

diff --git i/setuptools/tests/test_build_meta.py w/setuptools/tests/test_build_meta.py
index 7612ebd7..38886268 100644
--- i/setuptools/tests/test_build_meta.py
+++ w/setuptools/tests/test_build_meta.py
@@ -157,9 +157,10 @@ def test_build_wheel(self, build_backend):
 
         assert os.path.isfile(os.path.join(dist_dir, wheel_name))
 
-    def test_build_wheel_with_existing_wheel_file_present(self, tmpdir_cwd):
-        # Building a wheel should still succeed if there's already a wheel
-        # in the wheel directory
+    @pytest.mark.parametrize('build_type', ('wheel', 'sdist'))
+    def test_build_with_existing_file_present(self, build_type, tmpdir_cwd):
+        # Building a sdist/wheel should still succeed if there's
+        # already a sdist/wheel in the destination directory.
         files = {
             'setup.py': "from setuptools import setup\nsetup()",
             'VERSION': "0.0.1",
@@ -177,28 +178,30 @@ def test_build_wheel_with_existing_wheel_file_present(self, tmpdir_cwd):
 
         build_files(files)
 
-        dist_dir = os.path.abspath('pip-wheel-preexisting')
+        dist_dir = os.path.abspath('preexisting-' + build_type)
         os.makedirs(dist_dir)
 
-        # make first wheel
         build_backend = self.get_build_backend()
-        wheel_one = build_backend.build_wheel(dist_dir)
+        build_method = getattr(build_backend, 'build_' + build_type)
+
+        # build a first sdist/wheel
+        first_result = build_method(dist_dir)
 
         # change version
         with open("VERSION", "wt") as version_file:
             version_file.write("0.0.2")
 
-        # make *second* wheel
-        wheel_two = self.get_build_backend().build_wheel(dist_dir)
+        # build a *second* sdist/wheel
+        second_result = build_method(dist_dir)
 
-        assert os.path.isfile(os.path.join(dist_dir, wheel_one))
-        assert wheel_one != wheel_two
+        assert os.path.isfile(os.path.join(dist_dir, first_result))
+        assert first_result != second_result
 
-        # and if rebuilding the same wheel?
-        open(os.path.join(dist_dir, wheel_two), 'w').close()
-        wheel_three = self.get_build_backend().build_wheel(dist_dir)
-        assert wheel_three == wheel_two
-        assert os.path.getsize(os.path.join(dist_dir, wheel_three)) > 0
+        # and if rebuilding the exact same sdist/wheel?
+        open(os.path.join(dist_dir, second_result), 'w').close()
+        third_result = build_method(dist_dir)
+        assert third_result == second_result
+        assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0
 
     def test_build_sdist(self, build_backend):
         dist_dir = os.path.abspath('pip-sdist')
```

Resolving patch:
diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py
--- a/setuptools/build_meta.py
+++ b/setuptools/build_meta.py
@@ -38,6 +38,7 @@
 from setuptools.py31compat import TemporaryDirectory
 
 from pkg_resources import parse_requirements
+from pkg_resources.py31compat import makedirs
 
 __all__ = ['get_requires_for_build_sdist',
            'get_requires_for_build_wheel',
@@ -179,36 +180,38 @@ def prepare_metadata_for_build_wheel(self, metadata_directory,
 
         return dist_infos[0]
 
-    def build_wheel(self, wheel_directory, config_settings=None,
-                    metadata_directory=None):
+    def _build_with_temp_dir(self, setup_command, result_extension,
+                             result_directory, config_settings):
         config_settings = self._fix_config(config_settings)
-        wheel_directory = os.path.abspath(wheel_directory)
+        result_directory = os.path.abspath(result_directory)
 
-        # Build the wheel in a temporary directory, then copy to the target
-        with TemporaryDirectory(dir=wheel_directory) as tmp_dist_dir:
-            sys.argv = (sys.argv[:1] +
-                        ['bdist_wheel', '--dist-dir', tmp_dist_dir] +
+        # Build in a temporary directory, then copy to the target.
+        makedirs(result_directory, exist_ok=True)
+        with TemporaryDirectory(dir=result_directory) as tmp_dist_dir:
+            sys.argv = (sys.argv[:1] + setup_command +
+                        ['--dist-dir', tmp_dist_dir] +
                         config_settings["--global-option"])
             self.run_setup()
 
-            wheel_basename = _file_with_extension(tmp_dist_dir, '.whl')
-            wheel_path = os.path.join(wheel_directory, wheel_basename)
-            if os.path.exists(wheel_path):
-                # os.rename will fail overwriting on non-unix env
-                os.remove(wheel_path)
-            os.rename(os.path.join(tmp_dist_dir, wheel_basename), wheel_path)
+            result_basename = _file_with_extension(tmp_dist_dir, result_extension)
+            result_path = os.path.join(result_directory, result_basename)
+            if os.path.exists(result_path):
+                # os.rename will fail overwriting on non-Unix.
+                os.remove(result_path)
+            os.rename(os.path.join(tmp_dist_dir, result_basename), result_path)
 
-        return wheel_basename
+        return result_basename
 
-    def build_sdist(self, sdist_directory, config_settings=None):
-        config_settings = self._fix_config(config_settings)
-        sdist_directory = os.path.abspath(sdist_directory)
-        sys.argv = sys.argv[:1] + ['sdist', '--formats', 'gztar'] + \
-            config_settings["--global-option"] + \
-            ["--dist-dir", sdist_directory]
-        self.run_setup()
 
-        return _file_with_extension(sdist_directory, '.tar.gz')
+    def build_wheel(self, wheel_directory, config_settings=None,
+                    metadata_directory=None):
+        return self._build_with_temp_dir(['bdist_wheel'], '.whl',
+                                         wheel_directory, config_settings)
+
+    def build_sdist(self, sdist_directory, config_settings=None):
+        return self._build_with_temp_dir(['sdist', '--formats', 'gztar'],
+                                         '.tar.gz', sdist_directory,
+                                         config_settings)
 
 
 class _BuildMetaLegacyBackend(_BuildMetaBackend):


Test patch:
diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -157,9 +157,10 @@ def test_build_wheel(self, build_backend):
 
         assert os.path.isfile(os.path.join(dist_dir, wheel_name))
 
-    def test_build_wheel_with_existing_wheel_file_present(self, tmpdir_cwd):
-        # Building a wheel should still succeed if there's already a wheel
-        # in the wheel directory
+    @pytest.mark.parametrize('build_type', ('wheel', 'sdist'))
+    def test_build_with_existing_file_present(self, build_type, tmpdir_cwd):
+        # Building a sdist/wheel should still succeed if there's
+        # already a sdist/wheel in the destination directory.
         files = {
             'setup.py': "from setuptools import setup\nsetup()",
             'VERSION': "0.0.1",
@@ -177,28 +178,31 @@ def test_build_wheel_with_existing_wheel_file_present(self, tmpdir_cwd):
 
         build_files(files)
 
-        dist_dir = os.path.abspath('pip-wheel-preexisting')
-        os.makedirs(dist_dir)
+        dist_dir = os.path.abspath('preexisting-' + build_type)
 
-        # make first wheel
         build_backend = self.get_build_backend()
-        wheel_one = build_backend.build_wheel(dist_dir)
+        build_method = getattr(build_backend, 'build_' + build_type)
+
+        # Build a first sdist/wheel.
+        # Note: this also check the destination directory is
+        # successfully created if it does not exist already.
+        first_result = build_method(dist_dir)
 
-        # change version
+        # Change version.
         with open("VERSION", "wt") as version_file:
             version_file.write("0.0.2")
 
-        # make *second* wheel
-        wheel_two = self.get_build_backend().build_wheel(dist_dir)
+        # Build a *second* sdist/wheel.
+        second_result = build_method(dist_dir)
 
-        assert os.path.isfile(os.path.join(dist_dir, wheel_one))
-        assert wheel_one != wheel_two
+        assert os.path.isfile(os.path.join(dist_dir, first_result))
+        assert first_result != second_result
 
-        # and if rebuilding the same wheel?
-        open(os.path.join(dist_dir, wheel_two), 'w').close()
-        wheel_three = self.get_build_backend().build_wheel(dist_dir)
-        assert wheel_three == wheel_two
-        assert os.path.getsize(os.path.join(dist_dir, wheel_three)) > 0
+        # And if rebuilding the exact same sdist/wheel?
+        open(os.path.join(dist_dir, second_result), 'w').close()
+        third_result = build_method(dist_dir)
+        assert third_result == second_result
+        assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0
 
     def test_build_sdist(self, build_backend):
         dist_dir = os.path.abspath('pip-sdist')