bevy_mod_debugdump/
cli.rs1use 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
16pub 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 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 out_path: Option<PathBuf>,
80}
81
82enum ArgsCommand {
84 None,
85 DumpRender,
87 DumpSchedule {
89 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 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 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
221fn 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 .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}