diff --git a/src/command.rs b/src/command.rs index 0b0804f..68617e4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,10 +1,12 @@ 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, get_metadata, link, CTRConfig}; +use crate::{build_3dsx, build_smdh, find_cargo, get_metadata, link, print_command, CTRConfig}; #[derive(Parser, Debug)] #[command(name = "cargo", bin_name = "cargo")] @@ -20,9 +22,15 @@ pub struct Input { pub cmd: CargoCmd, /// Print the exact commands `cargo-3ds` is running. Note that this does not - /// set the verbose flag for cargo itself. - #[arg(long, short = 'v')] + /// set the verbose flag for cargo itself. To set cargo's verbose 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, } /// Run a cargo command. COMMAND will be forwarded to the real @@ -67,21 +75,25 @@ pub struct RemainingArgs { /// 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)] - #[arg(allow_hyphen_values = true)] - #[arg(global = true)] - #[arg(name = "CARGO_ARGS")] + #[arg( + trailing_var_arg = true, + allow_hyphen_values = true, + value_name = "CARGO_ARGS" + )] args: Vec, } -#[derive(Parser, Debug)] +#[derive(Args, Debug)] pub struct Build { + #[arg(from_global)] + pub verbose: bool, + // Passthrough cargo options. #[command(flatten)] - pub cargo_args: RemainingArgs, + pub passthrough: RemainingArgs, } -#[derive(Parser, Debug)] +#[derive(Args, Debug)] pub struct Run { /// Specify the IP address of the device to send the executable to. /// @@ -109,16 +121,20 @@ pub struct Run { // Passthrough `cargo build` options. #[command(flatten)] pub build_args: Build, + + #[arg(from_global)] + config: Vec, } -#[derive(Parser, Debug)] +#[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`. + /// This implies `--no-run`, unless Cargo's `target.armv6k-nintendo-3ds.runner` + /// is configured. #[arg(long)] pub doc: bool, @@ -127,7 +143,7 @@ pub struct Test { pub run_args: Run, } -#[derive(Parser, Debug)] +#[derive(Args, Debug)] pub struct New { /// Path of the new project. #[arg(required = true)] @@ -142,16 +158,15 @@ impl CargoCmd { /// Returns the additional arguments run by the "official" cargo subcommand. pub fn cargo_args(&self) -> Vec { match self { - CargoCmd::Build(build) => build.cargo_args.cargo_args(), - CargoCmd::Run(run) => run.build_args.cargo_args.cargo_args(), + CargoCmd::Build(build) => build.passthrough.cargo_args(), + CargoCmd::Run(run) => run.build_args.passthrough.cargo_args(), CargoCmd::Test(test) => { - let mut cargo_args = test.run_args.build_args.cargo_args.cargo_args(); + let mut cargo_args = test.run_args.build_args.passthrough.cargo_args(); - // We can't run 3DS executables on the host, so unconditionally pass - // --no-run here and send the executable with 3dslink later, if the - // user wants + // We can't run 3DS executables on the host, but we want to respect + // the user's "runner" configuration if set. if test.doc { - eprintln!("Documentation tests requested, no 3dsx will be built or run"); + eprintln!("Documentation tests requested, no 3dsx will be built"); // https://github.com/rust-lang/cargo/issues/7040 cargo_args.append(&mut vec![ @@ -163,7 +178,7 @@ impl CargoCmd { "-Z".to_string(), "build-std=std,test".to_string(), ]); - } else { + } else if !test.run_args.is_runner_configured() { cargo_args.push("--no-run".to_string()); } @@ -185,10 +200,18 @@ impl 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`). + /// 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(_) | CargoCmd::Run(_) => "build", + CargoCmd::Build(_) => "build", + CargoCmd::Run(run) => { + if run.is_runner_configured() { + "run" + } else { + "build" + } + } CargoCmd::Test(_) => "test", CargoCmd::New(_) => "new", CargoCmd::Passthrough(cmd) => &cmd[0], @@ -215,8 +238,8 @@ impl CargoCmd { /// `3dslink`. pub fn should_link_to_device(&self) -> bool { match self { - Self::Test(test) => !test.no_run, - Self::Run(_) => true, + Self::Test(test) => !(test.no_run || test.run_args.is_runner_configured()), + Self::Run(run) => !run.is_runner_configured(), _ => false, } } @@ -225,10 +248,10 @@ impl CargoCmd { pub fn extract_message_format(&mut self) -> Result, String> { let cargo_args = match self { - Self::Build(build) => &mut build.cargo_args.args, - Self::Run(run) => &mut run.build_args.cargo_args.args, + 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.cargo_args.args, + Self::Test(test) => &mut test.run_args.build_args.passthrough.args, Self::Passthrough(args) => args, }; @@ -287,7 +310,7 @@ impl CargoCmd { /// /// - `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], verbose: bool) { + 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"); @@ -299,9 +322,9 @@ impl CargoCmd { // Run callback only for commands that use it match self { - Self::Build(cmd) => cmd.callback(&config, verbose), - Self::Run(cmd) => cmd.callback(&config, verbose), - Self::Test(cmd) => cmd.callback(&config, verbose), + Self::Build(cmd) => cmd.callback(&config), + Self::Run(cmd) => cmd.callback(&config), + Self::Test(cmd) => cmd.callback(&config), Self::New(cmd) => cmd.callback(), _ => (), } @@ -309,7 +332,7 @@ impl CargoCmd { } impl RemainingArgs { - /// Get the args to be passed to the executable itself (not `cargo`). + /// Get the args to be passed to `cargo`. pub fn cargo_args(&self) -> Vec { self.split_args().0 } @@ -324,6 +347,8 @@ impl RemainingArgs { if let Some(split) = args.iter().position(|s| s == "--") { let second_half = args.split_off(split + 1); + // take off the "--" arg we found + args.pop(); (args, second_half) } else { @@ -336,13 +361,13 @@ impl Build { /// Callback for `cargo 3ds build`. /// /// This callback handles building the application as a `.3dsx` file. - fn callback(&self, config: &Option, verbose: bool) { + fn callback(&self, config: &Option) { if let Some(config) = config { eprintln!("Building smdh: {}", config.path_smdh().display()); - build_smdh(config, verbose); + build_smdh(config, self.verbose); eprintln!("Building 3dsx: {}", config.path_3dsx().display()); - build_3dsx(config, verbose); + build_3dsx(config, self.verbose); } } } @@ -368,7 +393,7 @@ impl Run { args.push("--server".to_string()); } - let exe_args = self.build_args.cargo_args.exe_args(); + 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... @@ -392,14 +417,59 @@ impl Run { /// Callback for `cargo 3ds run`. /// /// This callback handles launching the application via `3dslink`. - fn callback(&self, config: &Option, verbose: bool) { + fn callback(&self, config: &Option) { // Run the normal "build" callback - self.build_args.callback(config, verbose); + self.build_args.callback(config); + + if !self.is_runner_configured() { + 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 + /// [configuration](https://doc.rust-lang.org/cargo/reference/config.html): + /// + /// - `.cargo/config.toml` + /// - Environment variables + /// - Command-line `--config` overrides + pub fn is_runner_configured(&self) -> bool { + static HAS_RUNNER: OnceLock = OnceLock::new(); + + let has_runner = HAS_RUNNER.get_or_init(|| { + let mut cmd = find_cargo(); + + let config_args = self.config.iter().map(|cfg| format!("--config={cfg}")); + + cmd.args(config_args) + .args([ + "config", + "-Zunstable-options", + "get", + "target.armv6k-nintendo-3ds.runner", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + if self.build_args.verbose { + print_command(&cmd); + } - if let Some(cfg) = config { - eprintln!("Running 3dslink"); - link(cfg, self, verbose); + cmd.status().map_or(false, |status| status.success()) + }); + + if self.build_args.verbose { + eprintln!( + "Custom runner is {}configured", + if *has_runner { "" } else { "not " } + ); } + + *has_runner } } @@ -407,13 +477,13 @@ impl Test { /// Callback for `cargo 3ds test`. /// /// This callback handles launching the application via `3dslink`. - fn callback(&self, config: &Option, verbose: bool) { + fn callback(&self, config: &Option) { if self.no_run { // If the tests don't have to run, use the "build" callback - self.run_args.build_args.callback(config, verbose); + self.run_args.build_args.callback(config); } else { // If the tests have to run, use the "run" callback - self.run_args.callback(config, verbose); + self.run_args.callback(config); } } } @@ -485,10 +555,10 @@ impl New { #[cfg(test)] mod tests { - use super::*; - use clap::CommandFactory; + use super::*; + #[test] fn verify_app() { Cargo::command().debug_assert(); @@ -517,9 +587,10 @@ mod tests { for (args, expected) in CASES { let mut cmd = CargoCmd::Build(Build { - cargo_args: RemainingArgs { + passthrough: RemainingArgs { args: args.iter().map(ToString::to_string).collect(), }, + verbose: false, }); assert_eq!( @@ -528,7 +599,7 @@ mod tests { ); if let CargoCmd::Build(build) = cmd { - assert_eq!(build.cargo_args.args, vec!["--foo", "bar"]); + assert_eq!(build.passthrough.args, vec!["--foo", "bar"]); } else { unreachable!(); } @@ -539,9 +610,10 @@ mod tests { fn extract_format_err() { for args in [&["--message-format=foo"][..], &["--message-format", "foo"]] { let mut cmd = CargoCmd::Build(Build { - cargo_args: RemainingArgs { + passthrough: RemainingArgs { args: args.iter().map(ToString::to_string).collect(), }, + verbose: false, }); assert!(cmd.extract_message_format().is_err()); @@ -564,25 +636,37 @@ mod tests { }, TestParam { input: &["--example", "hello-world", "--", "--do-stuff", "foo"], - expected_cargo: &["--example", "hello-world", "--"], + expected_cargo: &["--example", "hello-world"], expected_exe: &["--do-stuff", "foo"], }, TestParam { input: &["--lib", "--", "foo"], - expected_cargo: &["--lib", "--"], + expected_cargo: &["--lib"], expected_exe: &["foo"], }, TestParam { input: &["foo", "--", "bar"], - expected_cargo: &["foo", "--"], + expected_cargo: &["foo"], expected_exe: &["bar"], }, ] { - let Run { build_args, .. } = - Run::parse_from(std::iter::once(&"run").chain(param.input)); + 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.cargo_args.cargo_args(), param.expected_cargo); - assert_eq!(build_args.cargo_args.exe_args(), param.expected_exe); + assert_eq!(build_args.passthrough.cargo_args(), param.expected_cargo); + assert_eq!(build_args.passthrough.exe_args(), param.expected_exe); } } } diff --git a/src/lib.rs b/src/lib.rs index 5d9f7cc..55291b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ pub mod command; -use crate::command::{CargoCmd, Run}; +use core::fmt; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus, Stdio}; +use std::{env, io, process}; use cargo_metadata::{Message, MetadataCommand}; use command::{Input, Test}; @@ -9,11 +13,7 @@ use semver::Version; use serde::Deserialize; use tee::TeeReader; -use core::fmt; -use std::io::{BufRead, BufReader}; -use std::path::{Path, PathBuf}; -use std::process::{Command, ExitStatus, Stdio}; -use std::{env, io, process}; +use crate::command::{CargoCmd, Run}; /// Build a command using [`make_cargo_build_command`] and execute it, /// parsing and returning the messages from the spawned process. @@ -21,7 +21,7 @@ use std::{env, io, process}; /// For commands that produce an executable output, this function will build the /// `.elf` binary that can be used to create other 3ds files. pub fn run_cargo(input: &Input, message_format: Option) -> (ExitStatus, Vec) { - let mut command = make_cargo_command(&input.cmd, &message_format); + let mut command = make_cargo_command(input, &message_format); if input.verbose { print_command(&command); @@ -62,11 +62,13 @@ pub fn run_cargo(input: &Input, message_format: Option) -> (ExitStatus, /// /// For "build" commands (which compile code, such as `cargo 3ds build` or `cargo 3ds clippy`), /// if there is no pre-built std detected in the sysroot, `build-std` will be used instead. -pub fn make_cargo_command(cmd: &CargoCmd, message_format: &Option) -> Command { - let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); +pub fn make_cargo_command(input: &Input, message_format: &Option) -> Command { + let cmd = &input.cmd; - let mut command = Command::new(cargo); - command.arg(cmd.subcommand_name()); + let mut command = find_cargo(); + command + .args(input.config.iter().map(|cfg| format!("--config={cfg}"))) + .arg(cmd.subcommand_name()); // Any command that needs to compile code will run under this environment. // Even `clippy` and `check` need this kind of context, so we'll just assume any other `Passthrough` command uses it too. @@ -91,24 +93,40 @@ pub fn make_cargo_command(cmd: &CargoCmd, message_format: &Option) -> Co let sysroot = find_sysroot(); if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() { eprintln!("No pre-build std found, using build-std"); + // TODO: should we consider always building `test` ? It's always needed + // if e.g. `test-runner` is a dependency, but not necessarily needed for + // production code. command.arg("-Z").arg("build-std"); } } - if matches!(cmd, CargoCmd::Test(_)) { - // Cargo doesn't like --no-run for doctests: - // https://github.com/rust-lang/rust/issues/87022 - let rustdoc_flags = std::env::var("RUSTDOCFLAGS").unwrap_or_default() - // TODO: should we make this output directory depend on profile etc? - + " --no-run --persist-doctests target/doctests"; + let cargo_args = cmd.cargo_args(); + command.args(cargo_args); + + if let CargoCmd::Test(test) = cmd { + let no_run_flag = if test.run_args.is_runner_configured() { + // TODO: should we persist here as well? Or maybe just let the user + // add that to RUSTDOCFLAGS if they want it... + "" + } else { + " --no-run" + }; + // Cargo doesn't like --no-run for doctests, so we have to plumb it in here + // https://github.com/rust-lang/rust/issues/87022 + let rustdoc_flags = std::env::var("RUSTDOCFLAGS").unwrap_or_default() + no_run_flag; command.env("RUSTDOCFLAGS", rustdoc_flags); } - let cargo_args = cmd.cargo_args(); + if let CargoCmd::Run(run) | CargoCmd::Test(Test { run_args: run, .. }) = &cmd { + if run.is_runner_configured() { + command + .arg("--") + .args(run.build_args.passthrough.exe_args()); + } + } command - .args(cargo_args) .stdout(Stdio::piped()) .stdin(Stdio::inherit()) .stderr(Stdio::inherit()); @@ -116,6 +134,12 @@ pub fn make_cargo_command(cmd: &CargoCmd, message_format: &Option) -> Co command } +/// Get the environment's version of cargo +fn find_cargo() -> Command { + let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + Command::new(cargo) +} + fn print_command(command: &Command) { let mut cmd_str = vec![command.get_program().to_string_lossy().to_string()]; cmd_str.extend(command.get_args().map(|s| s.to_string_lossy().to_string())); @@ -129,8 +153,7 @@ fn print_command(command: &Command) { v.map_or_else(String::new, |s| shlex::quote(&s).to_string()) ); } - eprintln!(" {}", shlex::join(cmd_str.iter().map(String::as_str))); - eprintln!(); + eprintln!(" {}\n", shlex::join(cmd_str.iter().map(String::as_str))); } /// Finds the sysroot path of the current toolchain diff --git a/src/main.rs b/src/main.rs index 13b1d14..022e635 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,9 @@ +use std::process; + use cargo_3ds::command::Cargo; use cargo_3ds::{check_rust_version, run_cargo}; - use clap::Parser; -use std::process; - fn main() { check_rust_version(); @@ -24,5 +23,5 @@ fn main() { process::exit(status.code().unwrap_or(1)); } - input.cmd.run_callback(&messages, input.verbose); + input.cmd.run_callback(&messages); }