#!/bin/bash
# mkwheels — build a reproducible, pinned Python wheels tarball for a package.
#
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published by
# the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, see <https://www.gnu.org/licenses/>.
set -eu
set -o pipefail   # so a tar/find failure can't be masked by gzip succeeding

usage() {
    cat <<EOF
usage: ${0##*/} <pkg> <ver> [epoch]

Build a reproducible pinned Python wheels tarball <pkg>-wheels-<ver>.tar.gz
plus a hashed requirements.txt, for vendoring into a SlackBuild.

  <pkg> <ver>  PyPI package name and exact version to vendor.
  [epoch]      SOURCE_DATE_EPOCH for the tarball mtime. Omitted -> auto-derived
               from the PyPI release upload time (a warning is printed).

  OUTPUT       env var: output directory (default: current dir).

Requires: python3+pip, jq, curl, tar, gzip, md5sum.
EOF
}

case "${1:-}" in
    -h|--help) usage; exit 0 ;;
esac
[ $# -ge 2 ] && [ $# -le 3 ] || { usage >&2; exit 2; }

pkg=$1
ver=$2
epoch=${3:-}
OUTPUT=${OUTPUT:-$PWD}

# Check required tools up front.
for tool in python3 jq curl tar gzip md5sum; do
    command -v "$tool" >/dev/null 2>&1 || {
        echo "error: required tool not found: $tool" >&2
        exit 1
    }
done
python3 -m pip --version >/dev/null 2>&1 || {
    echo "error: python3 pip module not available" >&2
    exit 1
}

echo "mkwheels: $pkg $ver -> $OUTPUT/$pkg-wheels-$ver.tar.gz"

# Resolve SOURCE_DATE_EPOCH. Explicit arg wins; otherwise derive it from the
# earliest file upload time of this version on PyPI (a real, reproducible,
# release-tied timestamp).
if [ -z "$epoch" ]; then
    meta=$(curl -fsSL "https://pypi.org/pypi/$pkg/$ver/json") || {
        echo "error: cannot fetch PyPI metadata for $pkg $ver" >&2
        exit 1
    }
    iso=$(printf '%s' "$meta" \
        | jq -r '[.urls[].upload_time_iso_8601] | sort | .[0] // empty')
    [ -n "$iso" ] || {
        echo "error: no upload time found for $pkg $ver on PyPI" >&2
        exit 1
    }
    epoch=$(date -u -d "$iso" +%s)
    echo "warning: epoch not given; using PyPI upload time $iso (epoch $epoch)" >&2
fi
export SOURCE_DATE_EPOCH="$epoch"

# Throwaway workdir, cleaned on exit.
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

wheels="$work/wheels"
mkdir -p "$wheels"

# Isolated build env so host pip config / installed pkgs don't leak in.
python3 -m venv "$work/venv"
"$work/venv/bin/pip" install --quiet --upgrade pip wheel >/dev/null

# Resolve the full tree into $wheels (sdists are built to wheels).
"$work/venv/bin/pip" download "$pkg==$ver" --dest "$wheels"

# Emit a pinned, hashed requirements.txt from the downloaded files. Each
# distribution is pinned to its version with a sha256 hash per file.
req="$work/requirements.txt"
: > "$req"
for f in "$wheels"/*; do
    base=$(basename "$f")
    # name-version from the wheel/sdist filename: split on first two '-' fields
    # wheels: name-version-...; sdists: name-version.tar.gz
    name=${base%%-*}
    rest=${base#*-}
    version=${rest%%-*}
    version=${version%.tar.gz}
    hash=$(python3 -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$f")
    printf '%s==%s --hash=sha256:%s\n' "$name" "$version" "$hash" >> "$req"
done
sort -o "$req" "$req"

mkdir -p "$OUTPUT"
tarball="$OUTPUT/$pkg-wheels-$ver.tar.gz"

# Reproducible archive: sorted entries, normalized ownership/mtime, gzip -n.
# Run from $work so the archive holds a top-level 'wheels/' dir.
( cd "$work" \
  && find wheels -print0 | LC_ALL=C sort -z \
     | tar --no-recursion --null --files-from=- \
           --mtime="@$SOURCE_DATE_EPOCH" \
           --owner=0 --group=0 --numeric-owner \
           -cf - \
     | gzip -n > "$tarball" )

cp "$work/requirements.txt" "$OUTPUT/requirements.txt"

md5=$(md5sum "$tarball" | cut -d' ' -f1)
echo "wheels tarball: $tarball"
echo "requirements:   $OUTPUT/requirements.txt"
echo "epoch:          $SOURCE_DATE_EPOCH"
echo "md5sum:         $md5"
