9b7e474e80
- Add #![forbid(unsafe_code)] to main.rs (issue #3) - Replace raw ANSI escape codes with owo-colors crate (issue #2) - Replace manual HOME path construction with dirs::home_dir() (issue #5) - Ship umutray.service as a static file; service::install() substitutes the binary path at install time instead of generating the unit at runtime - Add packaging/PKGBUILD following Arch Rust package guidelines - Add CLAUDE.md tracking refactor tasks - setup.rs: clean up downloaded temp files on abort/back, save launcher to config only after successful install, auto-start download when a preset has an installer_url - util.rs: add pick_folder() using zenity/kdialog subprocesses (no rfd) - config.rs: populate installer_url for all 6 built-in presets with official download URLs - Document the Option<Option<Vec<String>>> gamescope pattern at main.rs:307 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
255 lines
7.3 KiB
Rust
255 lines
7.3 KiB
Rust
use crate::config::{Config, Launcher};
|
|
use anyhow::Result;
|
|
use owo_colors::OwoColorize;
|
|
use std::os::unix::fs::MetadataExt;
|
|
use std::path::Path;
|
|
use std::process::{Command, Stdio};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckResult {
|
|
pub label: String,
|
|
pub pass: bool,
|
|
pub detail: String,
|
|
}
|
|
|
|
impl CheckResult {
|
|
fn pass(label: impl Into<String>, detail: impl Into<String>) -> Self {
|
|
Self { label: label.into(), pass: true, detail: detail.into() }
|
|
}
|
|
fn fail(label: impl Into<String>, detail: impl Into<String>) -> Self {
|
|
Self { label: label.into(), pass: false, detail: detail.into() }
|
|
}
|
|
}
|
|
|
|
pub fn run_checks(config: &Config, name: Option<&str>) -> Result<Vec<CheckResult>> {
|
|
let mut checks = vec![
|
|
global_umu_check(),
|
|
global_vulkan_check(),
|
|
global_display_check(),
|
|
compat_dir_check(config),
|
|
wineserver_check(config),
|
|
];
|
|
let launchers: Vec<&Launcher> = if let Some(n) = name {
|
|
let l = config
|
|
.find(n)
|
|
.ok_or_else(|| anyhow::anyhow!("unknown launcher '{n}'"))?;
|
|
vec![l]
|
|
} else {
|
|
config.launchers.iter().collect()
|
|
};
|
|
for l in launchers {
|
|
checks.extend(launcher_checks(l));
|
|
}
|
|
Ok(checks)
|
|
}
|
|
|
|
pub fn run(config: &Config, name: Option<&str>) -> Result<()> {
|
|
let checks = run_checks(config, name)?;
|
|
let mut issues = 0u32;
|
|
println!();
|
|
for c in &checks {
|
|
if !c.pass {
|
|
issues += 1;
|
|
}
|
|
if c.pass {
|
|
println!(" {} {:24} {}", "✓".green().bold(), c.label, c.detail);
|
|
} else {
|
|
println!(" {} {:24} {}", "✗".red().bold(), c.label, c.detail);
|
|
}
|
|
}
|
|
println!();
|
|
if issues == 0 {
|
|
println!(" {}", "All checks passed.".green().bold());
|
|
} else {
|
|
println!(" {}", format!("{issues} issue(s) found — see ✗ items above.").red().bold());
|
|
}
|
|
println!();
|
|
Ok(())
|
|
}
|
|
|
|
fn global_umu_check() -> CheckResult {
|
|
match which("umu-run") {
|
|
Some(p) => CheckResult::pass("umu-run", format!("found at {p}")),
|
|
None => CheckResult::fail("umu-run", "not found — install umu-launcher"),
|
|
}
|
|
}
|
|
|
|
fn global_vulkan_check() -> CheckResult {
|
|
let ok = Command::new("vulkaninfo")
|
|
.arg("--summary")
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false);
|
|
if ok {
|
|
CheckResult::pass("vulkan", "vulkaninfo OK")
|
|
} else {
|
|
CheckResult::fail(
|
|
"vulkan",
|
|
"vulkaninfo failed — check GPU drivers / vulkan-tools",
|
|
)
|
|
}
|
|
}
|
|
|
|
fn global_display_check() -> CheckResult {
|
|
let display = std::env::var("DISPLAY").ok();
|
|
let wayland = std::env::var("WAYLAND_DISPLAY").ok();
|
|
match (display, wayland) {
|
|
(Some(d), Some(_)) => CheckResult::pass("display", format!("XWayland (DISPLAY={d})")),
|
|
(Some(d), None) => CheckResult::pass("display", format!("X11 (DISPLAY={d})")),
|
|
(None, Some(_)) => CheckResult::fail(
|
|
"display",
|
|
"Wayland session but DISPLAY unset; XWayland needed",
|
|
),
|
|
(None, None) => CheckResult::fail("display", "neither DISPLAY nor WAYLAND_DISPLAY set"),
|
|
}
|
|
}
|
|
|
|
fn compat_dir_check(config: &Config) -> CheckResult {
|
|
let n = count_ge_proton(&config.proton_compat_dir);
|
|
if config.proton_version == "GE-Proton" {
|
|
CheckResult::pass(
|
|
"proton",
|
|
format!(
|
|
"tracking latest; {n} version(s) in {}",
|
|
config.proton_compat_dir.display()
|
|
),
|
|
)
|
|
} else {
|
|
let path = config.proton_compat_dir.join(&config.proton_version);
|
|
if path.exists() {
|
|
CheckResult::pass("proton", format!("{} installed", config.proton_version))
|
|
} else {
|
|
CheckResult::fail(
|
|
"proton",
|
|
format!(
|
|
"{} missing — run: umutray update-proton --version={}",
|
|
config.proton_version, config.proton_version
|
|
),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn wineserver_check(config: &Config) -> CheckResult {
|
|
let count = wineserver_count();
|
|
if count == 0 {
|
|
return CheckResult::pass("wine procs", "no wineserver running");
|
|
}
|
|
let any_running = config.launchers.iter().any(crate::launcher::is_running);
|
|
if any_running {
|
|
CheckResult::pass(
|
|
"wine procs",
|
|
format!("{count} wineserver process(es); launcher active"),
|
|
)
|
|
} else {
|
|
CheckResult::fail(
|
|
"wine procs",
|
|
format!("{count} stale wineserver process(es) — try: umutray kill"),
|
|
)
|
|
}
|
|
}
|
|
|
|
fn wineserver_count() -> usize {
|
|
Command::new("pgrep")
|
|
.args(["-c", "-f", "wineserver"])
|
|
.output()
|
|
.ok()
|
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
|
.and_then(|s| s.trim().parse::<usize>().ok())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn launcher_checks(l: &Launcher) -> Vec<CheckResult> {
|
|
let mut out = Vec::new();
|
|
let tag = format!("[{}]", l.name);
|
|
|
|
if !l.prefix_dir.exists() {
|
|
out.push(CheckResult::fail(
|
|
format!("{tag} prefix"),
|
|
format!(
|
|
"{} missing — run: umutray setup {}",
|
|
l.prefix_dir.display(),
|
|
l.name
|
|
),
|
|
));
|
|
return out;
|
|
}
|
|
out.push(CheckResult::pass(
|
|
format!("{tag} prefix"),
|
|
l.prefix_dir.display().to_string(),
|
|
));
|
|
|
|
let exe = l.full_exe_path();
|
|
if exe.exists() {
|
|
out.push(CheckResult::pass(format!("{tag} exe"), "installed"));
|
|
} else {
|
|
out.push(CheckResult::fail(
|
|
format!("{tag} exe"),
|
|
format!("missing — run: umutray setup {}", l.name),
|
|
));
|
|
}
|
|
|
|
if is_owned_by_current_user(&l.prefix_dir) {
|
|
out.push(CheckResult::pass(format!("{tag} owner"), "owned by current user"));
|
|
} else {
|
|
out.push(CheckResult::fail(
|
|
format!("{tag} owner"),
|
|
"not owned by current user",
|
|
));
|
|
}
|
|
|
|
if crate::launcher::is_running(l) {
|
|
out.push(CheckResult::pass(format!("{tag} process"), "currently running"));
|
|
} else {
|
|
out.push(CheckResult::pass(format!("{tag} process"), "not running"));
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
fn count_ge_proton(dir: &Path) -> usize {
|
|
std::fs::read_dir(dir)
|
|
.ok()
|
|
.map(|entries| {
|
|
entries
|
|
.filter_map(|e| e.ok())
|
|
.filter(|e| {
|
|
e.file_name()
|
|
.to_str()
|
|
.map(|s| s.starts_with("GE-Proton"))
|
|
.unwrap_or(false)
|
|
})
|
|
.count()
|
|
})
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn which(cmd: &str) -> Option<String> {
|
|
Command::new("which")
|
|
.arg(cmd)
|
|
.output()
|
|
.ok()
|
|
.filter(|o| o.status.success())
|
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
|
.map(|s| s.trim().to_string())
|
|
}
|
|
|
|
fn is_owned_by_current_user(path: &Path) -> bool {
|
|
let file_uid = match std::fs::metadata(path) {
|
|
Ok(m) => m.uid(),
|
|
Err(_) => return true,
|
|
};
|
|
let current_uid: Option<u32> = Command::new("id")
|
|
.arg("-u")
|
|
.output()
|
|
.ok()
|
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
|
.and_then(|s| s.trim().parse().ok());
|
|
match current_uid {
|
|
Some(uid) => uid == file_uid,
|
|
None => true,
|
|
}
|
|
}
|