Merge pull request #931 from schoeller/rustc_plugin_gate

feat(plugins): include rustc version in plugin compatibility gate
This commit is contained in:
Hakan Seven authored and GitHub committed 2026-08-29 15:55:04 +03:00
commit 5062c26c88
10 files changed
+344 -12

No files matched your search

+160 -1
View File
@@ -36,6 +36,12 @@ pub struct ReleaseInfo {
pub acadrust_declared: bool,
/// Whether the release matches this host.
pub acadrust_compatible: bool,
/// Full `rustc --version` output used by the release.
pub rustc_version: Option<String>,
/// Whether `[opencad]` declares `rustc_version`.
pub rustc_declared: bool,
/// Whether the release's rustc matches this host.
pub rustc_compatible: bool,
}
/// An add-on package found on disk (not necessarily loaded or compatible).
@@ -54,6 +60,10 @@ pub struct ExternalPlugin {
pub acadrust_source: Option<String>,
/// Whether `[opencad]` declares `acadrust_source`.
pub acadrust_declared: bool,
/// Full `rustc --version` output used by the plugin.
pub rustc_version: Option<String>,
/// Whether `[opencad]` declares `rustc_version`.
pub rustc_declared: bool,
pub ribbon_order: i32,
pub command_prefixes: Vec<String>,
/// The package directory under the plugins folder.
@@ -85,10 +95,27 @@ impl ExternalPlugin {
}
}
/// Returns whether the package's rustc matches the host.
pub fn rustc_compatible(&self) -> bool {
if !ocs_plugin_api::version_info::uses_acadrust_gate(self.api_version) {
return true;
}
match self.rustc_version.as_deref() {
None | Some("") => false,
Some(version) => ocs_plugin_api::version_info::rustc_versions_compatible(
version,
ocs_plugin_api::version_info::host_rustc_version(),
),
}
}
/// Returns whether the package can be loaded.
#[allow(dead_code)] // plugin-host surface (issue #100); not yet wired
pub fn loadable(&self) -> bool {
self.api_compatible() && self.acadrust_compatible() && self.lib_present
self.api_compatible()
&& self.acadrust_compatible()
&& self.rustc_compatible()
&& self.lib_present
}
}
@@ -204,6 +231,8 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
let mut api_version: u32 = 0;
let mut acadrust_source: Option<String> = None;
let mut acadrust_declared = false;
let mut rustc_version: Option<String> = None;
let mut rustc_declared = false;
let mut ribbon_order: i32 = 0;
let mut command_prefixes: Vec<String> = Vec::new();
let mut section = None;
@@ -236,6 +265,11 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
let v = unquote(value);
acadrust_source = if v.is_empty() { None } else { Some(v) };
}
"rustc_version" if section == Some("opencad") => {
rustc_declared = true;
let v = unquote(value);
rustc_version = if v.is_empty() { None } else { Some(v) };
}
"ribbon_order" => ribbon_order = value.parse().unwrap_or(0),
"command_prefixes" => command_prefixes = parse_string_array(value),
_ => {}
@@ -251,6 +285,8 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
api_version,
acadrust_source,
acadrust_declared,
rustc_version,
rustc_declared,
ribbon_order,
command_prefixes,
dir: PathBuf::new(),
@@ -372,6 +408,17 @@ mod loader {
));
continue;
}
if !d.rustc_compatible() {
let host_rustc = ocs_plugin_api::version_info::host_rustc_version();
let plugin_rustc = d.rustc_version.as_deref().unwrap_or("unknown");
out.push((
d.id.clone(),
Err(format!(
"Plugin built with {plugin_rustc}, host requires {host_rustc} - rebuild required"
)),
));
continue;
}
let Some(path) = lib_file(&d.dir) else {
out.push((
d.id.clone(),
@@ -483,6 +530,118 @@ xdata_apps = ["MYPLUGIN_RECORD"]
assert!(!p.loadable());
}
#[test]
fn undeclared_rustc_is_incompatible() {
let toml = r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
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());
assert!(!p.loadable());
}
#[test]
fn declared_empty_rustc_version_is_incompatible() {
let toml = r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 4
[opencad]
rustc_version = ""
"#;
let p = parse_plugin_toml(toml).expect("parsed");
assert!(p.rustc_declared);
assert!(p.rustc_version.is_none());
assert!(!p.rustc_compatible(), "declared but empty rustc is incompatible");
assert!(!p.loadable());
}
#[test]
fn rustc_version_outside_opencad_is_ignored() {
let toml = r#"
[plugin]
id = "opencad.test"
api_version = 4
rustc_version = "rustc 1.98.0"
"#;
let p = parse_plugin_toml(toml).expect("parsed");
assert!(!p.rustc_declared);
assert!(p.rustc_version.is_none());
}
#[test]
fn rustc_mismatch_detected() {
let _host = ocs_plugin_api::version_info::host_rustc_version();
let other = "rustc 0.0.0-fake (not matching anything)";
let toml = format!(
r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 4
[opencad]
rustc_version = "{other}"
"#
);
let p = parse_plugin_toml(&toml).expect("parsed");
assert!(p.rustc_declared);
assert!(!p.rustc_compatible(), "mismatched rustc should be incompatible");
assert!(!p.loadable());
}
#[test]
fn rustc_match_detected() {
let host = ocs_plugin_api::version_info::host_rustc_version();
let toml = format!(
r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 4
[opencad]
rustc_version = "{host}"
"#
);
let p = parse_plugin_toml(&toml).expect("parsed");
assert!(p.rustc_declared);
assert!(p.rustc_compatible(), "matching rustc should be compatible");
}
#[test]
fn rustc_gate_only_applies_to_api_v4_and_newer() {
let toml = r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 2
[opencad]
rustc_version = "rustc 0.0.0-fake"
"#;
let mut p = parse_plugin_toml(toml).expect("parsed");
p.lib_present = true;
assert!(p.rustc_declared);
assert!(
p.rustc_compatible(),
"API v2 plugin should bypass rustc gate"
);
assert!(p.loadable());
}
#[test]
fn undeclared_acadrust_falls_back_to_api_gate() {
let toml = r#"
+38 -3
View File
@@ -236,12 +236,30 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
),
}
};
let rustc_version = manifest.rustc_version.clone();
let rustc_declared = manifest.rustc_declared;
let rustc_compatible = if !ocs_plugin_api::version_info::uses_acadrust_gate(
manifest.api_version,
) {
true
} else {
match rustc_version.as_deref() {
None | Some("") => false,
Some(version) => ocs_plugin_api::version_info::rustc_versions_compatible(
version,
ocs_plugin_api::version_info::host_rustc_version(),
),
}
};
Ok::<_, String>(ReleaseInfo {
tag: release.tag,
api_version: manifest.api_version,
acadrust_source,
acadrust_declared,
acadrust_compatible,
rustc_version,
rustc_declared,
rustc_compatible,
})
})();
match result {
@@ -251,8 +269,8 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
}
// Once a repo declares a fingerprint, require it on later API versions.
let any_declared = info.iter().any(|r| r.acadrust_declared);
if any_declared {
let any_acadrust_declared = info.iter().any(|r| r.acadrust_declared);
if any_acadrust_declared {
for r in &mut info {
if !r.acadrust_declared
&& ocs_plugin_api::version_info::uses_acadrust_gate(r.api_version)
@@ -261,7 +279,6 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
}
}
}
if info.is_empty() {
Err(last_error.unwrap_or_else(|| "no installable releases found".to_string()))
} else {
@@ -361,6 +378,24 @@ pub fn install(release: &Release, repository: &str) -> Result<String, String> {
}
}
if ocs_plugin_api::version_info::uses_acadrust_gate(manifest.api_version) {
let Some(version) = manifest.rustc_version.as_deref() else {
return Err("Release has no rustc version; cannot verify ABI compatibility".to_string());
};
if version.is_empty() {
return Err("Release has no rustc version; cannot verify ABI compatibility".to_string());
}
if !ocs_plugin_api::version_info::rustc_versions_compatible(
version,
ocs_plugin_api::version_info::host_rustc_version(),
) {
let host_rustc = ocs_plugin_api::version_info::host_rustc_version();
return Err(format!(
"Plugin built with {version}, host requires {host_rustc} - rebuild required"
));
}
}
let dir = external::plugins_dir()
.ok_or("cannot locate the plugins folder")?
.join(&manifest.id);
+21 -2
View File
@@ -203,6 +203,7 @@ fn newest_update(installed: &str, releases: &[ReleaseInfo]) -> Option<String> {
.filter(|release| {
ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version)
&& release.acadrust_compatible
&& release.rustc_compatible
})
.filter_map(|release| {
semver::Version::parse(trim_version_prefix(&release.tag))
@@ -225,13 +226,15 @@ 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 = 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 {
(t!("Loaded"), StatusKind::Success)
} else if !p.api_compatible() || failed_old_api {
(t!("API incompatible"), StatusKind::Danger)
} else if acadrust_mismatch {
} else if acadrust_mismatch || rustc_mismatch {
(t!("Incompatible"), StatusKind::Danger)
} else if load_error.is_some() {
(t!("Load failed"), StatusKind::Danger)
@@ -375,7 +378,8 @@ fn install_controls<'a>(
let action = match selected_release {
Some(release)
if ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version)
&& release.acadrust_compatible =>
&& release.acadrust_compatible
&& release.rustc_compatible =>
{
pill_button(
t!("Install"),
@@ -990,6 +994,9 @@ mod tests {
acadrust_source: None,
acadrust_declared: false,
acadrust_compatible: true,
rustc_version: None,
rustc_declared: false,
rustc_compatible: true,
},
ReleaseInfo {
tag: "v2.0.0".to_string(),
@@ -997,6 +1004,9 @@ mod tests {
acadrust_source: None,
acadrust_declared: false,
acadrust_compatible: true,
rustc_version: None,
rustc_declared: false,
rustc_compatible: true,
},
ReleaseInfo {
tag: "v1.9.9".to_string(),
@@ -1004,6 +1014,9 @@ mod tests {
acadrust_source: None,
acadrust_declared: false,
acadrust_compatible: true,
rustc_version: None,
rustc_declared: false,
rustc_compatible: true,
},
];
assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v2.0.0"));
@@ -1021,6 +1034,9 @@ mod tests {
),
acadrust_declared: true,
acadrust_compatible: true,
rustc_version: None,
rustc_declared: false,
rustc_compatible: true,
},
ReleaseInfo {
tag: "v2.0.0".to_string(),
@@ -1030,6 +1046,9 @@ mod tests {
),
acadrust_declared: true,
acadrust_compatible: false,
rustc_version: None,
rustc_declared: false,
rustc_compatible: true,
},
];
assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v1.4.0"));