1use miette::{IntoDiagnostic, Result, miette};
9use semver::Version;
10use tracing::info;
11
12const TARGET: &str = env!("TARGET");
14const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
16const BASE_URL: &str = "https://vit.am/~ololduck/abbaye";
18const FEED_URL: &str = "https://vit.am/~ololduck/abbaye/releases.atom";
20
21fn parse_versions_from_feed(xml: &str) -> Vec<Version> {
29 let mut versions = Vec::new();
30 let mut in_entry = false;
31
32 for line in xml.lines() {
33 let trimmed = line.trim();
34 if trimmed.starts_with("<entry>") {
35 in_entry = true;
36 } else if trimmed == "</entry>" {
37 in_entry = false;
38 } else if in_entry {
39 if let Some(rest) = trimmed.strip_prefix("<title>") {
40 if let Some(title) = rest.strip_suffix("</title>") {
41 let v = title.strip_prefix('v').unwrap_or(title);
43 if let Ok(version) = Version::parse(v) {
44 versions.push(version);
45 }
46 }
47 }
48 }
49 }
50
51 versions
52}
53
54pub async fn self_update(check_only: bool) -> Result<()> {
62 let current = Version::parse(CURRENT_VERSION).into_diagnostic()?;
63
64 info!("Checking for updates (current: v{current})…");
65 println!("Checking for updates (current: v{current})…");
66
67 let client = reqwest::Client::builder()
68 .user_agent(format!("abbaye/{CURRENT_VERSION}"))
69 .build()
70 .into_diagnostic()?;
71
72 let response = client.get(FEED_URL).send().await.into_diagnostic()?;
73
74 if !response.status().is_success() {
75 return Err(miette!(
76 "Failed to fetch release feed (HTTP {}): {FEED_URL}",
77 response.status()
78 ));
79 }
80
81 let feed_xml = response.text().await.into_diagnostic()?;
82 let mut versions = parse_versions_from_feed(&feed_xml);
83
84 if versions.is_empty() {
85 return Err(miette!(
86 "No versions found in the release feed at {FEED_URL}"
87 ));
88 }
89
90 versions.sort();
91 let latest = versions.into_iter().max().unwrap();
92
93 if latest <= current {
94 println!("Already up to date (v{current}).");
95 return Ok(());
96 }
97
98 println!("Update available: v{current} → v{latest}");
99
100 if check_only {
101 println!("Run `abbaye self-update` without --check to apply the update.");
102 return Ok(());
103 }
104
105 let exe_suffix = std::env::consts::EXE_SUFFIX; let artifact_name = format!("abbaye-{latest}-{TARGET}{exe_suffix}");
107 let download_url = format!("{BASE_URL}/{latest}/dist/{artifact_name}");
108
109 println!("Downloading {artifact_name}…");
110 info!("Downloading from {download_url}");
111
112 let dl_response = client.get(&download_url).send().await.into_diagnostic()?;
113
114 if !dl_response.status().is_success() {
115 return Err(miette!(
116 "Download failed (HTTP {}): {download_url}",
117 dl_response.status()
118 ));
119 }
120
121 let bytes = dl_response.bytes().await.into_diagnostic()?;
122
123 let current_exe = std::env::current_exe()
126 .into_diagnostic()?
127 .canonicalize()
128 .into_diagnostic()?;
129
130 let exe_dir = current_exe
131 .parent()
132 .ok_or_else(|| miette!("Could not determine the directory of the current executable"))?;
133
134 let tmp_path = exe_dir.join(format!("abbaye.new{exe_suffix}"));
135
136 std::fs::write(&tmp_path, &bytes).into_diagnostic()?;
137
138 #[cfg(unix)]
140 {
141 use std::os::unix::fs::PermissionsExt;
142 let perms = std::fs::Permissions::from_mode(0o755);
143 std::fs::set_permissions(&tmp_path, perms).into_diagnostic()?;
144 }
145
146 std::fs::rename(&tmp_path, ¤t_exe).into_diagnostic()?;
148
149 println!("Successfully updated to v{latest}!");
150 info!("Updated to v{latest}");
151
152 Ok(())
153}