Bunch of changes, like a lot

This commit is contained in:
Elias Wendland
2026-07-17 18:58:52 +02:00
parent f07f54d4c1
commit 3297f60f00
25 changed files with 566 additions and 285 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
lfs: true
- uses: dtolnay/rust-toolchain@stable
- 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
run: |
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 }}
steps:
- uses: actions/checkout@v4
- name: Check release commit and version
with:
fetch-depth: 0
- name: Check for a new stable engine version
id: check
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
BEFORE: ${{ gitea.event.before }}
run: |
if echo "$COMMIT_MSG" | grep -Eq 'Release [vV]?[0-9]+\.[0-9]+\.[0-9]+'; then
version=$(echo "$COMMIT_MSG" | grep -Eo 'Release [vV]?[0-9]+\.[0-9]+\.[0-9]+' | head -n1 | sed -E 's/Release [vV]?//')
package_version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)
test "$version" = "$package_version"
version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)
previous=""
if [[ -n "$BEFORE" ]] && git cat-file -e "$BEFORE:Cargo.toml" 2>/dev/null; then
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 "version=$version" >> "$GITHUB_OUTPUT"
else
@@ -37,15 +41,14 @@ jobs:
with:
targets: x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace --locked
- run: cargo build --release --workspace --locked --target x86_64-unknown-linux-gnu
- run: cargo test --package convertis --locked
- run: cargo build --release --package convertis --locked --target x86_64-unknown-linux-gnu
- uses: actions/upload-artifact@v3
with:
name: build-linux
if-no-files-found: error
path: |
target/x86_64-unknown-linux-gnu/release/convertis
target/x86_64-unknown-linux-gnu/release/libconvertis_*.so
target/man/convertis.1
package:
@@ -123,12 +126,9 @@ jobs:
merge-multiple: false
- name: Prepare release assets
run: |
mkdir -p release-assets/plugins
mkdir -p release-assets
binary=$(find all-artifacts/build-linux -type f -name convertis | head -n1)
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
run: |
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
files: |
release-assets/convertis-x86_64-unknown-linux-gnu
release-assets/convertis-plugins-${{ needs.check-release.outputs.version }}-x86_64-unknown-linux-gnu.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish packages to Gitea registries
Generated
+10 -17
View File
@@ -321,7 +321,6 @@ dependencies = [
"gif",
"infer",
"libloading",
"semver",
"serde",
"serde_json",
"tempfile",
@@ -331,14 +330,14 @@ dependencies = [
[[package]]
name = "convertis-ffmpeg-audio"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"convertis-plugin-api",
]
[[package]]
name = "convertis-ffmpeg-frames-to-video"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"convertis-plugin-api",
"serde",
@@ -347,14 +346,14 @@ dependencies = [
[[package]]
name = "convertis-ffmpeg-video"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"convertis-plugin-api",
]
[[package]]
name = "convertis-ffmpeg-video-to-frames"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"convertis-plugin-api",
"serde",
@@ -363,14 +362,14 @@ dependencies = [
[[package]]
name = "convertis-graphicsmagick"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"convertis-plugin-api",
]
[[package]]
name = "convertis-html"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"base64",
"convertis-plugin-api",
@@ -379,7 +378,7 @@ dependencies = [
[[package]]
name = "convertis-image-ascii"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"convertis-plugin-api",
"image",
@@ -387,14 +386,14 @@ dependencies = [
[[package]]
name = "convertis-imagemagick"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"convertis-plugin-api",
]
[[package]]
name = "convertis-native-image"
version = "0.3.0-dev"
version = "0.1.0"
dependencies = [
"convertis-plugin-api",
"image",
@@ -402,7 +401,7 @@ dependencies = [
[[package]]
name = "convertis-plugin-api"
version = "0.3.0-dev"
version = "2.0.0"
dependencies = [
"serde",
"serde_json",
@@ -1137,12 +1136,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
+2 -11
View File
@@ -2,15 +2,7 @@
members = [
".",
"crates/convertis-plugin-api",
"plugins/ffmpeg-audio",
"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",
"plugins/*",
]
resolver = "2"
@@ -22,7 +14,7 @@ license = "GPL-3.0-only"
repository = "https://git.ewenlau.net/ewenlau/convertis"
[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"
image = "0.25.10"
serde = { version = "1", features = ["derive"] }
@@ -55,7 +47,6 @@ gif = "0.14"
libloading = "0.8"
serde.workspace = true
serde_json.workspace = true
semver = "1"
tempfile.workspace = true
tracing = "0.1.44"
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
```
Convenience bundles are available:
- `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.
Every plugin is independently versioned and published, so updating one converter does not rebuild or replace unrelated plugins.
When no installed route can perform a conversion, Convertis prints the individual package or packages that provide one.
### 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 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.
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.
## Usage
@@ -108,9 +102,12 @@ Useful inspection commands:
```sh
convertis --list-plugins
convertis --list-formats
convertis --plugin-api
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:
```sh
@@ -125,27 +122,42 @@ Plugin settings use repeatable `--option key=value` arguments. A plugin-qualifie
## 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
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
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;
- every wire-protocol version implemented by the plugin;
- 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.
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
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "convertis-plugin-api"
version.workspace = true
version = "2.0.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
+3 -10
View File
@@ -21,18 +21,12 @@ use std::{
/// The newest stable wire protocol implemented by this SDK.
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 METADATA_SYMBOL_V2: &[u8] = b"convertis_plugin_metadata_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 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)]
pub enum ArtifactKind {
File,
@@ -109,7 +103,6 @@ pub trait Plugin: Send + Sync {
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 PluginJsonCall = unsafe extern "C" fn() -> *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)]
pub extern "C" fn convertis_plugin_manifest() -> *const std::ffi::c_char {
static MANIFEST: &str = concat!(
"{\"manifest_version\":1,\"protocol_versions\":[2],\"engine_requirement\":\">=",
env!("CARGO_PKG_VERSION"),
"\",\"plugin_id\":\"",
"{\"manifest_version\":1,\"protocol_versions\":[2],\"plugin_id\":\"",
$id,
"\",\"plugin_version\":\"",
env!("CARGO_PKG_VERSION"),
"\",\"engine_version\":\"",
env!("CONVERTIS_ENGINE_VERSION"),
"\",\"target\":\"",
env!("CONVERTIS_TARGET"),
"\"}\0"
+4 -59
View File
@@ -9,18 +9,10 @@ mkdir -p "$dist_dir"
if [[ "$packager" == "deb" ]]; then
architecture=amd64
engine_dependency="convertis (= ${version}-1)"
system_dependencies=("libc6" "libgcc-s1")
ffmpeg_dependencies=("ffmpeg")
imagemagick_dependencies=("imagemagick")
graphicsmagick_dependencies=("graphicsmagick")
else
architecture=amd64
engine_dependency="convertis = ${version}-1"
system_dependencies=("glibc" "libgcc")
ffmpeg_dependencies=("/usr/bin/ffmpeg" "/usr/bin/ffprobe")
imagemagick_dependencies=("/usr/bin/magick")
graphicsmagick_dependencies=("/usr/bin/gm")
fi
make_package() {
@@ -46,6 +38,10 @@ make_package() {
echo "description: $description"
echo "license: GPL-3.0-only"
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
echo "contents:"
for mapping in "${contents[@]}"; do
@@ -71,14 +67,6 @@ make_package() {
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" \
"content:${release_root}/convertis::/usr/bin/convertis" \
"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:packaging/plugin-catalog.json::/usr/share/convertis/plugin-catalog.json" \
"${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,
"engine_version": "0.3.0",
"plugin_api_version": 2,
"plugins": [
{ "id": "ffmpeg-audio", "package": "convertis-ffmpeg-audio" },
{ "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
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::process::Command;
use std::{path::Path, process::Command};
fn main() {
let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
let version = Command::new(rustc)
.arg("--version")
fn latest_engine_version(workspace: &Path) -> String {
if let Ok(version) = std::env::var("CONVERTIS_ENGINE_VERSION") {
return version;
}
Command::new("git")
.args(["describe", "--tags", "--abbrev=0", "--match", "v[0-9]*"])
.current_dir(workspace)
.output()
.ok()
.filter(|output| output.status.success())
.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());
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}");
}
+8 -1
View File
@@ -1,12 +1,19 @@
[package]
name = "convertis-ffmpeg-audio"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.1.0"
description = "FFmpeg audio plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.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]
crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package]
name = "convertis-ffmpeg-frames-to-video"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.3.1-dev"
description = "FFmpeg frames-to-video plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.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]
crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package]
name = "convertis-ffmpeg-video-to-frames"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.1.0"
description = "FFmpeg video-to-frames plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.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]
crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package]
name = "convertis-ffmpeg-video"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.3.1-dev"
description = "FFmpeg video plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.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]
crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package]
name = "convertis-graphicsmagick"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.1.0"
description = "GraphicsMagick plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[package.metadata.convertis]
id = "graphicsmagick"
api-version = 2
debian-dependencies = ["graphicsmagick"]
rpm-dependencies = ["/usr/bin/gm"]
[lib]
crate-type = ["cdylib"]
+6 -1
View File
@@ -1,12 +1,17 @@
[package]
name = "convertis-html"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.1.0"
description = "Self-contained HTML plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[package.metadata.convertis]
id = "html"
api-version = 2
[lib]
crate-type = ["cdylib"]
+6 -1
View File
@@ -1,12 +1,17 @@
[package]
name = "convertis-image-ascii"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.1.0"
description = "Image-to-ASCII plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[package.metadata.convertis]
id = "image-ascii"
api-version = 2
[lib]
crate-type = ["cdylib"]
+8 -1
View File
@@ -1,12 +1,19 @@
[package]
name = "convertis-imagemagick"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.1.0"
description = "ImageMagick plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[package.metadata.convertis]
id = "imagemagick"
api-version = 2
debian-dependencies = ["imagemagick"]
rpm-dependencies = ["/usr/bin/magick"]
[lib]
crate-type = ["cdylib"]
+6 -1
View File
@@ -1,12 +1,17 @@
[package]
name = "convertis-native-image"
build = "../../plugin-build.rs"
version.workspace = true
version = "0.1.0"
description = "Native common-image plugin for Convertis"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[package.metadata.convertis]
id = "native-image"
api-version = 2
[lib]
crate-type = ["cdylib"]
+7
View File
@@ -18,6 +18,13 @@ use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(about = "A modular file converter.", long_about = None)]
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(
long,
help = "List formats supported by installed plugins.",
+50 -4
View File
@@ -117,6 +117,15 @@ fn main() {
);
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);
tracing::trace!(
plugin_count = registry.plugins.len(),
@@ -128,8 +137,8 @@ fn main() {
}
if args.list_plugins {
tracing::debug!("listing official plugins");
println!("Official plugins:");
tracing::debug!("listing installed and official plugins");
println!("Plugins:");
for metadata in catalog::official_plugins() {
tracing::trace!(plugin = %metadata.id, package = %metadata.package, "checking official plugin state");
let installed = registry
@@ -137,8 +146,20 @@ fn main() {
.iter()
.find(|plugin| plugin.metadata().id == metadata.id);
let state = match installed.map(|plugin| plugin.availability()) {
Some(Ok(())) => "installed".to_owned(),
Some(Err(error)) => format!("unavailable: {error}"),
Some(Ok(())) => {
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(),
};
tracing::trace!(plugin = %metadata.id, state, "resolved official plugin state");
@@ -147,6 +168,31 @@ fn main() {
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");
return;
}
+85 -137
View File
@@ -5,16 +5,14 @@
// the Free Software Foundation, version 3 exclusively.
use convertis_plugin_api::{
AVAILABILITY_SYMBOL_V2, CONVERT_SYMBOL_V2, ConversionRequest, FREE_SYMBOL_V2,
LEGACY_FACTORY_SYMBOL_V1, LEGACY_MANIFEST_SYMBOL_V1, MANIFEST_SYMBOL, METADATA_SYMBOL_V2,
Plugin, PluginConvertCall, PluginFactory, PluginJsonCall, PluginManifest, PluginMetadata,
AVAILABILITY_SYMBOL_V2, CONVERT_SYMBOL_V2, ConversionRequest, FREE_SYMBOL_V2, MANIFEST_SYMBOL,
METADATA_SYMBOL_V2, Plugin, PluginConvertCall, PluginJsonCall, PluginManifest, PluginMetadata,
PluginStringFree, WireResponse,
};
use libloading::Library;
use semver::{Version, VersionReq};
use serde::{Deserialize, de::DeserializeOwned};
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
ffi::{CStr, CString, c_char},
fs,
path::{Path, PathBuf},
@@ -26,18 +24,9 @@ const SUPPORTED_PROTOCOLS: &[u32] = &[2];
struct StableManifest {
manifest_version: u32,
protocol_versions: Vec<u32>,
engine_requirement: String,
plugin_id: String,
plugin_version: String,
target: String,
}
#[derive(Debug, Deserialize)]
struct LegacyManifestV1 {
api_version: u32,
engine_version: String,
plugin_id: String,
rustc_version: String,
target: String,
}
@@ -48,6 +37,20 @@ struct ProtocolV2Plugin {
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 {
unsafe fn call<T: DeserializeOwned>(
&self,
@@ -107,6 +110,7 @@ pub struct PluginRegistry {
pub plugins: Vec<Box<dyn Plugin>>,
libraries: Vec<Library>,
pub diagnostics: Vec<String>,
pub info: HashMap<String, PluginInfo>,
}
impl PluginRegistry {
@@ -151,6 +155,7 @@ impl PluginRegistry {
plugins: Vec::new(),
libraries: Vec::new(),
diagnostics: Vec::new(),
info: HashMap::new(),
};
let mut seen_paths = HashSet::new();
let mut seen_ids = HashSet::new();
@@ -198,11 +203,18 @@ impl PluginRegistry {
}
tracing::debug!(path = %path.display(), "loading plugin candidate");
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()) {
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.libraries.push(library);
registry.info.insert(id, info);
} else {
tracing::debug!(plugin = %id, path = %path.display(), "ignored duplicate plugin");
registry.diagnostics.push(format!(
@@ -228,98 +240,68 @@ impl PluginRegistry {
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");
let library = unsafe { Library::new(path) }.map_err(|error| error.to_string())?;
tracing::trace!(path = %path.display(), "dynamic library opened");
if let Ok(manifest_fn) = unsafe { library.get::<PluginManifest>(MANIFEST_SYMBOL) } {
tracing::trace!(path = %path.display(), "found stable plugin manifest entry point");
let manifest: StableManifest = unsafe { read_manifest(*manifest_fn) }?;
let protocol = validate_stable_manifest(&manifest, env!("CARGO_PKG_VERSION"))?;
tracing::debug!(
plugin = %manifest.plugin_id,
plugin_version = %manifest.plugin_version,
protocol,
engine_requirement = %manifest.engine_requirement,
"negotiated plugin protocol"
);
let metadata_call = *unsafe { library.get::<PluginJsonCall>(METADATA_SYMBOL_V2) }
.map_err(|error| format!("protocol v2 metadata entry point is missing: {error}"))?;
let availability = *unsafe { library.get::<PluginJsonCall>(AVAILABILITY_SYMBOL_V2) }
.map_err(|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 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");
let metadata: PluginMetadata = unsafe {
let pointer = metadata_call();
decode_response(pointer, free, "metadata")
}?;
if metadata.id != manifest.plugin_id {
return Err("manifest and plugin metadata IDs differ".to_owned());
}
let id = manifest.plugin_id;
let plugin = ProtocolV2Plugin {
metadata,
availability,
convert,
free,
};
return Ok((library, Box::new(plugin), id, protocol));
}
tracing::trace!(path = %path.display(), "stable manifest absent; trying legacy protocol v1 adapter");
let manifest_fn = unsafe { library.get::<PluginManifest>(LEGACY_MANIFEST_SYMBOL_V1) }
.map_err(|error| format!("missing stable or legacy ABI manifest: {error}"))?;
let manifest: LegacyManifestV1 = unsafe { read_manifest(*manifest_fn) }?;
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());
}
let manifest_fn = unsafe { library.get::<PluginManifest>(MANIFEST_SYMBOL) }
.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 protocol = validate_stable_manifest(&manifest)?;
tracing::debug!(
plugin = %manifest.plugin_id,
built_for_engine = %manifest.engine_version,
"loaded plugin through legacy protocol v1 adapter"
plugin_version = %manifest.plugin_version,
protocol,
"negotiated plugin protocol"
);
Ok((library, plugin, manifest.plugin_id, 1))
let metadata_call = *unsafe { library.get::<PluginJsonCall>(METADATA_SYMBOL_V2) }
.map_err(|error| format!("protocol v2 metadata entry point is missing: {error}"))?;
let availability = *unsafe { library.get::<PluginJsonCall>(AVAILABILITY_SYMBOL_V2) }
.map_err(|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 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");
let metadata: PluginMetadata = unsafe {
let pointer = metadata_call();
decode_response(pointer, free, "metadata")
}?;
if metadata.id != manifest.plugin_id {
return Err("manifest and plugin metadata IDs differ".to_owned());
}
let id = manifest.plugin_id;
let version = manifest.plugin_version;
let engine_version = manifest.engine_version;
let plugin = ProtocolV2Plugin {
metadata,
availability,
convert,
free,
};
Ok(LoadedPlugin {
library,
plugin: Box::new(plugin),
id,
info: PluginInfo {
version,
protocol,
engine_version,
},
})
}
}
fn validate_stable_manifest(
manifest: &StableManifest,
engine_version: &str,
) -> Result<u32, String> {
tracing::trace!(
?manifest,
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 {
return Err(format!(
"manifest format {} is not supported",
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") {
return Err(format!(
"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> {
tracing::trace!("calling plugin manifest entry point");
let pointer = unsafe { manifest() };
@@ -430,33 +379,32 @@ unsafe fn decode_response<T: DeserializeOwned>(
mod tests {
use super::*;
fn manifest(requirement: &str, protocols: &[u32]) -> StableManifest {
fn manifest(protocols: &[u32]) -> StableManifest {
StableManifest {
manifest_version: 1,
protocol_versions: protocols.to_vec(),
engine_requirement: requirement.into(),
plugin_id: "example".into(),
plugin_version: "0.3.0".into(),
engine_version: "0.3.0".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]
fn protocol_is_negotiated_independently_of_release_version() {
let manifest = manifest(">=0.1.0", &[1, 2, 3]);
assert_eq!(validate_stable_manifest(&manifest, "1.0.0"), Ok(2));
let manifest = manifest(&[2, 3]);
assert_eq!(validate_stable_manifest(&manifest), Ok(2));
}
#[test]
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"));
}
}