1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
use std::fs;
use std::io::Read;
use std::process::Stdio;
use std::sync::OnceLock;
use cargo_metadata::Message;
use clap::{Args, Parser, Subcommand};
use crate::{build_3dsx, build_smdh, cargo, get_metadata, link, print_command, CTRConfig};
#[derive(Parser, Debug)]
#[command(name = "cargo", bin_name = "cargo")]
pub enum Cargo {
#[command(name = "3ds")]
Input(Input),
}
#[derive(Args, Debug)]
#[command(version, about)]
pub struct Input {
#[command(subcommand)]
pub cmd: CargoCmd,
/// Print the exact commands `cargo-3ds` is running. Note that this does not
/// set the verbose flag for cargo itself. To set cargo's verbosity flag, add
/// `-- -v` to the end of the command line.
#[arg(long, short = 'v', global = true)]
pub verbose: bool,
/// Set cargo configuration on the command line. This is equivalent to
/// cargo's `--config` option.
#[arg(long, global = true)]
pub config: Vec<String>,
}
/// Run a cargo command. COMMAND will be forwarded to the real
/// `cargo` with the appropriate arguments for the 3DS target.
///
/// If an unrecognized COMMAND is used, it will be passed through unmodified
/// to `cargo` with the appropriate flags set for the 3DS target.
#[derive(Subcommand, Debug)]
#[command(allow_external_subcommands = true)]
pub enum CargoCmd {
/// Builds an executable suitable to run on a 3DS (3dsx).
Build(Build),
/// Builds an executable and sends it to a device with `3dslink`.
Run(Run),
/// Builds a test executable and sends it to a device with `3dslink`.
///
/// This can be used with `--test` for integration tests, or `--lib` for
/// unit tests (which require a custom test runner).
Test(Test),
/// Sets up a new cargo project suitable to run on a 3DS.
New(New),
// NOTE: it seems docstring + name for external subcommands are not rendered
// in help, but we might as well set them here in case a future version of clap
// does include them in help text.
/// Run any other `cargo` command with custom building tailored for the 3DS.
#[command(external_subcommand, name = "COMMAND")]
Passthrough(Vec<String>),
}
#[derive(Args, Debug)]
pub struct RemainingArgs {
/// Pass additional options through to the `cargo` command.
///
/// All arguments after the first `--`, or starting with the first unrecognized
/// option, will be passed through to `cargo` unmodified.
///
/// To pass arguments to an executable being run, a *second* `--` must be
/// used to disambiguate cargo arguments from executable arguments.
/// For example, `cargo 3ds run -- -- xyz` runs an executable with the argument
/// `xyz`.
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
value_name = "CARGO_ARGS"
)]
args: Vec<String>,
}
#[derive(Args, Debug)]
pub struct Build {
#[arg(from_global)]
pub verbose: bool,
// Passthrough cargo options.
#[command(flatten)]
pub passthrough: RemainingArgs,
}
#[derive(Args, Debug)]
pub struct Run {
/// Specify the IP address of the device to send the executable to.
///
/// Corresponds to 3dslink's `--address` arg, which defaults to automatically
/// finding the device.
#[arg(long, short = 'a')]
pub address: Option<std::net::Ipv4Addr>,
/// Set the 0th argument of the executable when running it. Corresponds to
/// 3dslink's `--argv0` argument.
#[arg(long, short = '0')]
pub argv0: Option<String>,
/// Start the 3dslink server after sending the executable. Corresponds to
/// 3dslink's `--server` argument.
#[arg(long, short = 's', default_value_t = false)]
pub server: bool,
/// Set the number of tries when connecting to the device to send the executable.
/// Corresponds to 3dslink's `--retries` argument.
// Can't use `short = 'r'` because that would conflict with cargo's `--release/-r`
#[arg(long)]
pub retries: Option<usize>,
// Passthrough `cargo build` options.
#[command(flatten)]
pub build_args: Build,
#[arg(from_global)]
config: Vec<String>,
}
#[derive(Args, Debug)]
pub struct Test {
/// If set, the built executable will not be sent to the device to run it.
#[arg(long)]
pub no_run: bool,
/// If set, documentation tests will be built instead of unit tests.
/// This implies `--no-run`, unless Cargo's `target.armv6k-nintendo-3ds.runner`
/// is configured.
#[arg(long)]
pub doc: bool,
// The test command uses a superset of the same arguments as Run.
#[command(flatten)]
pub run_args: Run,
}
#[derive(Args, Debug)]
pub struct New {
/// Path of the new project.
#[arg(required = true)]
pub path: String,
// The test command uses a superset of the same arguments as Run.
#[command(flatten)]
pub cargo_args: RemainingArgs,
}
impl CargoCmd {
/// Returns the additional arguments run by the "official" cargo subcommand.
pub fn cargo_args(&self) -> Vec<String> {
match self {
CargoCmd::Build(build) => build.passthrough.cargo_args(),
CargoCmd::Run(run) => run.build_args.passthrough.cargo_args(),
CargoCmd::Test(test) => test.cargo_args(),
CargoCmd::New(new) => {
// We push the original path in the new command (we captured it in [`New`] to learn about the context)
let mut cargo_args = new.cargo_args.cargo_args();
cargo_args.push(new.path.clone());
cargo_args
}
CargoCmd::Passthrough(other) => other.clone().split_off(1),
}
}
/// Returns the cargo subcommand run by `cargo-3ds` when handling a [`CargoCmd`].
///
/// # Notes
///
/// This is not equivalent to the lowercase name of the [`CargoCmd`] variant.
/// Commands may use different commands under the hood to function (e.g. [`CargoCmd::Run`] uses `build`
/// if no custom runner is configured).
pub fn subcommand_name(&self) -> &str {
match self {
CargoCmd::Build(_) => "build",
CargoCmd::Run(run) => {
if run.use_custom_runner() {
"run"
} else {
"build"
}
}
CargoCmd::Test(_) => "test",
CargoCmd::New(_) => "new",
CargoCmd::Passthrough(cmd) => &cmd[0],
}
}
/// Whether or not this command should compile any code, and thus needs import the custom environment configuration (e.g. target spec).
pub fn should_compile(&self) -> bool {
matches!(
self,
Self::Build(_) | Self::Run(_) | Self::Test(_) | Self::Passthrough(_)
)
}
/// Whether or not this command should build a 3DSX executable file.
pub fn should_build_3dsx(&self) -> bool {
match self {
Self::Build(_) | CargoCmd::Run(_) => true,
&Self::Test(Test { doc, .. }) => {
if doc {
eprintln!("Documentation tests requested, no 3dsx will be built");
false
} else {
true
}
}
_ => false,
}
}
/// Whether or not the resulting executable should be sent to the 3DS with
/// `3dslink`.
pub fn should_link_to_device(&self) -> bool {
match self {
Self::Test(Test { no_run: true, .. }) => false,
Self::Run(run) | Self::Test(Test { run_args: run, .. }) => !run.use_custom_runner(),
_ => false,
}
}
pub const DEFAULT_MESSAGE_FORMAT: &'static str = "json-render-diagnostics";
pub fn extract_message_format(&mut self) -> Result<Option<String>, String> {
let cargo_args = match self {
Self::Build(build) => &mut build.passthrough.args,
Self::Run(run) => &mut run.build_args.passthrough.args,
Self::New(new) => &mut new.cargo_args.args,
Self::Test(test) => &mut test.run_args.build_args.passthrough.args,
Self::Passthrough(args) => args,
};
let format = Self::extract_message_format_from_args(cargo_args)?;
if format.is_some() {
return Ok(format);
}
if let Self::Test(Test { doc: true, .. }) = self {
// We don't care about JSON output for doctests since we're not
// building any 3dsx etc. Just use the default output as it's more
// readable compared to DEFAULT_MESSAGE_FORMAT
Ok(Some(String::from("human")))
} else {
Ok(None)
}
}
fn extract_message_format_from_args(
cargo_args: &mut Vec<String>,
) -> Result<Option<String>, String> {
// Checks for a position within the args where '--message-format' is located
if let Some(pos) = cargo_args
.iter()
.position(|s| s.starts_with("--message-format"))
{
// Remove the arg from list so we don't pass anything twice by accident
let arg = cargo_args.remove(pos);
// Allows for usage of '--message-format=<format>' and also using space separation.
// Check for a '=' delimiter and use the second half of the split as the format,
// otherwise remove next arg which is now at the same position as the original flag.
let format = if let Some((_, format)) = arg.split_once('=') {
format.to_string()
} else {
// Also need to remove the argument to the --message-format option
cargo_args.remove(pos)
};
// Non-json formats are not supported so the executable exits.
if format.starts_with("json") {
Ok(Some(format))
} else {
Err(String::from(
"error: non-JSON `message-format` is not supported",
))
}
} else {
Ok(None)
}
}
/// Runs the custom callback *after* the cargo command, depending on the type of command launched.
///
/// # Examples
///
/// - `cargo 3ds build` and other "build" commands will use their callbacks to build the final `.3dsx` file and link it.
/// - `cargo 3ds new` and other generic commands will use their callbacks to make 3ds-specific changes to the environment.
pub fn run_callback(&self, messages: &[Message]) {
// Process the metadata only for commands that have it/use it
let config = if self.should_build_3dsx() {
eprintln!("Getting metadata");
Some(get_metadata(messages))
} else {
None
};
// Run callback only for commands that use it
match self {
Self::Build(cmd) => cmd.callback(&config),
Self::Run(cmd) => cmd.callback(&config),
Self::Test(cmd) => cmd.callback(&config),
Self::New(cmd) => cmd.callback(),
_ => (),
}
}
}
impl RemainingArgs {
/// Get the args to be passed to `cargo`.
pub fn cargo_args(&self) -> Vec<String> {
self.split_args().0
}
/// Get the args to be passed to the executable itself (not `cargo`).
pub fn exe_args(&self) -> Vec<String> {
self.split_args().1
}
fn split_args(&self) -> (Vec<String>, Vec<String>) {
let mut args = self.args.clone();
if let Some(split) = args.iter().position(|s| s == "--") {
let second_half = args.split_off(split + 1);
// take off the "--" arg we found, we'll add one later if needed
args.pop();
(args, second_half)
} else {
(args, Vec::new())
}
}
}
impl Build {
/// Callback for `cargo 3ds build`.
///
/// This callback handles building the application as a `.3dsx` file.
fn callback(&self, config: &Option<CTRConfig>) {
if let Some(config) = config {
eprintln!("Building smdh: {}", config.path_smdh().display());
build_smdh(config, self.verbose);
eprintln!("Building 3dsx: {}", config.path_3dsx().display());
build_3dsx(config, self.verbose);
}
}
}
impl Run {
/// Get the args to pass to `3dslink` based on these options.
pub fn get_3dslink_args(&self) -> Vec<String> {
let mut args = Vec::new();
if let Some(address) = self.address {
args.extend(["--address".to_string(), address.to_string()]);
}
if let Some(argv0) = &self.argv0 {
args.extend(["--arg0".to_string(), argv0.clone()]);
}
if let Some(retries) = self.retries {
args.extend(["--retries".to_string(), retries.to_string()]);
}
if self.server {
args.push("--server".to_string());
}
let exe_args = self.build_args.passthrough.exe_args();
if !exe_args.is_empty() {
// For some reason 3dslink seems to want 2 instances of `--`, one
// in front of all of the args like this...
args.extend(["--args".to_string(), "--".to_string()]);
let mut escaped = false;
for arg in exe_args.iter().cloned() {
if arg.starts_with('-') && !escaped {
// And one before the first `-` arg that is passed in.
args.extend(["--".to_string(), arg]);
escaped = true;
} else {
args.push(arg);
}
}
}
args
}
/// Callback for `cargo 3ds run`.
///
/// This callback handles launching the application via `3dslink`.
fn callback(&self, config: &Option<CTRConfig>) {
// Run the normal "build" callback
self.build_args.callback(config);
if !self.use_custom_runner() {
if let Some(cfg) = config {
eprintln!("Running 3dslink");
link(cfg, self, self.build_args.verbose);
}
}
}
/// Returns whether the cargo environment has `target.armv6k-nintendo-3ds.runner`
/// configured. This will only be checked once during the lifetime of the program,
/// and takes into account the usual ways Cargo looks for its
/// [configuration](https://doc.rust-lang.org/cargo/reference/config.html):
///
/// - `.cargo/config.toml`
/// - Environment variables
/// - Command-line `--config` overrides
pub fn use_custom_runner(&self) -> bool {
static HAS_RUNNER: OnceLock<bool> = OnceLock::new();
let &custom_runner_configured = HAS_RUNNER.get_or_init(|| {
let mut cmd = cargo(&self.config);
cmd.args([
// https://github.com/rust-lang/cargo/issues/9301
"-Z",
"unstable-options",
"config",
"get",
"target.armv6k-nintendo-3ds.runner",
])
.stdout(Stdio::null())
.stderr(Stdio::null());
if self.build_args.verbose {
print_command(&cmd);
}
// `cargo config get` exits zero if the config exists, or nonzero otherwise
cmd.status().map_or(false, |status| status.success())
});
if self.build_args.verbose {
eprintln!(
"Custom runner is {}configured",
if custom_runner_configured { "" } else { "not " }
);
}
custom_runner_configured
}
}
impl Test {
/// Callback for `cargo 3ds test`.
///
/// This callback handles launching the application via `3dslink`.
fn callback(&self, config: &Option<CTRConfig>) {
if self.no_run {
// If the tests don't have to run, use the "build" callback
self.run_args.build_args.callback(config);
} else {
// If the tests have to run, use the "run" callback
self.run_args.callback(config);
}
}
fn should_run(&self) -> bool {
self.run_args.use_custom_runner() && !self.no_run
}
/// The args to pass to the underlying `cargo test` command.
fn cargo_args(&self) -> Vec<String> {
let mut cargo_args = self.run_args.build_args.passthrough.cargo_args();
// We can't run 3DS executables on the host, but we want to respect
// the user's "runner" configuration if set.
//
// If doctests were requested, `--no-run` will be rejected on the
// command line and must be set with RUSTDOCFLAGS instead:
// https://github.com/rust-lang/rust/issues/87022
if self.doc {
cargo_args.extend([
"--doc".into(),
// https://github.com/rust-lang/cargo/issues/7040
"-Z".into(),
"doctest-xcompile".into(),
]);
} else if !self.should_run() {
cargo_args.push("--no-run".into());
}
cargo_args
}
/// Flags to pass to rustdoc via RUSTDOCFLAGS
pub(crate) fn rustdocflags(&self) -> &'static str {
if self.should_run() {
""
} else {
// We don't support running doctests by default, but cargo doesn't like
// --no-run for doctests, so we have to plumb it in via RUSTDOCFLAGS
" --no-run"
}
}
}
const TOML_CHANGES: &str = r#"ctru-rs = { git = "https://github.com/rust3ds/ctru-rs" }
[package.metadata.cargo-3ds]
romfs_dir = "romfs"
"#;
const CUSTOM_MAIN_RS: &str = r#"use ctru::prelude::*;
fn main() {
let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap();
let _console = Console::new(gfx.top_screen.borrow_mut());
println!("Hello, World!");
println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() {
gfx.wait_for_vblank();
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
}
}
}
"#;
impl New {
/// Callback for `cargo 3ds new`.
///
/// This callback handles the custom environment modifications when creating a new 3DS project.
fn callback(&self) {
// Commmit changes to the project only if is meant to be a binary
if self.cargo_args.args.contains(&"--lib".to_string()) {
return;
}
// Attain a canonicalised path for the new project and it's TOML manifest
let project_path = fs::canonicalize(&self.path).unwrap();
let toml_path = project_path.join("Cargo.toml");
let romfs_path = project_path.join("romfs");
let main_rs_path = project_path.join("src/main.rs");
// Create the "romfs" directory
fs::create_dir(romfs_path).unwrap();
// Read the contents of `Cargo.toml` to a string
let mut buf = String::new();
fs::File::open(&toml_path)
.unwrap()
.read_to_string(&mut buf)
.unwrap();
// Add the custom changes to the TOML
let buf = buf + TOML_CHANGES;
fs::write(&toml_path, buf).unwrap();
// Add the custom changes to the main.rs file
fs::write(main_rs_path, CUSTOM_MAIN_RS).unwrap();
}
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::*;
#[test]
fn verify_app() {
Cargo::command().debug_assert();
}
#[test]
fn extract_format() {
const CASES: &[(&[&str], Option<&str>)] = &[
(&["--foo", "--message-format=json", "bar"], Some("json")),
(&["--foo", "--message-format", "json", "bar"], Some("json")),
(
&[
"--foo",
"--message-format",
"json-render-diagnostics",
"bar",
],
Some("json-render-diagnostics"),
),
(
&["--foo", "--message-format=json-render-diagnostics", "bar"],
Some("json-render-diagnostics"),
),
(&["--foo", "bar"], None),
];
for (args, expected) in CASES {
let mut cmd = CargoCmd::Build(Build {
passthrough: RemainingArgs {
args: args.iter().map(ToString::to_string).collect(),
},
verbose: false,
});
assert_eq!(
cmd.extract_message_format().unwrap(),
expected.map(ToString::to_string)
);
if let CargoCmd::Build(build) = cmd {
assert_eq!(build.passthrough.args, vec!["--foo", "bar"]);
} else {
unreachable!();
}
}
}
#[test]
fn extract_format_err() {
for args in [&["--message-format=foo"][..], &["--message-format", "foo"]] {
let mut cmd = CargoCmd::Build(Build {
passthrough: RemainingArgs {
args: args.iter().map(ToString::to_string).collect(),
},
verbose: false,
});
assert!(cmd.extract_message_format().is_err());
}
}
#[test]
fn split_run_args() {
struct TestParam {
input: &'static [&'static str],
expected_cargo: &'static [&'static str],
expected_exe: &'static [&'static str],
}
for param in [
TestParam {
input: &["--example", "hello-world", "--no-default-features"],
expected_cargo: &["--example", "hello-world", "--no-default-features"],
expected_exe: &[],
},
TestParam {
input: &["--example", "hello-world", "--", "--do-stuff", "foo"],
expected_cargo: &["--example", "hello-world"],
expected_exe: &["--do-stuff", "foo"],
},
TestParam {
input: &["--lib", "--", "foo"],
expected_cargo: &["--lib"],
expected_exe: &["foo"],
},
TestParam {
input: &["foo", "--", "bar"],
expected_cargo: &["foo"],
expected_exe: &["bar"],
},
] {
let input: Vec<&str> = ["cargo", "3ds", "run"]
.iter()
.chain(param.input)
.copied()
.collect();
dbg!(&input);
let Cargo::Input(Input {
cmd: CargoCmd::Run(Run { build_args, .. }),
..
}) = Cargo::try_parse_from(input).unwrap_or_else(|e| panic!("{e}"))
else {
panic!("parsed as something other than `run` subcommand")
};
assert_eq!(build_args.passthrough.cargo_args(), param.expected_cargo);
assert_eq!(build_args.passthrough.exe_args(), param.expected_exe);
}
}
}