diff --git a/CHANGELOG.md b/CHANGELOG.md index 3672a05b4..e49de62b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - Improved the error message shown when pip install fails due to pip rejecting a package with invalid version metadata. ([#1718](https://github.com/heroku/heroku-buildpack-python/pull/1718)) +- Improved the error message shown when the copy of pip bundled in the `ensurepip` module cannot be found. ([#1720](https://github.com/heroku/heroku-buildpack-python/pull/1720)) ## [v270] - 2024-12-10 diff --git a/lib/pip.sh b/lib/pip.sh index 3b26250f6..c6bedb92a 100644 --- a/lib/pip.sh +++ b/lib/pip.sh @@ -15,7 +15,7 @@ function pip::install_pip_setuptools_wheel() { # We use the pip wheel bundled within Python's standard library to install our chosen # pip version, since it's faster than `ensurepip` followed by an upgrade in place. local bundled_pip_module_path - bundled_pip_module_path="$(utils::bundled_pip_module_path "${python_home}")" + bundled_pip_module_path="$(utils::bundled_pip_module_path "${python_home}" "${python_major_version}")" meta_set "pip_version" "${PIP_VERSION}" diff --git a/lib/utils.sh b/lib/utils.sh index 61254de5c..2bcae44c2 100644 --- a/lib/utils.sh +++ b/lib/utils.sh @@ -20,24 +20,31 @@ function utils::get_requirement_version() { # pip version from PyPI, saving us from having to download the usual pip bootstrap script. function utils::bundled_pip_module_path() { local python_home="${1}" + local python_major_version="${2}" - # We have to use a glob since the bundled wheel filename contains the pip version, which - # differs between Python versions. We also have to handle the case where there are multiple - # matching pip wheels, since in some versions of Python (eg 3.9.0) multiple versions of pip - # were accidentally bundled upstream. Note: This implementation relies upon `nullglob` being - # set, which is the case thanks to the `bin/utils` that was run earlier. - local bundled_pip_wheel_list=("${python_home}"/lib/python*/ensurepip/_bundled/pip-*.whl) - local bundled_pip_wheel="${bundled_pip_wheel_list[0]}" - - if [[ -z "${bundled_pip_wheel}" ]]; then - output::error <<-'EOF' - Internal Error: Unable to locate the ensurepip pip wheel file. + local bundled_wheels_dir="${python_home}/lib/python${python_major_version}/ensurepip/_bundled" + + # We have to use a glob since the bundled wheel filename contains the pip version, which differs + # between Python versions. We use compgen to avoid having to set nullglob, since there may be no + # matches in the case of a broken Python install. We also have to handle the case where there are + # multiple matching pip wheels, since in some versions of Python (eg 3.9.0) multiple versions of + # pip were accidentally bundled upstream (we use tail since we want the newest pip version). + if bundled_pip_wheel="$(compgen -G "${bundled_wheels_dir}/pip-*.whl" | tail --lines=1)"; then + # The pip module exists inside the pip wheel (which is a zip file), however, Python can load + # it directly by appending the module name to the zip filename, as though it were a path. + echo "${bundled_pip_wheel}/pip" + else + output::error <<-EOF + Internal Error: Unable to locate the bundled copy of pip. + + The Python buildpack could not locate the copy of pip bundled + inside Python's 'ensurepip' module: + + $(find "${bundled_wheels_dir}/" 2>&1 || find "${python_home}/" -type d 2>&1 || true) EOF meta_set "failure_reason" "bundled-pip-not-found" exit 1 fi - - echo "${bundled_pip_wheel}/pip" } function utils::abort_internal_error() { diff --git a/spec/fixtures/python_in_app_source/.heroku/python/bin/python b/spec/fixtures/python_in_app_source/.heroku/python/bin/python new file mode 100644 index 000000000..04b03ed28 --- /dev/null +++ b/spec/fixtures/python_in_app_source/.heroku/python/bin/python @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# This file emulates a Python install having been committed to the app's Git repo. +# For example, by downloading a slug, extracting it, and committing the results. + +set -euo pipefail + +exit 0 diff --git a/spec/fixtures/python_in_app_source/.python-version b/spec/fixtures/python_in_app_source/.python-version new file mode 100644 index 000000000..24ee5b1be --- /dev/null +++ b/spec/fixtures/python_in_app_source/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/spec/fixtures/python_in_app_source/requirements.txt b/spec/fixtures/python_in_app_source/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/spec/hatchet/checks_spec.rb b/spec/hatchet/checks_spec.rb new file mode 100644 index 000000000..d29554821 --- /dev/null +++ b/spec/hatchet/checks_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +RSpec.describe 'Buildpack validation checks' do + context 'when the app source contains a broken Python install' do + let(:app) { Hatchet::Runner.new('spec/fixtures/python_in_app_source', allow_failure: true) } + + it 'fails detection' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Python app detected + remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version + remote: -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} + remote: + remote: ! Internal Error: Unable to locate the bundled copy of pip. + remote: ! + remote: ! The Python buildpack could not locate the copy of pip bundled + remote: ! inside Python's 'ensurepip' module: + remote: ! + remote: ! find: ‘/app/.heroku/python/lib/python3.13/ensurepip/_bundled/’: No such file or directory + remote: ! /app/.heroku/python/ + remote: ! /app/.heroku/python/bin + remote: + remote: ! Push rejected, failed to compile Python app. + OUTPUT + end + end + end +end