Skip to main content

xtask_todo_lib/devshell/
mod.rs

1//! Devshell REPL and VFS: same logic as the `cargo-devshell` binary, exposed so tests can cover it.
2
3pub mod command;
4pub mod completion;
5pub mod host_text;
6pub mod parser;
7pub mod sandbox;
8pub mod script;
9pub mod serialization;
10pub mod session_store;
11pub mod todo_io;
12pub mod vfs;
13pub mod vm;
14pub mod workspace;
15
16mod repl;
17
18use std::cell::RefCell;
19use std::io::{self, BufReader, IsTerminal};
20use std::path::Path;
21use std::rc::Rc;
22
23use vfs::Vfs;
24
25/// Error from `run_with` (usage or REPL failure).
26#[derive(Debug)]
27pub enum RunWithError {
28    Usage,
29    ReplFailed,
30}
31
32impl std::fmt::Display for RunWithError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::Usage => f.write_str("usage error"),
36            Self::ReplFailed => f.write_str("repl failed"),
37        }
38    }
39}
40
41impl std::error::Error for RunWithError {}
42
43/// Run the devshell using process args and standard I/O (for the binary).
44///
45/// # Errors
46/// Returns an error if usage is wrong or I/O fails critically.
47pub fn run_main() -> Result<(), Box<dyn std::error::Error>> {
48    let args: Vec<String> = std::env::args().collect();
49    let is_tty = io::stdin().is_terminal();
50    let mut stdin = BufReader::new(io::stdin());
51    let mut stdout = io::stdout();
52    let mut stderr = io::stderr();
53    run_main_from_args(&args, is_tty, &mut stdin, &mut stdout, &mut stderr)
54}
55
56fn vm_session_for_entry<W: std::io::Write>(
57    stderr: &mut W,
58) -> Result<Rc<RefCell<vm::SessionHolder>>, Box<dyn std::error::Error>> {
59    if cfg!(unix) {
60        Ok(vm::try_session_rc_or_host(stderr))
61    } else {
62        vm::try_session_rc(stderr).map_err(|_| {
63            Box::new(std::io::Error::other("vm session")) as Box<dyn std::error::Error>
64        })
65    }
66}
67
68fn run_script_mode<R, W1, W2>(
69    positionals: &[&str],
70    set_e: bool,
71    stdin: &mut R,
72    stdout: &mut W1,
73    stderr: &mut W2,
74) -> Result<(), Box<dyn std::error::Error>>
75where
76    R: std::io::BufRead + std::io::Read,
77    W1: std::io::Write,
78    W2: std::io::Write,
79{
80    if positionals.len() != 1 {
81        writeln!(stderr, "usage: dev_shell [-e] -f script.dsh")?;
82        return Err(Box::new(std::io::Error::other("usage")));
83    }
84    let script_path = positionals[0];
85    let script_src = match host_text::read_host_text(Path::new(script_path)) {
86        Ok(s) => s,
87        Err(e) => {
88            writeln!(stderr, "dev_shell: {script_path}: {e}")?;
89            return Err(e.into());
90        }
91    };
92    let bin_path = Path::new(".dev_shell.bin");
93    #[cfg(unix)]
94    vm::export_devshell_workspace_root_env();
95    let vm_session = vm_session_for_entry(stderr)?;
96    let vfs: Rc<RefCell<Vfs>> = if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
97        let root = vm::vm_workspace_host_root();
98        std::fs::create_dir_all(&root)?;
99        Rc::new(RefCell::new(Vfs::new_host_root(root)?))
100    } else {
101        let v = match serialization::load_from_file(bin_path) {
102            Ok(v) => v,
103            Err(e) => {
104                if e.kind() != io::ErrorKind::NotFound {
105                    let _ = writeln!(stderr, "Failed to load {}: {}", bin_path.display(), e);
106                }
107                Vfs::new()
108            }
109        };
110        Rc::new(RefCell::new(v))
111    };
112    if vm_session.borrow().is_guest_primary() {
113        if let Err(e) = session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), bin_path)
114        {
115            let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
116        }
117    }
118    script::run_script(&vfs, &vm_session, &script_src, set_e, stdin, stdout, stderr)
119        .map_err(|e| Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error>)
120}
121
122fn run_repl_mode<R, W1, W2>(
123    positionals: &[&str],
124    is_tty: bool,
125    run_script: bool,
126    stdin: &mut R,
127    stdout: &mut W1,
128    stderr: &mut W2,
129) -> Result<(), Box<dyn std::error::Error>>
130where
131    R: std::io::BufRead + std::io::Read,
132    W1: std::io::Write,
133    W2: std::io::Write,
134{
135    #[cfg(any(test, not(unix)))]
136    let _ = run_script;
137
138    let path = match positionals {
139        [] => Path::new(".dev_shell.bin"),
140        [p] => Path::new(p),
141        _ => {
142            writeln!(stderr, "usage: dev_shell [options] [path]")?;
143            return Err(Box::new(std::io::Error::other("usage")));
144        }
145    };
146    #[cfg(unix)]
147    vm::export_devshell_workspace_root_env();
148    let vm_session = vm_session_for_entry(stderr)?;
149
150    #[cfg(all(unix, not(test)))]
151    if vm::should_delegate_lima_shell(&vm_session, is_tty, run_script) {
152        let vfs = Rc::new(RefCell::new(Vfs::new()));
153        vm_session
154            .borrow_mut()
155            .ensure_ready(&vfs.borrow(), vfs.borrow().cwd())
156            .map_err(|e| {
157                Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error>
158            })?;
159        let err = vm_session.borrow().exec_lima_interactive_shell();
160        return Err(Box::new(err));
161    }
162
163    let vfs: Rc<RefCell<Vfs>> = if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
164        let root = vm::vm_workspace_host_root();
165        std::fs::create_dir_all(&root)?;
166        Rc::new(RefCell::new(Vfs::new_host_root(root)?))
167    } else {
168        let v = match serialization::load_from_file(path) {
169            Ok(v) => v,
170            Err(e) => {
171                if e.kind() == io::ErrorKind::NotFound {
172                    if positionals.len() > 1 {
173                        let _ = writeln!(stderr, "File not found, starting with empty VFS");
174                    }
175                } else {
176                    let _ = writeln!(stderr, "Failed to load {}: {}", path.display(), e);
177                }
178                Vfs::new()
179            }
180        };
181        Rc::new(RefCell::new(v))
182    };
183    if vm_session.borrow().is_guest_primary() {
184        if let Err(e) = session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), path) {
185            let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
186        }
187    }
188    repl::run(&vfs, &vm_session, is_tty, path, stdin, stdout, stderr).map_err(|()| {
189        Box::new(std::io::Error::other("repl error")) as Box<dyn std::error::Error>
190    })?;
191    Ok(())
192}
193
194/// Same as `run_main` but takes args, `is_tty`, and streams (for tests and callers that supply I/O).
195///
196/// # Errors
197/// Returns an error if usage is wrong or I/O fails critically.
198pub fn run_main_from_args<R, W1, W2>(
199    args: &[String],
200    is_tty: bool,
201    stdin: &mut R,
202    stdout: &mut W1,
203    stderr: &mut W2,
204) -> Result<(), Box<dyn std::error::Error>>
205where
206    R: std::io::BufRead + std::io::Read,
207    W1: std::io::Write,
208    W2: std::io::Write,
209{
210    let positionals: Vec<&str> = args
211        .iter()
212        .skip(1)
213        .filter(|a| *a != "-e" && *a != "-f")
214        .map(String::as_str)
215        .collect();
216    let set_e = args.iter().skip(1).any(|a| a == "-e");
217    let run_script = args.iter().skip(1).any(|a| a == "-f");
218
219    if run_script {
220        run_script_mode(&positionals, set_e, stdin, stdout, stderr)
221    } else {
222        run_repl_mode(&positionals, is_tty, run_script, stdin, stdout, stderr)
223    }
224}
225
226/// Run the devshell with given args and streams (for tests).
227///
228/// # Errors
229/// Returns `RunWithError::Usage` on invalid args; `RunWithError::ReplFailed` if the REPL exits with error.
230pub fn run_with<R, W1, W2>(
231    args: &[String],
232    stdin: &mut R,
233    stdout: &mut W1,
234    stderr: &mut W2,
235) -> Result<(), RunWithError>
236where
237    R: std::io::BufRead + std::io::Read,
238    W1: std::io::Write,
239    W2: std::io::Write,
240{
241    let path = match args {
242        [] | [_] => Path::new(".dev_shell.bin"),
243        [_, path] => Path::new(path),
244        _ => {
245            let _ = writeln!(stderr, "usage: dev_shell [path]");
246            return Err(RunWithError::Usage);
247        }
248    };
249    #[cfg(unix)]
250    vm::export_devshell_workspace_root_env();
251    let vm_session = if cfg!(unix) {
252        vm::try_session_rc_or_host(stderr)
253    } else {
254        vm::try_session_rc(stderr).map_err(|_| RunWithError::ReplFailed)?
255    };
256    let vfs: Rc<RefCell<Vfs>> = if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
257        let root = vm::vm_workspace_host_root();
258        std::fs::create_dir_all(&root).map_err(|_| RunWithError::ReplFailed)?;
259        Rc::new(RefCell::new(
260            Vfs::new_host_root(root).map_err(|_| RunWithError::ReplFailed)?,
261        ))
262    } else {
263        Rc::new(RefCell::new(
264            serialization::load_from_file(path).unwrap_or_default(),
265        ))
266    };
267    if vm_session.borrow().is_guest_primary() {
268        if let Err(e) = session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), path) {
269            let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
270        }
271    }
272    repl::run(&vfs, &vm_session, false, path, stdin, stdout, stderr)
273        .map_err(|()| RunWithError::ReplFailed)
274}
275
276#[cfg(test)]
277mod tests;