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