# mkwheels A standalone bash CLI that builds a **reproducible, pinned Python wheels tarball** for a given package + version. Primary use: vendoring a Python project's full dependency tree into a SlackBuild so the build installs into a venv from local wheels with no network (the feroxbuster vendored-crates pattern, applied to Python). ## Layout ``` mkwheels # the whole CLI (single-file bash) selftest # reproducibility check (both modes, asserts md5 match) LICENSE # GPLv2 full text README.md # user-facing usage + rationale docs/superpowers/ # design spec + implementation plan ``` Single-file script by design. Keep it that way unless the tool genuinely outgrows one file. ## Invocation Two subcommands; all options are explicit flags, no positionals. ``` mkwheels pypi --name PKG --ver VER [--epoch N] mkwheels gh --repo OWNER/REPO --ver VER [--name PKG] [--tag TAG] [--epoch N] [--env K=V]... ``` - `--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`. - `--env K=V` (gh, repeatable): extra env for the source build. gh mode also auto-sets `POETRY_DYNAMIC_VERSIONING_BYPASS` and `SETUPTOOLS_SCM_PRETEND_VERSION` to `--ver` (source tarballs have no `.git`, so dynamic-versioning projects like NetExec fail VCS detection otherwise); `--env` overrides these. - `OUTPUT` env var — output dir (default: `$PWD`). ## How it works 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 ` (with the auto-set + `--env` build env) 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). 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 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 current, and the tarball, once built, is the source of truth. The `requirements.txt` records the exact resolved versions. ## Conventions - `bash`, no third-party Python packages (the tool only drives `pip`). - `set -eu` + `set -o pipefail`; temp workdir removed via `trap` on EXIT. - GPLv2 (v2-only): per-file header, `Copyright (C) Danilo M. `, License section in README. - Commits GPG-signed. Work directly on master (small project). ## Testing `./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 danix — danix@danix.xyz