corundum_framework_package/
start.rs1use std::fs;
2use std::path::Path;
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use corundum_framework_platform::dirs::AppDirs;
6use corundum_framework_platform::package;
7use semver::Version;
8use velopack::VelopackApp;
9
10use crate::backend::{self, Located};
11
12#[cfg(windows)]
13use crate::cli_path;
14use crate::cli_path::CliSpec;
15
16const FIRSTRUN: &str = "VELOPACK_FIRSTRUN";
19
20const RESTART: &str = "VELOPACK_RESTART";
23
24static RESTARTED: AtomicBool = AtomicBool::new(false);
26
27const KEPT_VERSION: &str = "package-version";
29
30#[derive(Clone, Debug)]
32pub struct PackageSpec {
33 pub app_name: &'static str,
35 pub on_installed: Option<fn()>,
38 pub on_uninstalling: Option<fn()>,
41 pub before_apply: Option<fn() -> Result<(), String>>,
44 pub cli: Option<CliSpec>,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
50pub enum Start {
51 NotPackaged,
53 FirstStart { version: Version },
55 Updated { from: Version, to: Version },
57 Same { version: Version },
59}
60
61pub fn start(spec: &PackageSpec) -> Start {
66 let firstrun = std::env::var_os(FIRSTRUN).is_some();
67 RESTARTED.store(std::env::var_os(RESTART).is_some(), Ordering::Relaxed);
68 let app = VelopackApp::build().set_auto_apply_on_startup(false);
71 let located = backend::locate();
74 let app = match &located {
75 Ok(Located {
76 config: Some(config),
77 ..
78 }) => app.set_locator(config.clone()),
79 _ => app,
80 };
81 #[cfg(windows)]
82 let app = windows_hooks(app, spec);
83 let mut app = app;
84 app.run();
85
86 if package::current().is_none() {
87 return Start::NotPackaged;
88 }
89 let now = match located {
90 Ok(Located { version, .. }) => version,
91 Err(e) => {
92 tracing::warn!("in a package, but its version could not be read: {e}");
93 return Start::NotPackaged;
94 }
95 };
96 let kept_path = AppDirs::new(spec.app_name).map(|dirs| dirs.data().join(KEPT_VERSION));
97 let kept = kept_path
100 .as_deref()
101 .and_then(|path| fs::read(path).ok())
102 .map(|bytes| String::from_utf8_lossy(&bytes).into_owned());
103 let start = classify(kept.as_deref(), firstrun, &now);
104 match &kept_path {
105 Some(path) => {
106 if let Err(e) = keep(path, &now) {
107 tracing::warn!("could not keep the version in {}: {e}", path.display());
108 }
109 }
110 None => tracing::warn!("no data directory to keep the version in"),
111 }
112 #[cfg(not(windows))]
115 if matches!(start, Start::FirstStart { .. })
116 && let Some(on_installed) = spec.on_installed
117 {
118 on_installed();
119 }
120 start
121}
122
123pub(crate) fn restarted() -> bool {
127 RESTARTED.load(Ordering::Relaxed)
128}
129
130#[cfg(windows)]
134fn windows_hooks<'a>(app: VelopackApp<'a>, spec: &PackageSpec) -> VelopackApp<'a> {
135 let on_installed = spec.on_installed;
136 let on_uninstalling = spec.on_uninstalling;
137 let cli = spec.cli;
138 app.on_after_install_fast_callback(move |_| {
140 if let Some(cli) = cli
141 && let Err(e) = cli_path::install(&cli)
142 {
143 tracing::warn!("could not put {} on PATH: {e}", cli.command);
144 }
145 if let Some(on_installed) = on_installed {
146 on_installed();
147 }
148 })
149 .on_before_uninstall_fast_callback(move |_| {
150 if let Some(on_uninstalling) = on_uninstalling {
151 on_uninstalling();
152 }
153 if let Some(cli) = cli
154 && let Err(e) = cli_path::uninstall(&cli)
155 {
156 tracing::warn!("could not take {} off PATH: {e}", cli.command);
157 }
158 })
159}
160
161fn keep(path: &Path, version: &Version) -> std::io::Result<()> {
163 if let Some(dir) = path.parent() {
164 fs::create_dir_all(dir)?;
165 }
166 fs::write(path, version.to_string())
167}
168
169pub(crate) fn classify(kept: Option<&str>, firstrun: bool, now: &Version) -> Start {
173 match kept.and_then(|kept| Version::parse(kept.trim()).ok()) {
174 Some(from) if !firstrun => {
175 if from == *now {
176 Start::Same {
177 version: now.clone(),
178 }
179 } else {
180 Start::Updated {
181 from,
182 to: now.clone(),
183 }
184 }
185 }
186 _ => Start::FirstStart {
187 version: now.clone(),
188 },
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 fn v(s: &str) -> Version {
197 Version::parse(s).unwrap()
198 }
199
200 #[test]
201 fn a_start_is_told_from_the_kept_version_the_variable_and_the_package() {
202 let first = |now: &str| Start::FirstStart { version: v(now) };
203 let same = |now: &str| Start::Same { version: v(now) };
204 let updated = |from: &str, to: &str| Start::Updated {
205 from: v(from),
206 to: v(to),
207 };
208 let cases: &[(Option<&str>, bool, &str, Start)] = &[
209 (None, false, "1.0.0", first("1.0.0")),
210 (Some("1.0.0"), false, "1.0.0", same("1.0.0")),
211 (Some("1.0.0"), false, "1.1.0", updated("1.0.0", "1.1.0")),
212 (Some("1.1.0"), false, "1.0.0", updated("1.1.0", "1.0.0")),
213 (Some("1.0.0"), true, "1.0.0", first("1.0.0")),
214 (Some(""), false, "1.0.0", first("1.0.0")),
215 (Some("garbage"), false, "1.0.0", first("1.0.0")),
216 (Some(" 1.0.0\n"), false, "1.0.0", same("1.0.0")),
217 ];
218 for (kept, firstrun, now, want) in cases {
219 assert_eq!(
220 classify(*kept, *firstrun, &v(now)),
221 *want,
222 "kept {kept:?}, firstrun {firstrun}, now {now}"
223 );
224 }
225 }
226}