diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 52082f8a2..33be3e324 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,6 @@ # Default to requesting pull request reviews from the Heroku Languages team. +#ECCN:Open Source +#GUSINFO:Languages,Heroku Python Platform * @heroku/languages # However, request review from the language owner instead for files that are updated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57f26122d..56224eeb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,4 +73,4 @@ jobs: - name: Run buildpack using default app fixture run: make run - name: Run buildpack using an app fixture that's expected to fail - run: make run FIXTURE=spec/fixtures/python_version_file_invalid_version/ + run: make run FIXTURE=spec/fixtures/python_version_file_invalid_version/ COMPILE_FAILURE_EXIT_CODE=0 diff --git a/.rubocop.yml b/.rubocop.yml index 44a322aef..2edf1e039 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,4 @@ -require: +plugins: - rubocop-rspec AllCops: diff --git a/CHANGELOG.md b/CHANGELOG.md index c7817d81e..d45887f0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,42 @@ ## [Unreleased] +## [v281] - 2025-04-08 + +- The Python 3.13 version alias now resolves to Python 3.13.3. ([#1775](https://github.com/heroku/heroku-buildpack-python/pull/1775)) +- The Python 3.12 version alias now resolves to Python 3.12.10. ([#1775](https://github.com/heroku/heroku-buildpack-python/pull/1775)) +- The Python 3.11 version alias now resolves to Python 3.11.12. ([#1775](https://github.com/heroku/heroku-buildpack-python/pull/1775)) +- The Python 3.10 version alias now resolves to Python 3.10.17. ([#1775](https://github.com/heroku/heroku-buildpack-python/pull/1775)) +- The Python 3.9 version alias now resolves to Python 3.9.22. ([#1775](https://github.com/heroku/heroku-buildpack-python/pull/1775)) + +## [v280] - 2025-04-08 + +- Updated pip from 24.3.1 to 25.0.1. ([#1759](https://github.com/heroku/heroku-buildpack-python/pull/1759)) +- Updated Poetry from 2.1.1 to 2.1.2. ([#1772](https://github.com/heroku/heroku-buildpack-python/pull/1772)) + +## [v279] - 2025-02-26 + +- Updated Poetry from 2.0.1 to 2.1.1. ([#1758](https://github.com/heroku/heroku-buildpack-python/pull/1758)) +- Stopped filtering out pip's `Requirement already satisfied:` log lines when installing dependencies. ([#1765](https://github.com/heroku/heroku-buildpack-python/pull/1765)) +- Improved the error messages shown if installing pip/Poetry/Pipenv fails. ([#1764](https://github.com/heroku/heroku-buildpack-python/pull/1764)) +- Stopped installing pip into Poetry's virtual environment. ([#1761](https://github.com/heroku/heroku-buildpack-python/pull/1761)) + +## [v278] - 2025-02-24 + +- Added build-time rewriting of editable VCS dependency paths (in addition to the existing run-time rewriting), to work around an upstream Pipenv bug with editable VCS dependencies not being reinstalled correctly for cached builds. ([#1756](https://github.com/heroku/heroku-buildpack-python/pull/1756)) +- Changed the location of repositories for editable VCS dependencies when using pip and Pipenv, to improve build performance and match the behaviour when using Poetry. ([#1753](https://github.com/heroku/heroku-buildpack-python/pull/1753)) + +## [v277] - 2025-02-17 + +- Improved the warning message shown when the requested Python version is not the latest patch version. ([#1749](https://github.com/heroku/heroku-buildpack-python/pull/1749)) +- Improved the error message shown when the requested Python patch version isn't available. ([#1749](https://github.com/heroku/heroku-buildpack-python/pull/1749)) +- Improved the error message shown if there was a networking or server related error downloading Python. ([#1749](https://github.com/heroku/heroku-buildpack-python/pull/1749)) +- Adjusted the curl options used when downloading Python to set a maximum download time of 120s to prevent hanging builds in the case of network issues. ([#1749](https://github.com/heroku/heroku-buildpack-python/pull/1749)) +- Refactored the Python download step to avoid an unnecessary version check `HEAD` request to S3 prior to downloading Python or reusing a cached install. ([#1749](https://github.com/heroku/heroku-buildpack-python/pull/1749)) +- Updated the `runtime.txt` deprecation warning to include a link to the deprecation changelog post. ([#1747](https://github.com/heroku/heroku-buildpack-python/pull/1747)) +- Improved buildpack metrics for Python version selection. ([#1749](https://github.com/heroku/heroku-buildpack-python/pull/1749)) +- Improved buildpack metrics for builds that fail. ([#1746](https://github.com/heroku/heroku-buildpack-python/pull/1746) and [#1749](https://github.com/heroku/heroku-buildpack-python/pull/1749)) + ## [v276] - 2025-02-05 - The Python 3.13 version alias now resolves to Python 3.13.2. ([#1744](https://github.com/heroku/heroku-buildpack-python/pull/1744)) @@ -1157,7 +1193,12 @@ Default Python is now latest 2.7.10. Updated pip and Distribute. - Setuptools updated to v16.0 - pip updated to v7.0.1 -[unreleased]: https://github.com/heroku/heroku-buildpack-python/compare/v276...main +[unreleased]: https://github.com/heroku/heroku-buildpack-python/compare/v281...main +[v281]: https://github.com/heroku/heroku-buildpack-python/compare/v280...v281 +[v280]: https://github.com/heroku/heroku-buildpack-python/compare/v279...v280 +[v279]: https://github.com/heroku/heroku-buildpack-python/compare/v278...v279 +[v278]: https://github.com/heroku/heroku-buildpack-python/compare/v277...v278 +[v277]: https://github.com/heroku/heroku-buildpack-python/compare/v276...v277 [v276]: https://github.com/heroku/heroku-buildpack-python/compare/v275...v276 [v275]: https://github.com/heroku/heroku-buildpack-python/compare/v274...v275 [v274]: https://github.com/heroku/heroku-buildpack-python/compare/v273...v274 @@ -1168,65 +1209,65 @@ Default Python is now latest 2.7.10. Updated pip and Distribute. [v269]: https://github.com/heroku/heroku-buildpack-python/compare/v268...v269 [v268]: https://github.com/heroku/heroku-buildpack-python/compare/v267...v268 [v267]: https://github.com/heroku/heroku-buildpack-python/compare/v266...v267 -[v266]: https://github.com/heroku/heroku-buildpack-python/compare/v265...v266 -[v265]: https://github.com/heroku/heroku-buildpack-python/compare/v264...v265 -[v264]: https://github.com/heroku/heroku-buildpack-python/compare/v263...v264 -[v263]: https://github.com/heroku/heroku-buildpack-python/compare/v262...v263 -[v262]: https://github.com/heroku/heroku-buildpack-python/compare/v261...v262 -[v261]: https://github.com/heroku/heroku-buildpack-python/compare/v260...v261 -[v260]: https://github.com/heroku/heroku-buildpack-python/compare/v259...v260 -[v259]: https://github.com/heroku/heroku-buildpack-python/compare/v258...v259 -[v258]: https://github.com/heroku/heroku-buildpack-python/compare/v257...v258 -[v257]: https://github.com/heroku/heroku-buildpack-python/compare/v256...v257 -[v256]: https://github.com/heroku/heroku-buildpack-python/compare/v255...v256 -[v255]: https://github.com/heroku/heroku-buildpack-python/compare/v254...v255 -[v254]: https://github.com/heroku/heroku-buildpack-python/compare/v253...v254 -[v253]: https://github.com/heroku/heroku-buildpack-python/compare/v252...v253 -[v252]: https://github.com/heroku/heroku-buildpack-python/compare/v251...v252 -[v251]: https://github.com/heroku/heroku-buildpack-python/compare/v250...v251 -[v250]: https://github.com/heroku/heroku-buildpack-python/compare/v249...v250 -[v249]: https://github.com/heroku/heroku-buildpack-python/compare/v248...v249 -[v248]: https://github.com/heroku/heroku-buildpack-python/compare/v247...v248 -[v247]: https://github.com/heroku/heroku-buildpack-python/compare/v246...v247 -[v246]: https://github.com/heroku/heroku-buildpack-python/compare/v245...v246 -[v245]: https://github.com/heroku/heroku-buildpack-python/compare/v244...v245 -[v244]: https://github.com/heroku/heroku-buildpack-python/compare/v243...v244 -[v243]: https://github.com/heroku/heroku-buildpack-python/compare/v242...v243 -[v242]: https://github.com/heroku/heroku-buildpack-python/compare/v241...v242 -[v241]: https://github.com/heroku/heroku-buildpack-python/compare/v240...v241 -[v240]: https://github.com/heroku/heroku-buildpack-python/compare/v239...v240 -[v239]: https://github.com/heroku/heroku-buildpack-python/compare/v238...v239 -[v238]: https://github.com/heroku/heroku-buildpack-python/compare/v237...v238 -[v237]: https://github.com/heroku/heroku-buildpack-python/compare/v236...v237 -[v236]: https://github.com/heroku/heroku-buildpack-python/compare/v235...v236 -[v235]: https://github.com/heroku/heroku-buildpack-python/compare/v234...v235 -[v234]: https://github.com/heroku/heroku-buildpack-python/compare/v233...v234 -[v233]: https://github.com/heroku/heroku-buildpack-python/compare/v232...v233 -[v232]: https://github.com/heroku/heroku-buildpack-python/compare/v231...v232 -[v231]: https://github.com/heroku/heroku-buildpack-python/compare/v230...v231 -[v230]: https://github.com/heroku/heroku-buildpack-python/compare/v229...v230 -[v229]: https://github.com/heroku/heroku-buildpack-python/compare/v228...v229 -[v228]: https://github.com/heroku/heroku-buildpack-python/compare/v227...v228 -[v227]: https://github.com/heroku/heroku-buildpack-python/compare/v226...v227 -[v226]: https://github.com/heroku/heroku-buildpack-python/compare/v225...v226 -[v225]: https://github.com/heroku/heroku-buildpack-python/compare/v224...v225 -[v224]: https://github.com/heroku/heroku-buildpack-python/compare/v223...v224 -[v223]: https://github.com/heroku/heroku-buildpack-python/compare/v222...v223 -[v222]: https://github.com/heroku/heroku-buildpack-python/compare/v221...v222 -[v221]: https://github.com/heroku/heroku-buildpack-python/compare/v220...v221 -[v220]: https://github.com/heroku/heroku-buildpack-python/compare/v219...v220 -[v219]: https://github.com/heroku/heroku-buildpack-python/compare/v218...v219 -[v218]: https://github.com/heroku/heroku-buildpack-python/compare/v217...v218 -[v217]: https://github.com/heroku/heroku-buildpack-python/compare/v216...v217 -[v216]: https://github.com/heroku/heroku-buildpack-python/compare/v215...v216 -[v215]: https://github.com/heroku/heroku-buildpack-python/compare/v214...v215 -[v214]: https://github.com/heroku/heroku-buildpack-python/compare/v213...v214 -[v213]: https://github.com/heroku/heroku-buildpack-python/compare/v212...v213 -[v212]: https://github.com/heroku/heroku-buildpack-python/compare/v211...v212 -[v211]: https://github.com/heroku/heroku-buildpack-python/compare/v210...v211 -[v210]: https://github.com/heroku/heroku-buildpack-python/compare/v209...v210 -[v209]: https://github.com/heroku/heroku-buildpack-python/compare/v208...v209 -[v208]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v207...v208 +[v266]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v265...v266 +[v265]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v264...archive/v265 +[v264]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v263...archive/v264 +[v263]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v262...archive/v263 +[v262]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v261...archive/v262 +[v261]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v260...archive/v261 +[v260]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v259...archive/v260 +[v259]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v258...archive/v259 +[v258]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v257...archive/v258 +[v257]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v256...archive/v257 +[v256]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v255...archive/v256 +[v255]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v254...archive/v255 +[v254]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v253...archive/v254 +[v253]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v252...archive/v253 +[v252]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v251...archive/v252 +[v251]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v250...archive/v251 +[v250]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v249...archive/v250 +[v249]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v248...archive/v249 +[v248]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v247...archive/v248 +[v247]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v246...archive/v247 +[v246]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v245...archive/v246 +[v245]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v244...archive/v245 +[v244]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v243...archive/v244 +[v243]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v242...archive/v243 +[v242]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v241...archive/v242 +[v241]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v240...archive/v241 +[v240]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v239...archive/v240 +[v239]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v238...archive/v239 +[v238]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v237...archive/v238 +[v237]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v236...archive/v237 +[v236]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v235...archive/v236 +[v235]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v234...archive/v235 +[v234]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v233...archive/v234 +[v233]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v232...archive/v233 +[v232]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v231...archive/v232 +[v231]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v230...archive/v231 +[v230]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v229...archive/v230 +[v229]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v228...archive/v229 +[v228]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v227...archive/v228 +[v227]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v226...archive/v227 +[v226]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v225...archive/v226 +[v225]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v224...archive/v225 +[v224]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v223...archive/v224 +[v223]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v222...archive/v223 +[v222]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v221...archive/v222 +[v221]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v220...archive/v221 +[v220]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v219...archive/v220 +[v219]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v218...archive/v219 +[v218]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v217...archive/v218 +[v217]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v216...archive/v217 +[v216]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v215...archive/v216 +[v215]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v214...archive/v215 +[v214]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v213...archive/v214 +[v213]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v212...archive/v213 +[v212]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v211...archive/v212 +[v211]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v210...archive/v211 +[v210]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v209...archive/v210 +[v209]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v208...archive/v209 +[v208]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v207...archive/v208 [v207]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v206...archive/v207 [v206]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v205...archive/v206 [v205]: https://github.com/heroku/heroku-buildpack-python/compare/archive/v204...archive/v205 diff --git a/Gemfile b/Gemfile index 215de3b19..2abf2b7cb 100644 --- a/Gemfile +++ b/Gemfile @@ -6,8 +6,6 @@ ruby '>= 3.2', '< 3.5' group :test, :development do gem 'heroku_hatchet' - # Work around https://github.com/excon/excon/issues/860 - gem 'logger' gem 'parallel_split_test' gem 'rspec-core' gem 'rspec-expectations' diff --git a/Gemfile.lock b/Gemfile.lock index b708a7e83..d633d680f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,12 @@ GEM remote: https://rubygems.org/ specs: - ast (2.4.2) + ast (2.4.3) base64 (0.2.0) - diff-lcs (1.5.1) + diff-lcs (1.6.0) erubis (2.7.0) - excon (0.112.0) + excon (1.2.5) + logger heroics (0.1.3) base64 erubis (~> 2.0) @@ -13,34 +14,36 @@ GEM moneta multi_json (>= 1.9.2) webrick - heroku_hatchet (8.0.4) - excon (~> 0) + heroku_hatchet (8.0.5) + excon (< 2) platform-api (~> 3) rrrretry (~> 1) thor (~> 1) threaded (~> 0) - json (2.9.1) + json (2.10.2) language_server-protocol (3.17.0.4) - logger (1.6.5) + lint_roller (1.1.0) + logger (1.6.6) moneta (1.0.0) multi_json (1.15.0) parallel (1.26.3) parallel_split_test (0.10.0) parallel (>= 0.5.13) rspec-core (>= 3.9.0) - parser (3.3.7.0) + parser (3.3.7.4) ast (~> 2.4.1) racc - platform-api (3.7.0) + platform-api (3.8.0) heroics (~> 0.1.1) moneta (~> 1.0.0) rate_throttle_client (~> 0.1.0) + prism (1.4.0) racc (1.8.1) rainbow (3.1.1) rate_throttle_client (0.1.2) regexp_parser (2.10.0) rrrretry (1.0.0) - rspec-core (3.13.2) + rspec-core (3.13.3) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) @@ -48,20 +51,23 @@ GEM rspec-retry (0.6.2) rspec-core (> 3.3) rspec-support (3.13.2) - rubocop (1.71.1) + rubocop (1.75.1) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.43.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.38.0) - parser (>= 3.3.1.0) - rubocop-rspec (3.4.0) - rubocop (~> 1.61) + rubocop-ast (1.43.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-rspec (3.5.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) ruby-progressbar (1.13.0) thor (1.3.2) threaded (0.0.4) @@ -75,7 +81,6 @@ PLATFORMS DEPENDENCIES heroku_hatchet - logger parallel_split_test rspec-core rspec-expectations @@ -84,7 +89,7 @@ DEPENDENCIES rubocop-rspec RUBY VERSION - ruby 3.4.1p0 + ruby 3.4.2p28 BUNDLED WITH - 2.6.2 + 2.6.3 diff --git a/Makefile b/Makefile index 43a6113b3..6ba94fef7 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ STACK ?= heroku-24 FIXTURE ?= spec/fixtures/python_version_unspecified +# Allow overriding the exit code in CI, so we can test bin/report works for failing builds. +COMPILE_FAILURE_EXIT_CODE ?= 1 # Converts a stack name of `heroku-NN` to its build Docker image tag of `heroku/heroku:NN-build`. STACK_IMAGE_TAG := heroku/$(subst -,:,$(STACK))-build @@ -24,18 +26,21 @@ format: run: @echo "Running buildpack using: STACK=$(STACK) FIXTURE=$(FIXTURE)" @docker run --rm -v $(PWD):/src:ro --tmpfs /app -e "HOME=/app" -e "STACK=$(STACK)" "$(STACK_IMAGE_TAG)" \ - bash -euo pipefail -c '\ - mkdir /tmp/buildpack /tmp/build /tmp/cache /tmp/env; \ + bash -euo pipefail -O dotglob -c '\ + mkdir /tmp/buildpack /tmp/cache /tmp/env; \ cp -r /src/{bin,lib,requirements,vendor} /tmp/buildpack; \ - cp -rT /src/$(FIXTURE) /tmp/build; \ + cp -r /src/$(FIXTURE) /tmp/build_1; \ cd /tmp/buildpack; \ unset $$(printenv | cut -d '=' -f 1 | grep -vE "^(HOME|LANG|PATH|STACK)$$"); \ - echo -e "\n~ Detect:" && ./bin/detect /tmp/build; \ - echo -e "\n~ Compile:" && { ./bin/compile /tmp/build /tmp/cache /tmp/env || COMPILE_FAILED=1; }; \ - echo -e "\n~ Report:" && ./bin/report /tmp/build /tmp/cache /tmp/env; \ - [[ "$${COMPILE_FAILED:-}" == "1" ]] && exit 0; \ - [[ -f /tmp/build/bin/compile ]] && { echo -e "\n~ Compile (Inline Buildpack):" && (source ./export && /tmp/build/bin/compile /tmp/build /tmp/cache /tmp/env); }; \ - echo -e "\n~ Release:" && ./bin/release /tmp/build; \ + echo -en "\n~ Detect: " && ./bin/detect /tmp/build_1; \ + echo -e "\n~ Compile:" && { ./bin/compile /tmp/build_1 /tmp/cache /tmp/env || COMPILE_FAILED=1; }; \ + echo -e "\n~ Report:" && ./bin/report /tmp/build_1 /tmp/cache /tmp/env; \ + [[ "$${COMPILE_FAILED:-}" == "1" ]] && exit $(COMPILE_FAILURE_EXIT_CODE); \ + [[ -f /tmp/build_1/bin/compile ]] && { echo -e "\n~ Compile (Inline Buildpack):" && (source ./export && /tmp/build_1/bin/compile /tmp/build_1 /tmp/cache /tmp/env); }; \ + echo -e "\n~ Release:" && ./bin/release /tmp/build_1; \ + rm -rf /app/* /tmp/buildpack/export /tmp/build_1; \ + cp -r /src/$(FIXTURE) /tmp/build_2; \ + echo -e "\n~ Recompile:" && ./bin/compile /tmp/build_2 /tmp/cache /tmp/env; \ echo -e "\nBuild successful!"; \ ' @echo diff --git a/bin/compile b/bin/compile index 10a2187ce..b75752b91 100755 --- a/bin/compile +++ b/bin/compile @@ -28,8 +28,9 @@ source "${BUILDPACK_DIR}/lib/output.sh" source "${BUILDPACK_DIR}/lib/package_manager.sh" source "${BUILDPACK_DIR}/lib/pip.sh" source "${BUILDPACK_DIR}/lib/pipenv.sh" -source "${BUILDPACK_DIR}/lib/python_version.sh" source "${BUILDPACK_DIR}/lib/poetry.sh" +source "${BUILDPACK_DIR}/lib/python_version.sh" +source "${BUILDPACK_DIR}/lib/python.sh" compile_start_time=$(nowms) @@ -49,13 +50,6 @@ export PATH=:/usr/local/bin:$PATH # Exported for use in subshells, such as the steps run via sub_env. export BUILD_DIR CACHE_DIR ENV_DIR -# Set the base URL for downloading buildpack assets like Python runtimes. -# The user can provide BUILDPACK_S3_BASE_URL to specify a custom target. -# Note: this is designed for non-Heroku use, as it does not use the user-provided -# environment variable mechanism (the ENV_DIR). -S3_BASE_URL="${BUILDPACK_S3_BASE_URL:-"https://heroku-buildpack-python.s3.us-east-1.amazonaws.com"}" -# This has to be exported since it's used by the geo-libs step which is run in a subshell. - # Common Problem Warnings: # This section creates a temporary file in which to stick the output of `pip install`. # The `warnings` subscript then greps through this for common problems and guides @@ -100,10 +94,6 @@ export LIBRARY_PATH="/app/.heroku/python/lib${LIBRARY_PATH:+:${LIBRARY_PATH}}" export LD_LIBRARY_PATH="/app/.heroku/python/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" export PKG_CONFIG_PATH="/app/.heroku/python/lib/pkg-config${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" -# Global pip options (https://pip.pypa.io/en/stable/user_guide/#environment-variables). -# Disable pip's warnings about EOL Python since we show our own. -export PIP_NO_PYTHON_VERSION_WARNING=1 - cd "$BUILD_DIR" # Runs a `bin/pre_compile` script if found in the app source, allowing build customisation. @@ -121,6 +111,7 @@ cached_python_full_version="$(cache::cached_python_full_version "${CACHE_DIR}")" # We use the Bash 4.3+ `nameref` feature to pass back multiple values from this function # without having to hardcode globals. See: https://stackoverflow.com/a/38997681 python_version::read_requested_python_version "${BUILD_DIR}" "${package_manager}" "${cached_python_full_version}" requested_python_version python_version_origin +meta_set "python_version_requested" "${requested_python_version}" meta_set "python_version_reason" "${python_version_origin}" # TODO: More strongly recommend specifying a Python version (eg switch the messaging to @@ -144,12 +135,19 @@ python_major_version="${python_full_version%.*}" meta_set "python_version" "${python_full_version}" meta_set "python_version_major" "${python_major_version}" +if [[ "${requested_python_version}" == "${python_full_version}" ]]; then + meta_set "python_version_pinned" "true" +else + meta_set "python_version_pinned" "false" +fi + if [[ "${python_version_origin}" == "runtime.txt" ]]; then output::warning <<-EOF Warning: The runtime.txt file is deprecated. The runtime.txt file is deprecated since it has been replaced - by the more widely supported .python-version file. + by the more widely supported .python-version file: + https://devcenter.heroku.com/changelog-items/3141 Please delete your runtime.txt file and create a new file named: .python-version @@ -169,12 +167,13 @@ if [[ "${python_version_origin}" == "runtime.txt" ]]; then EOF fi +python_version::warn_if_deprecated_major_version "${python_major_version}" "${python_version_origin}" +python_version::warn_if_patch_update_available "${python_full_version}" "${python_major_version}" "${python_version_origin}" + cache::restore "${BUILD_DIR}" "${CACHE_DIR}" "${STACK}" "${cached_python_full_version}" "${python_full_version}" "${package_manager}" # The directory for the .profile.d scripts. mkdir -p "$(dirname "$PROFILE_PATH")" -# The directory for editable VCS dependencies. -mkdir -p /app/.heroku/src # On Heroku CI, builds happen in `/app`. Otherwise, on the Heroku platform, # they occur in a temp directory. Because Python is not portable, we must create @@ -186,13 +185,9 @@ if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then # python expects to reside in /app, so set up symlinks # we will not remove these later so subsequent buildpacks can still invoke it ln -nsf "$BUILD_DIR/.heroku/python" /app/.heroku/python - # Note: .heroku/src is copied in later. fi -# Download and install Python using pre-built binaries from S3. -install_python_start_time=$(nowms) -source "${BUILDPACK_DIR}/bin/steps/python" -meta_time "python_install_duration" "${install_python_start_time}" +python::install "${BUILD_DIR}" "${STACK}" "${python_full_version}" "${python_major_version}" "${python_version_origin}" # Install the package manager and related tools. package_manager_install_start_time=$(nowms) @@ -206,7 +201,7 @@ case "${package_manager}" in pipenv::install_pipenv ;; poetry) - poetry::install_poetry "${CACHE_DIR}" "${EXPORT_PATH}" + poetry::install_poetry "${python_home}" "${python_major_version}" "${CACHE_DIR}" "${EXPORT_PATH}" ;; *) utils::abort_internal_error "Unhandled package manager: ${package_manager}" @@ -248,15 +243,6 @@ nltk_downloader_start_time=$(nowms) sub_env "${BUILDPACK_DIR}/bin/steps/nltk" meta_time "nltk_downloader_duration" "${nltk_downloader_start_time}" -# Support for editable installations. -# In CI, $BUILD_DIR is /app. -# Realpath is used to support use-cases where one of the locations is a symlink to the other. -# shellcheck disable=SC2312 # TODO: Invoke this command separately to avoid masking its return value. -if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then - rm -rf "$BUILD_DIR/.heroku/src" - deep-cp /app/.heroku/src "$BUILD_DIR/.heroku/src" -fi - # Django collectstatic support. # The buildpack automatically runs collectstatic for Django applications. collectstatic_start_time=$(nowms) @@ -290,13 +276,43 @@ if [[ \$HOME != "/app" ]]; then fi EOT -# At runtime, rewrite paths in editable package .egg-link, .pth and finder files from the build time paths -# (such as `/tmp/build_`) back to `/app`. This is not done during the build itself, since later -# buildpacks still need the build time paths. +# When dependencies are installed in editable mode, the package manager/build backend creates `.pth` +# (and related) files in site-packages, which contain absolute path references to the actual location +# of the packages. By default the Heroku build runs from a directory like `/tmp/build_`, which +# changes every build and also differs from the app location at runtime (`/app`). This means any build +# directory paths referenced in .pth and related files will no longer exist at runtime or during cached +# rebuilds, unless we rewrite the paths. +# +# Ideally, we would be able to rewrite all paths to use the `/app/.heroku/python/` symlink trick we use +# when invoking Python, since then the same path would work across the current build, runtime and cached +# rebuilds. However, this trick only works for paths under that directory (since it's not possible to +# symlink `/app` or other directories we don't own), and when apps use path-based editable dependencies +# the paths will be outside of that (such as a subdirectory of the app source, or even the root of the +# build directory). We also can't just rewrite all paths now ready for runtime, since other buildpacks +# might run after this one that make use of the editable dependencies. As such, we have to perform path +# rewriting for path-based editable dependencies at app boot instead. +# +# For VCS editable dependencies, we can use the symlink trick and so configure the repo checkout location +# as `/app/.heroku/python/src/`, which in theory should mean the `.pth` files use that path. However, +# some build backends (such as setuptools' PEP660 implementation) call realpath on it causing the +# `/tmp/build_*` location to be written instead, meaning VCS src paths need to be rewritten regardless. +# +# In addition to ensuring dependencies work for subsequent buildpacks and at runtime, they must also +# work for cached rebuilds. Most package managers will reinstall editable dependencies regardless on +# next install, which means we can avoid having to rewrite paths on cache restore from the old build +# directory to the new location (`/tmp/build_`). However, Pipenv has a bug when using +# PEP660 style editable VCS dependencies where it won't reinstall if it's missing (or in our case, the +# path has changed), which means we must make sure that VCS src paths stored in the cache do use the +# symlink path. See: https://github.com/pypa/pipenv/issues/6348 +# +# As such, we have to perform two rewrites: +# 1. At build time, of just the VCS editable paths (which we can safely change to /app paths early). +# 2. At runtime, to rewrite the remaining path-based editable dependency paths. if [[ "${BUILD_DIR}" != "/app" ]]; then - cat <>"$PROFILE_PATH" -find .heroku/python/lib/python*/site-packages/ -type f -and \( -name '*.egg-link' -or -name '*.pth' -or -name '__editable___*_finder.py' \) -exec sed -i -e 's#${BUILD_DIR}#/app#' {} \+ -EOT + find .heroku/python/lib/python*/site-packages/ -type f -and \( -name '*.egg-link' -or -name '*.pth' -or -name '__editable___*_finder.py' \) -exec sed -i -e "s#${BUILD_DIR}/.heroku/python#/app/.heroku/python#" {} \+ + cat <<-EOT >>"${PROFILE_PATH}" + find .heroku/python/lib/python*/site-packages/ -type f -and \( -name '*.egg-link' -or -name '*.pth' -or -name '__editable___*_finder.py' \) -exec sed -i -e 's#${BUILD_DIR}#/app#' {} \+ + EOT fi # Install sane-default script for $WEB_CONCURRENCY and $FORWARDED_ALLOW_IPS. diff --git a/bin/report b/bin/report index f05d67b6c..9a3eca067 100755 --- a/bin/report +++ b/bin/report @@ -62,6 +62,7 @@ kv_pair_string() { STRING_FIELDS=( cache_status django_collectstatic + failure_detail failure_reason nltk_downloader package_manager @@ -71,6 +72,7 @@ STRING_FIELDS=( poetry_version python_version_major python_version_reason + python_version_requested python_version setuptools_version wheel_version @@ -80,6 +82,7 @@ STRING_FIELDS=( ALL_OTHER_FIELDS=( cache_restore_duration cache_save_duration + custom_s3_base_url dependencies_install_duration django_collectstatic_duration duplicate_python_buildpack @@ -93,6 +96,7 @@ ALL_OTHER_FIELDS=( pre_compile_hook_duration python_install_duration python_version_outdated + python_version_pinned setup_py_only sqlite_install_duration total_duration diff --git a/bin/steps/python b/bin/steps/python deleted file mode 100755 index b345d5cd8..000000000 --- a/bin/steps/python +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2154 # TODO: Env var is referenced but not assigned. - -set -euo pipefail - -# The Python runtime archive filename is of form: 'python-X.Y.Z-ubuntu-22.04-amd64.tar.zst' -# The Ubuntu version is calculated from `STACK` since it's faster than calling `lsb_release`. -UBUNTU_VERSION="${STACK/heroku-/}.04" -ARCH=$(dpkg --print-architecture) -PYTHON_URL="${S3_BASE_URL}/python-${python_full_version}-ubuntu-${UBUNTU_VERSION}-${ARCH}.tar.zst" - -# The Python version validation earlier will have filtered out most unsupported versions. -# However, the version might still not be found if either: -# 1. It's a Python major version we've deprecated and so is only available on older stacks (i.e: Python 3.8). -# 2. If an exact Python version was requested and the patch version doesn't exist (e.g. 3.12.999). -# 3. The user has pinned to an older buildpack version and the S3 bucket location or layout has changed since. -# TODO: Update this message to be more specific now that Python 3.8 support has been removed, and so (1) can no longer occur. -if ! curl --output /dev/null --silent --head --fail --retry 3 --retry-connrefused --connect-timeout 10 "${PYTHON_URL}"; then - output::error <<-EOF - Error: Python ${python_full_version} isn't available for this stack (${STACK}). - - For a list of the supported Python versions, see: - https://devcenter.heroku.com/articles/python-support#supported-python-versions - EOF - meta_set "failure_reason" "python-version-not-found" - exit 1 -fi - -if [[ -f "${BUILD_DIR}/.heroku/python/bin/python" ]]; then - output::step "Using cached install of Python ${python_full_version}" -else - output::step "Installing Python ${python_full_version}" - mkdir -p "${BUILD_DIR}/.heroku/python" - - if ! curl --silent --show-error --fail --retry 3 --retry-connrefused --connect-timeout 10 "${PYTHON_URL}" | tar --zstd --extract --directory "${BUILD_DIR}/.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. - output::error <<-EOF - Error: Failed to download/install Python ${python_full_version}. - - In some cases, this happens due to an unstable network connection. - Please try again and to see if the error resolves itself. - EOF - meta_set "failure_reason" "python-download" - exit 1 - fi - - hash -r -fi - -function warn_if_patch_update_available() { - local requested_full_version="${1}" - local requested_major_version="${2}" - local latest_patch_version - latest_patch_version="$(python_version::resolve_python_version "${requested_major_version}" "${python_version_origin}")" - # Extract the patch version component of the version strings (ie: the '5' in '3.10.5'). - local requested_patch_number="${requested_full_version##*.}" - local latest_patch_number="${latest_patch_version##*.}" - # TODO: Update this message to suggest using the .python-version major version syntax to stay up to date, - # once runtime.txt is deprecated and sticky-versioning only pins to the major version. - if ((requested_patch_number < latest_patch_number)); then - output::warning <<-EOF - Warning: A Python security update is available! - - Upgrade as soon as possible to: Python ${latest_patch_version} - See: https://devcenter.heroku.com/articles/python-runtimes - EOF - meta_set "python_version_outdated" "true" - else - meta_set "python_version_outdated" "false" - fi -} - -# We wait until now to display outdated Python version warnings, since we only want to show them -# if there weren't any errors with the version to avoid adding noise to the error messages. -# TODO: Move this into lib/ as part of the warnings refactor. -if [[ "${python_major_version}" == "3.9" ]]; then - output::warning <<-EOF - Warning: Support for Python 3.9 is ending soon! - - Python 3.9 will reach its upstream end-of-life in October 2025, - at which point it will no longer receive security updates: - https://devguide.python.org/versions/#supported-versions - - As such, support for Python 3.9 will be removed from this - buildpack on 7th January 2026. - - Upgrade to a newer Python version as soon as possible, by - changing the version in your ${python_version_origin} file. - - For more information, see: - https://devcenter.heroku.com/articles/python-support#supported-python-versions - EOF -fi - -warn_if_patch_update_available "${python_full_version}" "${python_major_version}" diff --git a/bin/utils b/bin/utils index 9e662191a..195172a7a 100755 --- a/bin/utils +++ b/bin/utils @@ -8,22 +8,6 @@ shopt -s nullglob source "${BUILDPACK_DIR:?}/vendor/buildpack-stdlib_v8.sh" -# Does some serious copying. -deep-cp() { - declare source="$1" target="$2" - - mkdir -p "$target" - - # cp doesn't like being called without source params, - # so make sure they expand to something first. - # subshell to avoid surprising caller with shopts. - ( - shopt -s nullglob dotglob - set -- "$source"/!(tmp|.|..) - [[ $# == 0 ]] || cp -a "$@" "$target" - ) -} - # Measure the size of the Python installation. measure-size() { { du -s .heroku/python 2>/dev/null || echo 0; } | awk '{print $1}' diff --git a/lib/cache.sh b/lib/cache.sh index ef62d582f..f5055b982 100644 --- a/lib/cache.sh +++ b/lib/cache.sh @@ -102,6 +102,10 @@ function cache::restore() { elif [[ "${cached_pipenv_version}" != "${PIPENV_VERSION:?}" ]]; then cache_invalidation_reasons+=("The Pipenv version has changed from ${cached_pipenv_version} to ${PIPENV_VERSION}") fi + # TODO: Remove this next time the Pipenv version is bumped (since it will trigger cache invalidation of its own) + if [[ -d "${cache_dir}/.heroku/src" ]]; then + cache_invalidation_reasons+=("The editable VCS repository location has changed (and Pipenv doesn't handle this correctly)") + fi ;; poetry) local cached_poetry_version @@ -129,7 +133,6 @@ function cache::restore() { "${cache_dir}/.heroku/python-poetry" \ "${cache_dir}/.heroku/python-stack" \ "${cache_dir}/.heroku/python-version" \ - "${cache_dir}/.heroku/src" \ "${cache_dir}/.heroku/requirements.txt" meta_set "cache_status" "discarded" @@ -143,17 +146,13 @@ function cache::restore() { # TODO: Compare the performance of moving the directory vs copying files. cp -R "${cache_dir}/.heroku/python" "${build_dir}/.heroku/" &>/dev/null || true - # Editable VCS code repositories, used by pip/pipenv. - if [[ -d "${cache_dir}/.heroku/src" ]]; then - cp -R "${cache_dir}/.heroku/src" "${build_dir}/.heroku/" &>/dev/null || true - fi - meta_set "cache_status" "reused" fi # Remove any legacy cache contents written by older buildpack versions. rm -rf \ "${cache_dir}/.heroku/python-sqlite3-version" \ + "${cache_dir}/.heroku/src" \ "${cache_dir}/.heroku/vendor" meta_time "cache_restore_duration" "${cache_restore_start_time}" @@ -175,13 +174,6 @@ function cache::save() { rm -rf "${cache_dir}/.heroku/python" cp -R "${build_dir}/.heroku/python" "${cache_dir}/.heroku/" - # Editable VCS code repositories, used by pip/pipenv. - rm -rf "${cache_dir}/.heroku/src" - if [[ -d "${build_dir}/.heroku/src" ]]; then - # TODO: Investigate why errors are ignored and ideally stop doing so. - cp -R "${build_dir}/.heroku/src" "${cache_dir}/.heroku/" &>/dev/null || true - fi - # Metadata used by subsequent builds to determine whether the cache can be reused. # These are written/consumed via separate files and not the metadata store for compatibility # with buildpack versions prior to the metadata store existing (which was only added in v252). diff --git a/lib/checks.sh b/lib/checks.sh index 9129244e7..3a43cdad7 100644 --- a/lib/checks.sh +++ b/lib/checks.sh @@ -20,6 +20,7 @@ function checks::ensure_supported_stack() { Upgrade to a newer stack to continue using this buildpack. EOF meta_set "failure_reason" "stack::eol" + meta_set "failure_detail" "${stack}" exit 1 ;; *) @@ -34,6 +35,7 @@ function checks::ensure_supported_stack() { https://devcenter.heroku.com/articles/managing-buildpacks#classic-buildpacks-references EOF meta_set "failure_reason" "stack::unknown" + meta_set "failure_detail" "${stack}" exit 1 ;; esac diff --git a/lib/kvstore.sh b/lib/kvstore.sh index c4a684fea..b5c15aa60 100644 --- a/lib/kvstore.sh +++ b/lib/kvstore.sh @@ -23,8 +23,20 @@ kv_set() { # TODO: Stop ignoring an incorrect number of passed arguments. if [[ $# -eq 3 ]]; then local f="${1}" + local key="${2}" + + # Truncate the value to an arbitrary 100 characters since it will sometimes contain user-provided + # inputs which may be unbounded in size. Ideally individual call sites will perform more aggressive + # truncation themselves based on the expected value size, however this is here as a fallback. + # (Honeycomb supports string fields up to 64KB in size, however, it's not worth filling up the + # metadata store or bloating the payload passed back to Vacuole/submitted to Honeycomb given the + # extra content in those cases is not normally useful.) + local value="${3:0:100}" + # Replace newlines since the data store file format requires that keys don't span multiple lines. + value="${value//$'\n'/ }" + if [[ -f "${f}" ]]; then - echo "${2}=${3}" >>"${f}" + echo "${key}=${value}" >>"${f}" fi fi } diff --git a/lib/pip.sh b/lib/pip.sh index 929063fb4..ca95abfd6 100644 --- a/lib/pip.sh +++ b/lib/pip.sh @@ -46,6 +46,7 @@ function pip::install_pip_setuptools_wheel() { # app's requirements.txt in the last build). The install will be a no-op if the versions match. output::step "Installing ${packages_display_text}" + # shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled. if ! { python "${bundled_pip_module_path}" \ install \ @@ -53,15 +54,18 @@ function pip::install_pip_setuptools_wheel() { --no-cache-dir \ --no-input \ --quiet \ - "${packages_to_install[@]}" + "${packages_to_install[@]}" \ + |& output::indent }; then output::error <<-EOF Error: Unable to install pip. + In some cases, this happens due to a temporary issue with + the network connection or Python Package Index (PyPI). + Try building again to see if the error resolves itself. - If that does not help, check the status of PyPI (the Python - package repository service), here: + If that does not help, check the status of PyPI here: https://status.python.org EOF meta_set "failure_reason" "install-package-manager::pip" @@ -102,6 +106,9 @@ function pip::install_dependencies() { # We only display the most relevant command args here, to improve the signal to noise ratio. output::step "Installing dependencies using '${pip_install_command[*]}'" + # TODO: Remove --disable-pip-version-check in favour of exporting PIP_DISABLE_PIP_VERSION_CHECK. + # The sed usage is to reduce the verbosity of output lines like: + # "Requirement already satisfied: typing-extensions==4.12.2 in /app/.heroku/python/lib/python3.13/site-packages (from -r requirements.txt (line 5)) (4.12.2)" # shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled. if ! { "${pip_install_command[@]}" \ @@ -110,9 +117,9 @@ function pip::install_dependencies() { --no-cache-dir \ --no-input \ --progress-bar off \ - --src='/app/.heroku/src' \ + --src='/app/.heroku/python/src' \ |& tee "${WARNINGS_LOG:?}" \ - |& sed --unbuffered --expression '/Requirement already satisfied/d' \ + |& sed --unbuffered --expression 's# in /app/.heroku/python/lib/python.*/site-packages##' \ |& output::indent }; then # TODO: Overhaul warnings and combine them with error handling. diff --git a/lib/pipenv.sh b/lib/pipenv.sh index 34519eb1b..c5c50beb4 100644 --- a/lib/pipenv.sh +++ b/lib/pipenv.sh @@ -18,6 +18,8 @@ function pipenv::install_pipenv() { # TODO: Install Pipenv into a venv so it isn't leaked into the app environment. # TODO: Skip installing Pipenv if its version hasn't changed (once it's installed into a venv). # TODO: Explore viability of making Pipenv only be available during the build, to reduce slug size. + + # shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled. if ! { pip \ install \ @@ -25,15 +27,18 @@ function pipenv::install_pipenv() { --no-cache-dir \ --no-input \ --quiet \ - "pipenv==${PIPENV_VERSION}" + "pipenv==${PIPENV_VERSION}" \ + |& output::indent }; then output::error <<-EOF Error: Unable to install Pipenv. + In some cases, this happens due to a temporary issue with + the network connection or Python Package Index (PyPI). + Try building again to see if the error resolves itself. - If that does not help, check the status of PyPI (the Python - package repository service), here: + If that does not help, check the status of PyPI here: https://status.python.org EOF meta_set "failure_reason" "install-package-manager::pipenv" @@ -81,7 +86,7 @@ function pipenv::install_dependencies() { # shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled. if ! { "${pipenv_install_command[@]}" \ - --extra-pip-args='--src=/app/.heroku/src' \ + --extra-pip-args='--src=/app/.heroku/python/src' \ --system \ |& tee "${WARNINGS_LOG:?}" \ |& output::indent diff --git a/lib/poetry.sh b/lib/poetry.sh index a227ae1cd..a453356a1 100644 --- a/lib/poetry.sh +++ b/lib/poetry.sh @@ -7,8 +7,10 @@ set -euo pipefail POETRY_VERSION=$(utils::get_requirement_version 'poetry') function poetry::install_poetry() { - local cache_dir="${1}" - local export_file="${2}" + local python_home="${1}" + local python_major_version="${2}" + local cache_dir="${3}" + local export_file="${4}" # We store Poetry in the build cache, since we only need it during the build. local poetry_root="${cache_dir}/.heroku/python-poetry" @@ -39,17 +41,13 @@ function poetry::install_poetry() { rm -rf "${poetry_root}" mkdir -p "${poetry_root}" - # We can't use the pip wheel bundled within Python's standard library to install Poetry - # (which would allow us to use `--without-pip` here to skip the pip install), since it - # requires using the `--python` option, which was only added in pip v22.3. And whilst - # all major Python versions we support now bundled a newer pip than that, some apps - # are still using outdated patch releases of those Python versions, whose bundled pip - # can be older (for example Python 3.9.0 ships with pip v20.2.1). Once Python 3.10 EOLs - # we can switch back to the previous approach since Python 3.11.0 ships with pip v22.3. - # Changing the working directory away from the build dir is required to work around an - # `ensurepip` bug in older Python versions, where it doesn't run Python in isolated mode: - # https://github.com/heroku/heroku-buildpack-python/issues/1697 - if ! (cd "${poetry_root}" && python -m venv "${poetry_venv_dir}"); then + # We use the pip wheel bundled within Python's standard library to install Poetry. + # Whilst Poetry does still require pip for some tasks (such as package uninstalls), + # it bundles its own copy for use as a fallback. As such we don't need to install pip + # into the Poetry venv (and in fact, Poetry wouldn't use this install anyway, since + # it only finds an external pip if it exists in the target venv). + # shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled. + if ! python -m venv --without-pip "${poetry_venv_dir}" |& output::indent; then output::error <<-EOF Internal Error: Unable to create virtual environment for Poetry. @@ -62,22 +60,32 @@ function poetry::install_poetry() { exit 1 fi + local bundled_pip_module_path + bundled_pip_module_path="$(utils::bundled_pip_module_path "${python_home}" "${python_major_version}")" + + # We must call the venv Python directly here, rather than relying on pip's `--python` + # option, since `--python` was only added in pip v22.3, so isn't supported by the older + # pip versions bundled with Python 3.9/3.10. + # shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled. if ! { - "${poetry_venv_dir}/bin/pip" \ + "${poetry_venv_dir}/bin/python" "${bundled_pip_module_path}" \ install \ --disable-pip-version-check \ --no-cache-dir \ --no-input \ --quiet \ - "poetry==${POETRY_VERSION}" + "poetry==${POETRY_VERSION}" \ + |& output::indent }; then output::error <<-EOF Error: Unable to install Poetry. + In some cases, this happens due to a temporary issue with + the network connection or Python Package Index (PyPI). + Try building again to see if the error resolves itself. - If that does not help, check the status of PyPI (the Python - package repository service), here: + If that does not help, check the status of PyPI here: https://status.python.org EOF meta_set "failure_reason" "install-package-manager::poetry" @@ -91,14 +99,24 @@ function poetry::install_poetry() { fi export PATH="${poetry_bin_dir}:${PATH}" - echo "export PATH=\"${poetry_bin_dir}:\${PATH}\"" >>"${export_file}" # Force Poetry to manage the system Python site-packages instead of using venvs. export POETRY_VIRTUALENVS_CREATE="false" - echo 'export POETRY_VIRTUALENVS_CREATE="false"' >>"${export_file}" + # Force Poetry to use our Python rather than scanning PATH (which might pick system Python). + # Though this currently doesn't work as documented: https://github.com/python-poetry/poetry/issues/10226 + export POETRY_VIRTUALENVS_USE_POETRY_PYTHON="true" + + # Set the same env vars in the environment used by later buildpacks. + cat >>"${export_file}" <<-EOF + export PATH="${poetry_bin_dir}:\${PATH}" + export POETRY_VIRTUALENVS_CREATE="false" + export POETRY_VIRTUALENVS_USE_POETRY_PYTHON="true" + EOF } # Note: We cache site-packages since: # - It results in faster builds than only caching Poetry's download/wheel cache. +# - It improves the UX of the build log, since Poetry will display which packages were +# added/removed since the last successful build. # - It's safe to do so, since `poetry sync` fully manages the environment (including # e.g. uninstalling packages when they are removed from the lockfile). # diff --git a/lib/python.sh b/lib/python.sh new file mode 100644 index 000000000..4bbeb344a --- /dev/null +++ b/lib/python.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash + +# This is technically redundant, since all consumers of this lib will have enabled these, +# however, it helps Shellcheck realise the options under which these functions will run. +set -euo pipefail + +DEFAULT_S3_BASE_URL="https://heroku-buildpack-python.s3.us-east-1.amazonaws.com" + +function python::install() { + local build_dir="${1}" + local stack="${2}" + local python_full_version="${3}" + local python_major_version="${4}" + local python_version_origin="${5}" + + local install_python_start_time + install_python_start_time=$(nowms) + local install_dir="${build_dir}/.heroku/python" + + if [[ -f "${install_dir}/bin/python" ]]; then + output::step "Using cached install of Python ${python_full_version}" + else + output::step "Installing Python ${python_full_version}" + + mkdir -p "${install_dir}" + + # Note: This can't be used via app config vars, since it doesn't reference the value from ENV_DIR. + # TODO: Remove this for parity with the Python CNB, if metrics show it to be unused on Heroku. + if [[ -v BUILDPACK_S3_BASE_URL ]]; then + local s3_base_url="${BUILDPACK_S3_BASE_URL}" + meta_set "custom_s3_base_url" "true" + else + local s3_base_url="${DEFAULT_S3_BASE_URL}" + fi + + # Calculating the Ubuntu version from the stack name saves having to shell out to `lsb_release`. + local ubuntu_version="${stack/heroku-/}.04" + local arch + arch=$(dpkg --print-architecture) + # e.g.: https://heroku-buildpack-python.s3.us-east-1.amazonaws.com/python-3.13.0-ubuntu-24.04-amd64.tar.zst + local python_url="${s3_base_url}/python-${python_full_version}-ubuntu-${ubuntu_version}-${arch}.tar.zst" + + local error_log + error_log=$(mktemp) + + # shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled. + if ! { + { + # We set max-time for improved UX/metrics for hanging downloads compared to relying + # on the build system timeout. The Python archives are only ~10 MB so take < 1s to + # download on Heroku's build system, however, we use much higher timeouts so that + # the buildpack works in non-Heroku environments that may be far from `us-east-1` + # or have a slower connection. We don't use `--speed-limit` since it gives worse + # error messages when used with retries and piping to tar. + curl \ + --connect-timeout 10 \ + --fail \ + --max-time 120 \ + --retry-max-time 120 \ + --retry 3 \ + --retry-connrefused \ + --show-error \ + --silent \ + "${python_url}" \ + | tar \ + --directory "${install_dir}" \ + --extract \ + --zstd + } \ + |& tee "${error_log}" \ + |& output::indent + }; then + local latest_known_patch_version + latest_known_patch_version="$(python_version::resolve_python_version "${python_major_version}" "${python_version_origin}")" + # Ideally we would inspect the HTTP status code directly instead of grepping, however: + # 1. We want to pipe to tar (since it's faster than performing the download and + # decompression/extraction as separate steps), so can't write to stdout. + # 2. We want to display the original stderr to the user, so can't write to stderr. + # 3. Curl's `--write-out` feature only supports outputting to a file (as opposed to + # stdout/stderr) as of curl v8.3.0, which is newer than the curl on Heroku-20/22. + # This has an integration test run against all stacks, which will mean we will know + # if future versions of curl change the error message string. + # + # We have to check for HTTP 403s too, since S3 will return a 403 instead of a 404 for + # missing files, if the S3 bucket does not have public list permissions enabled. + if [[ "${python_full_version}" != "${latest_known_patch_version}" ]] && grep --quiet "The requested URL returned error: 40[34]" "${error_log}"; then + output::error <<-EOF + Error: The requested Python version isn't available. + + Your app's ${python_version_origin} file specifies a Python version + of ${python_full_version}, however, we couldn't find that version on S3. + + Check that this Python version has been released upstream, + and that the Python buildpack has added support for it: + https://www.python.org/downloads/ + https://github.com/heroku/heroku-buildpack-python/blob/main/CHANGELOG.md + + If it has, make sure that you are using the latest version + of this buildpack, and haven't pinned to an older release: + https://devcenter.heroku.com/articles/managing-buildpacks#view-your-buildpacks + https://devcenter.heroku.com/articles/managing-buildpacks#classic-buildpacks-references + + We also strongly recommend that you do not pin your app to an + exact Python version such as ${python_full_version}, and instead only specify + the major Python version of ${python_major_version} in your ${python_version_origin} file. + This will allow your app to receive the latest available Python + patch version automatically, and prevent this type of error. + EOF + meta_set "failure_reason" "python-version::unknown-patch" + meta_set "failure_detail" "${python_full_version}" + else + output::error <<-EOF + Error: Unable to download/install Python. + + An error occurred while downloading/installing the Python + runtime archive from: + ${python_url} + + In some cases, this happens due to a temporary issue with + the network connection or server. + + First, make sure that you are using the latest version + of this buildpack, and haven't pinned to an older release: + https://devcenter.heroku.com/articles/managing-buildpacks#view-your-buildpacks + https://devcenter.heroku.com/articles/managing-buildpacks#classic-buildpacks-references + + Then try building again to see if the error resolves itself. + EOF + meta_set "failure_reason" "install-python" + # e.g.: 'curl: (6) Could not resolve host: heroku-buildpack-python.s3.us-east-1.amazonaws.com' + meta_set "failure_detail" "$(head --lines=1 "${error_log}" || true)" + fi + + exit 1 + fi + fi + + meta_time "python_install_duration" "${install_python_start_time}" +} diff --git a/lib/python_version.sh b/lib/python_version.sh index eec397496..02735b3b5 100644 --- a/lib/python_version.sh +++ b/lib/python_version.sh @@ -4,11 +4,11 @@ # however, it helps Shellcheck realise the options under which these functions will run. set -euo pipefail -LATEST_PYTHON_3_9="3.9.21" -LATEST_PYTHON_3_10="3.10.16" -LATEST_PYTHON_3_11="3.11.11" -LATEST_PYTHON_3_12="3.12.9" -LATEST_PYTHON_3_13="3.13.2" +LATEST_PYTHON_3_9="3.9.22" +LATEST_PYTHON_3_10="3.10.17" +LATEST_PYTHON_3_11="3.11.12" +LATEST_PYTHON_3_12="3.12.10" +LATEST_PYTHON_3_13="3.13.3" OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION=9 NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION=13 @@ -104,7 +104,7 @@ function python_version::parse_runtime_txt() { in the correct format. The following file contents were found, which aren't valid: - ${contents} + ${contents:0:100} However, the runtime.txt file is deprecated since it has been replaced by the .python-version file. As such, we @@ -125,6 +125,7 @@ function python_version::parse_runtime_txt() { your app to receive Python security updates. EOF meta_set "failure_reason" "runtime-txt::invalid-version" + meta_set "failure_detail" "${contents:0:50}" exit 1 fi } @@ -174,6 +175,7 @@ function python_version::parse_python_version_file() { your app to receive Python security updates. EOF meta_set "failure_reason" "python-version-file::invalid-version" + meta_set "failure_detail" "${line:0:50}" exit 1 fi ;; @@ -193,9 +195,11 @@ function python_version::parse_python_version_file() { begin with a '#', otherwise it will be treated as a comment. EOF meta_set "failure_reason" "python-version-file::no-version" + meta_set "failure_detail" "${contents:0:50}" exit 1 ;; *) + local first_five_version_lines=("${version_lines[@]:0:5}") output::error <<-EOF Error: Invalid Python version in .python-version. @@ -203,7 +207,7 @@ function python_version::parse_python_version_file() { $( IFS=$'\n' - echo "${version_lines[*]}" + echo "${first_five_version_lines[*]}" ) Update the file so it contains only one Python version. @@ -212,6 +216,10 @@ function python_version::parse_python_version_file() { lines begin with a '#', so that they are ignored. EOF meta_set "failure_reason" "python-version-file::multiple-versions" + meta_set "failure_detail" "$( + IFS=, + echo "${first_five_version_lines[*]}" + )" exit 1 ;; esac @@ -233,17 +241,19 @@ function python_version::read_pipenv_python_version() { fi if ! version=$(jq --raw-output '._meta.requires.python_full_version // ._meta.requires.python_version' "${pipfile_lock_path}" 2>&1); then + local jq_error_message="${version}" output::error <<-EOF Error: Can't parse Pipfile.lock. A Pipfile.lock file was found, however, it couldn't be parsed: - ${version} + ${jq_error_message} This is likely due to it not being valid JSON. Run 'pipenv lock' to regenerate/fix the lockfile. EOF meta_set "failure_reason" "pipfile-lock::invalid-json" + meta_set "failure_detail" "${jq_error_message:0:100}" exit 1 fi @@ -282,6 +292,7 @@ function python_version::read_pipenv_python_version() { https://pipenv.pypa.io/en/stable/specifiers.html#specifying-versions-of-python EOF meta_set "failure_reason" "pipfile-lock::invalid-version" + meta_set "failure_detail" "${version:0:50}" exit 1 fi } @@ -341,6 +352,7 @@ function python_version::resolve_python_version() { EOF fi meta_set "failure_reason" "python-version::eol" + meta_set "failure_detail" "${major}.${minor}" exit 1 fi @@ -388,6 +400,7 @@ function python_version::resolve_python_version() { EOF fi meta_set "failure_reason" "python-version::unknown-major" + meta_set "failure_detail" "${major}.${minor}" exit 1 fi @@ -403,3 +416,62 @@ function python_version::resolve_python_version() { *) utils::abort_internal_error "Unhandled Python major version: ${requested_python_version}" ;; esac } + +function python_version::warn_if_deprecated_major_version() { + local requested_major_version="${1}" + local version_origin="${2}" + + if [[ "${requested_major_version}" == "3.9" ]]; then + output::warning <<-EOF + Warning: Support for Python 3.9 is ending soon! + + Python 3.9 will reach its upstream end-of-life in October 2025, + at which point it will no longer receive security updates: + https://devguide.python.org/versions/#supported-versions + + As such, support for Python 3.9 will be removed from this + buildpack on 7th January 2026. + + Upgrade to a newer Python version as soon as possible, by + changing the version in your ${version_origin} file. + + For more information, see: + https://devcenter.heroku.com/articles/python-support#supported-python-versions + EOF + fi +} + +function python_version::warn_if_patch_update_available() { + local python_full_version="${1}" + local python_major_version="${2}" + local python_version_origin="${3}" + + local latest_known_patch_version + latest_known_patch_version="$(python_version::resolve_python_version "${python_major_version}" "${python_version_origin}")" + # Extract the patch version component of the version strings (ie: the '2' in '3.13.2'). + local requested_patch_number="${python_full_version##*.}" + local latest_patch_number="${latest_known_patch_version##*.}" + + if ((requested_patch_number < latest_patch_number)); then + output::warning <<-EOF + Warning: A Python patch update is available! + + Your app is using Python ${python_full_version}, however, there is a newer + patch release of Python ${python_major_version} available: ${latest_known_patch_version} + + It is important to always use the latest patch version of + Python to keep your app secure. + + Update your ${python_version_origin} file to use the new version. + + We strongly recommend that you do not pin your app to an + exact Python version such as ${python_full_version}, and instead only specify + the major Python version of ${python_major_version} in your ${python_version_origin} file. + This will allow your app to receive the latest available Python + patch version automatically and prevent this warning. + EOF + meta_set "python_version_outdated" "true" + else + meta_set "python_version_outdated" "false" + fi +} diff --git a/lib/utils.sh b/lib/utils.sh index 2bcae44c2..916886431 100644 --- a/lib/utils.sh +++ b/lib/utils.sh @@ -48,10 +48,12 @@ function utils::bundled_pip_module_path() { } function utils::abort_internal_error() { - local message="${1}" + local message + message="${1} (line $(caller || true))" output::error <<-EOF - Internal error: ${message} (line $(caller || true)). + Internal error: ${message}. EOF meta_set "failure_reason" "internal-error" + meta_set "failure_detail" "${message}" exit 1 } diff --git a/requirements/pip.txt b/requirements/pip.txt index 662f25f21..059862d50 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1 +1 @@ -pip==24.3.1 +pip==25.0.1 diff --git a/requirements/poetry.txt b/requirements/poetry.txt index d1c8013e2..7fc861c0f 100644 --- a/requirements/poetry.txt +++ b/requirements/poetry.txt @@ -1 +1 @@ -poetry==2.0.1 +poetry==2.1.2 diff --git a/spec/fixtures/pip_editable/bin/test-entrypoints.sh b/spec/fixtures/pip_editable/bin/test-entrypoints.sh index 4c27ba0f4..43afcb553 100755 --- a/spec/fixtures/pip_editable/bin/test-entrypoints.sh +++ b/spec/fixtures/pip_editable/bin/test-entrypoints.sh @@ -4,7 +4,7 @@ set -euo pipefail cd .heroku/python/lib/python*/site-packages/ -# List any path like strings in .egg-link, .pth, and finder files in site-packages. +# List any path like strings in the .egg-link, .pth, and finder files in site-packages. grep --extended-regexp --only-matching -- '/\S+' *.egg-link *.pth __editable___*_finder.py | sort echo diff --git a/spec/fixtures/pipenv_editable/.python-version b/spec/fixtures/pipenv_editable/.python-version deleted file mode 100644 index e4fba2183..000000000 --- a/spec/fixtures/pipenv_editable/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/spec/fixtures/pipenv_editable/Pipfile b/spec/fixtures/pipenv_editable/Pipfile index fcbd9c1a0..8bb51ee30 100644 --- a/spec/fixtures/pipenv_editable/Pipfile +++ b/spec/fixtures/pipenv_editable/Pipfile @@ -4,6 +4,7 @@ verify_ssl = true name = "pypi" [packages] +gunicorn = {git = "git+https://github.com/benoitc/gunicorn", editable = true} local-package-pyproject-toml = {file = "packages/local_package_pyproject_toml", editable = true} local-package-setup-py = {file = "packages/local_package_setup_py", editable = true} -gunicorn = {git = "git+https://github.com/benoitc/gunicorn", ref = "20.1.0", editable = true} +pipenv-editable = {file = ".", editable = true} diff --git a/spec/fixtures/pipenv_editable/Pipfile.lock b/spec/fixtures/pipenv_editable/Pipfile.lock index ae096d558..8c4b604a3 100644 --- a/spec/fixtures/pipenv_editable/Pipfile.lock +++ b/spec/fixtures/pipenv_editable/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5de61064f835cc6b28000b3ac1b7b5c38a40829ccc38ff46088e36bb3bd628fa" + "sha256": "bedd8ea507283c5458c9c2cb1fd55a6e5e69fecba7814ffc96bf25e03feaeabf" }, "pipfile-spec": 6, "requires": {}, @@ -18,7 +18,7 @@ "editable": true, "git": "git+https://github.com/benoitc/gunicorn", "markers": "python_version >= '3.7'", - "ref": "61ccfd6c38d477a908e0f376757bbb884438053a" + "ref": "bacbf8aa5152b94e44aa5d2a94aeaf0318a85248" }, "local-package-pyproject-toml": { "editable": true, @@ -28,13 +28,17 @@ "editable": true, "file": "packages/local_package_setup_py" }, - "setuptools": { + "packaging": { "hashes": [ - "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", - "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], - "markers": "python_version >= '3.9'", - "version": "==75.6.0" + "markers": "python_version >= '3.8'", + "version": "==24.2" + }, + "pipenv-editable": { + "editable": true, + "file": "." } }, "develop": {} diff --git a/spec/fixtures/pipenv_editable/bin/test-entrypoints.sh b/spec/fixtures/pipenv_editable/bin/test-entrypoints.sh index 4c27ba0f4..0e66f44bb 100755 --- a/spec/fixtures/pipenv_editable/bin/test-entrypoints.sh +++ b/spec/fixtures/pipenv_editable/bin/test-entrypoints.sh @@ -4,8 +4,8 @@ set -euo pipefail cd .heroku/python/lib/python*/site-packages/ -# List any path like strings in .egg-link, .pth, and finder files in site-packages. -grep --extended-regexp --only-matching -- '/\S+' *.egg-link *.pth __editable___*_finder.py | sort +# List any path like strings in the .pth and finder files in site-packages. +grep --extended-regexp --only-matching -- '/\S+' *.pth __editable___*_finder.py | sort echo echo -n "Running entrypoint for the pyproject.toml-based local package: " diff --git a/spec/fixtures/pipenv_editable/pipenv_editable/__init__.py b/spec/fixtures/pipenv_editable/pipenv_editable/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/pipenv_editable/pyproject.toml b/spec/fixtures/pipenv_editable/pyproject.toml new file mode 100644 index 000000000..2ca4481db --- /dev/null +++ b/spec/fixtures/pipenv_editable/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "pipenv-editable" +version = "0.0.0" +requires-python = ">=3.13" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/spec/fixtures/poetry_basic/poetry.lock b/spec/fixtures/poetry_basic/poetry.lock index 7fe5d0794..166f8fea8 100644 --- a/spec/fixtures/poetry_basic/poetry.lock +++ b/spec/fixtures/poetry_basic/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "colorama" @@ -6,6 +6,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -17,6 +19,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -28,6 +31,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -39,6 +43,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -54,6 +59,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -74,12 +80,13 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [metadata] -lock-version = "2.0" -python-versions = "^3.13" -content-hash = "56ec6342f4a39b402d2b65bd08cfb3af4136a02bb58674df49eac8d317376a7f" +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "8e82be2391208aa1b8599893f6abec30c0aa5702a846842fd442f50de45481e2" diff --git a/spec/fixtures/poetry_basic/pyproject.toml b/spec/fixtures/poetry_basic/pyproject.toml index 9fc0e8356..85bd48b9d 100644 --- a/spec/fixtures/poetry_basic/pyproject.toml +++ b/spec/fixtures/poetry_basic/pyproject.toml @@ -1,10 +1,14 @@ +[project] +name = "poetry-basic" +version = "0.0.0" +requires-python = ">=3.13" +dependencies = [ + "typing-extensions" +] + [tool.poetry] package-mode = false -[tool.poetry.dependencies] -python = "^3.13" -typing-extensions = "*" - # This group shouldn't be installed due to us passing `--only main`. -[tool.poetry.group.test.dependencies] +[tool.poetry.group.dev.dependencies] pytest = "*" diff --git a/spec/fixtures/poetry_editable/bin/test-entrypoints.sh b/spec/fixtures/poetry_editable/bin/test-entrypoints.sh index fc941ed3f..0e66f44bb 100755 --- a/spec/fixtures/poetry_editable/bin/test-entrypoints.sh +++ b/spec/fixtures/poetry_editable/bin/test-entrypoints.sh @@ -4,7 +4,7 @@ set -euo pipefail cd .heroku/python/lib/python*/site-packages/ -# List any path like strings in .pth, and finder files in site-packages. +# List any path like strings in the .pth and finder files in site-packages. grep --extended-regexp --only-matching -- '/\S+' *.pth __editable___*_finder.py | sort echo diff --git a/spec/fixtures/poetry_editable/poetry.lock b/spec/fixtures/poetry_editable/poetry.lock index a26fb9272..30df026b9 100644 --- a/spec/fixtures/poetry_editable/poetry.lock +++ b/spec/fixtures/poetry_editable/poetry.lock @@ -1,35 +1,38 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "gunicorn" -version = "20.1.0" +version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +groups = ["main"] files = [] develop = true [package.dependencies] -setuptools = ">=3.0" +packaging = "*" [package.extras] -eventlet = ["eventlet (>=0.24.1)"] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] [package.source] type = "git" url = "https://github.com/benoitc/gunicorn.git" -reference = "20.1.0" -resolved_reference = "61ccfd6c38d477a908e0f376757bbb884438053a" +reference = "HEAD" +resolved_reference = "bacbf8aa5152b94e44aa5d2a94aeaf0318a85248" [[package]] -name = "local_package_pyproject_toml" +name = "local-package-pyproject-toml" version = "0.0.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] develop = true @@ -43,6 +46,7 @@ version = "0.0.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] develop = true @@ -51,26 +55,18 @@ type = "directory" url = "packages/local_package_setup_py" [[package]] -name = "setuptools" -version = "75.6.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, - {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] - [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.13" -content-hash = "622d4971a16f64f64de12d8208024658e6d7b9e30e1315e6970110ec5e70975c" +content-hash = "f3fb8b00f781abfc5c963f9a266d16757b9b92cc415866eb28f156b4083fbf48" diff --git a/spec/fixtures/poetry_editable/pyproject.toml b/spec/fixtures/poetry_editable/pyproject.toml index b7659537b..256e2d984 100644 --- a/spec/fixtures/poetry_editable/pyproject.toml +++ b/spec/fixtures/poetry_editable/pyproject.toml @@ -6,7 +6,7 @@ authors = [] [tool.poetry.dependencies] python = "^3.13" -gunicorn = { git = "https://github.com/benoitc/gunicorn.git", tag = "20.1.0", develop = true } +gunicorn = { git = "https://github.com/benoitc/gunicorn.git", develop = true } local-package-pyproject-toml = { path = "packages/local_package_pyproject_toml", develop = true } local-package-setup-py = { path = "packages/local_package_setup_py", develop = true } diff --git a/spec/hatchet/ci_spec.rb b/spec/hatchet/ci_spec.rb index 30126af91..50d722c51 100644 --- a/spec/hatchet/ci_spec.rb +++ b/spec/hatchet/ci_spec.rb @@ -10,7 +10,7 @@ it 'installs both normal and test dependencies and uses cache on subsequent runs' do app.run_ci do |test_run| - expect(test_run.output).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) + expect(clean_output(test_run.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) -----> Python app detected -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} @@ -30,7 +30,6 @@ LD_LIBRARY_PATH=/app/.heroku/python/lib LIBRARY_PATH=/app/.heroku/python/lib PATH=/app/.heroku/python/bin::/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/ - PIP_NO_PYTHON_VERSION_WARNING=1 PKG_CONFIG_PATH=/app/.heroku/python/lib/pkg-config PYTHONUNBUFFERED=1 -----> Inline app detected @@ -62,16 +61,19 @@ REGEX test_run.run_again - expect(test_run.output).to include(<<~OUTPUT) + expect(clean_output(test_run.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) -----> Python app detected -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Restoring cache -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} -----> Installing pip #{PIP_VERSION} -----> Installing dependencies using 'pip install -r requirements.txt -r requirements-test.txt' + Requirement already satisfied: typing-extensions==.+ + Requirement already satisfied: pytest==.+ + .+ -----> Skipping Django collectstatic since the env var DISABLE_COLLECTSTATIC is set. -----> Running bin/post_compile hook - OUTPUT + REGEX end end end @@ -81,7 +83,7 @@ it 'installs both normal and test dependencies and uses cache on subsequent runs' do app.run_ci do |test_run| - expect(test_run.output).to match(Regexp.new(<<~REGEX)) + expect(clean_output(test_run.output)).to match(Regexp.new(<<~REGEX)) -----> Python app detected -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} @@ -102,7 +104,6 @@ LD_LIBRARY_PATH=/app/.heroku/python/lib LIBRARY_PATH=/app/.heroku/python/lib PATH=/app/.heroku/python/bin::/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/ - PIP_NO_PYTHON_VERSION_WARNING=1 PKG_CONFIG_PATH=/app/.heroku/python/lib/pkg-config PYTHONUNBUFFERED=1 -----> Inline app detected @@ -134,7 +135,7 @@ REGEX test_run.run_again - expect(test_run.output).to match(Regexp.new(<<~REGEX)) + expect(clean_output(test_run.output)).to match(Regexp.new(<<~REGEX)) -----> Python app detected -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Restoring cache @@ -156,7 +157,7 @@ it 'installs both normal and test dependencies and uses cache on subsequent runs' do app.run_ci do |test_run| - expect(test_run.output).to match(Regexp.new(<<~REGEX)) + expect(clean_output(test_run.output)).to match(Regexp.new(<<~REGEX)) -----> Python app detected -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} @@ -183,9 +184,9 @@ LD_LIBRARY_PATH=/app/.heroku/python/lib LIBRARY_PATH=/app/.heroku/python/lib PATH=/tmp/cache.+/.heroku/python-poetry/bin:/app/.heroku/python/bin::/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/ - PIP_NO_PYTHON_VERSION_WARNING=1 PKG_CONFIG_PATH=/app/.heroku/python/lib/pkg-config POETRY_VIRTUALENVS_CREATE=false + POETRY_VIRTUALENVS_USE_POETRY_PYTHON=true PYTHONUNBUFFERED=1 -----> Inline app detected LANG=en_US.UTF-8 @@ -193,6 +194,7 @@ LIBRARY_PATH=/app/.heroku/python/lib PATH=/app/.heroku/python/bin:/tmp/cache.+/.heroku/python-poetry/bin:/usr/local/bin:/usr/bin:/bin:/app/.sprettur/bin/ POETRY_VIRTUALENVS_CREATE=false + POETRY_VIRTUALENVS_USE_POETRY_PYTHON=true PYTHONHASHSEED=random PYTHONHOME=/app/.heroku/python PYTHONPATH=/app @@ -217,7 +219,7 @@ REGEX test_run.run_again - expect(test_run.output).to include(<<~OUTPUT) + expect(clean_output(test_run.output)).to include(<<~OUTPUT) -----> Python app detected -----> Using Python #{DEFAULT_PYTHON_MAJOR_VERSION} specified in .python-version -----> Restoring cache diff --git a/spec/hatchet/hooks_spec.rb b/spec/hatchet/hooks_spec.rb index 1f6af134e..eb20a7e83 100644 --- a/spec/hatchet/hooks_spec.rb +++ b/spec/hatchet/hooks_spec.rb @@ -24,7 +24,6 @@ remote: LD_LIBRARY_PATH=/app/.heroku/python/lib remote: LIBRARY_PATH=/app/.heroku/python/lib remote: PATH=/app/.heroku/python/bin::/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - remote: PIP_NO_PYTHON_VERSION_WARNING=1 remote: PKG_CONFIG_PATH=/app/.heroku/python/lib/pkg-config remote: PWD=/tmp/build_ remote: PYTHONUNBUFFERED=1 @@ -48,7 +47,6 @@ remote: LD_LIBRARY_PATH=/app/.heroku/python/lib remote: LIBRARY_PATH=/app/.heroku/python/lib remote: PATH=/app/.heroku/python/bin::/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - remote: PIP_NO_PYTHON_VERSION_WARNING=1 remote: PKG_CONFIG_PATH=/app/.heroku/python/lib/pkg-config remote: PWD=/tmp/build_ remote: PYTHONUNBUFFERED=1 diff --git a/spec/hatchet/pip_spec.rb b/spec/hatchet/pip_spec.rb index d4990ae33..5cc9f8b8e 100644 --- a/spec/hatchet/pip_spec.rb +++ b/spec/hatchet/pip_spec.rb @@ -53,6 +53,7 @@ remote: -----> Using cached install of Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION} remote: -----> Installing dependencies using 'pip install -r requirements.txt' + remote: Requirement already satisfied: typing-extensions==4.12.2 (from -r requirements.txt (line 5)) (4.12.2) remote: -----> Inline app detected OUTPUT end @@ -91,7 +92,8 @@ end # This test intentionally uses Python 3.12, so that we test rewriting using older globally installed - # setuptools. The Poetry equivalent of this test covers the PEP-517/518 setuptools case. + # setuptools (which causes .egg-link files to be created too). The Pipenv and Poetry equivalents of + # this test covers the PEP-517/518 setuptools case. context 'when requirements.txt contains editable requirements (both VCS and local package)' do let(:buildpacks) { [:default, 'heroku-community/inline'] } let(:app) { Hatchet::Runner.new('spec/fixtures/pip_editable', buildpacks:) } @@ -100,21 +102,21 @@ app.deploy do |app| expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Running bin/post_compile hook - remote: easy-install.pth:/app/.heroku/src/gunicorn - remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py - remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - remote: gunicorn.egg-link:/app/.heroku/src/gunicorn - remote: local-package-setup-py.egg-link:/tmp/build_.*/packages/local_package_setup_py + remote: easy-install.pth:/app/.heroku/python/src/gunicorn + remote: easy-install.pth:/tmp/build_.+/packages/local_package_setup_py + remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} + remote: gunicorn.egg-link:/app/.heroku/python/src/gunicorn + remote: local-package-setup-py.egg-link:/tmp/build_.+/packages/local_package_setup_py remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) remote: -----> Inline app detected - remote: easy-install.pth:/app/.heroku/src/gunicorn - remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py - remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - remote: gunicorn.egg-link:/app/.heroku/src/gunicorn - remote: local-package-setup-py.egg-link:/tmp/build_.*/packages/local_package_setup_py + remote: easy-install.pth:/app/.heroku/python/src/gunicorn + remote: easy-install.pth:/tmp/build_.+/packages/local_package_setup_py + remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} + remote: gunicorn.egg-link:/app/.heroku/python/src/gunicorn + remote: local-package-setup-py.egg-link:/tmp/build_.+/packages/local_package_setup_py remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! @@ -123,10 +125,10 @@ # Test rewritten paths work at runtime. expect(app.run('bin/test-entrypoints.sh')).to include(<<~OUTPUT) - easy-install.pth:/app/.heroku/src/gunicorn + easy-install.pth:/app/.heroku/python/src/gunicorn easy-install.pth:/app/packages/local_package_setup_py __editable___local_package_pyproject_toml_0_0_1_finder.py:/app/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - gunicorn.egg-link:/app/.heroku/src/gunicorn + gunicorn.egg-link:/app/.heroku/python/src/gunicorn local-package-setup-py.egg-link:/app/packages/local_package_setup_py Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! @@ -139,26 +141,28 @@ app.push! expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Running bin/post_compile hook - remote: easy-install.pth:/app/.heroku/src/gunicorn - remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py - remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - remote: gunicorn.egg-link:/app/.heroku/src/gunicorn - remote: local-package-setup-py.egg-link:/tmp/build_.*/packages/local_package_setup_py + remote: easy-install.pth:/app/.heroku/python/src/gunicorn + remote: easy-install.pth:/tmp/build_.+/packages/local_package_setup_py + remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} + remote: gunicorn.egg-link:/app/.heroku/python/src/gunicorn + remote: local-package-setup-py.egg-link:/tmp/build_.+/packages/local_package_setup_py remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) remote: -----> Inline app detected - remote: easy-install.pth:/app/.heroku/src/gunicorn - remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py - remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - remote: gunicorn.egg-link:/app/.heroku/src/gunicorn - remote: local-package-setup-py.egg-link:/tmp/build_.*/packages/local_package_setup_py + remote: easy-install.pth:/app/.heroku/python/src/gunicorn + remote: easy-install.pth:/tmp/build_.+/packages/local_package_setup_py + remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} + remote: gunicorn.egg-link:/app/.heroku/python/src/gunicorn + remote: local-package-setup-py.egg-link:/tmp/build_.+/packages/local_package_setup_py remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) REGEX + # Test that the VCS repo checkout was cached correctly. + expect(app.output).to include('Updating /app/.heroku/python/src/gunicorn clone (to revision 20.1.0)') end end end diff --git a/spec/hatchet/pipenv_spec.rb b/spec/hatchet/pipenv_spec.rb index 6949e3934..02f9007fa 100644 --- a/spec/hatchet/pipenv_spec.rb +++ b/spec/hatchet/pipenv_spec.rb @@ -47,7 +47,7 @@ remote: typing_extensions 4.12.2 remote: virtualenv .+ remote: - remote: \\ + remote: REGEX app.commit! app.push! @@ -78,7 +78,6 @@ expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected remote: -----> Using Python 3.9.0 specified in Pipfile.lock - remote: -----> Installing Python 3.9.0 remote: remote: ! Warning: Support for Python 3.9 is ending soon! remote: ! @@ -96,11 +95,23 @@ remote: ! https://devcenter.heroku.com/articles/python-support#supported-python-versions remote: remote: - remote: ! Warning: A Python security update is available! + remote: ! Warning: A Python patch update is available! + remote: ! + remote: ! Your app is using Python 3.9.0, however, there is a newer + remote: ! patch release of Python 3.9 available: #{LATEST_PYTHON_3_9} + remote: ! + remote: ! It is important to always use the latest patch version of + remote: ! Python to keep your app secure. remote: ! - remote: ! Upgrade as soon as possible to: Python #{LATEST_PYTHON_3_9} - remote: ! See: https://devcenter.heroku.com/articles/python-runtimes + remote: ! Update your Pipfile.lock file to use the new version. + remote: ! + remote: ! We strongly recommend that you do not pin your app to an + remote: ! exact Python version such as 3.9.0, and instead only specify + remote: ! the major Python version of 3.9 in your Pipfile.lock file. + remote: ! This will allow your app to receive the latest available Python + remote: ! patch version automatically and prevent this warning. remote: + remote: -----> Installing Python 3.9.0 remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing Pipenv #{PIPENV_VERSION} remote: -----> Installing SQLite3 @@ -311,7 +322,8 @@ end context 'when the Pipenv and Python versions have changed since the last build' do - let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#v253'] } + # TODO: Bump this buildpack version the next time we update the Pipenv version. + let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#archive/v253'] } let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_basic', buildpacks:) } it 'clears the cache before installing' do @@ -325,6 +337,7 @@ remote: -----> Discarding cache since: remote: - The Python version has changed from 3.12.4 to #{DEFAULT_PYTHON_FULL_VERSION} remote: - The Pipenv version has changed from 2023.12.1 to #{PIPENV_VERSION} + remote: - The editable VCS repository location has changed \\(and Pipenv doesn't handle this correctly\\) remote: -----> Installing Python #{DEFAULT_PYTHON_FULL_VERSION} remote: -----> Installing pip #{PIP_VERSION} remote: -----> Installing Pipenv #{PIPENV_VERSION} @@ -340,68 +353,67 @@ let(:buildpacks) { [:default, 'heroku-community/inline'] } let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_editable', buildpacks:) } - it 'rewrites .pth, .egg-link and finder paths correctly for hooks, later buildpacks, runtime and cached builds' do + it 'rewrites .pth and finder paths correctly for hooks, later buildpacks, runtime and cached builds' do app.deploy do |app| expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + remote: -----> Installing dependencies using 'pipenv install --deploy' + remote: Installing dependencies from Pipfile.lock \\(.+\\)... remote: -----> Running bin/post_compile hook - remote: easy-install.pth:/app/.heroku/src/gunicorn - remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py - remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - remote: gunicorn.egg-link:/app/.heroku/src/gunicorn - remote: local-package-setup-py.egg-link:/tmp/build_.*/packages/local_package_setup_py + remote: __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} + remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} + remote: __editable___local_package_setup_py_0_0_1_finder.py:/tmp/build_.+/packages/local_package_setup_py/local_package_setup_py'} + remote: _pipenv_editable.pth:/tmp/build_.+ remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! - remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) + remote: Running entrypoint for the VCS package: gunicorn \\(version 23.0.0\\) remote: -----> Inline app detected - remote: easy-install.pth:/app/.heroku/src/gunicorn - remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py - remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - remote: gunicorn.egg-link:/app/.heroku/src/gunicorn - remote: local-package-setup-py.egg-link:/tmp/build_.*/packages/local_package_setup_py + remote: __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} + remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} + remote: __editable___local_package_setup_py_0_0_1_finder.py:/tmp/build_.+/packages/local_package_setup_py/local_package_setup_py'} + remote: _pipenv_editable.pth:/tmp/build_.+ remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! - remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) + remote: Running entrypoint for the VCS package: gunicorn \\(version 23.0.0\\) REGEX # Test rewritten paths work at runtime. expect(app.run('bin/test-entrypoints.sh')).to include(<<~OUTPUT) - easy-install.pth:/app/.heroku/src/gunicorn - easy-install.pth:/app/packages/local_package_setup_py + __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} __editable___local_package_pyproject_toml_0_0_1_finder.py:/app/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - gunicorn.egg-link:/app/.heroku/src/gunicorn - local-package-setup-py.egg-link:/app/packages/local_package_setup_py + __editable___local_package_setup_py_0_0_1_finder.py:/app/packages/local_package_setup_py/local_package_setup_py'} + _pipenv_editable.pth:/app Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! Running entrypoint for the setup.py-based local package: Hello setup.py! - Running entrypoint for the VCS package: gunicorn (version 20.1.0) + Running entrypoint for the VCS package: gunicorn (version 23.0.0) OUTPUT # Test that the cached .pth files work correctly. app.commit! app.push! expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + remote: -----> Installing dependencies using 'pipenv install --deploy' + remote: Installing dependencies from Pipfile.lock \\(.+\\)... remote: -----> Running bin/post_compile hook - remote: easy-install.pth:/app/.heroku/src/gunicorn - remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py - remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - remote: gunicorn.egg-link:/app/.heroku/src/gunicorn - remote: local-package-setup-py.egg-link:/tmp/build_.*/packages/local_package_setup_py + remote: __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} + remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} + remote: __editable___local_package_setup_py_0_0_1_finder.py:/tmp/build_.+/packages/local_package_setup_py/local_package_setup_py'} + remote: _pipenv_editable.pth:/tmp/build_.+ remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! - remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) + remote: Running entrypoint for the VCS package: gunicorn \\(version 23.0.0\\) remote: -----> Inline app detected - remote: easy-install.pth:/app/.heroku/src/gunicorn - remote: easy-install.pth:/tmp/build_.*/packages/local_package_setup_py - remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'} - remote: gunicorn.egg-link:/app/.heroku/src/gunicorn - remote: local-package-setup-py.egg-link:/tmp/build_.*/packages/local_package_setup_py + remote: __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} + remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} + remote: __editable___local_package_setup_py_0_0_1_finder.py:/tmp/build_.+/packages/local_package_setup_py/local_package_setup_py'} + remote: _pipenv_editable.pth:/tmp/build_.+ remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! - remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) + remote: Running entrypoint for the VCS package: gunicorn \\(version 23.0.0\\) REGEX end end diff --git a/spec/hatchet/poetry_spec.rb b/spec/hatchet/poetry_spec.rb index e7e85d446..9d8d6c4ac 100644 --- a/spec/hatchet/poetry_spec.rb +++ b/spec/hatchet/poetry_spec.rb @@ -26,6 +26,7 @@ remote: LIBRARY_PATH=/app/.heroku/python/lib remote: PATH=/app/.heroku/python/bin:/tmp/codon/tmp/cache/.heroku/python-poetry/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin remote: POETRY_VIRTUALENVS_CREATE=false + remote: POETRY_VIRTUALENVS_USE_POETRY_PYTHON=true remote: PYTHONHASHSEED=random remote: PYTHONHOME=/app/.heroku/python remote: PYTHONPATH=/app @@ -62,7 +63,7 @@ end context 'when the Poetry and Python versions have changed since the last build' do - let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#v268'] } + let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#v275'] } let(:app) { Hatchet::Runner.new('spec/fixtures/poetry_basic', buildpacks:) } it 'clears the cache before installing' do @@ -74,8 +75,8 @@ remote: -----> Python app detected remote: -----> Using Python 3.13 specified in .python-version remote: -----> Discarding cache since: - remote: - The Python version has changed from 3.13.0 to #{LATEST_PYTHON_3_13} - remote: - The Poetry version has changed from 1.8.4 to #{POETRY_VERSION} + remote: - The Python version has changed from 3.13.1 to #{LATEST_PYTHON_3_13} + remote: - The Poetry version has changed from 2.0.1 to #{POETRY_VERSION} remote: -----> Installing Python #{LATEST_PYTHON_3_13} remote: -----> Installing Poetry #{POETRY_VERSION} remote: -----> Installing dependencies using 'poetry sync --only main' @@ -94,63 +95,84 @@ let(:buildpacks) { [:default, 'heroku-community/inline'] } let(:app) { Hatchet::Runner.new('spec/fixtures/poetry_editable', buildpacks:) } - it 'rewrites .pth, .egg-link and finder paths correctly for hooks, later buildpacks, runtime and cached builds' do + it 'rewrites .pth and finder paths correctly for hooks, later buildpacks, runtime and cached builds' do app.deploy do |app| expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + remote: -----> Installing dependencies using 'poetry sync --only main' + remote: Installing dependencies from lock file + remote: + remote: Package operations: 4 installs, 0 updates, 0 removals + remote: + remote: - Installing packaging \\(24.2\\) + remote: - Installing gunicorn \\(23.0.0 bacbf8a\\) + remote: - Installing local-package-pyproject-toml \\(0.0.1 /tmp/build_.+/packages/local_package_pyproject_toml\\) + remote: - Installing local-package-setup-py \\(0.0.1 /tmp/build_.+/packages/local_package_setup_py\\) + remote: + remote: Installing the current project: poetry-editable \\(0.0.1\\) remote: -----> Running bin/post_compile hook - remote: __editable___gunicorn_20_1_0_finder.py:/tmp/build_.+/.heroku/python/src/gunicorn/gunicorn'} + remote: __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} remote: __editable___local_package_setup_py_0_0_1_finder.py:/tmp/build_.+/packages/local_package_setup_py/local_package_setup_py'} remote: poetry_editable.pth:/tmp/build_.+ remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! - remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) + remote: Running entrypoint for the VCS package: gunicorn \\(version 23.0.0\\) remote: -----> Inline app detected - remote: __editable___gunicorn_20_1_0_finder.py:/tmp/build_.+/.heroku/python/src/gunicorn/gunicorn'} + remote: __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} remote: __editable___local_package_setup_py_0_0_1_finder.py:/tmp/build_.+/packages/local_package_setup_py/local_package_setup_py'} remote: poetry_editable.pth:/tmp/build_.+ remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! - remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) + remote: Running entrypoint for the VCS package: gunicorn \\(version 23.0.0\\) REGEX # Test rewritten paths work at runtime. expect(app.run('bin/test-entrypoints.sh')).to include(<<~OUTPUT) - __editable___gunicorn_20_1_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} + __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} __editable___local_package_pyproject_toml_0_0_1_finder.py:/app/packages/local_package_pyproject_toml/local_package_pyproject_toml'} __editable___local_package_setup_py_0_0_1_finder.py:/app/packages/local_package_setup_py/local_package_setup_py'} poetry_editable.pth:/app Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! Running entrypoint for the setup.py-based local package: Hello setup.py! - Running entrypoint for the VCS package: gunicorn (version 20.1.0) + Running entrypoint for the VCS package: gunicorn (version 23.0.0) OUTPUT # Test that the cached .pth files work correctly. app.commit! app.push! expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) + remote: -----> Installing dependencies using 'poetry sync --only main' + remote: Installing dependencies from lock file + remote: + remote: Package operations: 0 installs, 3 updates, 0 removals + remote: + remote: - Updating gunicorn \\(23.0.0 /app/.heroku/python/src/gunicorn -> 23.0.0 bacbf8a\\) + remote: - Updating local-package-pyproject-toml \\(0.0.1 /tmp/build_.+/packages/local_package_pyproject_toml -> 0.0.1 /tmp/build_.+/packages/local_package_pyproject_toml\\) + remote: - Updating local-package-setup-py \\(0.0.1 /tmp/build_.+/packages/local_package_setup_py -> 0.0.1 /tmp/build_.+/packages/local_package_setup_py\\) + remote: + remote: Installing the current project: poetry-editable \\(0.0.1\\) remote: -----> Running bin/post_compile hook - remote: __editable___gunicorn_20_1_0_finder.py:/tmp/build_.+/.heroku/python/src/gunicorn/gunicorn'} + remote: __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} remote: __editable___local_package_setup_py_0_0_1_finder.py:/tmp/build_.+/packages/local_package_setup_py/local_package_setup_py'} remote: poetry_editable.pth:/tmp/build_.+ remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! - remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) + remote: Running entrypoint for the VCS package: gunicorn \\(version 23.0.0\\) remote: -----> Inline app detected - remote: __editable___gunicorn_20_1_0_finder.py:/tmp/build_.+/.heroku/python/src/gunicorn/gunicorn'} + remote: __editable___gunicorn_23_0_0_finder.py:/app/.heroku/python/src/gunicorn/gunicorn'} remote: __editable___local_package_pyproject_toml_0_0_1_finder.py:/tmp/build_.+/packages/local_package_pyproject_toml/local_package_pyproject_toml'} remote: __editable___local_package_setup_py_0_0_1_finder.py:/tmp/build_.+/packages/local_package_setup_py/local_package_setup_py'} remote: poetry_editable.pth:/tmp/build_.+ remote: remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! remote: Running entrypoint for the setup.py-based local package: Hello setup.py! - remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) + remote: Running entrypoint for the VCS package: gunicorn \\(version 23.0.0\\) REGEX end end @@ -160,7 +182,7 @@ # chosen Poetry version also supports our oldest supported Python version. The fixture # also includes a `brotli` directory to test the workaround for an `ensurepip` bug in # older Python versions: https://github.com/heroku/heroku-buildpack-python/issues/1697 - context 'when using the oldest supported Python version' do + context 'when using our oldest supported Python version' do let(:app) { Hatchet::Runner.new('spec/fixtures/poetry_oldest_python') } it 'installs successfully' do @@ -168,7 +190,6 @@ expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> Using Python 3.9.0 specified in .python-version - remote: -----> Installing Python 3.9.0 remote: remote: ! Warning: Support for Python 3.9 is ending soon! remote: ! @@ -186,11 +207,23 @@ remote: ! https://devcenter.heroku.com/articles/python-support#supported-python-versions remote: remote: - remote: ! Warning: A Python security update is available! + remote: ! Warning: A Python patch update is available! + remote: ! + remote: ! Your app is using Python 3.9.0, however, there is a newer + remote: ! patch release of Python 3.9 available: #{LATEST_PYTHON_3_9} remote: ! - remote: ! Upgrade as soon as possible to: Python #{LATEST_PYTHON_3_9} - remote: ! See: https://devcenter.heroku.com/articles/python-runtimes + remote: ! It is important to always use the latest patch version of + remote: ! Python to keep your app secure. + remote: ! + remote: ! Update your .python-version file to use the new version. + remote: ! + remote: ! We strongly recommend that you do not pin your app to an + remote: ! exact Python version such as 3.9.0, and instead only specify + remote: ! the major Python version of 3.9 in your .python-version file. + remote: ! This will allow your app to receive the latest available Python + remote: ! patch version automatically and prevent this warning. remote: + remote: -----> Installing Python 3.9.0 remote: -----> Installing Poetry #{POETRY_VERSION} remote: -----> Installing dependencies using 'poetry sync --only main' remote: Installing dependencies from lock file diff --git a/spec/hatchet/python_update_warning_spec.rb b/spec/hatchet/python_update_warning_spec.rb index 23365eaba..c1025f415 100644 --- a/spec/hatchet/python_update_warning_spec.rb +++ b/spec/hatchet/python_update_warning_spec.rb @@ -18,7 +18,8 @@ remote: ! Warning: The runtime.txt file is deprecated. remote: ! remote: ! The runtime.txt file is deprecated since it has been replaced - remote: ! by the more widely supported .python-version file. + remote: ! by the more widely supported .python-version file: + remote: ! https://devcenter.heroku.com/changelog-items/3141 remote: ! remote: ! Please delete your runtime.txt file and create a new file named: remote: ! .python-version @@ -36,7 +37,6 @@ remote: ! In the future support for runtime.txt will be removed and remote: ! this warning will be made an error. remote: - remote: -----> Installing Python 3.9.0 remote: remote: ! Warning: Support for Python 3.9 is ending soon! remote: ! @@ -54,11 +54,23 @@ remote: ! https://devcenter.heroku.com/articles/python-support#supported-python-versions remote: remote: - remote: ! Warning: A Python security update is available! + remote: ! Warning: A Python patch update is available! + remote: ! + remote: ! Your app is using Python 3.9.0, however, there is a newer + remote: ! patch release of Python 3.9 available: #{LATEST_PYTHON_3_9} + remote: ! + remote: ! It is important to always use the latest patch version of + remote: ! Python to keep your app secure. remote: ! - remote: ! Upgrade as soon as possible to: Python #{LATEST_PYTHON_3_9} - remote: ! See: https://devcenter.heroku.com/articles/python-runtimes + remote: ! Update your runtime.txt file to use the new version. + remote: ! + remote: ! We strongly recommend that you do not pin your app to an + remote: ! exact Python version such as 3.9.0, and instead only specify + remote: ! the major Python version of 3.9 in your runtime.txt file. + remote: ! This will allow your app to receive the latest available Python + remote: ! patch version automatically and prevent this warning. remote: + remote: -----> Installing Python 3.9.0 remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} OUTPUT end @@ -73,13 +85,24 @@ expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> Using Python 3.10.0 specified in .python-version - remote: -----> Installing Python 3.10.0 remote: - remote: ! Warning: A Python security update is available! + remote: ! Warning: A Python patch update is available! + remote: ! + remote: ! Your app is using Python 3.10.0, however, there is a newer + remote: ! patch release of Python 3.10 available: #{LATEST_PYTHON_3_10} + remote: ! + remote: ! It is important to always use the latest patch version of + remote: ! Python to keep your app secure. remote: ! - remote: ! Upgrade as soon as possible to: Python #{LATEST_PYTHON_3_10} - remote: ! See: https://devcenter.heroku.com/articles/python-runtimes + remote: ! Update your .python-version file to use the new version. + remote: ! + remote: ! We strongly recommend that you do not pin your app to an + remote: ! exact Python version such as 3.10.0, and instead only specify + remote: ! the major Python version of 3.10 in your .python-version file. + remote: ! This will allow your app to receive the latest available Python + remote: ! patch version automatically and prevent this warning. remote: + remote: -----> Installing Python 3.10.0 remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} OUTPUT end diff --git a/spec/hatchet/python_version_spec.rb b/spec/hatchet/python_version_spec.rb index e01771661..9e2452e59 100644 --- a/spec/hatchet/python_version_spec.rb +++ b/spec/hatchet/python_version_spec.rb @@ -56,7 +56,7 @@ # - If no Python version is specified, the same major version as the # last build is used (sticky versioning). # - Changes in the pip version are handled correctly. - let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#v257'] } + let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#v267'] } it 'builds with the same Python version as the last build' do app.deploy do |app| @@ -68,7 +68,7 @@ remote: -----> No Python version was specified. Using the same major version as the last build: Python 3.12 remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes remote: -----> Discarding cache since: - remote: - The Python version has changed from 3.12.6 to #{LATEST_PYTHON_3_12} + remote: - The Python version has changed from 3.12.7 to #{LATEST_PYTHON_3_12} remote: - The pip version has changed from 24.0 to #{PIP_VERSION} remote: -----> Installing Python #{LATEST_PYTHON_3_12} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} @@ -87,7 +87,6 @@ expect(clean_output(app.output)).to include(<<~OUTPUT) remote: -----> Python app detected remote: -----> Using Python 3.9 specified in .python-version - remote: -----> Installing Python #{LATEST_PYTHON_3_9} remote: remote: ! Warning: Support for Python 3.9 is ending soon! remote: ! @@ -104,6 +103,7 @@ remote: ! For more information, see: remote: ! https://devcenter.heroku.com/articles/python-support#supported-python-versions remote: + remote: -----> Installing Python #{LATEST_PYTHON_3_9} remote: -----> Installing pip #{PIP_VERSION}, setuptools #{SETUPTOOLS_VERSION} and wheel #{WHEEL_VERSION} remote: -----> Installing SQLite3 remote: -----> Installing dependencies using 'pip install -r requirements.txt' @@ -293,17 +293,38 @@ it 'aborts the build with a version not available message' do app.deploy do |app| - expect(clean_output(app.output)).to include(<<~OUTPUT) + expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) remote: -----> Python app detected remote: -----> Using Python 3.12.999 specified in .python-version + remote: -----> Installing Python 3.12.999 + remote: curl: \\(22\\) The requested URL returned error: 404.* + remote: zstd: /\\*stdin\\*\\\\: unexpected end of file + remote: tar: Child returned status 1 + remote: tar: Error is not recoverable: exiting now remote: - remote: ! Error: Python 3.12.999 isn't available for this stack (#{app.stack}). + remote: ! Error: The requested Python version isn't available. remote: ! - remote: ! For a list of the supported Python versions, see: - remote: ! https://devcenter.heroku.com/articles/python-support#supported-python-versions + remote: ! Your app's .python-version file specifies a Python version + remote: ! of 3.12.999, however, we couldn't find that version on S3. + remote: ! + remote: ! Check that this Python version has been released upstream, + remote: ! and that the Python buildpack has added support for it: + remote: ! https://www.python.org/downloads/ + remote: ! https://github.com/heroku/heroku-buildpack-python/blob/main/CHANGELOG.md + remote: ! + remote: ! If it has, make sure that you are using the latest version + remote: ! of this buildpack, and haven't pinned to an older release: + remote: ! https://devcenter.heroku.com/articles/managing-buildpacks#view-your-buildpacks + remote: ! https://devcenter.heroku.com/articles/managing-buildpacks#classic-buildpacks-references + remote: ! + remote: ! We also strongly recommend that you do not pin your app to an + remote: ! exact Python version such as 3.12.999, and instead only specify + remote: ! the major Python version of 3.12 in your .python-version file. + remote: ! This will allow your app to receive the latest available Python + remote: ! patch version automatically, and prevent this type of error. remote: remote: ! Push rejected, failed to compile Python app. - OUTPUT + REGEX end end end @@ -389,7 +410,8 @@ remote: ! Warning: The runtime.txt file is deprecated. remote: ! remote: ! The runtime.txt file is deprecated since it has been replaced - remote: ! by the more widely supported .python-version file. + remote: ! by the more widely supported .python-version file: + remote: ! https://devcenter.heroku.com/changelog-items/3141 remote: ! remote: ! Please delete your runtime.txt file and create a new file named: remote: ! .python-version diff --git a/spec/hatchet/stack_spec.rb b/spec/hatchet/stack_spec.rb index 46817cad6..974e6d607 100644 --- a/spec/hatchet/stack_spec.rb +++ b/spec/hatchet/stack_spec.rb @@ -4,12 +4,12 @@ RSpec.describe 'Stack changes' do context 'when the stack is upgraded from Heroku-22 to Heroku-24', stacks: %w[heroku-22] do - # This test performs an initial build using an older buildpack version, followed - # by a build using the current version. This ensures that the current buildpack - # can successfully read the stack metadata written to the build cache in the past. - # The buildpack version chosen is one which had an older default Python version, so - # we can also prove that clearing the cache didn't lose the Python version metadata. - let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#v250'] } + # This test performs an initial build using an older buildpack version, followed by a build + # using the current version. This ensures that the current buildpack can successfully read + # the stack metadata written to the build cache in the past. The buildpack version chosen is + # the oldest to support Heroku-24, and which had an older default Python version so we can + # also prove that clearing the cache didn't lose the sticky Python version metadata. + let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#archive/v250'] } let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified', buildpacks:) } it 'clears the cache before installing again whilst preserving the sticky Python version' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b8e49eb22..2f561389c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,11 +9,11 @@ FIXTURE_DIR = Pathname.new(__FILE__).parent.join('fixtures') -LATEST_PYTHON_3_9 = '3.9.21' -LATEST_PYTHON_3_10 = '3.10.16' -LATEST_PYTHON_3_11 = '3.11.11' -LATEST_PYTHON_3_12 = '3.12.9' -LATEST_PYTHON_3_13 = '3.13.2' +LATEST_PYTHON_3_9 = '3.9.22' +LATEST_PYTHON_3_10 = '3.10.17' +LATEST_PYTHON_3_11 = '3.11.12' +LATEST_PYTHON_3_12 = '3.12.10' +LATEST_PYTHON_3_13 = '3.13.3' DEFAULT_PYTHON_FULL_VERSION = LATEST_PYTHON_3_13 DEFAULT_PYTHON_MAJOR_VERSION = DEFAULT_PYTHON_FULL_VERSION.gsub(/\.\d+$/, '')