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
56/// Same as `run_main` but takes args, `is_tty`, and streams (for tests and callers that supply I/O).
57///
58/// # Errors
59/// Returns an error if usage is wrong or I/O fails critically.
60#[allow(clippy::too_many_lines)] // script vs REPL branches; split would scatter entry-point logic
61pub fn run_main_from_args<R, W1, W2>(
62    args: &[String],
63    is_tty: bool,
64    stdin: &mut R,
65    stdout: &mut W1,
66    stderr: &mut W2,
67) -> Result<(), Box<dyn std::error::Error>>
68where
69    R: std::io::BufRead + std::io::Read,
70    W1: std::io::Write,
71    W2: std::io::Write,
72{
73    let positionals: Vec<&str> = args
74        .iter()
75        .skip(1)
76        .filter(|a| *a != "-e" && *a != "-f")
77        .map(String::as_str)
78        .collect();
79    let set_e = args.iter().skip(1).any(|a| a == "-e");
80    let run_script = args.iter().skip(1).any(|a| a == "-f");
81
82    if run_script {
83        if positionals.len() != 1 {
84            writeln!(stderr, "usage: dev_shell [-e] -f script.dsh")?;
85            return Err(Box::new(std::io::Error::other("usage")));
86        }
87        let script_path = positionals[0];
88        let script_src = match host_text::read_host_text(Path::new(script_path)) {
89            Ok(s) => s,
90            Err(e) => {
91                writeln!(stderr, "dev_shell: {script_path}: {e}")?;
92                return Err(e.into());
93            }
94        };
95        let bin_path = Path::new(".dev_shell.bin");
96        #[cfg(unix)]
97        vm::export_devshell_workspace_root_env();
98        let vm_session = if cfg!(unix) {
99            vm::try_session_rc_or_host(stderr)
100        } else {
101            match vm::try_session_rc(stderr) {
102                Ok(s) => s,
103                Err(()) => return Err(Box::new(std::io::Error::other("vm session"))),
104            }
105        };
106        let vfs: Rc<RefCell<Vfs>> =
107            if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
108                let root = vm::vm_workspace_host_root();
109                std::fs::create_dir_all(&root)?;
110                Rc::new(RefCell::new(Vfs::new_host_root(root)?))
111            } else {
112                let v = match serialization::load_from_file(bin_path) {
113                    Ok(v) => v,
114                    Err(e) => {
115                        if e.kind() != io::ErrorKind::NotFound {
116                            let _ =
117                                writeln!(stderr, "Failed to load {}: {}", bin_path.display(), e);
118                        }
119                        Vfs::new()
120                    }
121                };
122                Rc::new(RefCell::new(v))
123            };
124        if vm_session.borrow().is_guest_primary() {
125            if let Err(e) =
126                session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), bin_path)
127            {
128                let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
129            }
130        }
131        script::run_script(
132            &vfs,
133            &vm_session,
134            &script_src,
135            bin_path,
136            set_e,
137            stdin,
138            stdout,
139            stderr,
140        )
141        .map_err(|e| Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error>)
142    } else {
143        let path = match positionals.as_slice() {
144            [] => Path::new(".dev_shell.bin"),
145            [p] => Path::new(p),
146            _ => {
147                writeln!(stderr, "usage: dev_shell [options] [path]")?;
148                return Err(Box::new(std::io::Error::other("usage")));
149            }
150        };
151
152        #[cfg(unix)]
153        vm::export_devshell_workspace_root_env();
154
155        let vm_session = if cfg!(unix) {
156            vm::try_session_rc_or_host(stderr)
157        } else {
158            match vm::try_session_rc(stderr) {
159                Ok(s) => s,
160                Err(()) => return Err(Box::new(std::io::Error::other("vm session"))),
161            }
162        };
163
164        #[cfg(all(unix, not(test)))]
165        if vm::should_delegate_lima_shell(&vm_session, is_tty, run_script) {
166            let vfs = Rc::new(RefCell::new(Vfs::new()));
167            vm_session
168                .borrow_mut()
169                .ensure_ready(&vfs.borrow(), vfs.borrow().cwd())
170                .map_err(|e| {
171                    Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error>
172                })?;
173            let err = vm_session.borrow().exec_lima_interactive_shell();
174            return Err(Box::new(err));
175        }
176
177        let vfs: Rc<RefCell<Vfs>> =
178            if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
179                let root = vm::vm_workspace_host_root();
180                std::fs::create_dir_all(&root)?;
181                Rc::new(RefCell::new(Vfs::new_host_root(root)?))
182            } else {
183                let v = match serialization::load_from_file(path) {
184                    Ok(v) => v,
185                    Err(e) => {
186                        if e.kind() == io::ErrorKind::NotFound {
187                            if positionals.len() > 1 {
188                                let _ = writeln!(stderr, "File not found, starting with empty VFS");
189                            }
190                        } else {
191                            let _ = writeln!(stderr, "Failed to load {}: {}", path.display(), e);
192                        }
193                        Vfs::new()
194                    }
195                };
196                Rc::new(RefCell::new(v))
197            };
198        if vm_session.borrow().is_guest_primary() {
199            if let Err(e) = session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), path)
200            {
201                let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
202            }
203        }
204        repl::run(&vfs, &vm_session, is_tty, path, stdin, stdout, stderr).map_err(|()| {
205            Box::new(std::io::Error::other("repl error")) as Box<dyn std::error::Error>
206        })?;
207        Ok(())
208    }
209}
210
211/// Run the devshell with given args and streams (for tests).
212///
213/// # Errors
214/// Returns `RunWithError::Usage` on invalid args; `RunWithError::ReplFailed` if the REPL exits with error.
215pub fn run_with<R, W1, W2>(
216    args: &[String],
217    stdin: &mut R,
218    stdout: &mut W1,
219    stderr: &mut W2,
220) -> Result<(), RunWithError>
221where
222    R: std::io::BufRead + std::io::Read,
223    W1: std::io::Write,
224    W2: std::io::Write,
225{
226    let path = match args {
227        [] | [_] => Path::new(".dev_shell.bin"),
228        [_, path] => Path::new(path),
229        _ => {
230            let _ = writeln!(stderr, "usage: dev_shell [path]");
231            return Err(RunWithError::Usage);
232        }
233    };
234    #[cfg(unix)]
235    vm::export_devshell_workspace_root_env();
236    let vm_session = if cfg!(unix) {
237        vm::try_session_rc_or_host(stderr)
238    } else {
239        vm::try_session_rc(stderr).map_err(|()| RunWithError::ReplFailed)?
240    };
241    let vfs: Rc<RefCell<Vfs>> = if cfg!(unix) && vm_session.borrow().is_host_only() && !cfg!(test) {
242        let root = vm::vm_workspace_host_root();
243        std::fs::create_dir_all(&root).map_err(|_| RunWithError::ReplFailed)?;
244        Rc::new(RefCell::new(
245            Vfs::new_host_root(root).map_err(|_| RunWithError::ReplFailed)?,
246        ))
247    } else {
248        Rc::new(RefCell::new(
249            serialization::load_from_file(path).unwrap_or_default(),
250        ))
251    };
252    if vm_session.borrow().is_guest_primary() {
253        if let Err(e) = session_store::apply_guest_primary_startup(&mut vfs.borrow_mut(), path) {
254            let _ = writeln!(stderr, "dev_shell: guest-primary session: {e}");
255        }
256    }
257    repl::run(&vfs, &vm_session, false, path, stdin, stdout, stderr)
258        .map_err(|()| RunWithError::ReplFailed)
259}
260
261#[cfg(test)]
262mod tests;