Skip to main content

mecha10_cli/services/process/
resolve.rs

1//! Binary path resolution for `ProcessService`
2//!
3//! Handles finding globally installed binaries and resolving paths to the
4//! `mecha10-node-runner` binary and the `mecha10` CLI binary itself. Node
5//! binary resolution for framework nodes is handled by `BinaryManager`
6//! (see `spawn_node.rs`), which errors immediately on an unresolvable binary
7//! rather than falling back through further tiers.
8
9use super::ProcessService;
10use crate::paths;
11use anyhow::Result;
12
13impl ProcessService {
14    /// Find globally installed binary for a node
15    ///
16    /// Searches in:
17    /// 1. ~/.cargo/bin/{node_name}
18    /// 2. ~/.mecha10/bin/{node_name}
19    ///
20    /// # Arguments
21    ///
22    /// * `node_name` - Name of the node (e.g., "camera")
23    ///
24    /// # Returns
25    ///
26    /// Path to the binary if found, None otherwise
27    pub fn find_global_binary(node_name: &str) -> Option<std::path::PathBuf> {
28        let cargo_bin = paths::user::cargo_bin(node_name);
29        if cargo_bin.exists() && cargo_bin.is_file() {
30            return Some(cargo_bin);
31        }
32
33        let mecha10_bin = paths::user::bin(node_name);
34        if mecha10_bin.exists() && mecha10_bin.is_file() {
35            return Some(mecha10_bin);
36        }
37
38        None
39    }
40
41    /// Resolve path to mecha10-node-runner binary
42    ///
43    /// Resolution strategy:
44    /// 1. Global installation: ~/.cargo/bin/mecha10-node-runner
45    /// 2. Fallback: "mecha10-node-runner" (rely on PATH)
46    ///
47    /// # Returns
48    ///
49    /// Path to the mecha10-node-runner binary
50    pub fn resolve_node_runner_path() -> String {
51        if let Some(global_path) = Self::find_global_binary("mecha10-node-runner") {
52            return global_path.to_string_lossy().to_string();
53        }
54
55        "mecha10-node-runner".to_string()
56    }
57
58    /// Get the version for a node from the project config
59    pub(super) fn get_node_version_from_config(&self, node_identifier: &str) -> Option<String> {
60        use crate::types::ProjectConfig;
61
62        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
63        if !config_path.exists() {
64            return None;
65        }
66
67        let config_content = std::fs::read_to_string(config_path).ok()?;
68        let config: ProjectConfig = serde_json::from_str(&config_content).ok()?;
69
70        config.nodes.get_version(node_identifier).map(|s| s.to_string())
71    }
72
73    /// Check whether disk-based logging is enabled for the current project's *current*
74    /// environment (LAB-2041).
75    ///
76    /// Reads `environments.<current_env>.logging.disk_logging_enabled` from the
77    /// project's `mecha10.json`, where `<current_env>` is whatever
78    /// `EnvironmentsConfig::current_env_name` resolves to (`MECHA10_ENV`, defaulting to
79    /// "dev"). Defaults to `true` (existing behavior) when no project config is
80    /// present, it can't be parsed, or the current environment has no `logging`
81    /// section, so projects without an explicit override are unaffected.
82    ///
83    /// Edge/robot deployments that already ship logs to a remote server can set this to
84    /// `false` on their `prod`/`staging` environment to avoid unbounded local disk
85    /// growth and repeated writes wearing out SD/USB storage, without disabling disk
86    /// logging in `dev` too. This has no effect on remote/network log shipping.
87    pub(super) fn disk_logging_enabled(&self) -> bool {
88        use crate::types::ProjectConfig;
89
90        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
91        if !config_path.exists() {
92            return true;
93        }
94
95        let config_content = match std::fs::read_to_string(config_path) {
96            Ok(c) => c,
97            Err(_) => return true,
98        };
99
100        let config: ProjectConfig = match serde_json::from_str(&config_content) {
101            Ok(c) => c,
102            Err(_) => return true,
103        };
104
105        config.environments.disk_logging_enabled()
106    }
107
108    /// Check if a node is a local project node (declared with `NodeSource::Project` in
109    /// `mecha10.json`) - delegates to `mecha10_cli_core::types::is_local_project_node`, the
110    /// same check `mecha10-node-runner`'s `resolve_node_execution` uses, so the two can't
111    /// drift onto different classification rules for the same node.
112    pub(super) fn is_local_project_node(&self, node_name: &str) -> bool {
113        mecha10_cli_core::types::is_local_project_node(std::path::Path::new(paths::PROJECT_CONFIG), node_name)
114    }
115
116    /// Resolve path to mecha10 CLI binary
117    ///
118    /// Tiers below (`LAB-4558`: sequenced via `mecha10_binary_naming::try_resolution_tiers!`,
119    /// the same ordered-fallback-chain helper shared with `BinaryManager::resolve_with_version`
120    /// and `NodeResolver::resolve_with_version` - only the concrete tiers here are CLI-specific):
121    /// 1. Global installation: ~/.cargo/bin/mecha10 or ~/.mecha10/bin/mecha10
122    /// 2. Local/cargo bin fallback locations
123    /// 3. The currently running executable
124    pub(super) fn resolve_cli_binary() -> Result<String> {
125        mecha10_binary_naming::try_resolution_tiers!("mecha10 CLI binary", {
126            "global binary" => Self::find_global_binary("mecha10")
127                .map(|path| path.to_string_lossy().to_string())
128                .ok_or_else(|| "not found at ~/.cargo/bin or ~/.mecha10/bin".to_string()),
129
130            "local/cargo bin fallback locations" => {
131                let locations = [paths::user::local_bin("mecha10"), paths::user::cargo_bin("mecha10")];
132                locations
133                    .iter()
134                    .find(|path| path.exists())
135                    .map(|path| path.to_string_lossy().to_string())
136                    .ok_or_else(|| "not found at local_bin/cargo_bin fallback locations".to_string())
137            },
138
139            "current executable" => std::env::current_exe()
140                .map(|exe| exe.to_string_lossy().to_string())
141                .map_err(|e| format!("current_exe() failed: {e}")),
142        })
143        .map_err(|e| anyhow::anyhow!("{}. Ensure it's installed and in your PATH.", e))
144    }
145
146    /// Check if mecha10-node-runner is available
147    pub fn is_node_runner_available() -> bool {
148        Self::find_global_binary("mecha10-node-runner").is_some()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    /// `resolve_cli_binary` falls all the way through to the "current executable" tier when
157    /// none of the global/local/cargo-bin locations exist (the common case for `cargo test`,
158    /// where none of those directories contain a `mecha10` binary) - this must still resolve
159    /// successfully via `std::env::current_exe()` rather than erroring, preserving the
160    /// pre-`LAB-4558` three-tier fallback behavior.
161    #[test]
162    fn resolve_cli_binary_falls_back_to_current_exe_when_no_global_or_local_binary_exists() {
163        let resolved = ProcessService::resolve_cli_binary().expect("current_exe() tier must succeed");
164        assert!(!resolved.is_empty());
165    }
166}