feat(plugins): drop plugin API v2

A V2 plugin passed the version gate and then failed silently while
registering its ribbon, since the ribbon wire types have moved on. The
host now loads V3 and newer, so V2 plugins are rejected up front with a
version message. The V2 compatibility fixture crate goes too.

Fixes #1085
Fixes #987

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-09-28 20:32:02 +03:00
1 parent c45f22652d
commit 71b300cc44
6 files changed
+25 -110

No files matched your search

+1 -1
View File
@@ -1284,7 +1284,7 @@ fn generate_version_info(out_dir: &Path) {
"acadrust_source": opencadcodec.source.as_ref().map(|s| s.to_string()),
"rustc_version": rustc_version,
"api_version": 7,
"api_version_min_supported": 2,
"api_version_min_supported": 3,
"build_timestamp": build_timestamp,
});
fs::write(out_dir.join("version_info.json"), info.to_string()).unwrap();
+11 -9
View File
@@ -26,12 +26,14 @@ pub const API_VERSION: u32 = 7;
/// Oldest plugin API major the current host still loads. This keeps previously
/// compiled cdylibs usable as long as their vtable layout is a prefix of the
/// current `HostApi` trait.
pub const API_VERSION_MIN_SUPPORTED: u32 = 2;
/// current `HostApi` trait. V2 is no longer loaded: its ribbon wire types
/// predate the current ones, so a V2 plugin passed the gate and then failed
/// silently while registering its ribbon (#1085, #987).
pub const API_VERSION_MIN_SUPPORTED: u32 = 3;
/// Environment variable that caps the API major accepted by the host at
/// runtime. Set to `5` to disable V6 plugins, `4` to disable V5, or `2` for
/// V2-only mode.
/// runtime. Set to `5` to disable V6 plugins, `4` to disable V5, or `3` for
/// V3-only mode.
pub const MAX_API_VERSION_ENV: &str = "OCS_PLUGIN_MAX_API_VERSION";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -112,18 +114,18 @@ mod tests {
assert_eq!(effective_max_api_version(), 3);
assert!(!host_accepts_plugin_version(4));
assert!(host_accepts_plugin_version(3));
assert!(host_accepts_plugin_version(2));
assert!(!host_accepts_plugin_version(2));
std::env::remove_var(MAX_API_VERSION_ENV);
}
#[test]
fn effective_max_v2_only_mode() {
fn effective_max_below_minimum_is_v3_only_mode() {
let _guard = ENV_LOCK.lock().unwrap();
std::env::set_var(MAX_API_VERSION_ENV, "2");
assert_eq!(effective_max_api_version(), 2);
assert_eq!(effective_max_api_version(), 3);
assert!(!host_accepts_plugin_version(4));
assert!(!host_accepts_plugin_version(3));
assert!(host_accepts_plugin_version(2));
assert!(host_accepts_plugin_version(3));
assert!(!host_accepts_plugin_version(2));
std::env::remove_var(MAX_API_VERSION_ENV);
}
+4 -1
View File
@@ -148,7 +148,10 @@ mod tests {
Some(env!("CARGO_PKG_VERSION"))
);
assert_eq!(value["api_version"].as_i64(), Some(crate::API_VERSION as i64));
assert_eq!(value["api_version_min_supported"].as_i64(), Some(2));
assert_eq!(
value["api_version_min_supported"].as_i64(),
Some(crate::API_VERSION_MIN_SUPPORTED as i64)
);
// acadrust_version should have a patch component (e.g. "0.4.0").
let opencadcodec = value["acadrust_version"].as_str().expect("acadrust_version string");
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "plugin-template-api2"
version = "0.1.0"
edition = "2021"
description = "API v2 compatibility fixture for the out-of-process plugin path."
license = "GPL-3.0-only"
[lib]
crate-type = ["cdylib"]
[dependencies]
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
acadrust = { git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8a28c21", features = ["serde"] }
-79
View File
@@ -1,79 +0,0 @@
//! API v2 compatibility fixture.
//!
//! Mimics an old plugin compiled against `ocs_plugin_api` v2: it reports API
//! major 2 from `ocs_plugin_api_version()` and implements only the v2 surface
//! (`HostApi` methods up to `start_interactive`). The current host must still
//! be able to load and dispatch it.
use ocs_plugin_api::host::{BuiltinPlugin, HostApi};
use ocs_plugin_api::manifest::{ApiVersion, PluginManifest};
use ocs_plugin_api::ribbon::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
static MANIFEST: PluginManifest = PluginManifest {
id: "opencad.my_plugin",
name: "My Plugin",
version: env!("CARGO_PKG_VERSION"),
description: "API v2 fixture plugin.",
api_version: ApiVersion { major: 2 },
ribbon_order: 60,
xdata_apps: &[],
command_prefixes: &["MP_"],
};
struct MyModule;
impl CadModule for MyModule {
fn id(&self) -> &'static str {
"my_plugin"
}
fn title(&self) -> &'static str {
"My Plugin"
}
fn ribbon_groups(&self) -> &[RibbonGroup] {
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
GROUPS.get_or_init(|| {
vec![RibbonGroup {
title: "Tools",
tools: vec![RibbonItem::LargeTool(ToolDef {
id: "MP_HELLO",
label: "Hello",
icon: IconKind::Glyph("*"),
event: ModuleEvent::Command("MP_HELLO".to_string()),
})],
}]
})
}
}
struct MyPlugin;
impl BuiltinPlugin for MyPlugin {
fn manifest(&self) -> &'static PluginManifest {
&MANIFEST
}
fn ribbon(&self) -> Box<dyn CadModule> {
Box::new(MyModule)
}
fn dispatch(&self, host: &mut dyn HostApi, cmd: &str) -> bool {
match cmd {
"MP_HELLO" => {
host.push_info("Hello from API v2 plugin");
true
}
_ => false,
}
}
}
// Custom C-ABI export that reports API v2, emulating an older build of
// `ocs_plugin_api::export_plugin!`.
#[no_mangle]
pub extern "C" fn ocs_plugin_api_version() -> u32 {
2
}
#[no_mangle]
pub extern "C" fn ocs_plugin_register() -> *mut Box<dyn BuiltinPlugin> {
let plugin: Box<dyn BuiltinPlugin> = Box::new(MyPlugin);
Box::into_raw(Box::new(plugin))
}
+9 -7
View File
@@ -553,16 +553,18 @@ description = "Template plugin"
repository = "https://github.com/example/opencad-my-plugin.git"
[opencad]
api_version = 2
api_version = 3
ribbon_order = 60
command_prefixes = ["MP_"]
xdata_apps = ["MYPLUGIN_RECORD"]
"#;
let p = parse_plugin_toml(toml).expect("parsed");
assert_eq!(p.api_version, 2);
assert_eq!(p.api_version, 3);
assert_eq!(p.repository.as_deref(), Some("example/opencad-my-plugin"));
assert!(p.command_prefixes.contains(&"MP_".to_string()));
assert!(p.api_compatible(), "V2 plugins must be accepted by the V4 host");
assert!(p.api_compatible(), "V3 plugins must be accepted by the current host");
let v2 = parse_plugin_toml("id=\"old\"\napi_version = 2").expect("parsed");
assert!(!v2.api_compatible(), "V2 plugins are no longer loaded");
}
#[test]
@@ -710,7 +712,7 @@ rustc_version = "{host}"
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 2
api_version = 3
[opencad]
rustc_version = "rustc 0.0.0-fake"
@@ -720,7 +722,7 @@ rustc_version = "rustc 0.0.0-fake"
assert!(p.rustc_declared);
assert!(
p.rustc_compatible(),
"API v2 plugin should bypass rustc gate"
"API v3 plugin should bypass rustc gate"
);
assert!(p.loadable());
}
@@ -826,7 +828,7 @@ acadrust_source = "{host}"
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 2
api_version = 3
[opencad]
acadrust_source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb"
@@ -836,7 +838,7 @@ acadrust_source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=0908
assert!(p.acadrust_declared);
assert!(
p.acadrust_compatible(),
"API v2 plugin should bypass opencadcodec gate"
"API v3 plugin should bypass opencadcodec gate"
);
assert!(p.loadable());
}