Skip to main content

xtask_todo_lib/devshell/vm/
sync.rs

1//! Push/pull between VFS and a persistent host workspace directory (session VM staging).
2//!
3//! Layout matches [`super::super::sandbox::export_vfs_to_temp_dir`]: `workspace_parent` holds the
4//! leaf directory named after the last segment of `vfs_path` (same leaf naming as the sandbox export helpers).
5
6#![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/// Errors from workspace sync helpers.
14#[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
71/// Remove the exported leaf directory (if present), then copy the full VFS subtree at `vfs_path`
72/// into `workspace_parent`, and restore ELF execute bits under `target/`.
73///
74/// # Errors
75/// Returns [`VmSyncError`] on I/O or VFS failure.
76pub 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
89/// For each file under the VFS subtree at `vfs_path`, write to the host workspace only when missing or content differs.
90///
91/// # Errors
92/// Returns [`VmSyncError`] on failure.
93pub 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
124/// Merge host workspace tree into the VFS at `vfs_path` (add/update only; same semantics as [`sandbox::sync_host_dir_to_vfs`]).
125///
126/// Used for both “full” and “incremental” pull until delete-on-host is specified.
127pub 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}