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#[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 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
308pub trait UpdateProvider: Send + Sync + 'static {
310 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}