1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum Tool {
26 #[serde(rename = "rattler-build")]
28 RattlerBuild,
29 Patch,
31 Patchelf,
33 Codesign,
35 InstallNameTool,
37 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#[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 pub fn new() -> Self {
82 Self::default()
83 }
84
85 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 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 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 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 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 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 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 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 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}