Bunch of changes, like a lot

This commit is contained in:
Elias Wendland committed 2026-07-17 18:58:52 +02:00
1 parent f07f54d4c1
commit 3297f60f00
25 files changed
+546 -265

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
lfs: true lfs: true
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- run: cargo test --workspace --locked - run: cargo test --workspace --locked
- run: cargo build --release --workspace --locked --target x86_64-unknown-linux-gnu - run: cargo build --release --package convertis --locked --target x86_64-unknown-linux-gnu
- name: Verify plugin-free recommendation - name: Verify plugin-free recommendation
run: | run: |
test_dir=$(mktemp -d) test_dir=$(mktemp -d)
+120
View File
@@ -0,0 +1,120 @@
name: Plugin Releases
on:
push:
branches: ["main"]
paths:
- "plugins/**"
env:
CARGO_TERM_COLOR: always
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.plugins.outputs.matrix }}
count: ${{ steps.plugins.outputs.count }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- name: Install discovery tools
run: |
sudo apt-get update
sudo apt-get install -y jq
- name: Discover changed plugins
id: plugins
env:
BEFORE: ${{ gitea.event.before }}
AFTER: ${{ gitea.sha }}
run: |
matrix=$(packaging/changed-plugins.sh "$BEFORE" "$AFTER")
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "count=$(jq '.include | length' <<<"$matrix")" >> "$GITHUB_OUTPUT"
build-package-publish:
needs: detect
if: needs.detect.outputs.count != '0'
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@v2
- name: Test plugin
env:
CONVERTIS_ENGINE_VERSION: ${{ matrix.engine_version }}
run: cargo test --locked --manifest-path "${{ matrix.manifest }}"
- name: Build plugin
env:
CONVERTIS_ENGINE_VERSION: ${{ matrix.engine_version }}
run: cargo build --release --locked --manifest-path "${{ matrix.manifest }}" --target x86_64-unknown-linux-gnu
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Install packaging tools
run: |
go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0
sudo apt-get update
sudo apt-get install -y rpm
- name: Build plugin packages
run: |
export PATH="$HOME/go/bin:$PATH"
packaging/build-plugin-package.sh deb "${{ matrix.manifest }}" target/x86_64-unknown-linux-gnu/release dist
packaging/build-plugin-package.sh rpm "${{ matrix.manifest }}" target/x86_64-unknown-linux-gnu/release dist
- name: Import GPG key
id: import-gpg
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Sign and verify RPM
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
passphrase_file=$(mktemp)
public_key_file=$(mktemp)
trap 'rm -f "$passphrase_file" "$public_key_file"' EXIT
printf '%s' "$GPG_PASSPHRASE" > "$passphrase_file"
chmod 600 "$passphrase_file"
echo "%_signature gpg" > ~/.rpmmacros
echo "%_gpg_name ${{ steps.import-gpg.outputs.keyid }}" >> ~/.rpmmacros
echo "%_gpg_path $HOME/.gnupg" >> ~/.rpmmacros
echo "%_gpg_passphrase_file $passphrase_file" >> ~/.rpmmacros
echo '%__gpg_sign_cmd /usr/bin/gpg --batch --yes --pinentry-mode loopback --passphrase-file %{_gpg_passphrase_file} --no-verbose --no-armor --no-secmem-warning -u %{_gpg_name} -sbo %{__signature_filename} -- %{__plaintext_filename}' >> ~/.rpmmacros
rpm --addsign dist/*.rpm
gpg --armor --export "${{ steps.import-gpg.outputs.keyid }}" > "$public_key_file"
sudo rpm --import "$public_key_file"
rpm --checksig --verbose dist/*.rpm | grep -Eq '[Ss]ignature.*: OK'
- uses: actions/upload-artifact@v3
with:
name: ${{ matrix.package }}-${{ matrix.version }}
if-no-files-found: error
path: |
target/x86_64-unknown-linux-gnu/release/${{ matrix.library }}
dist/*
- name: Publish plugin packages to Gitea
env:
GITEA_URL: ${{ gitea.server_url }}
GITEA_OWNER: ${{ gitea.repository_owner }}
TOKEN: ${{ secrets.PACKAGE_PAT }}
run: |
for package in dist/*.rpm; do
code=$(curl -sS -w '%{http_code}' -o /dev/null -u "${{ gitea.actor }}:$TOKEN" --upload-file "$package" "$GITEA_URL/api/packages/$GITEA_OWNER/rpm/upload")
test "$code" = 201 || test "$code" = 409
done
for package in dist/*.deb; do
code=$(curl -sS -w '%{http_code}' -o /dev/null -u "${{ gitea.actor }}:$TOKEN" --upload-file "$package" "$GITEA_URL/api/packages/$GITEA_OWNER/debian/pool/debian/main/upload")
test "$code" = 201 || test "$code" = 409
done
+13 -14
View File
@@ -12,15 +12,19 @@ jobs:
version: ${{ steps.check.outputs.version }} version: ${{ steps.check.outputs.version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Check release commit and version with:
fetch-depth: 0
- name: Check for a new stable engine version
id: check id: check
env: env:
COMMIT_MSG: ${{ github.event.head_commit.message }} BEFORE: ${{ gitea.event.before }}
run: | run: |
if echo "$COMMIT_MSG" | grep -Eq 'Release [vV]?[0-9]+\.[0-9]+\.[0-9]+'; then version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)
version=$(echo "$COMMIT_MSG" | grep -Eo 'Release [vV]?[0-9]+\.[0-9]+\.[0-9]+' | head -n1 | sed -E 's/Release [vV]?//') previous=""
package_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1) if [[ -n "$BEFORE" ]] && git cat-file -e "$BEFORE:Cargo.toml" 2>/dev/null; then
test "$version" = "$package_version" previous=$(git show "$BEFORE:Cargo.toml" | sed -n 's/^version = "\([^"]*\)"/\1/p' | head -n1)
fi
if [[ -n "$version" && "$version" != *-dev* && "$version" != "$previous" ]]; then
echo "match=true" >> "$GITHUB_OUTPUT" echo "match=true" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT"
else else
@@ -37,15 +41,14 @@ jobs:
with: with:
targets: x86_64-unknown-linux-gnu targets: x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo test --workspace --locked - run: cargo test --package convertis --locked
- run: cargo build --release --workspace --locked --target x86_64-unknown-linux-gnu - run: cargo build --release --package convertis --locked --target x86_64-unknown-linux-gnu
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: build-linux name: build-linux
if-no-files-found: error if-no-files-found: error
path: | path: |
target/x86_64-unknown-linux-gnu/release/convertis target/x86_64-unknown-linux-gnu/release/convertis
target/x86_64-unknown-linux-gnu/release/libconvertis_*.so
target/man/convertis.1 target/man/convertis.1
package: package:
@@ -123,12 +126,9 @@ jobs:
merge-multiple: false merge-multiple: false
- name: Prepare release assets - name: Prepare release assets
run: | run: |
mkdir -p release-assets/plugins mkdir -p release-assets
binary=$(find all-artifacts/build-linux -type f -name convertis | head -n1) binary=$(find all-artifacts/build-linux -type f -name convertis | head -n1)
cp "$binary" release-assets/convertis-x86_64-unknown-linux-gnu cp "$binary" release-assets/convertis-x86_64-unknown-linux-gnu
find all-artifacts/build-linux -type f -name 'libconvertis_*.so' -exec cp {} release-assets/plugins/ \;
cd release-assets
zip -r "convertis-plugins-${{ needs.check-release.outputs.version }}-x86_64-unknown-linux-gnu.zip" plugins
- name: Generate changelog - name: Generate changelog
run: | run: |
last_tag=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD) last_tag=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD)
@@ -141,7 +141,6 @@ jobs:
body_path: changelog.md body_path: changelog.md
files: | files: |
release-assets/convertis-x86_64-unknown-linux-gnu release-assets/convertis-x86_64-unknown-linux-gnu
release-assets/convertis-plugins-${{ needs.check-release.outputs.version }}-x86_64-unknown-linux-gnu.zip
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish packages to Gitea registries - name: Publish packages to Gitea registries
Generated
+10 -17
View File
@@ -321,7 +321,6 @@ dependencies = [
"gif", "gif",
"infer", "infer",
"libloading", "libloading",
"semver",
"serde", "serde",
"serde_json", "serde_json",
"tempfile", "tempfile",
@@ -331,14 +330,14 @@ dependencies = [
[[package]] [[package]]
name = "convertis-ffmpeg-audio" name = "convertis-ffmpeg-audio"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"convertis-plugin-api", "convertis-plugin-api",
] ]
[[package]] [[package]]
name = "convertis-ffmpeg-frames-to-video" name = "convertis-ffmpeg-frames-to-video"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"convertis-plugin-api", "convertis-plugin-api",
"serde", "serde",
@@ -347,14 +346,14 @@ dependencies = [
[[package]] [[package]]
name = "convertis-ffmpeg-video" name = "convertis-ffmpeg-video"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"convertis-plugin-api", "convertis-plugin-api",
] ]
[[package]] [[package]]
name = "convertis-ffmpeg-video-to-frames" name = "convertis-ffmpeg-video-to-frames"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"convertis-plugin-api", "convertis-plugin-api",
"serde", "serde",
@@ -363,14 +362,14 @@ dependencies = [
[[package]] [[package]]
name = "convertis-graphicsmagick" name = "convertis-graphicsmagick"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"convertis-plugin-api", "convertis-plugin-api",
] ]
[[package]] [[package]]
name = "convertis-html" name = "convertis-html"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"base64", "base64",
"convertis-plugin-api", "convertis-plugin-api",
@@ -379,7 +378,7 @@ dependencies = [
[[package]] [[package]]
name = "convertis-image-ascii" name = "convertis-image-ascii"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"convertis-plugin-api", "convertis-plugin-api",
"image", "image",
@@ -387,14 +386,14 @@ dependencies = [
[[package]] [[package]]
name = "convertis-imagemagick" name = "convertis-imagemagick"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"convertis-plugin-api", "convertis-plugin-api",
] ]
[[package]] [[package]]
name = "convertis-native-image" name = "convertis-native-image"
version = "0.3.0-dev" version = "0.1.0"
dependencies = [ dependencies = [
"convertis-plugin-api", "convertis-plugin-api",
"image", "image",
@@ -402,7 +401,7 @@ dependencies = [
[[package]] [[package]]
name = "convertis-plugin-api" name = "convertis-plugin-api"
version = "0.3.0-dev" version = "2.0.0"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@@ -1137,12 +1136,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
+2 -11
View File
@@ -2,15 +2,7 @@
members = [ members = [
".", ".",
"crates/convertis-plugin-api", "crates/convertis-plugin-api",
"plugins/ffmpeg-audio", "plugins/*",
"plugins/ffmpeg-video",
"plugins/ffmpeg-video-to-frames",
"plugins/ffmpeg-frames-to-video",
"plugins/native-image",
"plugins/image-ascii",
"plugins/html",
"plugins/imagemagick",
"plugins/graphicsmagick",
] ]
resolver = "2" resolver = "2"
@@ -22,7 +14,7 @@ license = "GPL-3.0-only"
repository = "https://git.ewenlau.net/ewenlau/convertis" repository = "https://git.ewenlau.net/ewenlau/convertis"
[workspace.dependencies] [workspace.dependencies]
convertis-plugin-api = { path = "crates/convertis-plugin-api", version = "=0.3.0-dev" } convertis-plugin-api = { path = "crates/convertis-plugin-api", version = "=2.0.0" }
base64 = "0.22" base64 = "0.22"
image = "0.25.10" image = "0.25.10"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@@ -55,7 +47,6 @@ gif = "0.14"
libloading = "0.8" libloading = "0.8"
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
semver = "1"
tempfile.workspace = true tempfile.workspace = true
tracing = "0.1.44" tracing = "0.1.44"
tracing-subscriber = "0.3.23" tracing-subscriber = "0.3.23"
+26 -14
View File
@@ -74,21 +74,15 @@ sudo apt install convertis-native-image
sudo apt install convertis-ffmpeg-video convertis-ffmpeg-audio sudo apt install convertis-ffmpeg-video convertis-ffmpeg-audio
``` ```
Convenience bundles are available: Every plugin is independently versioned and published, so updating one converter does not rebuild or replace unrelated plugins.
- `convertis-ffmpeg`: all FFmpeg plugins, including frame extraction and assembly.
- `convertis-image`: native image, ASCII, ImageMagick, and GraphicsMagick plugins.
- `convertis-plugins-all`: every official plugin.
When no installed route can perform a conversion, Convertis prints the individual package or packages that provide one. When no installed route can perform a conversion, Convertis prints the individual package or packages that provide one.
### Raw release ### Raw release
The release page contains a raw `x86_64-unknown-linux-gnu` executable and one ZIP containing all official `.so` plugins. Extract its `plugins/` directory beside the executable. Plugins can also be placed directly beside the executable, in `~/.local/lib/convertis/plugins`, or in `/usr/lib/convertis/plugins`. The release page contains the raw `x86_64-unknown-linux-gnu` engine executable. Plugins are released independently through the package repositories when their own source and version change. Plugin `.so` files can be placed beside the executable, in a `plugins/` directory beside it, in `~/.local/lib/convertis/plugins`, or in `/usr/lib/convertis/plugins`.
Plugins use a version-negotiated wire protocol. A plugin advertises the protocol versions it speaks and an open-ended engine requirement (for example `>=0.3.0-dev`); the engine selects the newest protocol adapter both sides support. Engine releases are therefore decoupled from plugin releases, and a protocol-v2 plugin built today remains usable by future engines that retain the v2 adapter. Plugins use the stable API v2 wire protocol. Plugin and engine release numbers are independent: compatibility is determined by the advertised API, not by matching package or compiler versions. API v1 and its unstable Rust ABI are no longer supported.
Plugins built for the original protocol v1 are still recognized by a permanent legacy adapter. Because v1 passed Rust trait objects across the dynamic-library boundary, those old binaries must also match the engine's Rust compiler and target. Protocol v2 uses JSON over a C ABI and has no Rust compiler-version coupling.
## Usage ## Usage
@@ -108,9 +102,12 @@ Useful inspection commands:
```sh ```sh
convertis --list-plugins convertis --list-plugins
convertis --list-formats convertis --list-formats
convertis --plugin-api
convertis --help convertis --help
``` ```
`--plugin-api` prints the API supported by the engine. `--list-plugins` includes the independent release version, negotiated API, and engine release each loaded plugin was designed against.
Logging is controlled with `--verbose LEVEL` (or `-v LEVEL`). `trace` records plugin-directory discovery, manifest parsing and protocol negotiation, every format-identification rule, graph construction and route-search decision, option resolution, each plugin request, and filesystem staging/install operations: Logging is controlled with `--verbose LEVEL` (or `-v LEVEL`). `trace` records plugin-directory discovery, manifest parsing and protocol negotiation, every format-identification rule, graph construction and route-search decision, option resolution, each plugin request, and filesystem staging/install operations:
```sh ```sh
@@ -125,27 +122,42 @@ Plugin settings use repeatable `--option key=value` arguments. A plugin-qualifie
## Building ## Building
Build the engine and every official plugin: Build the engine without building plugins:
```sh
cargo build --release --package convertis
```
Build all plugins when working on the complete workspace, or build one plugin by package name:
```sh ```sh
cargo build --release --workspace cargo build --release --workspace
cargo build --release --package convertis-native-image
``` ```
The engine is `target/release/convertis`; plugins are `target/release/libconvertis_*.so`. The engine is `target/release/convertis`; plugin libraries are `target/release/libconvertis_*.so`. Workspace membership uses `plugins/*`, so a new plugin directory is discovered automatically.
## Plugin API ## Plugin API
The workspace crate `convertis-plugin-api` defines the plugin authoring trait and exports it through stable protocol v2. Its unversioned manifest entry point advertises: The independently versioned `convertis-plugin-api` 2.x crate defines the plugin authoring trait and exports it through stable API v2. Its unversioned manifest entry point advertises:
- the manifest schema version; - the manifest schema version;
- every wire-protocol version implemented by the plugin; - every wire-protocol version implemented by the plugin;
- the plugin ID and release version; - the plugin ID and release version;
- an open-ended semantic-version requirement for the engine; - the engine release current when the plugin was last updated;
- the target platform. - the target platform.
Metadata, availability checks, and conversion requests cross the library boundary as owned JSON messages through versioned C entry points. Returned strings are released by the plugin's matching deallocator, and plugin panics are converted to protocol errors instead of unwinding across the ABI boundary. Metadata, availability checks, and conversion requests cross the library boundary as owned JSON messages through versioned C entry points. Returned strings are released by the plugin's matching deallocator, and plugin panics are converted to protocol errors instead of unwinding across the ABI boundary.
Protocol versions are compatibility contracts, not Convertis release numbers. Existing adapters are retained by future engines; incompatible protocol evolution is introduced under a new version and selected through manifest negotiation. New optional JSON fields may be added without requiring a protocol bump. The official plugin crates are the reference implementations through the `export_plugin!` macro. API versions are compatibility contracts, not Convertis or plugin release numbers. API v2 is the sole supported baseline and is kept compatible through additive JSON changes. The official plugin crates are the reference implementations through the `export_plugin!` macro.
Each plugin declares its own package version and release metadata in its local `Cargo.toml`; initial plugin releases start at `0.1.0`. Gitea Actions discovers changed `plugins/*` directories and builds, packages, and publishes a plugin only when its version changed and does not contain `-dev`. Unchanged and development versions are skipped. No workflow matrix or workspace member list needs updating when another plugin directory is added.
At build time, the plugin manifest automatically records the latest stable engine version (or the stable engine version being released in the same revision) as its design target. This is informational: runtime compatibility remains governed by the plugin API version.
Engine releases follow the same rule: the release workflow runs only when the workspace version in `Cargo.toml` changed and does not contain `-dev`.
Engine packages provide the virtual capability `convertis-plugin-api-2`; independently released plugin packages depend on that capability instead of an exact engine package version.
## Platforms ## Platforms
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "convertis-plugin-api" name = "convertis-plugin-api"
version.workspace = true version = "2.0.0"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
+3 -10
View File
@@ -21,18 +21,12 @@ use std::{
/// The newest stable wire protocol implemented by this SDK. /// The newest stable wire protocol implemented by this SDK.
pub const API_VERSION: u32 = 2; pub const API_VERSION: u32 = 2;
pub const ENGINE_VERSION: &str = "0.3.0";
pub const MANIFEST_SYMBOL: &[u8] = b"convertis_plugin_manifest\0"; pub const MANIFEST_SYMBOL: &[u8] = b"convertis_plugin_manifest\0";
pub const METADATA_SYMBOL_V2: &[u8] = b"convertis_plugin_metadata_v2\0"; pub const METADATA_SYMBOL_V2: &[u8] = b"convertis_plugin_metadata_v2\0";
pub const AVAILABILITY_SYMBOL_V2: &[u8] = b"convertis_plugin_availability_v2\0"; pub const AVAILABILITY_SYMBOL_V2: &[u8] = b"convertis_plugin_availability_v2\0";
pub const CONVERT_SYMBOL_V2: &[u8] = b"convertis_plugin_convert_v2\0"; pub const CONVERT_SYMBOL_V2: &[u8] = b"convertis_plugin_convert_v2\0";
pub const FREE_SYMBOL_V2: &[u8] = b"convertis_plugin_free_string_v2\0"; pub const FREE_SYMBOL_V2: &[u8] = b"convertis_plugin_free_string_v2\0";
// Protocol v1 crossed the Rust ABI boundary. These names are kept forever so
// engines can retain an adapter for plugins built before the stable protocol.
pub const LEGACY_MANIFEST_SYMBOL_V1: &[u8] = b"convertis_plugin_manifest_v1\0";
pub const LEGACY_FACTORY_SYMBOL_V1: &[u8] = b"convertis_plugin_create_v1\0";
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum ArtifactKind { pub enum ArtifactKind {
File, File,
@@ -109,7 +103,6 @@ pub trait Plugin: Send + Sync {
fn convert(&self, request: &ConversionRequest) -> Result<(), String>; fn convert(&self, request: &ConversionRequest) -> Result<(), String>;
} }
pub type PluginFactory = unsafe fn() -> Box<dyn Plugin>;
pub type PluginManifest = unsafe extern "C" fn() -> *const std::ffi::c_char; pub type PluginManifest = unsafe extern "C" fn() -> *const std::ffi::c_char;
pub type PluginJsonCall = unsafe extern "C" fn() -> *mut c_char; pub type PluginJsonCall = unsafe extern "C" fn() -> *mut c_char;
pub type PluginConvertCall = unsafe extern "C" fn(*const c_char) -> *mut c_char; pub type PluginConvertCall = unsafe extern "C" fn(*const c_char) -> *mut c_char;
@@ -176,12 +169,12 @@ macro_rules! export_plugin {
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn convertis_plugin_manifest() -> *const std::ffi::c_char { pub extern "C" fn convertis_plugin_manifest() -> *const std::ffi::c_char {
static MANIFEST: &str = concat!( static MANIFEST: &str = concat!(
"{\"manifest_version\":1,\"protocol_versions\":[2],\"engine_requirement\":\">=", "{\"manifest_version\":1,\"protocol_versions\":[2],\"plugin_id\":\"",
env!("CARGO_PKG_VERSION"),
"\",\"plugin_id\":\"",
$id, $id,
"\",\"plugin_version\":\"", "\",\"plugin_version\":\"",
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
"\",\"engine_version\":\"",
env!("CONVERTIS_ENGINE_VERSION"),
"\",\"target\":\"", "\",\"target\":\"",
env!("CONVERTIS_TARGET"), env!("CONVERTIS_TARGET"),
"\"}\0" "\"}\0"
+4 -59
View File
@@ -9,18 +9,10 @@ mkdir -p "$dist_dir"
if [[ "$packager" == "deb" ]]; then if [[ "$packager" == "deb" ]]; then
architecture=amd64 architecture=amd64
engine_dependency="convertis (= ${version}-1)"
system_dependencies=("libc6" "libgcc-s1") system_dependencies=("libc6" "libgcc-s1")
ffmpeg_dependencies=("ffmpeg")
imagemagick_dependencies=("imagemagick")
graphicsmagick_dependencies=("graphicsmagick")
else else
architecture=amd64 architecture=amd64
engine_dependency="convertis = ${version}-1"
system_dependencies=("glibc" "libgcc") system_dependencies=("glibc" "libgcc")
ffmpeg_dependencies=("/usr/bin/ffmpeg" "/usr/bin/ffprobe")
imagemagick_dependencies=("/usr/bin/magick")
graphicsmagick_dependencies=("/usr/bin/gm")
fi fi
make_package() { make_package() {
@@ -46,6 +38,10 @@ make_package() {
echo "description: $description" echo "description: $description"
echo "license: GPL-3.0-only" echo "license: GPL-3.0-only"
echo "homepage: https://git.ewenlau.net/ewenlau/convertis" echo "homepage: https://git.ewenlau.net/ewenlau/convertis"
if [[ "$name" == "convertis" ]]; then
echo "provides:"
echo " - convertis-plugin-api-2"
fi
if [[ ${#contents[@]} -gt 0 ]]; then if [[ ${#contents[@]} -gt 0 ]]; then
echo "contents:" echo "contents:"
for mapping in "${contents[@]}"; do for mapping in "${contents[@]}"; do
@@ -71,14 +67,6 @@ make_package() {
rm -f "$config" rm -f "$config"
} }
exact_dependency() {
if [[ "$packager" == "deb" ]]; then
printf '%s (= %s-1)' "$1" "$version"
else
printf '%s = %s-1' "$1" "$version"
fi
}
make_package convertis "Plugin-based universal file converter" \ make_package convertis "Plugin-based universal file converter" \
"content:${release_root}/convertis::/usr/bin/convertis" \ "content:${release_root}/convertis::/usr/bin/convertis" \
"content:target/man/convertis.1::/usr/share/man/man1/convertis.1" \ "content:target/man/convertis.1::/usr/share/man/man1/convertis.1" \
@@ -86,46 +74,3 @@ make_package convertis "Plugin-based universal file converter" \
"content:LICENSE::/usr/share/doc/convertis/LICENSE" \ "content:LICENSE::/usr/share/doc/convertis/LICENSE" \
"content:packaging/plugin-catalog.json::/usr/share/convertis/plugin-catalog.json" \ "content:packaging/plugin-catalog.json::/usr/share/convertis/plugin-catalog.json" \
"${system_dependencies[@]}" "${system_dependencies[@]}"
plugin_package() {
local package=$1 library=$2 description=$3 dependency_group=${4:-none}
local dependencies=("$engine_dependency")
case "$dependency_group" in
ffmpeg) dependencies+=("${ffmpeg_dependencies[@]}") ;;
imagemagick) dependencies+=("${imagemagick_dependencies[@]}") ;;
graphicsmagick) dependencies+=("${graphicsmagick_dependencies[@]}") ;;
esac
make_package "$package" "$description" \
"content:${release_root}/${library}::/usr/lib/convertis/plugins/${library}" \
"${dependencies[@]}"
}
plugin_package convertis-ffmpeg-audio libconvertis_ffmpeg_audio.so "FFmpeg audio plugin for Convertis" ffmpeg
plugin_package convertis-ffmpeg-video libconvertis_ffmpeg_video.so "FFmpeg video plugin for Convertis" ffmpeg
plugin_package convertis-ffmpeg-video-to-frames libconvertis_ffmpeg_video_to_frames.so "FFmpeg video-to-frames plugin for Convertis" ffmpeg
plugin_package convertis-ffmpeg-frames-to-video libconvertis_ffmpeg_frames_to_video.so "FFmpeg frames-to-video plugin for Convertis" ffmpeg
plugin_package convertis-native-image libconvertis_native_image.so "Native common-image plugin for Convertis"
plugin_package convertis-image-ascii libconvertis_image_ascii.so "Image-to-ASCII plugin for Convertis"
plugin_package convertis-html libconvertis_html.so "Self-contained HTML plugin for Convertis"
plugin_package convertis-imagemagick libconvertis_imagemagick.so "ImageMagick plugin for Convertis" imagemagick
plugin_package convertis-graphicsmagick libconvertis_graphicsmagick.so "GraphicsMagick plugin for Convertis" graphicsmagick
make_package convertis-ffmpeg "All official FFmpeg plugins for Convertis" \
"$engine_dependency" \
"$(exact_dependency convertis-ffmpeg-audio)" \
"$(exact_dependency convertis-ffmpeg-video)" \
"$(exact_dependency convertis-ffmpeg-video-to-frames)" \
"$(exact_dependency convertis-ffmpeg-frames-to-video)"
make_package convertis-image "All official image plugins for Convertis" \
"$engine_dependency" \
"$(exact_dependency convertis-native-image)" \
"$(exact_dependency convertis-image-ascii)" \
"$(exact_dependency convertis-imagemagick)" \
"$(exact_dependency convertis-graphicsmagick)"
make_package convertis-plugins-all "Every official Convertis plugin" \
"$engine_dependency" \
"$(exact_dependency convertis-ffmpeg)" \
"$(exact_dependency convertis-image)" \
"$(exact_dependency convertis-html)"
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail
packager=${1:?packager must be deb or rpm}
manifest=${2:?plugin Cargo.toml is required}
release_root=${3:?release target directory is required}
dist_dir=${4:-dist}
nfpm_command=${NFPM:-nfpm}
if [[ "$packager" != "deb" && "$packager" != "rpm" ]]; then
echo "packager must be deb or rpm" >&2
exit 1
fi
metadata=$(cargo metadata --format-version 1 --no-deps --manifest-path "$manifest")
manifest_absolute=$(realpath "$manifest")
package_metadata=$(jq -c --arg manifest "$manifest_absolute" \
'.packages[] | select(.manifest_path == $manifest)' <<<"$metadata")
package=$(jq -r '.name' <<<"$package_metadata")
version=$(jq -r '.version' <<<"$package_metadata")
description=$(jq -r '.description' <<<"$package_metadata")
api_version=$(jq -r '.metadata.convertis["api-version"]' <<<"$package_metadata")
target=$(jq -r '.targets[] | select(.kind | index("cdylib")) | .name' <<<"$package_metadata")
library="lib${target}.so"
if [[ "$api_version" != "2" ]]; then
echo "$manifest advertises unsupported plugin API $api_version" >&2
exit 1
fi
if [[ ! -f "$release_root/$library" ]]; then
echo "plugin library not found: $release_root/$library" >&2
exit 1
fi
mkdir -p "$dist_dir"
config=$(mktemp)
trap 'rm -f "$config"' EXIT
if [[ "$packager" == "deb" ]]; then
architecture=amd64
dependency_key="debian-dependencies"
else
architecture=x86_64
dependency_key="rpm-dependencies"
fi
{
echo "name: $package"
echo "arch: $architecture"
echo "platform: linux"
echo "version: $version"
echo "release: 1"
echo "section: utils"
echo "priority: optional"
echo "maintainer: Elias Wendland <eliaswendland@pm.me>"
echo "description: $description"
echo "license: GPL-3.0-only"
echo "homepage: https://git.ewenlau.net/ewenlau/convertis"
echo "contents:"
echo " - src: $release_root/$library"
echo " dst: /usr/lib/convertis/plugins/$library"
echo " file_info:"
echo " mode: 0755"
echo "depends:"
echo " - convertis-plugin-api-2"
jq -r --arg key "$dependency_key" \
'.metadata.convertis[$key] // [] | .[] | " - " + .' \
<<<"$package_metadata"
} >"$config"
"$nfpm_command" package --config "$config" --packager "$packager" --target "$dist_dir/"
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
before=${1:-}
after=${2:-HEAD}
if [[ -n "$before" && "$before" != "0000000000000000000000000000000000000000" ]] \
&& git cat-file -e "${before}^{commit}" 2>/dev/null; then
changed=$(git diff --name-only "$before" "$after" -- 'plugins/*')
else
changed=$(find plugins -mindepth 2 -maxdepth 2 -name Cargo.toml -print)
fi
directories=$(printf '%s\n' "$changed" \
| awk -F/ '$1 == "plugins" && NF >= 3 { print $1 "/" $2 }' \
| sort -u)
workspace_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)
if [[ "$workspace_version" != *-dev* ]]; then
engine_version=$workspace_version
else
engine_version=$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null \
| sed 's/^v//' || true)
engine_version=${engine_version:-${workspace_version%-dev}}
fi
entries='[]'
while IFS= read -r directory; do
[[ -n "$directory" && -f "$directory/Cargo.toml" ]] || continue
manifest="$directory/Cargo.toml"
declared_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' "$manifest" | head -n1)
if [[ -z "$declared_version" ]]; then
echo "$manifest must declare its own package version" >&2
exit 1
fi
manifest_absolute=$(realpath "$manifest")
metadata=$(cargo metadata --format-version 1 --no-deps --manifest-path "$manifest")
entry=$(jq -c \
--arg manifest "$manifest" \
--arg manifest_absolute "$manifest_absolute" \
--arg engine_version "$engine_version" '
(.packages[] | select(.manifest_path == $manifest_absolute)) as $package
| ($package.targets[] | select(.kind | index("cdylib"))) as $target
| {
manifest: $manifest,
package: $package.name,
version: $package.version,
plugin_id: $package.metadata.convertis.id,
api_version: $package.metadata.convertis["api-version"],
engine_version: $engine_version,
library: ("lib" + $target.name + ".so")
}
' <<<"$metadata")
api_version=$(jq -r '.api_version' <<<"$entry")
if [[ "$api_version" != "2" ]]; then
echo "$manifest must declare package.metadata.convertis.api-version = 2" >&2
exit 1
fi
current_version=$(jq -r '.version' <<<"$entry")
if [[ "$current_version" != "$declared_version" ]]; then
echo "$manifest resolved version $current_version but declares $declared_version" >&2
exit 1
fi
if [[ -n "$before" ]] && git cat-file -e "$before:$manifest" 2>/dev/null; then
previous_version=$(git show "$before:$manifest" \
| sed -n 's/^version = "\([^"]*\)"/\1/p' \
| head -n1)
if [[ -n "$previous_version" && "$previous_version" == "$current_version" ]]; then
echo "Skipping $manifest: version $current_version did not change" >&2
continue
fi
fi
if [[ "$current_version" == *-dev* ]]; then
echo "Skipping $manifest: development version $current_version is not releasable" >&2
continue
fi
entries=$(jq -c --argjson entry "$entry" '. + [$entry]' <<<"$entries")
done <<<"$directories"
jq -cn --argjson include "$entries" '{include: $include}'
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"schema_version": 1, "schema_version": 1,
"engine_version": "0.3.0", "plugin_api_version": 2,
"plugins": [ "plugins": [
{ "id": "ffmpeg-audio", "package": "convertis-ffmpeg-audio" }, { "id": "ffmpeg-audio", "package": "convertis-ffmpeg-audio" },
{ "id": "ffmpeg-video", "package": "convertis-ffmpeg-video" }, { "id": "ffmpeg-video", "package": "convertis-ffmpeg-video" },
+23 -7
View File
@@ -12,18 +12,34 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::process::Command; use std::{path::Path, process::Command};
fn main() { fn latest_engine_version(workspace: &Path) -> String {
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned()); if let Ok(version) = std::env::var("CONVERTIS_ENGINE_VERSION") {
let version = Command::new(rustc) return version;
.arg("--version") }
Command::new("git")
.args(["describe", "--tags", "--abbrev=0", "--match", "v[0-9]*"])
.current_dir(workspace)
.output() .output()
.ok() .ok()
.filter(|output| output.status.success()) .filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned()) .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
.unwrap_or_else(|| "unknown".to_owned()); .filter(|version| !version.is_empty())
.map(|version| version.strip_prefix('v').unwrap_or(&version).to_owned())
.unwrap_or_else(|| "unknown".to_owned())
}
fn main() {
let manifest = std::env::var_os("CARGO_MANIFEST_DIR").expect("Cargo provides manifest dir");
let workspace = Path::new(&manifest).join("../..");
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_owned()); let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_owned());
println!("cargo:rustc-env=CONVERTIS_RUSTC_VERSION={version}"); let engine_version = latest_engine_version(&workspace);
println!("cargo:rerun-if-env-changed=CONVERTIS_ENGINE_VERSION");
println!(
"cargo:rerun-if-changed={}",
workspace.join(".git/HEAD").display()
);
println!("cargo:rustc-env=CONVERTIS_ENGINE_VERSION={engine_version}");
println!("cargo:rustc-env=CONVERTIS_TARGET={target}"); println!("cargo:rustc-env=CONVERTIS_TARGET={target}");
} }
+8 -1
View File
@@ -1,12 +1,19 @@
[package] [package]
name = "convertis-ffmpeg-audio" name = "convertis-ffmpeg-audio"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.1.0"
description = "FFmpeg audio plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "ffmpeg-audio"
api-version = 2
debian-dependencies = ["ffmpeg"]
rpm-dependencies = ["/usr/bin/ffmpeg", "/usr/bin/ffprobe"]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package] [package]
name = "convertis-ffmpeg-frames-to-video" name = "convertis-ffmpeg-frames-to-video"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.3.1-dev"
description = "FFmpeg frames-to-video plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "ffmpeg-frames-to-video"
api-version = 2
debian-dependencies = ["ffmpeg"]
rpm-dependencies = ["/usr/bin/ffmpeg", "/usr/bin/ffprobe"]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package] [package]
name = "convertis-ffmpeg-video-to-frames" name = "convertis-ffmpeg-video-to-frames"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.1.0"
description = "FFmpeg video-to-frames plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "ffmpeg-video-to-frames"
api-version = 2
debian-dependencies = ["ffmpeg"]
rpm-dependencies = ["/usr/bin/ffmpeg", "/usr/bin/ffprobe"]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package] [package]
name = "convertis-ffmpeg-video" name = "convertis-ffmpeg-video"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.3.1-dev"
description = "FFmpeg video plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "ffmpeg-video"
api-version = 2
debian-dependencies = ["ffmpeg"]
rpm-dependencies = ["/usr/bin/ffmpeg", "/usr/bin/ffprobe"]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package] [package]
name = "convertis-graphicsmagick" name = "convertis-graphicsmagick"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.1.0"
description = "GraphicsMagick plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "graphicsmagick"
api-version = 2
debian-dependencies = ["graphicsmagick"]
rpm-dependencies = ["/usr/bin/gm"]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+6 -1
View File
@@ -1,12 +1,17 @@
[package] [package]
name = "convertis-html" name = "convertis-html"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.1.0"
description = "Self-contained HTML plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "html"
api-version = 2
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+6 -1
View File
@@ -1,12 +1,17 @@
[package] [package]
name = "convertis-image-ascii" name = "convertis-image-ascii"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.1.0"
description = "Image-to-ASCII plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "image-ascii"
api-version = 2
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package] [package]
name = "convertis-imagemagick" name = "convertis-imagemagick"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.1.0"
description = "ImageMagick plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "imagemagick"
api-version = 2
debian-dependencies = ["imagemagick"]
rpm-dependencies = ["/usr/bin/magick"]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+6 -1
View File
@@ -1,12 +1,17 @@
[package] [package]
name = "convertis-native-image" name = "convertis-native-image"
build = "../../plugin-build.rs" build = "../../plugin-build.rs"
version.workspace = true version = "0.1.0"
description = "Native common-image plugin for Convertis"
edition.workspace = true edition.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
repository.workspace = true repository.workspace = true
[package.metadata.convertis]
id = "native-image"
api-version = 2
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+7
View File
@@ -18,6 +18,13 @@ use std::path::PathBuf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(about = "A modular file converter.", long_about = None)] #[command(about = "A modular file converter.", long_about = None)]
pub struct Args { pub struct Args {
#[arg(
long,
help = "Print plugin API versions supported by this engine.",
conflicts_with_all = ["list_formats", "list_plugins"]
)]
pub plugin_api: bool,
#[arg( #[arg(
long, long,
help = "List formats supported by installed plugins.", help = "List formats supported by installed plugins.",
+50 -4
View File
@@ -117,6 +117,15 @@ fn main() {
); );
tracing::trace!(?args, "parsed command-line arguments"); tracing::trace!(?args, "parsed command-line arguments");
if args.plugin_api {
println!("{}", convertis_plugin_api::API_VERSION);
tracing::debug!(
api = convertis_plugin_api::API_VERSION,
"reported supported plugin API"
);
return;
}
let registry = plugin::PluginRegistry::load(&args.plugin_dirs, !args.no_default_plugins); let registry = plugin::PluginRegistry::load(&args.plugin_dirs, !args.no_default_plugins);
tracing::trace!( tracing::trace!(
plugin_count = registry.plugins.len(), plugin_count = registry.plugins.len(),
@@ -128,8 +137,8 @@ fn main() {
} }
if args.list_plugins { if args.list_plugins {
tracing::debug!("listing official plugins"); tracing::debug!("listing installed and official plugins");
println!("Official plugins:"); println!("Plugins:");
for metadata in catalog::official_plugins() { for metadata in catalog::official_plugins() {
tracing::trace!(plugin = %metadata.id, package = %metadata.package, "checking official plugin state"); tracing::trace!(plugin = %metadata.id, package = %metadata.package, "checking official plugin state");
let installed = registry let installed = registry
@@ -137,8 +146,20 @@ fn main() {
.iter() .iter()
.find(|plugin| plugin.metadata().id == metadata.id); .find(|plugin| plugin.metadata().id == metadata.id);
let state = match installed.map(|plugin| plugin.availability()) { let state = match installed.map(|plugin| plugin.availability()) {
Some(Ok(())) => "installed".to_owned(), Some(Ok(())) => {
Some(Err(error)) => format!("unavailable: {error}"), let info = &registry.info[&metadata.id];
format!(
"{} (API v{}, engine {})",
info.version, info.protocol, info.engine_version
)
}
Some(Err(error)) => {
let info = &registry.info[&metadata.id];
format!(
"{} (API v{}, engine {}, unavailable: {error})",
info.version, info.protocol, info.engine_version
)
}
None => "not installed".to_owned(), None => "not installed".to_owned(),
}; };
tracing::trace!(plugin = %metadata.id, state, "resolved official plugin state"); tracing::trace!(plugin = %metadata.id, state, "resolved official plugin state");
@@ -147,6 +168,31 @@ fn main() {
metadata.package, state, metadata.description metadata.package, state, metadata.description
); );
} }
let official: HashSet<_> = catalog::official_plugins()
.into_iter()
.map(|metadata| metadata.id)
.collect();
for plugin in &registry.plugins {
let metadata = plugin.metadata();
if official.contains(&metadata.id) {
continue;
}
let info = &registry.info[&metadata.id];
let state = match plugin.availability() {
Ok(()) => format!(
"{} (API v{}, engine {})",
info.version, info.protocol, info.engine_version
),
Err(error) => format!(
"{} (API v{}, engine {}, unavailable: {error})",
info.version, info.protocol, info.engine_version
),
};
println!(
" {:32} {:28} {}",
metadata.package, state, metadata.description
);
}
tracing::debug!("finished listing official plugins"); tracing::debug!("finished listing official plugins");
return; return;
} }
+65 -117
View File
@@ -5,16 +5,14 @@
// the Free Software Foundation, version 3 exclusively. // the Free Software Foundation, version 3 exclusively.
use convertis_plugin_api::{ use convertis_plugin_api::{
AVAILABILITY_SYMBOL_V2, CONVERT_SYMBOL_V2, ConversionRequest, FREE_SYMBOL_V2, AVAILABILITY_SYMBOL_V2, CONVERT_SYMBOL_V2, ConversionRequest, FREE_SYMBOL_V2, MANIFEST_SYMBOL,
LEGACY_FACTORY_SYMBOL_V1, LEGACY_MANIFEST_SYMBOL_V1, MANIFEST_SYMBOL, METADATA_SYMBOL_V2, METADATA_SYMBOL_V2, Plugin, PluginConvertCall, PluginJsonCall, PluginManifest, PluginMetadata,
Plugin, PluginConvertCall, PluginFactory, PluginJsonCall, PluginManifest, PluginMetadata,
PluginStringFree, WireResponse, PluginStringFree, WireResponse,
}; };
use libloading::Library; use libloading::Library;
use semver::{Version, VersionReq};
use serde::{Deserialize, de::DeserializeOwned}; use serde::{Deserialize, de::DeserializeOwned};
use std::{ use std::{
collections::HashSet, collections::{HashMap, HashSet},
ffi::{CStr, CString, c_char}, ffi::{CStr, CString, c_char},
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -26,18 +24,9 @@ const SUPPORTED_PROTOCOLS: &[u32] = &[2];
struct StableManifest { struct StableManifest {
manifest_version: u32, manifest_version: u32,
protocol_versions: Vec<u32>, protocol_versions: Vec<u32>,
engine_requirement: String,
plugin_id: String, plugin_id: String,
plugin_version: String, plugin_version: String,
target: String,
}
#[derive(Debug, Deserialize)]
struct LegacyManifestV1 {
api_version: u32,
engine_version: String, engine_version: String,
plugin_id: String,
rustc_version: String,
target: String, target: String,
} }
@@ -48,6 +37,20 @@ struct ProtocolV2Plugin {
free: PluginStringFree, free: PluginStringFree,
} }
#[derive(Clone, Debug)]
pub struct PluginInfo {
pub version: String,
pub protocol: u32,
pub engine_version: String,
}
struct LoadedPlugin {
library: Library,
plugin: Box<dyn Plugin>,
id: String,
info: PluginInfo,
}
impl ProtocolV2Plugin { impl ProtocolV2Plugin {
unsafe fn call<T: DeserializeOwned>( unsafe fn call<T: DeserializeOwned>(
&self, &self,
@@ -107,6 +110,7 @@ pub struct PluginRegistry {
pub plugins: Vec<Box<dyn Plugin>>, pub plugins: Vec<Box<dyn Plugin>>,
libraries: Vec<Library>, libraries: Vec<Library>,
pub diagnostics: Vec<String>, pub diagnostics: Vec<String>,
pub info: HashMap<String, PluginInfo>,
} }
impl PluginRegistry { impl PluginRegistry {
@@ -151,6 +155,7 @@ impl PluginRegistry {
plugins: Vec::new(), plugins: Vec::new(),
libraries: Vec::new(), libraries: Vec::new(),
diagnostics: Vec::new(), diagnostics: Vec::new(),
info: HashMap::new(),
}; };
let mut seen_paths = HashSet::new(); let mut seen_paths = HashSet::new();
let mut seen_ids = HashSet::new(); let mut seen_ids = HashSet::new();
@@ -198,11 +203,18 @@ impl PluginRegistry {
} }
tracing::debug!(path = %path.display(), "loading plugin candidate"); tracing::debug!(path = %path.display(), "loading plugin candidate");
match unsafe { Self::load_one(&path) } { match unsafe { Self::load_one(&path) } {
Ok((library, plugin, id, protocol)) => { Ok(loaded) => {
let LoadedPlugin {
library,
plugin,
id,
info,
} = loaded;
if seen_ids.insert(id.clone()) { if seen_ids.insert(id.clone()) {
tracing::info!(plugin = %id, protocol, path = %path.display(), "loaded plugin"); tracing::info!(plugin = %id, protocol = info.protocol, path = %path.display(), "loaded plugin");
registry.plugins.push(plugin); registry.plugins.push(plugin);
registry.libraries.push(library); registry.libraries.push(library);
registry.info.insert(id, info);
} else { } else {
tracing::debug!(plugin = %id, path = %path.display(), "ignored duplicate plugin"); tracing::debug!(plugin = %id, path = %path.display(), "ignored duplicate plugin");
registry.diagnostics.push(format!( registry.diagnostics.push(format!(
@@ -228,35 +240,30 @@ impl PluginRegistry {
registry registry
} }
unsafe fn load_one(path: &Path) -> Result<(Library, Box<dyn Plugin>, String, u32), String> { unsafe fn load_one(path: &Path) -> Result<LoadedPlugin, String> {
tracing::trace!(path = %path.display(), "opening dynamic library"); tracing::trace!(path = %path.display(), "opening dynamic library");
let library = unsafe { Library::new(path) }.map_err(|error| error.to_string())?; let library = unsafe { Library::new(path) }.map_err(|error| error.to_string())?;
tracing::trace!(path = %path.display(), "dynamic library opened"); tracing::trace!(path = %path.display(), "dynamic library opened");
if let Ok(manifest_fn) = unsafe { library.get::<PluginManifest>(MANIFEST_SYMBOL) } { let manifest_fn = unsafe { library.get::<PluginManifest>(MANIFEST_SYMBOL) }
tracing::trace!(path = %path.display(), "found stable plugin manifest entry point"); .map_err(|error| format!("missing protocol v2 manifest: {error}"))?;
tracing::trace!(path = %path.display(), "found plugin manifest entry point");
let manifest: StableManifest = unsafe { read_manifest(*manifest_fn) }?; let manifest: StableManifest = unsafe { read_manifest(*manifest_fn) }?;
let protocol = validate_stable_manifest(&manifest, env!("CARGO_PKG_VERSION"))?; let protocol = validate_stable_manifest(&manifest)?;
tracing::debug!( tracing::debug!(
plugin = %manifest.plugin_id, plugin = %manifest.plugin_id,
plugin_version = %manifest.plugin_version, plugin_version = %manifest.plugin_version,
protocol, protocol,
engine_requirement = %manifest.engine_requirement,
"negotiated plugin protocol" "negotiated plugin protocol"
); );
let metadata_call = *unsafe { library.get::<PluginJsonCall>(METADATA_SYMBOL_V2) } let metadata_call = *unsafe { library.get::<PluginJsonCall>(METADATA_SYMBOL_V2) }
.map_err(|error| format!("protocol v2 metadata entry point is missing: {error}"))?; .map_err(|error| format!("protocol v2 metadata entry point is missing: {error}"))?;
let availability = *unsafe { library.get::<PluginJsonCall>(AVAILABILITY_SYMBOL_V2) } let availability = *unsafe { library.get::<PluginJsonCall>(AVAILABILITY_SYMBOL_V2) }
.map_err(|error| { .map_err(|error| format!("protocol v2 availability entry point is missing: {error}"))?;
format!("protocol v2 availability entry point is missing: {error}") let convert = *unsafe { library.get::<PluginConvertCall>(CONVERT_SYMBOL_V2) }
})?; .map_err(|error| format!("protocol v2 conversion entry point is missing: {error}"))?;
let convert = *unsafe { library.get::<PluginConvertCall>(CONVERT_SYMBOL_V2) }.map_err( let free = *unsafe { library.get::<PluginStringFree>(FREE_SYMBOL_V2) }
|error| format!("protocol v2 conversion entry point is missing: {error}"), .map_err(|error| format!("protocol v2 string-free entry point is missing: {error}"))?;
)?;
let free =
*unsafe { library.get::<PluginStringFree>(FREE_SYMBOL_V2) }.map_err(|error| {
format!("protocol v2 string-free entry point is missing: {error}")
})?;
tracing::trace!(plugin = %manifest.plugin_id, "resolved all protocol v2 entry points"); tracing::trace!(plugin = %manifest.plugin_id, "resolved all protocol v2 entry points");
let metadata: PluginMetadata = unsafe { let metadata: PluginMetadata = unsafe {
let pointer = metadata_call(); let pointer = metadata_call();
@@ -266,60 +273,35 @@ impl PluginRegistry {
return Err("manifest and plugin metadata IDs differ".to_owned()); return Err("manifest and plugin metadata IDs differ".to_owned());
} }
let id = manifest.plugin_id; let id = manifest.plugin_id;
let version = manifest.plugin_version;
let engine_version = manifest.engine_version;
let plugin = ProtocolV2Plugin { let plugin = ProtocolV2Plugin {
metadata, metadata,
availability, availability,
convert, convert,
free, free,
}; };
return Ok((library, Box::new(plugin), id, protocol)); Ok(LoadedPlugin {
} library,
plugin: Box::new(plugin),
tracing::trace!(path = %path.display(), "stable manifest absent; trying legacy protocol v1 adapter"); id,
let manifest_fn = unsafe { library.get::<PluginManifest>(LEGACY_MANIFEST_SYMBOL_V1) } info: PluginInfo {
.map_err(|error| format!("missing stable or legacy ABI manifest: {error}"))?; version,
let manifest: LegacyManifestV1 = unsafe { read_manifest(*manifest_fn) }?; protocol,
validate_legacy_manifest(&manifest)?;
let factory = unsafe { library.get::<PluginFactory>(LEGACY_FACTORY_SYMBOL_V1) }
.map_err(|error| format!("missing legacy Rust plugin factory: {error}"))?;
let plugin = unsafe { factory() };
if plugin.metadata().id != manifest.plugin_id {
return Err("legacy manifest and plugin IDs differ".to_owned());
}
tracing::debug!(
plugin = %manifest.plugin_id,
built_for_engine = %manifest.engine_version,
"loaded plugin through legacy protocol v1 adapter"
);
Ok((library, plugin, manifest.plugin_id, 1))
}
}
fn validate_stable_manifest(
manifest: &StableManifest,
engine_version: &str,
) -> Result<u32, String> {
tracing::trace!(
?manifest,
engine_version, engine_version,
"validating stable plugin manifest" },
); })
}
}
fn validate_stable_manifest(manifest: &StableManifest) -> Result<u32, String> {
tracing::trace!(?manifest, "validating plugin manifest");
if manifest.manifest_version != 1 { if manifest.manifest_version != 1 {
return Err(format!( return Err(format!(
"manifest format {} is not supported", "manifest format {} is not supported",
manifest.manifest_version manifest.manifest_version
)); ));
} }
let engine = Version::parse(engine_version)
.map_err(|error| format!("engine has an invalid version: {error}"))?;
let requirement = VersionReq::parse(&manifest.engine_requirement)
.map_err(|error| format!("plugin has an invalid engine requirement: {error}"))?;
if !requirement.matches(&engine) {
return Err(format!(
"plugin requires engine {}, but this engine is {}",
manifest.engine_requirement, engine_version
));
}
if manifest.target != env!("CONVERTIS_TARGET") { if manifest.target != env!("CONVERTIS_TARGET") {
return Err(format!( return Err(format!(
"plugin targets {}, but the engine targets {}", "plugin targets {}, but the engine targets {}",
@@ -340,39 +322,6 @@ fn validate_stable_manifest(
}) })
} }
fn validate_legacy_manifest(manifest: &LegacyManifestV1) -> Result<(), String> {
tracing::trace!(?manifest, "validating legacy plugin manifest");
if manifest.api_version != 1 {
return Err(format!(
"legacy plugin API {} is not supported",
manifest.api_version
));
}
if manifest.engine_version != convertis_plugin_api::ENGINE_VERSION {
tracing::debug!(
plugin = %manifest.plugin_id,
built_for_engine = %manifest.engine_version,
current_engine = convertis_plugin_api::ENGINE_VERSION,
"legacy engine release differs; continuing because protocol v1 is retained"
);
}
if manifest.rustc_version != env!("CONVERTIS_RUSTC_VERSION") {
return Err(format!(
"legacy plugin was built with {}, but the engine uses {}; protocol v1 uses the unstable Rust ABI",
manifest.rustc_version,
env!("CONVERTIS_RUSTC_VERSION")
));
}
if manifest.target != env!("CONVERTIS_TARGET") {
return Err(format!(
"legacy plugin targets {}, but the engine targets {}",
manifest.target,
env!("CONVERTIS_TARGET")
));
}
Ok(())
}
unsafe fn read_manifest<T: DeserializeOwned>(manifest: PluginManifest) -> Result<T, String> { unsafe fn read_manifest<T: DeserializeOwned>(manifest: PluginManifest) -> Result<T, String> {
tracing::trace!("calling plugin manifest entry point"); tracing::trace!("calling plugin manifest entry point");
let pointer = unsafe { manifest() }; let pointer = unsafe { manifest() };
@@ -430,33 +379,32 @@ unsafe fn decode_response<T: DeserializeOwned>(
mod tests { mod tests {
use super::*; use super::*;
fn manifest(requirement: &str, protocols: &[u32]) -> StableManifest { fn manifest(protocols: &[u32]) -> StableManifest {
StableManifest { StableManifest {
manifest_version: 1, manifest_version: 1,
protocol_versions: protocols.to_vec(), protocol_versions: protocols.to_vec(),
engine_requirement: requirement.into(),
plugin_id: "example".into(), plugin_id: "example".into(),
plugin_version: "0.3.0".into(), plugin_version: "0.3.0".into(),
engine_version: "0.3.0".into(),
target: env!("CONVERTIS_TARGET").into(), target: env!("CONVERTIS_TARGET").into(),
} }
} }
#[test]
fn current_plugin_requirement_accepts_future_engines() {
let manifest = manifest(">=0.3.0", &[2]);
assert_eq!(validate_stable_manifest(&manifest, "0.3.0"), Ok(2));
assert_eq!(validate_stable_manifest(&manifest, "99.0.0"), Ok(2));
}
#[test] #[test]
fn protocol_is_negotiated_independently_of_release_version() { fn protocol_is_negotiated_independently_of_release_version() {
let manifest = manifest(">=0.1.0", &[1, 2, 3]); let manifest = manifest(&[2, 3]);
assert_eq!(validate_stable_manifest(&manifest, "1.0.0"), Ok(2)); assert_eq!(validate_stable_manifest(&manifest), Ok(2));
} }
#[test] #[test]
fn unsupported_protocol_is_rejected() { fn unsupported_protocol_is_rejected() {
let error = validate_stable_manifest(&manifest(">=0.3.0", &[7]), "0.3.0").unwrap_err(); let error = validate_stable_manifest(&manifest(&[7])).unwrap_err();
assert!(error.contains("not supported"));
}
#[test]
fn api_v1_is_permanently_rejected() {
let error = validate_stable_manifest(&manifest(&[1])).unwrap_err();
assert!(error.contains("not supported")); assert!(error.contains("not supported"));
} }
} }