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