miden_debug/config.rs
1use std::{
2 borrow::Cow,
3 path::{Path, PathBuf},
4 str::FromStr,
5};
6
7use miden_debug_engine::{LinkLibrary, profiling::ProfilerCliArgs};
8
9use crate::{exec::ExecutionConfig, input::InputFile};
10
11/// Run a compiled Miden package with the Miden VM
12#[derive(Default, Debug, clap::Args)]
13pub struct DebuggerConfig {
14 /// Specify the path to a Miden package artifact to execute.
15 ///
16 /// Miden Assembly packages are emitted by the compiler with a `.masp` extension.
17 ///
18 /// You may use `-` as a file name to read a file from stdin.
19 #[arg(value_name = "FILE")]
20 pub input: Option<InputFile>,
21 /// Specify the path to a file containing program inputs.
22 ///
23 /// Program inputs are stack and advice provider values which the program can
24 /// access during execution. The inputs file is a TOML file which describes
25 /// what the inputs are, or where to source them from.
26 #[arg(long, value_name = "FILE")]
27 pub inputs: Option<ExecutionConfig>,
28 /// Arguments to pass to the program entrypoint.
29 ///
30 /// When the selected entrypoint has a component-model signature, arguments are encoded using
31 /// its canonical ABI. This supports Rust values such as booleans and integers wider than a
32 /// felt. Otherwise each argument is parsed as a raw field element and pushed in order.
33 ///
34 /// NOTE: These arguments will override any stack values provided via --inputs
35 #[arg(last(true), value_name = "ARGV")]
36 pub args: Vec<String>,
37 /// The working directory for the debugger
38 ///
39 /// By default this will be the working directory the debugger is executed from
40 #[arg(long, value_name = "DIR", help_heading = "Execution")]
41 pub working_dir: Option<PathBuf>,
42 /// The path to the root directory of the current Miden toolchain
43 ///
44 /// By default this is assumed to be `$(midenup show home)/toolchains/$(midenup show active-toolchain)
45 #[arg(
46 long,
47 value_name = "DIR",
48 env = "MIDEN_SYSROOT",
49 help_heading = "Linker"
50 )]
51 pub sysroot: Option<PathBuf>,
52 /// Whether, and how, to color terminal output
53 #[arg(
54 long,
55 value_enum,
56 default_value_t = ColorChoice::Auto,
57 default_missing_value = "auto",
58 num_args(0..=1),
59 help_heading = "Output"
60 )]
61 pub color: ColorChoice,
62 /// Specify the function to call as the entrypoint for the program
63 /// in the format `<module_name>::<function>`
64 #[arg(long, help_heading = "Execution")]
65 pub entrypoint: Option<String>,
66 /// Connect to a remote DAP debug server instead of running a local program.
67 ///
68 /// Specify the address of the DAP server (e.g. "127.0.0.1:4711").
69 /// When this flag is set, the debugger connects to an existing remote session.
70 #[cfg(all(feature = "dap", feature = "tui"))]
71 #[arg(long, value_name = "ADDR", help_heading = "Execution")]
72 pub dap_connect: Option<String>,
73 /// Start a DAP debug server for the local program and wait for a client to connect.
74 ///
75 /// Specify the address to listen on (e.g. "127.0.0.1:4711").
76 #[cfg(feature = "dap")]
77 #[arg(long, value_name = "ADDR", help_heading = "Execution")]
78 pub start_debug_adapter: Option<String>,
79 /// Source path prefixes used by the compiler's `-Zremap-path-prefix` option.
80 ///
81 /// When debug info stores trimmed source paths, DAP clients may still send
82 /// absolute editor paths. These prefixes provide an explicit mapping between
83 /// the two forms.
84 #[arg(
85 long = "source-path-prefix",
86 alias = "trim-path-prefix",
87 value_name = "PATH",
88 help_heading = "Debugging"
89 )]
90 pub source_path_prefixes: Vec<PathBuf>,
91 /// Replay a recorded execution snapshot in the TUI debugger.
92 ///
93 /// FILE is a snapshot written during a recorded debug session (e.g.
94 /// `miden-client exec --start-debug-adapter <ADDR> --record <FILE>`). The recorded program,
95 /// inputs, resolved code, and event log are replayed so the same execution can be stepped
96 /// through offline, without the original host.
97 #[cfg(feature = "tui")]
98 #[arg(long, value_name = "FILE", help_heading = "Execution")]
99 pub replay: Option<PathBuf>,
100 /// Specify one or more search paths for link libraries requested via `-l`
101 #[arg(
102 long = "search-path",
103 short = 'L',
104 value_name = "PATH",
105 help_heading = "Linker"
106 )]
107 pub search_path: Vec<PathBuf>,
108 /// Link compiled projects to the specified library NAME.
109 ///
110 /// The optional KIND can be provided to indicate what type of library it is.
111 ///
112 /// NAME must either be an absolute path (with extension when applicable), or
113 /// a library namespace (no extension). The former will be used as the path
114 /// to load the library, without looking for it in the library search paths,
115 /// while the latter will be located in the search path based on its KIND.
116 ///
117 /// See below for valid KINDs:
118 #[arg(
119 long = "link-library",
120 short = 'l',
121 value_name = "[KIND=]NAME",
122 value_delimiter = ',',
123 next_line_help(true),
124 help_heading = "Linker"
125 )]
126 pub link_libraries: Vec<LinkLibrary>,
127 /// Use the REPL (text-mode) debugger instead of the TUI
128 #[arg(long, help_heading = "Output")]
129 pub repl: bool,
130 /// Run a script of debugger commands non-interactively, then exit.
131 ///
132 /// FILE is a list of debugger commands, one per line, using the same syntax
133 /// as the interactive REPL prompt. Blank lines and lines beginning with `#`
134 /// are ignored, so scripts may be commented. This is analogous to
135 /// `gdb -x <file> -batch` or `lldb -s <file>`, and is primarily used to
136 /// drive the debugger from lit/FileCheck tests.
137 #[arg(
138 long = "commands",
139 visible_alias = "source",
140 short = 'x',
141 value_name = "FILE",
142 help_heading = "Execution"
143 )]
144 pub commands: Option<PathBuf>,
145 /// Do not auto-load the project-local `.miden-debug.py` file.
146 #[cfg(feature = "python")]
147 #[arg(long, help_heading = "Scripting")]
148 pub no_user_python_init: bool,
149 /// Profiler configuration.
150 #[command(flatten)]
151 pub profiler_cli_args: ProfilerCliArgs,
152}
153
154/// ColorChoice represents the color preferences of an end user.
155///
156/// The `Default` implementation for this type will select `Auto`, which tries
157/// to do the right thing based on the current environment.
158///
159/// The `FromStr` implementation for this type converts a lowercase kebab-case
160/// string of the variant name to the corresponding variant. Any other string
161/// results in an error.
162#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, clap::ValueEnum)]
163pub enum ColorChoice {
164 /// Try very hard to emit colors. This includes emitting ANSI colors
165 /// on Windows if the console API is unavailable.
166 Always,
167 /// AlwaysAnsi is like Always, except it never tries to use anything other
168 /// than emitting ANSI color codes.
169 AlwaysAnsi,
170 /// Try to use colors, but don't force the issue. If the console isn't
171 /// available on Windows, or if TERM=dumb, or if `NO_COLOR` is defined, for
172 /// example, then don't use colors.
173 #[default]
174 Auto,
175 /// Never emit colors.
176 Never,
177}
178
179#[derive(Debug, thiserror::Error)]
180#[error("invalid color choice: {0}")]
181pub struct ColorChoiceParseError(std::borrow::Cow<'static, str>);
182
183impl FromStr for ColorChoice {
184 type Err = ColorChoiceParseError;
185
186 fn from_str(s: &str) -> Result<Self, Self::Err> {
187 match s.to_lowercase().as_str() {
188 "always" => Ok(ColorChoice::Always),
189 "always-ansi" => Ok(ColorChoice::AlwaysAnsi),
190 "never" => Ok(ColorChoice::Never),
191 "auto" => Ok(ColorChoice::Auto),
192 unknown => Err(ColorChoiceParseError(unknown.to_string().into())),
193 }
194 }
195}
196
197impl ColorChoice {
198 /// Returns true if we should attempt to write colored output.
199 pub fn should_attempt_color(&self) -> bool {
200 match *self {
201 ColorChoice::Always => true,
202 ColorChoice::AlwaysAnsi => true,
203 ColorChoice::Never => false,
204 #[cfg(feature = "std")]
205 ColorChoice::Auto => self.env_allows_color(),
206 #[cfg(not(feature = "std"))]
207 ColorChoice::Auto => false,
208 }
209 }
210
211 #[cfg(not(windows))]
212 pub fn env_allows_color(&self) -> bool {
213 match std::env::var_os("TERM") {
214 // If TERM isn't set, then we are in a weird environment that
215 // probably doesn't support colors.
216 None => return false,
217 Some(k) => {
218 if k == "dumb" {
219 return false;
220 }
221 }
222 }
223 // If TERM != dumb, then the only way we don't allow colors at this
224 // point is if NO_COLOR is set.
225 if std::env::var_os("NO_COLOR").is_some() {
226 return false;
227 }
228 true
229 }
230
231 #[cfg(windows)]
232 pub fn env_allows_color(&self) -> bool {
233 // On Windows, if TERM isn't set, then we shouldn't automatically
234 // assume that colors aren't allowed. This is unlike Unix environments
235 // where TERM is more rigorously set.
236 if let Some(k) = std::env::var_os("TERM")
237 && k == "dumb"
238 {
239 return false;
240 }
241 // If TERM != dumb, then the only way we don't allow colors at this
242 // point is if NO_COLOR is set.
243 if std::env::var_os("NO_COLOR").is_some() {
244 return false;
245 }
246 true
247 }
248
249 /// Returns true if this choice should forcefully use ANSI color codes.
250 ///
251 /// It's possible that ANSI is still the correct choice even if this
252 /// returns false.
253 #[cfg(all(feature = "tui", windows))]
254 pub fn should_ansi(&self) -> bool {
255 match *self {
256 ColorChoice::Always => false,
257 ColorChoice::AlwaysAnsi => true,
258 ColorChoice::Never => false,
259 ColorChoice::Auto => {
260 match std::env::var("TERM") {
261 Err(_) => false,
262 // cygwin doesn't seem to support ANSI escape sequences
263 // and instead has its own variety. However, the Windows
264 // console API may be available.
265 Ok(k) => k != "dumb" && k != "cygwin",
266 }
267 }
268 }
269 }
270
271 /// Returns true if this choice should forcefully use ANSI color codes.
272 ///
273 /// It's possible that ANSI is still the correct choice even if this
274 /// returns false.
275 #[cfg(not(feature = "tui"))]
276 pub fn should_ansi(&self) -> bool {
277 match *self {
278 ColorChoice::Always => false,
279 ColorChoice::AlwaysAnsi => true,
280 ColorChoice::Never => false,
281 ColorChoice::Auto => false,
282 }
283 }
284}
285
286impl DebuggerConfig {
287 pub fn working_dir(&self) -> Cow<'_, Path> {
288 match self.working_dir.as_deref() {
289 Some(path) => Cow::Borrowed(path),
290 None => std::env::current_dir()
291 .map(Cow::Owned)
292 .unwrap_or(Cow::Borrowed(Path::new("./"))),
293 }
294 }
295
296 pub fn toolchain_dir(&self) -> Option<PathBuf> {
297 let sysroot = if let Some(sysroot) = self.sysroot.as_deref() {
298 Cow::Borrowed(sysroot)
299 } else if let Some((midenup_home, midenup_channel)) = midenup_home().zip(midenup_channel())
300 {
301 Cow::Owned(midenup_home.join("toolchains").join(midenup_channel))
302 } else {
303 return None;
304 };
305
306 if sysroot.try_exists().ok().is_some_and(|exists| exists) {
307 Some(sysroot.into_owned())
308 } else {
309 None
310 }
311 }
312}
313
314fn midenup_home() -> Option<PathBuf> {
315 use std::process::Command;
316
317 let mut cmd = Command::new("midenup");
318 let mut output = cmd.args(["show", "home"]).output().ok()?;
319 if !output.status.success() {
320 return None;
321 }
322 let output = String::from_utf8(core::mem::take(&mut output.stdout)).ok()?;
323 let trimmed = output.trim_ascii();
324 if trimmed.is_empty() {
325 return None;
326 }
327 PathBuf::from_str(trimmed).ok()
328}
329
330fn midenup_channel() -> Option<String> {
331 use std::process::Command;
332
333 let mut cmd = Command::new("midenup");
334 let mut output = cmd.args(["show", "active-toolchain"]).output().ok()?;
335 if !output.status.success() {
336 return None;
337 }
338 let output = String::from_utf8(core::mem::take(&mut output.stdout)).ok()?;
339 let trimmed = output.trim_ascii();
340 if trimmed.is_empty() {
341 return None;
342 }
343 if output.len() == trimmed.len() {
344 Some(output)
345 } else {
346 Some(trimmed.to_string())
347 }
348}