1use std::collections::BTreeMap;
2use std::env;
3use std::fs::{symlink_metadata, File};
4use std::io::{BufRead, BufReader};
5use std::path::{Path, PathBuf};
6
7use crate::errors::{TreeError, TreeResult};
8use crate::util::path::{ensure_file_exists, PathExt};
9use regex::Regex;
10use tracing::{debug, instrument};
11use walkdir::WalkDir;
12
13pub mod arena;
14pub mod builder;
15pub mod cli;
16pub mod edit;
17pub mod envrc;
18pub mod errors;
19pub mod tree_traits;
20pub mod util;
21
22pub fn expand_env_vars(path: &str) -> String {
38 let mut result = path.to_string();
39
40 let env_var_pattern = Regex::new(r"\$(\w+)|\$\{(\w+)\}").unwrap();
42
43 let matches: Vec<_> = env_var_pattern.captures_iter(path).collect();
45
46 for cap in matches {
47 let var_name = cap.get(1).or_else(|| cap.get(2)).unwrap().as_str();
49 let var_placeholder = if cap.get(1).is_some() {
50 format!("${}", var_name)
51 } else {
52 format!("${{{}}}", var_name)
53 };
54
55 if let Ok(var_value) = std::env::var(var_name) {
57 result = result.replace(&var_placeholder, &var_value);
58 }
59 }
60
61 result
62}
63
64#[instrument(level = "trace")]
65pub fn get_files(file_path: &Path) -> TreeResult<Vec<PathBuf>> {
66 ensure_file_exists(file_path)?;
67 let (_, files, _) = build_env(file_path)?;
68 Ok(files)
69}
70
71#[instrument(level = "trace")]
72pub fn print_files(file_path: &Path) -> TreeResult<()> {
73 let files = get_files(file_path)?;
74 for f in files {
75 println!("{}", f.display());
76 }
77 Ok(())
78}
79
80#[instrument(level = "trace")]
81pub fn build_env_vars(file_path: &Path) -> TreeResult<String> {
82 ensure_file_exists(file_path)?;
83
84 let mut env_vars = String::new();
85 let (variables, _, _) = build_env(file_path)?;
86
87 for (k, v) in variables {
88 env_vars.push_str(&format!("export {}={}\n", k, v));
89 }
90
91 Ok(env_vars)
92}
93
94#[instrument(level = "trace")]
95pub fn is_dag(dir_path: &Path) -> TreeResult<bool> {
96 let re = Regex::new(r"# rsenv:\s*(.+)").map_err(|e| TreeError::InternalError(e.to_string()))?;
97
98 for entry in WalkDir::new(dir_path) {
100 let entry = entry.map_err(|e| TreeError::PathResolution {
101 path: dir_path.to_path_buf(),
102 reason: e.to_string(),
103 })?;
104
105 if entry.file_type().is_file() {
106 let file = File::open(entry.path()).map_err(TreeError::FileReadError)?;
107 let reader = BufReader::new(file);
108
109 for line in reader.lines() {
110 let line = line.map_err(TreeError::FileReadError)?;
111 if let Some(caps) = re.captures(&line) {
112 let parent_references: Vec<&str> = caps[1].split_whitespace().collect();
113 if parent_references.len() > 1 {
114 return Ok(true);
115 }
116 }
117 }
118 }
119 }
120 Ok(false)
121}
122
123#[instrument(level = "debug")]
136pub fn build_env(file_path: &Path) -> TreeResult<(BTreeMap<String, String>, Vec<PathBuf>, bool)> {
137 warn_if_symlink(file_path)?;
138 let file_path = file_path.to_canonical()?;
139 ensure_file_exists(&file_path)?;
140 debug!("Current file_path: {:?}", file_path);
141
142 let mut variables: BTreeMap<String, String> = BTreeMap::new();
143 let mut files_read: Vec<PathBuf> = Vec::new();
144 let mut is_dag = false;
145
146 let mut to_read_files: Vec<PathBuf> = vec![file_path];
147
148 while let Some(current_file) = to_read_files.pop() {
149 ensure_file_exists(¤t_file)?;
150 if files_read.contains(¤t_file) {
151 continue;
152 }
153
154 files_read.push(current_file.clone());
155
156 let (vars, parents) = extract_env(¤t_file)?;
157 is_dag = is_dag || parents.len() > 1;
158
159 debug!(
160 "vars: {:?}, parents: {:?}, is_dag: {:?}",
161 vars, parents, is_dag
162 );
163
164 for (k, v) in vars {
165 variables.entry(k).or_insert(v); }
167
168 for parent in parents {
169 to_read_files.push(parent);
170 }
171 }
172
173 Ok((variables, files_read, is_dag))
174}
175
176#[instrument(level = "debug")]
211pub fn extract_env(file_path: &Path) -> TreeResult<(BTreeMap<String, String>, Vec<PathBuf>)> {
212 warn_if_symlink(file_path)?;
213 let file_path = file_path.to_canonical()?;
214 debug!("Current file_path: {:?}", file_path);
215
216 let original_dir = env::current_dir()
218 .map_err(|e| TreeError::InternalError(format!("Failed to get current dir: {}", e)))?;
219
220 let parent_dir = file_path
222 .parent()
223 .ok_or_else(|| TreeError::InvalidParent(file_path.clone()))?;
224 env::set_current_dir(parent_dir)
225 .map_err(|e| TreeError::InternalError(format!("Failed to change dir: {}", e)))?;
226
227 debug!(
228 "Current directory: {:?}",
229 env::current_dir().unwrap_or_default()
230 );
231
232 let file = File::open(&file_path).map_err(TreeError::FileReadError)?;
233 let reader = BufReader::new(file);
234
235 let mut variables: BTreeMap<String, String> = BTreeMap::new();
236 let mut parent_paths: Vec<PathBuf> = Vec::new();
237
238 for line in reader.lines() {
239 let line = line.map_err(TreeError::FileReadError)?;
240
241 if line.starts_with("# rsenv:") {
243 let parents: Vec<&str> = line
244 .trim_start_matches("# rsenv:")
245 .split_whitespace()
246 .collect();
247 for parent in parents {
248 if !parent.is_empty() {
249 let expanded_path = expand_env_vars(parent);
251 let parent_path = PathBuf::from(expanded_path)
252 .to_canonical()
253 .map_err(|_| TreeError::InvalidParent(PathBuf::from(parent)))?;
254 parent_paths.push(parent_path);
255 }
256 }
257 debug!("parent_paths: {:?}", parent_paths);
258 }
259 else if line.starts_with("export ") {
261 let parts: Vec<&str> = line.split('=').collect();
262 if parts.len() > 1 {
263 let var_name: Vec<&str> = parts[0].split_whitespace().collect();
264 if var_name.len() > 1 {
265 variables.insert(var_name[1].to_string(), parts[1].to_string());
266 }
267 }
268 }
269 }
270
271 env::set_current_dir(original_dir)
273 .map_err(|e| TreeError::InternalError(format!("Failed to restore dir: {}", e)))?;
274
275 Ok((variables, parent_paths))
276}
277
278#[instrument(level = "trace")]
279fn warn_if_symlink(file_path: &Path) -> TreeResult<()> {
280 let metadata = symlink_metadata(file_path).map_err(TreeError::FileReadError)?;
281 if metadata.file_type().is_symlink() {
282 eprintln!(
283 "Warning: The file {} is a symbolic link.",
284 file_path.display()
285 );
286 }
287 Ok(())
288}
289
290#[instrument(level = "debug")]
295pub fn link(parent: &Path, child: &Path) -> TreeResult<()> {
296 let parent = parent.to_canonical()?;
297 let child = child.to_canonical()?;
298 debug!("parent: {:?} <- child: {:?}", parent, child);
299
300 let mut child_contents = std::fs::read_to_string(&child).map_err(TreeError::FileReadError)?;
301 let mut lines: Vec<_> = child_contents.lines().map(|s| s.to_string()).collect();
302
303 let relative_path =
305 pathdiff::diff_paths(&parent, child.parent().unwrap()).ok_or_else(|| {
306 TreeError::PathResolution {
307 path: parent.clone(),
308 reason: "Failed to compute relative path".to_string(),
309 }
310 })?;
311
312 let mut rsenv_lines = 0;
314 let mut rsenv_index = None;
315 for (i, line) in lines.iter().enumerate() {
316 if line.starts_with("# rsenv:") {
317 rsenv_lines += 1;
318 rsenv_index = Some(i);
319 }
320 }
321
322 match rsenv_lines {
324 0 => {
325 lines.insert(0, format!("# rsenv: {}", relative_path.display()));
327 }
328 1 => {
329 if let Some(index) = rsenv_index {
331 lines[index] = format!("# rsenv: {}", relative_path.display());
332 }
333 }
334 _ => {
335 return Err(TreeError::MultipleParents(child));
337 }
338 }
339
340 child_contents = lines.join("\n");
342 std::fs::write(&child, child_contents).map_err(TreeError::FileReadError)?;
343
344 Ok(())
345}
346
347#[instrument(level = "debug")]
348pub fn unlink(child: &Path) -> TreeResult<()> {
349 let child = child.to_canonical()?;
350 debug!("child: {:?}", child);
351
352 let mut child_contents = std::fs::read_to_string(&child).map_err(TreeError::FileReadError)?;
353 let mut lines: Vec<_> = child_contents.lines().map(|s| s.to_string()).collect();
354
355 let mut rsenv_lines = 0;
357 let mut rsenv_index = None;
358 for (i, line) in lines.iter().enumerate() {
359 if line.starts_with("# rsenv:") {
360 rsenv_lines += 1;
361 rsenv_index = Some(i);
362 }
363 }
364
365 match rsenv_lines {
366 0 => {}
367 1 => {
368 if let Some(index) = rsenv_index {
370 lines[index] = "# rsenv:".to_string();
371 }
372 }
373 _ => {
374 return Err(TreeError::MultipleParents(child));
375 }
376 }
377 child_contents = lines.join("\n");
379 std::fs::write(&child, child_contents).map_err(TreeError::FileReadError)?;
380
381 Ok(())
382}
383
384#[instrument(level = "debug")]
386pub fn link_all(nodes: &[PathBuf]) {
387 debug!("nodes: {:?}", nodes);
388 let mut parent = None;
389 for node in nodes {
390 if let Some(parent_path) = parent {
391 link(parent_path, node).expect("Failed to link");
392 } else {
393 unlink(node).unwrap();
394 }
395 parent = Some(node);
396 }
397}