diff --git a/CHANGELOG.md b/CHANGELOG.md index b3417b0e8..6dd2ef56f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Updated uv from 0.9.7 to 0.9.9. ([#1961](https://github.com/heroku/heroku-buildpack-python/pull/1961)) - Improved the error message shown for `.python-version` files that contain unexpected ASCII control code characters. ([#1962](https://github.com/heroku/heroku-buildpack-python/pull/1962)) - Fixed Bash command substitution warnings from being shown if `runtime.txt` contains null byte characters. ([#1962](https://github.com/heroku/heroku-buildpack-python/pull/1962)) +- Improved the error message shown if the buildpack's build data file is deleted by a pre/post-compile hook. ([#1963](https://github.com/heroku/heroku-buildpack-python/pull/1963)) ## [v318] - 2025-11-12 diff --git a/lib/build_data.sh b/lib/build_data.sh index 9841e0f45..93434ac38 100644 --- a/lib/build_data.sh +++ b/lib/build_data.sh @@ -104,9 +104,25 @@ function build_data::_set() { local jq_args=(--argjson value "${value}") fi - local new_data_file_contents - new_data_file_contents="$(jq --arg key "${key}" "${jq_args[@]}" '. + { ($key): ($value) }' "${BUILD_DATA_FILE}")" - echo "${new_data_file_contents}" >"${BUILD_DATA_FILE}" + if [[ -f "${BUILD_DATA_FILE}" ]]; then + local new_data_file_contents + new_data_file_contents="$(jq --exit-status --arg key "${key}" "${jq_args[@]}" '. + { ($key): ($value) }' "${BUILD_DATA_FILE}")" + echo "${new_data_file_contents}" >"${BUILD_DATA_FILE}" + else + output::error <<-EOF + Error: Can't find the buildpack's build data file. + + The Python buildpack's internal build data file is missing: + ${BUILD_DATA_FILE} + + This file is required for the buildpack to work correctly, + and so you must not delete it when removing files from the + build cache or /tmp directories. + EOF + build_data::setup + build_data::set_string "failure_reason" "build-data::data-file-deleted" + exit 1 + fi } # Check whether an entry exists in the build data store for the current build. @@ -143,6 +159,7 @@ function build_data::get_previous() { tac "${LEGACY_BUILD_DATA_FILE}" | { grep --perl-regexp --only-matching --max-count=1 "^${key}=\K.*$" || true; } elif [[ -f "${PREVIOUS_BUILD_DATA_FILE}" ]]; then # The `// empty` ensures we return the empty string rather than `null` if the key doesn't exist. + # We don't use `--exit-status` since `false` is a valid value for us to retrieve. jq --raw-output ".${key} // empty" "${PREVIOUS_BUILD_DATA_FILE}" fi } @@ -170,5 +187,5 @@ function build_data::current_unix_realtime() { # build_data::print_bin_report_json # ``` function build_data::print_bin_report_json() { - jq --sort-keys '.' "${BUILD_DATA_FILE}" + jq --exit-status --sort-keys '.' "${BUILD_DATA_FILE}" } diff --git a/spec/fixtures/hooks_delete_cache_dir/bin/post_compile b/spec/fixtures/hooks_delete_cache_dir/bin/post_compile new file mode 100644 index 000000000..383d0656b --- /dev/null +++ b/spec/fixtures/hooks_delete_cache_dir/bin/post_compile @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -euo pipefail + +set -x +rm -rf "${CACHE_DIR:?}" diff --git a/spec/fixtures/hooks_delete_cache_dir/requirements.txt b/spec/fixtures/hooks_delete_cache_dir/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/spec/hatchet/hooks_spec.rb b/spec/hatchet/hooks_spec.rb index e58a7969c..18fa2eed6 100644 --- a/spec/hatchet/hooks_spec.rb +++ b/spec/hatchet/hooks_spec.rb @@ -78,4 +78,28 @@ end end end + + context 'when an app tries to delete the whole cache directory including the build data file' do + let(:app) { Hatchet::Runner.new('spec/fixtures/hooks_delete_cache_dir', allow_failure: true) } + + it 'aborts the build with a suitable error message' do + app.deploy do |app| + expect(clean_output(app.output)).to include(<<~OUTPUT) + remote: -----> Running bin/post_compile hook + remote: + rm -rf /tmp/codon/tmp/cache + remote: + remote: ! Error: Can't find the buildpack's build data file. + remote: ! + remote: ! The Python buildpack's internal build data file is missing: + remote: ! /tmp/codon/tmp/cache/build-data/python.json + remote: ! + remote: ! This file is required for the buildpack to work correctly, + remote: ! and so you must not delete it when removing files from the + remote: ! build cache or /tmp directories. + remote: + remote: ! Push rejected, failed to compile Python app. + OUTPUT + end + end + end end