Skip to content

Commit

Permalink
Deprecate support for the runtime.txt file
Browse files Browse the repository at this point in the history
The `runtime.txt` file is a classic Heroku Python buildpack invention
that's not widely supported in the Python ecosystem. Instead, most
other tooling (pyenv, package managers, GitHub Actions, dependency
update bots etc) support/use the `.python-version` file.

As such, we recently added `.python-version` support to both the Python
CNB and the classic Python buildpack, and updated all documentation and
guides to use it instead of the `runtime.txt` file. eg:
https://devcenter.heroku.com/articles/python-runtimes

We would prefer apps use the new file, since it helps ensure their
deployed app is using the same Python version used locally (via eg
pyenv or uv) or in CI.

As such this adds a deprecation warning for apps using `runtime.txt`.

Closes #1642.
GUS-W-16878260.
  • Loading branch information
edmorley committed Feb 4, 2025
1 parent b3e0b29 commit b290f6d
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 79 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- Deprecated support for the `runtime.txt` file. ([#1743](https://github.com/heroku/heroku-buildpack-python/pull/1743))
- Improved the error messages shown when `.python-version`, `runtime.txt` or `Pipfile.lock` contain an invalid Python version. ([#1743](https://github.com/heroku/heroku-buildpack-python/pull/1743))

## [v275] - 2025-01-13

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ For example, to request the latest patch release of Python 3.13, create a `.pyth
the root directory of your app containing:
`3.13`

We strongly recommend that you use the major version form instead of pinning to an exact version,
since it will allow your app to receive Python security updates.

The buildpack will look for a Python version in the following places (in descending order of precedence):

1. `runtime.txt` file (deprecated)
Expand Down
26 changes: 25 additions & 1 deletion bin/compile
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ meta_set "python_version_reason" "${python_version_origin}"

# TODO: More strongly recommend specifying a Python version (eg switch the messaging to
# be a warning instead, after version resolution, and mention .python-version inline)
# TODO: Add runtime.txt deprecation warning.
case "${python_version_origin}" in
default)
output::step "No Python version was specified. Using the buildpack default: Python ${requested_python_version}"
Expand All @@ -145,6 +144,31 @@ python_major_version="${python_full_version%.*}"
meta_set "python_version" "${python_full_version}"
meta_set "python_version_major" "${python_major_version}"

if [[ "${python_version_origin}" == "runtime.txt" ]]; then
output::warning <<-EOF
Warning: The runtime.txt file is deprecated.
The runtime.txt file is deprecated since it has been replaced
by the more widely supported .python-version file.
Please delete your runtime.txt file and create a new file named:
.python-version
Make sure to include the '.' at the start of the filename.
In the new file, specify your app's Python version without
quotes or a 'python-' prefix. For example:
${python_major_version}
We strongly recommend that you use the major version form
instead of pinning to an exact version, since it will allow
your app to receive Python security updates.
In the future support for runtime.txt will be removed and
this warning will be made an error.
EOF
fi

cache::restore "${BUILD_DIR}" "${CACHE_DIR}" "${STACK}" "${cached_python_full_version}" "${python_full_version}" "${package_manager}"

# The directory for the .profile.d scripts.
Expand Down
101 changes: 62 additions & 39 deletions lib/python_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ PYTHON_FULL_VERSION_REGEX="${INT_REGEX}\.${INT_REGEX}\.${INT_REGEX}"
# resolved to an exact Python version.
#
# If an app specifies the Python version via multiple means, then the order of precedence is:
# 1. runtime.txt
# 2. .python-version
# 3. Pipfile.lock (`python_full_version` field)
# 4. Pipfile.lock (`python_version` field)
# 1. `runtime.txt` file (deprecated)
# 2. `.python-version` file (recommended)
# 3. The `python_full_version` field in the `Pipfile.lock` file
# 4. The `python_version` field in the `Pipfile.lock` file
#
# If a version wasn't specified by the app, then new apps/those with an empty cache will use
# a buildpack default version for the first build, and then subsequent cached builds will use
Expand Down Expand Up @@ -100,21 +100,29 @@ function python_version::parse_runtime_txt() {
output::error <<-EOF
Error: Invalid Python version in runtime.txt.
The Python version specified in 'runtime.txt' isn't in
the correct format.
The Python version specified in your runtime.txt file isn't
in the correct format.
The following file contents were found:
The following file contents were found, which aren't valid:
${contents}
However, the version must be specified as either:
1. 'python-<major>.<minor>' (recommended, for automatic patch updates)
2. 'python-<major>.<minor>.<patch>' (to pin to an exact patch version)
However, the runtime.txt file is deprecated since it has
been replaced by the .python-version file. As such, we
recommend that you switch to using a .python-version file
instead of fixing your runtime.txt file.
Remember to include the 'python-' prefix. Comments aren't supported.
Please delete your runtime.txt file and create a new file named:
.python-version
For example, to request the latest version of Python ${DEFAULT_PYTHON_MAJOR_VERSION},
update the 'runtime.txt' file so it contains:
python-${DEFAULT_PYTHON_MAJOR_VERSION}
Make sure to include the '.' at the start of the filename.
In the new file, specify your app's Python version without
quotes or a 'python-' prefix. For example:
${DEFAULT_PYTHON_MAJOR_VERSION}
We strongly recommend that you use the major version form
instead of pinning to an exact version, since it will allow
your app to receive Python security updates.
EOF
meta_set "failure_reason" "runtime-txt::invalid-version"
exit 1
Expand Down Expand Up @@ -144,22 +152,26 @@ function python_version::parse_python_version_file() {
output::error <<-EOF
Error: Invalid Python version in .python-version.
The Python version specified in '.python-version' isn't in
the correct format.
The Python version specified in your .python-version file
isn't in the correct format.
The following version was found:
${line}
However, the version must be specified as either:
1. '<major>.<minor>' (recommended, for automatic patch updates)
2. '<major>.<minor>.<patch>' (to pin to an exact patch version)
However, the Python version must be specified as either:
1. The major version only: 3.X (recommended)
2. An exact patch version: 3.X.Y
Don't include quotes or a 'python-' prefix. To include
comments, add them on their own line, prefixed with '#'.
For example, to request the latest version of Python ${DEFAULT_PYTHON_MAJOR_VERSION},
update the '.python-version' file so it contains:
update your .python-version file so it contains:
${DEFAULT_PYTHON_MAJOR_VERSION}
We strongly recommend that you use the major version form
instead of pinning to an exact version, since it will allow
your app to receive Python security updates.
EOF
meta_set "failure_reason" "python-version-file::invalid-version"
exit 1
Expand All @@ -169,10 +181,13 @@ function python_version::parse_python_version_file() {
output::error <<-EOF
Error: Invalid Python version in .python-version.
No Python version was found in the '.python-version' file.
No Python version was found in your .python-version file.
Update the file so that it contains a valid Python version
such as '${DEFAULT_PYTHON_MAJOR_VERSION}'.
Update the file so that it contains a valid Python version.
For example, to request the latest version of Python ${DEFAULT_PYTHON_MAJOR_VERSION},
update your .python-version file so it contains:
${DEFAULT_PYTHON_MAJOR_VERSION}
If the file already contains a version, check the line doesn't
begin with a '#', otherwise it will be treated as a comment.
Expand All @@ -184,8 +199,7 @@ function python_version::parse_python_version_file() {
output::error <<-EOF
Error: Invalid Python version in .python-version.
Multiple Python versions were found in the '.python-version'
file:
Multiple versions were found in your .python-version file:
$(
IFS=$'\n'
Expand All @@ -194,8 +208,8 @@ function python_version::parse_python_version_file() {
Update the file so it contains only one Python version.
If the additional versions are actually comments, prefix
those lines with '#'.
If you have added comments to the file, make sure that those
lines begin with a '#', so that they are ignored.
EOF
meta_set "failure_reason" "python-version-file::multiple-versions"
exit 1
Expand Down Expand Up @@ -245,20 +259,24 @@ function python_version::read_pipenv_python_version() {
echo "${version}"
else
output::error <<-EOF
Error: Invalid Python version in Pipfile / Pipfile.lock.
Error: Invalid Python version in Pipfile.lock.
The Python version specified in Pipfile / Pipfile.lock by the
'python_version' or 'python_full_version' field isn't valid.
The Python version specified in your Pipfile.lock file by the
'python_version' or 'python_full_version' fields isn't valid.
The following version was found:
${version}
However, the version must be specified as either:
1. '<major>.<minor>' (recommended, for automatic patch updates)
2. '<major>.<minor>.<patch>' (to pin to an exact patch version)
However, the Python version must be specified as either:
1. The major version only: 3.X (recommended)
2. An exact patch version: 3.X.Y
Please update your Pipfile to use a valid Python version and
then run 'pipenv lock' to regenerate Pipfile.lock.
Please update your 'Pipfile' to use a valid Python version and
then run 'pipenv lock' to regenerate the lockfile.
We strongly recommend that you use the major version form
instead of pinning to an exact version, since it will allow
your app to receive Python security updates.
For more information, see:
https://pipenv.pypa.io/en/stable/specifiers.html#specifying-versions-of-python
Expand Down Expand Up @@ -297,10 +315,15 @@ function python_version::resolve_python_version() {
As such, it's no longer supported by this buildpack:
https://devcenter.heroku.com/articles/python-support#supported-python-versions
Please upgrade to at least Python 3.${OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION} by creating a
.python-version file in the root directory of your app,
that contains a Python version like:
3.${OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION}
Please upgrade to at least Python 3.${OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION} by configuring an
explicit Python version for your app.
Create a .python-version file in the root directory of your
app, that contains a Python version like:
3.${NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION}
When creating this file make sure to include the '.' at the
start of the filename.
EOF
else
output::error <<-EOF
Expand Down
40 changes: 24 additions & 16 deletions spec/hatchet/pipenv_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,20 +219,24 @@
expect(clean_output(app.output)).to include(<<~OUTPUT)
remote: -----> Python app detected
remote:
remote: ! Error: Invalid Python version in Pipfile / Pipfile.lock.
remote: ! Error: Invalid Python version in Pipfile.lock.
remote: !
remote: ! The Python version specified in Pipfile / Pipfile.lock by the
remote: ! 'python_version' or 'python_full_version' field isn't valid.
remote: ! The Python version specified in your Pipfile.lock file by the
remote: ! 'python_version' or 'python_full_version' fields isn't valid.
remote: !
remote: ! The following version was found:
remote: ! ^3.12
remote: !
remote: ! However, the version must be specified as either:
remote: ! 1. '<major>.<minor>' (recommended, for automatic patch updates)
remote: ! 2. '<major>.<minor>.<patch>' (to pin to an exact patch version)
remote: ! However, the Python version must be specified as either:
remote: ! 1. The major version only: 3.X (recommended)
remote: ! 2. An exact patch version: 3.X.Y
remote: !
remote: ! Please update your 'Pipfile' to use a valid Python version and
remote: ! then run 'pipenv lock' to regenerate the lockfile.
remote: ! Please update your Pipfile to use a valid Python version and
remote: ! then run 'pipenv lock' to regenerate Pipfile.lock.
remote: !
remote: ! We strongly recommend that you use the major version form
remote: ! instead of pinning to an exact version, since it will allow
remote: ! your app to receive Python security updates.
remote: !
remote: ! For more information, see:
remote: ! https://pipenv.pypa.io/en/stable/specifiers.html#specifying-versions-of-python
Expand All @@ -251,20 +255,24 @@
expect(clean_output(app.output)).to include(<<~OUTPUT)
remote: -----> Python app detected
remote:
remote: ! Error: Invalid Python version in Pipfile / Pipfile.lock.
remote: ! Error: Invalid Python version in Pipfile.lock.
remote: !
remote: ! The Python version specified in Pipfile / Pipfile.lock by the
remote: ! 'python_version' or 'python_full_version' field isn't valid.
remote: ! The Python version specified in your Pipfile.lock file by the
remote: ! 'python_version' or 'python_full_version' fields isn't valid.
remote: !
remote: ! The following version was found:
remote: ! 3.9.*
remote: !
remote: ! However, the version must be specified as either:
remote: ! 1. '<major>.<minor>' (recommended, for automatic patch updates)
remote: ! 2. '<major>.<minor>.<patch>' (to pin to an exact patch version)
remote: ! However, the Python version must be specified as either:
remote: ! 1. The major version only: 3.X (recommended)
remote: ! 2. An exact patch version: 3.X.Y
remote: !
remote: ! Please update your Pipfile to use a valid Python version and
remote: ! then run 'pipenv lock' to regenerate Pipfile.lock.
remote: !
remote: ! Please update your 'Pipfile' to use a valid Python version and
remote: ! then run 'pipenv lock' to regenerate the lockfile.
remote: ! We strongly recommend that you use the major version form
remote: ! instead of pinning to an exact version, since it will allow
remote: ! your app to receive Python security updates.
remote: !
remote: ! For more information, see:
remote: ! https://pipenv.pypa.io/en/stable/specifiers.html#specifying-versions-of-python
Expand Down
22 changes: 22 additions & 0 deletions spec/hatchet/python_update_warning_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@
expect(clean_output(app.output)).to include(<<~OUTPUT)
remote: -----> Python app detected
remote: -----> Using Python 3.9.0 specified in runtime.txt
remote:
remote: ! Warning: The runtime.txt file is deprecated.
remote: !
remote: ! The runtime.txt file is deprecated since it has been replaced
remote: ! by the more widely supported .python-version file.
remote: !
remote: ! Please delete your runtime.txt file and create a new file named:
remote: ! .python-version
remote: !
remote: ! Make sure to include the '.' at the start of the filename.
remote: !
remote: ! In the new file, specify your app's Python version without
remote: ! quotes or a 'python-' prefix. For example:
remote: ! 3.9
remote: !
remote: ! We strongly recommend that you use the major version form
remote: ! instead of pinning to an exact version, since it will allow
remote: ! your app to receive Python security updates.
remote: !
remote: ! In the future support for runtime.txt will be removed and
remote: ! this warning will be made an error.
remote:
remote: -----> Installing Python 3.9.0
remote:
remote: ! Warning: Support for Python 3.9 is ending soon!
Expand Down
Loading

0 comments on commit b290f6d

Please sign in to comment.