bevy_mod_debugdump/
cli.rs

1use std::{fs::File, path::PathBuf};
2
3use bevy_app::App;
4use bevy_ecs::{
5    intern::Interned,
6    message::MessageWriter,
7    schedule::{ScheduleLabel, Schedules},
8};
9use bevy_log::{error, info};
10use std::io::Write;
11
12#[cfg(feature = "render_graph")]
13use crate::{render_graph, render_graph_dot};
14use crate::{schedule_graph, schedule_graph_dot};
15
16/// Check the command line for arguments relevant to this crate.
17///
18/// ## Dump the render graph
19///
20/// Use `dump-render <file.dot>` to dump the render graph.
21///
22/// ## Dump the schedule graph
23///
24/// Use `dump-update-schedule <file.dot>` to dump the `Update` schedule graph.
25///
26/// ## Exit the app
27///
28/// By default the app will exit after performing the dump. If you want to keep
29/// the app running, use `--no-exit`.
30///
31/// # Usage
32///
33/// Set up your app as usual. No log disabling required. Add the
34/// `bevy_mod_debugdump::CommandLineArgs` plugin at the end. And run your app.
35///
36/// ```rust,no_run
37/// use bevy::prelude::*;
38///
39/// fn main() {
40///     App::new()
41///         .add_plugins(DefaultPlugins)
42///         // Include all other setup as normal.
43///         .add_plugins(bevy_mod_debugdump::CommandLineArgs)
44///         .run();
45/// }
46/// ```
47///
48pub struct CommandLineArgs;
49
50impl bevy_app::Plugin for CommandLineArgs {
51    fn build(&self, _app: &mut App) {}
52
53    fn finish(&self, app: &mut App) {
54        let exit = match execute_cli(app) {
55            Ok(args) => args.exit,
56            Err(e) => {
57                error!("{e:?}");
58                true
59            }
60        };
61
62        if exit {
63            // TODO: It would be nice if we could exit before the window
64            // opens, but I don't see how.
65            app.add_systems(
66                bevy_app::First,
67                |mut app_exit_events: MessageWriter<bevy_app::AppExit>| {
68                    app_exit_events.write(bevy_app::AppExit::Success);
69                },
70            );
71        }
72    }
73}
74
75struct Args {
76    command: ArgsCommand,
77    exit: bool,
78    /// The path to write the graph dot to. If unset, write to stdout.
79    out_path: Option<PathBuf>,
80}
81
82/// A command to execute from the CLI.
83enum ArgsCommand {
84    None,
85    /// Dumps the render graph to the specified file path.
86    DumpRender,
87    /// Dumps the schedule graph.
88    DumpSchedule {
89        /// The schedule to dump.
90        schedule: String,
91    },
92}
93
94fn parse_args() -> Result<Args, lexopt::Error> {
95    use lexopt::prelude::*;
96
97    let mut command = ArgsCommand::None;
98    let mut exit = true;
99    let mut out_path = None;
100
101    let mut parser = lexopt::Parser::from_env();
102    while let Some(arg) = parser.next()? {
103        match &arg {
104            Value(value) => {
105                if !matches!(command, ArgsCommand::None) {
106                    return Err(arg.unexpected());
107                }
108
109                if value == "dump-schedule" {
110                    let schedule = parser.value()?.parse()?;
111                    command = ArgsCommand::DumpSchedule { schedule };
112                } else if value == "dump-render" {
113                    command = ArgsCommand::DumpRender;
114                } else {
115                    return Err(arg.unexpected());
116                }
117            }
118            Short('o') | Long("output") => out_path = Some(parser.value()?.parse()?),
119            Long("no-exit") => exit = false,
120            Long("help") => {
121                info!(
122                    "Usage:\n\
123                    dump-schedule <schedule_name> \n\
124                    dump-render \n\n\
125                      -o, --output  Write output to file instead of printing to stdout\n\
126                      --no-exit     Do not exit after performing debugdump actions"
127                );
128                std::process::exit(0);
129            }
130            _ => return Err(arg.unexpected()),
131        }
132    }
133
134    Ok(Args {
135        command,
136        exit,
137        out_path,
138    })
139}
140
141type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
142
143fn execute_cli(app: &mut App) -> Result<Args> {
144    let mut args = parse_args()?;
145
146    let write = |out: &str| -> Result<()> {
147        match &args.out_path {
148            None => {
149                println!("{out}");
150                Ok(())
151            }
152            Some(path) => {
153                let mut out_file = File::create(path)?;
154                write!(out_file, "{out}")?;
155                Ok(())
156            }
157        }
158    };
159
160    match &args.command {
161        ArgsCommand::None => {
162            // Don't exit unless we do something here.
163            args.exit = false;
164            Ok(args)
165        }
166        #[cfg(feature = "render_graph")]
167        ArgsCommand::DumpRender => {
168            let settings = render_graph::Settings::default();
169            write(&render_graph_dot(app, &settings))?;
170
171            Ok(args)
172        }
173        #[cfg(not(feature = "render_graph"))]
174        ArgsCommand::DumpRender => Err(
175            "cannot dump renderer, consider enabling the feature `bevy_mod_debugdump/render_graph"
176                .into(),
177        ),
178        ArgsCommand::DumpSchedule { schedule } => {
179            let schedule = find_schedule(app, schedule)?;
180
181            let settings = schedule_graph::Settings::default();
182            write(&schedule_graph_dot(app, schedule, &settings))?;
183
184            Ok(args)
185        }
186    }
187}
188
189enum FindScheduleError {
190    /// There was no match. Holds the requested schedule, and the list of valid
191    /// schedules by string.
192    NoMatch(String, Vec<String>),
193    MoreThanOneMatch(String),
194}
195
196impl std::fmt::Debug for FindScheduleError {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        match self {
199            Self::NoMatch(request, schedules) => {
200                f.write_fmt(format_args!("No schedules matched the requested schedule '{request}'. The valid schedules are:\n"))?;
201                for schedule in schedules {
202                    f.write_fmt(format_args!("\n{schedule}"))?;
203                }
204                Ok(())
205            }
206            Self::MoreThanOneMatch(request) => f.write_fmt(format_args!(
207                "More than one schedule matched requested schedule '{request}'"
208            )),
209        }
210    }
211}
212
213impl std::fmt::Display for FindScheduleError {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        <Self as std::fmt::Debug>::fmt(self, f)
216    }
217}
218
219impl std::error::Error for FindScheduleError {}
220
221/// Looks up a schedule by its string name in `App`.
222fn find_schedule(
223    app: &App,
224    schedule_name: &str,
225) -> Result<Interned<dyn ScheduleLabel>, FindScheduleError> {
226    let lower_schedule_name = schedule_name.to_lowercase();
227
228    let schedules = app.world().resource::<Schedules>();
229    let schedules = schedules
230        .iter()
231        // Note we get the Interned label from `schedule` since `&dyn ScheduleLabel` doesn't `impl
232        // ScheduleLabel`.
233        .map(|(label, schedule)| (format!("{label:?}").to_lowercase(), schedule.label()))
234        .collect::<Vec<_>>();
235
236    let mut found_label = None;
237    for (str, label) in schedules.iter() {
238        if str == &lower_schedule_name {
239            if found_label.is_some() {
240                return Err(FindScheduleError::MoreThanOneMatch(
241                    schedule_name.to_string(),
242                ));
243            }
244            found_label = Some(*label);
245        }
246    }
247
248    if let Some(label) = found_label {
249        Ok(label)
250    } else {
251        Err(FindScheduleError::NoMatch(
252            schedule_name.to_string(),
253            schedules.into_iter().map(|(str, _)| str).collect(),
254        ))
255    }
256}