rattler_build/
system_tools.rs

1//! System tools are installed on the system (git, patchelf, install_name_tool, etc.)
2
3use rattler_conda_types::Platform;
4use rattler_shell::{activation::Activator, shell};
5use serde::{Deserialize, Serialize, Serializer};
6use std::{
7    collections::{BTreeMap, HashMap},
8    path::{Path, PathBuf},
9    process::Command,
10    sync::{Arc, Mutex},
11};
12use thiserror::Error;
13
14#[derive(Error, Debug)]
15#[allow(missing_docs)]
16pub enum ToolError {
17    #[error("failed to find `{0}` ({1})")]
18    ToolNotFound(Tool, which::Error),
19}
20
21/// Any third party tool that is used by rattler build should be added here
22/// and the tool should be invoked through the system tools object.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum Tool {
26    /// The rattler build tool itself
27    #[serde(rename = "rattler-build")]
28    RattlerBuild,
29    /// The patch tool
30    Patch,
31    /// The patchelf tool (for Linux / ELF targets)
32    Patchelf,
33    /// The codesign tool (for macOS targets)
34    Codesign,
35    /// The install_name_tool (for macOS / MachO targets)
36    InstallNameTool,
37    /// The git tool
38    Git,
39}
40
41impl std::fmt::Display for Tool {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(
44            f,
45            "{}",
46            match self {
47                Tool::RattlerBuild => "rattler-build".to_string(),
48                Tool::Codesign => "codesign".to_string(),
49                Tool::Patch => "patch".to_string(),
50                Tool::Patchelf => "patchelf".to_string(),
51                Tool::InstallNameTool => "install_name_tool".to_string(),
52                Tool::Git => "git".to_string(),
53            }
54        )
55    }
56}
57
58/// The system tools object is used to find and call system tools. It also keeps track of the
59/// versions of the tools that are used.
60#[derive(Debug, Clone)]
61pub struct SystemTools {
62    rattler_build_version: String,
63    used_tools: Arc<Mutex<HashMap<Tool, String>>>,
64    found_tools: Arc<Mutex<HashMap<Tool, PathBuf>>>,
65    build_prefix: Option<PathBuf>,
66}
67
68impl Default for SystemTools {
69    fn default() -> Self {
70        Self {
71            rattler_build_version: env!("CARGO_PKG_VERSION").to_string(),
72            used_tools: Arc::new(Mutex::new(HashMap::new())),
73            found_tools: Arc::new(Mutex::new(HashMap::new())),
74            build_prefix: None,
75        }
76    }
77}
78
79impl SystemTools {
80    /// Create a new system tools object
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Create a copy of the system tools object and add a build prefix to search for tools.
86    /// Tools that are found in the build prefix are not added to the used tools list.
87    pub fn with_build_prefix(&self, prefix: &Path) -> Self {
88        Self {
89            build_prefix: Some(prefix.to_path_buf()),
90            ..self.clone()
91        }
92    }
93
94    /// Create a new system tools object from a previous run so that we can warn if the versions
95    /// of the tools have changed
96    pub fn from_previous_run(
97        rattler_build_version: String,
98        used_tools: HashMap<Tool, String>,
99    ) -> Self {
100        if rattler_build_version != env!("CARGO_PKG_VERSION") {
101            tracing::warn!(
102                "Found different version of rattler build: {} and {}",
103                rattler_build_version,
104                env!("CARGO_PKG_VERSION")
105            );
106        }
107
108        Self {
109            rattler_build_version,
110            used_tools: Arc::new(Mutex::new(used_tools)),
111            found_tools: Arc::new(Mutex::new(HashMap::new())),
112            build_prefix: None,
113        }
114    }
115
116    /// Find the tool in the system and return the path to the tool
117    fn find_tool(&self, tool: Tool) -> Result<PathBuf, which::Error> {
118        let which = |tool: &str| -> Result<PathBuf, which::Error> {
119            if let Some(build_prefix) = &self.build_prefix {
120                let build_prefix_activator =
121                    Activator::from_path(build_prefix, shell::Bash, Platform::current()).unwrap();
122
123                let paths = std::env::join_paths(build_prefix_activator.paths).ok();
124                let mut found_tool = which::which_in_global(&tool, paths)?;
125
126                // if the tool is found in the build prefix, return it
127                if let Some(found_tool) = found_tool.next() {
128                    return Ok(found_tool);
129                }
130            }
131            which::which(tool)
132        };
133
134        let (tool_path, found_version) = match tool {
135            Tool::Patchelf => {
136                let path = which("patchelf")?;
137                // patch elf version
138                let output = std::process::Command::new(&path)
139                    .arg("--version")
140                    .output()
141                    .expect("Failed to execute command");
142                let found_version = String::from_utf8_lossy(&output.stdout);
143
144                (path, found_version.to_string())
145            }
146            Tool::InstallNameTool => {
147                let path = which("install_name_tool")?;
148                (path, "".to_string())
149            }
150            Tool::Codesign => {
151                let path = which("codesign")?;
152                (path, "".to_string())
153            }
154            Tool::Git => {
155                let path = which("git")?;
156                let output = std::process::Command::new(&path)
157                    .arg("--version")
158                    .output()
159                    .expect("Failed to execute command");
160                let found_version = String::from_utf8_lossy(&output.stdout);
161
162                (path, found_version.to_string())
163            }
164            Tool::Patch => {
165                let path = which("patch")?;
166                let version = std::process::Command::new(&path)
167                    .arg("--version")
168                    .output()
169                    .expect("Failed to execute `patch` command");
170                let version = String::from_utf8_lossy(&version.stdout);
171                (path, version.to_string())
172            }
173            Tool::RattlerBuild => {
174                let path = std::env::current_exe().expect("Failed to get current executable path");
175                (path, env!("CARGO_PKG_VERSION").to_string())
176            }
177        };
178
179        let found_version = found_version.trim().to_string();
180
181        if let Some(build_prefix) = &self.build_prefix {
182            // Do not cache tools found in the (temporary) build prefix
183            if tool_path.starts_with(build_prefix) {
184                return Ok(tool_path);
185            }
186        }
187
188        self.found_tools
189            .lock()
190            .unwrap()
191            .insert(tool, tool_path.clone());
192        let prev_version = self.used_tools.lock().unwrap().get(&tool).cloned();
193
194        if let Some(prev_version) = prev_version {
195            if prev_version != found_version {
196                tracing::warn!(
197                    "Found different version of patchelf: {} and {}",
198                    prev_version,
199                    found_version
200                );
201            }
202        } else {
203            self.used_tools.lock().unwrap().insert(tool, found_version);
204        }
205
206        Ok(tool_path)
207    }
208
209    /// Create a new `std::process::Command` for the given tool. The command is created with the
210    /// path to the tool and can be further configured with arguments and environment variables.
211    pub fn call(&self, tool: Tool) -> Result<Command, ToolError> {
212        let tool_path = self
213            .find_tool(tool)
214            .map_err(|e| ToolError::ToolNotFound(tool, e))?;
215        Ok(std::process::Command::new(tool_path))
216    }
217}
218
219impl Serialize for SystemTools {
220    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
221        let mut ordered_map = BTreeMap::new();
222        let used_tools = self.used_tools.lock().unwrap();
223        for (tool, version) in used_tools.iter() {
224            ordered_map.insert(tool.to_string(), version);
225        }
226        ordered_map.insert(Tool::RattlerBuild.to_string(), &self.rattler_build_version);
227
228        ordered_map.serialize(serializer)
229    }
230}
231
232impl<'de> serde::Deserialize<'de> for SystemTools {
233    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
234        let mut map = HashMap::<Tool, String>::deserialize(deserializer)?;
235        // remove rattler build version
236        let rattler_build_version = map.remove(&Tool::RattlerBuild).unwrap_or_else(|| {
237            tracing::warn!(
238                "No rattler build version found in encoded system tool configuration. Using current version {}",
239                env!("CARGO_PKG_VERSION"));
240            env!("CARGO_PKG_VERSION").to_string()
241        });
242
243        Ok(SystemTools::from_previous_run(rattler_build_version, map))
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
253    fn test_system_tool() {
254        let system_tool = SystemTools::new();
255        let mut cmd = system_tool.call(Tool::Patchelf).unwrap();
256        let stdout = cmd.arg("--version").output().unwrap().stdout;
257        let version = String::from_utf8_lossy(&stdout).trim().to_string();
258
259        let found_tools = system_tool.found_tools.lock().unwrap();
260        assert!(found_tools.contains_key(&Tool::Patchelf));
261
262        let used_tools = system_tool.used_tools.lock().unwrap();
263        assert!(used_tools.contains_key(&Tool::Patchelf));
264
265        assert!(used_tools.get(&Tool::Patchelf).unwrap() == &version);
266    }
267
268    #[test]
269    fn test_serialize() {
270        // fix versions in used tools to test deserialization
271        let mut used_tools = HashMap::new();
272        used_tools.insert(Tool::Patchelf, "1.0.0".to_string());
273        used_tools.insert(Tool::InstallNameTool, "2.0.0".to_string());
274        used_tools.insert(Tool::Git, "3.0.0".to_string());
275
276        let system_tool = SystemTools {
277            rattler_build_version: "0.0.0".to_string(),
278            used_tools: Arc::new(Mutex::new(used_tools)),
279            found_tools: Arc::new(Mutex::new(HashMap::new())),
280            build_prefix: None,
281        };
282
283        let json = serde_json::to_string_pretty(&system_tool).unwrap();
284        insta::assert_snapshot!(json);
285
286        let deserialized: SystemTools = serde_json::from_str(&json).unwrap();
287        assert!(deserialized
288            .used_tools
289            .lock()
290            .unwrap()
291            .contains_key(&Tool::Patchelf));
292    }
293}