A simple, zero-dependency tool for patching Python wheel files.
wheel-patcher allows you to add files to existing Python wheel (.whl) files while maintaining wheel integrity and PEP 427 compliance. The primary use case is adding SBOM (Software Bill of Materials) files to wheels, but the tool is generic and can add any files.
- Add files to existing wheels
- Batch operations via manifest files
- PEP 427 compliant RECORD file handling
- Zero runtime dependencies (stdlib only)
- Safe by default (creates new wheel, preserves original)
- Simple CLI interface
pip install .pip install -e .Add a file to a wheel:
wheel-patcher add mypackage-1.0-py3-none-any.whl sbom.json \
--dest .dist-info/sbom.jsonBy default, this creates mypackage-1.0-py3-none-any-patched.whl.
Note: The .dist-info/ prefix is automatically resolved to the actual dist-info directory name (e.g., mypackage-1.0.dist-info/), so you don't need to know the exact package name and version. This works in both commands and manifest files.
wheel-patcher add mypackage-1.0-py3-none-any.whl sbom.json \
--dest .dist-info/sbom.json \
--output mypackage-1.0-py3-none-any-with-sbom.whlwheel-patcher add mypackage-1.0-py3-none-any.whl sbom.json \
--dest .dist-info/sbom.json \
--in-placeCreate a manifest file (manifest.json):
{
"files": [
{
"source": "sbom.json",
"dest": ".dist-info/sbom.json"
},
{
"source": "LICENSE-THIRD-PARTY",
"dest": ".dist-info/licenses/LICENSE-THIRD-PARTY"
}
]
}Apply the manifest:
wheel-patcher apply mypackage-1.0-py3-none-any.whl --manifest manifest.jsonwheel-patcher list mypackage-1.0-py3-none-any.whlwheel-patcher extract mypackage-1.0-py3-none-any.whl --output extracted/Add a single file to a wheel.
wheel-patcher add <wheel-file> <file-to-add> [options]
Options:
--dest,-d: Destination path within wheel (default: filename)--output,-o: Output path for patched wheel (default: adds -patched suffix)--in-place: Modify wheel in-place--force,-f: Overwrite existing files in wheel
Apply batch changes from a manifest file.
wheel-patcher apply <wheel-file> --manifest <manifest.json> [options]
Options:
--manifest,-m: Path to manifest JSON file (required)--output,-o: Output path for patched wheel--in-place: Modify wheel in-place--force,-f: Overwrite existing files in wheel
List contents of a wheel.
wheel-patcher list <wheel-file>
Extract wheel to a directory.
wheel-patcher extract <wheel-file> [--output <directory>]
Options:
--output,-o: Output directory (default: wheel name)
Manifest files are JSON with the following structure:
{
"files": [
{
"source": "path/to/source/file",
"dest": "path/in/wheel"
}
]
}Each file entry must have:
source: Path to the source file on diskdest: Destination path within the wheel (use.dist-info/prefix for auto-resolution)
# Generate SBOM (example using cyclonedx)
cyclonedx-py -o sbom.json
# Add to wheel
wheel-patcher add mypackage-1.0-py3-none-any.whl sbom.json \
--dest .dist-info/sbom.jsonwheel-patcher add mypackage-1.0-py3-none-any.whl LICENSE-THIRD-PARTY \
--dest .dist-info/licenses/LICENSE-THIRD-PARTYwheel-patcher add mypackage-1.0-py3-none-any.whl SECURITY.md \
--dest .dist-info/SECURITY.md- Opens the wheel file (which is a ZIP archive)
- Reads and parses the RECORD file (PEP 427)
- Adds new files to the wheel
- Calculates SHA256 hashes for new files
- Updates the RECORD file with new entries
- Writes the patched wheel
The tool ensures:
- Proper SHA256 hash format (urlsafe base64, no padding)
- RECORD file compliance (itself listed with empty hash)
- Path validation (prevents path traversal)
- Wheel integrity (maintains ZIP structure)
Per PEP 427, the RECORD file is CSV format:
path/to/file,sha256=base64hash,size
another/file,sha256=anotherhash,1234
package.dist-info/RECORD,,
The RECORD entry itself must have empty hash and size.
Hashes are calculated as:
digest = hashlib.sha256(content).digest()
hash_str = "sha256=" + base64.urlsafe_b64encode(digest).rstrip(b'=').decode()This matches the PEP 427 specification exactly.
- Python 3.9 or higher
- No external dependencies (uses stdlib only)
This project uses nox for test automation and uv for fast package installation.
Install nox:
pip install noxRun all tests across all supported Python versions:
noxRun tests on a specific Python version:
nox -s tests-3.12Other available sessions:
# Run linting
nox -s lint
# Check code formatting
nox -s format_check
# Auto-format code
nox -s format
# Run type checking
nox -s type_checkFor faster iteration during development, reuse virtual environments:
nox -s tests-3.12 -RAlternatively, run tests directly with pytest:
pip install pytest
pytest tests/ -vMIT License
Contributions welcome! Please ensure:
- Tests pass
- Code follows existing style
- RECORD format compliance is maintained