Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ resolve_source = "fromager.sources:default_resolve_source"
build_sdist = "fromager.sources:default_build_sdist"
build_wheel = "fromager.wheels:default_build_wheel"

[project.entry-points."virtualenv.seed"]
fromager = "fromager.virtualenv.seeder:FromagerSeeder"

[tool.coverage.run]
branch = true
parallel = true
Expand Down Expand Up @@ -130,5 +133,5 @@ exclude = [

[[tool.mypy.overrides]]
# packages without typing annotations and stubs
module = ["pyproject_hooks", "requests_mock", "resolver", "stevedore"]
module = ["pyproject_hooks", "requests_mock", "resolver", "stevedore", "virtualenv.*"]
ignore_missing_imports = true
13 changes: 11 additions & 2 deletions src/fromager/build_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,19 @@ def _createenv(self) -> None:
logger.info("reusing build environment in %s", self.path)
return

# use our seeder plugin to install `build` command
logger.debug("creating build environment in %s", self.path)
external_commands.run(
[sys.executable, "-m", "virtualenv", str(self.path)],
network_isolation=False,
[
sys.executable,
"-m",
"virtualenv",
"--never-download",
"--no-periodic-update",
"--seeder=fromager",
str(self.path),
],
network_isolation=self._ctx.network_isolation,
)
logger.info("created build environment in %s", self.path)

Expand Down
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
68 changes: 68 additions & 0 deletions src/fromager/virtualenv/seeder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Virtualenv seeder plugin for Fromager

The plugin installs ``build`` and its dependencies ``packaging`` +
``pyproject_hooks`` into a new virtual environment. The wheel files for
the three packages are bundled and shipped with Fromager. This solves
problems with bootstrapping.

Either Fromager would have to implement a special bootstrapping mode to
build ``build``, ``packaging``, ``pyproject_hooks``, and ``flit-core``
wheels first, then switch its wheel build command to ``build``.

Or Fromager would have to download three pre-built wheels from PyPI.

2. Fromager can create a build environment with the ``build`` command without
downloading any content from PyPI. Instead it uses the same bundle approach
as ``virtualenv`` and ``ensurepip``.


"""

import pathlib
import typing

from packaging.utils import parse_wheel_filename

from virtualenv.config.cli.parser import VirtualEnvOptions
from virtualenv.seed.embed.via_app_data.via_app_data import FromAppData
from virtualenv.seed.wheels import Version
from virtualenv.seed.wheels.embed import BUNDLE_SUPPORT

BUNDLED_DIR = pathlib.Path(__file__).parent.resolve() / "bundled"
# `build`` depends on packaging and pyproject_hooks
BUNDLED_PACKAGES = ["build", "packaging", "pyproject_hooks"]


class FromagerSeeder(FromAppData):
"""Custom virtualenv seeder to install build command"""

def __init__(self, options: VirtualEnvOptions) -> None:
# register our packages
for whl, name, version in self.list_extra_packages():
# add option defaults
setattr(self, f"no_{name}", False)
setattr(self, f"{name}_version", version)
# register wheel files with virtualenv's bundle support
for py_version in BUNDLE_SUPPORT:
BUNDLE_SUPPORT[py_version][name] = str(whl)

# virtualenv no longer installs setuptool and wheels for
# Python >= 3.12, force installation.
for opt in ("setuptools", "wheel"):
if getattr(options, opt) == "none":
setattr(options, opt, Version.bundle)

super().__init__(options)

@classmethod
def list_extra_packages(cls) -> typing.Iterable[tuple[pathlib.Path, str, str]]:
for whl in sorted(BUNDLED_DIR.glob("*.whl")):
name, version, _, _ = parse_wheel_filename(whl.name)
yield whl, name, str(version)

@classmethod
def distributions(cls) -> dict[str, str]:
dist = super().distributions()
for _, name, version in cls.list_extra_packages():
dist[name] = version
return dist
16 changes: 16 additions & 0 deletions src/fromager/virtualenv/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Update bundled wheels"""

import subprocess

from .seeder import BUNDLED_DIR, BUNDLED_PACKAGES


def main():
for whl in BUNDLED_DIR.glob("*.whl"):
whl.unlink()
cmd = ["pip", "download", "-d", str(BUNDLED_DIR), *BUNDLED_PACKAGES]
subprocess.check_call(cmd)


if __name__ == "__main__":
main()