Skip to content

Commit

Permalink
Improve the error message when bundled pip cannot be found
Browse files Browse the repository at this point in the history
The Python stdlib bundles a copy of pip, which the buildpack uses to
bootstrap the real pip installation. This copy of pip should always
exist, and thus not finding it is treated as an internal error.

However, via Honeycomb I discovered one app that manages to hit this
case regardless, and when it does so, the error message is not the
"internal error" error message, but instead a Bash unbound variable
error like:

```
-----> Using Python 3.9.20 specified in .python-version
-----> Using cached install of Python 3.9.20
/tmp/buildpack/lib/utils.sh: line 33: bundled_pip_wheel_list[0]: unbound variable
```

I believe the reason for pip not being found in this case is that the
app source has a broken/EOL Python install committed to it (which
doesn't include the `ensurepip` module), which tricks the cache
restoration into thinking it can re-use the cache.

I will be adding an explicit warning/error for finding an existing
`.heroku/python/` directory in the app in a later PR, however, for now
this change:
(a) fixes the display of the internal error message,
(b) adds more debugging output to the error message so that I can
    confirm my theory as to the root cause for this app.

Towards #1710.
GUS-W-17386432.
  • Loading branch information
edmorley committed Dec 12, 2024
1 parent 07005f2 commit 8e8f09e
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]

- 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

Expand Down
2 changes: 1 addition & 1 deletion lib/pip.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down
33 changes: 20 additions & 13 deletions lib/utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
8 changes: 8 additions & 0 deletions spec/fixtures/python_in_app_source/.heroku/python/bin/python
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions spec/fixtures/python_in_app_source/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
Empty file.
30 changes: 30 additions & 0 deletions spec/hatchet/checks_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 8e8f09e

Please sign in to comment.