From b290f6d043657299bd16ba9dfb8300840365ca1a Mon Sep 17 00:00:00 2001 From: Ed Morley <501702+edmorley@users.noreply.github.com> Date: Tue, 4 Feb 2025 22:35:41 +0000 Subject: [PATCH] Deprecate support for the `runtime.txt` file 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. --- CHANGELOG.md | 2 + README.md | 3 + bin/compile | 26 +++++- lib/python_version.sh | 101 +++++++++++++-------- spec/hatchet/pipenv_spec.rb | 40 ++++---- spec/hatchet/python_update_warning_spec.rb | 22 +++++ spec/hatchet/python_version_spec.rb | 82 ++++++++++++----- 7 files changed, 197 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caeaa579b..ca8032023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 3c76ddc14..557d77e6d 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/bin/compile b/bin/compile index 08232b5e7..10a2187ce 100755 --- a/bin/compile +++ b/bin/compile @@ -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}" @@ -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. diff --git a/lib/python_version.sh b/lib/python_version.sh index f7bacb6ad..eaf201e9a 100644 --- a/lib/python_version.sh +++ b/lib/python_version.sh @@ -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 @@ -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-.' (recommended, for automatic patch updates) - 2. 'python-..' (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 @@ -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. '.' (recommended, for automatic patch updates) - 2. '..' (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 @@ -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. @@ -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' @@ -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 @@ -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. '.' (recommended, for automatic patch updates) - 2. '..' (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 @@ -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 diff --git a/spec/hatchet/pipenv_spec.rb b/spec/hatchet/pipenv_spec.rb index b97bfe7b9..6949e3934 100644 --- a/spec/hatchet/pipenv_spec.rb +++ b/spec/hatchet/pipenv_spec.rb @@ -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. '.' (recommended, for automatic patch updates) - remote: ! 2. '..' (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 @@ -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. '.' (recommended, for automatic patch updates) - remote: ! 2. '..' (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 diff --git a/spec/hatchet/python_update_warning_spec.rb b/spec/hatchet/python_update_warning_spec.rb index 09123a5b6..23365eaba 100644 --- a/spec/hatchet/python_update_warning_spec.rb +++ b/spec/hatchet/python_update_warning_spec.rb @@ -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! diff --git a/spec/hatchet/python_version_spec.rb b/spec/hatchet/python_version_spec.rb index 635bca936..e01771661 100644 --- a/spec/hatchet/python_version_spec.rb +++ b/spec/hatchet/python_version_spec.rb @@ -148,22 +148,26 @@ remote: remote: ! Error: Invalid Python version in .python-version. remote: ! - remote: ! The Python version specified in '.python-version' isn't in - remote: ! the correct format. + remote: ! The Python version specified in your .python-version file + remote: ! isn't in the correct format. remote: ! remote: ! The following version was found: remote: ! 3.12.0invalid remote: ! - remote: ! However, the version must be specified as either: - remote: ! 1. '.' (recommended, for automatic patch updates) - remote: ! 2. '..' (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: ! Don't include quotes or a 'python-' prefix. To include remote: ! comments, add them on their own line, prefixed with '#'. remote: ! remote: ! For example, to request the latest version of Python #{DEFAULT_PYTHON_MAJOR_VERSION}, - remote: ! update the '.python-version' file so it contains: + remote: ! update your .python-version file so it contains: remote: ! #{DEFAULT_PYTHON_MAJOR_VERSION} + 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: ! Push rejected, failed to compile Python app. OUTPUT @@ -181,10 +185,13 @@ remote: remote: ! Error: Invalid Python version in .python-version. remote: ! - remote: ! No Python version was found in the '.python-version' file. + remote: ! No Python version was found in your .python-version file. + remote: ! + remote: ! Update the file so that it contains a valid Python version. remote: ! - remote: ! Update the file so that it contains a valid Python version - remote: ! such as '#{DEFAULT_PYTHON_MAJOR_VERSION}'. + remote: ! For example, to request the latest version of Python #{DEFAULT_PYTHON_MAJOR_VERSION}, + remote: ! update your .python-version file so it contains: + remote: ! #{DEFAULT_PYTHON_MAJOR_VERSION} remote: ! remote: ! If the file already contains a version, check the line doesn't remote: ! begin with a '#', otherwise it will be treated as a comment. @@ -205,8 +212,7 @@ remote: remote: ! Error: Invalid Python version in .python-version. remote: ! - remote: ! Multiple Python versions were found in the '.python-version' - remote: ! file: + remote: ! Multiple versions were found in your .python-version file: remote: ! remote: ! // invalid comment remote: ! 3.12 @@ -214,8 +220,8 @@ remote: ! remote: ! Update the file so it contains only one Python version. remote: ! - remote: ! If the additional versions are actually comments, prefix - remote: ! those lines with '#'. + remote: ! If you have added comments to the file, make sure that those + remote: ! lines begin with a '#', so that they are ignored. remote: remote: ! Push rejected, failed to compile Python app. OUTPUT @@ -312,21 +318,29 @@ remote: remote: ! Error: Invalid Python version in runtime.txt. remote: ! - remote: ! The Python version specified in 'runtime.txt' isn't in - remote: ! the correct format. + remote: ! The Python version specified in your runtime.txt file isn't + remote: ! in the correct format. remote: ! - remote: ! The following file contents were found: + remote: ! The following file contents were found, which aren't valid: remote: ! python-3.12.0invalid remote: ! - remote: ! However, the version must be specified as either: - remote: ! 1. 'python-.' (recommended, for automatic patch updates) - remote: ! 2. 'python-..' (to pin to an exact patch version) + remote: ! However, the runtime.txt file is deprecated since it has + remote: ! been replaced by the .python-version file. As such, we + remote: ! recommend that you switch to using a .python-version file + remote: ! instead of fixing your runtime.txt file. remote: ! - remote: ! Remember to include the 'python-' prefix. Comments aren't supported. + remote: ! Please delete your runtime.txt file and create a new file named: + remote: ! .python-version remote: ! - remote: ! For example, to request the latest version of Python #{DEFAULT_PYTHON_MAJOR_VERSION}, - remote: ! update the 'runtime.txt' file so it contains: - remote: ! python-#{DEFAULT_PYTHON_MAJOR_VERSION} + 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: ! #{DEFAULT_PYTHON_MAJOR_VERSION} + 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: ! Push rejected, failed to compile Python app. OUTPUT @@ -371,6 +385,28 @@ expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> Using Python 3.13 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.13 + 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 #{LATEST_PYTHON_3_13} remote: -----> Installing pip #{PIP_VERSION} remote: -----> Installing dependencies using 'pip install -r requirements.txt'