import contextlib
import importlib
import os
import re
import shutil
import signal
import sys
import tarfile
from concurrent import futures
from pathlib import Path
from typing import Any, Callable
from zipfile import ZipFile

import pytest
from jaraco import path
from packaging.requirements import Requirement

from .textwrap import DALS

SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"


TIMEOUT = int(os.getenv("TIMEOUT_BACKEND_TEST", "180"))  # in seconds
IS_PYPY = '__pypy__' in sys.builtin_module_names


pytestmark = pytest.mark.skipif(
    sys.platform == "win32" and IS_PYPY,
    reason="The combination of PyPy + Windows + pytest-xdist + ProcessPoolExecutor "
    "is flaky and problematic",
)


class BuildBackendBase:
    def __init__(self, cwd='.', env=None, backend_name='setuptools.build_meta'):
        self.cwd = cwd
        self.env = env or {}
        self.backend_name = backend_name


class BuildBackend(BuildBackendBase):
    """PEP 517 Build Backend"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.pool = futures.ProcessPoolExecutor(max_workers=1)

    def __getattr__(self, name: str) -> Callable[..., Any]:
        """Handles arbitrary function invocations on the build backend."""

        def method(*args, **kw):
            root = os.path.abspath(self.cwd)
            caller = BuildBackendCaller(root, self.env, self.backend_name)
            pid = None
            try:
                pid = self.pool.submit(os.getpid).result(TIMEOUT)
                return self.pool.submit(caller, name, *args, **kw).result(TIMEOUT)
            except futures.TimeoutError:
                self.pool.shutdown(wait=False)  # doesn't stop already running processes
                self._kill(pid)
                pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)")
            except (futures.process.BrokenProcessPool, MemoryError, OSError):
                if IS_PYPY:
                    pytest.xfail("PyPy frequently fails tests with ProcessPoolExector")
                raise

        return method

    def _kill(self, pid):
        if pid is None:
            return
        with contextlib.suppress(ProcessLookupError, OSError):
            os.kill(pid, signal.SIGTERM if os.name == "nt" else signal.SIGKILL)


class BuildBackendCaller(BuildBackendBase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        (self.backend_name, _, self.backend_obj) = self.backend_name.partition(':')

    def __call__(self, name, *args, **kw):
        """Handles arbitrary function invocations on the build backend."""
        os.chdir(self.cwd)
        os.environ.update(self.env)
        mod = importlib.import_module(self.backend_name)

        if self.backend_obj:
            backend = getattr(mod, self.backend_obj)
        else:
            backend = mod

        return getattr(backend, name)(*args, **kw)


defns = [
    {  # simple setup.py script
        'setup.py': DALS(
            """
            __import__('setuptools').setup(
                name='foo',
                version='0.0.0',
                py_modules=['hello'],
                setup_requires=['six'],
            )
            """
        ),
        'hello.py': DALS(
            """
            def run():
                print('hello')
            """
        ),
    },
    {  # setup.py that relies on __name__
        'setup.py': DALS(
            """
            assert __name__ == '__main__'
            __import__('setuptools').setup(
                name='foo',
                version='0.0.0',
                py_modules=['hello'],
                setup_requires=['six'],
            )
            """
        ),
        'hello.py': DALS(
            """
            def run():
                print('hello')
            """
        ),
    },
    {  # setup.py script that runs arbitrary code
        'setup.py': DALS(
            """
            variable = True
            def function():
                return variable
            assert variable
            __import__('setuptools').setup(
                name='foo',
                version='0.0.0',
                py_modules=['hello'],
                setup_requires=['six'],
            )
            """
        ),
        'hello.py': DALS(
            """
            def run():
                print('hello')
            """
        ),
    },
    {  # setup.py script that constructs temp files to be included in the distribution
        'setup.py': DALS(
            """
            # Some packages construct files on the fly, include them in the package,
            # and immediately remove them after `setup()` (e.g. pybind11==2.9.1).
            # Therefore, we cannot use `distutils.core.run_setup(..., stop_after=...)`
            # to obtain a distribution object first, and then run the distutils
            # commands later, because these files will be removed in the meantime.

            with open('world.py', 'w', encoding="utf-8") as f:
                f.write('x = 42')

            try:
                __import__('setuptools').setup(
                    name='foo',
                    version='0.0.0',
                    py_modules=['world'],
                    setup_requires=['six'],
                )
            finally:
                # Some packages will clean temporary files
                __import__('os').unlink('world.py')
            """
        ),
    },
    {  # setup.cfg only
        'setup.cfg': DALS(
            """
        [metadata]
        name = foo
        version = 0.0.0

        [options]
        py_modules=hello
        setup_requires=six
        """
        ),
        'hello.py': DALS(
            """
        def run():
            print('hello')
        """
        ),
    },
    {  # setup.cfg and setup.py
        'setup.cfg': DALS(
            """
        [metadata]
        name = foo
        version = 0.0.0

        [options]
        py_modules=hello
        setup_requires=six
        """
        ),
        'setup.py': "__import__('setuptools').setup()",
        'hello.py': DALS(
            """
        def run():
            print('hello')
        """
        ),
    },
]


class TestBuildMetaBackend:
    backend_name = 'setuptools.build_meta'

    def get_build_backend(self):
        return BuildBackend(backend_name=self.backend_name)

    @pytest.fixture(params=defns)
    def build_backend(self, tmpdir, request):
        path.build(request.param, prefix=str(tmpdir))
        with tmpdir.as_cwd():
            yield self.get_build_backend()

    def test_get_requires_for_build_wheel(self, build_backend):
        actual = build_backend.get_requires_for_build_wheel()
        expected = ['six']
        assert sorted(actual) == sorted(expected)

    def test_get_requires_for_build_sdist(self, build_backend):
        actual = build_backend.get_requires_for_build_sdist()
        expected = ['six']
        assert sorted(actual) == sorted(expected)

    def test_build_wheel(self, build_backend):
        dist_dir = os.path.abspath('pip-wheel')
        os.makedirs(dist_dir)
        wheel_name = build_backend.build_wheel(dist_dir)

        wheel_file = os.path.join(dist_dir, wheel_name)
        assert os.path.isfile(wheel_file)

        # Temporary files should be removed
        assert not os.path.isfile('world.py')

        with ZipFile(wheel_file) as zipfile:
            wheel_contents = set(zipfile.namelist())

        # Each one of the examples have a single module
        # that should be included in the distribution
        python_scripts = (f for f in wheel_contents if f.endswith('.py'))
        modules = [f for f in python_scripts if not f.endswith('setup.py')]
        assert len(modules) == 1

    @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",
            'setup.cfg': DALS(
                """
                [metadata]
                name = foo
                version = file: VERSION
                """
            ),
            'pyproject.toml': DALS(
                """
                [build-system]
                requires = ["setuptools", "wheel"]
                build-backend = "setuptools.build_meta"
                """
            ),
        }

        path.build(files)

        dist_dir = os.path.abspath('preexisting-' + build_type)

        build_backend = self.get_build_backend()
        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.
        with open("VERSION", "wt", encoding="utf-8") as version_file:
            version_file.write("0.0.2")

        # Build a *second* sdist/wheel.
        second_result = build_method(dist_dir)

        assert os.path.isfile(os.path.join(dist_dir, first_result))
        assert first_result != second_result

        # And if rebuilding the exact same sdist/wheel?
        open(os.path.join(dist_dir, second_result), 'wb').close()
        third_result = build_method(dist_dir)
        assert third_result == second_result
        assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0

    @pytest.mark.parametrize("setup_script", [None, SETUP_SCRIPT_STUB])
    def test_build_with_pyproject_config(self, tmpdir, setup_script):
        files = {
            'pyproject.toml': DALS(
                """
                [build-system]
                requires = ["setuptools", "wheel"]
                build-backend = "setuptools.build_meta"

                [project]
                name = "foo"
                license = {text = "MIT"}
                description = "This is a Python package"
                dynamic = ["version", "readme"]
                classifiers = [
                    "Development Status :: 5 - Production/Stable",
                    "Intended Audience :: Developers"
                ]
                urls = {Homepage = "http://github.com"}
                dependencies = [
                    "appdirs",
                ]

                [project.optional-dependencies]
                all = [
                    "tomli>=1",
                    "pyscaffold>=4,<5",
                    'importlib; python_version == "2.6"',
                ]

                [project.scripts]
                foo = "foo.cli:main"

                [tool.setuptools]
                zip-safe = false
                package-dir = {"" = "src"}
                packages = {find = {where = ["src"]}}
                license-files = ["LICENSE*"]

                [tool.setuptools.dynamic]
                version = {attr = "foo.__version__"}
                readme = {file = "README.rst"}

                [tool.distutils.sdist]
                formats = "gztar"
                """
            ),
            "MANIFEST.in": DALS(
                """
                global-include *.py *.txt
                global-exclude *.py[cod]
                """
            ),
            "README.rst": "This is a ``README``",
            "LICENSE.txt": "---- placeholder MIT license ----",
            "src": {
                "foo": {
                    "__init__.py": "__version__ = '0.1'",
                    "__init__.pyi": "__version__: str",
                    "cli.py": "def main(): print('hello world')",
                    "data.txt": "def main(): print('hello world')",
                    "py.typed": "",
                }
            },
        }
        if setup_script:
            files["setup.py"] = setup_script

        build_backend = self.get_build_backend()
        with tmpdir.as_cwd():
            path.build(files)
            sdist_path = build_backend.build_sdist("temp")
            wheel_file = build_backend.build_wheel("temp")

        with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
            sdist_contents = set(tar.getnames())

        with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile:
            wheel_contents = set(zipfile.namelist())
            metadata = str(zipfile.read("foo-0.1.dist-info/METADATA"), "utf-8")
            license = str(zipfile.read("foo-0.1.dist-info/LICENSE.txt"), "utf-8")
            epoints = str(zipfile.read("foo-0.1.dist-info/entry_points.txt"), "utf-8")

        assert sdist_contents - {"foo-0.1/setup.py"} == {
            'foo-0.1',
            'foo-0.1/LICENSE.txt',
            'foo-0.1/MANIFEST.in',
            'foo-0.1/PKG-INFO',
            'foo-0.1/README.rst',
            'foo-0.1/pyproject.toml',
            'foo-0.1/setup.cfg',
            'foo-0.1/src',
            'foo-0.1/src/foo',
            'foo-0.1/src/foo/__init__.py',
            'foo-0.1/src/foo/__init__.pyi',
            'foo-0.1/src/foo/cli.py',
            'foo-0.1/src/foo/data.txt',
            'foo-0.1/src/foo/py.typed',
            'foo-0.1/src/foo.egg-info',
            'foo-0.1/src/foo.egg-info/PKG-INFO',
            'foo-0.1/src/foo.egg-info/SOURCES.txt',
            'foo-0.1/src/foo.egg-info/dependency_links.txt',
            'foo-0.1/src/foo.egg-info/entry_points.txt',
            'foo-0.1/src/foo.egg-info/requires.txt',
            'foo-0.1/src/foo.egg-info/top_level.txt',
            'foo-0.1/src/foo.egg-info/not-zip-safe',
        }
        assert wheel_contents == {
            "foo/__init__.py",
            "foo/__init__.pyi",  # include type information by default
            "foo/cli.py",
            "foo/data.txt",  # include_package_data defaults to True
            "foo/py.typed",  # include type information by default
            "foo-0.1.dist-info/LICENSE.txt",
            "foo-0.1.dist-info/METADATA",
            "foo-0.1.dist-info/WHEEL",
            "foo-0.1.dist-info/entry_points.txt",
            "foo-0.1.dist-info/top_level.txt",
            "foo-0.1.dist-info/RECORD",
        }
        assert license == "---- placeholder MIT license ----"

        for line in (
            "Summary: This is a Python package",
            "License: MIT",
            "Classifier: Intended Audience :: Developers",
            "Requires-Dist: appdirs",
            "Requires-Dist: " + str(Requirement('tomli>=1 ; extra == "all"')),
            "Requires-Dist: "
            + str(Requirement('importlib; python_version=="2.6" and extra =="all"')),
        ):
            assert line in metadata, (line, metadata)

        assert metadata.strip().endswith("This is a ``README``")
        assert epoints.strip() == "[console_scripts]\nfoo = foo.cli:main"

    def test_static_metadata_in_pyproject_config(self, tmpdir):
        # Make sure static metadata in pyproject.toml is not overwritten by setup.py
        # as required by PEP 621
        files = {
            'pyproject.toml': DALS(
                """
                [build-system]
                requires = ["setuptools", "wheel"]
                build-backend = "setuptools.build_meta"

                [project]
                name = "foo"
                description = "This is a Python package"
                version = "42"
                dependencies = ["six"]
                """
            ),
            'hello.py': DALS(
                """
                def run():
                    print('hello')
                """
            ),
            'setup.py': DALS(
                """
                __import__('setuptools').setup(
                    name='bar',
                    version='13',
                )
                """
            ),
        }
        build_backend = self.get_build_backend()
        with tmpdir.as_cwd():
            path.build(files)
            sdist_path = build_backend.build_sdist("temp")
            wheel_file = build_backend.build_wheel("temp")

        assert (tmpdir / "temp/foo-42.tar.gz").exists()
        assert (tmpdir / "temp/foo-42-py3-none-any.whl").exists()
        assert not (tmpdir / "temp/bar-13.tar.gz").exists()
        assert not (tmpdir / "temp/bar-42.tar.gz").exists()
        assert not (tmpdir / "temp/foo-13.tar.gz").exists()
        assert not (tmpdir / "temp/bar-13-py3-none-any.whl").exists()
        assert not (tmpdir / "temp/bar-42-py3-none-any.whl").exists()
        assert not (tmpdir / "temp/foo-13-py3-none-any.whl").exists()

        with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
            pkg_info = str(tar.extractfile('foo-42/PKG-INFO').read(), "utf-8")
            members = tar.getnames()
            assert "bar-13/PKG-INFO" not in members

        with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile:
            metadata = str(zipfile.read("foo-42.dist-info/METADATA"), "utf-8")
            members = zipfile.namelist()
            assert "bar-13.dist-info/METADATA" not in members

        for file in pkg_info, metadata:
            for line in ("Name: foo", "Version: 42"):
                assert line in file
            for line in ("Name: bar", "Version: 13"):
                assert line not in file

    def test_build_sdist(self, build_backend):
        dist_dir = os.path.abspath('pip-sdist')
        os.makedirs(dist_dir)
        sdist_name = build_backend.build_sdist(dist_dir)

        assert os.path.isfile(os.path.join(dist_dir, sdist_name))

    def test_prepare_metadata_for_build_wheel(self, build_backend):
        dist_dir = os.path.abspath('pip-dist-info')
        os.makedirs(dist_dir)

        dist_info = build_backend.prepare_metadata_for_build_wheel(dist_dir)

        assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA'))

    def test_prepare_metadata_inplace(self, build_backend):
        """
        Some users might pass metadata_directory pre-populated with `.tox` or `.venv`.
        See issue #3523.
        """
        for pre_existing in [
            ".tox/python/lib/python3.10/site-packages/attrs-22.1.0.dist-info",
            ".tox/python/lib/python3.10/site-packages/autocommand-2.2.1.dist-info",
            ".nox/python/lib/python3.10/site-packages/build-0.8.0.dist-info",
            ".venv/python3.10/site-packages/click-8.1.3.dist-info",
            "venv/python3.10/site-packages/distlib-0.3.5.dist-info",
            "env/python3.10/site-packages/docutils-0.19.dist-info",
        ]:
            os.makedirs(pre_existing, exist_ok=True)
        dist_info = build_backend.prepare_metadata_for_build_wheel(".")
        assert os.path.isfile(os.path.join(dist_info, 'METADATA'))

    def test_build_sdist_explicit_dist(self, build_backend):
        # explicitly specifying the dist folder should work
        # the folder sdist_directory and the ``--dist-dir`` can be the same
        dist_dir = os.path.abspath('dist')
        sdist_name = build_backend.build_sdist(dist_dir)
        assert os.path.isfile(os.path.join(dist_dir, sdist_name))

    def test_build_sdist_version_change(self, build_backend):
        sdist_into_directory = os.path.abspath("out_sdist")
        os.makedirs(sdist_into_directory)

        sdist_name = build_backend.build_sdist(sdist_into_directory)
        assert os.path.isfile(os.path.join(sdist_into_directory, sdist_name))

        # if the setup.py changes subsequent call of the build meta
        # should still succeed, given the
        # sdist_directory the frontend specifies is empty
        setup_loc = os.path.abspath("setup.py")
        if not os.path.exists(setup_loc):
            setup_loc = os.path.abspath("setup.cfg")

        with open(setup_loc, 'rt', encoding="utf-8") as file_handler:
            content = file_handler.read()
        with open(setup_loc, 'wt', encoding="utf-8") as file_handler:
            file_handler.write(content.replace("version='0.0.0'", "version='0.0.1'"))

        shutil.rmtree(sdist_into_directory)
        os.makedirs(sdist_into_directory)

        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_pyproject_toml_exists(self, tmpdir_cwd):
        files = {
            'setup.py': DALS(
                """
                __import__('setuptools').setup(
                    name='foo',
                    version='0.0.0',
                    py_modules=['hello']
                )"""
            ),
            'hello.py': '',
            'pyproject.toml': DALS(
                """
                [build-system]
                requires = ["setuptools", "wheel"]
                build-backend = "setuptools.build_meta"
                """
            ),
        }
        path.build(files)
        build_backend = self.get_build_backend()
        targz_path = build_backend.build_sdist("temp")
        with tarfile.open(os.path.join("temp", targz_path)) as tar:
            assert any('pyproject.toml' in name for name in tar.getnames())

    def test_build_sdist_setup_py_exists(self, tmpdir_cwd):
        # If build_sdist is called from a script other than setup.py,
        # ensure setup.py is included
        path.build(defns[0])

        build_backend = self.get_build_backend()
        targz_path = build_backend.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(self, 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
        """
            ),
        }

        path.build(files)

        build_backend = self.get_build_backend()
        targz_path = build_backend.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())

    def test_build_sdist_builds_targz_even_if_zip_indicated(self, tmpdir_cwd):
        files = {
            'setup.py': DALS(
                """
                __import__('setuptools').setup(
                    name='foo',
                    version='0.0.0',
                    py_modules=['hello']
                )"""
            ),
            'hello.py': '',
            'setup.cfg': DALS(
                """
                [sdist]
                formats=zip
                """
            ),
        }

        path.build(files)

        build_backend = self.get_build_backend()
        build_backend.build_sdist("temp")

    _relative_path_import_files = {
        'setup.py': DALS(
            """
            __import__('setuptools').setup(
                name='foo',
                version=__import__('hello').__version__,
                py_modules=['hello']
            )"""
        ),
        'hello.py': '__version__ = "0.0.0"',
        'setup.cfg': DALS(
            """
            [sdist]
            formats=zip
            """
        ),
    }

    def test_build_sdist_relative_path_import(self, tmpdir_cwd):
        path.build(self._relative_path_import_files)
        build_backend = self.get_build_backend()
        with pytest.raises(ImportError, match="^No module named 'hello'$"):
            build_backend.build_sdist("temp")

    _simple_pyproject_example = {
        "pyproject.toml": DALS(
            """
            [project]
            name = "proj"
            version = "42"
            """
        ),
        "src": {"proj": {"__init__.py": ""}},
    }

    def _assert_link_tree(self, parent_dir):
        """All files in the directory should be either links or hard links"""
        files = list(Path(parent_dir).glob("**/*"))
        assert files  # Should not be empty
        for file in files:
            assert file.is_symlink() or os.stat(file).st_nlink > 0

    def test_editable_without_config_settings(self, tmpdir_cwd):
        """
        Sanity check to ensure tests with --mode=strict are different from the ones
        without --mode.

        --mode=strict should create a local directory with a package tree.
        The directory should not get created otherwise.
        """
        path.build(self._simple_pyproject_example)
        build_backend = self.get_build_backend()
        assert not Path("build").exists()
        build_backend.build_editable("temp")
        assert not Path("build").exists()

    def test_build_wheel_inplace(self, tmpdir_cwd):
        config_settings = {"--build-option": ["build_ext", "--inplace"]}
        path.build(self._simple_pyproject_example)
        build_backend = self.get_build_backend()
        assert not Path("build").exists()
        Path("build").mkdir()
        build_backend.prepare_metadata_for_build_wheel("build", config_settings)
        build_backend.build_wheel("build", config_settings)
        assert Path("build/proj-42-py3-none-any.whl").exists()

    @pytest.mark.parametrize("config_settings", [{"editable-mode": "strict"}])
    def test_editable_with_config_settings(self, tmpdir_cwd, config_settings):
        path.build({**self._simple_pyproject_example, '_meta': {}})
        assert not Path("build").exists()
        build_backend = self.get_build_backend()
        build_backend.prepare_metadata_for_build_editable("_meta", config_settings)
        build_backend.build_editable("temp", config_settings, "_meta")
        self._assert_link_tree(next(Path("build").glob("__editable__.*")))

    @pytest.mark.parametrize(
        ("setup_literal", "requirements"),
        [
            ("'foo'", ['foo']),
            ("['foo']", ['foo']),
            (r"'foo\n'", ['foo']),
            (r"'foo\n\n'", ['foo']),
            ("['foo', 'bar']", ['foo', 'bar']),
            (r"'# Has a comment line\nfoo'", ['foo']),
            (r"'foo # Has an inline comment'", ['foo']),
            (r"'foo \\\n >=3.0'", ['foo>=3.0']),
            (r"'foo\nbar'", ['foo', 'bar']),
            (r"'foo\nbar\n'", ['foo', 'bar']),
            (r"['foo\n', 'bar\n']", ['foo', 'bar']),
        ],
    )
    @pytest.mark.parametrize('use_wheel', [True, False])
    def test_setup_requires(self, setup_literal, requirements, use_wheel, tmpdir_cwd):
        files = {
            'setup.py': DALS(
                """
                from setuptools import setup

                setup(
                    name="qux",
                    version="0.0.0",
                    py_modules=["hello"],
                    setup_requires={setup_literal},
                )
            """
            ).format(setup_literal=setup_literal),
            'hello.py': DALS(
                """
            def run():
                print('hello')
            """
            ),
        }

        path.build(files)

        build_backend = self.get_build_backend()

        if use_wheel:
            get_requires = build_backend.get_requires_for_build_wheel
        else:
            get_requires = build_backend.get_requires_for_build_sdist

        # Ensure that the build requirements are properly parsed
        expected = sorted(requirements)
        actual = get_requires()

        assert expected == sorted(actual)

    def test_setup_requires_with_auto_discovery(self, tmpdir_cwd):
        # Make sure patches introduced to retrieve setup_requires don't accidentally
        # activate auto-discovery and cause problems due to the incomplete set of
        # attributes passed to MinimalDistribution
        files = {
            'pyproject.toml': DALS(
                """
                [project]
                name = "proj"
                version = "42"
            """
            ),
            "setup.py": DALS(
                """
                __import__('setuptools').setup(
                    setup_requires=["foo"],
                    py_modules = ["hello", "world"]
                )
            """
            ),
            'hello.py': "'hello'",
            'world.py': "'world'",
        }
        path.build(files)
        build_backend = self.get_build_backend()
        setup_requires = build_backend.get_requires_for_build_wheel()
        assert setup_requires == ["foo"]

    def test_dont_install_setup_requires(self, tmpdir_cwd):
        files = {
            'setup.py': DALS(
                """
                        from setuptools import setup

                        setup(
                            name="qux",
                            version="0.0.0",
                            py_modules=["hello"],
                            setup_requires=["does-not-exist >99"],
                        )
                    """
            ),
            'hello.py': DALS(
                """
                    def run():
                        print('hello')
                    """
            ),
        }

        path.build(files)

        build_backend = self.get_build_backend()

        dist_dir = os.path.abspath('pip-dist-info')
        os.makedirs(dist_dir)

        # does-not-exist can't be satisfied, so if it attempts to install
        # setup_requires, it will fail.
        build_backend.prepare_metadata_for_build_wheel(dist_dir)

    _sys_argv_0_passthrough = {
        'setup.py': DALS(
            """
            import os
            import sys

            __import__('setuptools').setup(
                name='foo',
                version='0.0.0',
            )

            sys_argv = os.path.abspath(sys.argv[0])
            file_path = os.path.abspath('setup.py')
            assert sys_argv == file_path
            """
        )
    }

    def test_sys_argv_passthrough(self, tmpdir_cwd):
        path.build(self._sys_argv_0_passthrough)
        build_backend = self.get_build_backend()
        with pytest.raises(AssertionError):
            build_backend.build_sdist("temp")

    _setup_py_file_abspath = {
        'setup.py': DALS(
            """
            import os
            assert os.path.isabs(__file__)
            __import__('setuptools').setup(
                name='foo',
                version='0.0.0',
                py_modules=['hello'],
                setup_requires=['six'],
            )
            """
        )
    }

    def test_setup_py_file_abspath(self, tmpdir_cwd):
        path.build(self._setup_py_file_abspath)
        build_backend = self.get_build_backend()
        build_backend.build_sdist("temp")

    @pytest.mark.parametrize('build_hook', ('build_sdist', 'build_wheel'))
    def test_build_with_empty_setuppy(self, build_backend, build_hook):
        files = {'setup.py': ''}
        path.build(files)

        msg = re.escape('No distribution was found.')
        with pytest.raises(ValueError, match=msg):
            getattr(build_backend, build_hook)("temp")


class TestBuildMetaLegacyBackend(TestBuildMetaBackend):
    backend_name = 'setuptools.build_meta:__legacy__'

    # build_meta_legacy-specific tests
    def test_build_sdist_relative_path_import(self, tmpdir_cwd):
        # This must fail in build_meta, but must pass in build_meta_legacy
        path.build(self._relative_path_import_files)

        build_backend = self.get_build_backend()
        build_backend.build_sdist("temp")

    def test_sys_argv_passthrough(self, tmpdir_cwd):
        path.build(self._sys_argv_0_passthrough)

        build_backend = self.get_build_backend()
        build_backend.build_sdist("temp")


def test_legacy_editable_install(venv, tmpdir, tmpdir_cwd):
    pyproject = """
    [build-system]
    requires = ["setuptools"]
    build-backend = "setuptools.build_meta"
    [project]
    name = "myproj"
    version = "42"
    """
    path.build({"pyproject.toml": DALS(pyproject), "mymod.py": ""})

    # First: sanity check
    cmd = ["pip", "install", "--no-build-isolation", "-e", "."]
    output = venv.run(cmd, cwd=tmpdir).lower()
    assert "running setup.py develop for myproj" not in output
    assert "created wheel for myproj" in output

    # Then: real test
    env = {**os.environ, "SETUPTOOLS_ENABLE_FEATURES": "legacy-editable"}
    cmd = ["pip", "install", "--no-build-isolation", "-e", "."]
    output = venv.run(cmd, cwd=tmpdir, env=env).lower()
    assert "running setup.py develop for myproj" in output


@pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning")
def test_sys_exit_0_in_setuppy(monkeypatch, tmp_path):
    """Setuptools should be resilient to setup.py with ``sys.exit(0)`` (#3973)."""
    monkeypatch.chdir(tmp_path)
    setuppy = """
        import sys, setuptools
        setuptools.setup(name='foo', version='0.0.0')
        sys.exit(0)
        """
    (tmp_path / "setup.py").write_text(DALS(setuppy), encoding="utf-8")
    backend = BuildBackend(backend_name="setuptools.build_meta")
    assert backend.get_requires_for_build_wheel() == []


def test_system_exit_in_setuppy(monkeypatch, tmp_path):
    monkeypatch.chdir(tmp_path)
    setuppy = "import sys; sys.exit('some error')"
    (tmp_path / "setup.py").write_text(setuppy, encoding="utf-8")
    with pytest.raises(SystemExit, match="some error"):
        backend = BuildBackend(backend_name="setuptools.build_meta")
        backend.get_requires_for_build_wheel()
