Skip to content

Commit 2c47531

Browse files
committed
Add virtualenv seeder plugin to install build
Implements a seeder plugin that extends the seed function of virtualenv. The plugin allows us to seed the `build` package into a new virtual env and work around missing `setuptools` and `wheel` commands in Python 3.12+ virtual envs. The seeder plugin uses bundled wheel files just like `virtualenv` and `ensurepip`. Related: python-wheel-build#126 Signed-off-by: Christian Heimes <[email protected]>
1 parent d80da7c commit 2c47531

8 files changed

+99
-3
lines changed

pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ resolve_source = "fromager.sources:default_resolve_source"
5454
build_sdist = "fromager.sources:default_build_sdist"
5555
build_wheel = "fromager.wheels:default_build_wheel"
5656

57+
[project.entry-points."virtualenv.seed"]
58+
fromager = "fromager.virtualenv.seeder:FromagerSeeder"
59+
5760
[tool.coverage.run]
5861
branch = true
5962
parallel = true
@@ -130,5 +133,5 @@ exclude = [
130133

131134
[[tool.mypy.overrides]]
132135
# packages without typing annotations and stubs
133-
module = ["pyproject_hooks", "requests_mock", "resolver", "stevedore"]
136+
module = ["pyproject_hooks", "requests_mock", "resolver", "stevedore", "virtualenv.*"]
134137
ignore_missing_imports = true

src/fromager/build_environment.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,19 @@ def _createenv(self) -> None:
8888
logger.info("reusing build environment in %s", self.path)
8989
return
9090

91+
# use our seeder plugin to install `build` command
9192
logger.debug("creating build environment in %s", self.path)
9293
external_commands.run(
93-
[sys.executable, "-m", "virtualenv", str(self.path)],
94-
network_isolation=False,
94+
[
95+
sys.executable,
96+
"-m",
97+
"virtualenv",
98+
"--never-download",
99+
"--no-periodic-update",
100+
"--seeder=fromager",
101+
str(self.path),
102+
],
103+
network_isolation=self._ctx.network_isolation,
95104
)
96105
logger.info("created build environment in %s", self.path)
97106

src/fromager/virtualenv/__init__.py

Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.

src/fromager/virtualenv/seeder.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Virtualenv seeder plugin for Fromager
2+
3+
The plugin installs ``build`` and its dependencies ``packaging`` +
4+
``pyproject_hooks`` into a new virtual environment. The wheel files for
5+
the three packages are bundled and shipped with Fromager. This solves
6+
problems with bootstrapping.
7+
8+
Either Fromager would have to implement a special bootstrapping mode to
9+
build ``build``, ``packaging``, ``pyproject_hooks``, and ``flit-core``
10+
wheels first, then switch its wheel build command to ``build``.
11+
12+
Or Fromager would have to download three pre-built wheels from PyPI.
13+
14+
2. Fromager can create a build environment with the ``build`` command without
15+
downloading any content from PyPI. Instead it uses the same bundle approach
16+
as ``virtualenv`` and ``ensurepip``.
17+
18+
19+
"""
20+
21+
import pathlib
22+
import typing
23+
24+
from packaging.utils import parse_wheel_filename
25+
26+
from virtualenv.config.cli.parser import VirtualEnvOptions
27+
from virtualenv.seed.embed.via_app_data.via_app_data import FromAppData
28+
from virtualenv.seed.wheels import Version
29+
from virtualenv.seed.wheels.embed import BUNDLE_SUPPORT
30+
31+
BUNDLED_DIR = pathlib.Path(__file__).parent.resolve() / "bundled"
32+
# `build`` depends on packaging and pyproject_hooks
33+
BUNDLED_PACKAGES = ["build", "packaging", "pyproject_hooks"]
34+
35+
36+
class FromagerSeeder(FromAppData):
37+
"""Custom virtualenv seeder to install build command"""
38+
39+
def __init__(self, options: VirtualEnvOptions) -> None:
40+
# register our packages
41+
for whl, name, version in self.list_extra_packages():
42+
# add option defaults
43+
setattr(self, f"no_{name}", False)
44+
setattr(self, f"{name}_version", version)
45+
# register wheel files with virtualenv's bundle support
46+
for py_version in BUNDLE_SUPPORT:
47+
BUNDLE_SUPPORT[py_version][name] = str(whl)
48+
49+
# virtualenv no longer installs setuptool and wheels for
50+
# Python >= 3.12, force installation.
51+
for opt in ("setuptools", "wheel"):
52+
if getattr(options, opt) == "none":
53+
setattr(options, opt, Version.bundle)
54+
55+
super().__init__(options)
56+
57+
@classmethod
58+
def list_extra_packages(cls) -> typing.Iterable[tuple[pathlib.Path, str, str]]:
59+
for whl in sorted(BUNDLED_DIR.glob("*.whl")):
60+
name, version, _, _ = parse_wheel_filename(whl.name)
61+
yield whl, name, str(version)
62+
63+
@classmethod
64+
def distributions(cls) -> dict[str, str]:
65+
dist = super().distributions()
66+
for _, name, version in cls.list_extra_packages():
67+
dist[name] = version
68+
return dist

src/fromager/virtualenv/update.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Update bundled wheels"""
2+
3+
import subprocess
4+
5+
from .seeder import BUNDLED_DIR, BUNDLED_PACKAGES
6+
7+
8+
def main():
9+
for whl in BUNDLED_DIR.glob("*.whl"):
10+
whl.unlink()
11+
cmd = ["pip", "download", "-d", str(BUNDLED_DIR), *BUNDLED_PACKAGES]
12+
subprocess.check_call(cmd)
13+
14+
15+
if __name__ == "__main__":
16+
main()

0 commit comments

Comments
 (0)