Browse Source

Add support for custom runner

Use `cargo config get` to check if the runner is configured, and if so
use it like a normal `cargo run` invocation would. Otherwise, fall back
to the default `3dslink` behavior we had before.
pull/43/head
Ian Chamberlain 1 year ago
parent
commit
0db0ad3b96
No known key found for this signature in database
GPG Key ID: AE5484D09405AA60
  1. 194
      src/command.rs
  2. 65
      src/lib.rs
  3. 7
      src/main.rs

194
src/command.rs

@ -1,10 +1,12 @@ @@ -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 { @@ -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<String>,
}
/// Run a cargo command. COMMAND will be forwarded to the real
@ -67,21 +75,25 @@ pub struct RemainingArgs { @@ -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<String>,
}
#[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 { @@ -109,16 +121,20 @@ pub struct Run {
// Passthrough `cargo build` options.
#[command(flatten)]
pub build_args: Build,
#[arg(from_global)]
config: Vec<String>,
}
#[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 { @@ -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 { @@ -142,16 +158,15 @@ 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.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 { @@ -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 { @@ -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 { @@ -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 { @@ -225,10 +248,10 @@ impl CargoCmd {
pub fn extract_message_format(&mut self) -> Result<Option<String>, 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 { @@ -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 { @@ -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 { @@ -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<String> {
self.split_args().0
}
@ -324,6 +347,8 @@ impl RemainingArgs { @@ -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 { @@ -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<CTRConfig>, verbose: bool) {
fn callback(&self, config: &Option<CTRConfig>) {
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 { @@ -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,28 +417,73 @@ impl Run { @@ -392,28 +417,73 @@ impl Run {
/// Callback for `cargo 3ds run`.
///
/// This callback handles launching the application via `3dslink`.
fn callback(&self, config: &Option<CTRConfig>, verbose: bool) {
fn callback(&self, config: &Option<CTRConfig>) {
// 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, verbose);
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<bool> = 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);
}
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
}
}
impl Test {
/// Callback for `cargo 3ds test`.
///
/// This callback handles launching the application via `3dslink`.
fn callback(&self, config: &Option<CTRConfig>, verbose: bool) {
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, 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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
}
}

65
src/lib.rs

@ -1,6 +1,10 @@ @@ -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; @@ -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}; @@ -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<String>) -> (ExitStatus, Vec<Message>) {
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<String>) -> (ExitStatus, @@ -62,11 +62,13 @@ pub fn run_cargo(input: &Input, message_format: Option<String>) -> (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<String>) -> Command {
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
pub fn make_cargo_command(input: &Input, message_format: &Option<String>) -> 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<String>) -> Co @@ -91,24 +93,40 @@ pub fn make_cargo_command(cmd: &CargoCmd, message_format: &Option<String>) -> 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<String>) -> Co @@ -116,6 +134,12 @@ pub fn make_cargo_command(cmd: &CargoCmd, message_format: &Option<String>) -> 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) { @@ -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

7
src/main.rs

@ -1,10 +1,9 @@ @@ -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() { @@ -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);
}

Loading…
Cancel
Save