forked from 1b-agent/opencadstudio-upstream
feat(plugin): gate marketplace and load on acadrust source
- Add host_acadrust_source and source-hash comparison helpers to ocs_plugin_api. - Track acadrust_source and acadrust_declared in ExternalPlugin/ReleaseInfo. - Apply repo-level policy: if any release in a repo declares acadrust_source, undeclared v4+ releases from that repo are incompatible. - Preserve API v2/v3 backwards compatibility: legacy undeclared releases stay installable. - Update Plugin Manager UI to only offer compatible releases and show 'acadrust mismatch' status. - Drop acadrust_version from gate logic; only acadrust_source is required.
This commit is contained in:
1 parent
1fb770fb15
commit
3d35919656
4 files changed
+384
-39
No files matched your search
@@ -1,5 +1,7 @@
|
||||
//! Embedded version metadata generated at build time.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// The embedded version info as a JSON string.
|
||||
pub const EMBEDDED_VERSION_INFO_JSON: &str =
|
||||
include_str!(concat!(env!("OUT_DIR"), "/version_info.json"));
|
||||
@@ -9,6 +11,60 @@ pub fn get_embedded_version_info_json() -> &'static str {
|
||||
EMBEDDED_VERSION_INFO_JSON
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct VersionInfo {
|
||||
acadrust_source: String,
|
||||
}
|
||||
|
||||
/// Extract the string value for a top-level JSON key from a compact JSON
|
||||
/// object. Only handles string values and does not unescape.
|
||||
fn json_string_value(json: &str, key: &str) -> Option<String> {
|
||||
let pattern = format!(r#""{key}":""#);
|
||||
let start = json.find(&pattern)? + pattern.len();
|
||||
let end = json[start..].find('"')?;
|
||||
Some(json[start..start + end].to_string())
|
||||
}
|
||||
|
||||
fn parsed_version_info() -> &'static VersionInfo {
|
||||
static INFO: OnceLock<VersionInfo> = OnceLock::new();
|
||||
INFO.get_or_init(|| VersionInfo {
|
||||
acadrust_source: json_string_value(EMBEDDED_VERSION_INFO_JSON, "acadrust_source")
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The full `acadrust` git source this host was built against, in Cargo source
|
||||
/// form (e.g. `git+https://github.com/...?rev=<short>#<full-sha>`).
|
||||
pub fn host_acadrust_source() -> &'static str {
|
||||
&parsed_version_info().acadrust_source
|
||||
}
|
||||
|
||||
/// Extract the canonical 40-character lowercase git commit hash from an
|
||||
/// `acadrust_source` string.
|
||||
///
|
||||
/// Cargo git sources end in `#<full-sha>` regardless of whether the `rev`
|
||||
/// query parameter was a short hash, tag, or branch. Returns `None` if no
|
||||
/// 40-character hex suffix is present.
|
||||
pub fn acadrust_source_hash(source: &str) -> Option<&str> {
|
||||
let hash = source.rsplit('#').next()?;
|
||||
if hash.len() == 40 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
Some(hash)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// True when two `acadrust_source` strings refer to the same full commit.
|
||||
///
|
||||
/// Missing or unparseable sources are treated as incompatible (this helper is
|
||||
/// intended for callers that have already decided a fingerprint is present).
|
||||
pub fn acadrust_sources_compatible(a: &str, b: &str) -> bool {
|
||||
match (acadrust_source_hash(a), acadrust_source_hash(b)) {
|
||||
(Some(ha), Some(hb)) => ha.eq_ignore_ascii_case(hb),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -19,7 +75,7 @@ mod tests {
|
||||
assert!(!json.is_empty());
|
||||
let value: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
|
||||
assert!(value.get("ocs_version").is_some());
|
||||
assert!(value.get("acadrust_version").is_some());
|
||||
assert!(value.get("acadrust_source").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -54,4 +110,50 @@ mod tests {
|
||||
let acadrust = value["acadrust_version"].as_str().expect("acadrust_version string");
|
||||
assert_eq!(acadrust.split('.').count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_acadrust_source_is_non_empty() {
|
||||
assert!(!host_acadrust_source().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_full_hash_from_cargo_source() {
|
||||
let src = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
|
||||
assert_eq!(
|
||||
acadrust_source_hash(src),
|
||||
Some("94df2c3f87fa051b16ffc3923f80e9247c85c5fd")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_or_missing_hash() {
|
||||
assert!(acadrust_source_hash("").is_none());
|
||||
assert!(acadrust_source_hash("registry+https://crates.io").is_none());
|
||||
assert!(acadrust_source_hash("git+https://github.com/foo/bar.git#short").is_none());
|
||||
assert!(
|
||||
acadrust_source_hash("git+https://github.com/foo/bar.git#gggggggggggggggggggggggggggggggggggggggg")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_comparison_matches_full_hashes() {
|
||||
let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
|
||||
let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
|
||||
assert!(acadrust_sources_compatible(a, b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_comparison_detects_mismatch() {
|
||||
let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
|
||||
let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb";
|
||||
assert!(!acadrust_sources_compatible(a, b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_comparison_case_insensitive() {
|
||||
let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
|
||||
let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94DF2C3F87FA051B16FFC3923F80E9247C85C5FD";
|
||||
assert!(acadrust_sources_compatible(a, b));
|
||||
}
|
||||
}
|
||||
+149
-4
@@ -30,6 +30,13 @@ pub struct RegistryEntry {
|
||||
pub struct ReleaseInfo {
|
||||
pub tag: String,
|
||||
pub api_version: u32,
|
||||
/// Full `acadrust` git source the release was built against.
|
||||
pub acadrust_source: Option<String>,
|
||||
/// Whether the manifest explicitly declares `acadrust_source` under
|
||||
/// `[opencad]`. When false, the acadrust gate is skipped.
|
||||
pub acadrust_declared: bool,
|
||||
/// Whether the release's `acadrust_source` matches this host.
|
||||
pub acadrust_compatible: bool,
|
||||
}
|
||||
|
||||
/// An add-on package found on disk (not necessarily loaded or compatible).
|
||||
@@ -44,6 +51,12 @@ pub struct ExternalPlugin {
|
||||
/// an older `plugin.toml` does not declare `repository`.
|
||||
pub repository: Option<String>,
|
||||
pub api_version: u32,
|
||||
/// Full `acadrust` git source the plugin was built against.
|
||||
pub acadrust_source: Option<String>,
|
||||
/// Whether the manifest explicitly declares `acadrust_source` under
|
||||
/// `[opencad]`. When false, the acadrust gate is skipped and only the
|
||||
/// API-version gate is applied (legacy plugins).
|
||||
pub acadrust_declared: bool,
|
||||
pub ribbon_order: i32,
|
||||
pub command_prefixes: Vec<String>,
|
||||
/// The package directory under the plugins folder.
|
||||
@@ -58,11 +71,28 @@ impl ExternalPlugin {
|
||||
ocs_plugin_api::manifest::host_accepts_plugin_version(self.api_version)
|
||||
}
|
||||
|
||||
/// True when the package can be loaded today: compatible API *and* a native
|
||||
/// library present for this platform.
|
||||
/// True when the package's `acadrust` fingerprint matches the host.
|
||||
///
|
||||
/// If the manifest does not declare `acadrust_source`, the gate is skipped
|
||||
/// (legacy fallback). If declared but empty or mismatched, incompatible.
|
||||
pub fn acadrust_compatible(&self) -> bool {
|
||||
if !self.acadrust_declared {
|
||||
return true;
|
||||
}
|
||||
match self.acadrust_source.as_deref() {
|
||||
None | Some("") => false,
|
||||
Some(source) => ocs_plugin_api::version_info::acadrust_sources_compatible(
|
||||
source,
|
||||
ocs_plugin_api::version_info::host_acadrust_source(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// True when the package can be loaded today: compatible API *and* matching
|
||||
/// `acadrust` fingerprint (if declared) *and* a native library present.
|
||||
#[allow(dead_code)] // plugin-host surface (issue #100); not yet wired
|
||||
pub fn loadable(&self) -> bool {
|
||||
self.api_compatible() && self.lib_present
|
||||
self.api_compatible() && self.acadrust_compatible() && self.lib_present
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +206,8 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
let mut description = String::new();
|
||||
let mut repository = None;
|
||||
let mut api_version: u32 = 0;
|
||||
let mut acadrust_source: Option<String> = None;
|
||||
let mut acadrust_declared = false;
|
||||
let mut ribbon_order: i32 = 0;
|
||||
let mut command_prefixes: Vec<String> = Vec::new();
|
||||
|
||||
@@ -196,6 +228,11 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
"description" => description = unquote(value),
|
||||
"repository" => repository = normalize_repository(&unquote(value)),
|
||||
"api_version" => api_version = value.parse().unwrap_or(0),
|
||||
"acadrust_source" => {
|
||||
acadrust_declared = true;
|
||||
let v = unquote(value);
|
||||
acadrust_source = 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),
|
||||
_ => {}
|
||||
@@ -209,6 +246,8 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
description,
|
||||
repository,
|
||||
api_version,
|
||||
acadrust_source,
|
||||
acadrust_declared,
|
||||
ribbon_order,
|
||||
command_prefixes,
|
||||
dir: PathBuf::new(),
|
||||
@@ -298,7 +337,33 @@ mod loader {
|
||||
manager.set_notification_handler(v4_support::notification_handler());
|
||||
let mut out = Vec::new();
|
||||
for d in &discovered {
|
||||
if !d.api_compatible() || !d.lib_present {
|
||||
if !d.api_compatible() {
|
||||
continue;
|
||||
}
|
||||
if !d.lib_present {
|
||||
continue;
|
||||
}
|
||||
if d.acadrust_declared && d.acadrust_source.is_none() {
|
||||
eprintln!(
|
||||
"[plugin] {} declares acadrust metadata but has no fingerprint; cannot verify compatibility",
|
||||
d.id
|
||||
);
|
||||
}
|
||||
if !d.acadrust_compatible() {
|
||||
let host_src = ocs_plugin_api::version_info::host_acadrust_source();
|
||||
let plugin_hash = d
|
||||
.acadrust_source
|
||||
.as_deref()
|
||||
.and_then(ocs_plugin_api::version_info::acadrust_source_hash)
|
||||
.unwrap_or("unknown");
|
||||
let host_hash = ocs_plugin_api::version_info::acadrust_source_hash(host_src)
|
||||
.unwrap_or("unknown");
|
||||
out.push((
|
||||
d.id.clone(),
|
||||
Err(format!(
|
||||
"Plugin built for acadrust @{plugin_hash}, but this host uses @{host_hash}"
|
||||
)),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let Some(path) = lib_file(&d.dir) else {
|
||||
@@ -412,6 +477,86 @@ xdata_apps = ["MYPLUGIN_RECORD"]
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undeclared_acadrust_falls_back_to_api_gate() {
|
||||
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.acadrust_declared);
|
||||
assert!(p.acadrust_source.is_none());
|
||||
assert!(p.acadrust_compatible(), "undeclared acadrust is treated as compatible");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_empty_acadrust_source_is_incompatible() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
acadrust_source = ""
|
||||
"#;
|
||||
let p = parse_plugin_toml(toml).expect("parsed");
|
||||
assert!(p.acadrust_declared);
|
||||
assert!(p.acadrust_source.is_none());
|
||||
assert!(!p.acadrust_compatible(), "declared but empty source is incompatible");
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acadrust_mismatch_detected() {
|
||||
let host = ocs_plugin_api::version_info::host_acadrust_source();
|
||||
let other = if host.contains("94df2c3") {
|
||||
"git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb"
|
||||
} else {
|
||||
"git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"
|
||||
};
|
||||
let toml = format!(
|
||||
r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
acadrust_source = "{other}"
|
||||
"#
|
||||
);
|
||||
let p = parse_plugin_toml(&toml).expect("parsed");
|
||||
assert!(p.acadrust_declared);
|
||||
assert!(!p.acadrust_compatible(), "mismatched acadrust fingerprint should be incompatible");
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acadrust_match_detected() {
|
||||
let host = ocs_plugin_api::version_info::host_acadrust_source();
|
||||
let toml = format!(
|
||||
r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
acadrust_source = "{host}"
|
||||
"#
|
||||
);
|
||||
let p = parse_plugin_toml(&toml).expect("parsed");
|
||||
assert!(p.acadrust_declared);
|
||||
assert!(p.acadrust_compatible(), "matching acadrust fingerprint should be compatible");
|
||||
}
|
||||
|
||||
/// Integration smoke test for the out-of-process plugin path.
|
||||
/// Set `OCS_TEST_PLUGIN` to the built cdylib path and make sure the
|
||||
/// `OpenCADStudio` binary is built; the test uses it as the runner host.
|
||||
|
||||
@@ -219,9 +219,27 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
|
||||
let manifest_text = download_string(&manifest_asset.url)?;
|
||||
let manifest = external::parse_plugin_toml(&manifest_text)
|
||||
.ok_or_else(|| format!("release {} plugin.toml is missing an id", release.tag))?;
|
||||
let acadrust_source = manifest.acadrust_source.clone();
|
||||
let acadrust_declared = manifest.acadrust_declared;
|
||||
let acadrust_compatible = if !acadrust_declared {
|
||||
// Legacy release that predates acadrust metadata: defer the
|
||||
// decision to the repo-level policy below.
|
||||
true
|
||||
} else {
|
||||
match acadrust_source.as_deref() {
|
||||
None | Some("") => false,
|
||||
Some(source) => ocs_plugin_api::version_info::acadrust_sources_compatible(
|
||||
source,
|
||||
ocs_plugin_api::version_info::host_acadrust_source(),
|
||||
),
|
||||
}
|
||||
};
|
||||
Ok::<_, String>(ReleaseInfo {
|
||||
tag: release.tag,
|
||||
api_version: manifest.api_version,
|
||||
acadrust_source,
|
||||
acadrust_declared,
|
||||
acadrust_compatible,
|
||||
})
|
||||
})();
|
||||
match result {
|
||||
@@ -229,6 +247,21 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
// Repo-level policy: if any release in this repo declares an acadrust
|
||||
// fingerprint, treat undeclared releases from the same repo as
|
||||
// incompatible — but only for releases targeting the current host API
|
||||
// generation (v4 and above). This preserves API v2/v3 backwards
|
||||
// compatibility for legacy plugins that predate acadrust metadata.
|
||||
let any_declared = info.iter().any(|r| r.acadrust_declared);
|
||||
if any_declared {
|
||||
for r in &mut info {
|
||||
if !r.acadrust_declared && r.api_version >= ocs_plugin_api::API_VERSION {
|
||||
r.acadrust_compatible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if info.is_empty() {
|
||||
Err(last_error.unwrap_or_else(|| "no installable releases found".to_string()))
|
||||
} else {
|
||||
@@ -283,8 +316,8 @@ fn download_bytes(url: &str) -> Result<Vec<u8>, String> {
|
||||
}
|
||||
|
||||
/// Download and install a release's package into the plugins folder. Verifies
|
||||
/// the API version from the package's `plugin.toml` first. Returns the plugin
|
||||
/// id on success.
|
||||
/// the API version and, when present, the `acadrust` fingerprint from the
|
||||
/// package's `plugin.toml` first. Returns the plugin id on success.
|
||||
pub fn install(release: &Release, repository: &str) -> Result<String, String> {
|
||||
let lib = release.lib_asset().ok_or("no library for this platform")?;
|
||||
let toml = release.toml_asset().ok_or("release has no plugin.toml")?;
|
||||
@@ -300,6 +333,32 @@ pub fn install(release: &Release, repository: &str) -> Result<String, String> {
|
||||
));
|
||||
}
|
||||
|
||||
if manifest.acadrust_declared {
|
||||
let Some(source) = manifest.acadrust_source.as_deref() else {
|
||||
return Err(
|
||||
"Release declares acadrust metadata but has no source; cannot verify ABI compatibility".to_string(),
|
||||
);
|
||||
};
|
||||
if source.is_empty() {
|
||||
return Err(
|
||||
"Release declares acadrust metadata but has no source; cannot verify ABI compatibility".to_string(),
|
||||
);
|
||||
}
|
||||
if !ocs_plugin_api::version_info::acadrust_sources_compatible(
|
||||
source,
|
||||
ocs_plugin_api::version_info::host_acadrust_source(),
|
||||
) {
|
||||
let host_src = ocs_plugin_api::version_info::host_acadrust_source();
|
||||
let plugin_hash = ocs_plugin_api::version_info::acadrust_source_hash(source)
|
||||
.unwrap_or("unknown");
|
||||
let host_hash = ocs_plugin_api::version_info::acadrust_source_hash(host_src)
|
||||
.unwrap_or("unknown");
|
||||
return Err(format!(
|
||||
"Plugin built for acadrust @{plugin_hash}, but this host uses @{host_hash}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let dir = external::plugins_dir()
|
||||
.ok_or("cannot locate the plugins folder")?
|
||||
.join(&manifest.id);
|
||||
|
||||
@@ -196,17 +196,22 @@ fn trim_version_prefix(value: &str) -> &str {
|
||||
value.trim_start_matches(|c| c == 'v' || c == 'V')
|
||||
}
|
||||
|
||||
fn newest_update(installed: &str, tags: &[String]) -> Option<String> {
|
||||
fn newest_update(installed: &str, releases: &[ReleaseInfo]) -> Option<String> {
|
||||
let installed = semver::Version::parse(trim_version_prefix(installed)).ok()?;
|
||||
tags.iter()
|
||||
.filter_map(|tag| {
|
||||
semver::Version::parse(trim_version_prefix(tag))
|
||||
releases
|
||||
.iter()
|
||||
.filter(|release| {
|
||||
ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version)
|
||||
&& release.acadrust_compatible
|
||||
})
|
||||
.filter_map(|release| {
|
||||
semver::Version::parse(trim_version_prefix(&release.tag))
|
||||
.ok()
|
||||
.map(|version| (version, tag))
|
||||
.map(|version| (version, release.tag.clone()))
|
||||
})
|
||||
.filter(|(version, _)| version > &installed)
|
||||
.max_by(|(left, _), (right, _)| left.cmp(right))
|
||||
.map(|(_, tag)| tag.clone())
|
||||
.map(|(_, tag)| tag)
|
||||
}
|
||||
|
||||
fn external_card<'a>(
|
||||
@@ -219,12 +224,15 @@ fn external_card<'a>(
|
||||
selected: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let failed_old_api = load_error.is_some() && !p.api_compatible();
|
||||
let acadrust_mismatch = p.acadrust_declared && !p.acadrust_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 {
|
||||
(t!("acadrust mismatch"), StatusKind::Danger)
|
||||
} else if load_error.is_some() {
|
||||
(t!("Load failed"), StatusKind::Danger)
|
||||
} else if !p.lib_present {
|
||||
@@ -352,11 +360,8 @@ fn install_controls<'a>(
|
||||
.iter()
|
||||
.map(|release| release.tag.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let selected_api = selected.as_ref().and_then(|selected| {
|
||||
releases
|
||||
.iter()
|
||||
.find(|release| release.tag == *selected)
|
||||
.map(|release| release.api_version)
|
||||
let selected_release = selected.as_ref().and_then(|selected| {
|
||||
releases.iter().find(|release| release.tag == *selected)
|
||||
});
|
||||
let picker: Element<'_, Message> = if tags.is_empty() {
|
||||
text(t!("no releases")).size(11).style(muted_style).into()
|
||||
@@ -367,9 +372,10 @@ fn install_controls<'a>(
|
||||
.text_size(12)
|
||||
.into()
|
||||
};
|
||||
let action = match selected_api {
|
||||
Some(api_version)
|
||||
if ocs_plugin_api::manifest::host_accepts_plugin_version(api_version) =>
|
||||
let action = match selected_release {
|
||||
Some(release)
|
||||
if ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version)
|
||||
&& release.acadrust_compatible =>
|
||||
{
|
||||
pill_button(
|
||||
t!("Install"),
|
||||
@@ -842,18 +848,7 @@ pub fn view_window<'a>(
|
||||
let update_tag = repository
|
||||
.as_ref()
|
||||
.and_then(|repo| market.release_tags.get(repo))
|
||||
.and_then(|releases| {
|
||||
let compatible_tags = releases
|
||||
.iter()
|
||||
.filter(|release| {
|
||||
ocs_plugin_api::manifest::host_accepts_plugin_version(
|
||||
release.api_version,
|
||||
)
|
||||
})
|
||||
.map(|release| release.tag.clone())
|
||||
.collect::<Vec<_>>();
|
||||
newest_update(&p.version, &compatible_tags)
|
||||
});
|
||||
.and_then(|releases| newest_update(&p.version, releases));
|
||||
list = list.push(external_card(
|
||||
p,
|
||||
repository,
|
||||
@@ -984,16 +979,60 @@ pub fn view_web_notice<'a>() -> Element<'a, Message> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{newest_update, registry_error_message, repository_display_name};
|
||||
use crate::plugin::external::ReleaseInfo;
|
||||
|
||||
#[test]
|
||||
fn newest_update_uses_semver_not_release_order() {
|
||||
let tags = vec![
|
||||
"v1.4.0".to_string(),
|
||||
"v2.0.0".to_string(),
|
||||
"v1.9.9".to_string(),
|
||||
let releases = vec![
|
||||
ReleaseInfo {
|
||||
tag: "v1.4.0".to_string(),
|
||||
api_version: 4,
|
||||
acadrust_source: None,
|
||||
acadrust_declared: false,
|
||||
acadrust_compatible: true,
|
||||
},
|
||||
ReleaseInfo {
|
||||
tag: "v2.0.0".to_string(),
|
||||
api_version: 4,
|
||||
acadrust_source: None,
|
||||
acadrust_declared: false,
|
||||
acadrust_compatible: true,
|
||||
},
|
||||
ReleaseInfo {
|
||||
tag: "v1.9.9".to_string(),
|
||||
api_version: 4,
|
||||
acadrust_source: None,
|
||||
acadrust_declared: false,
|
||||
acadrust_compatible: true,
|
||||
},
|
||||
];
|
||||
assert_eq!(newest_update("1.3.0", &tags).as_deref(), Some("v2.0.0"));
|
||||
assert_eq!(newest_update("2.0.0", &tags), None);
|
||||
assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v2.0.0"));
|
||||
assert_eq!(newest_update("2.0.0", &releases), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newest_update_ignores_incompatible_releases() {
|
||||
let releases = vec![
|
||||
ReleaseInfo {
|
||||
tag: "v1.4.0".to_string(),
|
||||
api_version: 4,
|
||||
acadrust_source: Some(
|
||||
"git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd".to_string(),
|
||||
),
|
||||
acadrust_declared: true,
|
||||
acadrust_compatible: true,
|
||||
},
|
||||
ReleaseInfo {
|
||||
tag: "v2.0.0".to_string(),
|
||||
api_version: 4,
|
||||
acadrust_source: Some(
|
||||
"git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb".to_string(),
|
||||
),
|
||||
acadrust_declared: true,
|
||||
acadrust_compatible: false,
|
||||
},
|
||||
];
|
||||
assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v1.4.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user