Repository: pypa/setuptools

Issue:
build_meta does not include setup.py
When someone invokes the build operation via the ``build_meta`` (via https://github.com/pypa/setuptools/blob/master/setuptools/build_meta.py#L170 - https://www.python.org/dev/peps/pep-0517/#build-sdist) the ``setup.py`` is no longer automatically packaged.

In case of ``python setup.py sdist`` what ensured that this will happen is https://github.com/python/cpython/blob/master/Lib/distutils/command/sdist.py#L250. Here ``distribution.script_name`` is set to ``setup.py`` when called via ``python setup.py sdist``. This is not set though when calling it directly via ``build_meta`` so packaging ``setup.py`` is safely ignored. 

Note the files to package is determined while running ``egg_info``, and this is re-used across runs (by reading ``SOURCES.txt``). Therefore this bug is hidden when:
- anyone beforehand calls ``python setup.py sdist``,
- when there's a distutils extension that automatically pulls in setup.py (such as setuptools-scm), 
- when some config value pulls in (one in ``MANIFEST.in``, ``setup.cfg``, ``setup.py``).


Discussion:
Here is a minimal script that confirms the bug.

Here is a script to create an MWE repo:

```bash
#!/usr/bin/bash

# Make the directory
mkdir mypkg
cd mypkg
mkdir mypkg
touch mypkg/__init__.py

# Make setup.py
cat << EOF > setup.py
from setuptools import setup

setup(name="mypkg", packages=["mypkg"], version="0.0.1")
EOF
```

After running it, `cd` into `mypkg` and run:

```bash
python -c 'from setuptools.build_meta import build_sdist; build_sdist(".")'
```

This will build the sdist, and you can confirm that `setup.py` is not included in the tarball:

```bash
$ tar -tf mypkg-0.0.1.tar.gz 
mypkg-0.0.1/
mypkg-0.0.1/PKG-INFO
mypkg-0.0.1/mypkg/
mypkg-0.0.1/mypkg/__init__.py
mypkg-0.0.1/mypkg.egg-info/
mypkg-0.0.1/mypkg.egg-info/PKG-INFO
mypkg-0.0.1/mypkg.egg-info/SOURCES.txt
mypkg-0.0.1/mypkg.egg-info/dependency_links.txt
mypkg-0.0.1/mypkg.egg-info/top_level.txt
mypkg-0.0.1/setup.cfg
```

A PR with a failing test that basically repeats those steps and asserts that `setup.py` is included in the resulting tarball would be accepted, as would a fix for said test.
Working on this based on the PyPA sprint : https://github.com/orgs/pypa/projects/1

Resolving patch:
diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -575,6 +575,12 @@ def add_defaults(self):
             self.filelist.extend(rcfiles)
         elif os.path.exists(self.manifest):
             self.read_manifest()
+
+        if os.path.exists("setup.py"):
+            # setup.py should be included by default, even if it's not
+            # the script called to create the sdist
+            self.filelist.append("setup.py")
+
         ei_cmd = self.get_finalized_command('egg_info')
         self.filelist.graft(ei_cmd.egg_info)
 


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
@@ -2,9 +2,11 @@
 
 import os
 import shutil
+import tarfile
 
 import pytest
 
+from setuptools.build_meta import build_sdist
 from .files import build_files
 from .textwrap import DALS
 from . import py2_only
@@ -181,3 +183,34 @@ def test_build_sdist_version_change(build_backend):
 
     sdist_name = build_backend.build_sdist("out_sdist")
     assert os.path.isfile(os.path.join(os.path.abspath("out_sdist"), sdist_name))
+
+
+def test_build_sdist_setup_py_exists(tmpdir_cwd):
+    # If build_sdist is called from a script other than setup.py,
+    # ensure setup.py is include
+    build_files(defns[0])
+    targz_path = build_sdist("temp")
+    with tarfile.open(os.path.join("temp", targz_path)) as tar:
+        assert any('setup.py' in name for name in tar.getnames())
+
+
+def test_build_sdist_setup_py_manifest_excluded(tmpdir_cwd):
+    # Ensure that MANIFEST.in can exclude setup.py
+    files = {
+        'setup.py': DALS("""
+    __import__('setuptools').setup(
+        name='foo',
+        version='0.0.0',
+        py_modules=['hello']
+    )"""),
+        'hello.py': '',
+        'MANIFEST.in': DALS("""
+    exclude setup.py
+    """)
+    }
+
+    build_files(files)
+    targz_path = build_sdist("temp")
+    with tarfile.open(os.path.join("temp", targz_path)) as tar:
+        assert not any('setup.py' in name for name in tar.getnames())
+
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -569,6 +569,20 @@ def test_manifest_maker_warning_suppression(self):
         for msg in fixtures:
             assert manifest_maker._should_suppress_warning(msg)
 
+    def test_egg_info_includes_setup_py(self, tmpdir_cwd):
+        self._create_project()
+        dist = Distribution({"name": "foo", "version": "0.0.1"})
+        dist.script_name = "non_setup.py"
+        egg_info_instance = egg_info(dist)
+        egg_info_instance.finalize_options()
+        egg_info_instance.run()
+
+        assert 'setup.py' in egg_info_instance.filelist.files
+
+        with open(egg_info_instance.egg_info + "/SOURCES.txt") as f:
+            sources = f.read().split('\n')
+            assert 'setup.py' in sources
+
     def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None):
         environ = os.environ.copy().update(
             HOME=env.paths['home'],
@@ -605,4 +619,4 @@ def test_egg_info_tag_only_once(self, tmpdir_cwd, env):
         assert 'Version: 0.0.0.dev0' in pkg_info_lines
 
     def test_get_pkg_info_revision_deprecated(self):
-        pytest.warns(EggInfoDeprecationWarning, get_pkg_info_revision)
\ No newline at end of file
+        pytest.warns(EggInfoDeprecationWarning, get_pkg_info_revision)
diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py
--- a/setuptools/tests/test_sdist.py
+++ b/setuptools/tests/test_sdist.py
@@ -92,9 +92,8 @@ def latin1_fail():
 class TestSdistTest:
     def setup_method(self, method):
         self.temp_dir = tempfile.mkdtemp()
-        f = open(os.path.join(self.temp_dir, 'setup.py'), 'w')
-        f.write(SETUP_PY)
-        f.close()
+        with open(os.path.join(self.temp_dir, 'setup.py'), 'w') as f:
+            f.write(SETUP_PY)
 
         # Set up the rest of the test package
         test_pkg = os.path.join(self.temp_dir, 'sdist_test')
@@ -135,6 +134,47 @@ def test_package_data_in_sdist(self):
         assert os.path.join('sdist_test', 'c.rst') not in manifest
         assert os.path.join('d', 'e.dat') in manifest
 
+    def test_setup_py_exists(self):
+        dist = Distribution(SETUP_ATTRS)
+        dist.script_name = 'foo.py'
+        cmd = sdist(dist)
+        cmd.ensure_finalized()
+
+        with quiet():
+            cmd.run()
+
+        manifest = cmd.filelist.files
+        assert 'setup.py' in manifest
+
+    def test_setup_py_missing(self):
+        dist = Distribution(SETUP_ATTRS)
+        dist.script_name = 'foo.py'
+        cmd = sdist(dist)
+        cmd.ensure_finalized()
+
+        if os.path.exists("setup.py"):
+            os.remove("setup.py")
+        with quiet():
+            cmd.run()
+
+        manifest = cmd.filelist.files
+        assert 'setup.py' not in manifest
+
+    def test_setup_py_excluded(self):
+        with open("MANIFEST.in", "w") as manifest_file:
+            manifest_file.write("exclude setup.py")
+
+        dist = Distribution(SETUP_ATTRS)
+        dist.script_name = 'foo.py'
+        cmd = sdist(dist)
+        cmd.ensure_finalized()
+
+        with quiet():
+            cmd.run()
+
+        manifest = cmd.filelist.files
+        assert 'setup.py' not in manifest
+
     def test_defaults_case_sensitivity(self):
         """
         Make sure default files (README.*, etc.) are added in a case-sensitive