1use std::fs;
2use std::io::Read;
3use std::process::{self, Stdio};
4use std::sync::OnceLock;
5
6use cargo_metadata::{Message, Metadata};
7use clap::{Args, Parser, Subcommand};
8
9use crate::{CTRConfig, build_3dsx, cargo, get_artifact_config, link, print_command};
10
11#[derive(Parser, Debug)]
12#[command(name = "cargo", bin_name = "cargo")]
13pub enum Cargo {
14 #[command(name = "3ds")]
15 Input(Input),
16}
17
18#[derive(Args, Debug)]
19#[command(version, about)]
20pub struct Input {
21 #[command(subcommand)]
22 pub cmd: CargoCmd,
23
24 #[arg(long, short = 'v', global = true)]
28 pub verbose: bool,
29
30 #[arg(long, global = true)]
33 pub config: Vec<String>,
34}
35
36#[derive(Subcommand, Debug)]
42#[command(allow_external_subcommands = true)]
43pub enum CargoCmd {
44 Build(Build),
46
47 Run(Run),
49
50 Test(Test),
55
56 New(New),
58
59 #[command(external_subcommand, name = "COMMAND")]
64 Passthrough(Vec<String>),
65}
66
67#[derive(Args, Debug)]
68pub struct RemainingArgs {
69 #[arg(
79 trailing_var_arg = true,
80 allow_hyphen_values = true,
81 value_name = "CARGO_ARGS"
82 )]
83 args: Vec<String>,
84}
85
86#[allow(unused_variables)]
87trait Callbacks {
88 fn build_callback(&self, config: &CTRConfig) {}
89 fn run_callback(&self, config: &CTRConfig) {}
90}
91
92#[derive(Args, Debug)]
93pub struct Build {
94 #[arg(from_global)]
95 pub verbose: bool,
96
97 #[command(flatten)]
99 pub passthrough: RemainingArgs,
100}
101
102#[derive(Args, Debug)]
103pub struct Run {
104 #[arg(long, short = 'a')]
109 pub address: Option<std::net::Ipv4Addr>,
110
111 #[arg(long, short = '0')]
114 pub argv0: Option<String>,
115
116 #[arg(long, short = 's', default_value_t = false)]
119 pub server: bool,
120
121 #[arg(long)]
125 pub retries: Option<usize>,
126
127 #[command(flatten)]
129 pub build_args: Build,
130
131 #[arg(from_global)]
132 config: Vec<String>,
133}
134
135#[derive(Args, Debug)]
136pub struct Test {
137 #[arg(long)]
139 pub no_run: bool,
140
141 #[arg(long)]
145 pub doc: bool,
146
147 #[command(flatten)]
149 pub run_args: Run,
150}
151
152#[derive(Args, Debug)]
153pub struct New {
154 #[arg(required = true)]
156 pub path: String,
157
158 #[command(flatten)]
160 pub cargo_args: RemainingArgs,
161}
162
163impl CargoCmd {
164 pub(crate) fn cargo_args(&self) -> Vec<String> {
166 match self {
167 CargoCmd::Build(build) => build.passthrough.cargo_args(),
168 CargoCmd::Run(run) => run.build_args.passthrough.cargo_args(),
169 CargoCmd::Test(test) => test.cargo_args(),
170 CargoCmd::New(new) => {
171 let mut cargo_args = new.cargo_args.cargo_args();
173 cargo_args.push(new.path.clone());
174
175 cargo_args
176 }
177 CargoCmd::Passthrough(other) => other.clone().split_off(1),
178 }
179 }
180
181 pub(crate) fn subcommand_name(&self) -> &str {
189 match self {
190 CargoCmd::Build(_) => "build",
191 CargoCmd::Run(run) => {
192 if run.use_custom_runner() {
193 "run"
194 } else {
195 "build"
196 }
197 }
198 CargoCmd::Test(_) => "test",
199 CargoCmd::New(_) => "new",
200 CargoCmd::Passthrough(cmd) => &cmd[0],
201 }
202 }
203
204 pub(crate) fn should_compile(&self) -> bool {
206 matches!(
207 self,
208 Self::Build(_) | Self::Run(_) | Self::Test(_) | Self::Passthrough(_)
209 )
210 }
211
212 pub fn should_build_3dsx(&self) -> bool {
214 match self {
215 Self::Build(_) | CargoCmd::Run(_) => true,
216 &Self::Test(Test { doc, .. }) => {
217 if doc {
218 eprintln!("Documentation tests requested, no 3dsx will be built");
219 false
220 } else {
221 true
222 }
223 }
224 _ => false,
225 }
226 }
227
228 pub const DEFAULT_MESSAGE_FORMAT: &'static str = "json-render-diagnostics";
229
230 pub fn extract_message_format(&mut self) -> Result<Option<String>, String> {
231 let cargo_args = match self {
232 Self::Build(build) => &mut build.passthrough.args,
233 Self::Run(run) => &mut run.build_args.passthrough.args,
234 Self::New(new) => &mut new.cargo_args.args,
235 Self::Test(test) => &mut test.run_args.build_args.passthrough.args,
236 Self::Passthrough(args) => args,
237 };
238
239 let format = Self::extract_message_format_from_args(cargo_args)?;
240 if format.is_some() {
241 return Ok(format);
242 }
243
244 if let Self::Test(Test { doc: true, .. }) = self {
245 Ok(Some(String::from("human")))
249 } else {
250 Ok(None)
251 }
252 }
253
254 fn extract_message_format_from_args(
255 cargo_args: &mut Vec<String>,
256 ) -> Result<Option<String>, String> {
257 if let Some(pos) = cargo_args
259 .iter()
260 .position(|s| s.starts_with("--message-format"))
261 {
262 let arg = cargo_args.remove(pos);
264
265 let format = if let Some((_, format)) = arg.split_once('=') {
269 format.to_string()
270 } else {
271 cargo_args.remove(pos)
273 };
274
275 if format.starts_with("json") {
277 Ok(Some(format))
278 } else {
279 Err(String::from(
280 "error: non-JSON `message-format` is not supported",
281 ))
282 }
283 } else {
284 Ok(None)
285 }
286 }
287
288 pub fn run_callbacks(&self, messages: &[Message], metadata: Option<&Metadata>) {
295 let configs = metadata
296 .map(|metadata| self.build_callbacks(messages, metadata))
297 .unwrap_or_default();
298
299 let config = match self {
300 _ if configs.len() == 1 => configs.into_iter().next().unwrap(),
302
303 Self::Test(Test { no_run: true, .. }) => return,
305
306 Self::Test(Test { run_args: run, .. }) | Self::Run(run) if run.use_custom_runner() => {
310 return;
311 }
312
313 Self::New(_) => CTRConfig::default(),
315
316 Self::Test(_) | Self::Run(_) => {
318 let paths: Vec<_> = configs.into_iter().map(|c| c.path_3dsx()).collect();
319 let names: Vec<_> = paths.iter().filter_map(|p| p.file_name()).collect();
320 eprintln!(
321 "Error: expected exactly one (1) executable to run, got {}: {names:?}",
322 paths.len(),
323 );
324 process::exit(1);
325 }
326
327 _ => return,
328 };
329
330 self.run_callback(&config);
331 }
332
333 fn build_callbacks(&self, messages: &[Message], metadata: &Metadata) -> Vec<CTRConfig> {
336 let max_artifact_count = metadata.packages.iter().map(|pkg| pkg.targets.len()).sum();
337 let mut configs = Vec::with_capacity(max_artifact_count);
338
339 for message in messages {
340 let Message::CompilerArtifact(artifact) = message else {
341 continue;
342 };
343
344 if artifact.executable.is_none()
345 || !metadata.workspace_members.contains(&artifact.package_id)
346 {
347 continue;
348 }
349
350 let package = &metadata[&artifact.package_id];
351 let config = get_artifact_config(package.clone(), artifact.clone());
352
353 self.build_callback(&config);
354
355 configs.push(config);
356 }
357
358 configs
359 }
360
361 fn inner_callback(&self) -> Option<&dyn Callbacks> {
362 match self {
363 Self::Build(cmd) => Some(cmd),
364 Self::Run(cmd) => Some(cmd),
365 Self::Test(cmd) => Some(cmd),
366 Self::New(cmd) => Some(cmd),
367 _ => None,
368 }
369 }
370}
371
372impl Callbacks for CargoCmd {
373 fn build_callback(&self, config: &CTRConfig) {
374 if let Some(cb) = self.inner_callback() {
375 cb.build_callback(config);
376 }
377 }
378
379 fn run_callback(&self, config: &CTRConfig) {
380 if let Some(cb) = self.inner_callback() {
381 cb.run_callback(config);
382 }
383 }
384}
385
386impl RemainingArgs {
387 pub(crate) fn cargo_args(&self) -> Vec<String> {
389 self.split_args().0
390 }
391
392 pub(crate) fn exe_args(&self) -> Vec<String> {
394 self.split_args().1
395 }
396
397 fn split_args(&self) -> (Vec<String>, Vec<String>) {
398 let mut args = self.args.clone();
399
400 if let Some(split) = args.iter().position(|s| s == "--") {
401 let second_half = args.split_off(split + 1);
402 args.pop();
404
405 (args, second_half)
406 } else {
407 (args, Vec::new())
408 }
409 }
410}
411
412impl Callbacks for Build {
413 fn build_callback(&self, config: &CTRConfig) {
417 eprintln!("Building smdh: {}", config.path_smdh());
418 config.build_smdh(self.verbose);
419
420 eprintln!("Building 3dsx: {}", config.path_3dsx());
421 build_3dsx(config, self.verbose);
422 }
423}
424
425impl Callbacks for Run {
426 fn build_callback(&self, config: &CTRConfig) {
427 self.build_args.build_callback(config);
428 }
429
430 fn run_callback(&self, config: &CTRConfig) {
434 if !self.use_custom_runner() {
435 eprintln!("Running 3dslink");
436 link(config, self, self.build_args.verbose);
437 }
438 }
439}
440
441impl Run {
442 pub(crate) fn get_3dslink_args(&self) -> Vec<String> {
444 let mut args = Vec::new();
445
446 if let Some(address) = self.address {
447 args.extend(["--address".to_string(), address.to_string()]);
448 }
449
450 if let Some(argv0) = &self.argv0 {
451 args.extend(["--arg0".to_string(), argv0.clone()]);
452 }
453
454 if let Some(retries) = self.retries {
455 args.extend(["--retries".to_string(), retries.to_string()]);
456 }
457
458 if self.server {
459 args.push("--server".to_string());
460 }
461
462 let exe_args = self.build_args.passthrough.exe_args();
463 if !exe_args.is_empty() {
464 args.extend(["--args".to_string(), "--".to_string()]);
467
468 let mut escaped = false;
469 for arg in exe_args.iter().cloned() {
470 if arg.starts_with('-') && !escaped {
471 args.extend(["--".to_string(), arg]);
473 escaped = true;
474 } else {
475 args.push(arg);
476 }
477 }
478 }
479
480 args
481 }
482
483 pub(crate) fn use_custom_runner(&self) -> bool {
492 static HAS_RUNNER: OnceLock<bool> = OnceLock::new();
493
494 let &custom_runner_configured = HAS_RUNNER.get_or_init(|| {
495 let mut cmd = cargo(&self.config);
496 cmd.args([
497 "-Z",
499 "unstable-options",
500 "config",
501 "get",
502 "target.armv6k-nintendo-3ds.runner",
503 ])
504 .stdout(Stdio::null())
505 .stderr(Stdio::null());
506
507 if self.build_args.verbose {
508 print_command(&cmd);
509 }
510
511 cmd.status().is_ok_and(|status| status.success())
513 });
514
515 if self.build_args.verbose {
516 eprintln!(
517 "Custom runner is {}configured",
518 if custom_runner_configured { "" } else { "not " }
519 );
520 }
521
522 custom_runner_configured
523 }
524}
525
526impl Callbacks for Test {
527 fn build_callback(&self, config: &CTRConfig) {
528 self.run_args.build_callback(config);
529 }
530
531 fn run_callback(&self, config: &CTRConfig) {
535 if !self.no_run {
536 self.run_args.run_callback(config);
537 }
538 }
539}
540
541impl Test {
542 fn should_run(&self) -> bool {
543 self.run_args.use_custom_runner() && !self.no_run
544 }
545
546 fn cargo_args(&self) -> Vec<String> {
548 let mut cargo_args = self.run_args.build_args.passthrough.cargo_args();
549
550 if self.doc {
558 cargo_args.extend([
559 "--doc".into(),
560 "-Z".into(),
562 "doctest-xcompile".into(),
563 ]);
564 } else if !self.should_run() {
565 cargo_args.push("--no-run".into());
566 }
567
568 cargo_args
569 }
570
571 pub(crate) fn rustdocflags(&self) -> &'static str {
573 if self.should_run() {
574 ""
575 } else {
576 " --no-run"
579 }
580 }
581}
582
583const TOML_CHANGES: &str = r#"ctru-rs = { git = "https://github.com/rust3ds/ctru-rs" }
584
585[package.metadata.cargo-3ds]
586romfs_dir = "romfs"
587"#;
588
589const CUSTOM_MAIN_RS: &str = r#"use ctru::prelude::*;
590
591fn main() {
592 let apt = Apt::new().unwrap();
593 let mut hid = Hid::new().unwrap();
594 let gfx = Gfx::new().unwrap();
595 let _console = Console::new(gfx.top_screen.borrow_mut());
596
597 println!("Hello, World!");
598 println!("\x1b[29;16HPress Start to exit");
599
600 while apt.main_loop() {
601 gfx.wait_for_vblank();
602
603 hid.scan_input();
604 if hid.keys_down().contains(KeyPad::START) {
605 break;
606 }
607 }
608}
609"#;
610
611impl Callbacks for New {
612 fn run_callback(&self, _: &CTRConfig) {
616 if self.cargo_args.args.contains(&"--lib".to_string()) {
618 return;
619 }
620
621 let project_path = fs::canonicalize(&self.path).unwrap();
623 let toml_path = project_path.join("Cargo.toml");
624 let romfs_path = project_path.join("romfs");
625 let main_rs_path = project_path.join("src/main.rs");
626 let dummy_romfs_path = romfs_path.join("PUT_YOUR_ROMFS_FILES_HERE.txt");
627
628 fs::create_dir(romfs_path).unwrap();
630 fs::File::create(dummy_romfs_path).unwrap();
631
632 let mut buf = String::new();
634 fs::File::open(&toml_path)
635 .unwrap()
636 .read_to_string(&mut buf)
637 .unwrap();
638
639 let buf = buf + TOML_CHANGES;
641 fs::write(&toml_path, buf).unwrap();
642
643 fs::write(main_rs_path, CUSTOM_MAIN_RS).unwrap();
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use clap::CommandFactory;
651
652 use super::*;
653
654 #[test]
655 fn verify_app() {
656 Cargo::command().debug_assert();
657 }
658
659 #[test]
660 fn extract_format() {
661 const CASES: &[(&[&str], Option<&str>)] = &[
662 (&["--foo", "--message-format=json", "bar"], Some("json")),
663 (&["--foo", "--message-format", "json", "bar"], Some("json")),
664 (
665 &[
666 "--foo",
667 "--message-format",
668 "json-render-diagnostics",
669 "bar",
670 ],
671 Some("json-render-diagnostics"),
672 ),
673 (
674 &["--foo", "--message-format=json-render-diagnostics", "bar"],
675 Some("json-render-diagnostics"),
676 ),
677 (&["--foo", "bar"], None),
678 ];
679
680 for (args, expected) in CASES {
681 let mut cmd = CargoCmd::Build(Build {
682 passthrough: RemainingArgs {
683 args: args.iter().map(ToString::to_string).collect(),
684 },
685 verbose: false,
686 });
687
688 assert_eq!(
689 cmd.extract_message_format().unwrap(),
690 expected.map(ToString::to_string)
691 );
692
693 if let CargoCmd::Build(build) = cmd {
694 assert_eq!(build.passthrough.args, vec!["--foo", "bar"]);
695 } else {
696 unreachable!();
697 }
698 }
699 }
700
701 #[test]
702 fn extract_format_err() {
703 for args in [&["--message-format=foo"][..], &["--message-format", "foo"]] {
704 let mut cmd = CargoCmd::Build(Build {
705 passthrough: RemainingArgs {
706 args: args.iter().map(ToString::to_string).collect(),
707 },
708 verbose: false,
709 });
710
711 assert!(cmd.extract_message_format().is_err());
712 }
713 }
714
715 #[test]
716 fn split_run_args() {
717 struct TestParam {
718 input: &'static [&'static str],
719 expected_cargo: &'static [&'static str],
720 expected_exe: &'static [&'static str],
721 }
722
723 for param in [
724 TestParam {
725 input: &["--example", "hello-world", "--no-default-features"],
726 expected_cargo: &["--example", "hello-world", "--no-default-features"],
727 expected_exe: &[],
728 },
729 TestParam {
730 input: &["--example", "hello-world", "--", "--do-stuff", "foo"],
731 expected_cargo: &["--example", "hello-world"],
732 expected_exe: &["--do-stuff", "foo"],
733 },
734 TestParam {
735 input: &["--lib", "--", "foo"],
736 expected_cargo: &["--lib"],
737 expected_exe: &["foo"],
738 },
739 TestParam {
740 input: &["foo", "--", "bar"],
741 expected_cargo: &["foo"],
742 expected_exe: &["bar"],
743 },
744 ] {
745 let input: Vec<&str> = ["cargo", "3ds", "run"]
746 .iter()
747 .chain(param.input)
748 .copied()
749 .collect();
750
751 dbg!(&input);
752 let Cargo::Input(Input {
753 cmd: CargoCmd::Run(Run { build_args, .. }),
754 ..
755 }) = Cargo::try_parse_from(input).unwrap_or_else(|e| panic!("{e}"))
756 else {
757 panic!("parsed as something other than `run` subcommand")
758 };
759
760 assert_eq!(build_args.passthrough.cargo_args(), param.expected_cargo);
761 assert_eq!(build_args.passthrough.exe_args(), param.expected_exe);
762 }
763 }
764}