Uv support in buildout 5.3.0a1

I have released zc.buildout 5.3.0a1. If you like buildout and would like to see it modernized, this is a first step with a performance improvement.

## TLDR

zc.buildout 5.3.0a1 adds opt-in uv support: buildout now delegates downloads and version resolution to uv instead of pip. Enable it with installer = uv in the [buildout] section; pip remains the default. Expect a large speedup — for Plone on my Mac M2, from 7 minutes down to under 2 on a cold cache, and seconds on a warm one.

## How it came about

Besides adding uv support, my goal was to learn how to let the agents write all the code for me. The work happened in three steps:

1. rewriting the legacy doctest test suite into a pytest suite;

2. improving the verification harness for the agents' work: lowering cyclomatic complexity, adding type annotations, reducing the use of Any;

3. replacing pip and setuptools.package_index usage with uv only.

I used the pstack poteto-mode skill as a driver to build a verify-buildout skill. I left every step to the agents through a series of prompts.

The first step ported the legacy doctest suite to pytest. That alone gave the test suite a solid performance boost thanks to pytest-xdist parallelization. The two suites have very similar coverage. From then on, development ran against the pytest suite; I ran the legacy suite only to confirm there were no regressions.

The second step made the codebase easier for agents to refactor. Reducing cyclomatic complexity made the code easier to understand. It exposed cases that only integration tests might have covered, so the agents added unit tests for them. Ruff linting raised the overall quality of the code. The agents added type annotations across the codebase to make the semantics explicit. They also made a deliberate effort to shrink the use of the Any type. They split large files into shorter ones that they can read and write more cheaply. All this tooling supplements the test suites. Together they form a verification harness that gives me confidence few regression slipped through.

With the codebase cleaned up, it was time for the heart of the move: adding uv support, to take advantage of the speed of uv's version resolution and parallel downloads.

That took two refactorings: replacing the pip install subprocesses with uv ones, and delegating version resolution to uv instead of setuptools.package_index. The delegation also reduces how many subprocesses buildout launches.

## What uv support actually does

In uv mode, a single `uv pip compile` handles package discovery and version resolution. After the environment check, uv resolves everything still open in one pass, with the buildout's develop projects riding along as overrides. One batched `uv pip install` then installs everything. The per-requirement pip subprocess fan-out is gone.

I kept uv support opt-in, behind a new `buildout:installer` option. Add `installer = uv` to the `[buildout]` section of your config, or override it on the command line with the usual assignment syntax, for example `bin/buildout buildout:installer=uv`. The default value remains `pip`. That makes it easy to compare the two installers and debug any regression that slipped through anyway. uv mode requires uv 0.12.11 or newer, and `uv` is now a declared dependency of zc.buildout; the pip code path is unchanged.

## What else is in the release

- New `--interpolated` option on the `query` and `annotate` commands: print values after applying the `${...}` substitutions, exactly the way recipes see them. Raw values remain the default output.

- Repeated buildout runs are faster: buildout no longer reinstalls the sources listed in the `develop` option on every run. It only redoes an editable install when `setup.py`, `setup.cfg` or `pyproject.toml` is newer than the egg-link.

- Bug fixes worth mentioning: spurious uninstall/reinstall cycles of parts whose recipe resolves to a develop egg; offline mode now reuses wheel-installed distributions; a `TypeError` that made zc.buildout unimportable on Python 3.9; and several Windows fixes, including local package indexes spelled as drive paths now correctly converting to `file://` URIs.

For the development infrastructure aficionados: I rebuilt CI and the development environment around devenv and Dagger. `dagger call ci` runs the whole CI job table locally or in containers. CI also gained a parallel uv test set: it reruns the legacy suite through the uv install pipeline, across the full platform matrix — including Windows — and across a matrix of uv versions.

## Tests done with the new release

I have run Plone 6.2.0 through 6.2.2, 6.1.0 through 6.1.5, and 6.0.11 through 6.0.15 under Python 3.10. I also tested the 6.2.x series with 3.14, and 6.1.x with 3.13.

In each case, downloading from scratch (nothing cached) shows a good performance boost. On my M2 with a bad internet connection, installation went from roughly 7 minutes with pip down to 1 minute 45 seconds.

As soon as the uv cache is hot, it gets much faster: under 10 seconds to install any of those Plone setups.

This is still an alpha. I would love to hear from you what works and what breaks for you. Enjoy.

13 Likes

I created an agentic (Claude Opus 5.5) benchmark with buildout.coredev#6.2 on my Mac. This is the result (spoiler - there might be a problem with allow-picked-versions = false in this release. see "Compatibility findings" at the bottom):

TL;DR

Scenario buildout (pip) buildout (uv) mxdev make install
Cold: empty download caches, fresh project 318 s 75 s 52 s
Warm: download caches filled, fresh project 303 s 46 s 29 s
Shared eggs: filled shared eggs-directory, fresh project 24 s 23 s n/a
No-op rerun: nothing changed 10.4 s 10.1 s 0.3 s ¹

Environment

Machine Apple M2, 8 cores, 8 GB RAM, macOS 26
Disk Project, caches and eggs on an external USB disk (HFS+), which is also the normal working disk
Python CPython 3.13.9 (uv-managed), same interpreter for all methods
buildout.coredev 6.2 @ c0b03f092 (identical to origin/6.2), exported with git archive, no local.cfg
zc.buildout 5.3.0a1 for both buildout modes
uv 0.12.19, both for buildout (pulled into the venv as a zc.buildout dependency) and for mxmake (global uv)
mxdev / mxmake as installed by the repository's Makefile (PYTHON_PACKAGE_INSTALLER=uv)
Date 2026-09-28/29

Because the disk is HFS+, uv cannot use APFS clones (reflinks) when installing from its cache.
On an APFS system disk the uv based numbers are probably lower.

Method

Each run starts from a fresh copy of the exported repository. The 6 auto-checkout packages from checkouts.cfg (mockup, Plone, plone.app.locales, plone.app.upgrade, Products.CMFPlone, plone.restapi) are cloned once from GitHub on the branches from sources.cfg and copied into src/ before every run, so the initial clone is not measured. The git fetch/update of these checkouts that both tools do on every run is measured: mr.developer runs with always-checkout = force, mxdev with mxdev -f.

Every method gets its own isolated caches (PIP_CACHE_DIR, UV_CACHE_DIR, cookiecutter dir). Runs were strictly sequential, with the methods interleaved to spread network variance.

Commands

buildout (pip) and buildout (uv)

python3.13 -m venv .venv
.venv/bin/pip install pip==26.2.1 setuptools==81.0.0 wheel==0.48.0 \
    horse-with-no-namespace==20260202.0 zc.buildout==5.3.0a1 uv==0.12.19
.venv/bin/buildout [buildout:installer=uv] \
    versions:zc.buildout=5.3.0a1 versions:uv=0.12.19 \
    versions:httpcore=1.0.9 versions:grpcio=1.81.0 \
    versions:grpcio-tools=1.81.0 versions:protobuf=6.33.6

The versions: overrides are the only change to the 6.2 configuration. versions.cfg pins zc.buildout 5.2.0, and the other four pins are explained below. Both buildout modes get exactly the same overrides. In the "shared eggs" scenario, buildout:eggs-directory=<persistent dir> is added.

mxdev

make install PRIMARY_PYTHON=/path/to/python3.13

This uses the unchanged Makefile, mx.ini, mxsources.ini and mxcheckouts.ini from 6.2.

Scenarios

Scenario Download caches Project dir Runs
Cold deleted before the run fresh 2
Warm kept from earlier runs fresh (new venv / new eggs/) 3
Shared eggs (buildout only) kept fresh, but eggs-directory points to a persistent, already filled directory, like a typical ~/.buildout/default.cfg 3 (after 1 priming run)
No-op rerun kept same dir, directly after each warm run 3

Results

Wall-clock time, all runs

Scenario buildout (pip) buildout (uv) mxdev
Cold 325.9, 309.6 72.4, 77.6 52.2, 50.9
Warm 313.4, 303.4, 299.2 46.0, 49.2, 43.8 28.5, 30.2, 26.4
Shared eggs 23.9, 23.7, 23.6 22.5, 22.9, 22.3 n/a
No-op rerun 10.6, 10.2, 10.4 10.2, 10.1, 9.8 0.21, 0.27, 0.31

Run-to-run variation stays within about ±8 %.

Phase breakdown, median in seconds

buildout

Phase pip cold uv cold pip warm uv warm pip shared uv shared
Bootstrap (venv + pip install zc.buildout) 8.3 8.3 5.5 5.7 5.5 5.5
Setup: extensions, mr.developer git update, develop eggs, recipes 81.6 24.7 79.3 17.5 13.7 12.1
Installing parts 227.9 42.0 218.5 23.0 4.7 4.7
Total 317.7 75.0 303.4 46.0 23.7 22.5

mxdev make install

Phase cold warm
mxenv (uv venv, mxdev, mxmake) 3.6 1.8
plone.releaser + manage buildout2pip / versions2constraints 3.0 2.1
git sources (mxdev -f) 7.3 6.3
mxfiles (mxdev -n) 0.6 0.3
packages (uv pip install -r requirements-mxdev.txt) 31.9 14.0
zope-testrunner, cookiecutter, zope instance 5.2 3.8
Total 51.5 28.5

Size

buildout (pip) buildout (uv) mxdev
Distributions installed 358 358 349
Installed size (eggs + develop-eggs + venv, or venv) 491 MB 419 MB 372 MB
Download cache after a run 24 MB ² 425 MB 424 MB

² In pip mode, buildout downloads distributions itself and hands local files to pip, so the pip cache stays almost empty. Without download-cache or a shared eggs-directory, pip mode has nothing it can reuse, which is why "warm" is barely faster than "cold" there.

The distribution sets are not identical: buildout also installs the releaser, z3c_checkversions,
dependencies, zodbupdate, robot etc. parts. Both cover Plone, the test dependencies and the dev tools.

Interpretation

  1. uv changes buildout a lot. Resolution and installation go from ~310 s to ~75 s (cold) and ~46 s (warm).
    The per-requirement pip subprocess fan-out is the main cost in pip mode.
  2. mxdev is still faster than buildout with uv. The gap comes from:
    • buildout's setup phase (extensions, mr.developer, develop eggs, recipes): 17–25 s vs. ~10 s for git and venv in mxdev;
    • buildout resolving and installing per part in several batches, while mxdev does one uv pip install;
    • buildout's pip-based bootstrap (~8 s cold) vs. uv venv in mxmake (~3.6 s).
  3. A shared eggs directory hides the installer. When all eggs are already there, both buildout modes take about 23 s.
    Most of that is bootstrap and git updates.

No-op reruns

These numbers are not comparable. make install only checks sentinel files (0.3 s) and does not touch git or packages. buildout always updates the git checkouts (always-checkout = force in checkouts.cfg), re-evaluates all parts and regenerates scripts (~10 s), and here the installer makes no difference.

Compatibility finding: installer = uv and picked versions

With the unchanged 6.2 configuration (allow-picked-versions = false), pip mode succeeds and uv mode fails:

While:
  Installing.
  Loading extensions.
  Getting distribution for 'mr.developer==3.0.0'.
Error: Picked: httpcore = 1.0.9

The `httpcore` egg does not have a version pin and `allow-picked-versions = false`.

After pinning httpcore, it fails again in the test part:

While:
  Installing test.
  Getting distribution for 'plone.app.robotframework[test]==3.0.0'.
Error: Picked: grpcio = 1.81.0

Checking every installed distribution against versions.cfg, versions-extra.cfg and the Zope 6.2 version files
found exactly four unpinned distributions:

Distribution Why it is unpinned pip mode uv mode
httpcore Dependency of the plone.versioncheck extension (via httpx) accepted "Picked" error
grpcio Deliberately unpinned in 6.2, exact == pin in robotframework-browser metadata (see [versionannotations]) accepted "Picked" error
grpcio-tools same accepted "Picked" error
protobuf same accepted "Picked" error

So there are two behaviour differences between the modes:

  • pip mode does not enforce allow-picked-versions for the dependencies of buildout extensions, uv mode does;
  • pip mode does not treat a version fixed by an exact == pin in a dependency's metadata as "picked", uv mode does.

The benchmark works around this by pinning the four distributions to the versions pip mode picks, in both modes.

2 Likes

This is awesome news and great work and a relief for legacy Plone installations.
However, I managed to migrate two Plone 4 legacy projects (always policy + theme package) directly to Plone 6.2 including code and database migration on top of a native uv workflow - and removing buildout forever - using AI. This works pretty well and fast (Deepseek 4.1 Flash) and is straight forward if your legacy buildout is basically straight forward (but also worked for me with a ZEO setup).

3 Likes

Thanks for that great feedback. Will look into the differences in the allow-picked-versions false case.

Do you have public examples of pure uv setup built by agents ? One of the features I really like about buildout is the ability to override versions through the CLI to use in CI when new dependencies are published. Would you know if something similar exists with uv ?

You're welcome. I'm using buildout in almost all deployments/development environments and would love to have uv support.

btw: did you see, that @mauritsvanrees already released a 6.0.0a1 version of zc.buildout?

I saw release 6.0.0a1. I wanted to use something proven as the foundation of the refactorings. I will definitely look into integrating the appropriate changes of 6.0 when done with the steps in the vision.

2 Likes

In case anybody wants to run the benchmarks on their machines, I've published the scripts here GitHub - kombinat/buildout-pip-benchmark: Benchmarks buildout (w/o uv support) and mxdev installations of plone coredev · GitHub

3 Likes

I have now released 5.3.0a2 with some fixes.

That has enabled me to run a Plone default buildout for each Plone release since Plone 5.2.12.
Smoke test showed a started zope instance where I could add a default Plone site and access its control panel and check Plone, Zope and Python versions.

Try the new release if you feel like it.

Meanwhile, I kept moving forward and among others, started to integrate changes made in 6.0.0a1.

PS: the date of the release is messed up in CHANGES, my bad, forgot nix isolation

Today I have released zc.buildout 6.0.0 (and zc.recipe.egg 6.0.0):

  • This vendorizes pkg_resources so buildout becomes compatible with latest setuptools.
  • You can use hatchling and others as build-backend in packages that are under development.
  • Support Python 3.15
  • Add support for python316-319 in conditional section expressions.

Note that this does not contain any of the work mentioned above by Godefroid. That would be something for probably a 6.1. I must say I was surprised by the 5.3.0 alpha releases when a 6.0.0a1 was already out. But I suppose this makes it easier for more people to try out, which is useful. Please don't make a 5.3.0 final release with this though, as that would just confuse people. Or at least me. :wink:

The problem with such large scale changes is: it is nice that it seems to work, but someone still has to review it. I had a look a few weeks ago at your draft PR for moving to pytest, but that was of course gigantic, and I did not have time to properly analyse it. FTR, I definitely agree with the direction: those doctests have been a pain (though useful).

And that is just one part. You are adding lots more, including changing the development to use devenv and Dagger, which I don't know. Once you are ready to create PRs, I think this can easily be 5 PRs (the new stacked PR option can be useful), and each of them will be big.

I guess as developers and reviewers we have to get used to that. And maybe use our own AIs to do part of the reviewing, or to more quickly check hunches that something might be wrong or missing. But just a warning: it can take a long time before all of this is reviewed.

But it's good that you are looking into Buildout again, @gotcha. You have taken care of it for years, then I took over as main developer for some years, and this year @erral and @icemac made some good contributions (those made 6.0.0 possible). So it is good that there are more (potential) developers and reviewers.

2 Likes

Nice to hear from you, @mauritsvanrees! I had tried to reach you earlier, but it was conference time and I did not want to hassle you. As I tried to explain, this was first and foremost a chance for me to learn how to delegate writing code to agents.

I was so impressed by the result that I decided to publish it to get feedback. I am still looking for more people to test it.

The reason for using 5.2 as a base was to start from a stable and known situation. I chose to publish an alpha release so that nobody would blindly install it. It was never my intention to publish a final release with this much changed code without community feedback and conversation.

Given the amount of code changed and added, we may well need to fork to give users a choice. From my current experience, I believe that once we trust the harnesses we build around the process and the agents, we will stop writing code ourselves in the short term.

A lot of the added code is my attempt at building such a harness, one I could trust because I had decided not to review a single line of code.

I am currently working on another branch whose goal is to get rid of the dependency on setuptools, pkg_resources (even vendored), and pip entirely. That branch already includes the PEP 660 work you published in 6.0.0. I will not publish anything until all the changes you made for that release have landed.

As a reminder, the main feature of the 5.3.x releases is the new installer = uv option: one batched resolve/process with uv instead of one pip process per package. The pip code path is untouched, and uv is opt-in.

Hi @gotcha, thanks for this!

We gave installer = uv a quick spin at iMio on a few of our Plone 6.1 and 6.2 buildouts and it worked out of the box. Builds are a lot faster... We used to wait several minutes for pip to go through the packages one by one.

We still have plenty of buildouts around and plan to keep using buildout for the time being, so a faster buildout is very welcome. Thanks again!

1 Like