xtask_todo_lib/devshell/vm/
sync.rs1#![allow(clippy::pedantic, clippy::nursery)]
7
8use std::path::{Path, PathBuf};
9
10use super::super::sandbox;
11use super::super::vfs::{Node, Vfs, VfsError};
12
13#[derive(Debug)]
15pub enum VmSyncError {
16 Io(std::io::Error),
17 Vfs(VfsError),
18 Sandbox(sandbox::SandboxError),
19}
20
21impl std::fmt::Display for VmSyncError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 Self::Io(e) => write!(f, "{e}"),
25 Self::Vfs(e) => write!(f, "{e}"),
26 Self::Sandbox(e) => write!(f, "{e}"),
27 }
28 }
29}
30
31impl std::error::Error for VmSyncError {
32 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
33 match self {
34 Self::Io(e) => Some(e),
35 Self::Vfs(e) => Some(e),
36 Self::Sandbox(e) => Some(e),
37 }
38 }
39}
40
41fn vfs_child_vfs_path(parent: &str, name: &str) -> String {
42 let p = parent.trim_end_matches('/');
43 if p.is_empty() || p == "/" {
44 format!("/{name}")
45 } else {
46 format!("{p}/{name}")
47 }
48}
49
50fn walk_vfs_files_recurse(
51 vfs: &Vfs,
52 dir_vfs: &str,
53 rel: PathBuf,
54 out: &mut Vec<(PathBuf, Vec<u8>)>,
55) -> Result<(), VfsError> {
56 for name in vfs.list_dir(dir_vfs)? {
57 let child = vfs_child_vfs_path(dir_vfs, &name);
58 let n = vfs.resolve_absolute(&child)?;
59 match n {
60 Node::File { content, .. } => {
61 out.push((rel.join(&name), content));
62 }
63 Node::Dir { .. } => {
64 walk_vfs_files_recurse(vfs, &child, rel.join(&name), out)?;
65 }
66 }
67 }
68 Ok(())
69}
70
71pub fn push_full(vfs: &Vfs, vfs_path: &str, workspace_parent: &Path) -> Result<(), VmSyncError> {
77 std::fs::create_dir_all(workspace_parent).map_err(VmSyncError::Io)?;
78 let host_leaf = sandbox::host_export_root(workspace_parent, vfs_path);
79 if host_leaf.exists() {
80 std::fs::remove_dir_all(&host_leaf).map_err(VmSyncError::Io)?;
81 }
82 vfs.copy_tree_to_host(vfs_path, workspace_parent)
83 .map_err(VmSyncError::Vfs)?;
84 let work_dir = sandbox::host_export_root(workspace_parent, vfs_path);
85 sandbox::restore_execute_bits_for_build_artifacts(&work_dir).map_err(VmSyncError::Sandbox)?;
86 Ok(())
87}
88
89pub fn push_incremental(
94 vfs: &Vfs,
95 vfs_path: &str,
96 workspace_parent: &Path,
97) -> Result<(), VmSyncError> {
98 let abs = vfs.resolve_to_absolute(vfs_path);
99 let host_root = sandbox::host_export_root(workspace_parent, vfs_path);
100 std::fs::create_dir_all(&host_root).map_err(VmSyncError::Io)?;
101
102 let mut files: Vec<(PathBuf, Vec<u8>)> = Vec::new();
103 walk_vfs_files_recurse(vfs, &abs, PathBuf::new(), &mut files).map_err(VmSyncError::Vfs)?;
104
105 for (rel, content) in files {
106 let host_file = host_root.join(&rel);
107 let write_it = match std::fs::read(&host_file) {
108 Ok(existing) => existing != content,
109 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
110 Err(e) => return Err(VmSyncError::Io(e)),
111 };
112 if write_it {
113 if let Some(d) = host_file.parent() {
114 std::fs::create_dir_all(d).map_err(VmSyncError::Io)?;
115 }
116 std::fs::write(&host_file, &content).map_err(VmSyncError::Io)?;
117 }
118 }
119
120 sandbox::restore_execute_bits_for_build_artifacts(&host_root).map_err(VmSyncError::Sandbox)?;
121 Ok(())
122}
123
124pub fn pull_workspace_to_vfs(
128 workspace_parent: &Path,
129 vfs_path: &str,
130 vfs: &mut Vfs,
131) -> Result<(), sandbox::SandboxError> {
132 sandbox::sync_host_dir_to_vfs(workspace_parent, vfs_path, vfs)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn push_incremental_writes_only_changed_file() {
141 let mut vfs = Vfs::new();
142 vfs.mkdir("/p").unwrap();
143 vfs.write_file("/p/a.txt", b"one").unwrap();
144 vfs.write_file("/p/b.txt", b"fix").unwrap();
145
146 let base = std::env::temp_dir().join(format!(
147 "devshell_vm_sync_{}_{}",
148 std::process::id(),
149 std::time::SystemTime::now()
150 .duration_since(std::time::UNIX_EPOCH)
151 .unwrap()
152 .as_nanos()
153 ));
154 let _ = std::fs::remove_dir_all(&base);
155 push_full(&vfs, "/p", &base).unwrap();
156
157 let p_host = base.join("p");
158 assert_eq!(
159 std::fs::read_to_string(p_host.join("a.txt")).unwrap(),
160 "one"
161 );
162 assert_eq!(
163 std::fs::read_to_string(p_host.join("b.txt")).unwrap(),
164 "fix"
165 );
166
167 vfs.write_file("/p/a.txt", b"two").unwrap();
168 push_incremental(&vfs, "/p", &base).unwrap();
169 assert_eq!(
170 std::fs::read_to_string(p_host.join("a.txt")).unwrap(),
171 "two"
172 );
173 assert_eq!(
174 std::fs::read_to_string(p_host.join("b.txt")).unwrap(),
175 "fix"
176 );
177
178 let mut vfs2 = Vfs::new();
179 vfs2.mkdir("/p").unwrap();
180 pull_workspace_to_vfs(&base, "/p", &mut vfs2).unwrap();
181 assert_eq!(vfs2.read_file("/p/a.txt").unwrap(), b"two");
182 assert_eq!(vfs2.read_file("/p/b.txt").unwrap(), b"fix");
183
184 let _ = std::fs::remove_dir_all(&base);
185 }
186}