# mkwheels — Design Date: 2026-06-26 Author: danix License: GPLv2 (v2-only) ## Goal A standalone bash CLI that generates 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). Generic over package/version, so it serves any future Python SBo, not just netexec. ## Repo `~/Programming/GIT/mkwheels`, its own git repo. GPLv2 (v2-only): full `LICENSE` text, a per-file header notice `Copyright (C) 2026 Danilo M. `, and a License section in the README. ## Files ``` mkwheels/ ├── mkwheels # the script (single-file bash) ├── LICENSE # GPLv2 full text (from gnu.org) └── README.md # usage, reproducibility rationale, SBo integration ``` ## Dependencies Runtime tools the script requires (documented in README, checked at startup): - `bash` - `python3` + `pip` (the venv + download engine) - `jq` (parse the PyPI JSON API) - `tar`, `gzip`, `md5sum`, `curl` (or `wget`) No third-party Python packages; the tool only drives `pip`. ## Interface ``` mkwheels [epoch] ``` - `-h` / `--help` → usage. - `` `` required. - `[epoch]` optional SOURCE_DATE_EPOCH. If omitted, auto-derived from the PyPI release upload time (see below) and a warning is printed. Pass it explicitly to override. - `OUTPUT` env var overrides the output directory (defaults to `$PWD`), matching the mksboarchive convention. ## Flow 1. Parse args; handle `-h`. Require `` and ``. Check required tools are on PATH; fail clearly if not. 2. **Resolve epoch.** If `[epoch]` was passed, use it. Otherwise fetch `https://pypi.org/pypi///json`, take the earliest file's `upload_time_iso_8601`, convert to epoch seconds with `jq` + `date`, and warn that it was auto-derived. This is a real, reproducible, package-tied timestamp. 3. Create a throwaway temp workdir; `trap` cleanup on exit. 4. `python3 -m venv` a throwaway build env (isolates pip from the host). 5. `pip download == -d wheels/` — resolves the full tree including git-sourced deps, building sdists to wheels. 6. Emit `requirements.txt`: every resolved distribution pinned to its exact version with `--hash=sha256:...` entries computed from the downloaded files. Pinned + hashed lockfile, shippable into the SlackBuild package dir. 7. **Reproducible tar.** Archive `wheels/` with normalized metadata so the output is byte-identical for the same inputs + epoch: - sorted entry order, - `--mtime=@$EPOCH --owner=0 --group=0 --numeric-owner`, - gzip `-n` (no embedded timestamp). Output: `$OUTPUT/-wheels-.tar.gz`. 8. Print the md5sum, the resolved epoch, and the output paths (tarball + requirements.txt). ## Reproducibility - PyPI releases are immutable, so the wheel set for a fixed version is deterministic. - **Git-sourced deps** (e.g. NetExec's impacket / certipy-ad / oscrypto / pynfsclient) are unversioned upstream; `pip download` freezes whatever is current at download time. Once the tarball is built it is the source of truth; re-running months later may resolve newer git deps. The emitted `requirements.txt` records the exact resolved versions for audit. This is the same caveat the feroxbuster crates tarball carries. - Tar normalization (step 7) makes the archive byte-identical for identical inputs + epoch. ## Error handling - `set -eu`. - `trap` removes the temp workdir on any exit. - Fail with a clear message if: a required tool is missing; the package/version is not found on PyPI (HTTP error or empty JSON); `pip download` fails. ## Test (one runnable check) A `selftest` path (or a small `test` script invoked manually) that runs mkwheels twice on `six` (tiny, pure-Python, stable) and asserts the two tarballs are byte-identical (md5 match). This is the smallest check that fails if the reproducibility normalization breaks. No test framework. ## Out of scope (YAGNI) - Resolving from an arbitrary input requirements.txt (the tool resolves from ` `). - Uploading the tarball anywhere (the maintainer uploads to packages.danix.xyz by hand, as with the crates tarball). - Non-PyPI indexes / private indexes. - Caching or incremental rebuilds.