Skip to content

Refactor package manager handling #1640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

## [Unreleased]

- Buildpack error messages are now more consistently formatted and use colour. ([#1639](https://github.com/heroku/heroku-buildpack-python/pull/1639))
- Moved the SQLite3 install step prior to installing dependencies when using Pipenv. This now matches the behaviour when using pip and allows dependencies to actually use the headers. ([#1640](https://github.com/heroku/heroku-buildpack-python/pull/1640))
- Stopped exposing the `SKIP_PIP_INSTALL` env var to `bin/post_compile` and other subprocesses when using Pipenv. ([#1640](https://github.com/heroku/heroku-buildpack-python/pull/1640))
- Stopped creating `.heroku/python/requirements-{declared,installed}.txt` files when using pip. ([#1640](https://github.com/heroku/heroku-buildpack-python/pull/1640))
- Stopped creating a placeholder `requirements.txt` file when an app only has a `setup.py` file and no other package manager files. Instead pip is now invoked directly using `--editable .`. ([#1640](https://github.com/heroku/heroku-buildpack-python/pull/1640))
- Improved buildpack metrics for package manager detection and duration of install steps. ([#1640](https://github.com/heroku/heroku-buildpack-python/pull/1640))
- Updated buildpack-generated error messages to use colour and be more consistently formatted. ([#1639](https://github.com/heroku/heroku-buildpack-python/pull/1639))

## [v256] - 2024-09-07

Expand Down
112 changes: 46 additions & 66 deletions bin/compile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)
source "${BUILDPACK_DIR}/bin/utils"
source "${BUILDPACK_DIR}/lib/metadata.sh"
source "${BUILDPACK_DIR}/lib/output.sh"
source "${BUILDPACK_DIR}/lib/package_manager.sh"
source "${BUILDPACK_DIR}/lib/pip.sh"
source "${BUILDPACK_DIR}/lib/pipenv.sh"
source "${BUILDPACK_DIR}/lib/utils.sh"

compile_start_time=$(nowms)

Expand Down Expand Up @@ -97,12 +101,10 @@ cd "$BUILD_DIR"
# - The build is executed, modifying `~/.heroku/{known-paths}`.
# - Once the build is complete, `~/.heroku/{known-paths}` is copied back into the cache.

# Create the cache directory, if it doesn't exist.
mkdir -p "$CACHE_DIR/.heroku"

# Restore old artifacts from the cache.
mkdir -p .heroku

# The Python installation.
cp -R "$CACHE_DIR/.heroku/python" .heroku/ &>/dev/null || true
# A plain text file which contains the current stack being used (used for cache busting).
Expand All @@ -116,10 +118,7 @@ if [[ -d "$CACHE_DIR/.heroku/src" ]]; then
cp -R "$CACHE_DIR/.heroku/src" .heroku/ &>/dev/null || true
fi

# The pre_compile hook. Customers rely on this. Don't remove it.
# This part of the code is used to allow users to customize their build experience
# without forking the buildpack by providing a `bin/pre_compile` script, which gets
# run inline with the buildpack automatically.
# Runs a `bin/pre_compile` script if found in the app source, allowing build customisation.
source "${BUILDPACK_DIR}/bin/steps/hooks/pre_compile"

# Sticky runtimes. If there was a previous build, and it used a given version of Python,
Expand All @@ -128,43 +127,15 @@ if [[ -f "$CACHE_DIR/.heroku/python-version" ]]; then
CACHED_PYTHON_VERSION=$(cat "$CACHE_DIR/.heroku/python-version")
fi

# We didn't always record the stack version. This code is in place because of that.
# We didn't always record the stack version.
if [[ -f "$CACHE_DIR/.heroku/python-stack" ]]; then
CACHED_PYTHON_STACK=$(cat "$CACHE_DIR/.heroku/python-stack")
else
CACHED_PYTHON_STACK=$STACK
fi

# TODO: Move this into a new package manager handling implementation when adding Poetry support.
# We intentionally don't mention `setup.py` here since it's being removed soon.
if [[ ! -f requirements.txt && ! -f Pipfile && ! -f setup.py ]]; then
display_error <<-EOF
Error: Couldn't find any supported Python package manager files.

A Python app on Heroku must have either a 'requirements.txt' or
'Pipfile' package manager file in the root directory of its
source code.

Currently the root directory of your app contains:

$(ls -1 --indicator-style=slash "${BUILD_DIR}")

If your app already has a package manager file, check that it:

1. Is in the top level directory (not a subdirectory).
2. Has the correct spelling (the filenames are case-sensitive).
3. Isn't listed in '.gitignore' or '.slugignore'.

Otherwise, add a package manager file to your app. If your app has
no dependencies, then create an empty 'requirements.txt' file.

For help with using Python on Heroku, see:
https://devcenter.heroku.com/articles/getting-started-with-python
https://devcenter.heroku.com/articles/python-support
EOF
meta_set "failure_reason" "package-manager-not-found"
exit 1
fi
PACKAGE_MANAGER=$(package_manager::determine_package_manager "${BUILD_DIR}")
meta_set "package_manager" "${PACKAGE_MANAGER}"

# Pipenv Python version support.
# Detect the version of Python requested from a Pipfile (e.g. python_version or python_full_version).
Expand All @@ -189,9 +160,9 @@ else
echo "${DEFAULT_PYTHON_VERSION}" >runtime.txt
fi

# Create the directory for .profile.d, if it doesn't exist.
# The directory for the .profile.d scripts.
mkdir -p "$(dirname "$PROFILE_PATH")"
# Create the directory for editable source code installation, if it doesn't exist.
# The directory for editable VCS dependencies.
mkdir -p /app/.heroku/src

# On Heroku CI, builds happen in `/app`. Otherwise, on the Heroku platform,
Expand All @@ -207,46 +178,58 @@ if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then
# Note: .heroku/src is copied in later.
fi

# Download / Install Python, from pre-build binaries available on Amazon S3.
# This step also bootstraps pip / setuptools.
# Download and install Python using pre-built binaries from S3.
install_python_start_time=$(nowms)
source "${BUILDPACK_DIR}/bin/steps/python"
meta_time "python_install_duration" "${install_python_start_time}"

# Install Pipenv dependencies, if a Pipfile was provided.
source "${BUILDPACK_DIR}/bin/steps/pipenv"

# If no requirements.txt file given, assume `setup.py develop` is intended.
# This allows for people to ship a setup.py application to Heroku

if [[ ! -f requirements.txt ]] && [[ ! -f Pipfile ]]; then
meta_set "setup_py_only" "true"
echo "-e ." >requirements.txt
else
meta_set "setup_py_only" "false"
fi
# Install the package manager and related tools.
package_manager_install_start_time=$(nowms)
bundled_pip_module_path="$(utils::bundled_pip_module_path "${BUILD_DIR}")"
case "${PACKAGE_MANAGER}" in
pip)
pip::install_pip_setuptools_wheel "${bundled_pip_module_path}"
;;
pipenv)
# TODO: Stop installing pip when using Pipenv.
pip::install_pip_setuptools_wheel "${bundled_pip_module_path}"
pipenv::install_pipenv
;;
*)
abort_internal_error "Unhandled package manager"
;;
esac
meta_time "package_manager_install_duration" "${package_manager_install_start_time}"

# SQLite3 support.
# This sets up and installs sqlite3 dev headers and the sqlite3 binary but not the
# libsqlite3-0 library since that exists on the stack image.
# Installs the sqlite3 dev headers and sqlite3 binary but not the
# libsqlite3-0 library since that exists in the base image.
install_sqlite_start_time=$(nowms)
source "${BUILDPACK_DIR}/bin/steps/sqlite3"
buildpack_sqlite3_install
meta_time "sqlite_install_duration" "${install_sqlite_start_time}"

# pip install
# -----------

# Install dependencies with pip (where the magic happens).
source "${BUILDPACK_DIR}/bin/steps/pip-install"
# Install app dependencies.
dependencies_install_start_time=$(nowms)
case "${PACKAGE_MANAGER}" in
pip)
pip::install_dependencies
;;
pipenv)
pipenv::install_dependencies
;;
*)
abort_internal_error "Unhandled package manager"
;;
esac
meta_time "dependencies_install_duration" "${dependencies_install_start_time}"

# Support for NLTK corpora.
nltk_downloader_start_time=$(nowms)
sub_env "${BUILDPACK_DIR}/bin/steps/nltk"
meta_time "nltk_downloader_duration" "${nltk_downloader_start_time}"

# Support for editable installations. Here, we are copying pip–created src directory,
# and copying it into the proper place (the logical place to do this was early, but it must be done here).
# Support for editable installations.
# In CI, $BUILD_DIR is /app.
# Realpath is used to support use-cases where one of the locations is a symlink to the other.
if [[ "$(realpath "${BUILD_DIR}")" != "$(realpath /app)" ]]; then
Expand All @@ -256,9 +239,6 @@ fi

# Django collectstatic support.
# The buildpack automatically runs collectstatic for Django applications.
# This is the cause for the majority of build failures on the Python platform.
# These failures are intentional — if collectstatic (which can be tricky, at times) fails,
# your build fails.
collectstatic_start_time=$(nowms)
sub_env "${BUILDPACK_DIR}/bin/steps/collectstatic"
meta_time "django_collectstatic_duration" "${collectstatic_start_time}"
Expand Down Expand Up @@ -304,7 +284,7 @@ fi
cp "${BUILDPACK_DIR}/vendor/WEB_CONCURRENCY.sh" "$WEB_CONCURRENCY_PROFILE_PATH"
cp "${BUILDPACK_DIR}/vendor/python.gunicorn.sh" "$GUNICORN_PROFILE_PATH"

# Experimental post_compile hook. Don't remove this.
# Runs a `bin/post_compile` script if found in the app source, allowing build customisation.
source "${BUILDPACK_DIR}/bin/steps/hooks/post_compile"

# Store new artifacts in the cache.
Expand Down
2 changes: 2 additions & 0 deletions bin/report
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ STRING_FIELDS=(
failure_reason
nltk_downloader
package_manager
package_manager_multiple_found
pip_version
pipenv_version
python_version_major
Expand All @@ -81,6 +82,7 @@ ALL_OTHER_FIELDS=(
dependencies_install_duration
django_collectstatic_duration
nltk_downloader_duration
package_manager_install_duration
pipenv_has_lockfile
post_compile_hook
post_compile_hook_duration
Expand Down
44 changes: 0 additions & 44 deletions bin/steps/pip-install

This file was deleted.

4 changes: 3 additions & 1 deletion bin/steps/pipenv-python-version
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/env bash

# TODO: Move this to lib/ as part of the refactoring for .python-version support.

# Detect Python-version with Pipenv.

if [[ -f $BUILD_DIR/Pipfile ]]; then
if [[ "${PACKAGE_MANAGER}" == "pipenv" ]]; then

if [[ ! -f $BUILD_DIR/runtime.txt ]]; then
if [[ ! -f $BUILD_DIR/Pipfile.lock ]]; then
Expand Down
31 changes: 0 additions & 31 deletions bin/steps/python
Original file line number Diff line number Diff line change
Expand Up @@ -166,34 +166,3 @@ else

hash -r
fi

PIP_VERSION=$(get_requirement_version 'pip')
SETUPTOOLS_VERSION=$(get_requirement_version 'setuptools')
WHEEL_VERSION=$(get_requirement_version 'wheel')

puts-step "Installing pip ${PIP_VERSION}, setuptools ${SETUPTOOLS_VERSION} and wheel ${WHEEL_VERSION}"

meta_set "pip_version" "${PIP_VERSION}"
meta_set "setuptools_version" "${SETUPTOOLS_VERSION}"
meta_set "wheel_version" "${WHEEL_VERSION}"

# Python bundles Pip within its standard library, which we can use to install our chosen
# pip version from PyPI, saving us from having to download the usual pip bootstrap script.
# We have to use a glob since the bundled wheel filename contains the pip version, which
# differs between Python versions. We also have to handle the case where there are multiple
# matching pip wheels, since in some versions of Python (eg 3.9.0) multiple versions of pip
# were accidentally bundled upstream. Note: This implementation relies upon `nullglob` being
# set, which is the case thanks to the `bin/utils` that was run earlier.
BUNDLED_PIP_WHEEL_LIST=(.heroku/python/lib/python*/ensurepip/_bundled/pip-*.whl)
BUNDLED_PIP_WHEEL="${BUNDLED_PIP_WHEEL_LIST[0]}"

if [[ -z "${BUNDLED_PIP_WHEEL}" ]]; then
display_error "Error: Failed to locate the bundled pip wheel."
meta_set "failure_reason" "bundled-pip-not-found"
exit 1
fi

/app/.heroku/python/bin/python "${BUNDLED_PIP_WHEEL}/pip" install --quiet --disable-pip-version-check --no-cache-dir \
"pip==${PIP_VERSION}" "setuptools==${SETUPTOOLS_VERSION}" "wheel==${WHEEL_VERSION}"

hash -r
1 change: 1 addition & 0 deletions buildpack.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ files = [
"builds/",
"etc/publish.sh",
"spec/",
".editorconfig",
".gitignore",
".rubocop.yml",
".shellcheckrc",
Expand Down
2 changes: 2 additions & 0 deletions lib/kvstore.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env bash

# TODO: Switch this file to using namespaced functions like `kvstore::<fn_name>`.

# Taken from: https://github.com/heroku/heroku-buildpack-nodejs/blob/main/lib/kvstore.sh

kv_create() {
Expand Down
2 changes: 2 additions & 0 deletions lib/metadata.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env bash

# TODO: Switch this file to using namespaced functions like `metadata::<fn_name>`.

# Based on: https://github.com/heroku/heroku-buildpack-nodejs/blob/main/lib/metadata.sh

source "${BUILDPACK_DIR}/lib/kvstore.sh"
Expand Down
2 changes: 2 additions & 0 deletions lib/output.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env bash

# TODO: Switch this file to using namespaced functions like `output::<fn_name>`.

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

Expand Down
Loading