Skip to content

Feature - add uv support - #1260

Open
dominikmuellerdmde wants to merge 14 commits into
cloudfoundry:masterfrom
dominikmuellerdmde:feature/uv
Open

dominikmuellerdmde wants to merge 14 commits into
cloudfoundry:masterfrom
dominikmuellerdmde:feature/uv

Conversation

@dominikmuellerdmde

@dominikmuellerdmde dominikmuellerdmde commented Oct 30, 2026 •

Copy link
Copy Markdown

Warning

This PR was written largely with AI assistance (Claude Code). That includes the code, the tests and this description. I have reviewed the changes and tested them on our own Cloud Foundry foundation, but please review and test it as carefully as any change you didn't write yourself. Don't assume the AI got it right.

A short explanation of the proposed change:

This adds support for apps managed with uv (pyproject.toml + uv.lock):

  • bin/detect recognises apps that contain both a uv.lock and a pyproject.toml.
  • The uv wheel already listed in manifest.yml is installed with pip into /tmp/uv, so uv stays out of the droplet.
  • The Python version is read from .python-version unless a runtime.txt exists. uv never downloads its own Python.
  • Dependencies are installed with uv sync --frozen --no-dev --no-editable. Every package is checked against the hashes in uv.lock, and staging fails on a mismatch. Indexes configured via [[tool.uv.index]] are used, with credentials passed as UV_INDEX_<NAME>_* environment variables.
  • Vendored (offline) apps are installed from vendor/ with uv pip install --offline, also checked against the hashes in uv.lock.
  • A requirements.txt exported from uv.lock is used for the existing package-specific steps (libffi, libmemcached, Django collectstatic). Package names in requirements are now matched case-insensitively and with -, _ and . treated as equal, as PEP 503 specifies.
  • If the app also contains a requirements.txt, it takes precedence: uv.lock and .python-version are ignored and pip is used as before. Pipfile apps are unchanged.
  • The README documents the behaviour.

Why the existing uv support is not sufficient:

manifest.yml already contains a uv dependency (since 0.12.23, now 0.13.5 for cflinuxfs4 and cflinuxfs5), but nothing in the buildpack uses it. The commits that added it only touch manifest.yml. As a result, uv apps don't work today:

  • Detection: bin/detect only looks for requirements.txt, setup.py, environment.yml and Pipfile, so an app with just pyproject.toml and uv.lock is not detected as a Python app.
  • Installation: if the buildpack is forced with -b, Python and pip are installed but the dependency step is skipped because there is no requirements.txt. Staging succeeds and the app crashes at start with ModuleNotFoundError.
  • The uv dependency itself: the manifest entry is a pip wheel inside a .tgz, not the standalone uv binary. It has to be installed with pip before it can run, which this PR does.

An explanation of the use cases your change solves:

uv is becoming one of the standard tools for Python projects. Today, teams using it have to export a requirements.txt before every push (uv export) and keep it in sync with uv.lock, which is easy to forget and loses the hash checking from the lock file. With this change, a uv project can be pushed as-is:

  • dependencies are installed exactly as locked, with hashes checked;
  • the Python version pinned with uv python pin is respected;
  • private indexes work with credentials from the app environment;
  • air-gapped foundations are supported through vendored wheels;
  • Django apps get collectstatic as with pip.

Testing:

  • Unit tests for supply, finalize and requirements.

  • New integration tests in src/python/integration/uv_test.go:

    • a basic uv app;
    • a uv.lock with a wrong hash, which must fail staging;
    • a Django app, checking that collectstatic runs;
    • an app with both uv.lock and requirements.txt, checking that pip is used.
  • The full integration suite was run against a local CF on cflinuxfs4, cached and uncached. The uv tests and all other suites pass. The only failures were the AppDynamics, Dynatrace and Proxy tests, which also fail with an unmodified master build on that foundation.

  • I have viewed signed and have submitted the Contributor License Agreement

  • I have made this pull request to the master branch

  • I have added an integration test

When an app provides a uv.lock but no requirements.txt, install uv and
run 'uv export --frozen' to generate a temporary requirements.txt, just
like it is done for Pipfile.lock. The regular pip flow then installs the
dependencies, so vendoring, libffi/libmemcache detection and stale
package removal keep working for uv apps.

Only the default (non-dev) dependencies are exported. The project itself
is emitted only when it is a package and is then installed non-editable.
uv projects usually pin their interpreter in .python-version instead of
runtime.txt. For apps with a uv.lock, translate the first CPython version
in that file into runtime.txt. An explicit runtime.txt still wins, and
apps without a uv.lock keep ignoring .python-version.
'uv export' does not emit the indexes configured via [[tool.uv.index]],
so pip would silently fall back to PyPI for apps that use a private
package index. Read the registry URLs from uv.lock and prepend them as
-i/--extra-index-url options, as is already done for Pipfile.lock
sources. PyPI stays the primary index when it is used.

Indexes configured via PIP_INDEX_URL or PIP_EXTRA_INDEX_URL take
precedence, as uv strips credentials from the URLs in uv.lock.
Add a uv-managed Flask fixture that pins its Python version via
.python-version and has a dev dependency group, and verify that it
deploys with the dependencies exported from uv.lock. Also verify that
an existing requirements.txt takes precedence over uv.lock.
Package names are normalized as defined in PEP 503 before comparing
them. Generated requirements files, such as those exported by uv, use
lowercase names, so packages like 'PyNaCl' or 'pyOpenSSL' were not
detected and libffi was not installed for them.
For apps using Pipfile.lock or uv.lock, supply generates a temporary
requirements.txt and deletes it at the end of the supply phase. Finalize
then no longer found Django in it and silently skipped collectstatic.

Keep a copy of the generated requirements.txt in the deps dir and let
finalize check it as well.
Install the packages of uv apps with 'uv sync --frozen' directly into
the Python installation of the buildpack instead of exporting them for
pip. uv verifies every package against the hashes in uv.lock and
supports all of uv's features, like workspaces, [tool.uv.sources] and
index configuration including credentials via UV_INDEX_* variables.
Reading the package indexes from uv.lock for pip is no longer needed.

uv.lock is still exported to a temporary requirements.txt, now with
hashes, so that packages which need libffi, libmemcached or collectstatic
are detected as before.

uv uses the system certificate store like pip does, and its cache is
kept in the app cache. Vendored uv apps are still installed with pip.
'uv sync' only downloads the files referenced in uv.lock and cannot
install from a vendor directory. For vendored uv apps, install the
packages exported from uv.lock with 'uv pip install --offline
--no-index --find-links vendor' instead. The exported requirements
contain the hashes from uv.lock, so modified or mismatching files in
the vendor directory are rejected.
Verify that uv apps are installed with uv sync, that staging fails when
a package does not match the hash in uv.lock, and that collectstatic
runs for a Django app managed with uv.
The buildpack manifest now provides uv as a pip wheel built by the
buildpacks pipeline (for cflinuxfs4 and cflinuxfs5) instead of the
upstream release binary. Install that wheel with pip into /tmp/uv, so
uv stays out of the droplet, and run uv from /tmp/uv/bin/uv.
- Ignore .python-version when the app provides a requirements.txt, as
  uv.lock is ignored in that case as well.
- Only keep a copy of the generated requirements.txt in the deps dir for
  uv apps. Pipfile apps keep their previous behaviour, so collectstatic
  does not suddenly run for Pipfile Django apps.
- Only detect uv apps that provide both uv.lock and pyproject.toml.
- Start from an empty /tmp/uv when installing uv.
- Mention the build backend when a vendored uv install fails.
- Compile the package name regex once.
@linux-foundation-easycla

linux-foundation-easycla Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

@dominikmuellerdmde dominikmuellerdmde changed the title Feature/uv Feature - add uv support Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant