Skip to content

Commit

Permalink
Switch to a new output helper for error messages (#1639)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
edmorley authored Sep 23, 2024
1 parent 3da3b3a commit 6697fd5
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 81 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]

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

## [v256] - 2024-09-07

Expand Down
51 changes: 25 additions & 26 deletions bin/compile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)

source "${BUILDPACK_DIR}/bin/utils"
source "${BUILDPACK_DIR}/lib/metadata.sh"
source "${BUILDPACK_DIR}/lib/output.sh"

compile_start_time=$(nowms)

Expand Down Expand Up @@ -137,32 +138,30 @@ fi
# TODO: Move this into a new package manager handling implementation when adding Poetry support.
# We intentionally don't mention `setup.py` here since it's being removed soon.
if [[ ! -f requirements.txt && ! -f Pipfile && ! -f setup.py ]]; then
puts-warn
puts-warn "Error: Couldn't find any supported Python package manager files."
puts-warn
puts-warn "A Python app on Heroku must have either a 'requirements.txt' or"
puts-warn "'Pipfile' package manager file in the root directory of its"
puts-warn "source code."
puts-warn
puts-warn "Currently the root directory of your app contains:"
puts-warn
# TODO: Overhaul logging helpers so they can handle prefixing multi-line strings, and switch to them.
# shellcheck disable=SC2012 # Using `ls` instead of `find` is absolutely fine for this use case.
ls -1 --indicator-style=slash "${BUILD_DIR}" | sed 's/^/ ! /'
puts-warn
puts-warn "If your app already has a package manager file, check that it:"
puts-warn
puts-warn "1. Is in the top level directory (not a subdirectory)."
puts-warn "2. Has the correct spelling (the filenames are case-sensitive)."
puts-warn "3. Isn't listed in '.gitignore' or '.slugignore'."
puts-warn
puts-warn "Otherwise, add a package manager file to your app. If your app has"
puts-warn "no dependencies, then create an empty 'requirements.txt' file."
puts-warn
puts-warn "For help with using Python on Heroku, see:"
puts-warn "https://devcenter.heroku.com/articles/getting-started-with-python"
puts-warn "https://devcenter.heroku.com/articles/python-support"
puts-warn
display_error <<-EOF
Error: Couldn't find any supported Python package manager files.
A Python app on Heroku must have either a 'requirements.txt' or
'Pipfile' package manager file in the root directory of its
source code.
Currently the root directory of your app contains:
$(ls -1 --indicator-style=slash "${BUILD_DIR}")
If your app already has a package manager file, check that it:
1. Is in the top level directory (not a subdirectory).
2. Has the correct spelling (the filenames are case-sensitive).
3. Isn't listed in '.gitignore' or '.slugignore'.
Otherwise, add a package manager file to your app. If your app has
no dependencies, then create an empty 'requirements.txt' file.
For help with using Python on Heroku, see:
https://devcenter.heroku.com/articles/getting-started-with-python
https://devcenter.heroku.com/articles/python-support
EOF
meta_set "failure_reason" "package-manager-not-found"
exit 1
fi
Expand Down
15 changes: 8 additions & 7 deletions bin/detect
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#!/usr/bin/env bash
# Usage: bin/compile <build-dir>
# Usage: bin/detect <build-dir>
# See: https://devcenter.heroku.com/articles/buildpack-api

set -euo pipefail

BUILD_DIR="${1}"

# The absolute path to the root of the buildpack.
BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)

source "${BUILDPACK_DIR}/lib/output.sh"

# Filenames that if found in a project mean it should be treated as a Python project,
# and so pass this buildpack's detection phase.
#
Expand Down Expand Up @@ -36,14 +41,10 @@ for filename in "${KNOWN_PYTHON_PROJECT_FILES[@]}"; do
fi
done

# Cytokine incorrectly indents the first line, so we have to leave it empty.
echo 1>&2

# Note: This error message intentionally doesn't list all of the filetypes above,
# since during compile the build will still require a package manager file, so it
# makes sense to describe the stricter requirements up front.
# TODO: Overhaul logging helpers so they can handle prefixing multi-line strings, and switch to them.
sed 's/^/ ! /' 1>&2 <<EOF
# makes sense to describe the stricter requirements upfront.
display_error <<EOF
Error: Your app is configured to use the Python buildpack,
but we couldn't find any supported Python project files.
Expand Down
42 changes: 22 additions & 20 deletions bin/steps/python
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ PYTHON_VERSION="${PYTHON_VERSION%%+([[:space:]])}"
function eol_python_version_error() {
local major_version="${1}"
local eol_date="${2}"
puts-warn
puts-warn "Python ${major_version} reached upstream end-of-life on ${eol_date}, and is"
puts-warn "therefore no longer receiving security updates:"
puts-warn "https://devguide.python.org/versions/#supported-versions"
puts-warn
puts-warn "As such, it is no longer supported by this buildpack."
puts-warn
puts-warn "Please upgrade to a newer Python version."
puts-warn
puts-warn "For a list of the supported Python versions, see:"
puts-warn "https://devcenter.heroku.com/articles/python-support#supported-runtimes"
puts-warn
display_error <<-EOF
Error: Python ${major_version} is no longer supported.
Python ${major_version} reached upstream end-of-life on ${eol_date}, and is
therefore no longer receiving security updates:
https://devguide.python.org/versions/#supported-versions
As such, it is no longer supported by this buildpack.
Please upgrade to a newer Python version.
For a list of the supported Python versions, see:
https://devcenter.heroku.com/articles/python-support#supported-runtimes
EOF
meta_set "failure_reason" "python-version-eol"
exit 1
}
Expand All @@ -44,12 +46,12 @@ ARCH=$(dpkg --print-architecture)
PYTHON_URL="${S3_BASE_URL}/${PYTHON_VERSION}-ubuntu-${UBUNTU_VERSION}-${ARCH}.tar.zst"

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

if [[ -z "${BUNDLED_PIP_WHEEL}" ]]; then
puts-warn "Failed to locate the bundled pip wheel"
display_error "Error: Failed to locate the bundled pip wheel."
meta_set "failure_reason" "bundled-pip-not-found"
exit 1
fi
Expand Down
11 changes: 7 additions & 4 deletions builds/test_python_runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ set -euo pipefail

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

function abort() {
echo "Error: ${1}" >&2
exit 1
}

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

# Check that optional and/or system library dependent stdlib modules were built.
Expand All @@ -46,6 +50,5 @@ if ! "${INSTALL_DIR}/bin/python3" -c "import $(
IFS=,
echo "${optional_stdlib_modules[*]}"
)"; then
echo "The above optional stdlib module failed to import! Check the compile logs to see if it was skipped due to missing libraries/headers."
exit 1
abort "The above optional stdlib module failed to import! Check the compile logs to see if it was skipped due to missing libraries/headers."
fi
18 changes: 18 additions & 0 deletions lib/output.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash

ANSI_RED='\033[1;31m'
ANSI_RESET='\033[0m'

# shellcheck disable=SC2120 # Prevent warnings about unused arguments due to the split args vs stdin API.
function display_error() {
# Send all output to stderr
exec 1>&2
# If arguments are given, redirect them to stdin. This allows the function
# to be invoked with either a string argument or stdin (e.g. via <<-EOF).
(($#)) && exec <<<"${@}"
echo
while IFS= read -r line; do
echo -e "${ANSI_RED} ! ${line}${ANSI_RESET}"
done
echo
}
1 change: 1 addition & 0 deletions spec/hatchet/detect_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
remote: ! https://devcenter.heroku.com/articles/getting-started-with-python
remote: ! https://devcenter.heroku.com/articles/python-support
remote:
remote:
remote: More info: https://devcenter.heroku.com/articles/buildpacks#detection-failure
OUTPUT
end
Expand Down
4 changes: 2 additions & 2 deletions spec/hatchet/package_manager_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
app.deploy do |app|
expect(clean_output(app.output)).to include(<<~OUTPUT)
remote: -----> Python app detected
remote: !
remote:
remote: ! Error: Couldn't find any supported Python package manager files.
remote: !
remote: ! A Python app on Heroku must have either a 'requirements.txt' or
Expand All @@ -34,7 +34,7 @@
remote: ! For help with using Python on Heroku, see:
remote: ! https://devcenter.heroku.com/articles/getting-started-with-python
remote: ! https://devcenter.heroku.com/articles/python-support
remote: !
remote:
remote: ! Push rejected, failed to compile Python app.
OUTPUT
end
Expand Down
26 changes: 15 additions & 11 deletions spec/hatchet/pipenv_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
expect(clean_output(app.output)).to include(<<~OUTPUT)
remote: -----> Python app detected
remote: -----> Using Python version specified in Pipfile.lock
remote: !
remote: ! Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}).
remote:
remote: ! Error: Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}).
remote: !
remote: ! For a list of the supported Python versions, see:
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
remote: !
remote:
remote: ! Push rejected, failed to compile Python app.
OUTPUT
end
Expand Down Expand Up @@ -125,6 +125,8 @@
expect(clean_output(app.output)).to match(Regexp.new(<<~OUTPUT))
remote: -----> Python app detected
remote: -----> Using Python version specified in Pipfile.lock
remote:
remote: ! Error: Python 3.6 is no longer supported.
remote: !
remote: ! Python 3.6 reached upstream end-of-life on December 23rd, 2021, and is
remote: ! therefore no longer receiving security updates:
Expand All @@ -136,7 +138,7 @@
remote: !
remote: ! For a list of the supported Python versions, see:
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
remote: !
remote:
remote: ! Push rejected, failed to compile Python app.
OUTPUT
end
Expand All @@ -151,6 +153,8 @@
expect(clean_output(app.output)).to match(Regexp.new(<<~OUTPUT))
remote: -----> Python app detected
remote: -----> Using Python version specified in Pipfile.lock
remote:
remote: ! Error: Python 3.7 is no longer supported.
remote: !
remote: ! Python 3.7 reached upstream end-of-life on June 27th, 2023, and is
remote: ! therefore no longer receiving security updates:
Expand All @@ -162,7 +166,7 @@
remote: !
remote: ! For a list of the supported Python versions, see:
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
remote: !
remote:
remote: ! Push rejected, failed to compile Python app.
OUTPUT
end
Expand Down Expand Up @@ -273,12 +277,12 @@
expect(clean_output(app.output)).to include(<<~OUTPUT)
remote: -----> Python app detected
remote: -----> Using Python version specified in Pipfile.lock
remote: !
remote: ! Requested runtime '^3.12' is not available for this stack (#{app.stack}).
remote:
remote: ! Error: Requested runtime '^3.12' is not available for this stack (#{app.stack}).
remote: !
remote: ! For a list of the supported Python versions, see:
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
remote: !
remote:
remote: ! Push rejected, failed to compile Python app.
OUTPUT
end
Expand All @@ -293,12 +297,12 @@
expect(clean_output(app.output)).to include(<<~OUTPUT)
remote: -----> Python app detected
remote: -----> Using Python version specified in Pipfile.lock
remote: !
remote: ! Requested runtime 'python-X.Y.Z' is not available for this stack (#{app.stack}).
remote:
remote: ! Error: Requested runtime 'python-X.Y.Z' is not available for this stack (#{app.stack}).
remote: !
remote: ! For a list of the supported Python versions, see:
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
remote: !
remote:
remote: ! Push rejected, failed to compile Python app.
OUTPUT
end
Expand Down
6 changes: 3 additions & 3 deletions spec/hatchet/python_update_warning_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
expect(clean_output(app.output)).to include(<<~OUTPUT)
remote: -----> Python app detected
remote: -----> Using Python version specified in runtime.txt
remote: !
remote: ! Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}).
remote:
remote: ! Error: Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}).
remote: !
remote: ! For a list of the supported Python versions, see:
remote: ! https://devcenter.heroku.com/articles/python-support#supported-runtimes
remote: !
remote:
remote: ! Push rejected, failed to compile Python app.
OUTPUT
end
Expand Down
Loading

0 comments on commit 6697fd5

Please sign in to comment.