From 97ac451a805ce085e8f7db722d53ac337e6d2e49 Mon Sep 17 00:00:00 2001
From: Kenneth Reitz <me@kennethreitz.org>
Date: Tue, 6 Mar 2018 15:44:50 -0500
Subject: [PATCH] Pipenv uninstall, and other improvements (#650)

---
 CHANGELOG.md            |  6 +++
 bin/compile             | 21 ++++------
 bin/steps/pip-uninstall | 21 ++++++----
 bin/steps/pipenv        | 89 +++++++++++++++++++++++++++--------------
 vendor/pip-pop/pip-diff |  1 +
 vendor/pipenv-to-pip    | 26 ++++++++++++
 6 files changed, 114 insertions(+), 50 deletions(-)
 mode change 100644 => 100755 bin/steps/pipenv
 create mode 100755 vendor/pipenv-to-pip

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76c085f2..676f8d59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Python Buildpack Changelog
 
+# 126
+
+Skip installs if Pipfile.lock hasn't changed, and uninstall stale dependencies with Pipenv.
+
+- No longer warn if there is no `Procfile`.
+
 # 125
 
 Set `PYTHONPATH` during collectstatic runs, other updates.
diff --git a/bin/compile b/bin/compile
index 39592548..8d7db16f 100755
--- a/bin/compile
+++ b/bin/compile
@@ -100,12 +100,6 @@ export PKG_CONFIG_PATH=/app/.heroku/vendor/lib/pkg-config:/app/.heroku/python/li
 # Switch to the repo's context.
 cd "$BUILD_DIR"
 
-# Warn for lack of Procfile.
-if [[ ! -f Procfile ]]; then
-  puts-warn 'Warning: Your application is missing a Procfile. This file tells Heroku how to run your application.'
-  puts-warn 'Learn more: https://devcenter.heroku.com/articles/procfile'
-fi
-
 # Prepare the cache.
 mkdir -p "$CACHE_DIR"
 
@@ -166,7 +160,14 @@ mtime "python.install.time" "${start}"
 
 # Pipenv support.
 # shellcheck source=bin/steps/pipenv
-source "$BIN_DIR/steps/pipenv"
+sub_env "$BIN_DIR/steps/pipenv"
+
+# Uninstall removed dependencies with Pip.
+let start=$(nowms)
+# shellcheck source=bin/steps/pip-uninstall
+source "$BIN_DIR/steps/pip-uninstall"
+mtime "pip.uninstall.time" "${start}"
+
 
 # If no requirements.txt file given, assume `setup.py develop` is intended.
 if [ ! -f requirements.txt ] && [ ! -f Pipfile ]; then
@@ -197,12 +198,6 @@ sub_env "$BIN_DIR/steps/geo-libs"
 # shellcheck source=bin/steps/gdal
 source "$BIN_DIR/steps/gdal"
 
-# Uninstall removed dependencies with Pip.
-let start=$(nowms)
-# shellcheck source=bin/steps/pip-uninstall
-source "$BIN_DIR/steps/pip-uninstall"
-mtime "pip.uninstall.time" "${start}"
-
 # Install dependencies with Pip (where the magic happens).
 let start=$(nowms)
 # shellcheck source=bin/steps/pip-install
diff --git a/bin/steps/pip-uninstall b/bin/steps/pip-uninstall
index 6cf469f1..9ec05394 100755
--- a/bin/steps/pip-uninstall
+++ b/bin/steps/pip-uninstall
@@ -2,19 +2,24 @@
 
 set +e
 # Install dependencies with Pip.
+# shellcheck source=bin/utils
+source $BIN_DIR/utils
 
-if [[ -f .heroku/python/requirements-declared.txt ]]; then
+if [ ! "$SKIP_PIP_INSTALL" ]; then
 
-  cp .heroku/python/requirements-declared.txt requirements-declared.txt
+  if [[ -f .heroku/python/requirements-declared.txt ]]; then
 
-  pip-diff --stale requirements-declared.txt requirements.txt --exclude setuptools pip wheel > .heroku/python/requirements-stale.txt
+    cp .heroku/python/requirements-declared.txt requirements-declared.txt
 
-  rm -fr requirements-declared.txt
+    pip-diff --stale requirements-declared.txt requirements.txt --exclude setuptools pip wheel > .heroku/python/requirements-stale.txt
 
-  if [[ -s .heroku/python/requirements-stale.txt ]]; then
-    puts-step "Uninstalling stale dependencies"
-    /app/.heroku/python/bin/pip uninstall -r .heroku/python/requirements-stale.txt -y --exists-action=w | cleanup | indent
+    rm -fr requirements-declared.txt
+
+    if [[ -s .heroku/python/requirements-stale.txt ]]; then
+      puts-step "Uninstalling stale dependencies"
+      /app/.heroku/python/bin/pip uninstall -r .heroku/python/requirements-stale.txt -y --exists-action=w | cleanup | indent
+    fi
   fi
-fi
 
+fi
 set -e
diff --git a/bin/steps/pipenv b/bin/steps/pipenv
old mode 100644
new mode 100755
index 37af5620..a61301ee
--- a/bin/steps/pipenv
+++ b/bin/steps/pipenv
@@ -5,41 +5,72 @@
 # shellcheck source=bin/utils
 source $BIN_DIR/utils
 
-# Pipenv support (Generate requriements.txt with pipenv).
-if [[ -f Pipfile ]]; then
-    if [[ ! -f requirements.txt ]]; then
-        puts-step "Installing requirements with latest Pipenv…"
-
-        # Measure that we're using Pipenv.
-        mcount "tool.pipenv"
-
-        # Set PIP_EXTRA_INDEX_URL
-        if [[ -r $ENV_DIR/PIP_EXTRA_INDEX_URL ]]; then
-            PIP_EXTRA_INDEX_URL="$(cat "$ENV_DIR/PIP_EXTRA_INDEX_URL")"
-            export PIP_EXTRA_INDEX_URL
-        fi
 
-        # Install pipenv.
-        /app/.heroku/python/bin/pip install pipenv --upgrade &> /dev/null
+if [[ -f Pipfile.lock ]]; then
+    if [[ -f .heroku/python/Pipfile.lock.sha256 ]]; then
+        if [[ $(openssl dgst -sha256 Pipfile.lock) == $(cat .heroku/python/Pipfile.lock.sha256) ]]; then
+            if [[ ! "$PIPENV_ALWAYS_INSTALL" ]]; then
+                echo "Skipping installation, as Pipfile.lock hasn't changed since last deploy." | indent
+                echo "To disable this functionality, run the following command:"
+                echo ""
+                echo "    $ heroku config:set PIPENV_ALWAYS_INSTALL=1" | indent
 
-        # Install the dependencies.
-        if [[ ! -f Pipfile.lock ]]; then
-            /app/.heroku/python/bin/pipenv install --system --skip-lock 2>&1 | indent
-        else
-            /app/.heroku/python/bin/pipenv install --system --deploy 2>&1 | indent
+                SKIP_PIPENV_INSTALL=1
+            fi
         fi
+    fi
+fi
 
-        # Install the test dependencies, for CI.
-        if [ "$INSTALL_TEST" ]; then
-            puts-step "Installing test dependencies…"
-            /app/.heroku/python/bin/pipenv install --dev --system --deploy 2>&1 | cleanup | indent
-        fi
 
-        # Skip pip install, later.
-        export SKIP_PIP_INSTALL=1
+if [ ! "$SKIP_PIPENV_INSTALL" ]; then
+
+    # Pipenv support (Generate requriements.txt with pipenv).
+    if [[ -f Pipfile ]]; then
+        if [[ ! -f requirements.txt ]]; then
+            puts-step "Installing requirements with latest Pipenv…"
+
+            # Measure that we're using Pipenv.
+            mcount "tool.pipenv"
+
+            # Set PIP_EXTRA_INDEX_URL
+            if [[ -r $ENV_DIR/PIP_EXTRA_INDEX_URL ]]; then
+                PIP_EXTRA_INDEX_URL="$(cat "$ENV_DIR/PIP_EXTRA_INDEX_URL")"
+                export PIP_EXTRA_INDEX_URL
+            fi
 
-        # Pip freeze, for compatibility.
-        /app/.heroku/python/bin/pip freeze > requirements.txt
+            # if [[ -f .heroku/python/requirements-declared.txt ]]; then
+            #     cp .heroku/python/requirements-declared.txt requirements.txt
+            # fi
 
+            # Install pipenv.
+            /app/.heroku/python/bin/pip install pipenv --upgrade &> /dev/null
+
+            # Install the dependencies.
+            if [[ ! -f Pipfile.lock ]]; then
+                /app/.heroku/python/bin/pipenv install --system --skip-lock 2>&1 | indent
+            else
+                pipenv-to-pip Pipfile.lock > requirements.txt
+                "$BIN_DIR/steps/pip-uninstall"
+                cp requirements.txt .heroku/python/requirements-declared.txt
+                openssl dgst -sha256 Pipfile.lock > .heroku/python/Pipfile.lock.sha256
+
+                /app/.heroku/python/bin/pipenv install --system --deploy 2>&1 | indent
+            fi
+
+            # Install the test dependencies, for CI.
+            if [ "$INSTALL_TEST" ]; then
+                puts-step "Installing test dependencies…"
+                /app/.heroku/python/bin/pipenv install --dev --system --deploy 2>&1 | cleanup | indent
+            fi
+
+            # Skip pip install, later.
+            export SKIP_PIP_INSTALL=1
+
+            # Pip freeze, for compatibility.
+            pip freeze > requirements.txt
+        fi
     fi
+else
+    pipenv-to-pip Pipfile.lock > requirements.txt
+    export SKIP_PIP_INSTALL=1
 fi
diff --git a/vendor/pip-pop/pip-diff b/vendor/pip-pop/pip-diff
index 1081f3d2..2bb1877c 100755
--- a/vendor/pip-pop/pip-diff
+++ b/vendor/pip-pop/pip-diff
@@ -41,6 +41,7 @@ class Requirements(object):
                 if not getattr(requirement.req, 'name', None):
                     # Prior to pip 8.1.2 the attribute `name` did not exist.
                     requirement.req.name = requirement.req.project_name
+                    requirement.req.name = requirement.req.name.lower()
                 self.requirements.append(requirement.req)
 
 
diff --git a/vendor/pipenv-to-pip b/vendor/pipenv-to-pip
new file mode 100755
index 00000000..95732486
--- /dev/null
+++ b/vendor/pipenv-to-pip
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+
+import json
+import sys
+
+
+def main():
+    INFILE = sys.argv[1]
+
+    with open(INFILE, 'rb') as f:
+        lockfile = json.load(f)
+
+    packages = []
+    for package in lockfile.get('default', {}):
+        try:
+            packages.append('{0}{1}'.format(package, lockfile['default'][package]['version']))
+        except KeyError:
+            pass
+
+    print('\n'.join(packages))
+
+
+try:
+    main()
+except Exception:
+    pass
-- 
GitLab