diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..98e67a5 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -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 diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index b5f2c06..b4b072b 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -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). diff --git a/docs/PYPI.md b/docs/PYPI.md new file mode 100644 index 0000000..36f74a5 --- /dev/null +++ b/docs/PYPI.md @@ -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=`). diff --git a/pypi/README.md b/pypi/README.md new file mode 100644 index 0000000..a6cbc7e --- /dev/null +++ b/pypi/README.md @@ -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. diff --git a/pypi/kingfisher/__init__.py b/pypi/kingfisher/__init__.py new file mode 100644 index 0000000..668a87e --- /dev/null +++ b/pypi/kingfisher/__init__.py @@ -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:]]) diff --git a/pypi/kingfisher/__main__.py b/pypi/kingfisher/__main__.py new file mode 100644 index 0000000..868d99e --- /dev/null +++ b/pypi/kingfisher/__main__.py @@ -0,0 +1,4 @@ +from . import main + +if __name__ == "__main__": + main() diff --git a/pypi/kingfisher/_version.py b/pypi/kingfisher/_version.py new file mode 100644 index 0000000..6c8e6b9 --- /dev/null +++ b/pypi/kingfisher/_version.py @@ -0,0 +1 @@ +__version__ = "0.0.0" diff --git a/pypi/pyproject.toml b/pypi/pyproject.toml new file mode 100644 index 0000000..4f8bee4 --- /dev/null +++ b/pypi/pyproject.toml @@ -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", +] diff --git a/scripts/build-pypi-wheel.sh b/scripts/build-pypi-wheel.sh new file mode 100755 index 0000000..2705886 --- /dev/null +++ b/scripts/build-pypi-wheel.sh @@ -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" <