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
6use std::path::{Path, PathBuf};
7
8use super::super::sandbox;
9use super::super::vfs::{Node, Vfs, VfsError};
10
11/// Errors from workspace sync helpers.
12#[derive(Debug)]
13pub enum VmSyncError {
14    Io(std::io::Error),
15    Vfs(VfsError),
16    Sandbox(sandbox::SandboxError),
17}
18
19impl std::fmt::Display for VmSyncError {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            Self::Io(e) => write!(f, "{e}"),
23            Self::Vfs(e) => write!(f, "{e}"),
24            Self::Sandbox(e) => write!(f, "{e}"),
25        }
26    }
27}
28
29impl std::error::Error for VmSyncError {
30    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
31        match self {
32            Self::Io(e) => Some(e),
33            Self::Vfs(e) => Some(e),
34            Self::Sandbox(e) => Some(e),
35        }
36    }
37}
38
39fn vfs_child_vfs_path(parent: &str, name: &str) -> String {
40    let p = parent.trim_end_matches('/');
41    if p.is_empty() || p == "/" {
42        format!("/{name}")
43    } else {
44        format!("{p}/{name}")
45    }
46}
47
48fn walk_vfs_files_recurse(
49    vfs: &Vfs,
50    dir_vfs: &str,
51    rel: &Path,
52    out: &mut Vec<(PathBuf, Vec<u8>)>,
53) -> Result<(), VfsError> {
54    for name in vfs.list_dir(dir_vfs)? {
55        let child = vfs_child_vfs_path(dir_vfs, &name);
56        let n = vfs.resolve_absolute(&child)?;
57        match n {
58            Node::File { content, .. } => {
59                out.push((rel.join(&name), content));
60            }
61            Node::Dir { .. } => {
62                let next_rel = rel.join(&name);
63                walk_vfs_files_recurse(vfs, &child, &next_rel, out)?;
64            }
65        }
66    }
67    Ok(())
68}
69
70/// Remove the exported leaf directory (if present), then copy the full VFS subtree at `vfs_path`
71/// into `workspace_parent`, and restore ELF execute bits under `target/`.
72///
73/// # Errors
74/// Returns [`VmSyncError`] on I/O or VFS failure.
75pub fn push_full(vfs: &Vfs, vfs_path: &str, workspace_parent: &Path) -> Result<(), VmSyncError> {
76    std::fs::create_dir_all(workspace_parent).map_err(VmSyncError::Io)?;
77    let host_leaf = sandbox::host_export_root(workspace_parent, vfs_path);
78    if host_leaf.exists() {
79        std::fs::remove_dir_all(&host_leaf).map_err(VmSyncError::Io)?;
80    }
81    vfs.copy_tree_to_host(vfs_path, workspace_parent)
82        .map_err(VmSyncError::Vfs)?;
83    let work_dir = sandbox::host_export_root(workspace_parent, vfs_path);
84    sandbox::restore_execute_bits_for_build_artifacts(&work_dir).map_err(VmSyncError::Sandbox)?;
85    Ok(())
86}
87
88/// For each file under the VFS subtree at `vfs_path`, write to the host workspace only when missing or content differs.
89///
90/// # Errors
91/// Returns [`VmSyncError`] on failure.
92pub fn push_incremental(
93    vfs: &Vfs,
94    vfs_path: &str,
95    workspace_parent: &Path,
96) -> Result<(), VmSyncError> {
97    let abs = vfs.resolve_to_absolute(vfs_path);
98    let host_root = sandbox::host_export_root(workspace_parent, vfs_path);
99    std::fs::create_dir_all(&host_root).map_err(VmSyncError::Io)?;
100
101    let mut files: Vec<(PathBuf, Vec<u8>)> = Vec::new();
102    let root_rel = PathBuf::new();
103    walk_vfs_files_recurse(vfs, &abs, &root_rel, &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.
127///
128/// # Errors
129/// Returns [`sandbox::SandboxError`] when host reads or VFS writes fail.
130pub fn pull_workspace_to_vfs(
131    workspace_parent: &Path,
132    vfs_path: &str,
133    vfs: &mut Vfs,
134) -> Result<(), sandbox::SandboxError> {
135    sandbox::sync_host_dir_to_vfs(workspace_parent, vfs_path, vfs)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn push_incremental_writes_only_changed_file() {
144        let mut vfs = Vfs::new();
145        vfs.mkdir("/p").unwrap();
146        vfs.write_file("/p/a.txt", b"one").unwrap();
147        vfs.write_file("/p/b.txt", b"fix").unwrap();
148
149        let base = std::env::temp_dir().join(format!(
150            "devshell_vm_sync_{}_{}",
151            std::process::id(),
152            std::time::SystemTime::now()
153                .duration_since(std::time::UNIX_EPOCH)
154                .unwrap()
155                .as_nanos()
156        ));
157        let _ = std::fs::remove_dir_all(&base);
158        push_full(&vfs, "/p", &base).unwrap();
159
160        let p_host = base.join("p");
161        assert_eq!(
162            std::fs::read_to_string(p_host.join("a.txt")).unwrap(),
163            "one"
164        );
165        assert_eq!(
166            std::fs::read_to_string(p_host.join("b.txt")).unwrap(),
167            "fix"
168        );
169
170        vfs.write_file("/p/a.txt", b"two").unwrap();
171        push_incremental(&vfs, "/p", &base).unwrap();
172        assert_eq!(
173            std::fs::read_to_string(p_host.join("a.txt")).unwrap(),
174            "two"
175        );
176        assert_eq!(
177            std::fs::read_to_string(p_host.join("b.txt")).unwrap(),
178            "fix"
179        );
180
181        let mut vfs2 = Vfs::new();
182        vfs2.mkdir("/p").unwrap();
183        pull_workspace_to_vfs(&base, "/p", &mut vfs2).unwrap();
184        assert_eq!(vfs2.read_file("/p/a.txt").unwrap(), b"two");
185        assert_eq!(vfs2.read_file("/p/b.txt").unwrap(), b"fix");
186
187        let _ = std::fs::remove_dir_all(&base);
188    }
189}