fix(plugins): enforce rustc compatibility

BREAKING CHANGE: API v4+ plugin packages must declare the exact rustc version used to build the binary.

Refs #901
This commit is contained in:
Hakan Seven committed 2026-08-29 15:54:41 +03:00
1 parent 4668042df5
commit 466ab99c63
9 files changed
+23 -40

No files matched your search

+1 -1
View File
@@ -54,7 +54,7 @@ The runtime enforces three gates:
1. `ocs_plugin_api_version()` exported by the cdylib must be within `[API_VERSION_MIN_SUPPORTED, effective_max_api_version()]`.
2. For v4+, `acadrust_sources_compatible(host_acadrust_source(), plugin_acadrust_source)` must be true.
3. For v4+, when the plugin declares `rustc_version`, `rustc_versions_compatible(host_rustc_version(), plugin_rustc_version)` must be true.
3. For v4+, the plugin must declare `rustc_version`, and `rustc_versions_compatible(host_rustc_version(), plugin_rustc_version)` must be true.
---
+2 -1
View File
@@ -384,7 +384,8 @@ fn generate_version_info(out_dir: &Path) {
.find(|p| p.name.as_str() == "acadrust")
.expect("acadrust package in Cargo.lock");
let rustc_version = Command::new("rustc")
let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
let rustc_version = Command::new(rustc)
.arg("--version")
.output()
.ok()
+7 -7
View File
@@ -99,10 +99,10 @@ affect whether a prebuilt plugin can safely be loaded:
- `rustc_version` — the output of `rustc --version` for the compiler that built
the host.
A plugin's `plugin.toml` may declare the same values under `[opencad]`. When
either is declared, the host refuses to load the plugin unless it matches the
host exactly. This turns the current silent runner crash into a one-line
diagnosis such as:
A plugin's `plugin.toml` declares the same values under `[opencad]`. For API v4
and later, `rustc_version` is required and the host refuses to load the plugin
unless it matches exactly. This turns the current silent runner crash into a
one-line diagnosis such as:
```
Plugin built with rustc 1.96.0, host requires 1.98.0 - rebuild required
@@ -229,12 +229,12 @@ version = "0.1.0"
description = "…"
[opencad]
api_version = 3
api_version = 4
ribbon_order = 50
command_prefixes = ["EX_"]
xdata_apps = []
# For API v4+, also record the exact rustc used to build the release asset.
# rustc_version = "rustc 1.98.0 (hash date)"
# Filled with the exact compiler output while staging the release asset.
rustc_version = "rustc 1.98.0 (hash date)"
```
The full, buildable scaffold is in [`docs/plugin-template/`](plugin-template);
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
run: |
mkdir -p dist
cp target/release/*.${{ matrix.ext }} "dist/${{ matrix.asset }}"
cp plugin.toml dist/plugin.toml
sed "s|__RUSTC_VERSION__|$(rustc --version)|" plugin.toml > dist/plugin.toml
- uses: softprops/action-gh-release@v2
with:
files: |
+2 -1
View File
@@ -28,7 +28,8 @@ folder into a new repo and rename the placeholders.
cargo build --release
mkdir -p "<config>/OpenCADStudio/plugins/opencad.my_plugin"
cp target/release/*my_plugin*.so "<config>/OpenCADStudio/plugins/opencad.my_plugin/"
cp plugin.toml "<config>/OpenCADStudio/plugins/opencad.my_plugin/"
sed "s|__RUSTC_VERSION__|$(rustc --version)|" plugin.toml > \
"<config>/OpenCADStudio/plugins/opencad.my_plugin/plugin.toml"
```
Restart Open CAD Studio: the ribbon tab appears and `MP_` commands route to your
+2 -1
View File
@@ -8,7 +8,8 @@ license = "GPL-3.0-only"
repository = "your-account/your-plugin-repo"
[opencad]
api_version = 3
api_version = 4
rustc_version = "__RUSTC_VERSION__"
ribbon_order = 60
command_prefixes = ["MP_"]
xdata_apps = ["MYPLUGIN_RECORD"]
+3 -5
View File
@@ -100,9 +100,6 @@ impl ExternalPlugin {
if !ocs_plugin_api::version_info::uses_acadrust_gate(self.api_version) {
return true;
}
if !self.rustc_declared {
return true;
}
match self.rustc_version.as_deref() {
None | Some("") => false,
Some(version) => ocs_plugin_api::version_info::rustc_versions_compatible(
@@ -534,7 +531,7 @@ xdata_apps = ["MYPLUGIN_RECORD"]
}
#[test]
fn undeclared_rustc_falls_back_to_api_gate() {
fn undeclared_rustc_is_incompatible() {
let toml = r#"
[plugin]
id = "opencad.test"
@@ -545,7 +542,8 @@ api_version = 4
let p = parse_plugin_toml(toml).expect("parsed");
assert!(!p.rustc_declared);
assert!(p.rustc_version.is_none());
assert!(p.rustc_compatible(), "undeclared rustc is treated as compatible");
assert!(!p.rustc_compatible());
assert!(!p.loadable());
}
#[test]
+3 -22
View File
@@ -242,8 +242,6 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
manifest.api_version,
) {
true
} else if !rustc_declared {
true
} else {
match rustc_version.as_deref() {
None | Some("") => false,
@@ -281,17 +279,6 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
}
}
}
let any_rustc_declared = info.iter().any(|r| r.rustc_declared);
if any_rustc_declared {
for r in &mut info {
if !r.rustc_declared
&& ocs_plugin_api::version_info::uses_acadrust_gate(r.api_version)
{
r.rustc_compatible = false;
}
}
}
if info.is_empty() {
Err(last_error.unwrap_or_else(|| "no installable releases found".to_string()))
} else {
@@ -391,18 +378,12 @@ pub fn install(release: &Release, repository: &str) -> Result<String, String> {
}
}
if ocs_plugin_api::version_info::uses_acadrust_gate(manifest.api_version)
&& manifest.rustc_declared
{
if ocs_plugin_api::version_info::uses_acadrust_gate(manifest.api_version) {
let Some(version) = manifest.rustc_version.as_deref() else {
return Err(
"Release declares rustc metadata but has no version; cannot verify ABI compatibility".to_string(),
);
return Err("Release has no rustc version; cannot verify ABI compatibility".to_string());
};
if version.is_empty() {
return Err(
"Release declares rustc metadata but has no version; cannot verify ABI compatibility".to_string(),
);
return Err("Release has no rustc version; cannot verify ABI compatibility".to_string());
}
if !ocs_plugin_api::version_info::rustc_versions_compatible(
version,
+2 -1
View File
@@ -226,7 +226,8 @@ fn external_card<'a>(
) -> Element<'a, Message> {
let failed_old_api = load_error.is_some() && !p.api_compatible();
let acadrust_mismatch = p.acadrust_declared && !p.acadrust_compatible();
let rustc_mismatch = p.rustc_declared && !p.rustc_compatible();
let rustc_mismatch = ocs_plugin_api::version_info::uses_acadrust_gate(p.api_version)
&& !p.rustc_compatible();
let (status, kind) = if loaded && disabled {
(t!("Disabled"), StatusKind::Muted)
} else if loaded {