#!/bin/bash
# Build packages with dh-virtualenv (sequence, --builtin-venv and the
# buildsystem), check that they do not hold the build path, then
# install, use and purge them. Runs offline: the virtualenvs use the
# system setuptools and pip does not reach the index.
set -u

WORK=${AUTOPKGTEST_TMP:-$(mktemp -d)}
export PIP_NO_INDEX=1 PIP_NO_BUILD_ISOLATION=0
FAILED=0
ok() { echo "ok: $*"; }
fail() { echo "FAIL: $*"; FAILED=1; }
check() {
    local msg=$1; shift
    if "$@"; then ok "$msg"; else fail "$msg"; fi
}

# make_source DIR RULES: a project with a console script and a local
# requirement, packaged with the given debian/rules
make_source() {
    local dir=$1
    mkdir -p "$dir/demo" "$dir/helper/helper" "$dir/debian"
    cat > "$dir/setup.py" <<'EOF'
from setuptools import setup

setup(name='demo', version='0.1', packages=['demo'],
      entry_points={'console_scripts': ['demo = demo:main']})
EOF
    cat > "$dir/demo/__init__.py" <<'EOF'
import helper


def main():
    print('demo ok', helper.status())
EOF
    cat > "$dir/helper/pyproject.toml" <<'EOF'
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "helper"
version = "1.0"
EOF
    printf 'def status():\n    return "helper ok"\n' > "$dir/helper/helper/__init__.py"
    echo ./helper > "$dir/requirements.txt"
    cat > "$dir/debian/control" <<'EOF'
Source: demo
Section: python
Maintainer: Test <test@example.org>
Build-Depends: debhelper-compat (= 13), dh-virtualenv

Package: demo
Architecture: any
Depends: ${misc:Depends}
Description: dh-virtualenv test package
 Test package.
EOF
    cat > "$dir/debian/changelog" <<'EOF'
demo (0.1) unstable; urgency=medium

  * Test.

 -- Test <test@example.org>  Wed, 01 Jan 2020 00:00:00 +0000
EOF
    printf '#!/usr/bin/make -f\n%s\n' "$2" > "$dir/debian/rules"
    chmod +x "$dir/debian/rules"
}

# build DIR [DEB_BUILD_OPTIONS]
build() {
    (cd "$1" && DEB_BUILD_OPTIONS=${2:-} dpkg-buildpackage -us -uc -b) \
        > "$1.log" 2>&1
}

# check_package NAME VENV: build, inspect, install, use and purge
check_package() {
    local name=$1 venv=$2 dir=$WORK/$1/demo
    echo "== $name"
    if ! build "$dir"; then
        fail "$name: build"; tail -30 "$dir.log"; return
    fi
    local deb tree=$WORK/$name.tree
    deb=$(ls "$WORK/$name"/demo_0.1_*.deb)
    dpkg-deb -x "$deb" "$tree"
    check "$name: no build path in the package" \
        sh -c "! grep -rqF '$WORK/$name' '$tree'"
    check "$name: bytecode records $venv" python3 - "$tree" "$venv" <<'EOF'
import glob, marshal, sys
tree, venv = sys.argv[1:]
pycs = glob.glob(tree + venv + '/lib/*/site-packages/demo/__pycache__/*.pyc')
names = [marshal.loads(open(p, 'rb').read()[16:]).co_filename for p in pycs]
sys.exit(not names or any(not n.startswith(venv + '/lib/') for n in names))
EOF
    check "$name: no direct_url.json, RECORD up to date" python3 - "$tree$venv" <<'EOF'
import base64, csv, glob, hashlib, os, sys
errors = 0
for dist_info in glob.glob(sys.argv[1] + '/lib/*/site-packages/*.dist-info'):
    if not os.path.basename(dist_info).startswith(('demo-', 'helper-')):
        continue
    errors += os.path.exists(dist_info + '/direct_url.json')
    with open(dist_info + '/RECORD', newline='') as fh:
        for row in csv.reader(fh):
            path = os.path.join(os.path.dirname(dist_info), row[0])
            if not os.path.lexists(path):
                errors += 1
            elif row[1]:
                algorithm, digest = row[1].split('=', 1)
                data = open(path, 'rb').read()
                errors += digest != base64.urlsafe_b64encode(
                    hashlib.new(algorithm, data).digest()).rstrip(b'=').decode()
                errors += row[2] != str(len(data))
sys.exit(errors)
EOF
    check "$name: prerm cleans $venv" \
        sh -c "dpkg-deb -I '$deb' prerm | grep -qx \"dh_venv_install_dir='$venv'\""

    dpkg -i "$deb" > /dev/null || { fail "$name: install"; return; }
    check "$name: console script runs" \
        sh -c "'$venv/bin/demo' | grep -qx 'demo ok helper ok'"
    check "$name: activate sets VIRTUAL_ENV" \
        bash -c ". '$venv/bin/activate' && [ \"\$VIRTUAL_ENV\" = '$venv' ]"
    check "$name: pip freeze without file URLs" \
        sh -c "'$venv/bin/python' -m pip freeze 2>/dev/null | grep -E '^(demo|helper)' | sort | tr '\n' ' ' | grep -qx 'demo==0.1 helper==1.0 '"
    check "$name: pip uninstall with the RECORD" \
        sh -c "'$venv/bin/python' -m pip uninstall -y -q helper && ! '$venv/bin/python' -c 'import helper' 2>/dev/null"
    # bytecode written at runtime, which dpkg does not know about
    "$venv/bin/python" -O -c 'import demo'
    dpkg --purge demo > /dev/null
    check "$name: purge removes $venv" [ ! -e "$venv" ]
}

seq=$'%:\n\tdh $@ --with python-virtualenv\n\noverride_dh_virtualenv:\n\tdh_virtualenv --use-system-packages'
make_source "$WORK/sequence/demo" "$seq"
check_package sequence /opt/venvs/demo
make_source "$WORK/builtin-venv/demo" "$seq --builtin-venv"
check_package builtin-venv /opt/venvs/demo
make_source "$WORK/buildsystem/demo" $'export DH_VIRTUALENV_ARGUMENTS = --system-site-packages\n\n%:\n\tdh $@ --buildsystem=dh_virtualenv'
check_package buildsystem /usr/share/python/demo

echo "== reproducibility"
make_source "$WORK/r1/demo" "$seq"
make_source "$WORK/reproducible-longer-path/demo" "$seq"
if build "$WORK/r1/demo" && build "$WORK/reproducible-longer-path/demo"; then
    check "same package from two build directories" \
        cmp "$WORK"/r1/demo_0.1_*.deb "$WORK"/reproducible-longer-path/demo_0.1_*.deb
else
    fail "reproducibility builds"
fi

echo "== DEB_BUILD_OPTIONS"
make_source "$WORK/options/demo" "$seq --setuptools-test --verbose"
for options in nocheck "parallel=2 nocheck" nocheckx foo-nocheck; do
    build "$WORK/options/demo" "$options"
    case $options in
        *" nocheck"|nocheck) check "'$options' skips the tests" grep -q 'Skipped tests' "$WORK/options/demo.log" ;;
        *) check "'$options' runs the tests" grep -q 'Running tests' "$WORK/options/demo.log" ;;
    esac
done

exit $FAILED
