//! Release checking, downloading, installing and self-updating for the Craft App Suite and ArtCraft Launcher. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] pub mod app; pub mod desktop_entry; pub mod detect; pub mod distro; mod folders; pub mod http; pub mod install; pub mod manifest; pub mod msi_context; mod msi_copy; mod msi_migrate; pub mod packages; pub mod paths; pub mod pins; pub mod platform; #[cfg(windows)] pub mod portable; pub mod process; pub mod protocol; pub mod proxy; pub mod queue; pub mod release; pub mod self_update; pub mod source; pub mod status; pub mod suite; pub mod time; pub mod translate; pub mod version; #[cfg(feature = "egui")] pub mod dialog; #[cfg(feature = "egui")] pub mod updater; pub use app::App; pub use http::{Http, Stop}; pub use paths::Paths; pub use platform::{Arch, Os, PackageKind, Platform, pick_asset}; pub use protocol::{InstallLocations, JobPhase, LatestRelease, Source}; pub use release::{Asset, Release, parse_github_release, parse_github_release_list}; pub use status::{InstallSource, Installed}; pub use suite::SUITE; pub use time::parse_rfc3339; pub use version::{Version, is_newer, parse as parse_version}; #[cfg(feature = "egui")] pub use dialog::{CheckResultDialog, CheckResultResponse, StartupPrompt, StartupPromptResponse}; /// Checks for an update against GitHub releases for the given app. pub fn check_for_update(app: &App, http: &Http, current_version: &str) -> Result, String> { let version = self_update::current_version(app, current_version).map(|v| v.to_string()); let current_version = version.as_deref().unwrap_or(current_version); let url = format!("https://api.github.com/repos/{}/releases?per_page=30", app.repo); match http.get_api(&url, None)? { crate::http::Fetched::Body { text, .. } => offered_release(&text, app.repo, current_version), crate::http::Fetched::RateLimited { .. } => Err("GitHub's request limit was reached".into()), crate::http::Fetched::NotModified | crate::http::Fetched::NotFound => Ok(None), } } /// Unix seconds now. pub fn now() -> i64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX)) } /// The release in GitHub's release list `json` to offer a copy running `current_version`, if any: /// the latest published release, when [`version::offers_update`] says so. fn offered_release(json: &str, repo: &str, current_version: &str) -> Result, String> { let latest = crate::release::parse_github_release_list(json, repo, false)?; Ok(latest.filter(|release| { let running = crate::version::parse(current_version); release.version.as_ref().zip(running.as_ref()).is_some_and(|(rel_v, cur_v)| crate::version::offers_update(rel_v, cur_v)) })) } #[cfg(test)] pub(crate) const TEST_AGENT: &str = "ArtCraft-Launcher-Test/0.0"; #[cfg(test)] mod offer_tests { use super::offered_release; const REPO: &str = "storytold/photocraft"; const LIST: &str = r#"[{"tag_name":"v0.17.0","prerelease":false,"draft":false,"assets":[]}, {"tag_name":"v0.16.4","prerelease":false,"draft":false,"assets":[]}]"#; fn offered(running: &str) -> Option { offered_release(LIST, REPO, running).unwrap().map(|release| release.tag) } #[test] fn a_build_from_main_is_not_offered_the_release_it_already_contains() { assert_eq!(offered("0.17.0-integration.20261029.f5b3f123"), None); assert_eq!(offered("0.16.5-dev"), Some("v0.17.0".into())); } #[test] fn releases_and_release_candidates_are_offered_as_before() { assert_eq!(offered("0.17.0-rc.2"), Some("v0.17.0".into())); assert_eq!(offered("0.16.4"), Some("v0.17.0".into())); assert_eq!(offered("0.17.0"), None); assert_eq!(offered("not a version"), None); } }