diff --git a/CHANGELOG.md b/CHANGELOG.md index b084b5158..dc1e65506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/bin/compile b/bin/compile index c89420201..9bd6482b7 100755 --- a/bin/compile +++ b/bin/compile @@ -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) @@ -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 diff --git a/bin/detect b/bin/detect index 498792bab..69345eb78 100755 --- a/bin/detect +++ b/bin/detect @@ -1,11 +1,16 @@ #!/usr/bin/env bash -# Usage: bin/compile +# Usage: bin/detect # 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. # @@ -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 <&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 @@ -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. @@ -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 diff --git a/lib/output.sh b/lib/output.sh new file mode 100644 index 000000000..e063ddd88 --- /dev/null +++ b/lib/output.sh @@ -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 +} diff --git a/spec/hatchet/detect_spec.rb b/spec/hatchet/detect_spec.rb index ceb25581f..dbe7f1e93 100644 --- a/spec/hatchet/detect_spec.rb +++ b/spec/hatchet/detect_spec.rb @@ -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 diff --git a/spec/hatchet/package_manager_spec.rb b/spec/hatchet/package_manager_spec.rb index d247e1deb..70473fc81 100644 --- a/spec/hatchet/package_manager_spec.rb +++ b/spec/hatchet/package_manager_spec.rb @@ -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 @@ -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 diff --git a/spec/hatchet/pipenv_spec.rb b/spec/hatchet/pipenv_spec.rb index 6c0e2fa97..ccd5dee93 100644 --- a/spec/hatchet/pipenv_spec.rb +++ b/spec/hatchet/pipenv_spec.rb @@ -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 @@ -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: @@ -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 @@ -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: @@ -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 @@ -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 @@ -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 diff --git a/spec/hatchet/python_update_warning_spec.rb b/spec/hatchet/python_update_warning_spec.rb index df909ee73..d6d43c2f6 100644 --- a/spec/hatchet/python_update_warning_spec.rb +++ b/spec/hatchet/python_update_warning_spec.rb @@ -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 diff --git a/spec/hatchet/python_version_spec.rb b/spec/hatchet/python_version_spec.rb index a324089ba..f13c148ac 100644 --- a/spec/hatchet/python_version_spec.rb +++ b/spec/hatchet/python_version_spec.rb @@ -25,12 +25,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 '#{requested_runtime}' is not available for this stack (#{app.stack}). + remote: + remote: ! Error: Requested runtime '#{requested_runtime}' 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 @@ -91,6 +91,8 @@ expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> Using Python version specified in runtime.txt + 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: @@ -102,7 +104,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 @@ -117,6 +119,8 @@ expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> Using Python version specified in runtime.txt + 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: @@ -128,7 +132,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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 93e3b2bdb..5f761bf3b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -51,9 +51,12 @@ def get_requirement_version(package_name) end def clean_output(output) - # Remove trailing whitespace characters added by Git: - # https://github.com/heroku/hatchet/issues/162 - output.gsub(/ {8}(?=\R)/, '') + output + # Remove trailing whitespace characters added by Git: + # https://github.com/heroku/hatchet/issues/162 + .gsub(/ {8}(?=\R)/, '') + # Remove ANSI colour codes used in buildpack output (e.g. error messages). + .gsub(/\e\[[0-9;]+m/, '') end def update_buildpacks(app, buildpacks)