diff --git a/poetry.lock b/poetry.lock
index 02dc2635f97..8f8122c985c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -28,6 +28,27 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
 tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
 tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
 
+[[package]]
+name = "build"
+version = "0.4.0"
+description = "A simple, correct PEP517 package builder"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "os_name == \"nt\""}
+importlib-metadata = {version = ">=0.22", markers = "python_version < \"3.8\""}
+packaging = ">=19.0"
+pep517 = ">=0.9.1"
+toml = ">=0.10.0"
+
+[package.extras]
+docs = ["furo (>=2020.11.19b18)", "sphinx (>=3.0,<4.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"]
+test = ["filelock (>=3)", "pytest (>=4)", "pytest-cov (>=2)", "pytest-mock (>=2)", "pytest-xdist (>=1.34)"]
+typing = ["mypy (==0.800)", "typing-extensions (>=3.7.4.3)"]
+virtualenv = ["virtualenv (>=20.0.35)"]
+
 [[package]]
 name = "cachecontrol"
 version = "0.12.6"
@@ -112,7 +133,7 @@ pylev = ">=1.3.0,<2.0.0"
 name = "colorama"
 version = "0.4.4"
 description = "Cross-platform colored terminal text."
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
@@ -283,6 +304,17 @@ category = "dev"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "installer"
+version = "0.2.3"
+description = "A library for installing Python wheels."
+category = "main"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+
+[package.dependencies]
+importlib-resources = {version = "*", markers = "python_version < \"3.7\""}
+
 [[package]]
 name = "jeepney"
 version = "0.7.1"
@@ -356,6 +388,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 [package.dependencies]
 pyparsing = ">=2.0.2"
 
+[[package]]
+name = "pep517"
+version = "0.10.0"
+description = "Wrappers to build Python packages using PEP 517 hooks"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+importlib_metadata = {version = "*", markers = "python_version < \"3.8\""}
+toml = "*"
+zipp = {version = "*", markers = "python_version < \"3.8\""}
+
 [[package]]
 name = "pexpect"
 version = "4.8.0"
@@ -612,7 +657,7 @@ python-versions = "*"
 name = "toml"
 version = "0.10.2"
 description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
@@ -703,7 +748,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6"
-content-hash = "458035eaa19225869083c98c699e753de1844dd82030447634836e2f79255df1"
+content-hash = "575f9d8b6c311a8238cf5fe5e665e0b7101addd1a3dd26813870165d41e871f8"
 
 [metadata.files]
 appdirs = [
@@ -718,6 +763,10 @@ attrs = [
     {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
     {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
 ]
+build = [
+    {file = "build-0.4.0-py2.py3-none-any.whl", hash = "sha256:5950f98775a59f0c5ac68586691003d2db58a809fbea2ade3fe32109dfd12790"},
+    {file = "build-0.4.0.tar.gz", hash = "sha256:b798f3f490c779fa88c99816ebee97ab636acd6630b1d91c8cf8eb8a4d922a19"},
+]
 cachecontrol = [
     {file = "CacheControl-0.12.6-py2.py3-none-any.whl", hash = "sha256:10d056fa27f8563a271b345207402a6dcce8efab7e5b377e270329c62471b10d"},
     {file = "CacheControl-0.12.6.tar.gz", hash = "sha256:be9aa45477a134aee56c8fac518627e1154df063e85f67d4f83ce0ccc23688e8"},
@@ -912,6 +961,10 @@ iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
 ]
+installer = [
+    {file = "installer-0.2.3-py2.py3-none-any.whl", hash = "sha256:e649b9c7a454708a33a39db69f8daee515c9345020ab2fb2299d6639aad65711"},
+    {file = "installer-0.2.3.tar.gz", hash = "sha256:82c899f5e3c78303242df9c9ca7ac58001c9806d8c23fa2772be769d1f560fe5"},
+]
 jeepney = [
     {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"},
     {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"},
@@ -965,6 +1018,10 @@ packaging = [
     {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
     {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
 ]
+pep517 = [
+    {file = "pep517-0.10.0-py2.py3-none-any.whl", hash = "sha256:eba39d201ef937584ad3343df3581069085bacc95454c80188291d5b3ac7a249"},
+    {file = "pep517-0.10.0.tar.gz", hash = "sha256:ac59f3f6b9726a49e15a649474539442cf76e0697e39df4869d25e68e880931b"},
+]
 pexpect = [
     {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
     {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
diff --git a/poetry/console/commands/source/add.py b/poetry/console/commands/source/add.py
index 1b097076b84..be83cb6df15 100644
--- a/poetry/console/commands/source/add.py
+++ b/poetry/console/commands/source/add.py
@@ -11,7 +11,6 @@
 from poetry.config.source import Source
 from poetry.console.commands.command import Command
 from poetry.factory import Factory
-from poetry.repositories import Pool
 
 
 class SourceAddCommand(Command):
@@ -89,13 +88,14 @@ def handle(self) -> Optional[int]:
             self.line(f"Adding source with name <c1>{name}</c1>.")
             sources.append(self.source_to_table(new_source))
 
+        self.poetry.config.merge(
+            {"sources": {source["name"]: source for source in sources}}
+        )
+
         # ensure new source is valid. eg: invalid name etc.
-        self.poetry._pool = Pool()
         try:
-            Factory.configure_sources(
-                self.poetry, sources, self.poetry.config, NullIO()
-            )
-            self.poetry.pool.repository(name)
+            pool = Factory.create_pool(self.poetry.config, NullIO())
+            pool.repository(name)
         except ValueError as e:
             self.line_error(
                 f"<error>Failed to validate addition of <c1>{name}</c1>: {e}</error>"
diff --git a/poetry/factory.py b/poetry/factory.py
index 144bfe35871..afefabb99f9 100644
--- a/poetry/factory.py
+++ b/poetry/factory.py
@@ -4,7 +4,6 @@
 from pathlib import Path
 from typing import TYPE_CHECKING
 from typing import Dict
-from typing import List
 from typing import Optional
 
 from cleo.io.io import IO
@@ -25,6 +24,7 @@
 
 if TYPE_CHECKING:
     from .repositories.legacy_repository import LegacyRepository
+    from .repositories.pool import Pool
 
 
 class Factory(BaseFactory):
@@ -80,11 +80,18 @@ def create_poetry(
             config,
         )
 
-        # Configuring sources
-        self.configure_sources(
-            poetry, poetry.local_config.get("source", []), config, io
+        config.merge(
+            {
+                "sources": {
+                    source["name"]: source
+                    for source in poetry.local_config.get("source", [])
+                }
+            }
         )
 
+        # Configuring sources
+        poetry.set_pool(self.create_pool(config, io))
+
         plugin_manager = PluginManager("plugin", disable_plugins=disable_plugins)
         plugin_manager.load_plugins()
         poetry.set_plugin_manager(plugin_manager)
@@ -133,10 +140,15 @@ def create_config(cls, io: Optional[IO] = None) -> Config:
         return config
 
     @classmethod
-    def configure_sources(
-        cls, poetry: "Poetry", sources: List[Dict[str, str]], config: "Config", io: "IO"
-    ) -> None:
-        for source in sources:
+    def create_pool(cls, config: "Config", io: Optional["IO"] = None) -> "Pool":
+        from poetry.repositories.pool import Pool
+
+        if io is None:
+            io = NullIO()
+
+        pool = Pool()
+
+        for source_name, source in config.get("sources").items():
             repository = cls.create_legacy_repository(source, config)
             is_default = source.get("default", False)
             is_secondary = source.get("secondary", False)
@@ -151,17 +163,19 @@ def configure_sources(
 
                 io.write_line(message)
 
-            poetry.pool.add_repository(repository, is_default, secondary=is_secondary)
+            pool.add_repository(repository, is_default, secondary=is_secondary)
 
         # Put PyPI last to prefer private repositories
         # unless we have no default source AND no primary sources
         # (default = false, secondary = false)
-        if poetry.pool.has_default():
+        if pool.has_default():
             if io.is_debug():
                 io.write_line("Deactivating the PyPI repository")
         else:
-            default = not poetry.pool.has_primary_repositories()
-            poetry.pool.add_repository(PyPiRepository(), default, not default)
+            default = not pool.has_primary_repositories()
+            pool.add_repository(PyPiRepository(), default, not default)
+
+        return pool
 
     @classmethod
     def create_legacy_repository(
diff --git a/poetry/installation/chef.py b/poetry/installation/chef.py
index ce095a03d5c..87794382ac5 100644
--- a/poetry/installation/chef.py
+++ b/poetry/installation/chef.py
@@ -1,23 +1,77 @@
 import hashlib
 import json
+import tarfile
+import tempfile
+import threading
+import zipfile
 
+from contextlib import redirect_stdout
+from io import StringIO
 from pathlib import Path
 from typing import TYPE_CHECKING
 from typing import List
 from typing import Optional
 
+from build import ProjectBuilder
+from build.env import IsolatedEnv as BaseIsolatedEnv
+from pep517.wrappers import quiet_subprocess_runner
+
 from poetry.core.packages.utils.link import Link
 
+from ..utils.helpers import temporary_directory
 from .chooser import InvalidWheelName
 from .chooser import Wheel
 
 
 if TYPE_CHECKING:
-
     from poetry.config.config import Config
     from poetry.utils.env import Env
 
 
+class IsolatedEnv(BaseIsolatedEnv):
+    def __init__(self, env: "Env", config: "Config") -> None:
+        self._env = env
+        self._config = config
+
+    @property
+    def executable(self) -> str:
+        return str(self._env.python)
+
+    @property
+    def scripts_dir(self) -> str:
+        return str(self._env._bin_dir)
+
+    def install(self, requirements) -> None:
+        from cleo.io.null_io import NullIO
+
+        from poetry.core.packages.dependency import Dependency
+        from poetry.core.packages.project_package import ProjectPackage
+        from poetry.factory import Factory
+        from poetry.installation.installer import Installer
+        from poetry.packages.locker import NullLocker
+        from poetry.repositories.installed_repository import InstalledRepository
+
+        # We build Poetry dependencies from the requirements
+        package = ProjectPackage("__root__", "0.0.0")
+        package.python_versions = ".".join(str(v) for v in self._env.version_info[:3])
+        for requirement in requirements:
+            dependency = Dependency.create_from_pep_508(requirement)
+            package.add_dependency(dependency)
+
+        pool = Factory.create_pool(self._config)
+        installer = Installer(
+            NullIO(),
+            self._env,
+            package,
+            NullLocker(self._env.path.joinpath("poetry.lock"), {}),
+            pool,
+            Factory.create_config(NullIO()),
+            InstalledRepository.load(self._env),
+        )
+        installer.update(True)
+        installer.run()
+
+
 class Chef:
     def __init__(self, config: "Config", env: "Env") -> None:
         self._config = config
@@ -25,27 +79,91 @@ def __init__(self, config: "Config", env: "Env") -> None:
         self._cache_dir = (
             Path(config.get("cache-dir")).expanduser().joinpath("artifacts")
         )
+        self._lock = threading.Lock()
+
+    def prepare(self, archive: Path, output_dir: Optional[Path] = None) -> Path:
+        if not self.should_prepare(archive):
+            return archive
+
+        if archive.is_dir():
+            tmp_dir = tempfile.mkdtemp(prefix="poetry-chef-")
+
+            return self._prepare(archive, Path(tmp_dir))
 
-    def prepare(self, archive: Path) -> Path:
-        return archive
+        return self._prepare_sdist(archive, destination=output_dir)
 
-    def prepare_sdist(self, archive: Path) -> Path:
-        return archive
+    def _prepare_sdist(self, archive: Path, destination: Optional[Path] = None) -> Path:
+        suffix = archive.suffix
 
-    def prepare_wheel(self, archive: Path) -> Path:
-        return archive
+        if suffix == ".zip":
+            context = zipfile.ZipFile
+        else:
+            context = tarfile.open
+
+        with temporary_directory() as archive_dir:
+            with context(archive.as_posix()) as archive_archive:
+                archive_archive.extractall(archive_dir)
+
+            archive_dir = Path(archive_dir)
+
+            elements = list(archive_dir.glob("*"))
+
+            if len(elements) == 1 and elements[0].is_dir():
+                sdist_dir = elements[0]
+            else:
+                sdist_dir = archive_dir / archive.name.rstrip(suffix)
+                if not sdist_dir.is_dir():
+                    sdist_dir = archive_dir
+
+            if destination is None:
+                destination = self.get_cache_directory_for_link(Link(archive.as_uri()))
+
+            destination.mkdir(parents=True, exist_ok=True)
+
+            return self._prepare(
+                sdist_dir,
+                destination,
+            )
+
+    def _prepare(self, directory: Path, destination: Path) -> Path:
+        from poetry.utils.env import EnvManager
+        from poetry.utils.env import VirtualEnv
+
+        with self._lock:
+            with temporary_directory() as tmp_dir:
+                EnvManager.build_venv(
+                    tmp_dir, executable=self._env.python, with_pip=True
+                )
+                venv = VirtualEnv(Path(tmp_dir))
+                env = IsolatedEnv(venv, self._config)
+                builder = ProjectBuilder(
+                    directory,
+                    python_executable=env.executable,
+                    scripts_dir=env.scripts_dir,
+                    runner=quiet_subprocess_runner,
+                )
+                env.install(builder.build_system_requires)
+                env.install(
+                    builder.build_system_requires
+                    | builder.get_requires_for_build("wheel")
+                )
+
+                stdout = StringIO()
+                with redirect_stdout(stdout):
+                    return Path(
+                        builder.build(
+                            "wheel",
+                            destination.as_posix(),
+                        )
+                    )
 
     def should_prepare(self, archive: Path) -> bool:
-        return not self.is_wheel(archive)
+        return archive.is_dir() or not self.is_wheel(archive)
 
     def is_wheel(self, archive: Path) -> bool:
         return archive.suffix == ".whl"
 
     def get_cached_archive_for_link(self, link: Link) -> Optional[Link]:
-        # If the archive is already a wheel, there is no need to cache it.
-        if link.is_wheel:
-            pass
-
         archives = self.get_cached_archives_for_link(link)
 
         if not archives:
diff --git a/poetry/installation/executor.py b/poetry/installation/executor.py
index 00b709da2aa..df48ded8fff 100644
--- a/poetry/installation/executor.py
+++ b/poetry/installation/executor.py
@@ -18,6 +18,7 @@
 
 from poetry.core.packages.file_dependency import FileDependency
 from poetry.core.packages.utils.link import Link
+from poetry.core.packages.utils.utils import url_to_path
 from poetry.core.pyproject.toml import PyProjectTOML
 from poetry.utils._compat import decode
 from poetry.utils.env import EnvCommandError
@@ -32,6 +33,7 @@
 from .operations.operation import Operation
 from .operations.uninstall import Uninstall
 from .operations.update import Update
+from .wheel_installer import WheelInstaller
 
 
 if TYPE_CHECKING:
@@ -62,6 +64,7 @@ def __init__(
         self._authenticator = Authenticator(config, self._io)
         self._chef = Chef(config, self._env)
         self._chooser = Chooser(pool, self._env)
+        self._wheel_installer = WheelInstaller(self._env)
 
         if parallel is None:
             parallel = config.get("installer.parallel", True)
@@ -100,6 +103,11 @@ def updates_count(self) -> int:
     def removals_count(self) -> int:
         return self._executed["uninstall"]
 
+    def set_chef(self, chef: "Chef") -> "Executor":
+        self._chef = chef
+
+        return self
+
     def supports_fancy_output(self) -> bool:
         return self._io.output.is_decorated() and not self._dry_run
 
@@ -480,7 +488,7 @@ def _execute_uninstall(self, operation: Uninstall) -> int:
         )
         self._write(operation, message)
 
-        return self._remove(operation)
+        return self._remove(operation.package)
 
     def _install(self, operation: Union[Install, Update]) -> int:
         package = operation.package
@@ -491,7 +499,7 @@ def _install(self, operation: Union[Install, Update]) -> int:
             return self._install_git(operation)
 
         if package.source_type == "file":
-            archive = self._prepare_file(operation)
+            archive = self._prepare_archive(operation)
         elif package.source_type == "url":
             archive = self._download_link(operation, Link(package.source_url))
         else:
@@ -504,14 +512,15 @@ def _install(self, operation: Union[Install, Update]) -> int:
             )
         )
         self._write(operation, message)
-        return self.pip_install(str(archive), upgrade=operation.job_type == "update")
+
+        self._wheel_installer.install(archive)
+
+        return 0
 
     def _update(self, operation: Union[Install, Update]) -> int:
         return self._install(operation)
 
-    def _remove(self, operation: Uninstall) -> int:
-        package = operation.package
-
+    def _remove(self, package: "Package") -> int:
         # If we have a VCS package, remove its source directory
         if package.source_type == "git":
             src_dir = self._env.path / "src" / package.name
@@ -526,7 +535,7 @@ def _remove(self, operation: Uninstall) -> int:
 
             raise
 
-    def _prepare_file(self, operation: Union[Install, Update]) -> Path:
+    def _prepare_archive(self, operation: Union[Install, Update]) -> Path:
         package = operation.package
 
         message = (
@@ -564,18 +573,10 @@ def _install_directory(self, operation: Union[Install, Update]) -> int:
 
         pyproject = PyProjectTOML(os.path.join(req, "pyproject.toml"))
 
-        if pyproject.is_poetry_project():
-            # Even if there is a build system specified
-            # some versions of pip (< 19.0.0) don't understand it
-            # so we need to check the version of pip to know
-            # if we can rely on the build system
-            legacy_pip = (
-                self._env.pip_version
-                < self._env.pip_version.__class__.from_parts(19, 0, 0)
-            )
+        if pyproject.is_poetry_project() and package.develop:
             package_poetry = Factory().create_poetry(pyproject.file.path.parent)
 
-            if package.develop and not package_poetry.package.build_script:
+            if package.develop:
                 from poetry.masonry.builders.editable import EditableBuilder
 
                 # This is a Poetry package in editable mode
@@ -585,24 +586,23 @@ def _install_directory(self, operation: Union[Install, Update]) -> int:
                 builder.build()
 
                 return 0
-            elif legacy_pip or package_poetry.package.build_script:
-                from poetry.core.masonry.builders.sdist import SdistBuilder
-
-                # We need to rely on creating a temporary setup.py
-                # file since the version of pip does not support
-                # build-systems
-                # We also need it for non-PEP-517 packages
-                builder = SdistBuilder(package_poetry)
+        elif package.develop:
+            # Editable installations are currently not supported
+            # for PEP-517 build systems so we defer to pip.
+            # TODO: Remove this workaround once either PEP-660 or PEP-662 is accepted
+            return self.pip_install(req, editable=True)
 
-                with builder.setup_py():
-                    if package.develop:
-                        return self.pip_install(req, editable=True)
-                    return self.pip_install(req, upgrade=True)
+        archive = self._prepare_archive(operation)
 
-        if package.develop:
-            return self.pip_install(req, editable=True)
+        try:
+            if operation.job_type == "update":
+                # Uninstall first
+                # TODO: Make an uninstaller and find a way to rollback in case the new package can't be installed
+                self._remove(operation.initial_package)
 
-        return self.pip_install(req, upgrade=True)
+            self._wheel_installer.install(archive)
+        finally:
+            archive.unlink()
 
     def _install_git(self, operation: Union[Install, Update]) -> int:
         from poetry.core.vcs import Git
@@ -650,27 +650,33 @@ def _download(self, operation: Union[Install, Update]) -> Link:
     def _download_link(self, operation: Union[Install, Update], link: Link) -> Link:
         package = operation.package
 
-        archive = self._chef.get_cached_archive_for_link(link)
-        if archive is link:
+        output_dir = self._chef.get_cache_directory_for_link(link)
+        archive_link = self._chef.get_cached_archive_for_link(link)
+        if archive_link is link:
             # No cached distributions was found, so we download and prepare it
             try:
                 archive = self._download_archive(operation, link)
             except BaseException:
                 cache_directory = self._chef.get_cache_directory_for_link(link)
                 cached_file = cache_directory.joinpath(link.filename)
-                # We can't use unlink(missing_ok=True) because it's not available
-                # in pathlib2 for Python 2.7
                 if cached_file.exists():
                     cached_file.unlink()
 
                 raise
 
             # TODO: Check readability of the created archive
+        else:
+            archive = Path(url_to_path(archive_link.url))
+
+        if not archive.suffix == ".whl":
+            message = "  <fg=blue;options=bold>•</> {message}: <info>Preparing...</info>".format(
+                message=self.get_operation_message(operation),
+            )
+            self._write(operation, message)
 
-            if not link.is_wheel:
-                archive = self._chef.prepare(archive)
+            archive = self._chef.prepare(archive, output_dir=output_dir)
 
-        if package.files:
+        if package.files and archive.name in {f["file"] for f in package.files}:
             archive_hash = (
                 "sha256:"
                 + FileDependency(
diff --git a/poetry/installation/wheel_installer.py b/poetry/installation/wheel_installer.py
new file mode 100644
index 00000000000..115716d262f
--- /dev/null
+++ b/poetry/installation/wheel_installer.py
@@ -0,0 +1,117 @@
+import os
+import platform
+import sys
+
+from pathlib import Path
+from typing import TYPE_CHECKING
+from typing import Iterator
+from typing import Tuple
+from typing import Union
+from typing import cast
+
+from installer.destinations import SchemeDictionaryDestination as BaseDestination
+from installer.records import parse_record_file
+from installer.sources import WheelFile as BaseWheelFile
+
+from poetry import __version__
+from poetry.utils._compat import WINDOWS
+
+
+if TYPE_CHECKING:
+    from typing import BinaryIO
+
+    from installer.records import RecordEntry
+    from installer.utils import Scheme
+
+    from poetry.utils.env import Env
+
+
+class WheelFile(BaseWheelFile):
+    def get_contents(
+        self,
+    ) -> Iterator[Tuple[Tuple[Union[Path, str], str, str], "BinaryIO"]]:
+        record_lines = self.read_dist_info("RECORD").splitlines()
+        records = parse_record_file(record_lines)
+        record_mapping = {record[0]: record for record in records}
+
+        for item in self._zipfile.infolist():
+            if item.is_dir():
+                continue
+
+            record = record_mapping.pop(item.filename)
+            assert record is not None, "In {}, {} is not mentioned in RECORD".format(
+                self._zipfile.filename,
+                item.filename,
+            )  # should not happen for valid wheels
+
+            with self._zipfile.open(item) as stream:
+                stream_casted = cast("BinaryIO", stream)
+                yield record, stream_casted
+
+
+class WheelDestination(BaseDestination):
+    def write_to_fs(
+        self, scheme: "Scheme", path: Union[Path, str], stream: "BinaryIO"
+    ) -> "RecordEntry":
+        from installer.records import Hash
+        from installer.records import RecordEntry
+        from installer.utils import copyfileobj_with_hashing
+
+        target_path = os.path.join(self.scheme_dict[scheme], path)
+        if os.path.exists(target_path):
+            # Contrary to the base library we don't raise an error
+            # here since it can break namespace packages (like Poetry's)
+            pass
+
+        parent_folder = os.path.dirname(target_path)
+        if not os.path.exists(parent_folder):
+            os.makedirs(parent_folder)
+
+        with open(target_path, "wb") as f:
+            hash_, size = copyfileobj_with_hashing(stream, f, self.hash_algorithm)
+
+        return RecordEntry(path, Hash(self.hash_algorithm, hash_), size)
+
+    def for_source(self, source: WheelFile) -> "WheelDestination":
+        scheme_dict = self.scheme_dict.copy()
+
+        scheme_dict["headers"] = os.path.join(
+            scheme_dict["headers"], source.distribution
+        )
+
+        return self.__class__(
+            scheme_dict, interpreter=self.interpreter, script_kind=self.script_kind
+        )
+
+
+class WheelInstaller:
+    def __init__(self, env: "Env") -> None:
+        self._env = env
+
+        if not WINDOWS:
+            script_kind = "posix"
+        else:
+            if platform.uname()[4].startswith("arm"):
+                script_kind = "win-arm64" if sys.maxsize > 2 ** 32 else "win-arm"
+            else:
+                script_kind = "win-amd64" if sys.maxsize > 2 ** 32 else "win-ia32"
+
+        schemes = self._env.paths
+        schemes["headers"] = schemes["include"]
+
+        self._destination = WheelDestination(
+            schemes, interpreter=self._env.python, script_kind=script_kind
+        )
+
+    def install(self, wheel: Path) -> None:
+        from installer import install
+
+        with WheelFile.open(wheel.as_posix()) as source:
+            install(
+                source=source,
+                destination=self._destination.for_source(source),
+                # Additional metadata that is generated by the installation tool.
+                additional_metadata={
+                    "INSTALLER": f"Poetry {__version__}".encode(),
+                },
+            )
diff --git a/poetry/repositories/installed_repository.py b/poetry/repositories/installed_repository.py
index e91e0bd3c9e..2c9f2101097 100644
--- a/poetry/repositories/installed_repository.py
+++ b/poetry/repositories/installed_repository.py
@@ -2,6 +2,8 @@
 import json
 
 from pathlib import Path
+from typing import Dict
+from typing import Optional
 from typing import Set
 from typing import Tuple
 from typing import Union
@@ -26,6 +28,9 @@
 
 
 class InstalledRepository(Repository):
+
+    _distributions: Dict[str, metadata.Distribution] = {}
+
     @classmethod
     def get_package_paths(cls, env: Env, name: str) -> Set[Path]:
         """
@@ -258,5 +263,9 @@ def load(cls, env: Env, with_dependencies: bool = False) -> "InstalledRepository
 
                 seen.add(package.name)
                 repo.add_package(package)
+                repo._distributions[package.name] = distribution
 
         return repo
+
+    def distribution(self, name: str) -> Optional[metadata.Distribution]:
+        return self._distributions.get(name)
diff --git a/poetry/utils/env.py b/poetry/utils/env.py
index 35493ec6f58..d1f55f1b856 100644
--- a/poetry/utils/env.py
+++ b/poetry/utils/env.py
@@ -1197,6 +1197,18 @@ def paths(self) -> Dict[str, str]:
         if self._paths is None:
             self._paths = self.get_paths()
 
+            if self.is_venv():
+                # We copy pip's logic here for the `include` path
+                self._paths["include"] = str(
+                    self.path.joinpath(
+                        "include",
+                        "site",
+                        "python{}.{}".format(
+                            self.version_info[0], self.version_info[1]
+                        ),
+                    )
+                )
+
         return self._paths
 
     @property
diff --git a/pyproject.toml b/pyproject.toml
index a049e1f8369..a2040461b33 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,8 @@ keyring = ">=21.2.0"
 entrypoints = "^0.3"
 importlib-metadata = {version = "^1.6.0", python = "<3.8"}
 dataclasses = {version = "^0.8", python = "~3.6"}
+build = "^0.4.0"
+installer = "^0.2.3"
 
 [tool.poetry.dev-dependencies]
 pytest = "^6.2"
diff --git a/tests/console/commands/plugin/conftest.py b/tests/console/commands/plugin/conftest.py
index 84509c81172..b916c73b159 100644
--- a/tests/console/commands/plugin/conftest.py
+++ b/tests/console/commands/plugin/conftest.py
@@ -17,19 +17,18 @@ def installed():
     return repository
 
 
-def configure_sources_factory(repo):
-    def _configure_sources(poetry, sources, config, io):  # noqa
+def create_pool_factory(repo):
+    def _create_pool(config, io):  # noqa
         pool = Pool()
         pool.add_repository(repo)
-        poetry.set_pool(pool)
 
-    return _configure_sources
+        return pool
+
+    return _create_pool
 
 
 @pytest.fixture(autouse=True)
 def setup_mocks(mocker, env, repo, installed):
     mocker.patch.object(EnvManager, "get_system_env", return_value=env)
     mocker.patch.object(InstalledRepository, "load", return_value=installed)
-    mocker.patch.object(
-        Factory, "configure_sources", side_effect=configure_sources_factory(repo)
-    )
+    mocker.patch.object(Factory, "create_pool", side_effect=create_pool_factory(repo))
diff --git a/tests/console/commands/plugin/test_add.py b/tests/console/commands/plugin/test_add.py
index 88c02812afd..602a8ba4545 100644
--- a/tests/console/commands/plugin/test_add.py
+++ b/tests/console/commands/plugin/test_add.py
@@ -2,6 +2,22 @@
 
 from poetry.core.packages.package import Package
 from poetry.factory import Factory
+from poetry.installation.chef import Chef
+from poetry.installation.executor import Executor
+from poetry.installation.wheel_installer import WheelInstaller
+
+
+@pytest.fixture()
+def setup(mocker, fixture_dir):
+    mocker.patch.object(
+        Executor,
+        "_download",
+        return_value=fixture_dir("distributions").joinpath(
+            "demo-0.1.2-py2.py3-none-any.whl"
+        ),
+    )
+
+    mocker.patch.object(WheelInstaller, "install")
 
 
 @pytest.fixture()
@@ -61,7 +77,9 @@ def test_add_with_constraint(app, repo, tester, env, installed):
     assert_plugin_add_result(tester, app, env, expected, "^0.2.0")
 
 
-def test_add_with_git_constraint(app, repo, tester, env, installed):
+def test_add_with_git_constraint(app, repo, tester, env, installed, mocker):
+    mocker.patch.object(Chef, "_prepare")
+
     repo.add_package(Package("pendulum", "2.0.5"))
 
     tester.execute("git+https://github.com/demo/poetry-plugin.git")
@@ -83,7 +101,9 @@ def test_add_with_git_constraint(app, repo, tester, env, installed):
     )
 
 
-def test_add_with_git_constraint_with_extras(app, repo, tester, env, installed):
+def test_add_with_git_constraint_with_extras(app, repo, tester, env, installed, mocker):
+    mocker.patch.object(Chef, "_prepare")
+
     repo.add_package(Package("pendulum", "2.0.5"))
     repo.add_package(Package("tomlkit", "0.7.0"))
 
diff --git a/tests/fixtures/distributions/demo-0.1.0.tar.gz b/tests/fixtures/distributions/demo-0.1.0.tar.gz
index 133b64421f8..42e4933130e 100644
Binary files a/tests/fixtures/distributions/demo-0.1.0.tar.gz and b/tests/fixtures/distributions/demo-0.1.0.tar.gz differ
diff --git a/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl b/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl
new file mode 100644
index 00000000000..a01175c144e
Binary files /dev/null and b/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl differ
diff --git a/tests/fixtures/extended_with_no_setup/README.rst b/tests/fixtures/extended_with_no_setup/README.rst
new file mode 100644
index 00000000000..a7508bd515e
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/README.rst
@@ -0,0 +1,2 @@
+Module 1
+========
diff --git a/tests/fixtures/extended_with_no_setup/build.py b/tests/fixtures/extended_with_no_setup/build.py
new file mode 100644
index 00000000000..4f1fee59574
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/build.py
@@ -0,0 +1,30 @@
+import os
+import shutil
+
+from distutils.command.build_ext import build_ext
+from distutils.core import Distribution
+from distutils.core import Extension
+
+
+extensions = [Extension("extended.extended", ["extended/extended.c"])]
+
+
+def build():
+    distribution = Distribution({"name": "extended", "ext_modules": extensions})
+    distribution.package_dir = "extended"
+
+    cmd = build_ext(distribution)
+    cmd.ensure_finalized()
+    cmd.run()
+
+    # Copy built extensions back to the project
+    for output in cmd.get_outputs():
+        relative_extension = os.path.relpath(output, cmd.build_lib)
+        shutil.copyfile(output, relative_extension)
+        mode = os.stat(relative_extension).st_mode
+        mode |= (mode & 0o444) >> 2
+        os.chmod(relative_extension, mode)
+
+
+if __name__ == "__main__":
+    build()
diff --git a/tests/fixtures/extended_with_no_setup/extended/__init__.py b/tests/fixtures/extended_with_no_setup/extended/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/fixtures/extended_with_no_setup/extended/extended.c b/tests/fixtures/extended_with_no_setup/extended/extended.c
new file mode 100644
index 00000000000..25a028eb11e
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/extended/extended.c
@@ -0,0 +1,58 @@
+#include <Python.h>
+
+
+static PyObject *hello(PyObject *self) {
+    return PyUnicode_FromString("Hello");
+}
+
+
+static PyMethodDef module_methods[] = {
+    {
+        "hello",
+        (PyCFunction) hello,
+        NULL,
+        PyDoc_STR("Say hello.")
+    },
+    {NULL}
+};
+
+#if PY_MAJOR_VERSION >= 3
+static struct PyModuleDef moduledef = {
+    PyModuleDef_HEAD_INIT,
+    "extended",
+    NULL,
+    -1,
+    module_methods,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+};
+#endif
+
+PyMODINIT_FUNC
+#if PY_MAJOR_VERSION >= 3
+PyInit_extended(void)
+#else
+init_extended(void)
+#endif
+{
+    PyObject *module;
+
+#if PY_MAJOR_VERSION >= 3
+    module = PyModule_Create(&moduledef);
+#else
+    module = Py_InitModule3("extended", module_methods, NULL);
+#endif
+
+    if (module == NULL)
+#if PY_MAJOR_VERSION >= 3
+        return NULL;
+#else
+        return;
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+    return module;
+#endif
+}
diff --git a/tests/fixtures/extended_with_no_setup/pyproject.toml b/tests/fixtures/extended_with_no_setup/pyproject.toml
new file mode 100644
index 00000000000..cebd29eabbb
--- /dev/null
+++ b/tests/fixtures/extended_with_no_setup/pyproject.toml
@@ -0,0 +1,26 @@
+[tool.poetry]
+name = "extended"
+version = "0.1"
+description = "Some description."
+authors = [
+    "Sébastien Eustace <sebastien@eustace.io>"
+]
+license = "MIT"
+
+readme = "README.rst"
+
+homepage = "https://python-poetry.org/"
+
+include = [
+    # C extensions must be included in the wheel distributions
+    {path = "extended/*.so", format = "wheel"},
+    {path = "extended/*.pyd", format = "wheel"},
+]
+
+[tool.poetry.build]
+script = "build.py"
+generate-setup-file = false
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py
index 332d9c4794f..136081b131b 100644
--- a/tests/installation/test_chef.py
+++ b/tests/installation/test_chef.py
@@ -1,10 +1,30 @@
 from pathlib import Path
 
+import pytest
+
 from packaging.tags import Tag
 
 from poetry.core.packages.utils.link import Link
+from poetry.factory import Factory
 from poetry.installation.chef import Chef
+from poetry.repositories import Pool
+from poetry.utils.env import EnvManager
 from poetry.utils.env import MockEnv
+from tests.repositories.test_pypi_repository import MockRepository
+
+
+@pytest.fixture()
+def pool():
+    pool = Pool()
+
+    pool.add_repository(MockRepository())
+
+    return pool
+
+
+@pytest.fixture(autouse=True)
+def setup(mocker, pool):
+    mocker.patch.object(Factory, "create_pool", return_value=pool)
 
 
 def test_get_cached_archive_for_link(config, mocker):
@@ -59,7 +79,7 @@ def test_get_cached_archives_for_link(config, mocker):
 
     assert archives
     assert set(archives) == {
-        Link(path.as_uri()) for path in distributions.glob("demo-0.1.0*")
+        Link(path.as_uri()) for path in distributions.glob("demo-0.1.*")
     }
 
 
@@ -82,3 +102,45 @@ def test_get_cache_directory_for_link(config, config_cache_dir):
     )
 
     assert expected == directory
+
+
+def test_prepare_sdist(config, config_cache_dir):
+    chef = Chef(config, EnvManager.get_system_env())
+
+    archive = (
+        Path(__file__)
+        .parent.parent.joinpath("fixtures/distributions/demo-0.1.0.tar.gz")
+        .resolve()
+    )
+
+    destination = chef.get_cache_directory_for_link(Link(archive.as_uri()))
+
+    wheel = chef.prepare(archive)
+
+    assert wheel.parent == destination
+    assert wheel.name == "demo-0.1.0-py2.py3-none-any.whl"
+
+
+def test_prepare_directory(config, config_cache_dir):
+    chef = Chef(config, EnvManager.get_system_env())
+
+    archive = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve()
+
+    wheel = chef.prepare(archive)
+
+    assert wheel.name == "simple_project-1.2.3-py2.py3-none-any.whl"
+
+
+def test_prepare_directory_with_extensions(config, config_cache_dir):
+    env = EnvManager.get_system_env()
+    chef = Chef(config, env)
+
+    archive = (
+        Path(__file__)
+        .parent.parent.joinpath("fixtures/extended_with_no_setup")
+        .resolve()
+    )
+
+    wheel = chef.prepare(archive)
+
+    assert wheel.name == "extended-0.1-{}.whl".format(env.supported_tags[0])
diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py
index 9b7b413b92b..2ab117c9a87 100644
--- a/tests/installation/test_executor.py
+++ b/tests/installation/test_executor.py
@@ -6,6 +6,7 @@
 import shutil
 
 from pathlib import Path
+from urllib.parse import urlparse
 
 import pytest
 
@@ -15,15 +16,41 @@
 from poetry.config.config import Config
 from poetry.core.packages.package import Package
 from poetry.core.utils._compat import PY36
+from poetry.installation.chef import Chef as BaseChef
 from poetry.installation.executor import Executor
 from poetry.installation.operations import Install
 from poetry.installation.operations import Uninstall
 from poetry.installation.operations import Update
+from poetry.installation.wheel_installer import WheelInstaller
 from poetry.repositories.pool import Pool
 from poetry.utils.env import MockEnv
 from tests.repositories.test_pypi_repository import MockRepository
 
 
+class Chef(BaseChef):
+
+    _directory_wheel = None
+    _sdist_wheel = None
+
+    def set_directory_wheel(self, wheel: Path) -> None:
+        self._directory_wheel = wheel
+
+    def set_sdist_wheel(self, wheel: Path) -> None:
+        self._sdist_wheel = wheel
+
+    def _prepare_sdist(self, archive: Path) -> Path:
+        if self._sdist_wheel is not None:
+            return self._sdist_wheel
+
+        return super()._prepare_sdist()
+
+    def _prepare(self, directory: Path, destination: Path) -> Path:
+        if self._directory_wheel is not None:
+            return self._directory_wheel
+
+        return super()._prepare(directory, destination)
+
+
 @pytest.fixture
 def env(tmp_dir):
     path = Path(tmp_dir) / ".venv"
@@ -70,9 +97,15 @@ def pool():
 @pytest.fixture()
 def mock_file_downloads(http):
     def callback(request, uri, headers):
+        name = Path(urlparse(uri).path).name
+
         fixture = Path(__file__).parent.parent.joinpath(
-            "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
+            "repositories/fixtures/pypi.org/dists/" + name
         )
+        if not fixture.exists():
+            fixture = Path(__file__).parent.parent.joinpath(
+                "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
+            )
 
         with fixture.open("rb") as f:
             return [200, headers, f.read()]
@@ -84,17 +117,37 @@ def callback(request, uri, headers):
     )
 
 
+@pytest.fixture()
+def wheel(tmp_dir):
+    shutil.copyfile(
+        Path(__file__)
+        .parent.parent.joinpath(
+            "fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl"
+        )
+        .as_posix(),
+        Path(tmp_dir).joinpath("demo-0.1.2-py2.py3-none-any.whl").as_posix(),
+    )
+
+    return Path(tmp_dir).joinpath("demo-0.1.2-py2.py3-none-any.whl")
+
+
 def test_execute_executes_a_batch_of_operations(
-    mocker, config, pool, io, tmp_dir, mock_file_downloads, env
+    mocker, config, pool, io, tmp_dir, mock_file_downloads, env, wheel
 ):
     pip_editable_install = mocker.patch(
         "poetry.installation.executor.pip_editable_install", unsafe=not PY36
     )
+    wheel_install = mocker.patch.object(WheelInstaller, "install")
 
     config = Config()
     config.merge({"cache-dir": tmp_dir})
 
+    chef = Chef(config, env)
+    chef.set_directory_wheel(wheel)
+    chef.set_sdist_wheel(wheel)
+
     executor = Executor(env, pool, config, io)
+    executor.set_chef(chef)
 
     file_package = Package(
         "demo",
@@ -155,9 +208,11 @@ def test_execute_executes_a_batch_of_operations(
     expected = set(expected.splitlines())
     output = set(io.fetch_output().splitlines())
     assert expected == output
-    assert 5 == len(env.executed)
+    # One pip uninstall command
+    assert 1 == len(env.executed)
     assert 0 == return_code
     pip_editable_install.assert_called_once()
+    assert wheel_install.call_count == 4
 
 
 def test_execute_shows_skipped_operations_if_verbose(
@@ -204,30 +259,25 @@ def test_execute_should_show_errors(config, mocker, io, env):
 
 
 def test_execute_works_with_ansi_output(
-    mocker, config, pool, io_decorated, tmp_dir, mock_file_downloads, env
+    config, pool, io_decorated, tmp_dir, mock_file_downloads, env, wheel
 ):
     config = Config()
     config.merge({"cache-dir": tmp_dir})
 
     executor = Executor(env, pool, config, io_decorated)
 
-    install_output = (
-        "some string that does not contain a keyb0ard !nterrupt or cance11ed by u$er"
-    )
-    mocker.patch.object(env, "_run", return_value=install_output)
     return_code = executor.execute(
         [
-            Install(Package("pytest", "3.5.2")),
+            Install(Package("clikit", "0.2.4")),
         ]
     )
-    env._run.assert_called_once()
 
     expected = [
         "\x1b[39;1mPackage operations\x1b[39;22m: \x1b[34m1\x1b[39m install, \x1b[34m0\x1b[39m updates, \x1b[34m0\x1b[39m removals",
-        "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m",
-        "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m",
-        "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m3.5.2\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m",
-        "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mpytest\x1b[39m\x1b[39m (\x1b[39m\x1b[32m3.5.2\x1b[39m\x1b[39m)\x1b[39m",  # finished
+        "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mclikit\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m0.2.4\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mPending...\x1b[39m",
+        "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mclikit\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m0.2.4\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mDownloading...\x1b[39m",
+        "\x1b[34;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mclikit\x1b[39m\x1b[39m (\x1b[39m\x1b[39;1m0.2.4\x1b[39;22m\x1b[39m)\x1b[39m: \x1b[34mInstalling...\x1b[39m",
+        "\x1b[32;1m•\x1b[39;22m \x1b[39mInstalling \x1b[39m\x1b[36mclikit\x1b[39m\x1b[39m (\x1b[39m\x1b[32m0.2.4\x1b[39m\x1b[39m)\x1b[39m",  # finished
     ]
     output = io_decorated.fetch_output()
     # hint: use print(repr(output)) if you need to debug this
@@ -238,28 +288,23 @@ def test_execute_works_with_ansi_output(
 
 
 def test_execute_works_with_no_ansi_output(
-    mocker, config, pool, io_not_decorated, tmp_dir, mock_file_downloads, env
+    config, pool, io_not_decorated, tmp_dir, mock_file_downloads, env
 ):
     config = Config()
     config.merge({"cache-dir": tmp_dir})
 
     executor = Executor(env, pool, config, io_not_decorated)
 
-    install_output = (
-        "some string that does not contain a keyb0ard !nterrupt or cance11ed by u$er"
-    )
-    mocker.patch.object(env, "_run", return_value=install_output)
     return_code = executor.execute(
         [
-            Install(Package("pytest", "3.5.2")),
+            Install(Package("clikit", "0.2.4")),
         ]
     )
-    env._run.assert_called_once()
 
     expected = """
 Package operations: 1 install, 0 updates, 0 removals
 
-  • Installing pytest (3.5.2)
+  • Installing clikit (0.2.4)
 """
     expected = set(expected.splitlines())
     output = set(io_not_decorated.fetch_output().splitlines())
@@ -389,14 +434,21 @@ def test_executor_should_write_pep610_url_references_for_files(
 
 
 def test_executor_should_write_pep610_url_references_for_directories(
-    tmp_venv, pool, config, io
+    tmp_venv, pool, config, io, wheel
 ):
-    url = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve()
+    url = (
+        Path(__file__)
+        .parent.parent.joinpath("fixtures/git/github.com/demo/demo")
+        .resolve()
+    )
     package = Package(
-        "simple-project", "1.2.3", source_type="directory", source_url=url.as_posix()
+        "demo", "0.1.2", source_type="directory", source_url=url.as_posix()
     )
 
+    chef = Chef(config, tmp_venv)
+    chef.set_directory_wheel(wheel)
     executor = Executor(tmp_venv, pool, config, io)
+    executor.set_chef(chef)
     executor.execute([Install(package)])
     verify_installed_distribution(
         tmp_venv, package, {"dir_info": {}, "url": url.as_uri()}
@@ -440,7 +492,7 @@ def test_executor_should_write_pep610_url_references_for_urls(
 
 
 def test_executor_should_write_pep610_url_references_for_git(
-    tmp_venv, pool, config, io, mock_file_downloads
+    tmp_venv, pool, config, io, mock_file_downloads, wheel
 ):
     package = Package(
         "demo",
@@ -451,7 +503,10 @@ def test_executor_should_write_pep610_url_references_for_git(
         source_url="https://github.com/demo/demo.git",
     )
 
+    chef = Chef(config, tmp_venv)
+    chef.set_directory_wheel(wheel)
     executor = Executor(tmp_venv, pool, config, io)
+    executor.set_chef(chef)
     executor.execute([Install(package)])
     verify_installed_distribution(
         tmp_venv,
diff --git a/tests/repositories/fixtures/pypi.org/dists/clikit-0.2.4-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/clikit-0.2.4-py2.py3-none-any.whl
index 87db51230e3..6afdd907294 100644
Binary files a/tests/repositories/fixtures/pypi.org/dists/clikit-0.2.4-py2.py3-none-any.whl and b/tests/repositories/fixtures/pypi.org/dists/clikit-0.2.4-py2.py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.0.3-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.0.3-py2.py3-none-any.whl
new file mode 100644
index 00000000000..dc468143f43
Binary files /dev/null and b/tests/repositories/fixtures/pypi.org/dists/poetry_core-1.0.3-py2.py3-none-any.whl differ
diff --git a/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json
new file mode 100644
index 00000000000..595674750f8
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/importlib-metadata.json
@@ -0,0 +1,140 @@
+{
+  "info": {
+    "author": "Barry Warsaw",
+    "author_email": "barry@python.org",
+    "bugtrack_url": null,
+    "classifiers": [
+      "Development Status :: 3 - Alpha",
+      "Intended Audience :: Developers",
+      "License :: OSI Approved :: Apache Software License",
+      "Programming Language :: Python :: 2",
+      "Programming Language :: Python :: 3",
+      "Topic :: Software Development :: Libraries"
+    ],
+    "description": "",
+    "description_content_type": "",
+    "docs_url": null,
+    "download_url": "",
+    "downloads": {
+      "last_day": -1,
+      "last_month": -1,
+      "last_week": -1
+    },
+    "home_page": "http://importlib-metadata.readthedocs.io/",
+    "keywords": "",
+    "license": "Apache Software License",
+    "maintainer": "",
+    "maintainer_email": "",
+    "name": "importlib-metadata",
+    "package_url": "https://pypi.org/project/importlib-metadata/",
+    "platform": "",
+    "project_url": "https://pypi.org/project/importlib-metadata/",
+    "project_urls": {
+      "Homepage": "http://importlib-metadata.readthedocs.io/"
+    },
+    "release_url": "https://pypi.org/project/importlib-metadata/1.7.0/",
+    "requires_dist": [
+      "zipp (>=0.5)",
+      "pathlib2 ; python_version < \"3\"",
+      "contextlib2 ; python_version < \"3\"",
+      "configparser (>=3.5) ; python_version < \"3\"",
+      "sphinx ; extra == 'docs'",
+      "rst.linker ; extra == 'docs'",
+      "packaging ; extra == 'testing'",
+      "pep517 ; extra == 'testing'",
+      "importlib-resources (>=1.3) ; (python_version < \"3.9\") and extra == 'testing'"
+    ],
+    "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+    "summary": "Read metadata from Python packages",
+    "version": "1.7.0",
+    "yanked": false,
+    "yanked_reason": null
+  },
+  "last_serial": 10821863,
+  "releases": {
+    "1.7.0": [
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "8ae1f31228e29443c08e07501a99d1b8",
+          "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
+        },
+        "downloads": -1,
+        "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl",
+        "has_sig": false,
+        "md5_digest": "8ae1f31228e29443c08e07501a99d1b8",
+        "packagetype": "bdist_wheel",
+        "python_version": "py2.py3",
+        "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+        "size": 31809,
+        "upload_time": "2020-06-26T21:38:16",
+        "upload_time_iso_8601": "2020-06-26T21:38:16.079439Z",
+        "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl",
+        "yanked": false,
+        "yanked_reason": null
+      },
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "4505ea85600cca1e693a4f8f5dd27ba8",
+          "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"
+        },
+        "downloads": -1,
+        "filename": "importlib_metadata-1.7.0.tar.gz",
+        "has_sig": false,
+        "md5_digest": "4505ea85600cca1e693a4f8f5dd27ba8",
+        "packagetype": "sdist",
+        "python_version": "source",
+        "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+        "size": 29233,
+        "upload_time": "2020-06-26T21:38:17",
+        "upload_time_iso_8601": "2020-06-26T21:38:17.338581Z",
+        "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz",
+        "yanked": false,
+        "yanked_reason": null
+      }
+    ]
+  },
+  "urls": [
+    {
+      "comment_text": "",
+      "digests": {
+        "md5": "8ae1f31228e29443c08e07501a99d1b8",
+        "sha256": "dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
+      },
+      "downloads": -1,
+      "filename": "importlib_metadata-1.7.0-py2.py3-none-any.whl",
+      "has_sig": false,
+      "md5_digest": "8ae1f31228e29443c08e07501a99d1b8",
+      "packagetype": "bdist_wheel",
+      "python_version": "py2.py3",
+      "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+      "size": 31809,
+      "upload_time": "2020-06-26T21:38:16",
+      "upload_time_iso_8601": "2020-06-26T21:38:16.079439Z",
+      "url": "https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl",
+      "yanked": false,
+      "yanked_reason": null
+    },
+    {
+      "comment_text": "",
+      "digests": {
+        "md5": "4505ea85600cca1e693a4f8f5dd27ba8",
+        "sha256": "90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"
+      },
+      "downloads": -1,
+      "filename": "importlib_metadata-1.7.0.tar.gz",
+      "has_sig": false,
+      "md5_digest": "4505ea85600cca1e693a4f8f5dd27ba8",
+      "packagetype": "sdist",
+      "python_version": "source",
+      "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7",
+      "size": 29233,
+      "upload_time": "2020-06-26T21:38:17",
+      "upload_time_iso_8601": "2020-06-26T21:38:17.338581Z",
+      "url": "https://files.pythonhosted.org/packages/e2/ae/0b037584024c1557e537d25482c306cf6327b5a09b6c4b893579292c1c38/importlib_metadata-1.7.0.tar.gz",
+      "yanked": false,
+      "yanked_reason": null
+    }
+  ]
+}
diff --git a/tests/repositories/fixtures/pypi.org/json/poetry-core.json b/tests/repositories/fixtures/pypi.org/json/poetry-core.json
new file mode 100644
index 00000000000..51eb4dbd135
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/poetry-core.json
@@ -0,0 +1,143 @@
+{
+  "info": {
+    "author": "Sébastien Eustace",
+    "author_email": "sebastien@eustace.io",
+    "bugtrack_url": null,
+    "classifiers": [
+      "License :: OSI Approved :: MIT License",
+      "Programming Language :: Python :: 2",
+      "Programming Language :: Python :: 2.7",
+      "Programming Language :: Python :: 3",
+      "Programming Language :: Python :: 3.5",
+      "Programming Language :: Python :: 3.6",
+      "Programming Language :: Python :: 3.7",
+      "Programming Language :: Python :: 3.8",
+      "Programming Language :: Python :: 3.9",
+      "Topic :: Software Development :: Build Tools",
+      "Topic :: Software Development :: Libraries :: Python Modules"
+    ],
+    "description": "",
+    "description_content_type": "text/markdown",
+    "docs_url": null,
+    "download_url": "",
+    "downloads": {
+      "last_day": -1,
+      "last_month": -1,
+      "last_week": -1
+    },
+    "home_page": "https://github.com/python-poetry/poetry-core",
+    "keywords": "packaging,dependency,poetry",
+    "license": "MIT",
+    "maintainer": "",
+    "maintainer_email": "",
+    "name": "poetry-core",
+    "package_url": "https://pypi.org/project/poetry-core/",
+    "platform": "",
+    "project_url": "https://pypi.org/project/poetry-core/",
+    "project_urls": {
+      "Bug Tracker": "https://github.com/python-poetry/poetry/issues",
+      "Homepage": "https://github.com/python-poetry/poetry-core",
+      "Repository": "https://github.com/python-poetry/poetry-core"
+    },
+    "release_url": "https://pypi.org/project/poetry-core/1.0.3/",
+    "requires_dist": [
+      "importlib-metadata (>=1.7.0,<2.0.0); python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.5\" and python_version < \"3.8\"",
+      "pathlib2 (>=2.3.5,<3.0.0); python_version >= \"2.7\" and python_version < \"2.8\"",
+      "typing (>=3.7.4.1,<4.0.0.0); python_version >= \"2.7\" and python_version < \"2.8\"",
+      "enum34 (>=1.1.10,<2.0.0); python_version >= \"2.7\" and python_version < \"2.8\"",
+      "functools32 (>=3.2.3-2,<4.0.0); python_version >= \"2.7\" and python_version < \"2.8\""
+    ],
+    "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+    "summary": "Poetry PEP 517 Build Backend",
+    "version": "1.0.3",
+    "yanked": false,
+    "yanked_reason": null
+  },
+  "last_serial": 10427564,
+  "releases": {
+    "1.0.3": [
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "16ce2526c342c1305d541425d406e35e",
+          "sha256": "c6bde46251112de8384013e1ab8d66e7323d2c75172f80220aba2bc07e208e9a"
+        },
+        "downloads": -1,
+        "filename": "poetry_core-1.0.3-py2.py3-none-any.whl",
+        "has_sig": false,
+        "md5_digest": "16ce2526c342c1305d541425d406e35e",
+        "packagetype": "bdist_wheel",
+        "python_version": "py2.py3",
+        "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+        "size": 424184,
+        "upload_time": "2021-04-09T15:44:41",
+        "upload_time_iso_8601": "2021-04-09T15:44:41.970294Z",
+        "url": "https://files.pythonhosted.org/packages/bf/e1/08c7478df1e93dea47b06c9d9a80dbb54af7421462e1b22c280d063df807/poetry_core-1.0.3-py2.py3-none-any.whl",
+        "yanked": false,
+        "yanked_reason": null
+      },
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "dc82606f54a984a99daf8864cca1f028",
+          "sha256": "2315c928249fc3207801a81868b64c66273077b26c8d8da465dccf8f488c90c5"
+        },
+        "downloads": -1,
+        "filename": "poetry-core-1.0.3.tar.gz",
+        "has_sig": false,
+        "md5_digest": "dc82606f54a984a99daf8864cca1f028",
+        "packagetype": "sdist",
+        "python_version": "source",
+        "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+        "size": 345626,
+        "upload_time": "2021-04-09T15:44:40",
+        "upload_time_iso_8601": "2021-04-09T15:44:40.639806Z",
+        "url": "https://files.pythonhosted.org/packages/d0/b3/1017f2f6d801f1e3e4ffee3f058a10d20df1a9560aba9c5b49e92cdd9912/poetry-core-1.0.3.tar.gz",
+        "yanked": false,
+        "yanked_reason": null
+      }
+    ]
+  },
+  "urls": [
+    {
+      "comment_text": "",
+      "digests": {
+        "md5": "16ce2526c342c1305d541425d406e35e",
+        "sha256": "c6bde46251112de8384013e1ab8d66e7323d2c75172f80220aba2bc07e208e9a"
+      },
+      "downloads": -1,
+      "filename": "poetry_core-1.0.3-py2.py3-none-any.whl",
+      "has_sig": false,
+      "md5_digest": "16ce2526c342c1305d541425d406e35e",
+      "packagetype": "bdist_wheel",
+      "python_version": "py2.py3",
+      "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+      "size": 424184,
+      "upload_time": "2021-04-09T15:44:41",
+      "upload_time_iso_8601": "2021-04-09T15:44:41.970294Z",
+      "url": "https://files.pythonhosted.org/packages/bf/e1/08c7478df1e93dea47b06c9d9a80dbb54af7421462e1b22c280d063df807/poetry_core-1.0.3-py2.py3-none-any.whl",
+      "yanked": false,
+      "yanked_reason": null
+    },
+    {
+      "comment_text": "",
+      "digests": {
+        "md5": "dc82606f54a984a99daf8864cca1f028",
+        "sha256": "2315c928249fc3207801a81868b64c66273077b26c8d8da465dccf8f488c90c5"
+      },
+      "downloads": -1,
+      "filename": "poetry-core-1.0.3.tar.gz",
+      "has_sig": false,
+      "md5_digest": "dc82606f54a984a99daf8864cca1f028",
+      "packagetype": "sdist",
+      "python_version": "source",
+      "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+      "size": 345626,
+      "upload_time": "2021-04-09T15:44:40",
+      "upload_time_iso_8601": "2021-04-09T15:44:40.639806Z",
+      "url": "https://files.pythonhosted.org/packages/d0/b3/1017f2f6d801f1e3e4ffee3f058a10d20df1a9560aba9c5b49e92cdd9912/poetry-core-1.0.3.tar.gz",
+      "yanked": false,
+      "yanked_reason": null
+    }
+  ]
+}
diff --git a/tests/repositories/fixtures/pypi.org/json/zipp.json b/tests/repositories/fixtures/pypi.org/json/zipp.json
new file mode 100644
index 00000000000..356bd2b3483
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/zipp.json
@@ -0,0 +1,142 @@
+{
+  "info": {
+    "author": "Jason R. Coombs",
+    "author_email": "jaraco@jaraco.com",
+    "bugtrack_url": null,
+    "classifiers": [
+      "Development Status :: 5 - Production/Stable",
+      "Intended Audience :: Developers",
+      "License :: OSI Approved :: MIT License",
+      "Programming Language :: Python :: 3",
+      "Programming Language :: Python :: 3 :: Only"
+    ],
+    "description": "",
+    "description_content_type": "",
+    "docs_url": null,
+    "download_url": "",
+    "downloads": {
+      "last_day": -1,
+      "last_month": -1,
+      "last_week": -1
+    },
+    "home_page": "https://github.com/jaraco/zipp",
+    "keywords": "",
+    "license": "",
+    "maintainer": "",
+    "maintainer_email": "",
+    "name": "zipp",
+    "package_url": "https://pypi.org/project/zipp/",
+    "platform": "",
+    "project_url": "https://pypi.org/project/zipp/",
+    "project_urls": {
+      "Homepage": "https://github.com/jaraco/zipp"
+    },
+    "release_url": "https://pypi.org/project/zipp/3.5.0/",
+    "requires_dist": [
+      "sphinx ; extra == 'docs'",
+      "jaraco.packaging (>=8.2) ; extra == 'docs'",
+      "rst.linker (>=1.9) ; extra == 'docs'",
+      "pytest (>=4.6) ; extra == 'testing'",
+      "pytest-checkdocs (>=2.4) ; extra == 'testing'",
+      "pytest-flake8 ; extra == 'testing'",
+      "pytest-cov ; extra == 'testing'",
+      "pytest-enabler (>=1.0.1) ; extra == 'testing'",
+      "jaraco.itertools ; extra == 'testing'",
+      "func-timeout ; extra == 'testing'",
+      "pytest-black (>=0.3.7) ; (platform_python_implementation != \"PyPy\" and python_version < \"3.10\") and extra == 'testing'",
+      "pytest-mypy ; (platform_python_implementation != \"PyPy\" and python_version < \"3.10\") and extra == 'testing'"
+    ],
+    "requires_python": ">=3.6",
+    "summary": "Backport of pathlib-compatible object wrapper for zip files",
+    "version": "3.5.0",
+    "yanked": false,
+    "yanked_reason": null
+  },
+  "last_serial": 10811847,
+  "releases": {
+    "3.5.0": [
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "0ec47fbf522751f6c5fa904cb33f1f59",
+          "sha256": "957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"
+        },
+        "downloads": -1,
+        "filename": "zipp-3.5.0-py3-none-any.whl",
+        "has_sig": false,
+        "md5_digest": "0ec47fbf522751f6c5fa904cb33f1f59",
+        "packagetype": "bdist_wheel",
+        "python_version": "py3",
+        "requires_python": ">=3.6",
+        "size": 5700,
+        "upload_time": "2021-07-02T23:51:45",
+        "upload_time_iso_8601": "2021-07-02T23:51:45.759726Z",
+        "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl",
+        "yanked": false,
+        "yanked_reason": null
+      },
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "617efbf3edb707c57008ec00f408972f",
+          "sha256": "f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
+        },
+        "downloads": -1,
+        "filename": "zipp-3.5.0.tar.gz",
+        "has_sig": false,
+        "md5_digest": "617efbf3edb707c57008ec00f408972f",
+        "packagetype": "sdist",
+        "python_version": "source",
+        "requires_python": ">=3.6",
+        "size": 13270,
+        "upload_time": "2021-07-02T23:51:47",
+        "upload_time_iso_8601": "2021-07-02T23:51:47.004396Z",
+        "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz",
+        "yanked": false,
+        "yanked_reason": null
+      }
+    ]
+  },
+  "urls": [
+    {
+      "comment_text": "",
+      "digests": {
+        "md5": "0ec47fbf522751f6c5fa904cb33f1f59",
+        "sha256": "957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"
+      },
+      "downloads": -1,
+      "filename": "zipp-3.5.0-py3-none-any.whl",
+      "has_sig": false,
+      "md5_digest": "0ec47fbf522751f6c5fa904cb33f1f59",
+      "packagetype": "bdist_wheel",
+      "python_version": "py3",
+      "requires_python": ">=3.6",
+      "size": 5700,
+      "upload_time": "2021-07-02T23:51:45",
+      "upload_time_iso_8601": "2021-07-02T23:51:45.759726Z",
+      "url": "https://files.pythonhosted.org/packages/92/d9/89f433969fb8dc5b9cbdd4b4deb587720ec1aeb59a020cf15002b9593eef/zipp-3.5.0-py3-none-any.whl",
+      "yanked": false,
+      "yanked_reason": null
+    },
+    {
+      "comment_text": "",
+      "digests": {
+        "md5": "617efbf3edb707c57008ec00f408972f",
+        "sha256": "f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
+      },
+      "downloads": -1,
+      "filename": "zipp-3.5.0.tar.gz",
+      "has_sig": false,
+      "md5_digest": "617efbf3edb707c57008ec00f408972f",
+      "packagetype": "sdist",
+      "python_version": "source",
+      "requires_python": ">=3.6",
+      "size": 13270,
+      "upload_time": "2021-07-02T23:51:47",
+      "upload_time_iso_8601": "2021-07-02T23:51:47.004396Z",
+      "url": "https://files.pythonhosted.org/packages/3a/9f/1d4b62cbe8d222539a84089eeab603d8e45ee1f897803a0ae0860400d6e7/zipp-3.5.0.tar.gz",
+      "yanked": false,
+      "yanked_reason": null
+    }
+  ]
+}
diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py
index d09012c821f..2ddeb16b1b8 100644
--- a/tests/utils/test_env.py
+++ b/tests/utils/test_env.py
@@ -950,6 +950,13 @@ def test_venv_has_correct_paths(tmp_venv):
     assert paths.get("platlib") is not None
     assert paths.get("scripts") is not None
     assert tmp_venv.site_packages.path == Path(paths["purelib"])
+    assert paths["include"] == str(
+        tmp_venv.path.joinpath(
+            "include/site/python{}.{}".format(
+                tmp_venv.version_info[0], tmp_venv.version_info[1]
+            )
+        )
+    )
 
 
 def test_env_system_packages(tmp_path, config):
