forked from 1b-agent/opencadstudio-upstream
Merge pull request #931 from schoeller/rustc_plugin_gate
feat(plugins): include rustc version in plugin compatibility gate
This commit is contained in:
10 files changed
+344
-12
No files matched your search
+160
-1
@@ -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#"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
Reference in new issue
Block a user