Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Removed support for the deprecated `runtime.txt` file. Python versions must now be specified using a `.python-version` file instead. ([#352](https://github.com/heroku/buildpacks-python/pull/352))

### Changed

- Improved the error messages shown when `.python-version` contains an invalid Python version. ([#353](https://github.com/heroku/buildpacks-python/pull/353))

## [0.26.1] - 2025-04-08

### Changed
Expand Down
70 changes: 46 additions & 24 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,11 @@ fn on_requested_python_version_error(error: RequestedPythonVersionError) {
{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
1. The major version only, for example: {DEFAULT_PYTHON_VERSION} (recommended)
2. An exact patch version, for example: {DEFAULT_PYTHON_VERSION}.999

Don't include quotes or a 'python-' prefix. Any code comments
must be on a separate line and be prefixed with '#'.
Don't include quotes, a 'python-' prefix or wildcards. Any
code comments must be on a separate line prefixed with '#'.

For example, to request the latest version of Python {DEFAULT_PYTHON_VERSION},
update your .python-version file so it contains exactly:
Expand Down Expand Up @@ -207,7 +207,7 @@ fn on_requested_python_version_error(error: RequestedPythonVersionError) {
version number. Don't include quotes or a 'python-' prefix.

For example, to request the latest version of Python {DEFAULT_PYTHON_VERSION},
update your .python-version file so it contains:
update your .python-version file so it contains exactly:
{DEFAULT_PYTHON_VERSION}

If the file already contains a version, check the line doesn't
Expand Down Expand Up @@ -305,10 +305,16 @@ fn on_python_layer_error(error: PythonLayerError) {
DownloadUnpackArchiveError::Request(ureq_error) => log_error(
"Unable to download Python",
formatdoc! {"
An error occurred whilst downloading the Python runtime archive.
An error occurred while downloading the Python runtime archive.

In some cases, this happens due to an unstable network connection.
Please try again to see if the error resolves itself.
In some cases, this happens due to a temporary issue with
the network connection or server.

First, make sure that you are using the latest version
of this buildpack, and haven't pinned to an older release
via a custom buildpack configuration in project.toml.

Then try building again to see if the error resolves itself.

Details: {ureq_error}
"},
Expand All @@ -319,22 +325,38 @@ fn on_python_layer_error(error: PythonLayerError) {
&io_error,
),
},
// This error will change once the Python version is validated against a manifest.
// TODO: (W-12613425) Write the supported Python versions inline, instead of linking out to Dev Center.
// TODO: Update this error message to suggest switching to the major version syntax in .python-version,
// which will prevent the error from ever occurring (now that all stacks support the same versions).
PythonLayerError::PythonArchiveNotFound { python_version } => log_error(
"The requested Python version wasn't found",
formatdoc! {"
The requested Python version ({python_version}) wasn't found.

Please switch to a supported Python version, or else don't specify a version
and the buildpack will use a default version (currently Python {DEFAULT_PYTHON_VERSION}).

For a list of the supported Python versions, see:
https://devcenter.heroku.com/articles/python-support#supported-runtimes
"},
),
// TODO: Remove this once versions are validated against a manifest (at which point all
// HTTP 403s/404s can be treated as an internal error).
PythonLayerError::PythonArchiveNotAvailable(requested_python_version) => {
let RequestedPythonVersion {
major,
minor,
origin,
..
} = &requested_python_version;
log_error(
"The requested Python version isn't available",
formatdoc! {"
Your app's {origin} file specifies a Python version
of {requested_python_version}, however, we couldn't find that version on S3.

Check that this Python version has been released upstream,
and that the Python buildpack has added support for it:
https://www.python.org/downloads/
https://github.com/heroku/buildpacks-python/blob/main/CHANGELOG.md

If it has, make sure that you are using the latest version
of this buildpack, and haven't pinned to an older release
via a custom buildpack configuration in project.toml.

We also strongly recommend that you do not pin your app to an
exact Python version such as {requested_python_version}, and instead only specify
the major Python version of {major}.{minor} in your {origin} file.
This will allow your app to receive the latest available Python
patch version automatically, and prevent this type of error.
"},
);
}
}
}

Expand Down
29 changes: 21 additions & 8 deletions src/layers/python.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::python_version::PythonVersion;
use crate::utils::{self, DownloadUnpackArchiveError};
use crate::{BuildpackError, PythonBuildpack};
use crate::{BuildpackError, PythonBuildpack, RequestedPythonVersion};
use libcnb::Env;
use libcnb::build::BuildContext;
use libcnb::data::layer_name;
Expand All @@ -17,6 +17,7 @@ pub(crate) fn install_python(
context: &BuildContext<PythonBuildpack>,
env: &mut Env,
python_version: &PythonVersion,
requested_python_version: &RequestedPythonVersion,
) -> Result<PathBuf, libcnb::Error<BuildpackError>> {
let new_metadata = PythonLayerMetadata {
arch: context.target.arch.clone(),
Expand Down Expand Up @@ -79,12 +80,24 @@ pub(crate) fn install_python(
let archive_url = python_version.url(&context.target);
utils::download_and_unpack_zstd_archive(&archive_url, &layer_path).map_err(
|error| match error {
// TODO: Remove this once the Python version is validated against a manifest (at
// which point 404s can be treated as an internal error, instead of user error)
DownloadUnpackArchiveError::Request(ureq::Error::Status(404, _)) => {
PythonLayerError::PythonArchiveNotFound {
python_version: python_version.clone(),
}
// If the request 404s then the most likely cause is that the user specified an
// invalid Python version. However, there is a chance there is an issue with the
// S3 bucket (eg files removed, or bucket made private). To try and tell the two
// cases apart, we check whether the requested version included a patch component.
// If it did, then it's most likely user error, but if it didn't, then the patch
// version is the one we resolved, and so know it should be valid.
//
// We have to check for 403s too, since S3 will return a 403 instead of a 404 for
// missing files, if the S3 bucket does not have public list permissions enabled.
//
// TODO: Remove this once versions are validated against a manifest (at which point
// all HTTP 403s/404s can be treated as an internal error).
DownloadUnpackArchiveError::Request(ureq::Error::Status(403 | 404, _))
if requested_python_version.patch.is_some() =>
{
PythonLayerError::PythonArchiveNotAvailable(
requested_python_version.clone(),
)
}
other_error => PythonLayerError::DownloadUnpackPythonArchive(other_error),
},
Expand Down Expand Up @@ -240,7 +253,7 @@ fn generate_layer_env(layer_path: &Path, python_version: &PythonVersion) -> Laye
#[derive(Debug)]
pub(crate) enum PythonLayerError {
DownloadUnpackPythonArchive(DownloadUnpackArchiveError),
PythonArchiveNotFound { python_version: PythonVersion },
PythonArchiveNotAvailable(RequestedPythonVersion),
}

impl From<PythonLayerError> for libcnb::Error<BuildpackError> {
Expand Down
9 changes: 7 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl Buildpack for PythonBuildpack {
minor: 9,
origin,
..
} = requested_python_version
} = &requested_python_version
{
log_warning(
"Support for Python 3.9 is deprecated",
Expand All @@ -115,7 +115,12 @@ impl Buildpack for PythonBuildpack {
}

log_header("Installing Python");
let python_layer_path = python::install_python(&context, &mut env, &python_version)?;
let python_layer_path = python::install_python(
&context,
&mut env,
&python_version,
&requested_python_version,
)?;

let dependencies_layer_dir = match package_manager {
PackageManager::Pip => {
Expand Down
32 changes: 21 additions & 11 deletions tests/python_version_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,11 @@ fn python_version_file_invalid_version() {
3.12.0invalid

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
1. The major version only, for example: {DEFAULT_PYTHON_VERSION} (recommended)
2. An exact patch version, for example: {DEFAULT_PYTHON_VERSION}.999

Don't include quotes or a 'python-' prefix. Any code comments
must be on a separate line and be prefixed with '#'.
Don't include quotes, a 'python-' prefix or wildcards. Any
code comments must be on a separate line prefixed with '#'.

For example, to request the latest version of Python {DEFAULT_PYTHON_VERSION},
update your .python-version file so it contains exactly:
Expand Down Expand Up @@ -244,7 +244,7 @@ fn python_version_file_no_version() {
version number. Don't include quotes or a 'python-' prefix.

For example, to request the latest version of Python {DEFAULT_PYTHON_VERSION},
update your .python-version file so it contains:
update your .python-version file so it contains exactly:
{DEFAULT_PYTHON_VERSION}
"}
);
Expand Down Expand Up @@ -318,14 +318,24 @@ fn python_version_non_existent_minor() {
assert_contains!(
context.pack_stderr,
&formatdoc! {"
[Error: The requested Python version wasn't found]
The requested Python version (3.12.999) wasn't found.
[Error: The requested Python version isn't available]
Your app's .python-version file specifies a Python version
of 3.12.999, however, we couldn't find that version on S3.

Check that this Python version has been released upstream,
and that the Python buildpack has added support for it:
https://www.python.org/downloads/
https://github.com/heroku/buildpacks-python/blob/main/CHANGELOG.md

Please switch to a supported Python version, or else don't specify a version
and the buildpack will use a default version (currently Python {DEFAULT_PYTHON_VERSION}).
If it has, make sure that you are using the latest version
of this buildpack, and haven't pinned to an older release
via a custom buildpack configuration in project.toml.

For a list of the supported Python versions, see:
https://devcenter.heroku.com/articles/python-support#supported-runtimes
We also strongly recommend that you do not pin your app to an
exact Python version such as 3.12.999, and instead only specify
the major Python version of 3.12 in your .python-version file.
This will allow your app to receive the latest available Python
patch version automatically, and prevent this type of error.
"}
);
});
Expand Down