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