name: Release Builds and Packaging
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.2.0)'
required: true
type: string
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
build-matrix:
name: Build ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform:
- name: Linux x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
rustflags: "-C target-feature=+aes,+sse2"
artifact_name: liblevenshtein-linux-x86_64
use_cross: false
- name: Linux ARM64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
rustflags: "-C target-feature=+aes,+neon"
artifact_name: liblevenshtein-linux-arm64
use_cross: false
- name: macOS ARM64
os: macos-latest
target: aarch64-apple-darwin
rustflags: "-C target-feature=+aes,+neon"
artifact_name: liblevenshtein-macos-arm64
use_cross: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Pin dictionary crates to crates.io
uses: ./.github/actions/pin-siblings-cratesio
- name: Register Tesseract Repository (Linux)
if: runner.os == 'Linux'
run: sudo add-apt-repository ppa:alex-p/tesseract-ocr5
- name: Update package index (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update
- name: Install protobuf compiler (Linux)
if: runner.os == 'Linux'
run: sudo apt-get install -y protobuf-compiler
- name: Install protobuf compiler (macOS)
if: runner.os == 'macOS'
run: brew install protobuf
- name: Install Leptonica (Linux)
if: runner.os == 'Linux'
run: sudo apt-get install -y libleptonica-dev
- name: Install Leptonica (macOS)
if: runner.os == 'macOS'
run: brew install leptonica
- name: Install Tesseract (Linux)
if: runner.os == 'Linux'
run: sudo apt-get install -y tesseract-ocr libtesseract-dev
- name: Install Tesseract (macOS)
if: runner.os == 'macOS'
run: brew install tesseract
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: ${{ matrix.platform.target }}
- name: Build liblevenshtein CLI
env:
RUSTFLAGS: ${{ matrix.platform.rustflags }}
run: cargo build --release --bin liblevenshtein --features cli,compression,protobuf --target ${{ matrix.platform.target }} --verbose
- name: Build liblevenshtein library
env:
RUSTFLAGS: ${{ matrix.platform.rustflags }}
run: cargo build --release --lib --all-features --target ${{ matrix.platform.target }} --verbose
- name: Prepare binaries (Unix)
if: runner.os != 'Windows'
run: |
mkdir -p dist
# Copy CLI binary
cp target/${{ matrix.platform.target }}/release/liblevenshtein dist/
# Copy library files
cp target/${{ matrix.platform.target }}/release/libliblevenshtein.rlib dist/
if [ "${{ runner.os }}" = "Linux" ]; then
cp target/${{ matrix.platform.target }}/release/libliblevenshtein.so dist/
elif [ "${{ runner.os }}" = "macOS" ]; then
cp target/${{ matrix.platform.target }}/release/libliblevenshtein.dylib dist/
fi
chmod +x dist/liblevenshtein
- name: Create tarball (Unix)
if: runner.os != 'Windows'
run: |
cd dist
tar czf ../${{ matrix.platform.artifact_name }}.tar.gz *
cd ..
- name: Create zip archive
run: |
cd dist
7z a ../${{ matrix.platform.artifact_name }}.zip *
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform.artifact_name }}
path: |
${{ matrix.platform.artifact_name }}.tar.gz
${{ matrix.platform.artifact_name }}.zip
retention-days: 30
package-deb:
name: Create .deb package
needs: build-matrix
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Pin dictionary crates to crates.io
uses: ./.github/actions/pin-siblings-cratesio
- name: Download Linux x86_64 build
uses: actions/download-artifact@v4
with:
name: liblevenshtein-linux-x86_64
- name: Extract binaries
run: |
tar xzf liblevenshtein-linux-x86_64.tar.gz
mkdir -p target/release
mv liblevenshtein target/release/
mv libliblevenshtein.so target/release/ 2>/dev/null || true
mv libliblevenshtein.rlib target/release/
- name: Install cargo-deb
run: cargo install cargo-deb
- name: Create .deb package metadata
run: |
cat >> Cargo.toml <<'EOFTOML'
[package.metadata.deb]
maintainer = "Dylon Edwards <dylon.devo@gmail.com>"
copyright = "2025, F1R3FLY.io <noreply@f1r3fly.io>"
license-file = ["LICENSE", "0"]
extended-description = """
Fast approximate string matching using Levenshtein automata.
Supports multiple dictionary backends, serialization formats,
and includes a full-featured CLI tool with compression support.
"""
depends = "$auto"
section = "utils"
priority = "optional"
assets = [
["target/release/liblevenshtein", "usr/bin/", "755"],
["target/release/libliblevenshtein.so", "usr/lib/", "644"],
["target/release/libliblevenshtein.rlib", "usr/lib/", "644"],
["README.md", "usr/share/doc/liblevenshtein/", "644"],
["CHANGELOG.md", "usr/share/doc/liblevenshtein/", "644"],
]
EOFTOML
- name: Create .deb package
run: cargo deb --no-build --no-strip
- name: Upload .deb package
uses: actions/upload-artifact@v4
with:
name: debian-package
path: target/debian/*.deb
retention-days: 30
test-deb-package:
name: Sanity Check - Debian Package
needs: package-deb
runs-on: ubuntu-latest
steps:
- name: Download .deb package
uses: actions/download-artifact@v4
with:
name: debian-package
- name: Install package
run: |
sudo dpkg -i *.deb || true
sudo apt-get install -f -y
- name: Verify installation
run: |
which liblevenshtein
liblevenshtein --version || liblevenshtein --help
- name: Check shared libraries
run: |
ldd $(which liblevenshtein)
ls -lh /usr/lib/libliblevenshtein.so || true
ls -lh /usr/lib/libliblevenshtein.rlib
- name: Test basic functionality
run: |
echo -e "test\ntesting\ntested\ntester" > /tmp/test-dict.txt
liblevenshtein --query --text "tset" --dict /tmp/test-dict.txt -m 2
echo "Basic query test passed!"
package-rpm:
name: Create .rpm package
needs: build-matrix
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Pin dictionary crates to crates.io
uses: ./.github/actions/pin-siblings-cratesio
- name: Download Linux x86_64 build
uses: actions/download-artifact@v4
with:
name: liblevenshtein-linux-x86_64
- name: Extract binaries
run: |
tar xzf liblevenshtein-linux-x86_64.tar.gz
mkdir -p target/release
mv liblevenshtein target/release/
mv libliblevenshtein.so target/release/ 2>/dev/null || true
mv libliblevenshtein.rlib target/release/
- name: Install cargo-generate-rpm
run: cargo install cargo-generate-rpm
- name: Create .rpm package
run: cargo generate-rpm
- name: Upload .rpm package
uses: actions/upload-artifact@v4
with:
name: rpm-package
path: target/generate-rpm/*.rpm
retention-days: 30
package-arch:
name: Create Arch Linux packages
needs: build-matrix
runs-on: ubuntu-latest
strategy:
matrix:
arch:
- name: x86_64
artifact: liblevenshtein-linux-x86_64
- name: aarch64
artifact: liblevenshtein-linux-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download Linux build
uses: actions/download-artifact@v4
with:
name: ${{ matrix.arch.artifact }}
- name: Extract binaries
run: |
tar xzf ${{ matrix.arch.artifact }}.tar.gz
mkdir -p binaries
mv liblevenshtein binaries/
mv libliblevenshtein.so binaries/ 2>/dev/null || true
mv libliblevenshtein.rlib binaries/
- name: Build Arch package in container
run: |
# Create a build script with architecture override
ARCH_NAME="${{ matrix.arch.name }}"
cat > build-arch-pkg.sh <<EOF
#!/bin/bash
set -e
# Install required packages
pacman -Sy --noconfirm base-devel git
# Create build user (makepkg won't run as root)
useradd -m builder
chown -R builder:builder /build
# Build as non-root user with CARCH override
cd /build
su builder -c "CARCH=${ARCH_NAME} makepkg --nodeps --skipinteg --ignorearch"
# Copy package to output (exclude debug packages)
for pkg in *.pkg.tar.zst; do
if [[ ! "\$pkg" =~ -debug- ]]; then
cp "\$pkg" /output/
fi
done
EOF
chmod +x build-arch-pkg.sh
# Prepare PKGBUILD with pre-built binaries
mkdir -p build-context
cp packaging/arch/PKGBUILD build-context/
cp -r binaries build-context/
# Modify PKGBUILD to use pre-built binaries instead of building from source
cat > build-context/PKGBUILD <<'PKGBUILD_EOF'
# Maintainer: F1R3FLY.io <noreply@f1r3fly.io>
pkgname=liblevenshtein
pkgver=__VERSION__
pkgrel=1
pkgdesc="Fast approximate string matching using Levenshtein automata"
arch=('__ARCH__')
url="https://github.com/universal-automata/liblevenshtein-rust"
license=('Apache')
depends=('gcc-libs')
source=()
sha256sums=()
package() {
install -Dm755 "/build/binaries/liblevenshtein" "$pkgdir/usr/bin/liblevenshtein"
install -Dm644 "/build/binaries/libliblevenshtein.rlib" "$pkgdir/usr/lib/libliblevenshtein.rlib"
if [ -f "/build/binaries/libliblevenshtein.so" ]; then
install -Dm644 "/build/binaries/libliblevenshtein.so" "$pkgdir/usr/lib/libliblevenshtein.so"
fi
}
PKGBUILD_EOF
# Replace placeholders
sed -i "s/__VERSION__/${GITHUB_REF_NAME#v}/" build-context/PKGBUILD
sed -i "s/__ARCH__/${{ matrix.arch.name }}/" build-context/PKGBUILD
# Debug: Show PKGBUILD content after substitution
echo "=== PKGBUILD for ${{ matrix.arch.name }} after sed ==="
cat build-context/PKGBUILD
echo "=== End PKGBUILD ==="
# Run build in container
mkdir -p output
docker run --rm \
-v "$PWD/build-context:/build" \
-v "$PWD/output:/output" \
-v "$PWD/build-arch-pkg.sh:/build-arch-pkg.sh" \
archlinux:latest \
/build-arch-pkg.sh
# Debug: Show what package was created
echo "=== Packages created for ${{ matrix.arch.name }} ==="
ls -la output/
echo "=== End package list ==="
- name: Upload Arch package
uses: actions/upload-artifact@v4
with:
name: arch-package-${{ matrix.arch.name }}
path: output/*.pkg.tar.zst
retention-days: 30
test-rpm-package:
name: Sanity Check - RPM Package
needs: package-rpm
runs-on: ubuntu-latest
container: fedora:40
steps:
- name: Download .rpm package
uses: actions/download-artifact@v4
with:
name: rpm-package
- name: Install package
run: |
dnf install -y *.rpm
- name: Verify installation
run: |
command -v liblevenshtein
liblevenshtein --version || liblevenshtein --help
- name: Check shared libraries
run: |
ldd $(command -v liblevenshtein)
ls -lh /usr/lib64/libliblevenshtein.so || true
ls -lh /usr/lib64/libliblevenshtein.rlib
- name: Test basic functionality
run: |
echo -e "test\ntesting\ntested\ntester" > /tmp/test-dict.txt
liblevenshtein --query --text "tset" --dict /tmp/test-dict.txt -m 2
echo "Basic query test passed!"
test-arch-package:
name: Sanity Check - Arch Package (${{ matrix.arch }})
needs: package-arch
runs-on: ubuntu-latest
strategy:
matrix:
arch: [x86_64]
container: archlinux:latest
steps:
- name: Download Arch package
uses: actions/download-artifact@v4
with:
name: arch-package-${{ matrix.arch }}
- name: Install package
run: |
pacman -Sy --noconfirm
pacman -U --noconfirm *.pkg.tar.zst
- name: Verify installation
run: |
command -v liblevenshtein
liblevenshtein --version || liblevenshtein --help
- name: Check shared libraries
run: |
ldd $(command -v liblevenshtein) || true
ls -lh /usr/lib/libliblevenshtein.so || true
ls -lh /usr/lib/libliblevenshtein.rlib
- name: Test basic functionality
run: |
echo -e "test\ntesting\ntested\ntester" > /tmp/test-dict.txt
liblevenshtein --query --text "tset" --dict /tmp/test-dict.txt -m 2
echo "Basic query test passed!"
create-release:
name: Create GitHub Release
needs: [build-matrix, package-deb, package-rpm, package-arch, test-deb-package, test-rpm-package, test-arch-package]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
liblevenshtein-linux-*/*.tar.gz
liblevenshtein-linux-*/*.zip
liblevenshtein-macos-*/*.tar.gz
liblevenshtein-macos-*/*.zip
debian-package/*.deb
rpm-package/*.rpm
arch-package-*/*.pkg.tar.zst
draft: false
prerelease: false
body: |
See [CHANGELOG.md](https://github.com/universal-automata/liblevenshtein-rust/blob/master/CHANGELOG.md) for details.
**Available on crates.io**: `cargo add liblevenshtein`
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-crates-io:
name: Publish to crates.io
needs: create-release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Pin dictionary crates to crates.io
uses: ./.github/actions/pin-siblings-cratesio
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -o pipefail
# Idempotent publish: re-running a release (e.g. rebuilding packages for
# an already-published version) must not fail. If `cargo publish` fails
# solely because this version is already on crates.io, treat it as
# success; any other failure is a real error.
if cargo publish --verbose --allow-dirty 2>&1 | tee /tmp/cargo-publish.log; then
echo "✅ Published to crates.io."
elif grep -qiE 'already (uploaded|exists)' /tmp/cargo-publish.log; then
echo "ℹ️ This version is already on crates.io — treating as success (idempotent re-release)."
else
echo "❌ cargo publish failed for a reason other than an already-published version." >&2
exit 1
fi
test-crates-io:
name: Test crates.io package
needs: publish-crates-io
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
env:
RUSTFLAGS: "-C target-feature=+aes,+sse2"
steps:
- name: Wait for crates.io to update
run: |
echo "Waiting 60 seconds for crates.io to propagate the new version..."
sleep 60
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Extract version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Installing version: $VERSION"
- name: Install library from crates.io
run: |
cargo install liblevenshtein --version ${{ steps.version.outputs.version }} --features cli --verbose
- name: Test CLI functionality
run: |
# Test CLI is available
which liblevenshtein
liblevenshtein --version
liblevenshtein --help
# Create test dictionary
echo -e "test\ntesting\ntested\ntester\nbest\nrest" > /tmp/test-dict.txt
# Test basic query
echo "Testing basic query..."
liblevenshtein --query --text "tset" --dict /tmp/test-dict.txt -m 2 | tee /tmp/results.txt
# Verify we got expected results
if grep -q "test" /tmp/results.txt; then
echo "✓ Basic query test passed!"
else
echo "✗ Basic query test failed - 'test' not found in results"
exit 1
fi
- name: Create test project
run: |
# New cargo project exercising downstream usage of the published crates.
# Dictionaries come from libdictenstein (their canonical home); the fuzzy
# Transducer/query comes from liblevenshtein.
cargo new --lib test-liblevenshtein
cd test-liblevenshtein
# Overwrite the generated manifest (cargo new already emits an empty
# [dependencies] table; appending another would duplicate the key).
cat > Cargo.toml <<EOF
[package]
name = "test-liblevenshtein"
version = "0.1.0"
edition = "2021"
[dependencies]
liblevenshtein = "${{ steps.version.outputs.version }}"
libdictenstein = "0.2"
EOF
cat > src/lib.rs <<'EOF'
use libdictenstein::double_array_trie::{DoubleArrayTrie, DoubleArrayTrieChar};
use liblevenshtein::prelude::{Algorithm, Transducer};
#[test]
fn test_basic_usage() {
let dict = DoubleArrayTrie::from_terms(vec!["test", "testing", "tested"]);
let transducer = Transducer::new(dict, Algorithm::Standard);
let results: Vec<String> = transducer.query("tset", 2).collect();
assert!(!results.is_empty(), "Should find matches");
assert!(results.contains(&"test".to_string()), "Should find 'test'");
}
#[test]
fn test_unicode_support() {
let dict = DoubleArrayTrieChar::from_terms(vec!["café", "naïve"]);
let transducer = Transducer::new(dict, Algorithm::Standard);
let results: Vec<String> = transducer.query("cafe", 1).collect();
assert!(!results.is_empty(), "Should find Unicode matches");
}
#[test]
fn test_algorithms() {
let dict = DoubleArrayTrie::from_terms(vec!["test", "best"]);
// Test all algorithms compile and run
let _ = Transducer::new(dict.clone(), Algorithm::Standard);
let _ = Transducer::new(dict.clone(), Algorithm::Transposition);
let _ = Transducer::new(dict, Algorithm::MergeAndSplit);
}
EOF
# Run tests
cargo test --verbose
- name: Test serialization feature
run: |
cd test-liblevenshtein
# Serialization lives in liblevenshtein (it re-exports libdictenstein's);
# the dictionary itself comes from libdictenstein.
cat > Cargo.toml <<EOF
[package]
name = "test-liblevenshtein"
version = "0.1.0"
edition = "2021"
[dependencies]
liblevenshtein = { version = "${{ steps.version.outputs.version }}", features = ["serialization"] }
libdictenstein = "0.2"
EOF
# Test serialization
cat > src/lib.rs <<'EOF'
use libdictenstein::double_array_trie::DoubleArrayTrie;
use liblevenshtein::serialization::{BincodeSerializer, DictionarySerializer};
#[test]
fn test_serialization() {
let dict = DoubleArrayTrie::from_terms(vec!["test", "testing"]);
// Serialize to bytes
let mut buffer = Vec::new();
BincodeSerializer::serialize(&dict, &mut buffer).unwrap();
assert!(!buffer.is_empty(), "Should serialize to non-empty buffer");
// Deserialize
let loaded: DoubleArrayTrie = BincodeSerializer::deserialize(&buffer[..]).unwrap();
assert_eq!(loaded.len(), Some(2), "Should have 2 terms after deserialization");
}
EOF
cargo test --verbose
- name: Report success
run: |
echo "✅ All crates.io sanity checks passed!"
echo ""
echo "Verified:"
echo " - Package installs from crates.io"
echo " - CLI tool works correctly"
echo " - Library API functions properly"
echo " - Unicode support works"
echo " - All algorithms available"
echo " - Serialization feature works"