aboutsummaryrefslogtreecommitdiff
path: root/bundle/src-tauri/src
diff options
context:
space:
mode:
Diffstat (limited to 'bundle/src-tauri/src')
-rw-r--r--bundle/src-tauri/src/main.rs7
-rw-r--r--bundle/src-tauri/src/update.rs124
2 files changed, 131 insertions, 0 deletions
diff --git a/bundle/src-tauri/src/main.rs b/bundle/src-tauri/src/main.rs
index 37b7431..c361260 100644
--- a/bundle/src-tauri/src/main.rs
+++ b/bundle/src-tauri/src/main.rs
@@ -3,6 +3,7 @@
mod compiler;
mod menu;
mod runner;
+mod update;
use compiler::Compiler;
use runner::{OutputEvent, Runner};
use std::{
@@ -90,6 +91,12 @@ fn main() {
let compiler = Compiler::load();
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
+ .plugin(tauri_plugin_opener::init())
+ // Once per launch, not per window.
+ .setup(|app| {
+ update::check(app.handle().clone());
+ Ok(())
+ })
.menu(menu::build)
// Menu items act on the focused window, like its keyboard shortcuts.
.on_menu_event(|app, event| {
diff --git a/bundle/src-tauri/src/update.rs b/bundle/src-tauri/src/update.rs
new file mode 100644
index 0000000..1d2127d
--- /dev/null
+++ b/bundle/src-tauri/src/update.rs
@@ -0,0 +1,124 @@
+// Startup check for a newer nightly build. It only reports; there is no auto-update.
+// Release builds get CPSC_BUNDLE_TIME and CPSC_BUNDLE_COMMIT at build time; without
+// them (dev, tests) nothing runs. Every failure is silent.
+use serde::Deserialize;
+use std::time::Duration;
+use tauri::AppHandle;
+use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
+use tauri_plugin_opener::OpenerExt;
+
+const LOCAL_TIME: Option<&str> = option_env!("CPSC_BUNDLE_TIME");
+const LOCAL_COMMIT: Option<&str> = option_env!("CPSC_BUNDLE_COMMIT");
+const VERSION_URL: &str = match option_env!("CPSC_BUNDLE_VERSION_URL") {
+ Some(url) => url,
+ None => "https://github.com/ovfor4/cie-pseudocode-compiler/releases/download/nightly-build/version.json",
+};
+const RELEASE_PAGE: &str = "https://github.com/ovfor4/cie-pseudocode-compiler/releases/tag/nightly-build";
+
+#[cfg(windows)]
+const ASSET: &str = "cpsc-bundle-latest-Windows-x86_64.exe";
+#[cfg(target_os = "macos")]
+const ASSET: &str = "cpsc-bundle-latest-Darwin-arm64.tar.xz";
+#[cfg(not(any(windows, target_os = "macos")))]
+const ASSET: &str = "";
+
+#[derive(Deserialize)]
+struct Product {
+ time: String,
+ commit: String,
+}
+
+#[derive(Deserialize)]
+struct Versions {
+ bundle: Product,
+}
+
+fn latest(json: &str) -> Option<Product> {
+ serde_json::from_str::<Versions>(json).ok().map(|v| v.bundle)
+}
+
+// Times look like YYYYMMDD-HHMMSS, so a string comparison orders them.
+fn valid(time: &str) -> bool {
+ let b = time.as_bytes();
+ b.len() == 15 && b[8] == b'-' && b.iter().enumerate().all(|(i, c)| i == 8 || c.is_ascii_digit())
+}
+
+fn newer(remote_time: &str, local_time: &str) -> bool {
+ valid(remote_time) && valid(local_time) && remote_time > local_time
+}
+
+fn fetch() -> Option<Product> {
+ let agent: ureq::Agent = ureq::Agent::config_builder()
+ .timeout_global(Some(Duration::from_secs(10)))
+ .build()
+ .into();
+ let body = agent.get(VERSION_URL).call().ok()?.body_mut().read_to_string().ok()?;
+ latest(&body)
+}
+
+pub fn check(app: AppHandle) {
+ let (Some(time), Some(commit)) = (LOCAL_TIME, LOCAL_COMMIT) else {
+ return;
+ };
+ std::thread::spawn(move || {
+ let Some(remote) = fetch() else { return };
+ if !newer(&remote.time, time) {
+ return;
+ }
+ let download = if ASSET.is_empty() {
+ String::new()
+ } else {
+ format!("Download {ASSET} from:\n")
+ };
+ let message = format!(
+ "A newer CPSC Bundle is available.\n\nInstalled: {time} ({commit})\nLatest: {} ({})\n\n{download}{RELEASE_PAGE}",
+ remote.time, remote.commit
+ );
+ let opener = app.clone();
+ app.dialog()
+ .message(message)
+ .title("Update available")
+ .kind(MessageDialogKind::Info)
+ .buttons(MessageDialogButtons::OkCancelCustom(
+ "Open Release Page".into(),
+ "Later".into(),
+ ))
+ .show(move |open| {
+ if open {
+ let _ = opener.opener().open_url(RELEASE_PAGE, None::<&str>);
+ }
+ });
+ });
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn compares_times() {
+ assert!(newer("20261022-044114", "20261021-140654"));
+ assert!(!newer("20261022-044114", "20261022-044114"));
+ assert!(!newer("20261021-140654", "20261022-044114"));
+ assert!(!newer("garbage", "20261022-044114"));
+ assert!(!newer("20261022-044114", "2026102-0441144"));
+ assert!(!newer("20261022x044114", "20200101-000000"));
+ }
+
+ #[test]
+ fn parses_version_json() {
+ let json = r#"{
+ "compiler": { "time": "20261022-044114", "commit": "620a154" },
+ "plugin": { "time": "20261021-140654", "commit": "899c2af" },
+ "bundle": { "time": "20261022-044114", "commit": "620a154" }
+ }"#;
+ let bundle = latest(json).unwrap();
+ assert_eq!((bundle.time.as_str(), bundle.commit.as_str()), ("20261022-044114", "620a154"));
+ }
+
+ #[test]
+ fn rejects_garbage_json() {
+ assert!(latest("not json").is_none());
+ assert!(latest("{}").is_none());
+ }
+}