├── .dockerignore ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── check_changelog.yml ├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── README.md ├── bin ├── compile ├── default_pythons ├── detect ├── release ├── steps │ ├── README.MD │ ├── collectstatic │ ├── hooks │ │ ├── post_compile │ │ └── pre_compile │ ├── nltk │ ├── pip-install │ ├── pipenv │ ├── pipenv-python-version │ ├── python │ └── sqlite3 ├── test-compile ├── utils └── warnings ├── buildpack.toml ├── builds ├── README.md ├── dockerenv.default ├── heroku-18.Dockerfile ├── heroku-20.Dockerfile ├── heroku-22.Dockerfile └── runtimes │ ├── python │ ├── python-3.10.4 │ ├── python-3.10.5 │ ├── python-3.10.6 │ ├── python-3.10.7 │ ├── python-3.7.13 │ ├── python-3.7.14 │ ├── python-3.8.13 │ ├── python-3.8.14 │ ├── python-3.9.12 │ ├── python-3.9.13 │ └── python-3.9.14 ├── etc └── publish.sh ├── hatchet.json ├── hatchet.lock ├── requirements.txt ├── spec ├── fixtures │ ├── ci_nose │ │ ├── app.json │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── django_broken_project │ │ ├── manage.py │ │ └── requirements.txt │ ├── django_collectstatic_disabled_file │ │ ├── .heroku │ │ │ └── collectstatic_disabled │ │ └── requirements.txt │ ├── hooks │ │ ├── bin │ │ │ ├── post_compile │ │ │ └── pre_compile │ │ └── requirements.txt │ ├── nltk_dependency_and_nltk_txt │ │ ├── nltk.txt │ │ └── requirements.txt │ ├── nltk_dependency_only │ │ └── requirements.txt │ ├── nltk_txt_but_no_dependency │ │ ├── nltk.txt │ │ └── requirements.txt │ ├── no_python_project_files │ │ └── README.md │ ├── pipenv_and_requirements_txt │ │ ├── Pipfile │ │ ├── Pipfile.lock │ │ └── requirements.txt │ ├── pipenv_and_runtime_txt │ │ ├── Pipfile │ │ ├── Pipfile.lock │ │ └── runtime.txt │ ├── pipenv_lockfile_out_of_sync │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_no_lockfile │ │ └── Pipfile │ ├── pipenv_python_2.7 │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_3.10 │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_3.5 │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_3.6 │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_3.7 │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_3.8 │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_3.9 │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_full_version │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_full_version_invalid │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_version_invalid │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pipenv_python_version_unspecified │ │ ├── Pipfile │ │ └── Pipfile.lock │ ├── pypy_3.6 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_2.7 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.10 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.10_outdated │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.4 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.5 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.6 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.6_outdated │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.7 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.7_outdated │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.8 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.8_outdated │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.9 │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_3.9_outdated │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_version_invalid │ │ ├── requirements.txt │ │ └── runtime.txt │ ├── python_version_unspecified │ │ └── requirements.txt │ ├── requirements_compiled │ │ └── requirements.txt │ ├── requirements_django_latest │ │ └── requirements.txt │ ├── requirements_editable │ │ ├── bin │ │ │ ├── compile │ │ │ ├── detect │ │ │ ├── post_compile │ │ │ └── test-entrypoints │ │ ├── packages │ │ │ ├── local_package_pyproject_toml │ │ │ │ ├── local_package_pyproject_toml │ │ │ │ │ └── __init__.py │ │ │ │ └── pyproject.toml │ │ │ └── local_package_setup_py │ │ │ │ ├── local_package_setup_py │ │ │ │ └── __init__.py │ │ │ │ ├── setup.cfg │ │ │ │ └── setup.py │ │ └── requirements.txt │ ├── requirements_gdal │ │ └── requirements.txt │ ├── requirements_git │ │ └── requirements.txt │ ├── requirements_txt_and_setup_py │ │ ├── requirements.txt │ │ └── setup.py │ ├── runtime_txt_only │ │ └── runtime.txt │ ├── runtime_txt_with_stray_whitespace │ │ ├── requirements.txt │ │ └── runtime.txt │ └── setup_py_only │ │ └── setup.py ├── hatchet │ ├── ci_spec.rb │ ├── detect_spec.rb │ ├── django_spec.rb │ ├── getting_started_spec.rb │ ├── hooks_spec.rb │ ├── nltk_spec.rb │ ├── pip_spec.rb │ ├── pipenv_spec.rb │ ├── python_update_warning_spec.rb │ ├── python_version_spec.rb │ └── stack_spec.rb └── spec_helper.rb └── vendor ├── WEB_CONCURRENCY.sh ├── buildpack-stdlib_v8.sh ├── pipenv-to-pip ├── python.gunicorn.sh └── runtime-fixer /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @heroku/languages 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "bundler" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "monthly" 15 | -------------------------------------------------------------------------------- /.github/workflows/check_changelog.yml: -------------------------------------------------------------------------------- 1 | name: Check Changelog 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited, labeled, unlabeled, synchronize] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | check-changelog: 12 | runs-on: ubuntu-22.04 13 | if: | 14 | !contains(github.event.pull_request.body, '[skip changelog]') && 15 | !contains(github.event.pull_request.body, '[changelog skip]') && 16 | !contains(github.event.pull_request.body, '[skip ci]') && 17 | !contains(github.event.pull_request.labels.*.name, 'skip changelog') && 18 | !contains(github.event.pull_request.labels.*.name, 'dependencies') 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | - name: Check that CHANGELOG is touched 23 | run: | 24 | git fetch origin ${{ github.base_ref }} --depth 1 && \ 25 | git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | site 3 | .DS_Store 4 | 5 | /.envrc 6 | .hatchet/repos/ 7 | 8 | #Venv 9 | buildpack/* 10 | 11 | builds/dockerenv.staging* 12 | builds/dockerenv.production 13 | 14 | .rspec_status 15 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rspec 3 | 4 | AllCops: 5 | NewCops: enable 6 | 7 | Layout/TrailingWhitespace: 8 | # Required since we use heredocs to assert against Hatchet output, and sometimes that output 9 | # contains trailing newlines which we must match against. The alternative is to end the lines 10 | # with the unsightly `#{' '}` workaround. 11 | AllowInHeredoc: true 12 | 13 | Metrics/BlockLength: 14 | Enabled: false 15 | 16 | RSpec/DescribeClass: 17 | Enabled: false 18 | 19 | RSpec/ExampleLength: 20 | Enabled: false 21 | 22 | RSpec/Focus: 23 | # Disable auto-correct otherwise format-on-save will remove the annotation 24 | # whilst developing locally. 25 | AutoCorrect: false 26 | 27 | RSpec/MultipleExpectations: 28 | Enabled: false 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | ruby '~> 2.7.0' 6 | 7 | group :test, :development do 8 | gem 'heroku_hatchet' 9 | gem 'parallel_split_test' 10 | gem 'rspec-core' 11 | gem 'rspec-expectations' 12 | gem 'rubocop', require: false 13 | gem 'rubocop-rspec', require: false 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | diff-lcs (1.5.0) 6 | erubis (2.7.0) 7 | excon (0.93.0) 8 | heroics (0.1.2) 9 | erubis (~> 2.0) 10 | excon 11 | moneta 12 | multi_json (>= 1.9.2) 13 | webrick 14 | heroku_hatchet (7.3.4) 15 | excon (~> 0) 16 | platform-api (~> 3) 17 | rrrretry (~> 1) 18 | thor (~> 1) 19 | threaded (~> 0) 20 | json (2.6.2) 21 | moneta (1.0.0) 22 | multi_json (1.15.0) 23 | parallel (1.22.1) 24 | parallel_split_test (0.10.0) 25 | parallel (>= 0.5.13) 26 | rspec-core (>= 3.9.0) 27 | parser (3.1.2.1) 28 | ast (~> 2.4.1) 29 | platform-api (3.5.0) 30 | heroics (~> 0.1.1) 31 | moneta (~> 1.0.0) 32 | rate_throttle_client (~> 0.1.0) 33 | rainbow (3.1.1) 34 | rate_throttle_client (0.1.2) 35 | regexp_parser (2.6.0) 36 | rexml (3.2.5) 37 | rrrretry (1.0.0) 38 | rspec-core (3.11.0) 39 | rspec-support (~> 3.11.0) 40 | rspec-expectations (3.11.1) 41 | diff-lcs (>= 1.2.0, < 2.0) 42 | rspec-support (~> 3.11.0) 43 | rspec-support (3.11.1) 44 | rubocop (1.36.0) 45 | json (~> 2.3) 46 | parallel (~> 1.10) 47 | parser (>= 3.1.2.1) 48 | rainbow (>= 2.2.2, < 4.0) 49 | regexp_parser (>= 1.8, < 3.0) 50 | rexml (>= 3.2.5, < 4.0) 51 | rubocop-ast (>= 1.20.1, < 2.0) 52 | ruby-progressbar (~> 1.7) 53 | unicode-display_width (>= 1.4.0, < 3.0) 54 | rubocop-ast (1.21.0) 55 | parser (>= 3.1.1.0) 56 | rubocop-rspec (2.13.2) 57 | rubocop (~> 1.33) 58 | ruby-progressbar (1.11.0) 59 | thor (1.2.1) 60 | threaded (0.0.4) 61 | unicode-display_width (2.3.0) 62 | webrick (1.7.0) 63 | 64 | PLATFORMS 65 | ruby 66 | 67 | DEPENDENCIES 68 | heroku_hatchet 69 | parallel_split_test 70 | rspec-core 71 | rspec-expectations 72 | rubocop 73 | rubocop-rspec 74 | 75 | RUBY VERSION 76 | ruby 2.7.6p219 77 | 78 | BUNDLED WITH 79 | 2.3.22 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License: 2 | 3 | Copyright (C) 2022 Heroku, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # These targets are not files 2 | .PHONY: lint lint-scripts lint-ruby compile builder-image buildenv deploy-runtimes publish 3 | 4 | STACK ?= heroku-22 5 | STACKS ?= heroku-18 heroku-20 heroku-22 6 | PLATFORM := linux/amd64 7 | FIXTURE ?= spec/fixtures/python_version_unspecified 8 | ENV_FILE ?= builds/dockerenv.default 9 | BUILDER_IMAGE_PREFIX := heroku-python-build 10 | 11 | # Converts a stack name of `heroku-NN` to its build Docker image tag of `heroku/heroku:NN-build`. 12 | STACK_IMAGE_TAG := heroku/$(subst -,:,$(STACK))-build 13 | 14 | lint: lint-scripts lint-ruby 15 | 16 | lint-scripts: 17 | @shellcheck -x bin/compile bin/detect bin/release bin/test-compile bin/utils bin/warnings bin/default_pythons 18 | @shellcheck -x bin/steps/collectstatic bin/steps/nltk bin/steps/pip-install bin/steps/pipenv bin/steps/pipenv-python-version bin/steps/python 19 | @shellcheck -x bin/steps/hooks/* 20 | @shellcheck -x builds/runtimes/* 21 | 22 | lint-ruby: 23 | @bundle exec rubocop 24 | 25 | compile: 26 | @echo "Running compile using: STACK=$(STACK) FIXTURE=$(FIXTURE)" 27 | @echo 28 | @docker run --rm -it -v $(PWD):/src:ro -e "STACK=$(STACK)" -w /buildpack --platform="$(PLATFORM)" "$(STACK_IMAGE_TAG)" \ 29 | bash -c 'cp -r /src/{bin,vendor} /buildpack && cp -r /src/$(FIXTURE) /build && mkdir /cache /env && bin/compile /build /cache /env' 30 | @echo 31 | 32 | builder-image: 33 | @echo "Generating binary builder image for $(STACK)..." 34 | @echo 35 | @docker build --pull -f builds/$(STACK).Dockerfile --platform="$(PLATFORM)" -t "$(BUILDER_IMAGE_PREFIX)-$(STACK)" . 36 | @echo 37 | 38 | buildenv: builder-image 39 | @echo "Starting build environment for $(STACK)..." 40 | @echo 41 | @echo "Usage..." 42 | @echo 43 | @echo " $$ bob build runtimes/python-X.Y.Z" 44 | @echo 45 | @docker run --rm -it --env-file="$(ENV_FILE)" -v $(PWD)/builds:/app/builds --platform="$(PLATFORM)" "$(BUILDER_IMAGE_PREFIX)-$(STACK)" bash 46 | 47 | deploy-runtimes: 48 | ifndef RUNTIMES 49 | $(error No runtimes specified! Use: "make deploy-runtimes RUNTIMES='python-X.Y.Z ...' [STACKS='heroku-18 ...'] [ENV_FILE=...]") 50 | endif 51 | @echo "Using: RUNTIMES='$(RUNTIMES)' STACKS='$(STACKS)' ENV_FILE='$(ENV_FILE)'" 52 | @echo 53 | @set -eu; for stack in $(STACKS); do \ 54 | $(MAKE) builder-image STACK=$${stack}; \ 55 | for runtime in $(RUNTIMES); do \ 56 | echo "Generating/deploying $${runtime} for $${stack}..."; \ 57 | echo; \ 58 | docker run --rm -it --env-file="$(ENV_FILE)" --platform="$(PLATFORM)" "$(BUILDER_IMAGE_PREFIX)-$${stack}" bob deploy "runtimes/$${runtime}"; \ 59 | echo; \ 60 | done; \ 61 | done 62 | 63 | publish: 64 | @etc/publish.sh 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![python](https://cloud.githubusercontent.com/assets/51578/13712821/b68a42ce-e793-11e5-96b0-d8eb978137ba.png) 2 | 3 | # Heroku Buildpack: Python 4 | 5 | [![CI](https://github.com/heroku/heroku-buildpack-python/actions/workflows/ci.yml/badge.svg)](https://github.com/heroku/heroku-buildpack-python/actions/workflows/ci.yml) 6 | 7 | This is the official [Heroku buildpack](https://devcenter.heroku.com/articles/buildpacks) for Python apps. 8 | 9 | Recommended web frameworks include **Django** and **Flask**, among others. The recommended webserver is **Gunicorn**. There are no restrictions around what software can be used (as long as it's pip-installable). Web processes must bind to `$PORT`, and only the HTTP protocol is permitted for incoming connections. 10 | 11 | See it in Action 12 | ---------------- 13 | ``` 14 | $ ls 15 | my-application requirements.txt runtime.txt 16 | 17 | $ git push heroku main 18 | Counting objects: 4, done. 19 | Delta compression using up to 8 threads. 20 | Compressing objects: 100% (2/2), done. 21 | Writing objects: 100% (4/4), 276 bytes | 276.00 KiB/s, done. 22 | Total 4 (delta 0), reused 0 (delta 0) 23 | remote: Compressing source files... done. 24 | remote: Building source: 25 | remote: 26 | remote: -----> Python app detected 27 | remote: -----> Installing python 28 | remote: -----> Installing pip 29 | remote: -----> Installing SQLite3 30 | remote: -----> Installing requirements with pip 31 | remote: Collecting flask (from -r /tmp/build_c2c067ef79ff14c9bf1aed6796f9ed1f/requirements.txt (line 1)) 32 | remote: Downloading ... 33 | remote: Installing collected packages: Werkzeug, click, MarkupSafe, Jinja2, itsdangerous, flask 34 | remote: Successfully installed Jinja2-2.10 MarkupSafe-1.1.0 Werkzeug-0.14.1 click-7.0 flask-1.0.2 itsdangerous-1.1.0 35 | remote: 36 | remote: -----> Discovering process types 37 | remote: Procfile declares types -> (none) 38 | remote: 39 | ``` 40 | 41 | A `requirements.txt` must be present at the root of your application's repository to deploy. 42 | 43 | To specify your python version, you also need a `runtime.txt` file - unless you are using the default Python runtime version. 44 | 45 | Current default Python Runtime: Python 3.10.7 46 | 47 | Alternatively, you can provide a `setup.py` file, or a `Pipfile`. 48 | Using `pipenv` will generate `runtime.txt` at build time if one of the field `python_version` or `python_full_version` is specified in the `requires` section of your `Pipfile`. 49 | 50 | Specify a Buildpack Version 51 | --------------------------- 52 | 53 | You can specify the latest production release of this buildpack for upcoming builds of an existing application: 54 | 55 | $ heroku buildpacks:set heroku/python 56 | 57 | 58 | Specify a Python Runtime 59 | ------------------------ 60 | 61 | Supported runtime options include: 62 | 63 | - `python-3.10.7` on all [supported stacks](https://devcenter.heroku.com/articles/stack#stack-support-details) 64 | - `python-3.9.14` on all [supported stacks](https://devcenter.heroku.com/articles/stack#stack-support-details) 65 | - `python-3.8.14` on Heroku-18 and Heroku-20 only 66 | - `python-3.7.14` on Heroku-18 and Heroku-20 only 67 | -------------------------------------------------------------------------------- /bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # The Heroku Python Buildpack. This script accepts parameters for a build 4 | # directory, a cache directory, and a directory for app environment variables. 5 | 6 | # Warning: there are a few hacks in this script to accommodate excellent builds 7 | # on Heroku. No guarantee for external compatibility is made. However, 8 | # everything should work fine outside of the Heroku environment, if the 9 | # environment is setup correctly. 10 | 11 | # Usage: 12 | # 13 | # $ bin/compile 14 | 15 | # Fail fast and fail hard. 16 | set -eo pipefail 17 | 18 | # Used by buildpack-stdlib's metrics features. 19 | export BPLOG_PREFIX="buildpack.python" 20 | export BUILDPACK_LOG_FILE=${BUILDPACK_LOG_FILE:-/dev/null} 21 | 22 | [ "$BUILDPACK_XTRACE" ] && set -o xtrace 23 | 24 | # Prepend proper path for old-school virtualenv hackery. 25 | # This may not be neccessary. 26 | export PATH=:/usr/local/bin:$PATH 27 | 28 | # Setup Path variables, for later use in the Buildpack. 29 | BIN_DIR=$(cd "$(dirname "$0")"; pwd) # absolute path 30 | ROOT_DIR=$(dirname "$BIN_DIR") 31 | BUILD_DIR=$1 32 | CACHE_DIR=$2 33 | ENV_DIR=$3 34 | 35 | # Export Path variables, for use in sub-scripts. 36 | export BUILD_DIR CACHE_DIR ENV_DIR 37 | 38 | # Set the base URL for downloading buildpack assets like Python runtimes. 39 | # The user can provide BUILDPACK_S3_BASE_URL to specify a custom target. 40 | # Note: this is designed for non-Heroku use, as it does not use the user-provided 41 | # environment variable mechanism (the ENV_DIR). 42 | S3_BASE_URL="${BUILDPACK_S3_BASE_URL:-"https://heroku-buildpack-python.s3.us-east-1.amazonaws.com"}" 43 | # This has to be exported since it's used by the geo-libs step which is run in a subshell. 44 | 45 | # Default Python Versions 46 | # shellcheck source=bin/default_pythons 47 | source "$BIN_DIR/default_pythons" 48 | 49 | # Common Problem Warnings: 50 | # This section creates a temporary file in which to stick the output of `pip install`. 51 | # The `warnings` subscript then greps through this for common problems and guides 52 | # the user towards resolution of known issues. 53 | WARNINGS_LOG=$(mktemp) 54 | 55 | # The buildpack ships with a few executable tools. 56 | # This installs them into the path, so we can execute them directly. 57 | export PATH=$PATH:$ROOT_DIR/vendor/ 58 | 59 | # Sanitize externally-provided environment variables: 60 | # The following environment variables are either problematic or simply unneccessary 61 | # for the buildpack to have knowledge of, so we unset them, to keep the environment 62 | # as clean and pristine as possible. 63 | unset PYTHONHOME PYTHONPATH 64 | 65 | # Import the utils script, which contains helper functions used throughout the buildpack. 66 | # shellcheck source=bin/utils 67 | source "$BIN_DIR/utils" 68 | 69 | # Import the warnings script, which contains the `pip install` user warning mechanisms 70 | # (mentioned and explained above) 71 | # shellcheck source=bin/warnings 72 | source "$BIN_DIR/warnings" 73 | 74 | # Make the directory in which we will create symlinks from the temporary build directory 75 | # to `/app`. 76 | # Symlinks are required, since Python is not a portable installation. 77 | # More on this topic later. 78 | mkdir -p /app/.heroku 79 | 80 | # This buildpack programatically generates (or simply copies) a number of files for 81 | # buildpack machinery: an export script, and a number of `.profile.d` scripts. This 82 | # section declares the locations of those files and targets. 83 | PROFILE_PATH="$BUILD_DIR/.profile.d/python.sh" 84 | EXPORT_PATH="$BIN_DIR/../export" 85 | GUNICORN_PROFILE_PATH="$BUILD_DIR/.profile.d/python.gunicorn.sh" 86 | WEB_CONCURRENCY_PROFILE_PATH="$BUILD_DIR/.profile.d/WEB_CONCURRENCY.sh" 87 | 88 | # We'll need to send these statics to other scripts we `source`. 89 | export BUILD_DIR CACHE_DIR BIN_DIR PROFILE_PATH EXPORT_PATH 90 | 91 | # Python Environment Variables 92 | # Set Python-specific environment variables, for running Python within the buildpack. 93 | # Notes on each variable included. 94 | 95 | # PATH is relatively obvious, we need to be able to execute 'python'. 96 | export PATH=/app/.heroku/python/bin:/app/.heroku/vendor/bin:$PATH 97 | # Tell Python to not buffer it's stdin/stdout. 98 | export PYTHONUNBUFFERED=1 99 | # Set the locale to a well-known and expected standard. 100 | export LANG=en_US.UTF-8 101 | # `~/.heroku/vendor` is an place where the buildpack may stick pre-build binaries for known 102 | # C dependencies. This section configures Python (GCC, more specifically) 103 | # and pip to automatically include these paths when building binaries. 104 | # TODO: Stop adding .heroku/vendor here now that the buildpack no longer vendors anything. 105 | export C_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include:$C_INCLUDE_PATH 106 | export CPLUS_INCLUDE_PATH=/app/.heroku/vendor/include:/app/.heroku/python/include:$CPLUS_INCLUDE_PATH 107 | export LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib:$LIBRARY_PATH 108 | export LD_LIBRARY_PATH=/app/.heroku/vendor/lib:/app/.heroku/python/lib:$LD_LIBRARY_PATH 109 | export PKG_CONFIG_PATH=/app/.heroku/vendor/lib/pkg-config:/app/.heroku/python/lib/pkg-config:$PKG_CONFIG_PATH 110 | 111 | # Global pip options (https://pip.pypa.io/en/stable/user_guide/#environment-variables). 112 | # Disable pip's warnings about EOL Python since we show our own. 113 | export PIP_NO_PYTHON_VERSION_WARNING=1 114 | 115 | # The Application Code 116 | # -------------------- 117 | 118 | # Switch to the repo's context. 119 | cd "$BUILD_DIR" 120 | 121 | # The Cache 122 | # --------- 123 | 124 | # The workflow for the Python Buildpack's cache is as follows: 125 | # 126 | # - `~/.heroku/{known-paths}` are copied from the cache into the slug. 127 | # - The build is executed, modifying `~/.heroku/{known-paths}`. 128 | # - Once the build is complete, `~/.heroku/{known-paths}` is copied back into the cache. 129 | 130 | # Create the cache directory, if it doesn't exist. 131 | mkdir -p "$CACHE_DIR/.heroku" 132 | 133 | # Restore old artifacts from the cache. 134 | mkdir -p .heroku 135 | 136 | # The Python installation. 137 | cp -R "$CACHE_DIR/.heroku/python" .heroku/ &> /dev/null || true 138 | # A plain text file which contains the current stack being used (used for cache busting). 139 | cp -R "$CACHE_DIR/.heroku/python-stack" .heroku/ &> /dev/null || true 140 | # A plain text file which contains the current python version being used (used for cache busting). 141 | cp -R "$CACHE_DIR/.heroku/python-version" .heroku/ &> /dev/null || true 142 | # A plain text file which contains the current sqlite3 version being used (used for cache busting). 143 | cp -R "$CACHE_DIR/.heroku/python-sqlite3-version" .heroku/ &> /dev/null || true 144 | # "editable" installations of code repositories, via pip or pipenv. 145 | if [[ -d "$CACHE_DIR/.heroku/src" ]]; then 146 | cp -R "$CACHE_DIR/.heroku/src" .heroku/ &> /dev/null || true 147 | fi 148 | 149 | # The pre_compile hook. Customers rely on this. Don't remove it. 150 | # This part of the code is used to allow users to customize their build experience 151 | # without forking the buildpack by providing a `bin/pre_compile` script, which gets 152 | # run inline with the buildpack automatically. 153 | 154 | # shellcheck source=bin/steps/hooks/pre_compile 155 | source "$BIN_DIR/steps/hooks/pre_compile" 156 | 157 | # Sticky runtimes. If there was a previous build, and it used a given version of Python, 158 | # continue to use that version of Python in perpituity (warnings will be raised if 159 | # they are out–of–date). 160 | if [ -f "$CACHE_DIR/.heroku/python-version" ]; then 161 | CACHED_PYTHON_VERSION=$(cat "$CACHE_DIR/.heroku/python-version") 162 | fi 163 | 164 | # We didn't always record the stack version. This code is in place because of that. 165 | if [ -f "$CACHE_DIR/.heroku/python-stack" ]; then 166 | CACHED_PYTHON_STACK=$(cat "$CACHE_DIR/.heroku/python-stack") 167 | else 168 | CACHED_PYTHON_STACK=$STACK 169 | fi 170 | 171 | # Pipenv Python version support. 172 | # Detect the version of Python requested from a Pipfile (e.g. python_version or python_full_version). 173 | # Convert it to a runtime.txt file. 174 | 175 | # shellcheck source=bin/steps/pipenv-python-version 176 | source "$BIN_DIR/steps/pipenv-python-version" 177 | 178 | if [[ -f runtime.txt ]]; then 179 | # PYTHON_VERSION_SOURCE may have already been set by the pipenv-python-version step. 180 | # TODO: Refactor this and stop pipenv-python-version using runtime.txt as an API. 181 | PYTHON_VERSION_SOURCE=${PYTHON_VERSION_SOURCE:-"runtime.txt"} 182 | puts-step "Using Python version specified in ${PYTHON_VERSION_SOURCE}" 183 | mcount "version.reason.python.specified" 184 | elif [[ -n "${CACHED_PYTHON_VERSION:-}" ]]; then 185 | puts-step "No Python version was specified. Using the same version as the last build: ${CACHED_PYTHON_VERSION}" 186 | echo " To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes" 187 | mcount "version.reason.python.cached" 188 | echo "${CACHED_PYTHON_VERSION}" > runtime.txt 189 | else 190 | puts-step "No Python version was specified. Using the buildpack default: ${DEFAULT_PYTHON_VERSION}" 191 | echo " To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes" 192 | mcount "version.reason.python.default" 193 | echo "${DEFAULT_PYTHON_VERSION}" > runtime.txt 194 | fi 195 | 196 | # Create the directory for .profile.d, if it doesn't exist. 197 | mkdir -p "$(dirname "$PROFILE_PATH")" 198 | # Create the directory for editable source code installation, if it doesn't exist. 199 | mkdir -p /app/.heroku/src 200 | 201 | # On Heroku CI, builds happen in `/app`. Otherwise, on the Heroku platform, 202 | # they occur in a temp directory. Beacuse Python is not portable, we must create 203 | # symlinks to emulate that we are operating in `/app` during the build process. 204 | # This is (hopefully obviously) because apps end up running from `/app` in production. 205 | # Realpath is used to support use-cases where one of the locations is a symlink to the other. 206 | if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then 207 | # python expects to reside in /app, so set up symlinks 208 | # we will not remove these later so subsequent buildpacks can still invoke it 209 | ln -nsf "$BUILD_DIR/.heroku/python" /app/.heroku/python 210 | ln -nsf "$BUILD_DIR/.heroku/vendor" /app/.heroku/vendor 211 | # Note: .heroku/src is copied in later. 212 | fi 213 | 214 | # Download / Install Python, from pre-build binaries available on Amazon S3. 215 | # This step also bootstraps pip / setuptools. 216 | (( start=$(nowms) )) 217 | # shellcheck source=bin/steps/python 218 | source "$BIN_DIR/steps/python" 219 | mtime "python.install.time" "${start}" 220 | 221 | # Install Pipenv dependencies, if a Pipfile was provided. 222 | # shellcheck source=bin/steps/pipenv 223 | source "$BIN_DIR/steps/pipenv" 224 | 225 | # If no requirements.txt file given, assume `setup.py develop` is intended. 226 | # This allows for people to ship a setup.py application to Heroku 227 | 228 | if [ ! -f requirements.txt ] && [ ! -f Pipfile ]; then 229 | echo "-e ." > requirements.txt 230 | fi 231 | 232 | # SQLite3 support. 233 | # This sets up and installs sqlite3 dev headers and the sqlite3 binary but not the 234 | # libsqlite3-0 library since that exists on the stack image. 235 | (( start=$(nowms) )) 236 | # shellcheck source=bin/steps/sqlite3 237 | source "$BIN_DIR/steps/sqlite3" 238 | buildpack_sqlite3_install 239 | mtime "sqlite3.install.time" "${start}" 240 | 241 | # pip install 242 | # ----------- 243 | 244 | # Install dependencies with pip (where the magic happens). 245 | (( start=$(nowms) )) 246 | # shellcheck source=bin/steps/pip-install 247 | source "$BIN_DIR/steps/pip-install" 248 | mtime "pip.install.time" "${start}" 249 | 250 | # Support for NLTK corpora. 251 | (( start=$(nowms) )) 252 | sub_env "$BIN_DIR/steps/nltk" 253 | mtime "nltk.download.time" "${start}" 254 | 255 | # Support for editable installations. Here, we are copying pip–created src directory, 256 | # and copying it into the proper place (the logical place to do this was early, but it must be done here). 257 | # In CI, $BUILD_DIR is /app. 258 | # Realpath is used to support use-cases where one of the locations is a symlink to the other. 259 | if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then 260 | rm -rf "$BUILD_DIR/.heroku/src" 261 | deep-cp /app/.heroku/src "$BUILD_DIR/.heroku/src" 262 | fi 263 | 264 | 265 | # Django collectstatic support. 266 | # The buildpack automatically runs collectstatic for Django applications. 267 | # This is the cause for the majority of build failures on the Python platform. 268 | # These failures are intentional — if collectstatic (which can be tricky, at times) fails, 269 | # your build fails. 270 | (( start=$(nowms) )) 271 | sub_env "$BIN_DIR/steps/collectstatic" 272 | mtime "collectstatic.time" "${start}" 273 | 274 | 275 | # ***** Do diagnostic work here. 276 | echo "" 277 | echo "*****> Begin diagnostic work..." 278 | echo "*****> BUILD_DIR: $BUILD_DIR" 279 | echo "*****> BUILD_DIR contents:" 280 | ls -alh $BUILD_DIR 281 | 282 | echo "*****> Is manage.py present?" 283 | if [[ -f "${BUILD_DIR}/manage.py" ]]; then 284 | echo " Found manage.py." 285 | else 286 | echo " Could not find manage.py." 287 | fi 288 | 289 | echo "*****> Is django available?" 290 | source "$BIN_DIR/utils" 291 | if is_module_available "django"; then 292 | echo " Django is available." 293 | else 294 | echo " Django is not avaialable." 295 | fi 296 | 297 | echo "*****> Is psycopg2 available?" 298 | source "$BIN_DIR/utils" 299 | if is_module_available "psycopg2"; then 300 | echo " Django is available." 301 | else 302 | echo " Django is not avaialable." 303 | fi 304 | 305 | echo "*****> Are all three conditions met?" 306 | if [[ -f "${BUILD_DIR}/manage.py" ]] && is_module_available 'django' && is_module_available 'psycopg2'; then 307 | echo " All three conditions met." 308 | else 309 | echo " All three conditions not met." 310 | fi 311 | 312 | echo "*****> End diagnostic work." 313 | echo "" 314 | 315 | # Progamatically create .profile.d script for application runtime environment variables. 316 | 317 | # Set the PATH to include Python / pip / pipenv / etc. 318 | set_env PATH "\$HOME/.heroku/python/bin:\$PATH" 319 | # Tell Python to run in unbuffered mode. 320 | set_env PYTHONUNBUFFERED true 321 | # Tell Python where it lives. 322 | set_env PYTHONHOME "\$HOME/.heroku/python" 323 | # Set variables for C libraries. 324 | set_env LIBRARY_PATH "\$HOME/.heroku/vendor/lib:\$HOME/.heroku/python/lib:\$LIBRARY_PATH" 325 | set_env LD_LIBRARY_PATH "\$HOME/.heroku/vendor/lib:\$HOME/.heroku/python/lib:\$LD_LIBRARY_PATH" 326 | # Locale. 327 | set_default_env LANG en_US.UTF-8 328 | # The Python hash seed is set to random. 329 | set_default_env PYTHONHASHSEED random 330 | # Tell Python to look for Python modules in the /app dir. Don't change this. 331 | set_default_env PYTHONPATH "\$HOME" 332 | 333 | # Python expects to be in /app, if at runtime, it is not, set 334 | # up symlinks… this can occur when the subdir buildpack is used. 335 | cat <> "$PROFILE_PATH" 336 | if [[ \$HOME != "/app" ]]; then 337 | mkdir -p /app/.heroku 338 | ln -nsf "\$HOME/.heroku/python" /app/.heroku/python 339 | ln -nsf "\$HOME/.heroku/vendor" /app/.heroku/vendor 340 | fi 341 | EOT 342 | 343 | # At runtime, rewrite paths in editable package .egg-link, .pth and finder files from the build time paths 344 | # (such as `/tmp/build_`) back to `/app`. This is not done during the build itself, since later 345 | # buildpacks still need the build time paths. 346 | if [[ "${BUILD_DIR}" != "/app" ]]; then 347 | cat <> "$PROFILE_PATH" 348 | 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#' {} \+ 349 | EOT 350 | fi 351 | 352 | # Install sane-default script for $WEB_CONCURRENCY and $FORWARDED_ALLOW_IPS. 353 | cp "$ROOT_DIR/vendor/WEB_CONCURRENCY.sh" "$WEB_CONCURRENCY_PROFILE_PATH" 354 | cp "$ROOT_DIR/vendor/python.gunicorn.sh" "$GUNICORN_PROFILE_PATH" 355 | 356 | # Experimental post_compile hook. Don't remove this. 357 | # shellcheck source=bin/steps/hooks/post_compile 358 | source "$BIN_DIR/steps/hooks/post_compile" 359 | 360 | # Store new artifacts in the cache. 361 | rm -rf "$CACHE_DIR/.heroku/python" 362 | rm -rf "$CACHE_DIR/.heroku/python-version" 363 | rm -rf "$CACHE_DIR/.heroku/python-stack" 364 | rm -rf "$CACHE_DIR/.heroku/vendor" 365 | rm -rf "$CACHE_DIR/.heroku/src" 366 | 367 | mkdir -p "$CACHE_DIR/.heroku" 368 | cp -R .heroku/python "$CACHE_DIR/.heroku/" 369 | cp -R .heroku/python-version "$CACHE_DIR/.heroku/" 370 | cp -R .heroku/python-stack "$CACHE_DIR/.heroku/" &> /dev/null || true 371 | if [[ -d .heroku/src ]]; then 372 | cp -R .heroku/src "$CACHE_DIR/.heroku/" &> /dev/null || true 373 | fi 374 | 375 | # Measure the size of the Python installation. 376 | # shellcheck disable=SC2119 377 | mmeasure 'python.size' "$(measure-size)" 378 | 379 | 380 | -------------------------------------------------------------------------------- /bin/default_pythons: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Disable unused env var warning, since shellcheck doesn't take into account 4 | # that this file is sourced. We don't want to use export since it exposes 5 | # the env vars to subprocesses. 6 | # shellcheck disable=2034 7 | 8 | LATEST_310="python-3.10.7" 9 | LATEST_39="python-3.9.14" 10 | LATEST_38="python-3.8.14" 11 | LATEST_37="python-3.7.14" 12 | LATEST_36="python-3.6.15" 13 | # TODO: Remove these EOL versions once pipenv-python-version is refactored. 14 | LATEST_35="python-3.5.10" 15 | LATEST_34="python-3.4.10" 16 | LATEST_27="python-2.7.18" 17 | DEFAULT_PYTHON_VERSION="${LATEST_310}" 18 | -------------------------------------------------------------------------------- /bin/detect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script serves as the 4 | # [**Python Buildpack**](https://github.com/heroku/heroku-buildpack-python) 5 | # detector. 6 | # 7 | # A [buildpack](https://devcenter.heroku.com/articles/buildpacks) is an 8 | # adapter between a Python application and Heroku's runtime. 9 | 10 | # ## Usage 11 | # Compiling an app into a slug is simple: 12 | # 13 | # $ bin/detect 14 | 15 | BUILD_DIR=$1 16 | 17 | # Exit early if app is clearly not Python. 18 | if [ ! -f "$BUILD_DIR/requirements.txt" ] && [ ! -f "$BUILD_DIR/setup.py" ] && [ ! -f "$BUILD_DIR/Pipfile" ]; then 19 | exit 1 20 | fi 21 | 22 | echo Python 23 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # bin/release 3 | 4 | set -euo pipefail 5 | 6 | BUILD_DIR=$1 7 | BIN_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 8 | 9 | # shellcheck source=bin/utils 10 | source "$BIN_DIR/utils" 11 | 12 | if [[ -f "${BUILD_DIR}/manage.py" ]] && is_module_available 'django' && is_module_available 'psycopg2'; then 13 | cat <&1 | tee "$COLLECTSTATIC_LOG" | sed '/^Post-processed/d;/^Copying/d;/^$/d' | indent 55 | COLLECTSTATIC_STATUS="${PIPESTATUS[0]}" 56 | set -e 57 | 58 | echo 59 | 60 | if [[ "${COLLECTSTATIC_STATUS}" == 0 ]]; then 61 | exit 0 62 | fi 63 | 64 | # Display a warning if collectstatic failed. 65 | if grep -q 'SyntaxError' "$COLLECTSTATIC_LOG"; then 66 | mcount "failure.collectstatic.syntax-error" 67 | elif grep -q 'ImproperlyConfigured' "$COLLECTSTATIC_LOG"; then 68 | mcount "failure.collectstatic.improper-configuration" 69 | elif grep -q 'The CSS file' "$COLLECTSTATIC_LOG"; then 70 | mcount "failure.collectstatic.fancy-references" 71 | elif grep -q 'OSError' "$COLLECTSTATIC_LOG"; then 72 | mcount "failure.collectstatic.missing-file" 73 | else 74 | mcount "failure.collectstatic.other" 75 | fi 76 | 77 | echo " ! Error while running '$ python $MANAGE_FILE collectstatic --noinput'." 78 | echo " See traceback above for details." 79 | echo 80 | echo " You may need to update application code to resolve this error." 81 | echo " Or, you can disable collectstatic for this application:" 82 | echo 83 | echo " $ heroku config:set DISABLE_COLLECTSTATIC=1" 84 | echo 85 | echo " https://devcenter.heroku.com/articles/django-assets" 86 | 87 | # Additionally, dump out the environment, if debug mode is on. 88 | if [ "$DEBUG_COLLECTSTATIC" ]; then 89 | echo 90 | echo "****** Collectstatic environment variables:" 91 | echo 92 | env | indent 93 | fi 94 | 95 | exit 1 96 | -------------------------------------------------------------------------------- /bin/steps/hooks/post_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -f bin/post_compile ]; then 4 | echo "-----> Running post-compile hook" 5 | chmod +x bin/post_compile 6 | sub_env bin/post_compile 7 | fi -------------------------------------------------------------------------------- /bin/steps/hooks/pre_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -f bin/pre_compile ]; then 4 | echo "-----> Running pre-compile hook" 5 | chmod +x bin/pre_compile 6 | sub_env bin/pre_compile 7 | fi -------------------------------------------------------------------------------- /bin/steps/nltk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script serves as the NLTK build step of the 4 | # [**Python Buildpack**](https://github.com/heroku/heroku-buildpack-python) 5 | # compiler. 6 | # 7 | # A [buildpack](https://devcenter.heroku.com/articles/buildpacks) is an 8 | # adapter between a Python application and Heroku's runtime. 9 | # 10 | # This script is invoked by [`bin/compile`](/). 11 | 12 | # Syntax sugar. 13 | # shellcheck source=bin/utils 14 | source "$BIN_DIR/utils" 15 | 16 | # Check that nltk was installed by pip, otherwise obviously not needed 17 | if is_module_available 'nltk'; then 18 | puts-step "Downloading NLTK corpora…" 19 | 20 | nltk_packages_definition="$BUILD_DIR/nltk.txt" 21 | 22 | if [ -f "$nltk_packages_definition" ]; then 23 | 24 | readarray -t nltk_packages < "$nltk_packages_definition" 25 | puts-step "Downloading NLTK packages: ${nltk_packages[*]}" 26 | 27 | python -m nltk.downloader -d "$BUILD_DIR/.heroku/python/nltk_data" "${nltk_packages[@]}" | indent 28 | set_env NLTK_DATA "/app/.heroku/python/nltk_data" 29 | 30 | mcount "buildvar.NLTK_PACKAGES_DEFINITION" 31 | mcount "steps.nltk" 32 | else 33 | puts-warn "'nltk.txt' not found, not downloading any corpora" 34 | puts-warn "Learn more: https://devcenter.heroku.com/articles/python-nltk" 35 | fi 36 | fi 37 | -------------------------------------------------------------------------------- /bin/steps/pip-install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck source=bin/utils 4 | source "$BIN_DIR/utils" 5 | 6 | if [ ! "$SKIP_PIP_INSTALL" ]; then 7 | 8 | # Install dependencies with Pip. 9 | puts-step "Installing requirements with pip" 10 | 11 | # Set Pip env vars 12 | # This reads certain environment variables set on the Heroku app config 13 | # and makes them accessible to the pip install process. 14 | # 15 | # PIP_EXTRA_INDEX_URL allows for an alternate pypi URL to be used. 16 | if [[ -r "$ENV_DIR/PIP_EXTRA_INDEX_URL" ]]; then 17 | PIP_EXTRA_INDEX_URL="$(cat "$ENV_DIR/PIP_EXTRA_INDEX_URL")" 18 | export PIP_EXTRA_INDEX_URL 19 | mcount "buildvar.PIP_EXTRA_INDEX_URL" 20 | fi 21 | 22 | set +e 23 | 24 | # Measure that we're using pip. 25 | mcount "tool.pip" 26 | 27 | if [ ! -f "$BUILD_DIR/.heroku/python/bin/pip" ]; then 28 | exit 1 29 | fi 30 | 31 | /app/.heroku/python/bin/pip install -r "$BUILD_DIR/requirements.txt" --exists-action=w --src=/app/.heroku/src --disable-pip-version-check --no-cache-dir --progress-bar off 2>&1 | tee "$WARNINGS_LOG" | cleanup | indent 32 | PIP_STATUS="${PIPESTATUS[0]}" 33 | set -e 34 | 35 | show-warnings 36 | 37 | if [[ ! $PIP_STATUS -eq 0 ]]; then 38 | mcount "failure.pip-install" 39 | exit 1 40 | fi 41 | 42 | cp requirements.txt .heroku/python/requirements-declared.txt 43 | /app/.heroku/python/bin/pip freeze --disable-pip-version-check > .heroku/python/requirements-installed.txt 44 | 45 | # Install test dependencies, for CI. 46 | if [ "$INSTALL_TEST" ]; then 47 | if [[ -f "$1/requirements-test.txt" ]]; then 48 | puts-step "Installing test dependencies…" 49 | /app/.heroku/python/bin/pip install -r "$1/requirements-test.txt" --exists-action=w --src=./.heroku/src --disable-pip-version-check --no-cache-dir 2>&1 | cleanup | indent 50 | fi 51 | fi 52 | fi 53 | -------------------------------------------------------------------------------- /bin/steps/pipenv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # export CLINT_FORCE_COLOR=1 4 | # export PIPENV_FORCE_COLOR=1 5 | # shellcheck source=bin/utils 6 | source "$BIN_DIR/utils" 7 | set -e 8 | 9 | if [[ -f Pipfile.lock ]]; then 10 | if [[ -f .heroku/python/Pipfile.lock.sha256 ]]; then 11 | if [[ $(openssl dgst -sha256 Pipfile.lock) == $(cat .heroku/python/Pipfile.lock.sha256) ]]; then 12 | # Don't skip installation if there are git deps. 13 | if ! grep -q 'git' Pipfile.lock; then 14 | echo "Skipping installation, as Pipfile.lock hasn't changed since last deploy." | indent 15 | 16 | mcount "tool.pipenv" 17 | export SKIP_PIPENV_INSTALL=1 18 | export SKIP_PIP_INSTALL=1 19 | fi 20 | fi 21 | fi 22 | fi 23 | 24 | 25 | if [ ! "$SKIP_PIPENV_INSTALL" ]; then 26 | # Pipenv support (Generate requirements.txt with pipenv). 27 | if [[ -f Pipfile ]]; then 28 | # Measure that we're using Pipenv. 29 | mcount "tool.pipenv" 30 | 31 | # Skip pip install, later. 32 | export SKIP_PIP_INSTALL=1 33 | 34 | # Set Pip env vars 35 | # This reads certain environment variables set on the Heroku app config 36 | # and makes them accessible to the pip install process. 37 | # 38 | # PIP_EXTRA_INDEX_URL allows for an alternate pypi URL to be used. 39 | if [[ -r "$ENV_DIR/PIP_EXTRA_INDEX_URL" ]]; then 40 | PIP_EXTRA_INDEX_URL="$(cat "$ENV_DIR/PIP_EXTRA_INDEX_URL")" 41 | export PIP_EXTRA_INDEX_URL 42 | mcount "buildvar.PIP_EXTRA_INDEX_URL" 43 | fi 44 | 45 | PIPENV_VERSION='2020.11.15' 46 | 47 | # Install pipenv. 48 | # Due to weird old pip behavior and pipenv behavior, pipenv upgrades pip 49 | # to latest if only --upgrade is specified. Specify upgrade strategy to 50 | # avoid this eager behavior. 51 | /app/.heroku/python/bin/pip install "pipenv==${PIPENV_VERSION}" --upgrade --upgrade-strategy only-if-needed &> /dev/null 52 | 53 | # Install the test dependencies, for CI. 54 | if [ "$INSTALL_TEST" ]; then 55 | puts-step "Installing test dependencies" 56 | /app/.heroku/python/bin/pipenv install --dev --system --deploy 2>&1 | cleanup | indent 57 | 58 | # Install the dependencies. 59 | elif [[ ! -f Pipfile.lock ]]; then 60 | puts-step "Installing dependencies with Pipenv ${PIPENV_VERSION}" 61 | /app/.heroku/python/bin/pipenv install --system --skip-lock 2>&1 | indent 62 | 63 | else 64 | pipenv-to-pip Pipfile.lock > requirements.txt 65 | cp requirements.txt .heroku/python/requirements-declared.txt 66 | openssl dgst -sha256 Pipfile.lock > .heroku/python/Pipfile.lock.sha256 67 | 68 | puts-step "Installing dependencies with Pipenv ${PIPENV_VERSION}" 69 | /app/.heroku/python/bin/pipenv install --system --deploy 2>&1 | indent 70 | fi 71 | fi 72 | else 73 | export SKIP_PIP_INSTALL=1 74 | pipenv-to-pip Pipfile.lock > requirements.txt 75 | fi 76 | -------------------------------------------------------------------------------- /bin/steps/pipenv-python-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Detect Python-version with Pipenv. 4 | 5 | if [[ -f $BUILD_DIR/Pipfile ]]; then 6 | 7 | if [[ ! -f $BUILD_DIR/runtime.txt ]]; then 8 | if [[ ! -f $BUILD_DIR/Pipfile.lock ]]; then 9 | puts-warn "No 'Pipfile.lock' found! We recommend you commit this into your repository." 10 | fi 11 | if [[ -f $BUILD_DIR/Pipfile.lock ]]; then 12 | # Ignore unused env var warning since this is used by bin/compile. 13 | # shellcheck disable=2034 14 | PYTHON_VERSION_SOURCE='Pipfile.lock' 15 | set +e 16 | PYTHON=$(jq -r '._meta.requires.python_full_version' "$BUILD_DIR/Pipfile.lock") 17 | if [[ "$PYTHON" != "null" ]]; then 18 | echo "python-$PYTHON" > "$BUILD_DIR/runtime.txt" 19 | fi 20 | set -e 21 | 22 | if [[ "$PYTHON" == "null" ]]; then 23 | PYTHON=$(jq -r '._meta.requires.python_version' "$BUILD_DIR/Pipfile.lock") 24 | case "${PYTHON}" in 25 | 2.7) 26 | echo "${LATEST_27}" > "${BUILD_DIR}/runtime.txt" 27 | ;; 28 | 3.4) 29 | echo "${LATEST_34}" > "${BUILD_DIR}/runtime.txt" 30 | ;; 31 | 3.5) 32 | echo "${LATEST_35}" > "${BUILD_DIR}/runtime.txt" 33 | ;; 34 | 3.6) 35 | echo "${LATEST_36}" > "${BUILD_DIR}/runtime.txt" 36 | ;; 37 | 3.7) 38 | echo "${LATEST_37}" > "${BUILD_DIR}/runtime.txt" 39 | ;; 40 | 3.8) 41 | echo "${LATEST_38}" > "${BUILD_DIR}/runtime.txt" 42 | ;; 43 | 3.9) 44 | echo "${LATEST_39}" > "${BUILD_DIR}/runtime.txt" 45 | ;; 46 | 3.10) 47 | echo "${LATEST_310}" > "${BUILD_DIR}/runtime.txt" 48 | ;; 49 | esac 50 | fi 51 | 52 | 53 | fi 54 | fi 55 | fi 56 | -------------------------------------------------------------------------------- /bin/steps/python: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO: Replace runtime-fixer with a bash equivalent, since currently on rebuilds it 4 | # gets run with the cached Python install (rather than system Python), which means it 5 | # fails to run with glibc errors on stack downgrades (see spec/hatchet/stack_spec.rb). 6 | # To work around this for now, non-zero exit codes are ignored (most runtime.txt files 7 | # don't need fixing up, so the script not running won't normally matter). 8 | runtime-fixer runtime.txt || true 9 | PYTHON_VERSION=$(cat runtime.txt) 10 | 11 | # The location of the pre-compiled python binary. 12 | PYTHON_URL="${S3_BASE_URL}/${STACK}/runtimes/${PYTHON_VERSION}.tar.gz" 13 | 14 | if ! curl --output /dev/null --silent --head --fail --retry 3 --retry-connrefused --connect-timeout 5 "${PYTHON_URL}"; then 15 | puts-warn "Requested runtime '${PYTHON_VERSION}' is not available for this stack (${STACK})." 16 | puts-warn "For supported versions, see: https://devcenter.heroku.com/articles/python-support" 17 | exit 1 18 | fi 19 | 20 | function eol_python_version_error() { 21 | local major_version="${1}" 22 | local eol_date="${2}" 23 | puts-warn 24 | puts-warn "Python ${major_version} reached upstream end-of-life on ${eol_date}, and is" 25 | puts-warn "therefore no longer receiving security updates:" 26 | puts-warn "https://devguide.python.org/versions/#supported-versions" 27 | puts-warn 28 | puts-warn "As such, it is no longer supported by the latest version of this buildpack." 29 | puts-warn 30 | puts-warn "Please upgrade to a newer Python version. See:" 31 | puts-warn "https://devcenter.heroku.com/articles/python-runtimes" 32 | puts-warn 33 | exit 1 34 | } 35 | 36 | function warn_if_patch_update_available() { 37 | local current_version="${1}" 38 | local latest_patch_version="${2}" 39 | if [[ "${current_version}" != "${latest_patch_version}" ]]; then 40 | puts-warn 41 | puts-warn "A Python security update is available! Upgrade as soon as possible to: ${latest_patch_version}" 42 | puts-warn "See: https://devcenter.heroku.com/articles/python-runtimes" 43 | puts-warn 44 | fi 45 | } 46 | 47 | case "${PYTHON_VERSION}" in 48 | python-3.10.*) 49 | warn_if_patch_update_available "${PYTHON_VERSION}" "${LATEST_310}" 50 | ;; 51 | python-3.9.*) 52 | warn_if_patch_update_available "${PYTHON_VERSION}" "${LATEST_39}" 53 | ;; 54 | python-3.8.*) 55 | warn_if_patch_update_available "${PYTHON_VERSION}" "${LATEST_38}" 56 | ;; 57 | python-3.7.*) 58 | warn_if_patch_update_available "${PYTHON_VERSION}" "${LATEST_37}" 59 | ;; 60 | python-3.6.*) 61 | puts-warn 62 | puts-warn "Python 3.6 reached upstream end-of-life on December 23rd, 2021, and is" 63 | puts-warn "therefore no longer receiving security updates:" 64 | puts-warn "https://devguide.python.org/versions/#supported-versions" 65 | puts-warn 66 | puts-warn "Upgrade to a newer Python version as soon as possible to keep your app secure." 67 | puts-warn "See: https://devcenter.heroku.com/articles/python-runtimes" 68 | puts-warn 69 | warn_if_patch_update_available "${PYTHON_VERSION}" "${LATEST_36}" 70 | ;; 71 | python-3.5.*) 72 | eol_python_version_error "3.5" "September 30th, 2020" 73 | ;; 74 | python-3.4.*) 75 | eol_python_version_error "3.4" "March 18th, 2019" 76 | ;; 77 | python-2.7.*) 78 | puts-warn 79 | puts-warn "Python 2 reached upstream end-of-life on January 1st, 2020, and is" 80 | puts-warn "therefore no longer receiving security updates. Apps still using it" 81 | puts-warn "contain potential security vulnerabilities and should be upgraded to" 82 | puts-warn "Python 3 as soon as possible." 83 | puts-warn 84 | puts-warn "In addition, Python 2 is only supported on our oldest stack, Heroku-18," 85 | puts-warn "which is deprecated and reaches end-of-life on April 30th, 2023." 86 | puts-warn 87 | puts-warn "As such, it is no longer supported by the latest version of this buildpack:" 88 | puts-warn "https://devcenter.heroku.com/changelog-items/2473" 89 | puts-warn 90 | puts-warn "You must either:" 91 | puts-warn " - Upgrade to Python 3 (recommended)" 92 | puts-warn " - Switch to the container stack and use the upstream legacy 'python:2.7' Docker images" 93 | puts-warn " - Switch to an older version of the Python buildpack (short term workaround only)" 94 | puts-warn 95 | puts-warn "For more details, see:" 96 | puts-warn "https://devcenter.heroku.com/articles/python-2-7-eol-faq" 97 | puts-warn 98 | exit 1 99 | ;; 100 | pypy*) 101 | puts-warn 102 | puts-warn "PyPy is no longer supported by the latest version of this buildpack." 103 | puts-warn 104 | puts-warn "Please switch to one of the supported CPython versions by updating your" 105 | puts-warn "runtime.txt file. See:" 106 | puts-warn "https://devcenter.heroku.com/articles/python-support" 107 | puts-warn 108 | exit 1 109 | ;; 110 | esac 111 | 112 | mcount "version.python.${PYTHON_VERSION}" 113 | 114 | if [[ "$STACK" != "$CACHED_PYTHON_STACK" ]]; then 115 | puts-step "Stack has changed from $CACHED_PYTHON_STACK to $STACK, clearing cache" 116 | rm -rf .heroku/python-stack .heroku/python-version .heroku/python .heroku/vendor .heroku/python .heroku/python-sqlite3-version 117 | fi 118 | 119 | if [ -f .heroku/python-version ]; then 120 | if [ ! "$(cat .heroku/python-version)" = "$PYTHON_VERSION" ]; then 121 | puts-step "Python version has changed from $(cat .heroku/python-version) to ${PYTHON_VERSION}, clearing cache" 122 | rm -rf .heroku/python 123 | else 124 | SKIP_INSTALL=1 125 | fi 126 | fi 127 | 128 | # If using Pip, check if we should reinstall python dependencies given that requirements.txt 129 | # is non-deterministic (not all packages pinned, doesn't handle uninstalls etc). We don't need 130 | # to do this when using Pipenv, since it has a lockfile and syncs the packages for us. 131 | if [[ -f "${BUILD_DIR}/requirements.txt" ]]; then 132 | if [[ ! -f "$CACHE_DIR/.heroku/requirements.txt" ]]; then 133 | # This is a the first build of an app (or the build cache was cleared). Since there 134 | # are no cached packages, we only need to store the requirements file for next time. 135 | cp -R "$BUILD_DIR/requirements.txt" "$CACHE_DIR/.heroku/requirements.txt" 136 | else 137 | # IF there IS a cached directory, check for differences with the new one 138 | if ! diff "$BUILD_DIR/requirements.txt" "$CACHE_DIR/.heroku/requirements.txt" &> /dev/null; then 139 | puts-step "Requirements file has been changed, clearing cached dependencies" 140 | # if there are any differences, clear the Python cache 141 | # Installing Python over again does not take noticably more time 142 | cp -R "$BUILD_DIR/requirements.txt" "$CACHE_DIR/.heroku/requirements.txt" 143 | rm -rf .heroku/python 144 | unset SKIP_INSTALL 145 | else 146 | puts-step "No change in requirements detected, installing from cache" 147 | fi 148 | fi 149 | fi 150 | 151 | if [[ -n "${SKIP_INSTALL}" ]]; then 152 | puts-step "Using cached install of ${PYTHON_VERSION}" 153 | else 154 | puts-step "Installing ${PYTHON_VERSION}" 155 | 156 | # Prepare destination directory. 157 | mkdir -p .heroku/python 158 | 159 | if ! curl --silent --show-error --fail --retry 3 --retry-connrefused --connect-timeout 5 "${PYTHON_URL}" | tar -zxC .heroku/python; then 160 | # The Python version was confirmed to exist previously, so any failure here is due to 161 | # a networking issue or archive/buildpack bug rather than the runtime not existing. 162 | puts-warn "Failed to download/install ${PYTHON_VERSION}" 163 | exit 1 164 | fi 165 | 166 | # Record for future reference. 167 | echo "$PYTHON_VERSION" > .heroku/python-version 168 | echo "$STACK" > .heroku/python-stack 169 | 170 | hash -r 171 | fi 172 | 173 | PIP_VERSION='22.2.2' 174 | SETUPTOOLS_VERSION='63.4.3' 175 | WHEEL_VERSION='0.37.1' 176 | 177 | case "${PYTHON_VERSION}" in 178 | python-3.6.*) 179 | # Python 3.6 support was dropped in pip 22+ and setuptools 59.7.0+. 180 | PIP_VERSION='21.3.1' 181 | SETUPTOOLS_VERSION='59.6.0' 182 | ;; 183 | esac 184 | 185 | puts-step "Installing pip ${PIP_VERSION}, setuptools ${SETUPTOOLS_VERSION} and wheel ${WHEEL_VERSION}" 186 | 187 | # We don't use get-pip.py, since: 188 | # - it uses `--force-reinstall`, which is unnecessary here and slows down repeat builds 189 | # - it means downloading pip twice (once embedded in get-pip.py, and again during 190 | # the install, since get-pip.py can't install the embedded version directly) 191 | # - we would still have to manage several versions of get-pip.py, to support older Pythons. 192 | # Instead, we use the pip wheel to install itself, using the method described here: 193 | # https://github.com/pypa/pip/issues/2351#issuecomment-69994524 194 | 195 | PIP_WHEEL_FILENAME="pip-${PIP_VERSION}-py3-none-any.whl" 196 | PIP_WHEEL_URL="${S3_BASE_URL}/common/${PIP_WHEEL_FILENAME}" 197 | PIP_WHEEL="${TMPDIR:-/tmp}/${PIP_WHEEL_FILENAME}" 198 | 199 | if ! curl --silent --show-error --fail --retry 3 --retry-connrefused --connect-timeout 5 "${PIP_WHEEL_URL}" -o "$PIP_WHEEL"; then 200 | mcount "failure.python.download-pip" 201 | puts-warn "Failed to download pip" 202 | exit 1 203 | fi 204 | 205 | /app/.heroku/python/bin/python "${PIP_WHEEL}/pip" install --quiet --disable-pip-version-check --no-cache \ 206 | "${PIP_WHEEL}" "setuptools==${SETUPTOOLS_VERSION}" "wheel==${WHEEL_VERSION}" 207 | 208 | hash -r 209 | -------------------------------------------------------------------------------- /bin/steps/sqlite3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck source=bin/utils 4 | source "$BIN_DIR/utils" 5 | 6 | sqlite3_install() { 7 | HEROKU_PYTHON_DIR="$1" 8 | HEADERS_ONLY="$3" 9 | 10 | mkdir -p "$HEROKU_PYTHON_DIR" 11 | 12 | APT_CACHE_DIR="$HEROKU_PYTHON_DIR/apt/cache" 13 | APT_STATE_DIR="$HEROKU_PYTHON_DIR/apt/state" 14 | 15 | mkdir -p "$APT_CACHE_DIR/archives/partial" 16 | mkdir -p "$APT_STATE_DIR/lists/partial" 17 | 18 | APT_OPTIONS="-o debug::nolocking=true" 19 | APT_OPTIONS="$APT_OPTIONS -o dir::cache=$APT_CACHE_DIR" 20 | APT_OPTIONS="$APT_OPTIONS -o dir::state=$APT_STATE_DIR" 21 | APT_OPTIONS="$APT_OPTIONS -o dir::etc::sourcelist=/etc/apt/sources.list" 22 | 23 | apt-get $APT_OPTIONS update > /dev/null 2>&1 24 | if [ -z "$HEADERS_ONLY" ]; then 25 | apt-get $APT_OPTIONS -y -d --reinstall install libsqlite3-dev sqlite3 > /dev/null 2>&1 26 | else 27 | apt-get $APT_OPTIONS -y -d --reinstall install libsqlite3-dev 28 | fi 29 | 30 | find "$APT_CACHE_DIR/archives/" -name "*.deb" -exec dpkg -x {} "$HEROKU_PYTHON_DIR/sqlite3/" \; 31 | 32 | mkdir -p "$HEROKU_PYTHON_DIR/include" 33 | mkdir -p "$HEROKU_PYTHON_DIR/lib" 34 | 35 | # remove old sqlite3 libraries/binaries 36 | find "$HEROKU_PYTHON_DIR/include/" -name "sqlite3*.h" -exec rm -f {} \; 37 | find "$HEROKU_PYTHON_DIR/lib/" -name "libsqlite3.*" -exec rm -f {} \; 38 | rm -f "$HEROKU_PYTHON_DIR/lib/pkgconfig/sqlite3.pc" 39 | rm -f "$HEROKU_PYTHON_DIR/bin/sqlite3" 40 | 41 | # copy over sqlite3 headers & bins and setup linking against the stack image library 42 | mv "$HEROKU_PYTHON_DIR/sqlite3/usr/include/"* "$HEROKU_PYTHON_DIR/include/" 43 | mv "$HEROKU_PYTHON_DIR/sqlite3/usr/lib/x86_64-linux-gnu"/libsqlite3.*a "$HEROKU_PYTHON_DIR/lib/" 44 | mkdir -p "$HEROKU_PYTHON_DIR/lib/pkgconfig" 45 | # set the right prefix/lib directories 46 | sed -e 's/prefix=\/usr/prefix=\/app\/.heroku\/python/' -e 's/\/x86_64-linux-gnu//' "$HEROKU_PYTHON_DIR/sqlite3/usr/lib/x86_64-linux-gnu/pkgconfig/sqlite3.pc" > "$HEROKU_PYTHON_DIR/lib/pkgconfig/sqlite3.pc" 47 | # need to point the libsqlite3.so to the stack image library for /usr/bin/ld -lsqlite3 48 | SQLITE3_LIBFILE="/usr/lib/x86_64-linux-gnu/$(readlink -n "$HEROKU_PYTHON_DIR/sqlite3/usr/lib/x86_64-linux-gnu/libsqlite3.so")" 49 | ln -s "$SQLITE3_LIBFILE" "$HEROKU_PYTHON_DIR/lib/libsqlite3.so" 50 | if [ -z "$HEADERS_ONLY" ]; then 51 | mv "$HEROKU_PYTHON_DIR/sqlite3/usr/bin"/* "$HEROKU_PYTHON_DIR/bin/" 52 | fi 53 | 54 | # cleanup 55 | rm -rf "$HEROKU_PYTHON_DIR/sqlite3/" 56 | rm -rf "$HEROKU_PYTHON_DIR/apt/" 57 | } 58 | 59 | buildpack_sqlite3_install() { 60 | HEROKU_PYTHON_DIR="$BUILD_DIR/.heroku/python" 61 | 62 | SQLITE3_VERSION_FILE="$BUILD_DIR/.heroku/python-sqlite3-version" 63 | if [ -f "$SQLITE3_VERSION_FILE" ]; then 64 | INSTALLED_SQLITE3_VERSION=$(cat "$SQLITE3_VERSION_FILE") 65 | fi 66 | 67 | puts-step "Installing SQLite3" 68 | 69 | if sqlite3_install "$BUILD_DIR/.heroku/python" ; then 70 | mcount "success.python.sqlite3" 71 | else 72 | echo "Sqlite3 failed to install." 73 | mcount "failure.python.sqlite3" 74 | fi 75 | 76 | mkdir -p "$CACHE_DIR/.heroku/" 77 | } 78 | -------------------------------------------------------------------------------- /bin/test-compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Syntax sugar. 4 | BIN_DIR=$(cd "$(dirname "$0")" || return; pwd) # absolute path 5 | 6 | # shellcheck source=bin/utils 7 | source "$BIN_DIR/utils" 8 | 9 | # Locale support for Pipenv. 10 | export LC_ALL=C.UTF-8 11 | export LANG=C.UTF-8 12 | 13 | DISABLE_COLLECTSTATIC=1 INSTALL_TEST=1 "$(dirname "${0:-}")/compile" "$1" "$2" "$3" -------------------------------------------------------------------------------- /bin/utils: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | shopt -s extglob 3 | shopt -s nullglob 4 | 5 | # This is necessary since this script is sometimes sourced from 6 | # subshells that don't have the variables from bin/compile. 7 | # Remove this once we no longer wrap all the things in `sub_env`. 8 | BIN_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 9 | ROOT_DIR=$(dirname "${BIN_DIR}") 10 | # shellcheck source=vendor/buildpack-stdlib_v8.sh 11 | source "${ROOT_DIR}/vendor/buildpack-stdlib_v8.sh" 12 | 13 | if [ "$(uname)" == Darwin ]; then 14 | sed() { command sed -l "$@"; } 15 | else 16 | sed() { command sed -u "$@"; } 17 | fi 18 | 19 | # Syntax sugar. 20 | indent() { 21 | sed "s/^/ /" 22 | } 23 | 24 | 25 | # Clean up pip output 26 | cleanup() { 27 | sed -e 's/\.\.\.\+/.../g' | sed -e '/already satisfied/Id' | sed -e '/No files were found to uninstall/Id' | sed -e '/Overwriting/Id' | sed -e '/python executable/Id' | sed -e '/no previously-included files/Id' 28 | } 29 | 30 | # Buildpack Steps. 31 | puts-step() { 32 | echo "-----> $*" 33 | } 34 | 35 | # Buildpack Warnings. 36 | puts-warn() { 37 | echo " ! $*" 38 | } 39 | 40 | # Does some serious copying. 41 | deep-cp() { 42 | declare source="$1" target="$2" 43 | 44 | mkdir -p "$target" 45 | 46 | # cp doesn't like being called without source params, 47 | # so make sure they expand to something first. 48 | # subshell to avoid surprising caller with shopts. 49 | ( 50 | shopt -s nullglob dotglob 51 | set -- "$source"/!(tmp|.|..) 52 | [[ $# == 0 ]] || cp -a "$@" "$target" 53 | ) 54 | } 55 | 56 | # Measure the size of the Python installation. 57 | measure-size() { 58 | echo "$(du -s .heroku/python 2>/dev/null || echo 0) | awk '{print $1}')" 59 | } 60 | 61 | is_module_available() { 62 | # Returns 0 if the specified module exists, otherwise returns 1. 63 | # Uses pkgutil rather than pkg_resources or pip's CLI, since pkgutil exists 64 | # in the stdlib, and doesn't depend on the choice of package manager. 65 | local module_name="${1}" 66 | python -c "import sys, pkgutil; sys.exit(0 if pkgutil.find_loader('${module_name}') else 1)" 67 | } 68 | -------------------------------------------------------------------------------- /bin/warnings: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | shopt -s extglob 3 | 4 | gdal-missing() { 5 | if grep -qi 'Could not find gdal-config' "$WARNINGS_LOG"; then 6 | mcount 'warnings.gdal' 7 | echo 8 | puts-warn "Hello! Package installation failed since the GDAL library was not found." 9 | puts-warn "For GDAL, GEOS and PROJ support, use the Geo buildpack alongside the Python buildpack:" 10 | puts-warn "https://github.com/heroku/heroku-geo-buildpack" 11 | puts-warn " -- Much Love, Heroku." 12 | fi 13 | } 14 | 15 | show-warnings() { 16 | gdal-missing 17 | } 18 | -------------------------------------------------------------------------------- /buildpack.toml: -------------------------------------------------------------------------------- 1 | [buildpack] 2 | name = "Python" 3 | 4 | [publish.Ignore] 5 | files = [ 6 | ".github/", 7 | "builds/", 8 | "spec/", 9 | ".dockerignore", 10 | ".gitignore", 11 | ".rubocop.yml", 12 | "Gemfile", 13 | "Gemfile.lock", 14 | "hatchet.json", 15 | "hatchet.lock", 16 | "Makefile", 17 | "requirements.txt", 18 | ] 19 | -------------------------------------------------------------------------------- /builds/README.md: -------------------------------------------------------------------------------- 1 | # Python Buildpack Binaries 2 | 3 | The binaries for this buildpack are built in Docker containers based on the Heroku stack image. 4 | 5 | ## Configuration 6 | 7 | In order to publish binaries AWS credentials must be passed to the build container. 8 | If you are testing only the build (ie: `bob build`), these are optional. 9 | 10 | In addition, unless you are building the official binaries for Heroku (which use the defaults 11 | specified in each `Dockerfile`), you will need to override `S3_BUCKET` and `S3_PREFIX` to 12 | match your own S3 bucket/use case. 13 | 14 | If you only need to set AWS credentials, you can do so by setting the environment variables 15 | `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` before calling the make commands. 16 | 17 | For example: 18 | 19 | ```bash 20 | set +o history # Disable bash history 21 | export AWS_ACCESS_KEY_ID=... 22 | export AWS_SECRET_ACCESS_KEY=... 23 | set -o history # Re-enable bash history 24 | make ... 25 | ``` 26 | 27 | If you need to override the default S3 bucket, or would prefer not to use credentials via 28 | environment variables, then you need to instead use a Docker env file like so: 29 | 30 | 1. Copy the `builds/dockerenv.default` env file to a location outside the buildpack repository. 31 | 2. Edit the new file, adding at a minimum the values for the variables 32 | `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (see Docker 33 | [env-file documentation](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file)). 34 | 3. Pass the path of the file to the make commands using `ENV_FILE`. For example: 35 | 36 | ```bash 37 | make ... ENV_FILE=~/.dockerenv.python-buildpack 38 | ``` 39 | 40 | ## Launching an interactive build environment 41 | 42 | To start an interactive version of the build environment (ideal for development) use the 43 | `buildenv` make target, passing in the desired `STACK` name. For example: 44 | 45 | ```bash 46 | make buildenv STACK=heroku-18 47 | ``` 48 | 49 | This will create the builder docker image based on the latest image for that stack, and 50 | then start a bash shell where you can run `bob build`, `bob deploy`, and so forth. 51 | 52 | The `builds/` directory is bind-mounted into the running container, so local build formula 53 | changes will appear there immediately without the need to rebuild the image. 54 | 55 | ## Bulk deploying runtimes 56 | 57 | When a new Python version is released, binaries have to be generated for multiple stacks. 58 | To automate this, use the `deploy-runtimes` make target, which will ensure the builder 59 | image is up to date, and then run `bob deploy` for each runtime-stack combination. 60 | 61 | The build formula name(s) are passed using `RUNTIMES`, like so: 62 | 63 | ```bash 64 | make deploy-runtimes RUNTIMES='python-X.Y.Z' 65 | ``` 66 | 67 | By default this will deploy to all supported stacks (see `STACKS` in `Makefile`), 68 | but this can be overridden using `STACKS`: 69 | 70 | ```bash 71 | make deploy-runtimes RUNTIMES='python-X.Y.Z' STACKS='heroku-18' 72 | ``` 73 | 74 | Multiple runtimes can also be specified (useful for when adding a new stack), like so: 75 | 76 | ```bash 77 | make deploy-runtimes RUNTIMES='python-A.B.C python-X.Y.Z' STACKS='heroku-22' 78 | ``` 79 | 80 | Note: Both `RUNTIMES` and `STACKS` are space delimited. 81 | -------------------------------------------------------------------------------- /builds/dockerenv.default: -------------------------------------------------------------------------------- 1 | # Since no values are specified here, these variables will be read from the environment at run time: 2 | # https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file 3 | AWS_ACCESS_KEY_ID 4 | AWS_SECRET_ACCESS_KEY 5 | 6 | # Uncomment these if you need to override the default S3 bucket and/or path prefixes. 7 | # S3_BUCKET 8 | # S3_PREFIX 9 | -------------------------------------------------------------------------------- /builds/heroku-18.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM heroku/heroku:18-build 2 | 3 | ENV WORKSPACE_DIR="/app/builds" \ 4 | S3_BUCKET="heroku-buildpack-python" \ 5 | S3_PREFIX="heroku-18/" \ 6 | STACK="heroku-18" 7 | 8 | RUN apt-get update \ 9 | && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ 10 | libsqlite3-dev \ 11 | python3-pip \ 12 | python3-setuptools \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | WORKDIR /app 16 | 17 | COPY requirements.txt /app/ 18 | RUN pip3 install --disable-pip-version-check --no-cache-dir -r /app/requirements.txt 19 | 20 | COPY . /app 21 | -------------------------------------------------------------------------------- /builds/heroku-20.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM heroku/heroku:20-build 2 | 3 | ENV WORKSPACE_DIR="/app/builds" \ 4 | S3_BUCKET="heroku-buildpack-python" \ 5 | S3_PREFIX="heroku-20/" \ 6 | STACK="heroku-20" 7 | 8 | RUN apt-get update \ 9 | && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ 10 | libsqlite3-dev \ 11 | python3-pip \ 12 | python3-setuptools \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | WORKDIR /app 16 | 17 | COPY requirements.txt /app/ 18 | RUN pip3 install --disable-pip-version-check --no-cache-dir -r /app/requirements.txt 19 | 20 | COPY . /app 21 | -------------------------------------------------------------------------------- /builds/heroku-22.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM heroku/heroku:22-build 2 | 3 | ENV WORKSPACE_DIR="/app/builds" \ 4 | S3_BUCKET="heroku-buildpack-python" \ 5 | S3_PREFIX="heroku-22/" \ 6 | STACK="heroku-22" 7 | 8 | RUN apt-get update \ 9 | && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ 10 | libsqlite3-dev \ 11 | python3-pip \ 12 | python3-setuptools \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | WORKDIR /app 16 | 17 | COPY requirements.txt /app/ 18 | RUN pip3 install --disable-pip-version-check --no-cache-dir -r /app/requirements.txt 19 | 20 | COPY . /app 21 | -------------------------------------------------------------------------------- /builds/runtimes/python: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # This is set by bob-builder from the "Build Path" comment line in the 6 | # build formula script that sourced this one. 7 | OUT_PREFIX=$1 8 | # The filename of the script that sourced this one (e.g. `python-3.10.0`). 9 | FORMULA_FILENAME=$(basename "${0}") 10 | # The version component (e.g. `3.10.0`). 11 | VERSION=$(echo "${FORMULA_FILENAME}" | cut --delimiter '-' --fields 2) 12 | 13 | echo "Building Python ${VERSION}..." 14 | 15 | if [[ "${STACK}" != "heroku-18" && "${STACK}" != "heroku-20" && "${VERSION}" == 3.[7-8].* ]]; then 16 | echo "Python 3.7 and 3.8 are only supported on Heroku-20 and older!" >&2 17 | echo "Override the default stacks list using: STACKS='heroku-18 heroku-20'" >&2 18 | exit 1 19 | fi 20 | 21 | # See: https://www.python.org/downloads/ -> "OpenPGP Public Keys" 22 | case "${VERSION}" in 23 | 3.10.*) 24 | # https://keybase.io/pablogsal/ 25 | GPG_KEY_FINGERPRINT='A035C8C19219BA821ECEA86B64E628F8D684696D' 26 | ;; 27 | 3.[8-9].*) 28 | # https://keybase.io/ambv/ 29 | GPG_KEY_FINGERPRINT='E3FF2839C048B25C084DEBE9B26995E310250568' 30 | ;; 31 | 3.7.*) 32 | # https://keybase.io/nad/ 33 | GPG_KEY_FINGERPRINT='0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D' 34 | ;; 35 | *) 36 | echo "Unsupported Python version!" >&2 37 | exit 1 38 | ;; 39 | esac 40 | 41 | SOURCE_URL="https://www.python.org/ftp/python/${VERSION}/Python-${VERSION}.tgz" 42 | SIGNATURE_URL="${SOURCE_URL}.asc" 43 | 44 | set -o xtrace 45 | 46 | curl --fail --retry 3 --retry-connrefused --connect-timeout 5 --max-time 60 -o python.tgz "${SOURCE_URL}" 47 | curl --fail --retry 3 --retry-connrefused --connect-timeout 5 --max-time 60 -o python.tgz.asc "${SIGNATURE_URL}" 48 | 49 | # Skip GPG verification on Heroku-18 since it fails to fetch keys: 50 | # `gpg: keyserver receive failed: Server indicated a failure` 51 | if [[ "${STACK}" != "heroku-18" ]]; then 52 | gpg --batch --verbose --recv-keys "${GPG_KEY_FINGERPRINT}" 53 | gpg --batch --verify python.tgz.asc python.tgz 54 | fi 55 | 56 | mkdir src 57 | tar --extract --file python.tgz --strip-components=1 --directory src/ 58 | cd src 59 | 60 | # Aim to keep this roughly consistent with the options used in the Python Docker images, 61 | # for maximum compatibility / most battle-tested build configuration: 62 | # https://github.com/docker-library/python 63 | CONFIGURE_OPTS=( 64 | # Support loadable extensions in the `_sqlite` extension module. 65 | "--enable-loadable-sqlite-extensions" 66 | # Make autoconf's configure option validation more strict. 67 | "--enable-option-checking=fatal" 68 | # Install Python into `/app/.heroku/python` rather than the default of `/usr/local`. 69 | "--prefix=${OUT_PREFIX}" 70 | # Skip running `ensurepip` as part of install, since the buildpack installs a curated 71 | # version of pip itself (which ensures it's consistent across Python patch releases). 72 | "--with-ensurepip=no" 73 | # Build the `pyexpat` module using the `expat` library in the stack image (which will 74 | # automatically receive security updates), rather than CPython's vendored version. 75 | "--with-system-expat" 76 | ) 77 | 78 | if [[ "${VERSION}" != 3.7.* ]]; then 79 | CONFIGURE_OPTS+=( 80 | # Python 3.7 and older run the whole test suite for PGO, which takes 81 | # much too long. Whilst this can be overridden via `PROFILE_TASK`, we 82 | # prefer to change as few of the upstream build options as possible. 83 | # As such, PGO is only enabled for Python 3.8+. 84 | "--enable-optimizations" 85 | ) 86 | fi 87 | 88 | if [[ "${VERSION}" == 3.10.* ]]; then 89 | CONFIGURE_OPTS+=( 90 | # Shared builds are beneficial for a number of reasons: 91 | # - Reduces the size of the build, since it avoids the duplication between 92 | # the Python binary and the static library. 93 | # - Permits use-cases that only work with the shared Python library, 94 | # and not the static library (such as `pycall.rb` or `PyO3`). 95 | # - More consistent with the official Python Docker images and other distributions. 96 | # 97 | # However, shared builds are slower unless `no-semantic-interposition`and LTO is used: 98 | # https://fedoraproject.org/wiki/Changes/PythonNoSemanticInterpositionSpeedup 99 | # 100 | # It's only as of Python 3.10 that `no-semantic-interposition` is enabled by default, 101 | # so we only use shared builds on Python 3.10+ to avoid needing to override the default 102 | # compiler flags. 103 | "--enable-shared" 104 | "--with-lto" 105 | # Counter-intuitively, the static library is still generated by default even when 106 | # the shared library is enabled, so we disable it to reduce the build size. 107 | "--without-static-libpython" 108 | ) 109 | fi 110 | 111 | ./configure "${CONFIGURE_OPTS[@]}" 112 | 113 | # Using LDFLAGS we instruct the linker to omit all symbol information from the final binary 114 | # and shared libraries, to reduce the size of the build. We have to use `--strip-all` and 115 | # not `--strip-unneeded` since `ld` only understands the former (unlike the `strip` command), 116 | # however it's safe to use since these options don't apply to static libraries. 117 | make -j "$(nproc)" LDFLAGS='-Wl,--strip-all' 118 | make install 119 | 120 | if [[ "${VERSION}" == 3.[7-9].* ]]; then 121 | # On older versions of Python we're still building the static library, which has to be 122 | # manually stripped since the linker stripping enabled in LDFLAGS doesn't cover them. 123 | # We're using `--strip-unneeded` since `--strip-all` would remove the `.symtab` section 124 | # that is required for static libraries to be able to be linked. 125 | # `find` is used since there are multiple copies of the static library in version-specific 126 | # locations, eg: 127 | # - `lib/libpython3.9.a` 128 | # - `lib/python3.9/config-3.9-x86_64-linux-gnu/libpython3.9.a` 129 | find "${OUT_PREFIX}" -type f -name '*.a' -print -exec strip --strip-unneeded '{}' + 130 | elif ! find "${OUT_PREFIX}" -type f -name '*.a' -print -exec false '{}' +; then 131 | echo "Unexpected static libraries found!" >&2 132 | exit 1 133 | fi 134 | 135 | # Remove unneeded test directories, similar to the official Docker Python images: 136 | # https://github.com/docker-library/python 137 | # TODO: Explore using --disable-test-modules once the PGO fix is in a released Python version: 138 | # https://bugs.python.org/issue45668 139 | find "${OUT_PREFIX}" -depth -type d -a \( -name 'test' -o -name 'tests' -o -name 'idle_test' \) -print -exec rm -rf '{}' + 140 | 141 | # The `make install` step automatically generates `.pyc` files for the stdlib, however: 142 | # - It generates these using the default `timestamp` invalidation mode, which does 143 | # not work well with the CNB file timestamp normalisation behaviour. As such, we 144 | # must use one of the hash-based invalidation modes to prevent the `.pyc`s from 145 | # always being treated as outdated and so being regenerated at application boot. 146 | # - It generates `.pyc`s for all three optimisation levels (standard, -O and -OO), 147 | # when the vast majority of apps only use the standard mode. As such, we can skip 148 | # regenerating/shipping those `.opt-{1,2}.pyc` files, reducing build output by 18MB. 149 | # 150 | # We use the `unchecked-hash` mode rather than `checked-hash` since it improves app startup 151 | # times by ~5%, and is only an issue if manual edits are made to the stdlib, which is not 152 | # something we support. 153 | # 154 | # See: 155 | # https://docs.python.org/3/reference/import.html#cached-bytecode-invalidation 156 | # https://docs.python.org/3/library/compileall.html 157 | # https://peps.python.org/pep-0488/ 158 | # https://peps.python.org/pep-0552/ 159 | find "${OUT_PREFIX}" -depth -type f -name "*.pyc" -delete 160 | # We use the Python binary from the original build output in the source directory, 161 | # rather than the installed binary in `$OUT_PREFIX`, for parity with the automatic 162 | # `.pyc` generation run by `make install`: 163 | # https://github.com/python/cpython/blob/v3.10.4/Makefile.pre.in#L1603-L1629 164 | LD_LIBRARY_PATH="${PWD}" ./python -m compileall -f --invalidation-mode unchecked-hash --workers 0 "${OUT_PREFIX}" 165 | 166 | # Support using Python 3 via the version-less `python` command, for parity with virtualenvs, 167 | # the Python Docker images and to also ensure buildpack Python shadows any installed system 168 | # Python, should that provide a version-less alias too. 169 | # This symlink must be relative, to ensure that the Python install remains relocatable. 170 | ln -srvT "${OUT_PREFIX}/bin/python3" "${OUT_PREFIX}/bin/python" 171 | 172 | du --max-depth 1 --human-readable "${OUT_PREFIX}" 173 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.10.4: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.10.5: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.10.6: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.10.7: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.7.13: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.7.14: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.8.13: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.8.14: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.9.12: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.9.13: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /builds/runtimes/python-3.9.14: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build Path: /app/.heroku/python 3 | 4 | # shellcheck source-path=SCRIPTDIR 5 | source "$(dirname "${0}")/python" 6 | -------------------------------------------------------------------------------- /etc/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | BP_NAME=${1:-"heroku/python"} 6 | 7 | curVersion=$(heroku buildpacks:versions "$BP_NAME" | awk 'FNR == 3 { print $1 }') 8 | newVersion="v$((curVersion + 1))" 9 | 10 | read -p "Deploy as version: $newVersion [y/n]? " choice 11 | case "$choice" in 12 | y|Y ) echo "";; 13 | n|N ) exit 0;; 14 | * ) exit 1;; 15 | esac 16 | 17 | git fetch origin 18 | originMain=$(git rev-parse origin/main) 19 | echo "Tagging commit $originMain with $newVersion... " 20 | git tag "$newVersion" "${originMain:?}" 21 | git push origin refs/tags/$newVersion 22 | 23 | echo -e "\nPublishing to the buildpack registry..." 24 | heroku buildpacks:publish "$BP_NAME" "$newVersion" 25 | echo 26 | heroku buildpacks:versions "${BP_NAME}" | head -n 3 27 | -------------------------------------------------------------------------------- /hatchet.json: -------------------------------------------------------------------------------- 1 | { 2 | "hatchet": { 3 | "directory": ".hatchet/" 4 | }, 5 | "heroku": [ 6 | "https://github.com/heroku/python-getting-started" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /hatchet.lock: -------------------------------------------------------------------------------- 1 | --- 2 | - - ".hatchet/repos/heroku/python-getting-started" 3 | - main 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Dependencies for generating/publishing Python binaries. 2 | bob-builder==0.0.19 3 | 4 | # Sub-dependencies of bob-builder. 5 | boto==2.49.0 6 | docopt==0.6.2 7 | -------------------------------------------------------------------------------- /spec/fixtures/ci_nose/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "environments": { 3 | "test": { 4 | "scripts": { 5 | "test": "nosetests" 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/ci_nose/requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | -------------------------------------------------------------------------------- /spec/fixtures/ci_nose/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.14 2 | -------------------------------------------------------------------------------- /spec/fixtures/django_broken_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gettingstarted.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /spec/fixtures/django_broken_project/requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | -------------------------------------------------------------------------------- /spec/fixtures/django_collectstatic_disabled_file/.heroku/collectstatic_disabled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/django_collectstatic_disabled_file/.heroku/collectstatic_disabled -------------------------------------------------------------------------------- /spec/fixtures/django_collectstatic_disabled_file/requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | -------------------------------------------------------------------------------- /spec/fixtures/hooks/bin/post_compile: -------------------------------------------------------------------------------- 1 | set -euo pipefail 2 | 3 | echo 'post_compile ran with env vars:' 4 | printenv | cut -d '=' -f 1 | sort 5 | -------------------------------------------------------------------------------- /spec/fixtures/hooks/bin/pre_compile: -------------------------------------------------------------------------------- 1 | set -euo pipefail 2 | 3 | echo 'pre_compile ran with env vars:' 4 | printenv | cut -d '=' -f 1 | sort 5 | -------------------------------------------------------------------------------- /spec/fixtures/hooks/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/hooks/requirements.txt -------------------------------------------------------------------------------- /spec/fixtures/nltk_dependency_and_nltk_txt/nltk.txt: -------------------------------------------------------------------------------- 1 | city_database 2 | stopwords 3 | -------------------------------------------------------------------------------- /spec/fixtures/nltk_dependency_and_nltk_txt/requirements.txt: -------------------------------------------------------------------------------- 1 | nltk 2 | -------------------------------------------------------------------------------- /spec/fixtures/nltk_dependency_only/requirements.txt: -------------------------------------------------------------------------------- 1 | nltk 2 | -------------------------------------------------------------------------------- /spec/fixtures/nltk_txt_but_no_dependency/nltk.txt: -------------------------------------------------------------------------------- 1 | city_database 2 | stopwords 3 | -------------------------------------------------------------------------------- /spec/fixtures/nltk_txt_but_no_dependency/requirements.txt: -------------------------------------------------------------------------------- 1 | # nltk mentioned in a comment, but the dependency is not present 2 | -------------------------------------------------------------------------------- /spec/fixtures/no_python_project_files/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/no_python_project_files/README.md -------------------------------------------------------------------------------- /spec/fixtures/pipenv_and_requirements_txt/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.10" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_and_requirements_txt/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "0975b9caa75f76ce36c7cf6ee6fc4b63510c0970ff8e322e0b5e6ee7af2d32e8" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_and_requirements_txt/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_and_runtime_txt/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.8" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_and_runtime_txt/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "e9eb0bba6cb0a97533d25cd3d68db92d4afd6b1dc88cd7a51607d8c34475eae0" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_and_runtime_txt/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.7 2 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_lockfile_out_of_sync/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.10" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_lockfile_out_of_sync/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "a95c318e4395270fbd1e0f52dd8f12185db4c3258d4e03dd80de54b7c7aad8b1" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "urllib3": { 18 | "hashes": [ 19 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 20 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 21 | ], 22 | "index": "pypi", 23 | "version": "==1.26.2" 24 | } 25 | }, 26 | "develop": {} 27 | } 28 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_no_lockfile/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_2.7/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "2.7" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_2.7/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "76bbc9cda0ef0576bfaac66930845fc9f994ca85ed19a19bc8c6f4f3a0b8efa9" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "2.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.10/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.10" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.10/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "0975b9caa75f76ce36c7cf6ee6fc4b63510c0970ff8e322e0b5e6ee7af2d32e8" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.5/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.5" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.5/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3a6323e6bc490e3ff501914a1a029cf76c96bfdf86cf8a48cd0d163a8cce4cb4" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.5" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.6/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.6" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.6/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "fe045e48581ffc6cb50af922ec04120b504f96ff6d6917fc535c8a9b76b8b71b" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.7/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.7/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b4a533c276cc9b89b506de9007fbe2210b33f8322ea81f62989117e5ec3adc54" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.8/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.8" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.8/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "e9eb0bba6cb0a97533d25cd3d68db92d4afd6b1dc88cd7a51607d8c34475eae0" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.9/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.9" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_3.9/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7776f372e3411c742b3e8a4209e67f0832f9e249d4c86ce28ece146afaef68d1" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_full_version/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_full_version = "3.10.4" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_full_version/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "355262e6cca53c94b8ef8837baf9556429f1c714da82c063c544856c11e047bc" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_full_version": "3.10.4" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", 22 | "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.9" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_full_version_invalid/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_full_version = "X.Y.Z" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_full_version_invalid/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7efd722f93856d46f698123972037594c6b8621e54b5a2be8ce71f5707a13c33" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_full_version": "X.Y.Z" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_version_invalid/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "^3.9" 13 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_version_invalid/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5ad0201581706317222acbcc0af4e0a03cb93dcdb0ce20833c27ada68df7f901" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "^3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "urllib3": { 20 | "hashes": [ 21 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 22 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.26.2" 26 | } 27 | }, 28 | "develop": {} 29 | } 30 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_version_unspecified/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | urllib3 = "*" 8 | 9 | [dev-packages] 10 | -------------------------------------------------------------------------------- /spec/fixtures/pipenv_python_version_unspecified/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "a95c318e4395270fbd1e0f52dd8f12185db4c3258d4e03dd80de54b7c7aad8b1" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "urllib3": { 18 | "hashes": [ 19 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 20 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 21 | ], 22 | "index": "pypi", 23 | "version": "==1.26.2" 24 | } 25 | }, 26 | "develop": {} 27 | } 28 | -------------------------------------------------------------------------------- /spec/fixtures/pypy_3.6/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/pypy_3.6/runtime.txt: -------------------------------------------------------------------------------- 1 | pypy3.6-7.3.2 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_2.7/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_2.7/runtime.txt: -------------------------------------------------------------------------------- 1 | python-2.7.18 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.10/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.10/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.7 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.10_outdated/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/python_3.10_outdated/requirements.txt -------------------------------------------------------------------------------- /spec/fixtures/python_3.10_outdated/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.5 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.4/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.4/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.4.10 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.5/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.5/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.5.10 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.6/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.6/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.15 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.6_outdated/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/python_3.6_outdated/requirements.txt -------------------------------------------------------------------------------- /spec/fixtures/python_3.6_outdated/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.14 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.7/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.7/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7.14 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.7_outdated/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/python_3.7_outdated/requirements.txt -------------------------------------------------------------------------------- /spec/fixtures/python_3.7_outdated/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7.12 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.8/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.8/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.14 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.8_outdated/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/python_3.8_outdated/requirements.txt -------------------------------------------------------------------------------- /spec/fixtures/python_3.8_outdated/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.12 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.9/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.9/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.14 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_3.9_outdated/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/python_3.9_outdated/requirements.txt -------------------------------------------------------------------------------- /spec/fixtures/python_3.9_outdated/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.12 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_version_invalid/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmatthes/heroku-buildpack-python/7ef6b1637730746aeb3f0ae42e481a5ff9837ed3/spec/fixtures/python_version_invalid/requirements.txt -------------------------------------------------------------------------------- /spec/fixtures/python_version_invalid/runtime.txt: -------------------------------------------------------------------------------- 1 | python-X.Y.Z 2 | -------------------------------------------------------------------------------- /spec/fixtures/python_version_unspecified/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_compiled/requirements.txt: -------------------------------------------------------------------------------- 1 | # Packages that rely on headers from the stack image (depending on wheel availability). 2 | cffi 3 | mysqlclient 4 | psycopg2 5 | pylibmc 6 | pysqlite3 7 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_django_latest/requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/bin/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This file is run by the inline buildpack, and tests that editable requirements are 4 | # usable by buildpacks that run after the Python buildpack during the build. 5 | 6 | set -euo pipefail 7 | 8 | BUILD_DIR="${1}" 9 | 10 | cd "${BUILD_DIR}" 11 | 12 | exec bin/test-entrypoints 13 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/bin/detect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This file is run by the inline buildpack. 4 | 5 | set -euo pipefail 6 | 7 | echo "Inline" 8 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/bin/post_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | exec bin/test-entrypoints 6 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/bin/test-entrypoints: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # List the filenames and contents of all .egg-link, .pth, and finder files in site-packages. 6 | find .heroku/python/lib*/*/site-packages/ -type f -and \( -name '*.egg-link' -or -name '*.pth' -or -name '__editable___*_finder.py' \) | sort | xargs -exec tail -n +1 7 | echo 8 | 9 | echo -n "Running entrypoint for the pyproject.toml-based local package: " 10 | local_package_pyproject_toml 11 | 12 | echo -n "Running entrypoint for the setup.py-based local package: " 13 | local_package_setup_py 14 | 15 | echo -n "Running entrypoint for the VCS package: " 16 | gunicorn --version 17 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/packages/local_package_pyproject_toml/local_package_pyproject_toml/__init__.py: -------------------------------------------------------------------------------- 1 | def hello(): 2 | print("Hello pyproject.toml!") 3 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/packages/local_package_pyproject_toml/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "local_package_pyproject_toml" 3 | version = "0.0.1" 4 | 5 | [project.scripts] 6 | local_package_pyproject_toml = "local_package_pyproject_toml:hello" 7 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/packages/local_package_setup_py/local_package_setup_py/__init__.py: -------------------------------------------------------------------------------- 1 | def hello(): 2 | print("Hello setup.py!") 3 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/packages/local_package_setup_py/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = local_package_setup_py 3 | version = 0.0.1 4 | 5 | [options] 6 | packages = local_package_setup_py 7 | 8 | [options.entry_points] 9 | console_scripts = 10 | local_package_setup_py = local_package_setup_py:hello 11 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/packages/local_package_setup_py/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_editable/requirements.txt: -------------------------------------------------------------------------------- 1 | # The packages have to be nested under `packages/` to work around: 2 | # https://github.com/pypa/setuptools/issues/3535 3 | -e ./packages/local_package_pyproject_toml 4 | -e ./packages/local_package_setup_py 5 | -e git+https://github.com/benoitc/gunicorn@20.1.0#egg=gunicorn 6 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_gdal/requirements.txt: -------------------------------------------------------------------------------- 1 | GDAL 2 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_git/requirements.txt: -------------------------------------------------------------------------------- 1 | # This relies upon the VCS binaries from the stack image. 2 | git+https://github.com/certifi/python-certifi 3 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_txt_and_setup_py/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/requirements_txt_and_setup_py/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='test', 5 | install_requires=['six'], 6 | ) 7 | -------------------------------------------------------------------------------- /spec/fixtures/runtime_txt_only/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.10.7 2 | -------------------------------------------------------------------------------- /spec/fixtures/runtime_txt_with_stray_whitespace/requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | -------------------------------------------------------------------------------- /spec/fixtures/runtime_txt_with_stray_whitespace/runtime.txt: -------------------------------------------------------------------------------- 1 | 2 | python-3.10.7 3 | -------------------------------------------------------------------------------- /spec/fixtures/setup_py_only/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='test', 5 | install_requires=['six'], 6 | ) 7 | -------------------------------------------------------------------------------- /spec/hatchet/ci_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.describe 'Heroku CI' do 6 | it 'works' do 7 | Hatchet::Runner.new('spec/fixtures/ci_nose').run_ci do |test_run| 8 | expect(test_run.output).to match('Downloading nose') 9 | expect(test_run.output).to match('OK') 10 | 11 | test_run.run_again 12 | 13 | expect(test_run.output).to match('installing from cache') 14 | expect(test_run.output).not_to match('Downloading nose') 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/hatchet/detect_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.describe 'Buildpack detection' do 6 | # This spec only tests cases where detection fails, since the success cases 7 | # are already tested in the specs for general buildpack functionality. 8 | 9 | context 'when there are no recognised Python project files' do 10 | let(:app) { Hatchet::Runner.new('spec/fixtures/no_python_project_files', allow_failure: true) } 11 | 12 | it 'fails detection' do 13 | app.deploy do |app| 14 | expect(clean_output(app.output)).to include(<<~OUTPUT) 15 | remote: -----> App not compatible with buildpack: #{DEFAULT_BUILDPACK_URL} 16 | remote: More info: https://devcenter.heroku.com/articles/buildpacks#detection-failure 17 | OUTPUT 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/hatchet/django_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.describe 'Django support' do 6 | describe 'collectstatic' do 7 | context 'when building a Django project' do 8 | let(:app) { Hatchet::Runner.new('python-getting-started') } 9 | 10 | it 'runs collectstatic' do 11 | app.deploy do |app| 12 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) 13 | remote: -----> \\$ python manage.py collectstatic --noinput 14 | remote: \\d+ static files copied to '/tmp/build_.*/staticfiles', \\d+ post-processed. 15 | REGEX 16 | end 17 | end 18 | end 19 | 20 | context 'when Django is installed but manage.py does not exist' do 21 | let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_django_latest') } 22 | 23 | it 'skips collectstatic' do 24 | app.deploy do |app| 25 | expect(app.output).to include('Skipping Django collectstatic since no manage.py file found.') 26 | expect(app.output).not_to include('manage.py collectstatic') 27 | end 28 | end 29 | end 30 | 31 | context 'when DISABLE_COLLECTSTATIC=1' do 32 | let(:app) do 33 | Hatchet::Runner.new('spec/fixtures/requirements_django_latest', config: { 'DISABLE_COLLECTSTATIC' => '1' }) 34 | end 35 | 36 | it 'skips collectstatic' do 37 | app.deploy do |app| 38 | expect(app.output).to include('Skipping Django collectstatic since the env var DISABLE_COLLECTSTATIC is set.') 39 | expect(app.output).not_to include('static files copied') 40 | end 41 | end 42 | end 43 | 44 | context 'when DISABLE_COLLECTSTATIC=0' do 45 | let(:app) { Hatchet::Runner.new('python-getting-started', config: { 'DISABLE_COLLECTSTATIC' => '0' }) } 46 | 47 | it 'still runs collectstatic' do 48 | app.deploy do |app| 49 | expect(app.output).to include('static files copied') 50 | end 51 | end 52 | end 53 | 54 | context 'when DISABLE_COLLECTSTATIC is null' do 55 | let(:app) { Hatchet::Runner.new('python-getting-started', config: { 'DISABLE_COLLECTSTATIC' => '' }) } 56 | 57 | it 'still runs collectstatic' do 58 | app.deploy do |app| 59 | expect(app.output).to include('static files copied') 60 | end 61 | end 62 | end 63 | 64 | context 'when .heroku/collectstatic_disabled exists' do 65 | let(:app) { Hatchet::Runner.new('spec/fixtures/django_collectstatic_disabled_file') } 66 | 67 | it 'skips collectstatic with a deprecation warning' do 68 | app.deploy do |app| 69 | expect(clean_output(app.output)).to include(<<~OUTPUT) 70 | remote: -----> Skipping Django collectstatic since the file '.heroku/collectstatic_disabled' exists. 71 | remote: ! This approach is deprecated, please set the env var DISABLE_COLLECTSTATIC=1 instead. 72 | OUTPUT 73 | expect(app.output).not_to include('static files copied') 74 | end 75 | end 76 | end 77 | 78 | context 'when building a broken Django project' do 79 | let(:app) { Hatchet::Runner.new('spec/fixtures/django_broken_project', allow_failure: true) } 80 | 81 | it 'fails collectstatic with an informative error message' do 82 | app.deploy do |app| 83 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) 84 | remote: -----> \\$ python manage.py collectstatic --noinput 85 | remote: Traceback \\(most recent call last\\): 86 | remote: .* 87 | remote: ModuleNotFoundError: No module named 'gettingstarted' 88 | remote: 89 | remote: ! Error while running '\\$ python manage.py collectstatic --noinput'. 90 | REGEX 91 | end 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/hatchet/getting_started_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.describe 'Python getting started project' do 6 | it 'builds successfully' do 7 | Hatchet::Runner.new('python-getting-started').deploy do |app| 8 | # TODO: Decide what to do with this test given it mostly duplicates the one in django_spec.rb. 9 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) 10 | remote: -----> \\$ python manage.py collectstatic --noinput 11 | remote: \\d+ static files copied to '/tmp/build_.*/staticfiles', \\d+ post-processed. 12 | REGEX 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/hatchet/hooks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.describe 'Compile hooks' do 6 | context 'when an app has bin/pre_compile and bin/post_compile scripts' do 7 | let(:app) { Hatchet::Runner.new('spec/fixtures/hooks', config: { 'SOME_APP_CONFIG_VAR' => '1' }) } 8 | 9 | it 'runs the hooks with the correct environment' do 10 | expected_env_vars = %w[ 11 | _ 12 | BIN_DIR 13 | BPLOG_PREFIX 14 | BUILD_DIR 15 | BUILDPACK_LOG_FILE 16 | CACHE_DIR 17 | C_INCLUDE_PATH 18 | CPLUS_INCLUDE_PATH 19 | DYNO 20 | ENV_DIR 21 | EXPORT_PATH 22 | HOME 23 | LANG 24 | LD_LIBRARY_PATH 25 | LIBRARY_PATH 26 | OLDPWD 27 | PATH 28 | PIP_NO_PYTHON_VERSION_WARNING 29 | PKG_CONFIG_PATH 30 | PROFILE_PATH 31 | PWD 32 | PYTHONUNBUFFERED 33 | REQUEST_ID 34 | SHLVL 35 | SOME_APP_CONFIG_VAR 36 | SOURCE_VERSION 37 | STACK 38 | ] 39 | 40 | app.deploy do |app| 41 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) 42 | remote: -----> Python app detected 43 | remote: -----> Running pre-compile hook 44 | remote: pre_compile ran with env vars: 45 | remote: #{expected_env_vars.join("\nremote: ")} 46 | remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} 47 | remote: .* 48 | remote: -----> Installing requirements with pip 49 | remote: -----> Running post-compile hook 50 | remote: post_compile ran with env vars: 51 | remote: #{expected_env_vars.join("\nremote: ")} 52 | REGEX 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/hatchet/nltk_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.describe 'NLTK corpora support' do 6 | context 'when the NLTK package is installed and nltk.txt is present' do 7 | let(:app) { Hatchet::Runner.new('spec/fixtures/nltk_dependency_and_nltk_txt') } 8 | 9 | it 'installs the specified NLTK corpora' do 10 | app.deploy do |app| 11 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) 12 | remote: -----> Downloading NLTK corpora… 13 | remote: -----> Downloading NLTK packages: city_database stopwords 14 | remote: .*: RuntimeWarning: 'nltk.downloader' found in sys.modules after import of package 'nltk', but prior to execution of 'nltk.downloader'; this may result in unpredictable behaviour 15 | remote: warn\\(RuntimeWarning\\(msg\\)\\) 16 | remote: \\[nltk_data\\] Downloading package city_database to 17 | remote: \\[nltk_data\\] /tmp/build_.*/.heroku/python/nltk_data... 18 | remote: \\[nltk_data\\] Unzipping corpora/city_database.zip. 19 | remote: \\[nltk_data\\] Downloading package stopwords to 20 | remote: \\[nltk_data\\] /tmp/build_.*/.heroku/python/nltk_data... 21 | remote: \\[nltk_data\\] Unzipping corpora/stopwords.zip. 22 | REGEX 23 | end 24 | end 25 | end 26 | 27 | context 'when the NLTK package is installed but there is no nltk.txt' do 28 | let(:app) { Hatchet::Runner.new('spec/fixtures/nltk_dependency_only') } 29 | 30 | it 'warns that nltk.txt was not found' do 31 | app.deploy do |app| 32 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) 33 | remote: -----> Downloading NLTK corpora… 34 | remote: ! 'nltk.txt' not found, not downloading any corpora 35 | remote: ! Learn more: https://devcenter.heroku.com/articles/python-nltk 36 | REGEX 37 | end 38 | end 39 | end 40 | 41 | context 'when only nltk.txt is present' do 42 | let(:app) { Hatchet::Runner.new('spec/fixtures/nltk_txt_but_no_dependency') } 43 | 44 | it 'does not try to install the specified NLTK corpora' do 45 | app.deploy do |app| 46 | expect(app.output.downcase).not_to include('nltk') 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/hatchet/pip_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.shared_examples 'installs successfully using pip' do 6 | it 'installs successfully using pip' do 7 | app.deploy do |app| 8 | expect(app.output).to include('Installing requirements with pip') 9 | expect(app.output).to include('Successfully installed') 10 | end 11 | end 12 | end 13 | 14 | RSpec.describe 'Pip support' do 15 | context 'when requirements.txt is unchanged since the last build' do 16 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified') } 17 | 18 | it 're-uses packages from the cache' do 19 | app.deploy do |app| 20 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) 21 | remote: -----> Python app detected 22 | remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} 23 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 24 | remote: -----> Installing python-#{DEFAULT_PYTHON_VERSION} 25 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 26 | remote: -----> Installing SQLite3 27 | remote: -----> Installing requirements with pip 28 | remote: Collecting urllib3 29 | remote: Downloading urllib3-.* 30 | remote: Installing collected packages: urllib3 31 | remote: Successfully installed urllib3-.* 32 | REGEX 33 | app.commit! 34 | app.push! 35 | expect(clean_output(app.output)).to include(<<~OUTPUT) 36 | remote: -----> Python app detected 37 | remote: -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} 38 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 39 | remote: -----> No change in requirements detected, installing from cache 40 | remote: -----> Using cached install of python-#{DEFAULT_PYTHON_VERSION} 41 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 42 | remote: -----> Installing SQLite3 43 | remote: -----> Installing requirements with pip 44 | remote: -----> Discovering process types 45 | OUTPUT 46 | end 47 | end 48 | end 49 | 50 | context 'when requirements.txt has changed since the last build' do 51 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified') } 52 | 53 | it 'clears the cache before installing the packages again' do 54 | app.deploy do |app| 55 | File.write('requirements.txt', 'six', mode: 'a') 56 | app.commit! 57 | app.push! 58 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) 59 | remote: -----> Python app detected 60 | remote: -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} 61 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 62 | remote: -----> Requirements file has been changed, clearing cached dependencies 63 | remote: -----> Installing python-#{DEFAULT_PYTHON_VERSION} 64 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 65 | remote: -----> Installing SQLite3 66 | remote: -----> Installing requirements with pip 67 | remote: Collecting urllib3 68 | remote: Downloading urllib3-.* 69 | remote: Collecting six 70 | remote: Downloading six-.* 71 | remote: Installing collected packages: urllib3, six 72 | remote: Successfully installed six-.* urllib3-.* 73 | REGEX 74 | end 75 | end 76 | end 77 | 78 | context 'when requirements.txt contains popular compiled packages' do 79 | let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_compiled') } 80 | 81 | include_examples 'installs successfully using pip' 82 | end 83 | 84 | context 'when requirements.txt contains Git requirements URLs' do 85 | let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_git') } 86 | 87 | include_examples 'installs successfully using pip' 88 | end 89 | 90 | context 'when requirements.txt contains editable requirements' do 91 | let(:buildpacks) { [:default, 'heroku-community/inline'] } 92 | let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_editable', buildpacks: buildpacks) } 93 | 94 | it 'rewrites .pth, .egg-link and finder paths correctly for hooks, later buildpacks, runtime and cached builds' do 95 | app.deploy do |app| 96 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) 97 | remote: Successfully installed gunicorn-20.1.0 local-package-pyproject-toml-0.0.1 local-package-setup-py-0.0.1 98 | remote: -----> Running post-compile hook 99 | remote: ==> .heroku/python/lib/python.*/site-packages/distutils-precedence.pth <== 100 | remote: .* 101 | remote: 102 | remote: ==> .heroku/python/lib/python.*/site-packages/easy-install.pth <== 103 | remote: /tmp/build_.*/packages/local_package_setup_py 104 | remote: /app/.heroku/src/gunicorn 105 | remote: 106 | remote: ==> .heroku/python/lib/python.*/site-packages/__editable___local_package_pyproject_toml_0_0_1_finder.py <== 107 | remote: .* 108 | remote: MAPPING = \{'local_package_pyproject_toml': '/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'\} 109 | remote: .* 110 | remote: 111 | remote: ==> .heroku/python/lib/python.*/site-packages/__editable__.local_package_pyproject_toml-0.0.1.pth <== 112 | remote: import __editable___.* 113 | remote: ==> .heroku/python/lib/python.*/site-packages/gunicorn.egg-link <== 114 | remote: /app/.heroku/src/gunicorn 115 | remote: . 116 | remote: ==> .heroku/python/lib/python.*/site-packages/local-package-setup-py.egg-link <== 117 | remote: /tmp/build_.*/packages/local_package_setup_py 118 | remote: . 119 | remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! 120 | remote: Running entrypoint for the setup.py-based local package: Hello setup.py! 121 | remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) 122 | remote: -----> Inline app detected 123 | remote: ==> .heroku/python/lib/python.*/site-packages/distutils-precedence.pth <== 124 | remote: .* 125 | remote: 126 | remote: ==> .heroku/python/lib/python.*/site-packages/easy-install.pth <== 127 | remote: /tmp/build_.*/packages/local_package_setup_py 128 | remote: /app/.heroku/src/gunicorn 129 | remote: 130 | remote: ==> .heroku/python/lib/python.*/site-packages/__editable___local_package_pyproject_toml_0_0_1_finder.py <== 131 | remote: .* 132 | remote: MAPPING = \{'local_package_pyproject_toml': '/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'\} 133 | remote: .* 134 | remote: 135 | remote: ==> .heroku/python/lib/python.*/site-packages/__editable__.local_package_pyproject_toml-0.0.1.pth <== 136 | remote: import __editable___.* 137 | remote: ==> .heroku/python/lib/python.*/site-packages/gunicorn.egg-link <== 138 | remote: /app/.heroku/src/gunicorn 139 | remote: . 140 | remote: ==> .heroku/python/lib/python.*/site-packages/local-package-setup-py.egg-link <== 141 | remote: /tmp/build_.*/packages/local_package_setup_py 142 | remote: . 143 | remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! 144 | remote: Running entrypoint for the setup.py-based local package: Hello setup.py! 145 | remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) 146 | REGEX 147 | 148 | # Test rewritten paths work at runtime. 149 | expect(app.run('bin/test-entrypoints')).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) 150 | ==> .heroku/python/lib/python.*/site-packages/distutils-precedence.pth <== 151 | .* 152 | 153 | ==> .heroku/python/lib/python.*/site-packages/easy-install.pth <== 154 | /app/packages/local_package_setup_py 155 | /app/.heroku/src/gunicorn 156 | 157 | ==> .heroku/python/lib/python.*/site-packages/__editable___local_package_pyproject_toml_0_0_1_finder.py <== 158 | .* 159 | MAPPING = \{'local_package_pyproject_toml': '/app/packages/local_package_pyproject_toml/local_package_pyproject_toml'\} 160 | .* 161 | 162 | ==> .heroku/python/lib/python.*/site-packages/__editable__.local_package_pyproject_toml-0.0.1.pth <== 163 | import __editable___.* 164 | ==> .heroku/python/lib/python.*/site-packages/gunicorn.egg-link <== 165 | /app/.heroku/src/gunicorn 166 | . 167 | ==> .heroku/python/lib/python.*/site-packages/local-package-setup-py.egg-link <== 168 | /app/packages/local_package_setup_py 169 | . 170 | Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! 171 | Running entrypoint for the setup.py-based local package: Hello setup.py! 172 | Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) 173 | REGEX 174 | 175 | # Test that the cached .pth files work correctly. 176 | app.commit! 177 | app.push! 178 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) 179 | remote: Successfully installed gunicorn-20.1.0 local-package-pyproject-toml-0.0.1 local-package-setup-py-0.0.1 180 | remote: -----> Running post-compile hook 181 | remote: ==> .heroku/python/lib/python.*/site-packages/distutils-precedence.pth <== 182 | remote: .* 183 | remote: 184 | remote: ==> .heroku/python/lib/python.*/site-packages/easy-install.pth <== 185 | remote: /app/.heroku/src/gunicorn 186 | remote: /tmp/build_.*/packages/local_package_setup_py 187 | remote: 188 | remote: ==> .heroku/python/lib/python.*/site-packages/__editable___local_package_pyproject_toml_0_0_1_finder.py <== 189 | remote: .* 190 | remote: MAPPING = \{'local_package_pyproject_toml': '/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'\} 191 | remote: .* 192 | remote: 193 | remote: ==> .heroku/python/lib/python.*/site-packages/__editable__.local_package_pyproject_toml-0.0.1.pth <== 194 | remote: import __editable___.* 195 | remote: ==> .heroku/python/lib/python.*/site-packages/gunicorn.egg-link <== 196 | remote: /app/.heroku/src/gunicorn 197 | remote: . 198 | remote: ==> .heroku/python/lib/python.*/site-packages/local-package-setup-py.egg-link <== 199 | remote: /tmp/build_.*/packages/local_package_setup_py 200 | remote: . 201 | remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! 202 | remote: Running entrypoint for the setup.py-based local package: Hello setup.py! 203 | remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) 204 | remote: -----> Inline app detected 205 | remote: ==> .heroku/python/lib/python.*/site-packages/distutils-precedence.pth <== 206 | remote: .* 207 | remote: 208 | remote: ==> .heroku/python/lib/python.*/site-packages/easy-install.pth <== 209 | remote: /app/.heroku/src/gunicorn 210 | remote: /tmp/build_.*/packages/local_package_setup_py 211 | remote: 212 | remote: ==> .heroku/python/lib/python.*/site-packages/__editable___local_package_pyproject_toml_0_0_1_finder.py <== 213 | remote: .* 214 | remote: MAPPING = \{'local_package_pyproject_toml': '/tmp/build_.*/packages/local_package_pyproject_toml/local_package_pyproject_toml'\} 215 | remote: .* 216 | remote: 217 | remote: ==> .heroku/python/lib/python.*/site-packages/__editable__.local_package_pyproject_toml-0.0.1.pth <== 218 | remote: import __editable___.* 219 | remote: ==> .heroku/python/lib/python.*/site-packages/gunicorn.egg-link <== 220 | remote: /app/.heroku/src/gunicorn 221 | remote: . 222 | remote: ==> .heroku/python/lib/python.*/site-packages/local-package-setup-py.egg-link <== 223 | remote: /tmp/build_.*/packages/local_package_setup_py 224 | remote: . 225 | remote: Running entrypoint for the pyproject.toml-based local package: Hello pyproject.toml! 226 | remote: Running entrypoint for the setup.py-based local package: Hello setup.py! 227 | remote: Running entrypoint for the VCS package: gunicorn \\(version 20.1.0\\) 228 | REGEX 229 | end 230 | end 231 | end 232 | 233 | context 'when there is only a setup.py' do 234 | let(:app) { Hatchet::Runner.new('spec/fixtures/setup_py_only') } 235 | 236 | it 'installs packages from setup.py' do 237 | app.deploy do |app| 238 | expect(app.output).to include('Running setup.py develop for test') 239 | expect(app.output).to include('Successfully installed six') 240 | end 241 | end 242 | end 243 | 244 | context 'when there is both a requirements.txt and setup.py' do 245 | let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_txt_and_setup_py') } 246 | 247 | it 'installs packages only from requirements.txt' do 248 | app.deploy do |app| 249 | expect(clean_output(app.output)).to include(<<~OUTPUT) 250 | remote: -----> Installing requirements with pip 251 | remote: Collecting urllib3 252 | OUTPUT 253 | expect(app.output).not_to include('Running setup.py develop') 254 | end 255 | end 256 | end 257 | 258 | context 'when requirements.txt contains GDAL but the GDAL C++ library is missing' do 259 | let(:app) { Hatchet::Runner.new('spec/fixtures/requirements_gdal', allow_failure: true) } 260 | 261 | it 'outputs instructions for how to resolve the build failure' do 262 | app.deploy do |app| 263 | expect(clean_output(app.output)).to include(<<~OUTPUT) 264 | remote: ! Hello! Package installation failed since the GDAL library was not found. 265 | remote: ! For GDAL, GEOS and PROJ support, use the Geo buildpack alongside the Python buildpack: 266 | remote: ! https://github.com/heroku/heroku-geo-buildpack 267 | remote: ! -- Much Love, Heroku. 268 | OUTPUT 269 | end 270 | end 271 | end 272 | end 273 | -------------------------------------------------------------------------------- /spec/hatchet/pipenv_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.shared_examples 'builds using Pipenv with the requested Python version' do |python_version| 6 | it "builds with Python #{python_version}" do 7 | app.deploy do |app| 8 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) 9 | remote: -----> Python app detected 10 | remote: -----> Using Python version specified in Pipfile.lock 11 | remote: -----> Installing python-#{python_version} 12 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 13 | remote: -----> Installing dependencies with Pipenv 2020.11.15 14 | remote: Installing dependencies from Pipfile.lock \\(.*\\)... 15 | remote: -----> Installing SQLite3 16 | REGEX 17 | end 18 | end 19 | end 20 | 21 | RSpec.shared_examples 'aborts the build with a runtime not available message (Pipenv)' do |requested_version| 22 | it 'aborts the build with a runtime not available message' do 23 | app.deploy do |app| 24 | expect(clean_output(app.output)).to include(<<~OUTPUT) 25 | remote: -----> Python app detected 26 | remote: -----> Using Python version specified in Pipfile.lock 27 | remote: ! Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}). 28 | remote: ! For supported versions, see: https://devcenter.heroku.com/articles/python-support 29 | OUTPUT 30 | end 31 | end 32 | end 33 | 34 | RSpec.describe 'Pipenv support' do 35 | context 'without a Pipfile.lock' do 36 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_no_lockfile') } 37 | 38 | it 'builds with the default Python version using just the Pipfile' do 39 | app.deploy do |app| 40 | expect(clean_output(app.output)).to include(<<~OUTPUT) 41 | remote: -----> Python app detected 42 | remote: ! No 'Pipfile.lock' found! We recommend you commit this into your repository. 43 | remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} 44 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 45 | remote: -----> Installing python-#{DEFAULT_PYTHON_VERSION} 46 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 47 | remote: -----> Installing dependencies with Pipenv 2020.11.15 48 | remote: Installing dependencies from Pipfile... 49 | remote: -----> Installing SQLite3 50 | OUTPUT 51 | end 52 | end 53 | end 54 | 55 | context 'with a Pipfile.lock but no Python version specified' do 56 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_unspecified') } 57 | 58 | it 'builds with the default Python version' do 59 | app.deploy do |app| 60 | expect(clean_output(app.output)).to include(<<~OUTPUT) 61 | remote: -----> Python app detected 62 | remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} 63 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 64 | remote: -----> Installing python-#{DEFAULT_PYTHON_VERSION} 65 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 66 | remote: -----> Installing dependencies with Pipenv 2020.11.15 67 | remote: Installing dependencies from Pipfile.lock (aad8b1)... 68 | remote: -----> Installing SQLite3 69 | OUTPUT 70 | end 71 | end 72 | end 73 | 74 | context 'with a Pipfile.lock containing python_version 2.7' do 75 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_2.7', allow_failure: true) } 76 | 77 | context 'when using Heroku-18', stacks: %w[heroku-18] do 78 | it 'aborts the build with an EOL message' do 79 | app.deploy do |app| 80 | expect(clean_output(app.output)).to include(<<~OUTPUT) 81 | remote: -----> Python app detected 82 | remote: -----> Using Python version specified in Pipfile.lock 83 | remote: ! 84 | remote: ! Python 2 reached upstream end-of-life on January 1st, 2020, and is 85 | remote: ! therefore no longer receiving security updates. Apps still using it 86 | remote: ! contain potential security vulnerabilities and should be upgraded to 87 | remote: ! Python 3 as soon as possible. 88 | remote: ! 89 | remote: ! In addition, Python 2 is only supported on our oldest stack, Heroku-18, 90 | remote: ! which is deprecated and reaches end-of-life on April 30th, 2023. 91 | remote: ! 92 | remote: ! As such, it is no longer supported by the latest version of this buildpack: 93 | remote: ! https://devcenter.heroku.com/changelog-items/2473 94 | remote: ! 95 | remote: ! You must either: 96 | remote: ! - Upgrade to Python 3 (recommended) 97 | remote: ! - Switch to the container stack and use the upstream legacy 'python:2.7' Docker images 98 | remote: ! - Switch to an older version of the Python buildpack (short term workaround only) 99 | remote: ! 100 | remote: ! For more details, see: 101 | remote: ! https://devcenter.heroku.com/articles/python-2-7-eol-faq 102 | remote: ! 103 | OUTPUT 104 | end 105 | end 106 | end 107 | 108 | context 'when using Heroku-20 or newer', stacks: %w[heroku-20 heroku-22] do 109 | # Python 2.7 is EOL, so has not been built for newer stacks. 110 | include_examples 'aborts the build with a runtime not available message (Pipenv)', '2.7.18' 111 | end 112 | end 113 | 114 | context 'with a Pipfile.lock containing python_version 3.5' do 115 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.5', allow_failure: true) } 116 | 117 | context 'when using Heroku-18', stacks: %w[heroku-18] do 118 | it 'aborts the build with an EOL message' do 119 | app.deploy do |app| 120 | expect(clean_output(app.output)).to include(<<~OUTPUT) 121 | remote: -----> Python app detected 122 | remote: -----> Using Python version specified in Pipfile.lock 123 | remote: ! 124 | remote: ! Python 3.5 reached upstream end-of-life on September 30th, 2020, and is 125 | remote: ! therefore no longer receiving security updates: 126 | remote: ! https://devguide.python.org/versions/#supported-versions 127 | remote: ! 128 | remote: ! As such, it is no longer supported by the latest version of this buildpack. 129 | remote: ! 130 | remote: ! Please upgrade to a newer Python version. See: 131 | remote: ! https://devcenter.heroku.com/articles/python-runtimes 132 | remote: ! 133 | OUTPUT 134 | end 135 | end 136 | end 137 | 138 | context 'when using Heroku-20 or newer', stacks: %w[heroku-20 heroku-22] do 139 | # Python 3.5 is EOL, so has not been built for newer stacks. 140 | include_examples 'aborts the build with a runtime not available message (Pipenv)', '3.5.10' 141 | end 142 | end 143 | 144 | context 'with a Pipfile.lock containing python_version 3.6' do 145 | let(:allow_failure) { false } 146 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.6', allow_failure: allow_failure) } 147 | 148 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 149 | it 'builds with the latest Python 3.6' do 150 | app.deploy do |app| 151 | expect(clean_output(app.output)).to include(<<~OUTPUT) 152 | remote: -----> Python app detected 153 | remote: -----> Using Python version specified in Pipfile.lock 154 | remote: ! 155 | remote: ! Python 3.6 reached upstream end-of-life on December 23rd, 2021, and is 156 | remote: ! therefore no longer receiving security updates: 157 | remote: ! https://devguide.python.org/versions/#supported-versions 158 | remote: ! 159 | remote: ! Upgrade to a newer Python version as soon as possible to keep your app secure. 160 | remote: ! See: https://devcenter.heroku.com/articles/python-runtimes 161 | remote: ! 162 | remote: -----> Installing python-#{LATEST_PYTHON_3_6} 163 | remote: -----> Installing pip 21.3.1, setuptools 59.6.0 and wheel 0.37.1 164 | remote: -----> Installing dependencies with Pipenv 2020.11.15 165 | remote: Installing dependencies from Pipfile.lock (b8b71b)... 166 | remote: -----> Installing SQLite3 167 | OUTPUT 168 | end 169 | end 170 | end 171 | 172 | context 'when using Heroku-22', stacks: %w[heroku-22] do 173 | let(:allow_failure) { true } 174 | 175 | # Python 3.6 is EOL, so has not been built for newer stacks. 176 | include_examples 'aborts the build with a runtime not available message (Pipenv)', LATEST_PYTHON_3_6 177 | end 178 | end 179 | 180 | context 'with a Pipfile.lock containing python_version 3.7' do 181 | let(:allow_failure) { false } 182 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.7', allow_failure: allow_failure) } 183 | 184 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 185 | include_examples 'builds using Pipenv with the requested Python version', LATEST_PYTHON_3_7 186 | end 187 | 188 | context 'when using Heroku-22', stacks: %w[heroku-22] do 189 | let(:allow_failure) { true } 190 | 191 | # Python 3.7 is in the security fix only stage of its lifecycle, so has not been built for newer stacks. 192 | include_examples 'aborts the build with a runtime not available message (Pipenv)', LATEST_PYTHON_3_7 193 | end 194 | end 195 | 196 | context 'with a Pipfile.lock containing python_version 3.8' do 197 | let(:allow_failure) { false } 198 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.8', allow_failure: allow_failure) } 199 | 200 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 201 | include_examples 'builds using Pipenv with the requested Python version', LATEST_PYTHON_3_8 202 | end 203 | 204 | context 'when using Heroku-22', stacks: %w[heroku-22] do 205 | let(:allow_failure) { true } 206 | 207 | # Python 3.8 is in the security fix only stage of its lifecycle, so has not been built for newer stacks. 208 | include_examples 'aborts the build with a runtime not available message (Pipenv)', LATEST_PYTHON_3_8 209 | end 210 | end 211 | 212 | context 'with a Pipfile.lock containing python_version 3.9' do 213 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.9') } 214 | 215 | include_examples 'builds using Pipenv with the requested Python version', LATEST_PYTHON_3_9 216 | end 217 | 218 | context 'with a Pipfile.lock containing python_version 3.10' do 219 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_3.10') } 220 | 221 | include_examples 'builds using Pipenv with the requested Python version', LATEST_PYTHON_3_10 222 | end 223 | 224 | context 'with a Pipfile.lock containing python_full_version 3.10.4' do 225 | let(:allow_failure) { false } 226 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_full_version', allow_failure: allow_failure) } 227 | 228 | it 'builds with the outdated Python version specified' do 229 | app.deploy do |app| 230 | expect(clean_output(app.output)).to include(<<~OUTPUT) 231 | remote: -----> Python app detected 232 | remote: -----> Using Python version specified in Pipfile.lock 233 | remote: ! 234 | remote: ! A Python security update is available! Upgrade as soon as possible to: python-#{LATEST_PYTHON_3_10} 235 | remote: ! See: https://devcenter.heroku.com/articles/python-runtimes 236 | remote: ! 237 | remote: -----> Installing python-3.10.4 238 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 239 | remote: -----> Installing dependencies with Pipenv 2020.11.15 240 | remote: Installing dependencies from Pipfile.lock (e047bc)... 241 | remote: -----> Installing SQLite3 242 | OUTPUT 243 | end 244 | end 245 | end 246 | 247 | context 'with a Pipfile.lock containing an invalid python_version', 248 | skip: 'unknown python_version values are currently ignored (W-8104668)' do 249 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_version_invalid', allow_failure: true) } 250 | 251 | it 'fails the build' do 252 | app.deploy do |app| 253 | expect(clean_output(app.output)).to include(<<~OUTPUT) 254 | remote: -----> Python app detected 255 | remote: -----> Using Python version specified in Pipfile.lock 256 | remote: ! Requested runtime '^3.9' is not available for this stack (#{app.stack}). 257 | remote: ! For supported versions, see: https://devcenter.heroku.com/articles/python-support 258 | OUTPUT 259 | end 260 | end 261 | end 262 | 263 | context 'with a Pipfile.lock containing an invalid python_full_version' do 264 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_python_full_version_invalid', allow_failure: true) } 265 | 266 | it 'fails the build' do 267 | app.deploy do |app| 268 | expect(clean_output(app.output)).to include(<<~OUTPUT) 269 | remote: -----> Python app detected 270 | remote: -----> Using Python version specified in Pipfile.lock 271 | remote: ! Requested runtime 'python-X.Y.Z' is not available for this stack (#{app.stack}). 272 | remote: ! For supported versions, see: https://devcenter.heroku.com/articles/python-support 273 | OUTPUT 274 | end 275 | end 276 | end 277 | 278 | context 'when there is a both a Pipfile.lock python_version and a runtime.txt' do 279 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_and_runtime_txt') } 280 | 281 | it 'builds with the Python version from runtime.txt' do 282 | app.deploy do |app| 283 | expect(clean_output(app.output)).to include(<<~OUTPUT) 284 | remote: -----> Python app detected 285 | remote: -----> Using Python version specified in runtime.txt 286 | remote: -----> Installing python-#{LATEST_PYTHON_3_10} 287 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 288 | remote: -----> Installing dependencies with Pipenv 2020.11.15 289 | remote: Installing dependencies from Pipfile.lock (75eae0)... 290 | remote: -----> Installing SQLite3 291 | OUTPUT 292 | end 293 | end 294 | end 295 | 296 | context 'when there is both a Pipfile.lock and a requirements.txt' do 297 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_and_requirements_txt') } 298 | 299 | it 'builds with Pipenv rather than pip' do 300 | app.deploy do |app| 301 | expect(clean_output(app.output)).to include(<<~OUTPUT) 302 | remote: -----> Python app detected 303 | remote: -----> Using Python version specified in Pipfile.lock 304 | remote: -----> Installing python-#{LATEST_PYTHON_3_10} 305 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 306 | remote: -----> Installing dependencies with Pipenv 2020.11.15 307 | remote: Installing dependencies from Pipfile.lock (2d32e8)... 308 | remote: -----> Installing SQLite3 309 | OUTPUT 310 | end 311 | end 312 | end 313 | 314 | context 'when the Pipfile.lock is out of sync with Pipfile' do 315 | let(:app) { Hatchet::Runner.new('spec/fixtures/pipenv_lockfile_out_of_sync', allow_failure: true) } 316 | 317 | it 'fails the build' do 318 | app.deploy do |app| 319 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX, Regexp::MULTILINE)) 320 | remote: -----> Python app detected 321 | remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} 322 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 323 | remote: -----> Installing python-#{DEFAULT_PYTHON_VERSION} 324 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 325 | remote: -----> Installing dependencies with Pipenv 2020.11.15 326 | remote: Your Pipfile.lock \\(aad8b1\\) is out of date. Expected: \\(2d32e8\\). 327 | remote: \\[DeployException\\]: .* 328 | remote: ERROR:: Aborting deploy 329 | REGEX 330 | end 331 | end 332 | end 333 | end 334 | -------------------------------------------------------------------------------- /spec/hatchet/python_update_warning_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.shared_examples 'warns there is a Python update available' do |requested_version, latest_version| 6 | it 'warns there is a Python update available' do 7 | app.deploy do |app| 8 | expect(clean_output(app.output)).to include(<<~OUTPUT) 9 | remote: -----> Python app detected 10 | remote: -----> Using Python version specified in runtime.txt 11 | remote: ! 12 | remote: ! A Python security update is available! Upgrade as soon as possible to: python-#{latest_version} 13 | remote: ! See: https://devcenter.heroku.com/articles/python-runtimes 14 | remote: ! 15 | remote: -----> Installing python-#{requested_version} 16 | OUTPUT 17 | end 18 | end 19 | end 20 | 21 | RSpec.shared_examples 'warns about both EOL major version and the patch update' do |requested_version, latest_version| 22 | it 'warns there is a Python update available' do 23 | app.deploy do |app| 24 | expect(clean_output(app.output)).to match(Regexp.new(<<~REGEX)) 25 | remote: -----> Python app detected 26 | remote: -----> Using Python version specified in runtime.txt 27 | remote: ! 28 | remote: ! Python .* reached upstream end-of-life on .*, and is 29 | remote: ! therefore no longer receiving security updates: 30 | remote: ! https://devguide.python.org/versions/#supported-versions 31 | remote: ! 32 | remote: ! Upgrade to a newer Python version as soon as possible to keep your app secure. 33 | remote: ! See: https://devcenter.heroku.com/articles/python-runtimes 34 | remote: ! 35 | remote: ! 36 | remote: ! A Python security update is available! Upgrade as soon as possible to: python-#{latest_version} 37 | remote: ! See: https://devcenter.heroku.com/articles/python-runtimes 38 | remote: ! 39 | remote: -----> Installing python-#{requested_version} 40 | REGEX 41 | end 42 | end 43 | end 44 | 45 | RSpec.shared_examples 'aborts the build without showing an update warning' do |requested_version| 46 | it 'aborts the build without showing an update warning' do 47 | app.deploy do |app| 48 | expect(clean_output(app.output)).to include(<<~OUTPUT) 49 | remote: -----> Python app detected 50 | remote: -----> Using Python version specified in runtime.txt 51 | remote: ! Requested runtime 'python-#{requested_version}' is not available for this stack (#{app.stack}). 52 | remote: ! For supported versions, see: https://devcenter.heroku.com/articles/python-support 53 | OUTPUT 54 | end 55 | end 56 | end 57 | 58 | RSpec.describe 'Python update warnings' do 59 | context 'with a runtime.txt containing python-3.6.14' do 60 | let(:allow_failure) { false } 61 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.6_outdated', allow_failure: allow_failure) } 62 | 63 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 64 | include_examples 'warns about both EOL major version and the patch update', '3.6.14', LATEST_PYTHON_3_6 65 | end 66 | 67 | context 'when using Heroku-22', stacks: %w[heroku-22] do 68 | let(:allow_failure) { true } 69 | 70 | include_examples 'aborts the build without showing an update warning', '3.6.14' 71 | end 72 | end 73 | 74 | context 'with a runtime.txt containing python-3.7.12' do 75 | let(:allow_failure) { false } 76 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.7_outdated', allow_failure: allow_failure) } 77 | 78 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 79 | include_examples 'warns there is a Python update available', '3.7.12', LATEST_PYTHON_3_7 80 | end 81 | 82 | context 'when using Heroku-22', stacks: %w[heroku-22] do 83 | let(:allow_failure) { true } 84 | 85 | include_examples 'aborts the build without showing an update warning', '3.7.12' 86 | end 87 | end 88 | 89 | context 'with a runtime.txt containing python-3.8.12' do 90 | let(:allow_failure) { false } 91 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.8_outdated', allow_failure: allow_failure) } 92 | 93 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 94 | include_examples 'warns there is a Python update available', '3.8.12', LATEST_PYTHON_3_8 95 | end 96 | 97 | context 'when using Heroku-22', stacks: %w[heroku-22] do 98 | let(:allow_failure) { true } 99 | 100 | include_examples 'aborts the build without showing an update warning', '3.8.12' 101 | end 102 | end 103 | 104 | context 'with a runtime.txt containing python-3.9.12' do 105 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.9_outdated') } 106 | 107 | include_examples 'warns there is a Python update available', '3.9.12', LATEST_PYTHON_3_9 108 | end 109 | 110 | context 'with a runtime.txt containing python-3.10.5' do 111 | let(:allow_failure) { false } 112 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.10_outdated', allow_failure: allow_failure) } 113 | 114 | include_examples 'warns there is a Python update available', '3.10.5', LATEST_PYTHON_3_10 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/hatchet/python_version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.shared_examples 'builds with the requested Python version' do |python_version| 6 | it "builds with Python #{python_version}" do 7 | app.deploy do |app| 8 | expect(clean_output(app.output)).to include(<<~OUTPUT) 9 | remote: -----> Python app detected 10 | remote: -----> Using Python version specified in runtime.txt 11 | remote: -----> Installing python-#{python_version} 12 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 13 | remote: -----> Installing SQLite3 14 | remote: -----> Installing requirements with pip 15 | remote: Collecting urllib3 16 | OUTPUT 17 | expect(app.run('python -V')).to include("Python #{python_version}") 18 | end 19 | end 20 | end 21 | 22 | RSpec.shared_examples 'aborts the build with a runtime not available message' do |requested_runtime| 23 | it 'aborts the build with a runtime not available message' do 24 | app.deploy do |app| 25 | expect(clean_output(app.output)).to include(<<~OUTPUT) 26 | remote: -----> Python app detected 27 | remote: -----> Using Python version specified in runtime.txt 28 | remote: ! Requested runtime '#{requested_runtime}' is not available for this stack (#{app.stack}). 29 | remote: ! For supported versions, see: https://devcenter.heroku.com/articles/python-support 30 | OUTPUT 31 | end 32 | end 33 | end 34 | 35 | RSpec.describe 'Python version support' do 36 | context 'when no Python version is specified' do 37 | let(:buildpacks) { [:default] } 38 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified', buildpacks: buildpacks) } 39 | 40 | context 'with a new app' do 41 | it 'builds with the default Python version' do 42 | app.deploy do |app| 43 | expect(clean_output(app.output)).to include(<<~OUTPUT) 44 | remote: -----> Python app detected 45 | remote: -----> No Python version was specified. Using the buildpack default: python-#{DEFAULT_PYTHON_VERSION} 46 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 47 | remote: -----> Installing python-#{DEFAULT_PYTHON_VERSION} 48 | OUTPUT 49 | end 50 | end 51 | end 52 | 53 | context 'with an app last built using an older default Python version' do 54 | # This test performs an initial build using an older buildpack version, followed 55 | # by a build using the current version. This ensures that the current buildpack 56 | # can successfully read the version metadata written to the build cache in the past. 57 | let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#v213'] } 58 | 59 | it 'builds with the same Python version as the last build' do 60 | app.deploy do |app| 61 | update_buildpacks(app, [:default]) 62 | app.commit! 63 | app.push! 64 | expect(clean_output(app.output)).to include(<<~OUTPUT) 65 | remote: -----> Python app detected 66 | remote: -----> No Python version was specified. Using the same version as the last build: python-3.10.5 67 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 68 | remote: ! 69 | remote: ! A Python security update is available! Upgrade as soon as possible to: python-#{LATEST_PYTHON_3_10} 70 | remote: ! See: https://devcenter.heroku.com/articles/python-runtimes 71 | remote: ! 72 | remote: -----> No change in requirements detected, installing from cache 73 | remote: -----> Using cached install of python-3.10.5 74 | OUTPUT 75 | expect(app.run('python -V')).to include('Python 3.10.5') 76 | end 77 | end 78 | end 79 | end 80 | 81 | context 'when runtime.txt contains python-2.7.18' do 82 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_2.7', allow_failure: true) } 83 | 84 | context 'when using Heroku-18', stacks: %w[heroku-18] do 85 | it 'aborts the build with an EOL message' do 86 | app.deploy do |app| 87 | expect(clean_output(app.output)).to include(<<~OUTPUT) 88 | remote: -----> Python app detected 89 | remote: -----> Using Python version specified in runtime.txt 90 | remote: ! 91 | remote: ! Python 2 reached upstream end-of-life on January 1st, 2020, and is 92 | remote: ! therefore no longer receiving security updates. Apps still using it 93 | remote: ! contain potential security vulnerabilities and should be upgraded to 94 | remote: ! Python 3 as soon as possible. 95 | remote: ! 96 | remote: ! In addition, Python 2 is only supported on our oldest stack, Heroku-18, 97 | remote: ! which is deprecated and reaches end-of-life on April 30th, 2023. 98 | remote: ! 99 | remote: ! As such, it is no longer supported by the latest version of this buildpack: 100 | remote: ! https://devcenter.heroku.com/changelog-items/2473 101 | remote: ! 102 | remote: ! You must either: 103 | remote: ! - Upgrade to Python 3 (recommended) 104 | remote: ! - Switch to the container stack and use the upstream legacy 'python:2.7' Docker images 105 | remote: ! - Switch to an older version of the Python buildpack (short term workaround only) 106 | remote: ! 107 | remote: ! For more details, see: 108 | remote: ! https://devcenter.heroku.com/articles/python-2-7-eol-faq 109 | remote: ! 110 | OUTPUT 111 | end 112 | end 113 | end 114 | 115 | context 'when using Heroku-20 or newer', stacks: %w[heroku-20 heroku-22] do 116 | # Python 2.7 is EOL, so has not been built for newer stacks. 117 | include_examples 'aborts the build with a runtime not available message', 'python-2.7.18' 118 | end 119 | end 120 | 121 | context 'when runtime.txt contains python-3.4.10' do 122 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.4', allow_failure: true) } 123 | 124 | context 'when using Heroku-18', stacks: %w[heroku-18] do 125 | it 'aborts the build with an EOL message' do 126 | app.deploy do |app| 127 | expect(clean_output(app.output)).to include(<<~OUTPUT) 128 | remote: -----> Python app detected 129 | remote: -----> Using Python version specified in runtime.txt 130 | remote: ! 131 | remote: ! Python 3.4 reached upstream end-of-life on March 18th, 2019, and is 132 | remote: ! therefore no longer receiving security updates: 133 | remote: ! https://devguide.python.org/versions/#supported-versions 134 | remote: ! 135 | remote: ! As such, it is no longer supported by the latest version of this buildpack. 136 | remote: ! 137 | remote: ! Please upgrade to a newer Python version. See: 138 | remote: ! https://devcenter.heroku.com/articles/python-runtimes 139 | remote: ! 140 | OUTPUT 141 | end 142 | end 143 | end 144 | 145 | context 'when using Heroku-20 or newer', stacks: %w[heroku-20 heroku-22] do 146 | # Python 3.4 is EOL, so has not been built for newer stacks. 147 | include_examples 'aborts the build with a runtime not available message', 'python-3.4.10' 148 | end 149 | end 150 | 151 | context 'when runtime.txt contains python-3.5.10' do 152 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.5', allow_failure: true) } 153 | 154 | context 'when using Heroku-18', stacks: %w[heroku-18] do 155 | it 'aborts the build with an EOL message' do 156 | app.deploy do |app| 157 | expect(clean_output(app.output)).to include(<<~OUTPUT) 158 | remote: -----> Python app detected 159 | remote: -----> Using Python version specified in runtime.txt 160 | remote: ! 161 | remote: ! Python 3.5 reached upstream end-of-life on September 30th, 2020, and is 162 | remote: ! therefore no longer receiving security updates: 163 | remote: ! https://devguide.python.org/versions/#supported-versions 164 | remote: ! 165 | remote: ! As such, it is no longer supported by the latest version of this buildpack. 166 | remote: ! 167 | remote: ! Please upgrade to a newer Python version. See: 168 | remote: ! https://devcenter.heroku.com/articles/python-runtimes 169 | remote: ! 170 | OUTPUT 171 | end 172 | end 173 | end 174 | 175 | context 'when using Heroku-20 or newer', stacks: %w[heroku-20 heroku-22] do 176 | # Python 3.5 is EOL, so has not been built for newer stacks. 177 | include_examples 'aborts the build with a runtime not available message', 'python-3.5.10' 178 | end 179 | end 180 | 181 | context 'when runtime.txt contains python-3.6.15' do 182 | let(:allow_failure) { false } 183 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.6', allow_failure: allow_failure) } 184 | 185 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 186 | it 'builds with Python 3.6.15' do 187 | app.deploy do |app| 188 | expect(clean_output(app.output)).to include(<<~OUTPUT) 189 | remote: -----> Python app detected 190 | remote: -----> Using Python version specified in runtime.txt 191 | remote: ! 192 | remote: ! Python 3.6 reached upstream end-of-life on December 23rd, 2021, and is 193 | remote: ! therefore no longer receiving security updates: 194 | remote: ! https://devguide.python.org/versions/#supported-versions 195 | remote: ! 196 | remote: ! Upgrade to a newer Python version as soon as possible to keep your app secure. 197 | remote: ! See: https://devcenter.heroku.com/articles/python-runtimes 198 | remote: ! 199 | remote: -----> Installing python-#{LATEST_PYTHON_3_6} 200 | remote: -----> Installing pip 21.3.1, setuptools 59.6.0 and wheel 0.37.1 201 | remote: -----> Installing SQLite3 202 | remote: -----> Installing requirements with pip 203 | remote: Collecting urllib3 204 | OUTPUT 205 | expect(app.run('python -V')).to include("Python #{LATEST_PYTHON_3_6}") 206 | end 207 | end 208 | end 209 | 210 | context 'when using Heroku-22', stacks: %w[heroku-22] do 211 | let(:allow_failure) { true } 212 | 213 | # Python 3.6 is EOL, so has not been built for newer stacks. 214 | include_examples 'aborts the build with a runtime not available message', "python-#{LATEST_PYTHON_3_6}" 215 | end 216 | end 217 | 218 | context 'when runtime.txt contains python-3.7.14' do 219 | let(:allow_failure) { false } 220 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.7', allow_failure: allow_failure) } 221 | 222 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 223 | include_examples 'builds with the requested Python version', LATEST_PYTHON_3_7 224 | end 225 | 226 | context 'when using Heroku-22', stacks: %w[heroku-22] do 227 | let(:allow_failure) { true } 228 | 229 | # Python 3.7 is in the security fix only stage of its lifecycle, so has not been built for newer stacks. 230 | include_examples 'aborts the build with a runtime not available message', "python-#{LATEST_PYTHON_3_7}" 231 | end 232 | end 233 | 234 | context 'when runtime.txt contains python-3.8.14' do 235 | let(:allow_failure) { false } 236 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.8', allow_failure: allow_failure) } 237 | 238 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 239 | include_examples 'builds with the requested Python version', LATEST_PYTHON_3_8 240 | end 241 | 242 | context 'when using Heroku-22', stacks: %w[heroku-22] do 243 | let(:allow_failure) { true } 244 | 245 | # Python 3.8 is in the security fix only stage of its lifecycle, so has not been built for newer stacks. 246 | include_examples 'aborts the build with a runtime not available message', "python-#{LATEST_PYTHON_3_8}" 247 | end 248 | end 249 | 250 | context 'when runtime.txt contains python-3.9.14' do 251 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.9') } 252 | 253 | include_examples 'builds with the requested Python version', LATEST_PYTHON_3_9 254 | end 255 | 256 | context 'when runtime.txt contains python-3.10.7' do 257 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.10') } 258 | 259 | include_examples 'builds with the requested Python version', LATEST_PYTHON_3_10 260 | end 261 | 262 | context 'when runtime.txt contains pypy3.6-7.3.2' do 263 | let(:app) { Hatchet::Runner.new('spec/fixtures/pypy_3.6', allow_failure: true) } 264 | 265 | context 'when using Heroku-18 or Heroku-20', stacks: %w[heroku-18 heroku-20] do 266 | it 'aborts the build with a sunset message' do 267 | app.deploy do |app| 268 | expect(clean_output(app.output)).to include(<<~OUTPUT) 269 | remote: -----> Python app detected 270 | remote: -----> Using Python version specified in runtime.txt 271 | remote: ! 272 | remote: ! PyPy is no longer supported by the latest version of this buildpack. 273 | remote: ! 274 | remote: ! Please switch to one of the supported CPython versions by updating your 275 | remote: ! runtime.txt file. See: 276 | remote: ! https://devcenter.heroku.com/articles/python-support 277 | remote: ! 278 | OUTPUT 279 | end 280 | end 281 | end 282 | 283 | context 'when using Heroku-22', stacks: %w[heroku-22] do 284 | # The beta PyPy support is deprecated and so not being made available for new stacks. 285 | include_examples 'aborts the build with a runtime not available message', 'pypy3.6-7.3.2' 286 | end 287 | end 288 | 289 | context 'when runtime.txt contains an invalid python version string' do 290 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_invalid', allow_failure: true) } 291 | 292 | include_examples 'aborts the build with a runtime not available message', 'python-X.Y.Z' 293 | end 294 | 295 | context 'when runtime.txt contains stray whitespace' do 296 | let(:app) { Hatchet::Runner.new('spec/fixtures/runtime_txt_with_stray_whitespace') } 297 | 298 | include_examples 'builds with the requested Python version', LATEST_PYTHON_3_10 299 | end 300 | 301 | context 'when there is only a runtime.txt and no requirements.txt', skip: 'not currently supported (W-8720280)' do 302 | let(:app) { Hatchet::Runner.new('spec/fixtures/runtime_txt_only', allow_failure: true) } 303 | 304 | include_examples 'builds with the requested Python version', LATEST_PYTHON_3_10 305 | end 306 | 307 | context 'when the requested Python version has changed since the last build' do 308 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_3.9') } 309 | 310 | it 'builds with the new Python version after removing the old install' do 311 | app.deploy do |app| 312 | File.write('runtime.txt', "python-#{LATEST_PYTHON_3_10}") 313 | app.commit! 314 | app.push! 315 | # TODO: The output shouldn't say "installing from cache", since it's not. 316 | expect(clean_output(app.output)).to include(<<~OUTPUT) 317 | remote: -----> Python app detected 318 | remote: -----> Using Python version specified in runtime.txt 319 | remote: -----> Python version has changed from python-#{LATEST_PYTHON_3_9} to python-#{LATEST_PYTHON_3_10}, clearing cache 320 | remote: -----> No change in requirements detected, installing from cache 321 | remote: -----> Installing python-#{LATEST_PYTHON_3_10} 322 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 323 | remote: -----> Installing SQLite3 324 | remote: -----> Installing requirements with pip 325 | remote: Collecting urllib3 326 | OUTPUT 327 | end 328 | end 329 | end 330 | end 331 | -------------------------------------------------------------------------------- /spec/hatchet/stack_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../spec_helper' 4 | 5 | RSpec.describe 'Stack changes' do 6 | context 'when the stack is upgraded from Heroku-20 to Heroku-22', stacks: %w[heroku-20] do 7 | # This test performs an initial build using an older buildpack version, followed 8 | # by a build using the current version. This ensures that the current buildpack 9 | # can successfully read the stack metadata written to the build cache in the past. 10 | # The buildpack version chosen is one which had an older default Python version, so 11 | # we can also prove that clearing the cache didn't lose the Python version metadata. 12 | let(:buildpacks) { ['https://github.com/heroku/heroku-buildpack-python#v213'] } 13 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified', buildpacks: buildpacks) } 14 | 15 | it 'clears the cache before installing again whilst preserving the sticky Python version' do 16 | app.deploy do |app| 17 | expect(app.output).to include('Building on the Heroku-20 stack') 18 | app.update_stack('heroku-22') 19 | update_buildpacks(app, [:default]) 20 | app.commit! 21 | app.push! 22 | # TODO: The requirements output shouldn't say "installing from cache", since it's not. 23 | expect(clean_output(app.output)).to include(<<~OUTPUT) 24 | remote: -----> Python app detected 25 | remote: -----> No Python version was specified. Using the same version as the last build: python-3.10.5 26 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 27 | remote: ! 28 | remote: ! A Python security update is available! Upgrade as soon as possible to: python-#{LATEST_PYTHON_3_10} 29 | remote: ! See: https://devcenter.heroku.com/articles/python-runtimes 30 | remote: ! 31 | remote: -----> Stack has changed from heroku-20 to heroku-22, clearing cache 32 | remote: -----> No change in requirements detected, installing from cache 33 | remote: -----> Installing python-3.10.5 34 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 35 | remote: -----> Installing SQLite3 36 | remote: -----> Installing requirements with pip 37 | remote: Collecting urllib3 38 | OUTPUT 39 | end 40 | end 41 | end 42 | 43 | context 'when the stack is downgraded from Heroku-22 to Heroku-20', stacks: %w[heroku-22] do 44 | let(:app) { Hatchet::Runner.new('spec/fixtures/python_version_unspecified') } 45 | 46 | it 'clears the cache before installing again' do 47 | app.deploy do |app| 48 | expect(app.output).to include('Building on the Heroku-22 stack') 49 | app.update_stack('heroku-20') 50 | app.commit! 51 | app.push! 52 | # TODO: Stop using Python scripts before Python is installed (or else ensure system 53 | # Python is always used) to avoid the glibc errors below. 54 | expect(clean_output(app.output)).to include(<<~OUTPUT) 55 | remote: -----> Python app detected 56 | remote: -----> No Python version was specified. Using the same version as the last build: python-#{DEFAULT_PYTHON_VERSION} 57 | remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes 58 | remote: python: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by python) 59 | remote: python: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.35' not found (required by /app/.heroku/python/lib/libpython3.10.so.1.0) 60 | remote: python: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found (required by /app/.heroku/python/lib/libpython3.10.so.1.0) 61 | remote: python: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by /app/.heroku/python/lib/libpython3.10.so.1.0) 62 | remote: python: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by /app/.heroku/python/lib/libpython3.10.so.1.0) 63 | remote: -----> Stack has changed from heroku-22 to heroku-20, clearing cache 64 | remote: -----> No change in requirements detected, installing from cache 65 | remote: -----> Installing python-#{DEFAULT_PYTHON_VERSION} 66 | remote: -----> Installing pip 22.2.2, setuptools 63.4.3 and wheel 0.37.1 67 | remote: -----> Installing SQLite3 68 | remote: -----> Installing requirements with pip 69 | remote: Collecting urllib3 70 | OUTPUT 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['HATCHET_BUILDPACK_BASE'] ||= 'https://github.com/heroku/heroku-buildpack-python.git' 4 | ENV['HATCHET_DEFAULT_STACK'] ||= 'heroku-22' 5 | 6 | require 'rspec/core' 7 | require 'hatchet' 8 | 9 | LATEST_PYTHON_3_6 = '3.6.15' 10 | LATEST_PYTHON_3_7 = '3.7.14' 11 | LATEST_PYTHON_3_8 = '3.8.14' 12 | LATEST_PYTHON_3_9 = '3.9.14' 13 | LATEST_PYTHON_3_10 = '3.10.7' 14 | DEFAULT_PYTHON_VERSION = LATEST_PYTHON_3_10 15 | 16 | # Work around the return value for `default_buildpack` changing after deploy: 17 | # https://github.com/heroku/hatchet/issues/180 18 | # Once we've updated to Hatchet release that includes the fix, consumers 19 | # of this can switch back to using `app.class.default_buildpack` 20 | DEFAULT_BUILDPACK_URL = Hatchet::App.default_buildpack 21 | 22 | RSpec.configure do |config| 23 | # Disables the legacy rspec globals and monkey-patched `should` syntax. 24 | config.disable_monkey_patching! 25 | # Enable flags like --only-failures and --next-failure. 26 | config.example_status_persistence_file_path = '.rspec_status' 27 | # Allows limiting a spec run to individual examples or groups by tagging them 28 | # with `:focus` metadata via the `fit`, `fcontext` and `fdescribe` aliases. 29 | config.filter_run_when_matching :focus 30 | # Allows declaring on which stacks a test/group should run by tagging it with `stacks`. 31 | config.filter_run_excluding stacks: ->(stacks) { !stacks.include?(ENV.fetch('HATCHET_DEFAULT_STACK')) } 32 | end 33 | 34 | def clean_output(output) 35 | # Remove trailing whitespace characters added by Git: 36 | # https://github.com/heroku/hatchet/issues/162 37 | output.gsub(/ {8}(?=\R)/, '') 38 | end 39 | 40 | def update_buildpacks(app, buildpacks) 41 | # Updates the list of buildpacks for an existing app, until Hatchet supports this natively: 42 | # https://github.com/heroku/hatchet/issues/166 43 | buildpack_list = buildpacks.map { |b| { buildpack: (b == :default ? DEFAULT_BUILDPACK_URL : b) } } 44 | app.api_rate_limit.call.buildpack_installation.update(app.name, updates: buildpack_list) 45 | end 46 | -------------------------------------------------------------------------------- /vendor/WEB_CONCURRENCY.sh: -------------------------------------------------------------------------------- 1 | case $(ulimit -u) in 2 | 3 | # Automatic configuration for Gunicorn's Workers setting. 4 | 5 | # Standard-1X (+Free, +Hobby) Dyno 6 | 256) 7 | export DYNO_RAM=512 8 | export WEB_CONCURRENCY=${WEB_CONCURRENCY:-2} 9 | ;; 10 | 11 | # Standard-2X Dyno 12 | 512) 13 | export DYNO_RAM=1024 14 | export WEB_CONCURRENCY=${WEB_CONCURRENCY:-4} 15 | ;; 16 | 17 | # Performance-M Dyno 18 | 16384) 19 | export DYNO_RAM=2560 20 | export WEB_CONCURRENCY=${WEB_CONCURRENCY:-8} 21 | ;; 22 | 23 | # Performance-L Dyno 24 | 32768) 25 | export DYNO_RAM=14336 26 | export WEB_CONCURRENCY=${WEB_CONCURRENCY:-11} 27 | ;; 28 | 29 | esac 30 | -------------------------------------------------------------------------------- /vendor/buildpack-stdlib_v8.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # From: 4 | # https://raw.githubusercontent.com/heroku/buildpack-stdlib/v8/stdlib.sh 5 | 6 | # Buildpack defaults 7 | # --------------- 8 | 9 | export BUILDPACK_LOG_FILE="${BUILDPACK_LOG_FILE:-/dev/null}" 10 | 11 | # Standard Output 12 | # --------------- 13 | 14 | # Buildpack Steps. 15 | puts_step() { 16 | if [[ "$*" == "-" ]]; then 17 | read -r output 18 | else 19 | output=$* 20 | fi 21 | echo -e "\\e[1m\\e[36m=== $output\\e[0m" 22 | unset output 23 | } 24 | 25 | # Buildpack Error. 26 | puts_error() { 27 | if [[ "$*" == "-" ]]; then 28 | read -r output 29 | else 30 | output=$* 31 | fi 32 | echo -e "\\e[1m\\e[31m=!= $output\\e[0m" 33 | } 34 | 35 | # Buildpack Warning. 36 | puts_warn() { 37 | if [[ "$*" == "-" ]]; then 38 | read -r output 39 | else 40 | output=$* 41 | fi 42 | echo -e "\\e[1m\\e[33m=!= $output\\e[0m" 43 | } 44 | 45 | # Is verbose set? 46 | is_verbose() { 47 | if [[ -n $BUILDPACK_VERBOSE ]]; then 48 | return 0 49 | else 50 | return 1 51 | fi 52 | } 53 | 54 | # Buildpack Verbose. 55 | puts_verbose() { 56 | if is_verbose; then 57 | if [[ "$*" == "-" ]]; then 58 | read -r output 59 | else 60 | output=$* 61 | fi 62 | echo "$output" 63 | unset output 64 | fi 65 | } 66 | 67 | # Buildpack Utilities 68 | # ------------------- 69 | 70 | # Usage: $ set-env key value 71 | # NOTICE: Expects PROFILE_PATH & EXPORT_PATH to be set! 72 | set_env() { 73 | # TODO: automatically create profile path directory if it doesn't exist. 74 | echo "export $1=$2" >> "$PROFILE_PATH" 75 | echo "export $1=$2" >> "$EXPORT_PATH" 76 | } 77 | 78 | # Usage: $ set-default-env key value 79 | # NOTICE: Expects PROFILE_PATH & EXPORT_PATH to be set! 80 | set_default_env() { 81 | echo "export $1=\${$1:-$2}" >> "$PROFILE_PATH" 82 | echo "export $1=\${$1:-$2}" >> "$EXPORT_PATH" 83 | } 84 | 85 | # Usage: $ un-set-env key 86 | # NOTICE: Expects PROFILE_PATH to be set! 87 | un_set_env() { 88 | echo "unset $1" >> "$PROFILE_PATH" 89 | } 90 | 91 | # Usage: $ _env-blacklist pattern 92 | # Outputs a regex of default blacklist env vars. 93 | _env_blacklist() { 94 | local regex=${1:-''} 95 | if [ -n "$regex" ]; then 96 | regex="|$regex" 97 | fi 98 | echo "^(PATH|GIT_DIR|CPATH|CPPATH|LD_PRELOAD|LIBRARY_PATH$regex)$" 99 | } 100 | 101 | # Usage: $ export-env ENV_DIR WHITELIST BLACKLIST 102 | # Exports the environment variables defined in the given directory. 103 | export_env() { 104 | local env_dir=${1:-$ENV_DIR} 105 | local whitelist=${2:-''} 106 | local blacklist 107 | blacklist="$(_env_blacklist "$3")" 108 | if [ -d "$env_dir" ]; then 109 | # Environment variable names won't contain characters affected by: 110 | # shellcheck disable=SC2045 111 | for e in $(ls "$env_dir"); do 112 | echo "$e" | grep -E "$whitelist" | grep -qvE "$blacklist" && 113 | export "$e=$(cat "$env_dir/$e")" 114 | : 115 | done 116 | fi 117 | } 118 | 119 | # Usage: $ sub-env command 120 | # Runs a subshell of specified command with user-provided config. 121 | # NOTICE: Expects ENV_DIR to be set. WHITELIST & BLACKLIST are optional. 122 | # Examples: 123 | # WHITELIST=${2:-''} 124 | # BLACKLIST=${3:-'^(GIT_DIR|PYTHONHOME|LD_LIBRARY_PATH|LIBRARY_PATH|PATH)$'} 125 | sub_env() { 126 | ( 127 | # TODO: Fix https://github.com/heroku/buildpack-stdlib/issues/37 128 | # shellcheck disable=SC2153 129 | export_env "$ENV_DIR" "$WHITELIST" "$BLACKLIST" 130 | 131 | "$@" 132 | ) 133 | } 134 | 135 | # Logging 136 | # ------- 137 | 138 | # Notice: These functions expect BPLOG_PREFIX and BUILDPACK_LOG_FILE to be defined (BUILDPACK_LOG_FILE can point to /dev/null if not provided by the buildpack). 139 | # Example: BUILDPACK_LOG_FILE=${BUILDPACK_LOG_FILE:-/dev/null}; BPLOG_PREFIX="buildpack.go" 140 | 141 | # Returns now, in milleseconds. Useful for logging. 142 | # Example: $ let start=$(nowms); sleep 30; mtime "glide.install.time" "${start}" 143 | nowms() { 144 | date +%s%3N 145 | } 146 | 147 | # Log arbitrary data to the logfile (e.g. a packaging file). 148 | # Usage: $ bplog "$(<${vendorJSON}) 149 | bplog() { 150 | echo -n "${@}" | awk 'BEGIN {printf "msg=\""; f="%s"} {gsub(/"/, "\\\"", $0); printf f, $0} {if (NR == 1) f="\\n%s" } END { print "\"" }' >> "${BUILDPACK_LOG_FILE}" 151 | } 152 | 153 | # Measures time elapsed for a specific build step. 154 | # Usage: $ let start=$(nowms); mtime "glide.install.time" "${start}" 155 | # https://github.com/heroku/engineering-docs/blob/master/guides/logs-as-data.md#distributions-measure 156 | mtime() { 157 | local key="${BPLOG_PREFIX}.${1}" 158 | local start="${2}" 159 | local end="${3:-$(nowms)}" 160 | echo "${key} ${start} ${end}" | awk '{ printf "measure#%s=%.3f\n", $1, ($3 - $2)/1000 }' >> "${BUILDPACK_LOG_FILE}" 161 | } 162 | 163 | # Logs a count for a specific built step. 164 | # Usage: $ mcount "tool.govendor" 165 | # https://github.com/heroku/engineering-docs/blob/master/guides/logs-as-data.md#counting-count 166 | mcount() { 167 | local k="${BPLOG_PREFIX}.${1}" 168 | local v="${2:-1}" 169 | echo "count#${k}=${v}" >> "${BUILDPACK_LOG_FILE}" 170 | } 171 | 172 | # Logs a measure for a specific build step. 173 | # Usage: $ mmeasure "tool.installed_dependencies" 42 174 | # https://github.com/heroku/engineering-docs/blob/master/guides/logs-as-data.md#distributions-measure 175 | mmeasure() { 176 | local k="${BPLOG_PREFIX}.${1}" 177 | local v="${2}" 178 | echo "measure#${k}=${v}" >> "${BUILDPACK_LOG_FILE}" 179 | } 180 | 181 | # Logs a unuique measurement build step. 182 | # Usage: $ munique "versions.count" 2.7.13 183 | # https://github.com/heroku/engineering-docs/blob/master/guides/logs-as-data.md#uniques-unique 184 | munique() { 185 | local k="${BPLOG_PREFIX}.${1}" 186 | local v="${2}" 187 | echo "unique#${k}=${v}" >> "${BUILDPACK_LOG_FILE}" 188 | } 189 | 190 | # Measures when an exit path to the buildpack is reached, given a name, then exits 1. 191 | # Usage: $ mcount-exi "binExists" 192 | mcount_exit() { 193 | mcount "error.${1}" 194 | exit 1 195 | } 196 | -------------------------------------------------------------------------------- /vendor/pipenv-to-pip: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import json 5 | import sys 6 | 7 | 8 | def main(): 9 | INFILE = sys.argv[1] 10 | 11 | with io.open(INFILE, 'r', encoding='utf-8') as f: 12 | lockfile = json.load(f) 13 | 14 | packages = [] 15 | for package in lockfile.get('default', {}): 16 | try: 17 | packages.append('{0}{1}'.format(package, lockfile['default'][package]['version'])) 18 | except KeyError: 19 | pass 20 | 21 | print('\n'.join(packages)) 22 | 23 | if __name__ == '__main__': 24 | main() 25 | -------------------------------------------------------------------------------- /vendor/python.gunicorn.sh: -------------------------------------------------------------------------------- 1 | # Automatic configuration for Gunicorn's ForwardedAllowIPS setting. 2 | export FORWARDED_ALLOW_IPS='*' 3 | 4 | # Automatic configuration for Gunicorn's stdout access log setting. 5 | export GUNICORN_CMD_ARGS=${GUNICORN_CMD_ARGS:-"--access-logfile -"} 6 | -------------------------------------------------------------------------------- /vendor/runtime-fixer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | runtime_file = sys.argv[1] 6 | 7 | with open(runtime_file, 'r') as f: 8 | r = f.read().strip() 9 | 10 | with open(runtime_file, 'w') as f: 11 | f.write(r) 12 | --------------------------------------------------------------------------------