Skip to main content

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}