Skip to main content

cuenv_workspaces/materializer/
cargo_deps.rs

1//! Materializer for Cargo dependencies.
2
3use super::Materializer;
4use crate::core::types::{LockfileEntry, PackageManager, Workspace};
5use crate::error::{Error, Result};
6use std::path::Path;
7
8#[cfg(unix)]
9use std::os::unix::fs::symlink;
10#[cfg(windows)]
11use std::os::windows::fs::symlink_dir as symlink;
12
13/// Materializer for Cargo projects.
14pub struct CargoMaterializer;
15
16impl Materializer for CargoMaterializer {
17    fn materialize(
18        &self,
19        workspace: &Workspace,
20        _entries: &[LockfileEntry],
21        target_dir: &Path,
22    ) -> Result<()> {
23        if workspace.manager != PackageManager::Cargo {
24            return Ok(());
25        }
26
27        // Symlink the target directory to share build artifacts
28        // This allows reusing incremental compilation results
29        let workspace_target = workspace.root.join("target");
30        let env_target = target_dir.join("target");
31
32        if !workspace_target.exists() {
33            std::fs::create_dir_all(&workspace_target).map_err(|e| Error::Io {
34                source: e,
35                path: Some(workspace_target.clone()),
36                operation: "create workspace target directory".to_string(),
37            })?;
38        }
39
40        if env_target.exists() {
41            // If it exists (e.g. from previous run or created by inputs), remove it
42            // to allow symlinking the shared target directory.
43            if env_target.is_symlink() || env_target.is_file() {
44                std::fs::remove_file(&env_target).map_err(|e| Error::Io {
45                    source: e,
46                    path: Some(env_target.clone()),
47                    operation: "removing existing target symlink/file".to_string(),
48                })?;
49            } else {
50                std::fs::remove_dir_all(&env_target).map_err(|e| Error::Io {
51                    source: e,
52                    path: Some(env_target.clone()),
53                    operation: "removing existing target directory".to_string(),
54                })?;
55            }
56        }
57
58        // We assume target_dir is the root of the hermetic environment.
59        // Cargo expects 'target' at the root usually.
60
61        // Note: Symlinking 'target' might cause locking issues if multiple tasks run in parallel
62        // and try to write to the same shared target.
63        // However, Cargo handles concurrent builds relatively well with file locking.
64        // But different tasks might need different profiles or features.
65        // Ideally, we should use CARGO_TARGET_DIR env var instead of symlinking,
66        // but symlinking works if we want it to appear local.
67
68        if let Err(e) = symlink(&workspace_target, &env_target) {
69            return Err(Error::Io {
70                source: e,
71                path: Some(env_target),
72                operation: "symlink target dir".to_string(),
73            });
74        }
75
76        Ok(())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use std::fs;
84    use tempfile::TempDir;
85
86    fn make_workspace(root: &Path, manager: PackageManager) -> Workspace {
87        Workspace::new(root.to_path_buf(), manager)
88    }
89
90    #[test]
91    fn test_cargo_materializer_skips_non_cargo() {
92        let temp_dir = TempDir::new().unwrap();
93        let workspace = make_workspace(temp_dir.path(), PackageManager::Npm);
94        let target_dir = temp_dir.path().join("hermetic");
95        fs::create_dir_all(&target_dir).unwrap();
96
97        let materializer = CargoMaterializer;
98        let result = materializer.materialize(&workspace, &[], &target_dir);
99
100        assert!(result.is_ok());
101        // No symlink should be created
102        assert!(!target_dir.join("target").exists());
103    }
104
105    #[test]
106    fn test_cargo_materializer_creates_target_dir() {
107        let temp_dir = TempDir::new().unwrap();
108        let workspace = make_workspace(temp_dir.path(), PackageManager::Cargo);
109        let target_dir = temp_dir.path().join("hermetic");
110        fs::create_dir_all(&target_dir).unwrap();
111
112        let materializer = CargoMaterializer;
113        let result = materializer.materialize(&workspace, &[], &target_dir);
114
115        assert!(result.is_ok());
116        // Workspace target should be created
117        assert!(temp_dir.path().join("target").exists());
118        // Hermetic env should have a symlink
119        assert!(target_dir.join("target").exists());
120    }
121
122    #[test]
123    fn test_cargo_materializer_replaces_existing_symlink() {
124        let temp_dir = TempDir::new().unwrap();
125        let workspace = make_workspace(temp_dir.path(), PackageManager::Cargo);
126        let target_dir = temp_dir.path().join("hermetic");
127        fs::create_dir_all(&target_dir).unwrap();
128
129        // Create an existing symlink pointing somewhere else
130        let other_dir = temp_dir.path().join("other");
131        fs::create_dir_all(&other_dir).unwrap();
132        let env_target = target_dir.join("target");
133        #[cfg(unix)]
134        std::os::unix::fs::symlink(&other_dir, &env_target).unwrap();
135        #[cfg(windows)]
136        std::os::windows::fs::symlink_dir(&other_dir, &env_target).unwrap();
137
138        let materializer = CargoMaterializer;
139        let result = materializer.materialize(&workspace, &[], &target_dir);
140
141        assert!(result.is_ok());
142        // Symlink should now point to workspace target
143        let workspace_target = temp_dir.path().join("target");
144        assert!(target_dir.join("target").exists());
145        assert!(workspace_target.exists());
146    }
147
148    #[test]
149    fn test_cargo_materializer_replaces_existing_directory() {
150        let temp_dir = TempDir::new().unwrap();
151        let workspace = make_workspace(temp_dir.path(), PackageManager::Cargo);
152        let target_dir = temp_dir.path().join("hermetic");
153        fs::create_dir_all(&target_dir).unwrap();
154
155        // Create an existing directory with some content
156        let env_target = target_dir.join("target");
157        fs::create_dir_all(&env_target).unwrap();
158        fs::write(env_target.join("somefile"), "content").unwrap();
159
160        let materializer = CargoMaterializer;
161        let result = materializer.materialize(&workspace, &[], &target_dir);
162
163        assert!(result.is_ok());
164        // Directory should be replaced with symlink
165        assert!(target_dir.join("target").exists());
166    }
167
168    #[test]
169    fn test_cargo_materializer_with_existing_workspace_target() {
170        let temp_dir = TempDir::new().unwrap();
171        let workspace = make_workspace(temp_dir.path(), PackageManager::Cargo);
172        let target_dir = temp_dir.path().join("hermetic");
173        fs::create_dir_all(&target_dir).unwrap();
174
175        // Pre-create workspace target with content
176        let workspace_target = temp_dir.path().join("target");
177        fs::create_dir_all(&workspace_target).unwrap();
178        fs::write(workspace_target.join("marker"), "exists").unwrap();
179
180        let materializer = CargoMaterializer;
181        let result = materializer.materialize(&workspace, &[], &target_dir);
182
183        assert!(result.is_ok());
184        // Symlink should point to existing workspace target
185        assert!(target_dir.join("target").exists());
186        // Original content should be preserved
187        assert!(workspace_target.join("marker").exists());
188    }
189}