Skip to main content

treeboot_core/
discovery.rs

1use std::path::{Path, PathBuf};
2
3use serde::Serialize;
4
5use crate::context::resolve_worktree_path;
6use crate::{Error, Result, Worktree};
7
8const INIT_SCRIPT_PATHS: &[&str] = &[".treeboot.sh", ".treebootrc", ".config/treeboot/init"];
9const IGNORE_REASON_NOT_EXECUTABLE: &str = "not_executable";
10const CONFIG_PATHS: &[&str] = &[
11    ".treeboot.toml",
12    "treeboot.toml",
13    ".config/treeboot/config.toml",
14];
15
16/// Discovered treeboot init scripts for a worktree.
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct InitScriptDiscovery {
19    /// First executable init script found in treeboot precedence order.
20    pub executable: Option<PathBuf>,
21    /// Existing init script paths that were ignored.
22    pub ignored: Vec<IgnoredInitScript>,
23}
24
25impl InitScriptDiscovery {
26    /// Discovers executable and ignored init scripts for a worktree.
27    #[must_use]
28    pub fn discover(context: &Worktree) -> Self {
29        discover_scripts(&context.worktree_path)
30    }
31}
32
33/// Existing init script path that treeboot skipped during discovery.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub struct IgnoredInitScript {
36    /// Script candidate path.
37    pub path: PathBuf,
38    /// Stable ignore reason.
39    ///
40    /// The initial reason is `not_executable`.
41    pub reason: &'static str,
42}
43
44pub(crate) fn discover_scripts(worktree_path: &Path) -> InitScriptDiscovery {
45    let mut ignored = Vec::new();
46
47    for relative in INIT_SCRIPT_PATHS {
48        let path = worktree_path.join(relative);
49
50        if !path.is_file() {
51            continue;
52        }
53
54        if is_executable(&path) {
55            return InitScriptDiscovery {
56                executable: Some(path),
57                ignored,
58            };
59        }
60
61        ignored.push(IgnoredInitScript {
62            path,
63            reason: IGNORE_REASON_NOT_EXECUTABLE,
64        });
65    }
66
67    InitScriptDiscovery {
68        executable: None,
69        ignored,
70    }
71}
72
73pub(crate) fn discover_config(
74    worktree_path: &Path,
75    requested_config: Option<&Path>,
76) -> Result<Option<PathBuf>> {
77    if let Some(path) = requested_config {
78        let path = resolve_worktree_path(worktree_path, path);
79
80        if path.is_file() {
81            return Ok(Some(path));
82        }
83
84        return Err(Error::ConfigNotFound(path));
85    }
86
87    Ok(CONFIG_PATHS
88        .iter()
89        .map(|relative| worktree_path.join(relative))
90        .find(|path| path.is_file()))
91}
92
93#[cfg(unix)]
94fn is_executable(path: &Path) -> bool {
95    use std::os::unix::fs::PermissionsExt;
96
97    path.metadata()
98        .map(|metadata| metadata.permissions().mode() & 0o111 != 0)
99        .unwrap_or(false)
100}
101
102#[cfg(not(unix))]
103fn is_executable(path: &Path) -> bool {
104    path.is_file()
105}