upstream_rs/application/operations/
init_operation.rs1use crate::utils::static_paths::UpstreamPaths;
2#[cfg(windows)]
3use anyhow::Context;
4use anyhow::Result;
5use std::collections::BTreeSet;
6use std::fs;
7use std::io::{self, Write};
8use std::path::Path;
9
10const SOURCE_LINE_BASH: &str =
12 "[ -f $HOME/.upstream/metadata/paths.sh ] && source $HOME/.upstream/metadata/paths.sh";
13const SOURCE_LINE_FISH: &str = "source $HOME/.upstream/metadata/paths.sh";
14
15pub struct InitCheckReport {
16 pub ok: bool,
17 pub messages: Vec<String>,
18}
19
20#[cfg(windows)]
21fn normalize_windows_path(path: &str) -> String {
22 let mut normalized = path.replace('/', "\\").trim().to_ascii_lowercase();
23 while normalized.ends_with('\\') {
24 normalized.pop();
25 }
26 normalized
27}
28
29pub fn initialize(paths: &UpstreamPaths) -> Result<()> {
30 create_package_dirs(paths)?;
31 create_metadata_files(paths)?;
32
33 #[cfg(windows)]
34 add_to_windows_path(paths)?;
35
36 #[cfg(unix)]
37 update_shell_profiles(paths)?;
38
39 Ok(())
40}
41
42pub fn check(paths: &UpstreamPaths) -> Result<InitCheckReport> {
43 let mut report = InitCheckReport {
44 ok: true,
45 messages: Vec::new(),
46 };
47
48 for (label, path) in [
49 ("config directory", &paths.dirs.config_dir),
50 ("data directory", &paths.dirs.data_dir),
51 ("metadata directory", &paths.dirs.metadata_dir),
52 ("symlinks directory", &paths.integration.symlinks_dir),
53 ("appimages directory", &paths.install.appimages_dir),
54 ("binaries directory", &paths.install.binaries_dir),
55 ("archives directory", &paths.install.archives_dir),
56 ] {
57 if path.exists() {
58 report
59 .messages
60 .push(format!("[OK] {} exists: {}", label, path.display()));
61 } else {
62 report.ok = false;
63 report
64 .messages
65 .push(format!("[FAIL] {} missing: {}", label, path.display()));
66 }
67 }
68
69 #[cfg(unix)]
70 check_unix_integration(paths, &mut report)?;
71
72 #[cfg(windows)]
73 check_windows_integration(paths, &mut report)?;
74
75 Ok(report)
76}
77
78#[cfg(unix)]
79fn get_installed_shells() -> io::Result<Vec<String>> {
80 const SHELLS_FILE: &str = "/etc/shells";
81 if !Path::new(SHELLS_FILE).exists() {
82 return Ok(Vec::new());
83 }
84 let content = fs::read_to_string(SHELLS_FILE)?;
85 let shells = content
86 .lines()
87 .map(|l| l.trim())
88 .filter(|l| !l.is_empty() && !l.starts_with('#'))
89 .map(|l| l.to_string())
90 .collect();
91 Ok(shells)
92}
93
94#[cfg(windows)]
95fn add_to_windows_path(paths: &UpstreamPaths) -> Result<()> {
96 use winreg::RegKey;
97 use winreg::enums::*;
98
99 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
100 let env_key = hkcu
101 .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
102 .context("Failed to open registry key")?;
103
104 let symlinks_path = paths.integration.symlinks_dir.display().to_string();
105 let symlinks_norm = normalize_windows_path(&symlinks_path);
106
107 let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
109
110 let path_entries: Vec<&str> = current_path.split(';').collect();
112 if path_entries
113 .iter()
114 .any(|&p| normalize_windows_path(p) == symlinks_norm)
115 {
116 return Ok(()); }
118
119 let new_path = if current_path.is_empty() {
121 symlinks_path
122 } else {
123 format!("{};{}", symlinks_path, current_path)
124 };
125
126 env_key
127 .set_value("Path", &new_path)
128 .context("Failed to set PATH")?;
129
130 broadcast_environment_change();
132
133 Ok(())
134}
135
136#[cfg(windows)]
137fn broadcast_environment_change() {
138 use std::ptr;
139 use winapi::shared::minwindef::LPARAM;
140 use winapi::um::winuser::{
141 HWND_BROADCAST, SMTO_ABORTIFHUNG, SendMessageTimeoutW, WM_SETTINGCHANGE,
142 };
143
144 unsafe {
145 let env_string: Vec<u16> = "Environment\0".encode_utf16().collect();
146 SendMessageTimeoutW(
147 HWND_BROADCAST,
148 WM_SETTINGCHANGE,
149 0,
150 env_string.as_ptr() as LPARAM,
151 SMTO_ABORTIFHUNG,
152 5000,
153 ptr::null_mut(),
154 );
155 }
156}
157
158fn create_package_dirs(paths: &UpstreamPaths) -> io::Result<()> {
159 fs::create_dir_all(&paths.dirs.config_dir)?;
160 fs::create_dir_all(&paths.dirs.data_dir)?;
161 fs::create_dir_all(&paths.dirs.metadata_dir)?;
162 fs::create_dir_all(&paths.install.appimages_dir)?;
163 fs::create_dir_all(&paths.install.binaries_dir)?;
164 fs::create_dir_all(&paths.install.archives_dir)?;
165 fs::create_dir_all(&paths.integration.icons_dir)?;
166 fs::create_dir_all(&paths.integration.symlinks_dir)?;
167 Ok(())
168}
169
170#[cfg(unix)]
171fn create_metadata_files(paths: &UpstreamPaths) -> io::Result<()> {
172 if !paths.config.paths_file.exists() {
173 let export_line = format!(
174 r#"export PATH="{}:$PATH""#,
175 paths.integration.symlinks_dir.display()
176 );
177 fs::write(
178 &paths.config.paths_file,
179 format!(
180 "#!/bin/bash\n# Upstream managed PATH additions\n{}\n",
181 export_line
182 ),
183 )?;
184 }
185 Ok(())
186}
187
188#[cfg(windows)]
189fn create_metadata_files(_paths: &UpstreamPaths) -> io::Result<()> {
190 Ok(())
192}
193
194#[cfg(unix)]
195fn update_shell_profiles(paths: &UpstreamPaths) -> io::Result<()> {
196 let shells = get_installed_shells()?;
197 for shell_path in shells {
198 let shell_name = Path::new(&shell_path)
199 .file_name()
200 .and_then(|s| s.to_str())
201 .unwrap_or("");
202 match shell_name.to_lowercase().as_str() {
203 "bash" | "sh" => {
204 add_line_to_profile(paths, ".bashrc", SOURCE_LINE_BASH)?;
205 }
206 "zsh" => {
207 add_line_to_profile(paths, ".zshrc", SOURCE_LINE_BASH)?;
208 }
209 "fish" => {
210 let fish_config = Path::new(".config").join("fish").join("config.fish");
211 add_line_to_profile(paths, &fish_config.to_string_lossy(), SOURCE_LINE_FISH)?;
212 }
213 _ => {}
214 }
215 }
216 Ok(())
217}
218
219#[cfg(unix)]
220fn check_unix_integration(paths: &UpstreamPaths, report: &mut InitCheckReport) -> io::Result<()> {
221 let expected_line = format!(
222 r#"export PATH="{}:$PATH""#,
223 paths.integration.symlinks_dir.display()
224 );
225
226 if !paths.config.paths_file.exists() {
227 report.ok = false;
228 report.messages.push(format!(
229 "[FAIL] PATH metadata file missing: {}",
230 paths.config.paths_file.display()
231 ));
232 } else {
233 let content = fs::read_to_string(&paths.config.paths_file)?;
234 if content.contains(&expected_line) {
235 report.messages.push(format!(
236 "[OK] PATH metadata file contains symlink export: {}",
237 paths.config.paths_file.display()
238 ));
239 } else {
240 report.ok = false;
241 report.messages.push(format!(
242 "[FAIL] PATH metadata file missing expected export line: {}",
243 paths.config.paths_file.display()
244 ));
245 }
246 }
247
248 let mut profiles_to_check: BTreeSet<(String, String)> = BTreeSet::new();
249 for shell_path in get_installed_shells()? {
250 let shell_name = Path::new(&shell_path)
251 .file_name()
252 .and_then(|s| s.to_str())
253 .unwrap_or("")
254 .to_ascii_lowercase();
255 match shell_name.as_str() {
256 "bash" | "sh" => {
257 profiles_to_check.insert((".bashrc".to_string(), SOURCE_LINE_BASH.to_string()));
258 }
259 "zsh" => {
260 profiles_to_check.insert((".zshrc".to_string(), SOURCE_LINE_BASH.to_string()));
261 }
262 "fish" => {
263 profiles_to_check.insert((
264 ".config/fish/config.fish".to_string(),
265 SOURCE_LINE_FISH.to_string(),
266 ));
267 }
268 _ => {}
269 }
270 }
271
272 for (profile_rel, expected_line) in profiles_to_check {
273 let profile_path = paths.dirs.user_dir.join(&profile_rel);
274 if !profile_path.exists() {
275 report.ok = false;
276 report.messages.push(format!(
277 "[FAIL] Shell profile missing: {}",
278 profile_path.display()
279 ));
280 continue;
281 }
282
283 let content = fs::read_to_string(&profile_path)?;
284 if content.contains(&expected_line) {
285 report.messages.push(format!(
286 "[OK] Shell profile contains upstream hook: {}",
287 profile_path.display()
288 ));
289 } else {
290 report.ok = false;
291 report.messages.push(format!(
292 "[FAIL] Shell profile missing upstream hook: {}",
293 profile_path.display()
294 ));
295 }
296 }
297
298 Ok(())
299}
300
301#[cfg(unix)]
302fn add_line_to_profile(paths: &UpstreamPaths, relative_path: &str, line: &str) -> io::Result<()> {
303 let profile_path = paths.dirs.user_dir.join(relative_path);
304
305 if let Some(parent) = profile_path.parent() {
307 fs::create_dir_all(parent)?;
308 }
309
310 if profile_path.exists() {
312 let backup_path = profile_path.with_extension("bak");
313 if !backup_path.exists() {
314 fs::copy(&profile_path, &backup_path)?;
315 }
316 }
317
318 if !profile_path.exists() {
319 fs::write(&profile_path, format!("{}\n", line))?;
320 return Ok(());
321 }
322
323 let content = fs::read_to_string(&profile_path)?;
324 if !content.contains(line) {
325 let mut file = fs::OpenOptions::new().append(true).open(&profile_path)?;
326 writeln!(file, "\n{}", line)?;
327 }
328
329 Ok(())
330}
331
332#[cfg(unix)]
333pub fn cleanup(paths: &UpstreamPaths) -> Result<()> {
334 let shells = get_installed_shells()?;
335 for shell_path in shells {
336 let shell_name = Path::new(&shell_path)
337 .file_name()
338 .and_then(|s| s.to_str())
339 .unwrap_or("");
340 let profile = match shell_name.to_lowercase().as_str() {
341 "bash" | "sh" => Some(".bashrc"),
342 "zsh" => Some(".zshrc"),
343 "fish" => Some(".config/fish/config.fish"),
344 _ => None,
345 };
346 if let Some(profile_rel) = profile {
347 let profile_path = paths.dirs.user_dir.join(profile_rel);
348 if !profile_path.exists() {
349 continue;
350 }
351 let mut content = fs::read_to_string(&profile_path)?;
352 content = content
353 .replace(&format!("{}\n", SOURCE_LINE_BASH), "")
354 .replace(SOURCE_LINE_BASH, "")
355 .replace(&format!("{}\n", SOURCE_LINE_FISH), "")
356 .replace(SOURCE_LINE_FISH, "");
357 fs::write(&profile_path, content)?;
358 }
359 }
360 Ok(())
361}
362
363#[cfg(windows)]
364pub fn cleanup(paths: &UpstreamPaths) -> Result<()> {
365 remove_from_windows_path(paths)
366}
367
368#[cfg(windows)]
369fn remove_from_windows_path(paths: &UpstreamPaths) -> Result<()> {
370 use winreg::RegKey;
371 use winreg::enums::*;
372
373 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
374 let env_key = hkcu
375 .open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
376 .context("Failed to open registry key")?;
377
378 let symlinks_path = paths.integration.symlinks_dir.display().to_string();
379 let symlinks_norm = normalize_windows_path(&symlinks_path);
380
381 let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
383
384 let path_entries: Vec<&str> = current_path
386 .split(';')
387 .filter(|&p| normalize_windows_path(p) != symlinks_norm)
388 .collect();
389
390 let new_path = path_entries.join(";");
391
392 env_key
393 .set_value("Path", &new_path)
394 .context("Failed to set PATH")?;
395
396 broadcast_environment_change();
398
399 Ok(())
400}
401
402#[cfg(windows)]
403fn check_windows_integration(paths: &UpstreamPaths, report: &mut InitCheckReport) -> Result<()> {
404 use winreg::RegKey;
405 use winreg::enums::*;
406
407 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
408 let env_key = hkcu
409 .open_subkey_with_flags("Environment", KEY_READ)
410 .context("Failed to open PATH")?;
411
412 let symlinks_path = paths.integration.symlinks_dir.display().to_string();
413 let symlinks_norm = normalize_windows_path(&symlinks_path);
414 let current_path: String = env_key.get_value("Path").unwrap_or_else(|_| String::new());
415
416 let in_path = current_path
417 .split(';')
418 .any(|p| normalize_windows_path(p) == symlinks_norm);
419
420 if in_path {
421 report
422 .messages
423 .push("[OK] Windows PATH contains upstream symlinks directory".to_string());
424 } else {
425 report.ok = false;
426 report
427 .messages
428 .push("[FAIL] Windows PATH missing upstream symlinks directory".to_string());
429 }
430
431 Ok(())
432}