Skip to content

Commit 6697fd5

Browse files
authored
Switch to a new output helper for error messages (#1639)
The existing logging functions in `bin/utils` don't support being passed multi-line output. Upcoming PRs are going to be adding a number of new error messages, so this adds a new output module under `lib/` (that supports multi-line output and also uses colour) and switches the existing buildpack error messages to it. Now that the buildpack error messages use ANSI colour codes, the Hatchet output has to have ANSI sequences stripped prior to the output assertions. GUS-W-16808943.
1 parent 3da3b3a commit 6697fd5

12 files changed

+117
-81
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Buildpack error messages are now more consistently formatted and use colour. ([#1639](https://github.com/heroku/heroku-buildpack-python/pull/1639))
56

67
## [v256] - 2024-09-07
78

bin/compile

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)
1515

1616
source "${BUILDPACK_DIR}/bin/utils"
1717
source "${BUILDPACK_DIR}/lib/metadata.sh"
18+
source "${BUILDPACK_DIR}/lib/output.sh"
1819

1920
compile_start_time=$(nowms)
2021

@@ -137,32 +138,30 @@ fi
137138
# TODO: Move this into a new package manager handling implementation when adding Poetry support.
138139
# We intentionally don't mention `setup.py` here since it's being removed soon.
139140
if [[ ! -f requirements.txt && ! -f Pipfile && ! -f setup.py ]]; then
140-
puts-warn
141-
puts-warn "Error: Couldn't find any supported Python package manager files."
142-
puts-warn
143-
puts-warn "A Python app on Heroku must have either a 'requirements.txt' or"
144-
puts-warn "'Pipfile' package manager file in the root directory of its"
145-
puts-warn "source code."
146-
puts-warn
147-
puts-warn "Currently the root directory of your app contains:"
148-
puts-warn
149-
# TODO: Overhaul logging helpers so they can handle prefixing multi-line strings, and switch to them.
150-
# shellcheck disable=SC2012 # Using `ls` instead of `find` is absolutely fine for this use case.
151-
ls -1 --indicator-style=slash "${BUILD_DIR}" | sed 's/^/ ! /'
152-
puts-warn
153-
puts-warn "If your app already has a package manager file, check that it:"
154-
puts-warn
155-
puts-warn "1. Is in the top level directory (not a subdirectory)."
156-
puts-warn "2. Has the correct spelling (the filenames are case-sensitive)."
157-
puts-warn "3. Isn't listed in '.gitignore' or '.slugignore'."
158-
puts-warn
159-
puts-warn "Otherwise, add a package manager file to your app. If your app has"
160-
puts-warn "no dependencies, then create an empty 'requirements.txt' file."
161-
puts-warn
162-
puts-warn "For help with using Python on Heroku, see:"
163-
puts-warn "https://devcenter.heroku.com/articles/getting-started-with-python"
164-
puts-warn "https://devcenter.heroku.com/articles/python-support"
165-
puts-warn
141+
display_error <<-EOF
142+
Error: Couldn't find any supported Python package manager files.
143+
144+
A Python app on Heroku must have either a 'requirements.txt' or
145+
'Pipfile' package manager file in the root directory of its
146+
source code.
147+
148+
Currently the root directory of your app contains:
149+
150+
$(ls -1 --indicator-style=slash "${BUILD_DIR}")
151+
152+
If your app already has a package manager file, check that it:
153+
154+
1. Is in the top level directory (not a subdirectory).
155+
2. Has the correct spelling (the filenames are case-sensitive).
156+
3. Isn't listed in '.gitignore' or '.slugignore'.
157+
158+
Otherwise, add a package manager file to your app. If your app has
159+
no dependencies, then create an empty 'requirements.txt' file.
160+
161+
For help with using Python on Heroku, see:
162+
https://devcenter.heroku.com/articles/getting-started-with-python
163+
https://devcenter.heroku.com/articles/python-support
164+
EOF
166165
meta_set "failure_reason" "package-manager-not-found"
167166
exit 1
168167
fi

bin/detect

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
#!/usr/bin/env bash
2-
# Usage: bin/compile <build-dir>
2+
# Usage: bin/detect <build-dir>
33
# See: https://devcenter.heroku.com/articles/buildpack-api
44

55
set -euo pipefail
66

77
BUILD_DIR="${1}"
88

9+
# The absolute path to the root of the buildpack.
10+
BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)
11+
12+
source "${BUILDPACK_DIR}/lib/output.sh"
13+
914
# Filenames that if found in a project mean it should be treated as a Python project,
1015
# and so pass this buildpack's detection phase.
1116
#
@@ -36,14 +41,10 @@ for filename in "${KNOWN_PYTHON_PROJECT_FILES[@]}"; do
3641
fi
3742
done
3843

39-
# Cytokine incorrectly indents the first line, so we have to leave it empty.
40-
echo 1>&2
41-
4244
# Note: This error message intentionally doesn't list all of the filetypes above,
4345
# since during compile the build will still require a package manager file, so it
44-
# makes sense to describe the stricter requirements up front.
45-
# TODO: Overhaul logging helpers so they can handle prefixing multi-line strings, and switch to them.
46-
sed 's/^/ ! /' 1>&2 <<EOF
46+
# makes sense to describe the stricter requirements upfront.
47+
display_error <<EOF
4748
Error: Your app is configured to use the Python buildpack,
4849
but we couldn't find any supported Python project files.
4950

bin/steps/python

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@ PYTHON_VERSION="${PYTHON_VERSION%%+([[:space:]])}"
99
function eol_python_version_error() {
1010
local major_version="${1}"
1111
local eol_date="${2}"
12-
puts-warn
13-
puts-warn "Python ${major_version} reached upstream end-of-life on ${eol_date}, and is"
14-
puts-warn "therefore no longer receiving security updates:"
15-
puts-warn "https://devguide.python.org/versions/#supported-versions"
16-
puts-warn
17-
puts-warn "As such, it is no longer supported by this buildpack."
18-
puts-warn
19-
puts-warn "Please upgrade to a newer Python version."
20-
puts-warn
21-
puts-warn "For a list of the supported Python versions, see:"
22-
puts-warn "https://devcenter.heroku.com/articles/python-support#supported-runtimes"
23-
puts-warn
12+
display_error <<-EOF
13+
Error: Python ${major_version} is no longer supported.
14+
15+
Python ${major_version} reached upstream end-of-life on ${eol_date}, and is
16+
therefore no longer receiving security updates:
17+
https://devguide.python.org/versions/#supported-versions
18+
19+
As such, it is no longer supported by this buildpack.
20+
21+
Please upgrade to a newer Python version.
22+
23+
For a list of the supported Python versions, see:
24+
https://devcenter.heroku.com/articles/python-support#supported-runtimes
25+
EOF
2426
meta_set "failure_reason" "python-version-eol"
2527
exit 1
2628
}
@@ -44,12 +46,12 @@ ARCH=$(dpkg --print-architecture)
4446
PYTHON_URL="${S3_BASE_URL}/${PYTHON_VERSION}-ubuntu-${UBUNTU_VERSION}-${ARCH}.tar.zst"
4547

4648
if ! curl --output /dev/null --silent --head --fail --retry 3 --retry-connrefused --connect-timeout 10 "${PYTHON_URL}"; then
47-
puts-warn
48-
puts-warn "Requested runtime '${PYTHON_VERSION}' is not available for this stack (${STACK})."
49-
puts-warn
50-
puts-warn "For a list of the supported Python versions, see:"
51-
puts-warn "https://devcenter.heroku.com/articles/python-support#supported-runtimes"
52-
puts-warn
49+
display_error <<-EOF
50+
Error: Requested runtime '${PYTHON_VERSION}' is not available for this stack (${STACK}).
51+
52+
For a list of the supported Python versions, see:
53+
https://devcenter.heroku.com/articles/python-support#supported-runtimes
54+
EOF
5355
meta_set "failure_reason" "python-version-not-found"
5456
exit 1
5557
fi
@@ -153,7 +155,7 @@ else
153155
if ! curl --silent --show-error --fail --retry 3 --retry-connrefused --connect-timeout 10 "${PYTHON_URL}" | tar --zstd --extract --directory .heroku/python; then
154156
# The Python version was confirmed to exist previously, so any failure here is due to
155157
# a networking issue or archive/buildpack bug rather than the runtime not existing.
156-
puts-warn "Failed to download/install ${PYTHON_VERSION}"
158+
display_error "Error: Failed to download/install ${PYTHON_VERSION}."
157159
meta_set "failure_reason" "python-download"
158160
exit 1
159161
fi
@@ -186,7 +188,7 @@ BUNDLED_PIP_WHEEL_LIST=(.heroku/python/lib/python*/ensurepip/_bundled/pip-*.whl)
186188
BUNDLED_PIP_WHEEL="${BUNDLED_PIP_WHEEL_LIST[0]}"
187189

188190
if [[ -z "${BUNDLED_PIP_WHEEL}" ]]; then
189-
puts-warn "Failed to locate the bundled pip wheel"
191+
display_error "Error: Failed to locate the bundled pip wheel."
190192
meta_set "failure_reason" "bundled-pip-not-found"
191193
exit 1
192194
fi

builds/test_python_runtime.sh

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ set -euo pipefail
44

55
ARCHIVE_FILEPATH="${1:?"Error: The filepath of the Python runtime archive must be specified as the first argument."}"
66

7+
function abort() {
8+
echo "Error: ${1}" >&2
9+
exit 1
10+
}
11+
712
# We intentionally extract the Python runtime into a different directory to the one into which it
813
# was originally installed before being packaged, to check that relocation works (since buildpacks
914
# depend on it). Since the Python binary was built in shared mode, `LD_LIBRARY_PATH` must be set
@@ -22,8 +27,7 @@ tar --zstd --extract --verbose --file "${ARCHIVE_FILEPATH}" --directory "${INSTA
2227
# Check that all dynamically linked libraries exist in the run image (since it has fewer packages than the build image).
2328
LDD_OUTPUT=$(find "${INSTALL_DIR}" -type f,l \( -name 'python3' -o -name '*.so*' \) -exec ldd '{}' +)
2429
if grep 'not found' <<<"${LDD_OUTPUT}" | sort --unique; then
25-
echo "The above dynamically linked libraries were not found!"
26-
exit 1
30+
abort "The above dynamically linked libraries were not found!"
2731
fi
2832

2933
# Check that optional and/or system library dependent stdlib modules were built.
@@ -46,6 +50,5 @@ if ! "${INSTALL_DIR}/bin/python3" -c "import $(
4650
IFS=,
4751
echo "${optional_stdlib_modules[*]}"
4852
)"; then
49-
echo "The above optional stdlib module failed to import! Check the compile logs to see if it was skipped due to missing libraries/headers."
50-
exit 1
53+
abort "The above optional stdlib module failed to import! Check the compile logs to see if it was skipped due to missing libraries/headers."
5154
fi

lib/output.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
3+
ANSI_RED='\033[1;31m'
4+
ANSI_RESET='\033[0m'
5+
6+
# shellcheck disable=SC2120 # Prevent warnings about unused arguments due to the split args vs stdin API.
7+
function display_error() {
8+
# Send all output to stderr
9+
exec 1>&2
10+
# If arguments are given, redirect them to stdin. This allows the function
11+
# to be invoked with either a string argument or stdin (e.g. via <<-EOF).
12+
(($#)) && exec <<<"${@}"
13+
echo
14+
while IFS= read -r line; do
15+
echo -e "${ANSI_RED} ! ${line}${ANSI_RESET}"
16+
done
17+
echo
18+
}

spec/hatchet/detect_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
remote: ! https://devcenter.heroku.com/articles/getting-started-with-python
4040
remote: ! https://devcenter.heroku.com/articles/python-support
4141
remote:
42+
remote:
4243
remote: More info: https://devcenter.heroku.com/articles/buildpacks#detection-failure
4344
OUTPUT
4445
end

spec/hatchet/package_manager_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
app.deploy do |app|
1111
expect(clean_output(app.output)).to include(<<~OUTPUT)
1212
remote: -----> Python app detected
13-
remote: !
13+
remote:
1414
remote: ! Error: Couldn't find any supported Python package manager files.
1515
remote: !
1616
remote: ! A Python app on Heroku must have either a 'requirements.txt' or
@@ -34,7 +34,7 @@
3434
remote: ! For help with using Python on Heroku, see:
3535
remote: ! https://devcenter.heroku.com/articles/getting-started-with-python
3636
remote: ! https://devcenter.heroku.com/articles/python-support
37-
remote: !
37+
remote:
3838
remote: ! Push rejected, failed to compile Python app.
3939
OUTPUT
4040
end

spec/hatchet/pipenv_spec.rb

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
expect(clean_output(app.output)).to include(<<~OUTPUT)
2525
remote: -----> Python app detected
2626
remote: -----> Using Python version specified in Pipfile.lock
27-
remote: !
28-
remote: ! Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}).
27+
remote:
28+
remote: ! Error: Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}).
2929
remote: !
3030
remote: ! For a list of the supported Python versions, see:
3131
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
32-
remote: !
32+
remote:
3333
remote: ! Push rejected, failed to compile Python app.
3434
OUTPUT
3535
end
@@ -125,6 +125,8 @@
125125
expect(clean_output(app.output)).to match(Regexp.new(<<~OUTPUT))
126126
remote: -----> Python app detected
127127
remote: -----> Using Python version specified in Pipfile.lock
128+
remote:
129+
remote: ! Error: Python 3.6 is no longer supported.
128130
remote: !
129131
remote: ! Python 3.6 reached upstream end-of-life on December 23rd, 2021, and is
130132
remote: ! therefore no longer receiving security updates:
@@ -136,7 +138,7 @@
136138
remote: !
137139
remote: ! For a list of the supported Python versions, see:
138140
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
139-
remote: !
141+
remote:
140142
remote: ! Push rejected, failed to compile Python app.
141143
OUTPUT
142144
end
@@ -151,6 +153,8 @@
151153
expect(clean_output(app.output)).to match(Regexp.new(<<~OUTPUT))
152154
remote: -----> Python app detected
153155
remote: -----> Using Python version specified in Pipfile.lock
156+
remote:
157+
remote: ! Error: Python 3.7 is no longer supported.
154158
remote: !
155159
remote: ! Python 3.7 reached upstream end-of-life on June 27th, 2023, and is
156160
remote: ! therefore no longer receiving security updates:
@@ -162,7 +166,7 @@
162166
remote: !
163167
remote: ! For a list of the supported Python versions, see:
164168
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
165-
remote: !
169+
remote:
166170
remote: ! Push rejected, failed to compile Python app.
167171
OUTPUT
168172
end
@@ -273,12 +277,12 @@
273277
expect(clean_output(app.output)).to include(<<~OUTPUT)
274278
remote: -----> Python app detected
275279
remote: -----> Using Python version specified in Pipfile.lock
276-
remote: !
277-
remote: ! Requested runtime '^3.12' is not available for this stack (#{app.stack}).
280+
remote:
281+
remote: ! Error: Requested runtime '^3.12' is not available for this stack (#{app.stack}).
278282
remote: !
279283
remote: ! For a list of the supported Python versions, see:
280284
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
281-
remote: !
285+
remote:
282286
remote: ! Push rejected, failed to compile Python app.
283287
OUTPUT
284288
end
@@ -293,12 +297,12 @@
293297
expect(clean_output(app.output)).to include(<<~OUTPUT)
294298
remote: -----> Python app detected
295299
remote: -----> Using Python version specified in Pipfile.lock
296-
remote: !
297-
remote: ! Requested runtime 'python-X.Y.Z' is not available for this stack (#{app.stack}).
300+
remote:
301+
remote: ! Error: Requested runtime 'python-X.Y.Z' is not available for this stack (#{app.stack}).
298302
remote: !
299303
remote: ! For a list of the supported Python versions, see:
300304
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
301-
remote: !
305+
remote:
302306
remote: ! Push rejected, failed to compile Python app.
303307
OUTPUT
304308
end

spec/hatchet/python_update_warning_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
expect(clean_output(app.output)).to include(<<~OUTPUT)
2525
remote: -----> Python app detected
2626
remote: -----> Using Python version specified in runtime.txt
27-
remote: !
28-
remote: ! Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}).
27+
remote:
28+
remote: ! Error: Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}).
2929
remote: !
3030
remote: ! For a list of the supported Python versions, see:
3131
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
32-
remote: !
32+
remote:
3333
remote: ! Push rejected, failed to compile Python app.
3434
OUTPUT
3535
end

0 commit comments

Comments
 (0)