initial support for distribution via pypi wheels

This commit is contained in:
Mick Grove 2026-02-04 12:15:23 -08:00
commit 3294b2baf7
9 changed files with 438 additions and 0 deletions

126
.github/workflows/pypi.yml vendored Normal file
View file

@ -0,0 +1,126 @@
name: pypi-wheels
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag to package (e.g., v1.2.3). Leave blank to use Cargo.toml."
required: false
type: string
jobs:
build-wheels:
name: Build PyPI wheels
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Determine version/tag
id: version
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
TAG="${{ github.event.release.tag_name }}"
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "${{ github.event.inputs.tag }}" ]]; then
TAG="${{ github.event.inputs.tag }}"
else
VERSION=$(grep -m1 '^version\s*=' Cargo.toml | cut -d '"' -f2)
TAG="v${VERSION}"
fi
VERSION="${TAG#v}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Download release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p release-assets
gh release download "${{ steps.version.outputs.tag }}" \
-p "kingfisher-*.tgz" \
-p "kingfisher-*.zip" \
-D release-assets
- name: Extract binaries
shell: bash
run: |
set -euo pipefail
mkdir -p extracted
for archive in release-assets/*; do
name=$(basename "$archive")
dir="extracted/${name%.*}"
mkdir -p "$dir"
case "$archive" in
*.tgz)
tar -xzf "$archive" -C "$dir"
;;
*.zip)
unzip -q "$archive" -d "$dir"
;;
*)
echo "Unknown archive: $archive" >&2
exit 1
;;
esac
done
mkdir -p extracted/bin
for bin in $(find extracted -type f -name "kingfisher" -o -name "kingfisher.exe"); do
chmod +x "$bin" || true
done
- name: Install build tooling
run: python -m pip install --upgrade build
- name: Build wheels
shell: bash
run: |
set -euo pipefail
version="${{ steps.version.outputs.version }}"
linux_x64=$(find extracted -type f -name "kingfisher" | rg -m1 "linux-x64" -)
linux_arm64=$(find extracted -type f -name "kingfisher" | rg -m1 "linux-arm64" -)
mac_x64=$(find extracted -type f -name "kingfisher" | rg -m1 "darwin-x64" -)
mac_arm64=$(find extracted -type f -name "kingfisher" | rg -m1 "darwin-arm64" -)
win_x64=$(find extracted -type f -name "kingfisher.exe" | rg -m1 "windows-x64" -)
scripts/build-pypi-wheel.sh \
--binary "$linux_x64" \
--version "$version" \
--plat-name musllinux_1_2_x86_64
scripts/build-pypi-wheel.sh \
--binary "$linux_arm64" \
--version "$version" \
--plat-name musllinux_1_2_aarch64
scripts/build-pypi-wheel.sh \
--binary "$mac_x64" \
--version "$version" \
--plat-name macosx_10_9_x86_64
scripts/build-pypi-wheel.sh \
--binary "$mac_arm64" \
--version "$version" \
--plat-name macosx_11_0_arm64
scripts/build-pypi-wheel.sh \
--binary "$win_x64" \
--version "$version" \
--plat-name win_amd64
scripts/build-pypi-wheel.sh \
--binary "$win_x64" \
--version "$version" \
--plat-name win_arm64
- name: Publish to PyPI (Trusted Publishing)
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist-pypi

View file

@ -16,6 +16,7 @@ This guide covers all installation methods for Kingfisher, including pre-commit
- [Using the pre-commit Framework](#using-the-pre-commit-framework)
- [Using Husky (Node.js projects)](#using-husky-nodejs-projects)
- [Compile from Source](#compile-from-source)
- [PyPI Wheels](#pypi-wheels)
- [Run Kingfisher in Docker](#run-kingfisher-in-docker)
## Pre-built Releases
@ -377,3 +378,22 @@ docker run --rm \
--format json \
--output /out/findings.json
```
## PyPI Wheels
If you want to run Kingfisher from PyPI, install the `kingfisher-bin` package
and use the `kingfisher` command it exposes:
```bash
pip install kingfisher-bin
kingfisher --help
```
Or run it without installation using `uvx`:
```bash
uvx kingfisher-bin --help
```
For maintainers who need to build and publish wheels, see
[docs/PYPI.md](PYPI.md).

91
docs/PYPI.md Normal file
View file

@ -0,0 +1,91 @@
# PyPI Wheel Distribution (Kingfisher CLI)
This document describes how to package the Kingfisher Rust binary into
platform-specific Python wheels so users can install and run `kingfisher` via
`pip` or `uv`.
## Overview
The Python package is a thin wrapper that bundles the compiled Kingfisher binary
inside `kingfisher/bin/` and exposes a `kingfisher` console entry point that
executes it.
Users can run it without installation via `uvx`:
```bash
uvx kingfisher-bin --help
```
## Build prerequisites
1. Build the Kingfisher binary for your target platform (see
[INSTALLATION.md](INSTALLATION.md) for `make` targets).
2. Install the Python build tooling:
```bash
python -m pip install build
```
## Build a wheel
Run the helper script from the repo root:
```bash
scripts/build-pypi-wheel.sh \
--binary ./path/to/kingfisher \
--version 1.2.3 \
--plat-name manylinux_2_17_x86_64
```
For Windows, pass the `.exe` binary and a Windows platform tag:
```bash
scripts/build-pypi-wheel.sh \
--binary .\\path\\to\\kingfisher.exe \
--version 1.2.3 \
--plat-name win_amd64
```
If you only build a Windows x64 binary, you can still ship a `win_arm64` wheel
using the same executable (it runs under emulation on ARM64 Windows):
```bash
scripts/build-pypi-wheel.sh \
--binary .\\path\\to\\kingfisher.exe \
--version 1.2.3 \
--plat-name win_arm64
```
The resulting wheel will be placed in `dist-pypi/` by default.
## Test locally
```bash
python -m pip install dist-pypi/kingfisher_bin-*.whl
kingfisher --help
```
## Publish
Upload the wheels to PyPI using `twine` (or your preferred tool):
```bash
python -m pip install twine
python -m twine upload dist-pypi/*
```
### GitHub Actions (recommended)
The repository includes a `pypi-wheels` workflow that:
1. Downloads the release binaries.
2. Builds platform-tagged wheels.
3. Publishes them to PyPI using Trusted Publishing (OIDC).
To use Trusted Publishing, create a PyPI project named `kingfisher-bin` and
enable GitHub Actions as a trusted publisher for this repository and workflow.
No API token is required once Trusted Publishing is configured.
If you do not use Trusted Publishing, generate a PyPI API token and provide it
to `twine` (for example via `TWINE_USERNAME=__token__` and
`TWINE_PASSWORD=<pypi-token>`).

17
pypi/README.md Normal file
View file

@ -0,0 +1,17 @@
# Kingfisher (Python wheel)
This package ships the Kingfisher CLI as a platform-specific Python wheel.
The `kingfisher` console script executes the bundled binary for your
OS/architecture.
## Usage
```bash
pip install kingfisher-bin
kingfisher --help
```
## Development
Use the helper script in `scripts/build-pypi-wheel.sh` from the repo root to
build a wheel for a specific target after compiling the Rust binary.

View file

@ -0,0 +1,48 @@
"""Python wrapper for the bundled Kingfisher binary."""
from __future__ import annotations
import os
import stat
import subprocess
import sys
from pathlib import Path
from ._version import __version__
def _binary_name() -> str:
return "kingfisher.exe" if sys.platform == "win32" else "kingfisher"
def get_binary_path() -> str:
"""Return the path to the bundled Kingfisher binary."""
binary = Path(__file__).resolve().parent / "bin" / _binary_name()
if not binary.exists():
raise FileNotFoundError(
"Kingfisher binary not found. "
"This wheel may not match your platform."
)
if sys.platform != "win32":
current_mode = binary.stat().st_mode
if not (current_mode & stat.S_IXUSR):
binary.chmod(
current_mode
| stat.S_IXUSR
| stat.S_IXGRP
| stat.S_IXOTH
)
return os.fspath(binary)
def main() -> None:
"""Execute the bundled Kingfisher binary."""
binary = get_binary_path()
if sys.platform == "win32":
raise SystemExit(subprocess.call([binary, *sys.argv[1:]]))
os.execvp(binary, [binary, *sys.argv[1:]])

View file

@ -0,0 +1,4 @@
from . import main
if __name__ == "__main__":
main()

View file

@ -0,0 +1 @@
__version__ = "0.0.0"

43
pypi/pyproject.toml Normal file
View file

@ -0,0 +1,43 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "kingfisher-bin"
description = "Kingfisher secret scanning CLI (packaged binary)"
readme = "README.md"
requires-python = ">=3.8"
license = { text = "Apache-2.0" }
authors = [
{ name = "MongoDB" }
]
classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
]
dynamic = ["version"]
[project.urls]
Homepage = "https://github.com/mongodb/kingfisher"
Repository = "https://github.com/mongodb/kingfisher"
[project.scripts]
kingfisher = "kingfisher:main"
[tool.hatch.version]
path = "kingfisher/_version.py"
[tool.hatch.build]
include = [
"kingfisher/**/*.py",
"kingfisher/bin/*",
"README.md",
]
[tool.hatch.build.targets.wheel]
only-include = [
"kingfisher",
"kingfisher/bin",
"README.md",
]

88
scripts/build-pypi-wheel.sh Executable file
View file

@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
scripts/build-pypi-wheel.sh \
--binary /path/to/kingfisher[.exe] \
--version 1.2.3 \
--plat-name manylinux_2_17_x86_64 \
[--out-dir dist-pypi]
Notes:
- Build the Rust binary for your target platform before running this script.
- Requires: python -m build (pip install build)
USAGE
}
binary_path=""
version=""
plat_name=""
out_dir="dist-pypi"
while [[ $# -gt 0 ]]; do
case "$1" in
--binary)
binary_path="$2"
shift 2
;;
--version)
version="$2"
shift 2
;;
--plat-name)
plat_name="$2"
shift 2
;;
--out-dir)
out_dir="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage
exit 1
;;
esac
done
if [[ -z "$binary_path" || -z "$version" || -z "$plat_name" ]]; then
usage
exit 1
fi
if [[ ! -f "$binary_path" ]]; then
echo "Binary not found: $binary_path" >&2
exit 1
fi
root_dir="$(git rev-parse --show-toplevel)"
pkg_dir="$root_dir/pypi"
bin_dir="$pkg_dir/kingfisher/bin"
mkdir -p "$bin_dir" "$out_dir"
binary_name="kingfisher"
if [[ "$binary_path" == *.exe ]]; then
binary_name="kingfisher.exe"
fi
cp "$binary_path" "$bin_dir/$binary_name"
chmod +x "$bin_dir/$binary_name" || true
cat > "$pkg_dir/kingfisher/_version.py" <<EOF
__version__ = "$version"
EOF
python -m build \
--wheel \
--outdir "$out_dir" \
--config-setting "--plat-name=$plat_name" \
"$pkg_dir"
echo "Built wheel(s) in $out_dir"