Skip to main content

lingxia_update/
lib.rs

1mod app;
2mod config;
3mod error;
4mod lxapp;
5
6use lingxia_provider::{BoxFuture, ProviderError};
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::fmt;
10use std::str::FromStr;
11
12pub use app::{
13    AppUpdateApply, AppUpdateEvent, AppUpdateEventReceiver, AppUpdateEventSender, AppUpdateHost,
14    AppUpdateProgressReporter, AppUpdateStage, app_update_scope_key, check_app_update,
15    ensure_app_update_candidate_version, send_app_update_event, send_app_update_failed,
16    subscribe_app_update_events,
17};
18pub use config::{UpdateConfig, configure_update, update_config};
19pub use error::UpdateError;
20pub use lxapp::{
21    LxAppUpdateHost, ensure_first_install as ensure_lxapp_first_install,
22    ensure_force_update_for_installed as ensure_lxapp_force_update_for_installed,
23    ensure_target_version_ready as ensure_lxapp_target_version_ready, lxapp_update_scope_key,
24    spawn_background_update_check as spawn_lxapp_background_update_check,
25};
26
27#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
28#[serde(rename_all = "lowercase")]
29pub enum ReleaseType {
30    #[default]
31    Release,
32    Preview,
33    Developer,
34}
35
36/// The release channel this host build belongs to, derived from `app.json`'s
37/// `envVersion`. The host app's own update check already uses it; lxapps the
38/// host installs and updates must agree, or a developer build pulls release
39/// packages over the ones it shipped with.
40pub fn host_channel() -> ReleaseType {
41    lingxia_app_context::env_version().into()
42}
43
44impl From<lingxia_app_context::EnvVersion> for ReleaseType {
45    fn from(env: lingxia_app_context::EnvVersion) -> Self {
46        match env {
47            lingxia_app_context::EnvVersion::Release => Self::Release,
48            lingxia_app_context::EnvVersion::Preview => Self::Preview,
49            lingxia_app_context::EnvVersion::Developer => Self::Developer,
50        }
51    }
52}
53
54impl ReleaseType {
55    pub fn as_str(self) -> &'static str {
56        match self {
57            Self::Release => "release",
58            Self::Preview => "preview",
59            Self::Developer => "developer",
60        }
61    }
62}
63
64impl fmt::Display for ReleaseType {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(self.as_str())
67    }
68}
69
70/// A semantic version representation (`major.minor.patch`) shared by update policy
71/// and lxapp metadata persistence.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Version {
74    pub major: u32,
75    pub minor: u32,
76    pub patch: u32,
77}
78
79impl Version {
80    pub fn parse(version_str: &str) -> Result<Self, VersionError> {
81        let parts: Vec<&str> = version_str.split('.').collect();
82        if parts.len() != 3 {
83            return Err(VersionError::InvalidFormat);
84        }
85
86        let major = parts[0]
87            .parse()
88            .map_err(|_| VersionError::InvalidComponent)?;
89        let minor = parts.get(1).map_or(Ok(0), |s| {
90            s.parse().map_err(|_| VersionError::InvalidComponent)
91        })?;
92        let patch = parts.get(2).map_or(Ok(0), |s| {
93            s.parse().map_err(|_| VersionError::InvalidComponent)
94        })?;
95
96        Ok(Self {
97            major,
98            minor,
99            patch,
100        })
101    }
102}
103
104impl FromStr for Version {
105    type Err = VersionError;
106
107    fn from_str(s: &str) -> Result<Self, Self::Err> {
108        Self::parse(s)
109    }
110}
111
112impl fmt::Display for Version {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
115    }
116}
117
118impl PartialOrd for Version {
119    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
120        Some(self.cmp(other))
121    }
122}
123
124impl Ord for Version {
125    fn cmp(&self, other: &Self) -> Ordering {
126        match self.major.cmp(&other.major) {
127            Ordering::Equal => match self.minor.cmp(&other.minor) {
128                Ordering::Equal => self.patch.cmp(&other.patch),
129                ordering => ordering,
130            },
131            ordering => ordering,
132        }
133    }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
137pub enum VersionError {
138    #[error("invalid version format, expected 'major.minor.patch'")]
139    InvalidFormat,
140    #[error("invalid version component, expected unsigned integer")]
141    InvalidComponent,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
145pub struct SemanticVersion {
146    pub major: u32,
147    pub minor: u32,
148    pub patch: u32,
149}
150
151impl SemanticVersion {
152    pub fn from_version(version: &Version) -> Self {
153        Self {
154            major: version.major,
155            minor: version.minor,
156            patch: version.patch,
157        }
158    }
159
160    pub fn to_version_string(&self) -> String {
161        format!("{}.{}.{}", self.major, self.minor, self.patch)
162    }
163}
164
165impl fmt::Display for SemanticVersion {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
168    }
169}
170
171#[derive(Clone, Debug, PartialEq, Eq)]
172pub enum LxAppUpdateQuery {
173    Latest { current_version: Option<String> },
174    TargetVersion(String),
175}
176
177impl LxAppUpdateQuery {
178    pub fn latest(current_version: Option<impl Into<String>>) -> Self {
179        Self::Latest {
180            current_version: current_version.map(Into::into),
181        }
182    }
183
184    pub fn target_version(version: impl Into<String>) -> Self {
185        Self::TargetVersion(version.into())
186    }
187}
188
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub enum UpdateTarget {
191    App {
192        current_version: Option<String>,
193    },
194    LxApp {
195        id: String,
196        channel: ReleaseType,
197        query: LxAppUpdateQuery,
198    },
199    Plugin {
200        id: String,
201        version: String,
202    },
203}
204
205impl UpdateTarget {
206    pub fn app(current_version: Option<impl Into<String>>) -> Self {
207        Self::App {
208            current_version: current_version.map(Into::into),
209        }
210    }
211
212    pub fn lxapp(id: impl Into<String>, channel: ReleaseType, query: LxAppUpdateQuery) -> Self {
213        Self::LxApp {
214            id: id.into(),
215            channel,
216            query,
217        }
218    }
219
220    pub fn plugin(id: impl Into<String>, version: impl Into<String>) -> Self {
221        Self::Plugin {
222            id: id.into(),
223            version: version.into(),
224        }
225    }
226
227    /// Stable routing key for dedupe, metrics, and diagnostics.
228    pub fn scope_key(&self) -> String {
229        match self {
230            Self::App { .. } => "app".to_string(),
231            Self::LxApp { id, channel, .. } => format!("lxapp:{id}@{}", channel.as_str()),
232            Self::Plugin { id, version } => format!("plugin:{id}@{version}"),
233        }
234    }
235}
236
237#[derive(Clone, Debug)]
238pub struct UpdatePackageInfo {
239    pub version: String,
240    pub url: String,
241    pub checksum_sha256: String,
242    pub size: Option<u64>,
243    pub release_notes: Option<Vec<String>>,
244    pub is_force_update: bool,
245    pub required_runtime_version: Option<String>,
246}
247
248impl UpdatePackageInfo {
249    pub fn should_replace_version(
250        candidate_version: &str,
251        installed_version: Option<&str>,
252    ) -> bool {
253        installed_version != Some(candidate_version)
254    }
255
256    pub fn should_replace_installed_version(&self, installed_version: Option<&str>) -> bool {
257        Self::should_replace_version(&self.version, installed_version)
258    }
259
260    pub fn required_runtime_version_trimmed(&self) -> Option<&str> {
261        self.required_runtime_version
262            .as_deref()
263            .map(str::trim)
264            .filter(|value| !value.is_empty())
265    }
266
267    pub fn ensure_runtime_compatible(
268        &self,
269        current_runtime_version: &str,
270        target_name: &str,
271    ) -> Result<(), RuntimeCompatibilityError> {
272        let Some(required_runtime_version) = self.required_runtime_version_trimmed() else {
273            return Ok(());
274        };
275
276        let current = Version::parse(current_runtime_version).map_err(|_| {
277            RuntimeCompatibilityError::InvalidCurrentRuntimeVersion {
278                runtime_version: current_runtime_version.to_string(),
279            }
280        })?;
281        let required = Version::parse(required_runtime_version).map_err(|_| {
282            RuntimeCompatibilityError::InvalidRequiredRuntimeVersion {
283                target: target_name.to_string(),
284                update_version: self.version.clone(),
285                runtime_version: required_runtime_version.to_string(),
286            }
287        })?;
288
289        if current < required {
290            return Err(RuntimeCompatibilityError::RequiresRuntimeUpgrade {
291                target: target_name.to_string(),
292                update_version: self.version.clone(),
293                required_runtime_version: required.to_string(),
294                current_runtime_version: current.to_string(),
295            });
296        }
297
298        Ok(())
299    }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
303pub enum RuntimeCompatibilityError {
304    #[error("invalid SDK runtime version '{runtime_version}'")]
305    InvalidCurrentRuntimeVersion { runtime_version: String },
306    #[error(
307        "invalid minRuntimeVersion '{runtime_version}' from update metadata for {target}@{update_version}"
308    )]
309    InvalidRequiredRuntimeVersion {
310        target: String,
311        update_version: String,
312        runtime_version: String,
313    },
314    #[error(
315        "{target} update {update_version} requires runtime >= {required_runtime_version}, current SDK runtime is {current_runtime_version}; update host app first"
316    )]
317    RequiresRuntimeUpgrade {
318        target: String,
319        update_version: String,
320        required_runtime_version: String,
321        current_runtime_version: String,
322    },
323}
324
325/// Update contract shared by app and lxapp update implementations.
326pub trait UpdateProvider: Send + Sync + 'static {
327    /// Returns `Some(package)` when an update package exists and `None` when the target
328    /// is already up to date or no matching package is available.
329    fn check_update<'a>(
330        &'a self,
331        target: UpdateTarget,
332    ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, ProviderError>>;
333}
334
335#[cfg(test)]
336mod tests {
337    use super::Version;
338
339    #[test]
340    fn version_parse_accepts_full_semver_only() {
341        assert!(Version::parse("1.2.3").is_ok());
342        assert!(Version::parse("1").is_err());
343        assert!(Version::parse("1.2").is_err());
344        assert!(Version::parse("1.2.3.4").is_err());
345    }
346}