# The package-content policy A release publishes wheels of two kinds and one sdist. This page states what a wheel may hold, what it may never hold, and what has to be there; `.github/scripts/verify_wheel_contents.py` enforces every rule down to the last section, and `tests/wheel_contents_test.py` compares the two in both directions for the member lists, so this page cannot state a member rule the script does not have, and the script cannot grow one this page does not state. The section on what the compiled file is built with is not compared with the script; the hardening tests in the same file test the script alone. The prose around the lists is prose, and is checked by nobody. `test.yml`'s `check-dist` job runs the script on every wheel it downloads from `build-cibuildwheel`, `build-dynamic` and `build-windows` — the artifacts `release.yml`'s own `test` job produces and the publish jobs upload, so this judges the files that actually reach PyPI rather than a copy of them. ## Two kinds of wheel `scripts/hatch_build.py` builds one or the other, never both, and tags each so the difference is legible from the file name: a **static** wheel carries one compiled extension, `_btclib_secp256k1`, linked against libsecp256k1 and tagged for the interpreter that built it (`cpNN-cpNN-`); a **dynamic** wheel compiles no C at all, carries the ABI-mode module `_btclib_secp256k1.py` and the shared `libsecp256k1` library it `dlopen`s at import, and is tagged `py3-none-` because neither file is interpreter-specific. The script reads the wheel's own file name to tell them apart — the same distinction `scripts/hatch_build.py` draws when it writes the tag in the first place. ## What may never be in the wheel Checked against every member of `btclib_secp256k1/` and the top-level artifact, not against `.dist-info` — already validated member by member, against its own allowlist, above: `FORBIDDEN_SUFFIXES` and `FORBIDDEN_NAMES`. - `.pth` — executed at interpreter startup, before the first import - `.pyc` — a compiled module whose source nobody reviewed - `.egg`, `.tar.gz`, `.whl`, `.zip` — an archive inside an archive, that is, a second package installing with the first - `sitecustomize.py`, `usercustomize.py` — the two names Python imports for their side effects alone, at startup, from the root of site-packages A `__pycache__` directory is refused too, by name rather than by suffix. ## What the wheel may hold Three regions, and nothing outside them. Under `btclib_secp256k1-.dist-info/` — `WHEEL_METADATA_FILES`, under `licenses/`, `WHEEL_LICENSE_FILES`, and, under `sboms/`, `WHEEL_SBOM_FILES`: - `METADATA`, `RECORD`, `WHEEL` — what hatchling writes for a package configured as this one is. No `top_level.txt`: that file is setuptools's, and this build backend is hatchling - `AUTHORS.md`, `LICENSE` — `project.license-files` in `pyproject.toml`, copied into `licenses/` verbatim. Not `COPYRIGHT`: that is a repository file, and `pyproject.toml` says why beside the setting - `secp256k1/COPYING` — libsecp256k1's MIT notice, by the same setting, under its own directory inside `licenses/`. secp256k1-zkp's notice is not listed, and `pyproject.toml` says why beside the setting - `btclib_secp256k1.cdx.json` — the CycloneDX bill of materials PEP 770 places there, written while the wheel is built: the distribution, its dependencies and the vendored libraries this build compiled, each at the commit its gitlink pins. `.github/scripts/generate_sbom.py` says what it holds, and `scripts/hatch_build.py` why a build from the sdist writes none Under `btclib_secp256k1/` — exactly the files this checkout's own `src/btclib_secp256k1/` directory has, source and `py.typed` alike. The script reads that directory fresh rather than carrying a copy of its file list, which is what `check-wheel-contents --package btclib_secp256k1` would check too, asked here by hand instead of through that flag: the flag also judges the third region below, which is not part of any package tree and is not a mistake for being outside one. At the wheel's own root, one artifact set per kind, and nothing else — a second extension, a stray file, a test package shipped by accident, all fail, whichever kind the wheel is. A static wheel: exactly one file whose name starts with `_btclib_secp256k1` and ends in a compiled-extension suffix, `EXTENSION_SUFFIXES`: - `.so` — Linux and macOS - `.pyd` — Windows A dynamic wheel: exactly `_btclib_secp256k1.py`, the ABI-mode module `scripts/cffi_build.py` emits, and exactly one file whose name starts with `libsecp256k1` and ends in a shared-library suffix, `SHARED_LIBRARY_SUFFIXES`: - `.so` — Linux - `.dylib` — macOS - `.dll` — Windows Never a versioned name from the symlink chain CMake also leaves behind, such as `libsecp256k1.so.2` — `scripts/cffi_build.py` itself skips those when it copies the artifact into the wheel, keeping to one shared library rather than the chain of names one file on disk can answer to. ## What has to be in there Every file `btclib_secp256k1/` may hold, per the diff above, and the kind-appropriate top-level artifact, checked for size as well as presence: an artifact `scripts/cffi_build.py` failed to finish copying would still be a member of the archive, at zero bytes, and no other check here or in `check-wheel-contents` reads a member's size at all. ## What the compiled file has to be built with The extension of a static wheel and the shared library of a dynamic one are read back for the hardening `scripts/cffi_build.py` builds them with, from the ELF, Mach-O or PE file itself. `hardening_gaps` names one phrase per property missing, and a wheel that lacks any is refused: - Linux, ELF: the stack protector (an import of `__stack_chk_fail`), `_FORTIFY_SOURCE` (an import of a `__*_chk` function), an immediate binding (`-z now`), a read-only relocation segment, and the CET marking on x86_64. A musllinux wheel is not asked for the fortified function, which musl does not have. The marking is asked only of a wheel whose C runtime carries it, since the linker writes it only if every input does: of an x86_64 glibc wheel, and of no musllinux or aarch64 wheel - macOS, Mach-O, every architecture of a universal file: the stack protector and `_FORTIFY_SOURCE` - Windows, `.pyd` from MSVC: address space layout randomization, DEP, high entropy address space layout, control flow guard and the security cookie. The cookie is in every image the C runtime links, so it shows `/GS` only loosely - Windows, `.dll` from mingw-w64: the first three of those ## The sdist is not this policy's subject A `MANIFEST.in`-based sdist is an *include* list: a file the tree gains and `MANIFEST.in` does not name is a file the sdist silently drops, which is the class of check an include list needs and this repository does not. `[tool.hatch.build.targets.sdist] exclude` in this repository's `pyproject.toml` is the opposite shape — a file the tree gains ships by default, and has to be named to be left out — so the failure mode here is an sdist too wide rather than one silently narrow, and modelling the members the vendored `secp256k1/` submodule and this repository's own tooling contribute would be an allowlist nobody could read, let alone maintain, for a question the `exclude` list itself already answers by construction. What that leaves unchecked — an exclude pattern naming a file a build still needs — fails the build outright rather than shipping silently, which is a different question with a different check to answer it.