Skip to content

Commit beefa31

Browse files
committed
Add support for the package manager uv
Adds support for the package manager uv: https://docs.astral.sh/uv/ And specifically uv's project mode (`uv sync`), which uses a lockfile rather then a `requirements.txt` file: https://docs.astral.sh/uv/guides/projects/ (While uv does have a pip compatible mode that supports requirements files, using a lockfile is a best practice for deploying an app, and not using one means losing most of the benefits of using uv.) Apps must have a `pyproject.toml`, `uv.lock` and `.python-version` file (all of which are created by default by `uv init` followed by either `uv lock` or any of the other commands that sync/update the lockfile). We don't support `runtime.txt` (or omitting the `.python-version` file) when using uv, since otherwise it leads to a much worse UX in several cases. (For example, when the `runtime.txt` version or buildpack default version differs from `requires-python` in the `pyproject.toml` file. Or worse, when `requires-python` happens to match the distro Python in the underlying base image.) For now, if a `requirements.txt`, `Pipfile` or `poetry.lock` are found, they will take precedence over `uv.lock` for backwards compatibility (currently a warning is shown, but in the future this will become an error). This means users of the third-party `heroku-buildpack-uv` will need to remove that buildpack in order to use the new native uv support, since it exports a `requirements.txt` file during the build which will take precedence. For standard (non-Heroku CI) builds, the buildpack installs dependencies using `uv sync --locked --no-default-groups`, to ensure development/test dependencies are not installed. We don't use `--no-dev` instead, since that only excludes the `dev` group, and some apps may be using other group names and have added them to uv's `default-groups` in `pyproject.toml` so that local development workflows work with a plain `uv sync` with no additional args. (PEP 735 doesn't actually prescribe recommended group names, and in fact uses `test` in its example rather than `dev`, so non-standard names may be common.) For Heroku CI builds, the buildpack installs dependencies using `uv sync --locked`, which will install all default groups. See: https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups For now, we don't expose user provided env vars to `uv sync` (similar to the behaviour for other package managers), however, will start doing so as part of addressing #1451 and #1700. uv is installed into the build cache rather than the slug, so is not available at run-time (since it's not typically needed at run-time and doing so reduces the slug size). Both Python and the entrypoints of installed dependencies are available on `PATH`, so use of `uv run` is not required at run-time to use dependencies in the environment. Closes #1616. GUS-W-17310796.
1 parent ba264cd commit beefa31

File tree

57 files changed

+1246
-35
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1246
-35
lines changed

.github/dependabot.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ updates:
2424
- package-ecosystem: "pip"
2525
directory: "/"
2626
schedule:
27-
interval: "monthly"
27+
# We set this to a more frequent interval than the above since uv is still under rapid
28+
# iteration, and we'll generally want to be on a recent (if not the latest) version.
29+
interval: "weekly"
2830
ignore:
2931
# We're not updating to setuptools v71+ due to its new approach to vendored dependencies:
3032
# https://github.com/heroku/heroku-buildpack-python/pull/1630#issuecomment-2324236653

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ jobs:
4747
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
4848
HEROKU_API_USER: ${{ secrets.HEROKU_API_USER }}
4949
HEROKU_DISABLE_AUTOUPDATE: 1
50-
PARALLEL_SPLIT_TEST_PROCESSES: 75
51-
RSPEC_RETRY_RETRY_COUNT: 2
50+
PARALLEL_SPLIT_TEST_PROCESSES: 85
51+
RSPEC_RETRY_RETRY_COUNT: 1
5252
steps:
5353
- name: Checkout
5454
uses: actions/checkout@v4

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Added support for the package manager uv. ([#1791](https://github.com/heroku/heroku-buildpack-python/pull/1791))
56

67
## [v285] - 2025-05-08
78

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the [Getting Started on Heroku with Python](https://devcenter.heroku.com/art
1414

1515
## Application Requirements
1616

17-
A `requirements.txt`, `Pipfile` or `poetry.lock` file must be present in the root (top-level) directory of your app's source code.
17+
A `requirements.txt`, `Pipfile`, `poetry.lock`, or `uv.lock` file must be present in the root (top-level) directory of your app's source code.
1818

1919
## Configuration
2020

bin/compile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ source "${BUILDPACK_DIR}/lib/pipenv.sh"
3131
source "${BUILDPACK_DIR}/lib/poetry.sh"
3232
source "${BUILDPACK_DIR}/lib/python_version.sh"
3333
source "${BUILDPACK_DIR}/lib/python.sh"
34+
source "${BUILDPACK_DIR}/lib/uv.sh"
3435

3536
compile_start_time=$(nowms)
3637

@@ -180,6 +181,9 @@ case "${package_manager}" in
180181
poetry)
181182
poetry::install_poetry "${python_home}" "${python_major_version}" "${CACHE_DIR}" "${EXPORT_PATH}"
182183
;;
184+
uv)
185+
uv::install_uv "${CACHE_DIR}" "${EXPORT_PATH}" "${python_home}"
186+
;;
183187
*)
184188
utils::abort_internal_error "Unhandled package manager: ${package_manager}"
185189
;;
@@ -189,8 +193,8 @@ meta_time "package_manager_install_duration" "${package_manager_install_start_ti
189193
# SQLite3 support.
190194
# Installs the sqlite3 dev headers and sqlite3 binary but not the
191195
# libsqlite3-0 library since that exists in the base image.
192-
# We skip this step on Python 3.13 or when using Poetry, as a first step towards removing this feature.
193-
if [[ "${python_major_version}" == +(3.9|3.10|3.11|3.12) && "${package_manager}" != "poetry" ]]; then
196+
# We skip this step on Python 3.13 or when using Poetry/uv, as a first step towards removing this feature.
197+
if [[ "${python_major_version}" == +(3.9|3.10|3.11|3.12) && "${package_manager}" != +(poetry|uv) ]]; then
194198
install_sqlite_start_time=$(nowms)
195199
source "${BUILDPACK_DIR}/bin/steps/sqlite3"
196200
buildpack_sqlite3_install
@@ -209,6 +213,9 @@ case "${package_manager}" in
209213
poetry)
210214
poetry::install_dependencies
211215
;;
216+
uv)
217+
uv::install_dependencies
218+
;;
212219
*)
213220
utils::abort_internal_error "Unhandled package manager: ${package_manager}"
214221
;;

bin/report

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ STRING_FIELDS=(
7676
python_version_requested
7777
python_version
7878
setuptools_version
79+
uv_version
7980
wheel_version
8081
)
8182

@@ -101,7 +102,6 @@ ALL_OTHER_FIELDS=(
101102
setup_py_only
102103
sqlite_install_duration
103104
total_duration
104-
uv_lockfile
105105
)
106106

107107
for field in "${STRING_FIELDS[@]}"; do

lib/cache.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ function cache::restore() {
115115
cache_invalidation_reasons+=("The Poetry version has changed from ${cached_poetry_version:-"unknown"} to ${POETRY_VERSION}")
116116
fi
117117
;;
118+
uv)
119+
local cached_uv_version
120+
cached_uv_version="$(meta_prev_get "uv_version")"
121+
# uv support was added after the metadata store, so we'll always have the version here.
122+
if [[ "${cached_uv_version}" != "${UV_VERSION:?}" ]]; then
123+
cache_invalidation_reasons+=("The uv version has changed from ${cached_uv_version:-"unknown"} to ${UV_VERSION}")
124+
fi
125+
;;
118126
*)
119127
utils::abort_internal_error "Unhandled package manager: ${package_manager}"
120128
;;
@@ -132,6 +140,7 @@ function cache::restore() {
132140
"${cache_dir}/.heroku/python" \
133141
"${cache_dir}/.heroku/python-poetry" \
134142
"${cache_dir}/.heroku/python-stack" \
143+
"${cache_dir}/.heroku/python-uv" \
135144
"${cache_dir}/.heroku/python-version" \
136145
"${cache_dir}/.heroku/requirements.txt"
137146

lib/package_manager.sh

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ function package_manager::determine_package_manager() {
4949
package_managers_found_display_text+=("poetry.lock (Poetry)")
5050
fi
5151

52+
if [[ -f "${build_dir}/uv.lock" ]]; then
53+
package_managers_found+=(uv)
54+
package_managers_found_display_text+=("uv.lock (uv)")
55+
fi
56+
5257
# TODO: Deprecate/sunset this fallback, since using setup.py declared dependencies is
5358
# not a best practice, and we can only guess as to which package manager to use.
5459
if ((${#package_managers_found[@]} == 0)) && [[ -f "${build_dir}/setup.py" ]]; then
@@ -59,10 +64,6 @@ function package_manager::determine_package_manager() {
5964
meta_set "setup_py_only" "false"
6065
fi
6166

62-
if [[ -f "${build_dir}/uv.lock" ]]; then
63-
meta_set "uv_lockfile" "true"
64-
fi
65-
6667
local num_package_managers_found=${#package_managers_found[@]}
6768

6869
case "${num_package_managers_found}" in
@@ -75,8 +76,8 @@ function package_manager::determine_package_manager() {
7576
Error: Couldn't find any supported Python package manager files.
7677
7778
A Python app on Heroku must have either a 'requirements.txt',
78-
'Pipfile' or 'poetry.lock' package manager file in the root
79-
directory of its source code.
79+
'Pipfile', 'poetry.lock' or 'uv.lock' package manager file in
80+
the root directory of its source code.
8081
8182
Currently the root directory of your app contains:
8283
@@ -93,10 +94,10 @@ function package_manager::determine_package_manager() {
9394
Otherwise, add a package manager file to your app. If your app has
9495
no dependencies, then create an empty 'requirements.txt' file.
9596
96-
If you would like to see support for the package manager uv,
97-
please vote and comment on these GitHub issues:
98-
https://github.com/heroku/heroku-buildpack-python/issues/1616
99-
https://github.com/heroku/roadmap/issues/323
97+
If you aren't sure which package manager to use, we recommend
98+
trying uv, since it supports lockfiles, is extremely fast, and
99+
is actively maintained by a full-time team:
100+
https://docs.astral.sh/uv/
100101
101102
For help with using Python on Heroku, see:
102103
https://devcenter.heroku.com/articles/getting-started-with-python
@@ -127,6 +128,11 @@ function package_manager::determine_package_manager() {
127128
128129
Decide which package manager you want to use with your app, and
129130
then delete the file(s) and any config from the others.
131+
132+
If you aren't sure which package manager to use, we recommend
133+
trying uv, since it supports lockfiles, is extremely fast, and
134+
is actively maintained by a full-time team:
135+
https://docs.astral.sh/uv/
130136
EOF
131137

132138
if [[ "${package_managers_found[*]}" == *"poetry"* ]]; then
@@ -138,6 +144,15 @@ function package_manager::determine_package_manager() {
138144
EOF
139145
fi
140146

147+
if [[ "${package_managers_found[*]}" == *"uv"* ]]; then
148+
output::notice <<-EOF
149+
Note: We recently added support for the package manager uv.
150+
If you are using a third-party uv buildpack you must remove
151+
it, otherwise the requirements.txt file it generates will cause
152+
the warning above.
153+
EOF
154+
fi
155+
141156
meta_set "package_manager_multiple_found" "$(
142157
IFS=,
143158
echo "${package_managers_found[*]}"

lib/python_version.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,31 @@ function python_version::warn_if_python_version_file_missing() {
432432

433433
case "${python_version_origin}" in
434434
default | cached)
435+
if [[ "${package_manager}" == "uv" ]]; then
436+
output::error <<-EOF
437+
Error: No Python version was specified.
438+
439+
When using the package manager uv on Heroku, you must specify
440+
your app's Python version with a .python-version file.
441+
442+
To add a .python-version file:
443+
444+
1. Make sure you are in the root directory of your app
445+
and not a subdirectory.
446+
2. Run 'uv python pin ${python_major_version}'
447+
(adjust to match your app's major Python version).
448+
3. Commit the changes to your Git repository using
449+
'git add --all' and then 'git commit'.
450+
451+
Note: We strongly recommend that you don't specify the Python
452+
patch version number in your .python-version file, since it will
453+
pin your app to an exact Python version and so stop your app from
454+
receiving security updates each time it builds.
455+
EOF
456+
meta_set "failure_reason" "python-version-file::not-found"
457+
exit 1
458+
fi
459+
435460
output::warning <<-EOF
436461
Warning: No Python version was specified.
437462
@@ -459,6 +484,33 @@ function python_version::warn_if_python_version_file_missing() {
459484
EOF
460485
;;
461486
runtime.txt)
487+
if [[ "${package_manager}" == "uv" ]]; then
488+
output::error <<-EOF
489+
Error: The runtime.txt file isn't supported when using uv.
490+
491+
When using the package manager uv on Heroku, you must specify
492+
your app's Python version with a .python-version file and not
493+
a runtime.txt file.
494+
495+
To switch to a .python-version file:
496+
497+
1. Make sure you are in the root directory of your app
498+
and not a subdirectory.
499+
2. Delete your runtime.txt file.
500+
3. Run 'uv python pin ${python_major_version}'
501+
(adjust to match your app's major Python version).
502+
4. Commit the changes to your Git repository using
503+
'git add --all' and then 'git commit'.
504+
505+
Note: We strongly recommend that you don't specify the Python
506+
patch version number in your .python-version file, since it will
507+
pin your app to an exact Python version and so stop your app from
508+
receiving security updates each time it builds.
509+
EOF
510+
meta_set "failure_reason" "runtime-txt::not-supported"
511+
exit 1
512+
fi
513+
462514
output::warning <<-EOF
463515
Warning: The runtime.txt file is deprecated.
464516

0 commit comments

Comments
 (0)