Skip to content

Commit 8a27499

Browse files
authored
Improve the error message when bundled pip cannot be found (#1720)
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 ``` My guess for why pip is not being found in this case is that the app's Git repo may have 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.
1 parent e74c46e commit 8a27499

File tree

7 files changed

+61
-14
lines changed

7 files changed

+61
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## [Unreleased]
44

55
- 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))
6+
- 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))
67

78
## [v270] - 2024-12-10
89

lib/pip.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function pip::install_pip_setuptools_wheel() {
1515
# We use the pip wheel bundled within Python's standard library to install our chosen
1616
# pip version, since it's faster than `ensurepip` followed by an upgrade in place.
1717
local bundled_pip_module_path
18-
bundled_pip_module_path="$(utils::bundled_pip_module_path "${python_home}")"
18+
bundled_pip_module_path="$(utils::bundled_pip_module_path "${python_home}" "${python_major_version}")"
1919

2020
meta_set "pip_version" "${PIP_VERSION}"
2121

lib/utils.sh

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,31 @@ function utils::get_requirement_version() {
2020
# pip version from PyPI, saving us from having to download the usual pip bootstrap script.
2121
function utils::bundled_pip_module_path() {
2222
local python_home="${1}"
23+
local python_major_version="${2}"
2324

24-
# We have to use a glob since the bundled wheel filename contains the pip version, which
25-
# differs between Python versions. We also have to handle the case where there are multiple
26-
# matching pip wheels, since in some versions of Python (eg 3.9.0) multiple versions of pip
27-
# were accidentally bundled upstream. Note: This implementation relies upon `nullglob` being
28-
# set, which is the case thanks to the `bin/utils` that was run earlier.
29-
local bundled_pip_wheel_list=("${python_home}"/lib/python*/ensurepip/_bundled/pip-*.whl)
30-
local bundled_pip_wheel="${bundled_pip_wheel_list[0]}"
31-
32-
if [[ -z "${bundled_pip_wheel}" ]]; then
33-
output::error <<-'EOF'
34-
Internal Error: Unable to locate the ensurepip pip wheel file.
25+
local bundled_wheels_dir="${python_home}/lib/python${python_major_version}/ensurepip/_bundled"
26+
27+
# We have to use a glob since the bundled wheel filename contains the pip version, which differs
28+
# between Python versions. We use compgen to avoid having to set nullglob, since there may be no
29+
# matches in the case of a broken Python install. We also have to handle the case where there are
30+
# multiple matching pip wheels, since in some versions of Python (eg 3.9.0) multiple versions of
31+
# pip were accidentally bundled upstream (we use tail since we want the newest pip version).
32+
if bundled_pip_wheel="$(compgen -G "${bundled_wheels_dir}/pip-*.whl" | tail --lines=1)"; then
33+
# The pip module exists inside the pip wheel (which is a zip file), however, Python can load
34+
# it directly by appending the module name to the zip filename, as though it were a path.
35+
echo "${bundled_pip_wheel}/pip"
36+
else
37+
output::error <<-EOF
38+
Internal Error: Unable to locate the bundled copy of pip.
39+
40+
The Python buildpack could not locate the copy of pip bundled
41+
inside Python's 'ensurepip' module:
42+
43+
$(find "${bundled_wheels_dir}/" 2>&1 || find "${python_home}/" -type d 2>&1 || true)
3544
EOF
3645
meta_set "failure_reason" "bundled-pip-not-found"
3746
exit 1
3847
fi
39-
40-
echo "${bundled_pip_wheel}/pip"
4148
}
4249

4350
function utils::abort_internal_error() {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
3+
# This file emulates a Python install having been committed to the app's Git repo.
4+
# For example, by downloading a slug, extracting it, and committing the results.
5+
6+
set -euo pipefail
7+
8+
exit 0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

spec/fixtures/python_in_app_source/requirements.txt

Whitespace-only changes.

spec/hatchet/checks_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../spec_helper'
4+
5+
RSpec.describe 'Buildpack validation checks' do
6+
context 'when the app source contains a broken Python install' do
7+
let(:app) { Hatchet::Runner.new('spec/fixtures/python_in_app_source', allow_failure: true) }
8+
9+
it 'fails detection' do
10+
app.deploy do |app|
11+
expect(clean_output(app.output)).to include(<<~OUTPUT)
12+
remote: -----> Python app detected
13+
remote: -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version
14+
remote: -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION}
15+
remote:
16+
remote: ! Internal Error: Unable to locate the bundled copy of pip.
17+
remote: !
18+
remote: ! The Python buildpack could not locate the copy of pip bundled
19+
remote: ! inside Python's 'ensurepip' module:
20+
remote: !
21+
remote: ! find: ‘/app/.heroku/python/lib/python3.13/ensurepip/_bundled/’: No such file or directory
22+
remote: ! /app/.heroku/python/
23+
remote: ! /app/.heroku/python/bin
24+
remote:
25+
remote: ! Push rejected, failed to compile Python app.
26+
OUTPUT
27+
end
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)