from __future__ import annotations

import os
import platform
import sys
import zipfile
from typing import TYPE_CHECKING

import packaging.tags
import pytest

from hatchling.builders.plugin.interface import BuilderInterface
from hatchling.builders.utils import get_known_python_major_versions
from hatchling.builders.wheel import WheelBuilder
from hatchling.metadata.spec import DEFAULT_METADATA_VERSION, get_core_metadata_constructors
from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT

if TYPE_CHECKING:
    from hatch.utils.fs import Path


def sys_tags():
    return iter(
        t for t in packaging.tags.sys_tags() if "manylinux" not in t.platform and "muslllinux" not in t.platform
    )


# https://github.com/python/cpython/pull/26184
fixed_pathlib_resolution = pytest.mark.skipif(
    sys.platform == "win32" and (sys.version_info < (3, 8) or sys.implementation.name == "pypy"),
    reason="pathlib.Path.resolve has bug on Windows",
)


def get_python_versions_tag():
    return ".".join(f"py{major_version}" for major_version in get_known_python_major_versions())


def extract_zip(zip_path: Path, target: Path) -> None:
    with zipfile.ZipFile(zip_path, "r") as z:
        for name in z.namelist():
            member = z.getinfo(name)
            path = z.extract(member, target)
            os.chmod(path, member.external_attr >> 16)


def test_class():
    assert issubclass(WheelBuilder, BuilderInterface)


def test_default_versions(isolation):
    builder = WheelBuilder(str(isolation))

    assert builder.get_default_versions() == ["standard"]


class TestDefaultFileSelection:
    def test_already_defined(self, temp_dir):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {
                "hatch": {
                    "build": {
                        "targets": {
                            "wheel": {
                                "include": ["foo"],
                                "exclude": ["bar"],
                                "packages": ["foo", "bar", "baz"],
                                "only-include": ["baz"],
                            }
                        }
                    }
                }
            },
        }
        builder = WheelBuilder(str(temp_dir), config=config)

        assert builder.config.default_include() == ["foo"]
        assert builder.config.default_exclude() == ["bar"]
        assert builder.config.default_packages() == ["foo", "bar", "baz"]
        assert builder.config.default_only_include() == ["baz"]

    def test_flat_layout(self, temp_dir):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"exclude": ["foobarbaz"]}}}}},
        }
        builder = WheelBuilder(str(temp_dir), config=config)

        flat_root = temp_dir / "my_app" / "__init__.py"
        flat_root.ensure_parent_dir_exists()
        flat_root.touch()

        src_root = temp_dir / "src" / "my_app" / "__init__.py"
        src_root.ensure_parent_dir_exists()
        src_root.touch()

        single_module_root = temp_dir / "my_app.py"
        single_module_root.touch()

        namespace_root = temp_dir / "ns" / "my_app" / "__init__.py"
        namespace_root.ensure_parent_dir_exists()
        namespace_root.touch()

        assert builder.config.default_include() == []
        assert builder.config.default_exclude() == ["foobarbaz"]
        assert builder.config.default_packages() == ["my_app"]
        assert builder.config.default_only_include() == []

    def test_src_layout(self, temp_dir):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"exclude": ["foobarbaz"]}}}}},
        }
        builder = WheelBuilder(str(temp_dir), config=config)

        src_root = temp_dir / "src" / "my_app" / "__init__.py"
        src_root.ensure_parent_dir_exists()
        src_root.touch()

        single_module_root = temp_dir / "my_app.py"
        single_module_root.touch()

        namespace_root = temp_dir / "ns" / "my_app" / "__init__.py"
        namespace_root.ensure_parent_dir_exists()
        namespace_root.touch()

        assert builder.config.default_include() == []
        assert builder.config.default_exclude() == ["foobarbaz"]
        assert builder.config.default_packages() == ["src/my_app"]
        assert builder.config.default_only_include() == []

    def test_single_module(self, temp_dir):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"exclude": ["foobarbaz"]}}}}},
        }
        builder = WheelBuilder(str(temp_dir), config=config)

        single_module_root = temp_dir / "my_app.py"
        single_module_root.touch()

        namespace_root = temp_dir / "ns" / "my_app" / "__init__.py"
        namespace_root.ensure_parent_dir_exists()
        namespace_root.touch()

        assert builder.config.default_include() == []
        assert builder.config.default_exclude() == ["foobarbaz"]
        assert builder.config.default_packages() == []
        assert builder.config.default_only_include() == ["my_app.py"]

    def test_namespace(self, temp_dir):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"exclude": ["foobarbaz"]}}}}},
        }
        builder = WheelBuilder(str(temp_dir), config=config)

        namespace_root = temp_dir / "ns" / "my_app" / "__init__.py"
        namespace_root.ensure_parent_dir_exists()
        namespace_root.touch()

        assert builder.config.default_include() == []
        assert builder.config.default_exclude() == ["foobarbaz"]
        assert builder.config.default_packages() == ["ns"]
        assert builder.config.default_only_include() == []

    def test_default_error(self, temp_dir):
        config = {
            "project": {"name": "MyApp", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"exclude": ["foobarbaz"]}}}}},
        }
        builder = WheelBuilder(str(temp_dir), config=config)

        for method in (
            builder.config.default_include,
            builder.config.default_exclude,
            builder.config.default_packages,
            builder.config.default_only_include,
        ):
            with pytest.raises(
                ValueError,
                match=(
                    "Unable to determine which files to ship inside the wheel using the following heuristics: "
                    "https://hatch.pypa.io/latest/plugins/builder/wheel/#default-file-selection\n\n"
                    "The most likely cause of this is that there is no directory that matches the name of your "
                    "project \\(MyApp or myapp\\).\n\n"
                    "At least one file selection option must be defined in the `tool.hatch.build.targets.wheel` "
                    "table, see: https://hatch.pypa.io/latest/config/build/\n\n"
                    "As an example, if you intend to ship a directory named `foo` that resides within a `src` "
                    "directory located at the root of your project, you can define the following:\n\n"
                    "\\[tool.hatch.build.targets.wheel\\]\n"
                    'packages = \\["src/foo"\\]'
                ),
            ):
                method()

    def test_bypass_selection_option(self, temp_dir):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"bypass-selection": True}}}}},
        }
        builder = WheelBuilder(str(temp_dir), config=config)

        assert builder.config.default_include() == []
        assert builder.config.default_exclude() == []
        assert builder.config.default_packages() == []
        assert builder.config.default_only_include() == []

    def test_force_include_option_considered_selection(self, temp_dir):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"force-include": {"foo": "bar"}}}}}},
        }
        builder = WheelBuilder(str(temp_dir), config=config)

        assert builder.config.default_include() == []
        assert builder.config.default_exclude() == []
        assert builder.config.default_packages() == []
        assert builder.config.default_only_include() == []

    def test_force_include_build_data_considered_selection(self, temp_dir):
        config = {"project": {"name": "my-app", "version": "0.0.1"}}
        builder = WheelBuilder(str(temp_dir), config=config)

        build_data = {"artifacts": [], "force_include": {"foo": "bar"}}
        with builder.config.set_build_data(build_data):
            assert builder.config.default_include() == []
            assert builder.config.default_exclude() == []
            assert builder.config.default_packages() == []
            assert builder.config.default_only_include() == []

    def test_artifacts_build_data_considered_selection(self, temp_dir):
        config = {"project": {"name": "my-app", "version": "0.0.1"}}
        builder = WheelBuilder(str(temp_dir), config=config)

        build_data = {"artifacts": ["foo"], "force_include": {}}
        with builder.config.set_build_data(build_data):
            assert builder.config.default_include() == []
            assert builder.config.default_exclude() == []
            assert builder.config.default_packages() == []
            assert builder.config.default_only_include() == []

    def test_unnormalized_name_with_unnormalized_directory(self, temp_dir):
        config = {"project": {"name": "MyApp", "version": "0.0.1"}}
        builder = WheelBuilder(str(temp_dir), config=config)

        src_root = temp_dir / "src" / "MyApp" / "__init__.py"
        src_root.ensure_parent_dir_exists()
        src_root.touch()

        assert builder.config.default_packages() == ["src/MyApp"]

    def test_unnormalized_name_with_normalized_directory(self, temp_dir):
        config = {"project": {"name": "MyApp", "version": "0.0.1"}}
        builder = WheelBuilder(str(temp_dir), config=config)

        src_root = temp_dir / "src" / "myapp" / "__init__.py"
        src_root.ensure_parent_dir_exists()
        src_root.touch()

        assert builder.config.default_packages() == ["src/myapp"]


class TestCoreMetadataConstructor:
    def test_default(self, isolation):
        builder = WheelBuilder(str(isolation))

        assert builder.config.core_metadata_constructor is builder.config.core_metadata_constructor
        assert builder.config.core_metadata_constructor is get_core_metadata_constructors()[DEFAULT_METADATA_VERSION]

    def test_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"core-metadata-version": 42}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError, match="Field `tool.hatch.build.targets.wheel.core-metadata-version` must be a string"
        ):
            _ = builder.config.core_metadata_constructor

    def test_unknown(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"core-metadata-version": "9000"}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match=(
                f"Unknown metadata version `9000` for field `tool.hatch.build.targets.wheel.core-metadata-version`. "
                f"Available: {', '.join(sorted(get_core_metadata_constructors()))}"
            ),
        ):
            _ = builder.config.core_metadata_constructor


class TestSharedData:
    def test_default(self, isolation):
        builder = WheelBuilder(str(isolation))

        assert builder.config.shared_data == builder.config.shared_data == {}

    def test_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-data": 42}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.wheel.shared-data` must be a mapping"):
            _ = builder.config.shared_data

    def test_absolute(self, isolation):
        config = {
            "tool": {
                "hatch": {"build": {"targets": {"wheel": {"shared-data": {str(isolation / "source"): "/target/"}}}}}
            }
        }
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.config.shared_data == {str(isolation / "source"): "target"}

    def test_relative(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-data": {"../source": "/target/"}}}}}}}
        builder = WheelBuilder(str(isolation / "foo"), config=config)

        assert builder.config.shared_data == {str(isolation / "source"): "target"}

    def test_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-data": {"": "/target/"}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match="Source #1 in field `tool.hatch.build.targets.wheel.shared-data` cannot be an empty string",
        ):
            _ = builder.config.shared_data

    def test_relative_path_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-data": {"source": 0}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError,
            match="Path for source `source` in field `tool.hatch.build.targets.wheel.shared-data` must be a string",
        ):
            _ = builder.config.shared_data

    def test_relative_path_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-data": {"source": ""}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match=(
                "Path for source `source` in field `tool.hatch.build.targets.wheel.shared-data` "
                "cannot be an empty string"
            ),
        ):
            _ = builder.config.shared_data

    def test_order(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "targets": {
                            "wheel": {
                                "shared-data": {
                                    "../very-nested": "target1/embedded",
                                    "../source1": "/target2/",
                                    "../source2": "/target1/",
                                }
                            }
                        }
                    }
                }
            }
        }
        builder = WheelBuilder(str(isolation / "foo"), config=config)

        assert builder.config.shared_data == {
            str(isolation / "source2"): "target1",
            str(isolation / "very-nested"): f"target1{os.sep}embedded",
            str(isolation / "source1"): "target2",
        }


class TestSharedScripts:
    def test_default(self, isolation):
        builder = WheelBuilder(str(isolation))

        assert builder.config.shared_scripts == builder.config.shared_scripts == {}

    def test_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-scripts": 42}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.wheel.shared-scripts` must be a mapping"):
            _ = builder.config.shared_scripts

    def test_absolute(self, isolation):
        config = {
            "tool": {
                "hatch": {"build": {"targets": {"wheel": {"shared-scripts": {str(isolation / "source"): "/target/"}}}}}
            }
        }
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.config.shared_scripts == {str(isolation / "source"): "target"}

    def test_relative(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-scripts": {"../source": "/target/"}}}}}}}
        builder = WheelBuilder(str(isolation / "foo"), config=config)

        assert builder.config.shared_scripts == {str(isolation / "source"): "target"}

    def test_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-scripts": {"": "/target/"}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match="Source #1 in field `tool.hatch.build.targets.wheel.shared-scripts` cannot be an empty string",
        ):
            _ = builder.config.shared_scripts

    def test_relative_path_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-scripts": {"source": 0}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError,
            match="Path for source `source` in field `tool.hatch.build.targets.wheel.shared-scripts` must be a string",
        ):
            _ = builder.config.shared_scripts

    def test_relative_path_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"shared-scripts": {"source": ""}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match=(
                "Path for source `source` in field `tool.hatch.build.targets.wheel.shared-scripts` "
                "cannot be an empty string"
            ),
        ):
            _ = builder.config.shared_scripts

    def test_order(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "targets": {
                            "wheel": {
                                "shared-scripts": {
                                    "../very-nested": "target1/embedded",
                                    "../source1": "/target2/",
                                    "../source2": "/target1/",
                                }
                            }
                        }
                    }
                }
            }
        }
        builder = WheelBuilder(str(isolation / "foo"), config=config)

        assert builder.config.shared_scripts == {
            str(isolation / "source2"): "target1",
            str(isolation / "very-nested"): f"target1{os.sep}embedded",
            str(isolation / "source1"): "target2",
        }


class TestExtraMetadata:
    def test_default(self, isolation):
        builder = WheelBuilder(str(isolation))

        assert builder.config.extra_metadata == builder.config.extra_metadata == {}

    def test_invalid_type(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"extra-metadata": 42}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.wheel.extra-metadata` must be a mapping"):
            _ = builder.config.extra_metadata

    def test_absolute(self, isolation):
        config = {
            "tool": {
                "hatch": {"build": {"targets": {"wheel": {"extra-metadata": {str(isolation / "source"): "/target/"}}}}}
            }
        }
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.config.extra_metadata == {str(isolation / "source"): "target"}

    def test_relative(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"extra-metadata": {"../source": "/target/"}}}}}}}
        builder = WheelBuilder(str(isolation / "foo"), config=config)

        assert builder.config.extra_metadata == {str(isolation / "source"): "target"}

    def test_source_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"extra-metadata": {"": "/target/"}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match="Source #1 in field `tool.hatch.build.targets.wheel.extra-metadata` cannot be an empty string",
        ):
            _ = builder.config.extra_metadata

    def test_relative_path_not_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"extra-metadata": {"source": 0}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError,
            match="Path for source `source` in field `tool.hatch.build.targets.wheel.extra-metadata` must be a string",
        ):
            _ = builder.config.extra_metadata

    def test_relative_path_empty_string(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"extra-metadata": {"source": ""}}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            ValueError,
            match=(
                "Path for source `source` in field `tool.hatch.build.targets.wheel.extra-metadata` "
                "cannot be an empty string"
            ),
        ):
            _ = builder.config.extra_metadata

    def test_order(self, isolation):
        config = {
            "tool": {
                "hatch": {
                    "build": {
                        "targets": {
                            "wheel": {
                                "extra-metadata": {
                                    "../very-nested": "target1/embedded",
                                    "../source1": "/target2/",
                                    "../source2": "/target1/",
                                }
                            }
                        }
                    }
                }
            }
        }
        builder = WheelBuilder(str(isolation / "foo"), config=config)

        assert builder.config.extra_metadata == {
            str(isolation / "source2"): "target1",
            str(isolation / "very-nested"): f"target1{os.sep}embedded",
            str(isolation / "source1"): "target2",
        }


class TestStrictNaming:
    def test_default(self, isolation):
        builder = WheelBuilder(str(isolation))

        assert builder.config.strict_naming is builder.config.strict_naming is True

    def test_target(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"strict-naming": False}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.config.strict_naming is False

    def test_target_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"strict-naming": 9000}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.wheel.strict-naming` must be a boolean"):
            _ = builder.config.strict_naming

    def test_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"strict-naming": False}}}}
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.config.strict_naming is False

    def test_global_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"strict-naming": 9000}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.strict-naming` must be a boolean"):
            _ = builder.config.strict_naming

    def test_target_overrides_global(self, isolation):
        config = {"tool": {"hatch": {"build": {"strict-naming": False, "targets": {"wheel": {"strict-naming": True}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.config.strict_naming is True


class TestMacOSMaxCompat:
    def test_default(self, isolation):
        builder = WheelBuilder(str(isolation))

        assert builder.config.macos_max_compat is builder.config.macos_max_compat is False

    def test_correct(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"macos-max-compat": True}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.config.macos_max_compat is True

    def test_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"macos-max-compat": 9000}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError, match="Field `tool.hatch.build.targets.wheel.macos-max-compat` must be a boolean"
        ):
            _ = builder.config.macos_max_compat


class TestBypassSelection:
    def test_default(self, isolation):
        builder = WheelBuilder(str(isolation))

        assert builder.config.bypass_selection is False

    def test_correct(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"bypass-selection": True}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.config.bypass_selection is True

    def test_not_boolean(self, isolation):
        config = {"tool": {"hatch": {"build": {"targets": {"wheel": {"bypass-selection": 9000}}}}}}
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError, match="Field `tool.hatch.build.targets.wheel.bypass-selection` must be a boolean"
        ):
            _ = builder.config.bypass_selection


class TestConstructEntryPointsFile:
    def test_default(self, isolation):
        config = {"project": {}}
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.construct_entry_points_file() == ""

    def test_scripts(self, isolation, helpers):
        config = {"project": {"scripts": {"foo": "pkg:bar", "bar": "pkg:foo"}}}
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.construct_entry_points_file() == helpers.dedent(
            """
            [console_scripts]
            bar = pkg:foo
            foo = pkg:bar
            """
        )

    def test_gui_scripts(self, isolation, helpers):
        config = {"project": {"gui-scripts": {"foo": "pkg:bar", "bar": "pkg:foo"}}}
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.construct_entry_points_file() == helpers.dedent(
            """
            [gui_scripts]
            bar = pkg:foo
            foo = pkg:bar
            """
        )

    def test_entry_points(self, isolation, helpers):
        config = {
            "project": {
                "entry-points": {
                    "foo": {"bar": "pkg:foo", "foo": "pkg:bar"},
                    "bar": {"foo": "pkg:bar", "bar": "pkg:foo"},
                }
            }
        }
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.construct_entry_points_file() == helpers.dedent(
            """
            [bar]
            bar = pkg:foo
            foo = pkg:bar

            [foo]
            bar = pkg:foo
            foo = pkg:bar
            """
        )

    def test_all(self, isolation, helpers):
        config = {
            "project": {
                "scripts": {"foo": "pkg:bar", "bar": "pkg:foo"},
                "gui-scripts": {"foo": "pkg:bar", "bar": "pkg:foo"},
                "entry-points": {
                    "foo": {"bar": "pkg:foo", "foo": "pkg:bar"},
                    "bar": {"foo": "pkg:bar", "bar": "pkg:foo"},
                },
            }
        }
        builder = WheelBuilder(str(isolation), config=config)

        assert builder.construct_entry_points_file() == helpers.dedent(
            """
            [console_scripts]
            bar = pkg:foo
            foo = pkg:bar

            [gui_scripts]
            bar = pkg:foo
            foo = pkg:bar

            [bar]
            bar = pkg:foo
            foo = pkg:bar

            [foo]
            bar = pkg:foo
            foo = pkg:bar
            """
        )


class TestBuildStandard:
    def test_default_auto_detection(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"

        with project_path.as_cwd():
            artifacts = list(builder.build())

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_license_single", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    def test_default_reproducible_timestamp(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd(env_vars={"SOURCE_DATE_EPOCH": "1580601700"}):
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_license_single", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 1, 40)

    def test_default_no_reproducible(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "reproducible": False}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd(env_vars={"SOURCE_DATE_EPOCH": "1580601700"}):
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_license_single", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    def test_default_multiple_licenses(self, hatch, helpers, config_file, temp_dir):
        project_name = "My.App"
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.model.template.licenses.default = ["MIT", "Apache-2.0"]
        config_file.save()

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        # Ensure that we trigger the non-file case for code coverage
        (project_path / "LICENSES" / "test").mkdir()

        config = {
            "project": {"name": project_name, "dynamic": ["version"], "license-files": ["LICENSES/*"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_license_multiple", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_include(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "include": ["my_app", "tests"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_tests", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_only_packages(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        tests_path = project_path / "tests"
        (tests_path / "__init__.py").replace(tests_path / "foo.py")

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {
                            "wheel": {"versions": ["standard"], "include": ["my_app", "tests"], "only-packages": True}
                        },
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_license_single", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_only_packages_artifact_override(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        tests_path = project_path / "tests"
        (tests_path / "__init__.py").replace(tests_path / "foo.py")

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "artifacts": ["foo.py"],
                        "targets": {
                            "wheel": {"versions": ["standard"], "include": ["my_app", "tests"], "only-packages": True}
                        },
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_only_packages_artifact_override", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

    @pytest.mark.parametrize(
        ("python_constraint", "expected_template_file"),
        [
            pytest.param(">3", "wheel.standard_default_python_constraint", id=">3"),
            pytest.param("==3.11.4", "wheel.standard_default_python_constraint_three_components", id="==3.11.4"),
        ],
    )
    def test_default_python_constraint(
        self, hatch, helpers, temp_dir, config_file, python_constraint, expected_template_file
    ):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        config = {
            "project": {"name": project_name, "requires-python": python_constraint, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-py3-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            expected_template_file, project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_default_tag(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    pass
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        tag = "py3-none-any"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script", project_name, metadata_directory=metadata_directory, tag=tag
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_set_tag(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['tag'] = 'foo-bar-baz'
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        tag = "foo-bar-baz"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script", project_name, metadata_directory=metadata_directory, tag=tag
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_known_artifacts(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True

                        pathlib.Path('my_app', 'lib.so').touch()
                        pathlib.Path('my_app', 'lib.h').touch()
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "artifacts": ["my_app/lib.so"],
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        best_matching_tag = next(sys_tags())
        tag = f"{best_matching_tag.interpreter}-{best_matching_tag.abi}-{best_matching_tag.platform}"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_artifacts",
            project_name,
            metadata_directory=metadata_directory,
            tag=tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_configured_build_hooks(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True

                        pathlib.Path('my_app', 'lib.so').write_text(','.join(build_data['build_hooks']))
                        pathlib.Path('my_app', 'lib.h').write_text(','.join(build_data['build_hooks']))
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "artifacts": ["my_app/lib.so"],
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        best_matching_tag = next(sys_tags())
        tag = f"{best_matching_tag.interpreter}-{best_matching_tag.abi}-{best_matching_tag.platform}"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_configured_build_hooks",
            project_name,
            metadata_directory=metadata_directory,
            tag=tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_extra_dependencies(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True
                        build_data['dependencies'].append('binary')

                        pathlib.Path('my_app', 'lib.so').touch()
                        pathlib.Path('my_app', 'lib.h').touch()
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "artifacts": ["my_app/lib.so"],
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        best_matching_tag = next(sys_tags())
        tag = f"{best_matching_tag.interpreter}-{best_matching_tag.abi}-{best_matching_tag.platform}"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_extra_dependencies",
            project_name,
            metadata_directory=metadata_directory,
            tag=tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_dynamic_artifacts(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True
                        build_data['artifacts'] = ['my_app/lib.so']

                        pathlib.Path('my_app', 'lib.so').touch()
                        pathlib.Path('my_app', 'lib.h').touch()
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        best_matching_tag = next(sys_tags())
        tag = f"{best_matching_tag.interpreter}-{best_matching_tag.abi}-{best_matching_tag.platform}"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_artifacts",
            project_name,
            metadata_directory=metadata_directory,
            tag=tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_dynamic_force_include(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True
                        build_data['artifacts'].extend(('lib.so', 'lib.h'))
                        build_data['force_include']['../artifacts'] = 'my_app'

                        artifact_path = pathlib.Path('..', 'artifacts')
                        artifact_path.mkdir()
                        (artifact_path / 'lib.so').touch()
                        (artifact_path / 'lib.h').touch()
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        best_matching_tag = next(sys_tags())
        tag = f"{best_matching_tag.interpreter}-{best_matching_tag.abi}-{best_matching_tag.platform}"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_force_include",
            project_name,
            metadata_directory=metadata_directory,
            tag=tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_dynamic_force_include_duplicate(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        target_file = project_path / "my_app" / "z.py"
        target_file.write_text('print("hello world")')

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True
                        build_data['force_include']['../tmp/new_z.py'] = 'my_app/z.py'

                        tmp_path = pathlib.Path('..', 'tmp')
                        tmp_path.mkdir()
                        (tmp_path / 'new_z.py').write_bytes(pathlib.Path('my_app/z.py').read_bytes())
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        best_matching_tag = next(sys_tags())
        tag = f"{best_matching_tag.interpreter}-{best_matching_tag.abi}-{best_matching_tag.platform}"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_force_include_no_duplication",
            project_name,
            metadata_directory=metadata_directory,
            tag=tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_build_script_dynamic_artifacts_with_src_layout(self, hatch, helpers, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.pyd\n*.h")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True
                        build_data['artifacts'] = ['src/my_app/lib.so']
                        build_data['force_include']['src/zlib.pyd'] = 'src/zlib.pyd'

                        pathlib.Path('src', 'my_app', 'lib.so').touch()
                        pathlib.Path('src', 'lib.h').touch()
                        pathlib.Path('src', 'zlib.pyd').touch()
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        best_matching_tag = next(sys_tags())
        tag = f"{best_matching_tag.interpreter}-{best_matching_tag.abi}-{best_matching_tag.platform}"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_artifacts_with_src_layout",
            project_name,
            metadata_directory=metadata_directory,
            tag=tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_shared_data(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        shared_data_path = temp_dir / "data"
        shared_data_path.ensure_dir_exists()
        (shared_data_path / "foo.txt").touch()
        nested_data_path = shared_data_path / "nested"
        nested_data_path.ensure_dir_exists()
        (nested_data_path / "bar.txt").touch()

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "shared-data": {"../data": "/"}}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        shared_data_directory = f"{builder.project_id}.data"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_shared_data",
            project_name,
            metadata_directory=metadata_directory,
            shared_data_directory=shared_data_directory,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_shared_data_from_build_data(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        shared_data_path = temp_dir / "data"
        shared_data_path.ensure_dir_exists()
        (shared_data_path / "foo.txt").touch()
        nested_data_path = shared_data_path / "nested"
        nested_data_path.ensure_dir_exists()
        (nested_data_path / "bar.txt").touch()

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['shared_data']['../data'] = '/'
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "hooks": {"custom": {}}}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        shared_data_directory = f"{builder.project_id}.data"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_shared_data",
            project_name,
            metadata_directory=metadata_directory,
            shared_data_directory=shared_data_directory,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_shared_scripts(self, hatch, platform, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        shared_data_path = temp_dir / "data"
        shared_data_path.ensure_dir_exists()

        binary_contents = os.urandom(1024)
        binary_file = shared_data_path / "binary"
        binary_file.write_bytes(binary_contents)
        if not platform.windows:
            expected_mode = 0o755
            binary_file.chmod(expected_mode)

        (shared_data_path / "other_script.sh").write_text(
            helpers.dedent(
                """

                #!/bin/sh arg1 arg2
                echo "Hello, World!"
                """
            )
        )
        (shared_data_path / "python_script.sh").write_text(
            helpers.dedent(
                """

                #!/usr/bin/env python3.11 arg1 arg2
                print("Hello, World!")
                """
            )
        )
        (shared_data_path / "pythonw_script.sh").write_text(
            helpers.dedent(
                """

                #!/usr/bin/pythonw3.11 arg1 arg2
                print("Hello, World!")
                """
            )
        )
        (shared_data_path / "pypy_script.sh").write_text(
            helpers.dedent(
                """

                #!/usr/bin/env pypy
                print("Hello, World!")
                """
            )
        )
        (shared_data_path / "pypyw_script.sh").write_text(
            helpers.dedent(
                """

                #!pypyw3.11 arg1 arg2
                print("Hello, World!")
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "shared-scripts": {"../data": "/"}}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        extraction_directory = temp_dir / "_archive"
        extract_zip(expected_artifact, extraction_directory)

        metadata_directory = f"{builder.project_id}.dist-info"
        shared_data_directory = f"{builder.project_id}.data"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_shared_scripts",
            project_name,
            metadata_directory=metadata_directory,
            shared_data_directory=shared_data_directory,
            binary_contents=binary_contents,
        )
        helpers.assert_files(extraction_directory, expected_files)

        if not platform.windows:
            extracted_binary = extraction_directory / shared_data_directory / "scripts" / "binary"
            assert extracted_binary.stat().st_mode & 0o777 == expected_mode

    def test_default_shared_scripts_from_build_data(self, hatch, platform, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        shared_data_path = temp_dir / "data"
        shared_data_path.ensure_dir_exists()

        binary_contents = os.urandom(1024)
        binary_file = shared_data_path / "binary"
        binary_file.write_bytes(binary_contents)
        if not platform.windows:
            expected_mode = 0o755
            binary_file.chmod(expected_mode)

        (shared_data_path / "other_script.sh").write_text(
            helpers.dedent(
                """

                #!/bin/sh arg1 arg2
                echo "Hello, World!"
                """
            )
        )
        (shared_data_path / "python_script.sh").write_text(
            helpers.dedent(
                """

                #!/usr/bin/env python3.11 arg1 arg2
                print("Hello, World!")
                """
            )
        )
        (shared_data_path / "pythonw_script.sh").write_text(
            helpers.dedent(
                """

                #!/usr/bin/pythonw3.11 arg1 arg2
                print("Hello, World!")
                """
            )
        )
        (shared_data_path / "pypy_script.sh").write_text(
            helpers.dedent(
                """

                #!/usr/bin/env pypy
                print("Hello, World!")
                """
            )
        )
        (shared_data_path / "pypyw_script.sh").write_text(
            helpers.dedent(
                """

                #!pypyw3.11 arg1 arg2
                print("Hello, World!")
                """
            )
        )

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['shared_scripts']['../data'] = '/'
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "hooks": {"custom": {}}}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        extraction_directory = temp_dir / "_archive"
        extract_zip(expected_artifact, extraction_directory)

        metadata_directory = f"{builder.project_id}.dist-info"
        shared_data_directory = f"{builder.project_id}.data"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_shared_scripts",
            project_name,
            metadata_directory=metadata_directory,
            shared_data_directory=shared_data_directory,
            binary_contents=binary_contents,
        )
        helpers.assert_files(extraction_directory, expected_files)

        if not platform.windows:
            extracted_binary = extraction_directory / shared_data_directory / "scripts" / "binary"
            assert extracted_binary.stat().st_mode & 0o777 == expected_mode

    def test_default_extra_metadata(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        extra_metadata_path = temp_dir / "data"
        extra_metadata_path.ensure_dir_exists()
        (extra_metadata_path / "foo.txt").touch()
        nested_data_path = extra_metadata_path / "nested"
        nested_data_path.ensure_dir_exists()
        (nested_data_path / "bar.txt").touch()

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "extra-metadata": {"../data": "/"}}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_extra_metadata",
            project_name,
            metadata_directory=metadata_directory,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_extra_metadata_build_data(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        extra_metadata_path = temp_dir / "data"
        extra_metadata_path.ensure_dir_exists()
        (extra_metadata_path / "foo.txt").touch()
        nested_data_path = extra_metadata_path / "nested"
        nested_data_path.ensure_dir_exists()
        (nested_data_path / "bar.txt").touch()

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['extra_metadata']['../data'] = '/'
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "hooks": {"custom": {}}}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_extra_metadata",
            project_name,
            metadata_directory=metadata_directory,
        )
        helpers.assert_files(extraction_directory, expected_files)

    @pytest.mark.requires_unix
    def test_default_symlink(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        (temp_dir / "foo.so").write_bytes(b"data")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import os
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True

                        pathlib.Path('my_app', 'lib.so').symlink_to(os.path.abspath(os.path.join('..', 'foo.so')))
                        pathlib.Path('my_app', 'lib.h').touch()
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "artifacts": ["my_app/lib.so"],
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        best_matching_tag = next(sys_tags())
        tag = f"{best_matching_tag.interpreter}-{best_matching_tag.abi}-{best_matching_tag.platform}"
        assert expected_artifact == str(build_path / f"{builder.project_id}-{tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_symlink",
            project_name,
            metadata_directory=metadata_directory,
            tag=tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    @fixed_pathlib_resolution
    def test_editable_default(self, hatch, helpers, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["editable"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_pth",
            project_name,
            metadata_directory=metadata_directory,
            package_paths=[str(project_path / "src")],
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_default_extra_dependencies(self, hatch, helpers, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['dependencies'].append('binary')
                """
            )
        )

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["editable"], "hooks": {"custom": {}}}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_pth_extra_dependencies",
            project_name,
            metadata_directory=metadata_directory,
            package_paths=[str(project_path / "src")],
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_default_force_include(self, hatch, helpers, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        # Prefix z just to satisfy our ordering test assertion
                        build_data['force_include_editable']['src/my_app/__about__.py'] = 'zfoo.py'
                """
            )
        )

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["editable"], "hooks": {"custom": {}}}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_pth_force_include",
            project_name,
            metadata_directory=metadata_directory,
            package_paths=[str(project_path / "src")],
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_default_force_include_option(self, hatch, helpers, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {
                        "targets": {
                            "wheel": {
                                "versions": ["editable"],
                                "force-include": {"src/my_app/__about__.py": "zfoo.py"},
                            }
                        }
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_pth_force_include",
            project_name,
            metadata_directory=metadata_directory,
            package_paths=[str(project_path / "src")],
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @pytest.mark.requires_unix
    def test_editable_default_symlink(self, hatch, helpers, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        symlink = project_path / "_" / "my_app"
        symlink.parent.ensure_dir_exists()
        symlink.symlink_to(project_path / "src" / "my_app")

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["editable"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_pth",
            project_name,
            metadata_directory=metadata_directory,
            package_paths=[str(project_path / "src")],
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_exact(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["editable"], "dev-mode-exact": True}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_exact",
            project_name,
            metadata_directory=metadata_directory,
            package_root=str(project_path / "my_app" / "__init__.py"),
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_exact_extra_dependencies(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['dependencies'].append('binary')
                """
            )
        )

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {
                            "wheel": {"versions": ["editable"], "dev-mode-exact": True, "hooks": {"custom": {}}}
                        }
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_exact_extra_dependencies",
            project_name,
            metadata_directory=metadata_directory,
            package_root=str(project_path / "my_app" / "__init__.py"),
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_exact_force_include(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        # Prefix z just to satisfy our ordering test assertion
                        build_data['force_include_editable']['my_app/__about__.py'] = 'zfoo.py'
                """
            )
        )

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {
                            "wheel": {"versions": ["editable"], "dev-mode-exact": True, "hooks": {"custom": {}}}
                        }
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_exact_force_include",
            project_name,
            metadata_directory=metadata_directory,
            package_root=str(project_path / "my_app" / "__init__.py"),
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_exact_force_include_option(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {
                            "wheel": {
                                "versions": ["editable"],
                                "dev-mode-exact": True,
                                "force-include": {"my_app/__about__.py": "zfoo.py"},
                            }
                        }
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_exact_force_include",
            project_name,
            metadata_directory=metadata_directory,
            package_root=str(project_path / "my_app" / "__init__.py"),
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_exact_force_include_build_data_precedence(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        # Prefix z just to satisfy our ordering test assertion
                        build_data['force_include_editable']['my_app/__about__.py'] = 'zfoo.py'
                """
            )
        )

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {
                            "wheel": {
                                "versions": ["editable"],
                                "dev-mode-exact": True,
                                "force-include": {"my_app/__about__.py": "zbar.py"},
                                "hooks": {"custom": {}},
                            }
                        }
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_exact_force_include",
            project_name,
            metadata_directory=metadata_directory,
            package_root=str(project_path / "my_app" / "__init__.py"),
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    @fixed_pathlib_resolution
    def test_editable_pth(self, hatch, helpers, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["editable"], "dev-mode-dirs": ["."]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_editable_pth",
            project_name,
            metadata_directory=metadata_directory,
            package_paths=[str(project_path)],
        )
        helpers.assert_files(extraction_directory, expected_files)

        # Inspect the archive rather than the extracted files because on Windows they lose their metadata
        # https://stackoverflow.com/q/9813243
        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_info = zip_archive.getinfo(f"{metadata_directory}/WHEEL")
            assert zip_info.date_time == (2020, 2, 2, 0, 0, 0)

    def test_default_namespace_package(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        package_path = project_path / "my_app"
        namespace_path = project_path / "namespace"
        namespace_path.mkdir()
        package_path.replace(namespace_path / "my_app")

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "namespace/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"

        with project_path.as_cwd():
            artifacts = list(builder.build())

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_namespace_package",
            project_name,
            metadata_directory=metadata_directory,
            namespace="namespace",
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_default_entry_points(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"], "scripts": {"foo": "pkg:bar", "bar": "pkg:foo"}},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"]}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"

        with project_path.as_cwd():
            artifacts = list(builder.build())

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(build_path / f"{builder.project_id}-{get_python_versions_tag()}-none-any.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_entry_points", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_explicit_selection_with_src_layout(self, hatch, helpers, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {
                        "targets": {
                            "wheel": {
                                "versions": ["standard"],
                                "artifacts": ["README.md"],
                                "only-include": ["src/my_app"],
                                "sources": ["src"],
                            }
                        },
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_license_single",
            project_name,
            metadata_directory=metadata_directory,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_single_module(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        (project_path / "my_app").remove()
        (project_path / "my_app.py").touch()

        config = {"project": {"name": project_name, "version": "0.0.1"}}
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_single_module",
            project_name,
            metadata_directory=metadata_directory,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_no_strict_naming(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "strict-naming": False}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"

        with project_path.as_cwd():
            artifacts = list(builder.build())

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(
            build_path / f"{builder.artifact_project_id}-{get_python_versions_tag()}-none-any.whl"
        )

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.artifact_project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_no_strict_naming", project_name, metadata_directory=metadata_directory
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_editable_sources_rewrite_error(self, hatch, temp_dir):
        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {
                        "targets": {
                            "wheel": {
                                "versions": ["editable"],
                                "only-include": ["src/my_app"],
                                "sources": {"src/my_app": "namespace/plugins/my_app"},
                            }
                        },
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with (
            project_path.as_cwd(),
            pytest.raises(
                ValueError,
                match=(
                    "Dev mode installations are unsupported when any path rewrite in the `sources` option "
                    "changes a prefix rather than removes it, see: "
                    "https://github.com/pfmoore/editables/issues/20"
                ),
            ),
        ):
            list(builder.build(directory=str(build_path)))

    @pytest.mark.skipif(
        sys.platform != "darwin" or sys.version_info < (3, 8),
        reason="requires support for ARM on macOS",
    )
    @pytest.mark.parametrize(
        ("archflags", "expected_arch"),
        [("-arch x86_64", "x86_64"), ("-arch arm64", "arm64"), ("-arch arm64 -arch x86_64", "universal2")],
    )
    def test_macos_archflags(self, hatch, helpers, temp_dir, config_file, archflags, expected_arch):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True

                        pathlib.Path('my_app', 'lib.so').touch()
                        pathlib.Path('my_app', 'lib.h').touch()
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"]}},
                        "artifacts": ["my_app/lib.so"],
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd({"ARCHFLAGS": archflags}):
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        tag = next(sys_tags())
        tag_parts = [tag.interpreter, tag.abi, tag.platform]
        tag_parts[2] = tag_parts[2].replace(platform.mac_ver()[2], expected_arch)
        expected_tag = "-".join(tag_parts)
        assert expected_artifact == str(build_path / f"{builder.project_id}-{expected_tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_artifacts",
            project_name,
            metadata_directory=metadata_directory,
            tag=expected_tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    @pytest.mark.requires_macos
    @pytest.mark.parametrize("macos_max_compat", [True, False])
    def test_macos_max_compat(self, hatch, helpers, temp_dir, config_file, macos_max_compat):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        vcs_ignore_file = project_path / ".gitignore"
        vcs_ignore_file.write_text("*.pyc\n*.so\n*.h")

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data['pure_python'] = False
                        build_data['infer_tag'] = True

                        pathlib.Path('my_app', 'lib.so').touch()
                        pathlib.Path('my_app', 'lib.h').touch()
                """
            )
        )

        config = {
            "project": {"name": project_name, "requires-python": ">3", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"versions": ["standard"], "macos-max-compat": macos_max_compat}},
                        "artifacts": ["my_app/lib.so"],
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])

        tag = next(sys_tags())
        tag_parts = [tag.interpreter, tag.abi, tag.platform]
        if macos_max_compat:
            sdk_version_major, sdk_version_minor = tag_parts[2].split("_")[1:3]
            if int(sdk_version_major) >= 11:
                tag_parts[2] = tag_parts[2].replace(f"{sdk_version_major}_{sdk_version_minor}", "10_16", 1)

        expected_tag = "-".join(tag_parts)
        assert expected_artifact == str(build_path / f"{builder.project_id}-{expected_tag}.whl")

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(expected_artifact), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_build_script_artifacts",
            project_name,
            metadata_directory=metadata_directory,
            tag=expected_tag,
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_file_permissions_normalized(self, hatch, temp_dir, config_file):
        config_file.model.template.plugins["default"]["src-layout"] = False
        config_file.save()

        project_name = "My.App"

        with temp_dir.as_cwd():
            result = hatch("new", project_name)

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"

        config = {
            "project": {"name": project_name, "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"versions": ["standard"], "strict-naming": False}}},
                },
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"

        with project_path.as_cwd():
            artifacts = list(builder.build())

        assert len(artifacts) == 1
        expected_artifact = artifacts[0]

        build_artifacts = list(build_path.iterdir())
        assert len(build_artifacts) == 1
        assert expected_artifact == str(build_artifacts[0])
        assert expected_artifact == str(
            build_path / f"{builder.artifact_project_id}-{get_python_versions_tag()}-none-any.whl"
        )
        file_stat = os.stat(expected_artifact)
        # we assert that at minimum 644 is set, based on the platform (e.g.)
        # windows it may be higher
        assert file_stat.st_mode & 0o644


class TestSBOMFiles:
    def test_single_sbom_file(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["tests"] = False
        config_file.save()

        with temp_dir.as_cwd():
            result = hatch("new", "My.App")

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        sbom_file = project_path / "my-sbom.spdx.json"
        sbom_file.write_text('{"spdxVersion": "SPDX-2.3"}')

        config = {
            "project": {"name": "My.App", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"sbom-files": ["my-sbom.spdx.json"]}}},
                }
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(artifacts[0]), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_sbom",
            "My.App",
            metadata_directory=metadata_directory,
            sbom_files=[("my-sbom.spdx.json", '{"spdxVersion": "SPDX-2.3"}')],
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_multiple_sbom_files(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["tests"] = False
        config_file.save()

        with temp_dir.as_cwd():
            result = hatch("new", "My.App")

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        (project_path / "sbom1.spdx.json").write_text('{"spdxVersion": "SPDX-2.3"}')
        (project_path / "sbom2.cyclonedx.json").write_text('{"bomFormat": "CycloneDX"}')

        config = {
            "project": {"name": "My.App", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"sbom-files": ["sbom1.spdx.json", "sbom2.cyclonedx.json"]}}},
                }
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(artifacts[0]), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_sbom",
            "My.App",
            metadata_directory=metadata_directory,
            sbom_files=[
                ("sbom1.spdx.json", '{"spdxVersion": "SPDX-2.3"}'),
                ("sbom2.cyclonedx.json", '{"bomFormat": "CycloneDX"}'),
            ],
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_nested_sbom_file(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["tests"] = False
        config_file.save()

        with temp_dir.as_cwd():
            result = hatch("new", "My.App")

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        sbom_dir = project_path / "sboms"
        sbom_dir.mkdir()
        (sbom_dir / "vendor.spdx.json").write_text('{"spdxVersion": "SPDX-2.3"}')

        config = {
            "project": {"name": "My.App", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {"targets": {"wheel": {"sbom-files": ["sboms/vendor.spdx.json"]}}},
                }
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(artifacts[0]), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_sbom",
            "My.App",
            metadata_directory=metadata_directory,
            sbom_files=[("vendor.spdx.json", '{"spdxVersion": "SPDX-2.3"}')],
        )
        helpers.assert_files(extraction_directory, expected_files)

    def test_sbom_files_invalid_type(self, isolation):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"sbom-files": "not-a-list"}}}}},
        }
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(TypeError, match="Field `tool.hatch.build.targets.wheel.sbom-files` must be an array"):
            _ = builder.config.sbom_files

    def test_sbom_file_invalid_item(self, isolation):
        config = {
            "project": {"name": "my-app", "version": "0.0.1"},
            "tool": {"hatch": {"build": {"targets": {"wheel": {"sbom-files": [123]}}}}},
        }
        builder = WheelBuilder(str(isolation), config=config)

        with pytest.raises(
            TypeError, match="SBOM file #1 in field `tool.hatch.build.targets.wheel.sbom-files` must be a string"
        ):
            _ = builder.config.sbom_files

    def test_sbom_from_build_data(self, hatch, helpers, temp_dir, config_file):
        config_file.model.template.plugins["default"]["tests"] = False
        config_file.save()

        with temp_dir.as_cwd():
            result = hatch("new", "My.App")

        assert result.exit_code == 0, result.output

        project_path = temp_dir / "my-app"
        (project_path / "sbom1.cyclonedx.json").write_text('{"bomFormat": "CycloneDX"}')
        (project_path / "sbom2.spdx.json").write_text('{"spdxVersion": "SPDX-2.3"}')

        build_script = project_path / DEFAULT_BUILD_SCRIPT
        build_script.write_text(
            helpers.dedent(
                """
                import pathlib

                from hatchling.builders.hooks.plugin.interface import BuildHookInterface

                class CustomHook(BuildHookInterface):
                    def initialize(self, version, build_data):
                        build_data["sbom_files"].append("sbom2.spdx.json")
                """
            )
        )

        config = {
            "project": {"name": "My.App", "dynamic": ["version"]},
            "tool": {
                "hatch": {
                    "version": {"path": "src/my_app/__about__.py"},
                    "build": {
                        "targets": {"wheel": {"sbom-files": ["sbom1.cyclonedx.json"]}},
                        "hooks": {"custom": {"path": DEFAULT_BUILD_SCRIPT}},
                    },
                }
            },
        }
        builder = WheelBuilder(str(project_path), config=config)

        build_path = project_path / "dist"
        build_path.mkdir()

        with project_path.as_cwd():
            artifacts = list(builder.build(directory=str(build_path)))

        assert len(artifacts) == 1

        extraction_directory = temp_dir / "_archive"
        extraction_directory.mkdir()

        with zipfile.ZipFile(str(artifacts[0]), "r") as zip_archive:
            zip_archive.extractall(str(extraction_directory))

        metadata_directory = f"{builder.project_id}.dist-info"
        expected_files = helpers.get_template_files(
            "wheel.standard_default_sbom",
            "My.App",
            metadata_directory=metadata_directory,
            sbom_files=[
                ("sbom1.cyclonedx.json", '{"bomFormat": "CycloneDX"}'),
                ("sbom2.spdx.json", '{"spdxVersion": "SPDX-2.3"}'),
            ],
        )
        helpers.assert_files(extraction_directory, expected_files)
