The package-content policy¶
A release publishes two files, a wheel and an sdist. This page states
what may be in them, what may never be, and what has to be: the wheel is
what lands on sys.path, the sdist is what a build starts from, and both
are read before either is published.
.github/scripts/verify_dist_contents.py enforces every rule down to the
last section — the dist job of test.yml runs it on a build of every
pull request, and the same job, reached through release.yml’s test
call, on the files reusable-build.yml signed. The last section is the
rules nothing here enforces, which say so one by one rather than leaving the
list looking complete.
The script’s constants are those rules, and the paragraph introducing
each list below names the ones the list states.
tests/verify_dist_contents_test.py compares the two in both directions,
so this page cannot state a rule the script does not have, and the script
cannot grow a rule this page does not state. The prose around the lists
is prose, and is checked by nobody.
What may never be in either file¶
Checked against every member of both archives, the ones the _data/
amnesty below admits by directory rather than by extension included:
FORBIDDEN_SUFFIXES and FORBIDDEN_NAMES.
.pth— executed at interpreter startup, before the first import.dll,.dylib,.pyd,.so— a compiled extension, which this project has none of: the secp256k1 bindings are a separate distribution, resolved at install time and never vendored.exe— a program rather than an extension, and refused for the same reason: nothing here is compiled, so nothing here ships a binary.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 firstsitecustomize.py,usercustomize.py— the two names Python imports for their side effects alone, at startup, from the root of site-packages. Both end in.py, which the wheel’s allowlist would otherwise admit underbtclib/: the rule is about the name, not about where it happens to be harmless
A __pycache__ directory is refused too, by name rather than by suffix:
an empty one is a member in its own right and carries no .pyc.
What the wheel may hold¶
An allowlist, it being what lands on sys.path: Python source under
btclib/, btclib/py.typed, whatever sits in a _data/ directory, and
the .dist-info metadata the backend writes. Anything else fails, which
is what makes a .pth file, a stray top-level module and a bundled
shared object one rule rather than three.
Under btclib-<version>.dist-info/ — WHEEL_METADATA_FILES, beside the
licenses/ directory the backend copies LICENSE and AUTHORS.md into:
METADATA,RECORD,WHEEL— whatuv_buildwrites for a package configured as this one is
entry_points.txt is deliberately not among them: pyproject.toml
declares no [project.scripts], and an entry point is a code path a user
runs without importing anything, so one appearing there is a packaging
change that has to be read.
What the sdist may hold¶
A structural check rather than an extension allowlist.
[tool.uv.build-backend] includes tests/** and docs/** whole, so no
pattern there says what a vendored vector may be, and saying it here
would be one more edit per extension while what decides is what git
tracks — the comparison check-sdist makes. What this adds is what
neither of them can say — that every member sits under the archive’s own
root directory, that every member is a regular file or a directory (a tar
can carry a symlink, a hardlink or a device node, where a zip cannot),
and that no directory holds the metadata of some other distribution.
The directories, SDIST_DIRECTORIES:
src— the package, atsrc/btclibtests— its suitedocs— the documentation sources
The files at that root, SDIST_ROOT_SUFFIXES and SDIST_ROOT_NAMES.
source-include ships most of them by glob, *.md and the ones beside
it, so a file landing at the root is shipped without a packaging decision
being made; this is where that decision is recorded:
.md,.toml,.yaml,.jsonc— what a reader of an unpacked sdist reads, and the configuration of the tools it namesLICENSE— the licence, whichproject.license-filescopies into the wheel as well.COPYRIGHTis not beside it: the notice is in every source file already, and the file is the repository’s (btclib-org/.github#135)PKG-INFO,pyproject.toml.orig— written by the backend while it builds the archive: the metadata PyPI reads off it, and the verbatim copy of pyproject.toml it keeps beside the normalized oneuv.lock,.python-version,.secrets.baseline— what makes an unpacked sdist buildable, and its lint gate runnable, which is why[tool.uv.build-backend]names the last two
What has to be in there¶
The wheel, WHEEL_REQUIRED, beside the metadata files above, which are
required as well as allowed:
btclib/__init__.py— the archive is what this reads, and amodule-rootpointing at nothing fails the build insteadbtclib/py.typed— PEP 561 makes its absence silent: the package installs, imports, and type checks asAny
And the sdist, SDIST_REQUIRED:
PKG-INFO— the metadata PyPI reads off the archivepyproject.toml— what makes the archive buildable at all
The two carry one payload. uv build builds the sdist and then the wheel
from it, so btclib/ is the same set of files in both; a difference
either way is a file that reaches a user installing from source and not
one installing the wheel, or the reverse.
check-wheel-contents names the package tree¶
check-wheel-contents runs with package = ["src/btclib"], in the one job
verify_dist_contents.py runs in — test.yml’s dist job, on every pull
request and, through release.yml’s test call, on the files a release
publishes.
What that flag buys is the one question the tool does not ask without it.
Unconfigured, it reads the wheel’s own RECORD and says nothing about the
tree the wheel was supposed to carry; with the package named it diffs the
wheel’s btclib/ against this checkout’s src/btclib/, file for file and
in both directions — W101 for what the wheel is missing, W102 for what it
has and the tree does not. Measured on a wheel with btclib/py.typed
removed and RECORD edited to match, which installs and imports cleanly:
unconfigured, OK; configured, W101: Wheel library is missing files in package tree: btclib/py.typed.
It ran unconfigured until btclib-org/btclib#1200, and the reason recorded
in btclib-org/btclib#1160 was redundancy rather than inapplicability:
check-sdist, a pre-commit hook gated by the lint workflow, diffs this
tree against the sdist, and the completeness check above diffs the sdist
against the wheel, so chained they imply checkout == sdist == wheel. The
organization standard’s section 12 no longer accepts that trade
(btclib-org/.github#51): some other check implying the same diff does not
stand in for the flag where the flag applies, because the implication is
the same assertion bought with code to maintain, and it moves what the
dist job knows about the artifact into a chain that holds only while
every link runs — and the two links here sit in different workflows.
No ignore list. Nothing here ships outside btclib/, so neither W003
nor W009 can fire, and a list naming codes that cannot trigger would be a
line asserting a fact instead of preventing one. btclib-secp256k1
configures ignore = ["W003", "W009"] because its dynamic wheel ships a
shared library beside the package on purpose, which is exactly what those
two checks exist to flag.
The rules nothing here enforces¶
Three rules of the policy diybitcoinhardware/embit publishes beside its
own checker are not lists of members, and no list of members can show
them. That project does not check them either — its policy says so, in as
many words — so what follows each here is what stands in for a check.
No install-time execution hook. Three shapes could carry one, and each is refused by a rule above:
setup.pyis not a root file the sdist ships,entry_points.txtis not one of the wheel’s metadata files, and a.pthcarries a forbidden suffix. None of those rules was written for this, which is worth knowing before trimming one to what a build happens to produce: it would take a guard with it. There is nosetup.pyin the tree at all —[build-system]namesuv_build, the configuration is declarative, and the backend reads no build script of any kind.No network access while the package installs. Which follows from the rule above and is not a check of its own: an installed wheel is Python source and data, pip runs none of it, and the three shapes that would run are the three that are refused.
No code generated from the network while a release is built.
[build-system] requiresis the one line that can put somebody else’s code in a build here, and it is one requirement long:tests/build_system_test.pyasserts it namesuv_buildand nothing else, so a build requirement cannot be added in silence. A diff is what reads it; the test is what says a reader has to.
What none of this claims is that a build made no network request.
Proving that needs a sandboxed build, which is a different undertaking at
a different cost, and naming it as policy is the honest version. What is
checkable instead is the result: the build is reproducible from
SOURCE_DATE_EPOCH, and RELEASING.md has the command that rebuilds a
release from its tag and the bounds on it.