From d71a10b8a10e04d9a1cd5683034f3f94d4a81a3a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 26 Jun 2026 13:33:50 +0200 Subject: mkwheels: add gh source mode (pypi/gh subcommands) Vendor GitHub source releases that are not on PyPI (e.g. NetExec, which also pulls git deps). New flag-based CLI with pypi/gh mode selectors: mkwheels pypi --name PKG --ver VER [--epoch N] mkwheels gh --repo OWNER/REPO --ver VER [--name PKG] [--tag TAG] [--epoch N] gh mode downloads the tagged source and uses `pip wheel` to build the project plus its whole dependency tree (PyPI + git deps) into wheels; `pip download ` is wrong for a local source since it only resolves metadata. Epoch auto-derives from the release published_at. selftest now covers both modes. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 54 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 21 deletions(-) (limited to 'CLAUDE.md') diff --git a/CLAUDE.md b/CLAUDE.md index c93c237..a20247a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ pattern, applied to Python). ``` mkwheels # the whole CLI (single-file bash) -selftest # reproducibility check (builds six twice, asserts md5 match) +selftest # reproducibility check (both modes, asserts md5 match) LICENSE # GPLv2 full text README.md # user-facing usage + rationale docs/superpowers/ # design spec + implementation plan @@ -21,35 +21,46 @@ outgrows one file. ## Invocation +Two subcommands; all options are explicit flags, no positionals. + ``` -mkwheels [epoch] +mkwheels pypi --name PKG --ver VER [--epoch N] +mkwheels gh --repo OWNER/REPO --ver VER [--name PKG] [--tag TAG] [--epoch N] ``` -- ` ` — PyPI package and exact version. -- `[epoch]` — optional `SOURCE_DATE_EPOCH`. Omitted → auto-derived from the - PyPI release upload time (earliest file's `upload_time_iso_8601`), with a - warning. Pass it explicitly to override. +- `--ver` / `--tag` strip a single leading `v`; the output version is always + without `v`. Output: `-wheels-.tar.gz` + `requirements.txt`. +- `--epoch` optional in both modes; omitted → auto-derived (with a warning): + - `pypi`: earliest file's `upload_time_iso_8601` from the PyPI JSON. + - `gh`: the GitHub release `published_at` for the tag. +- `gh` defaults: `--name` = repo basename lowercased; `--tag` = normalized + `--ver`; the real ref is resolved by trying `` then `v`. - `OUTPUT` env var — output dir (default: `$PWD`). -Outputs `-wheels-.tar.gz` + `requirements.txt`, prints md5 + epoch. - ## How it works -1. Arg parse + required-tool check (`python3`+pip, `jq`, `curl`, `tar`, `gzip`, - `md5sum`). -2. Resolve `SOURCE_DATE_EPOCH` (explicit arg, else PyPI JSON via `jq`). -3. Throwaway venv + `pip download ==` into `wheels/`. -4. Emit pinned + hashed `requirements.txt` (audit record only, not the install +1. Arg parse (mode selector + flags) + required-tool check (`python3`+pip, + `jq`, `curl`, `tar`, `gzip`, `md5sum`). +2. Mode resolution sets name, epoch, and how `wheels/` is populated: + - `pypi`: epoch from PyPI JSON; `pip download ==` (pre-built + wheels, deterministic). + - `gh`: resolve release ref + `published_at`; download+unpack the tagged + source; `pip wheel ` builds the project **and all deps** (PyPI + + `git+` deps) to wheels. `pip download ` is wrong here — it only + resolves metadata and leaves the local project unmaterialized. +3. Emit pinned + hashed `requirements.txt` (audit record only, not the install input). -5. Pack a byte-reproducible `.tar.gz`: sorted entries, `--mtime=@epoch`, +4. Pack a byte-reproducible `.tar.gz`: sorted entries, `--mtime=@epoch`, `--owner=0 --group=0 --numeric-owner`, `gzip -n`. ## Reproducibility -This is the whole point. The same ` [epoch]` MUST yield a -byte-identical tarball. The tar normalization (step 5) plus `set -o pipefail` -(so a `tar` failure can't be masked by `gzip` exiting 0) are what guarantees -it. +This is the whole point. The same inputs + epoch MUST yield a byte-identical +tarball. The tar normalization (step 4) plus `set -o pipefail` (so a `tar` +failure can't be masked by `gzip` exiting 0) are what guarantees it. In `gh` +mode the project is built from source, so reproducibility holds per-machine +(build once on the target platform, upload, pin md5); wheels with compiled +extensions may differ across toolchains. **Git-sourced deps** (packages whose upstream pins a git URL, e.g. NetExec's impacket) are frozen at download time: `pip download` resolves whatever is @@ -66,9 +77,10 @@ current, and the tarball, once built, is the source of truth. The ## Testing -`./selftest` — builds `six` twice with a fixed epoch and asserts the two -tarballs are byte-identical. Run it after any change to the tar/packing logic. -Needs network (pypi.org). No test framework. +`./selftest` — builds twice with a fixed epoch in both modes (`pypi` six, +`gh` pyparsing) and asserts each pair of tarballs is byte-identical. Run it +after any change to the tar/packing or mode-resolution logic. Needs network +(pypi.org, github.com). No test framework. ## Maintainer -- cgit v1.2.3