Browse Source

Merge pull request #26 from ian-h-chamberlain/feature/clap4-upgrade-extra-args

Upgrade to clap4, add extra args and help strings
pull/30/head
Meziu 2 years ago committed by GitHub
parent
commit
83032c6a71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      Cargo.toml
  2. 107
      README.md
  3. 337
      src/command.rs
  4. 129
      src/lib.rs
  5. 22
      src/main.rs

4
Cargo.toml

@ -11,7 +11,7 @@ edition = "2021" @@ -11,7 +11,7 @@ edition = "2021"
cargo_metadata = "0.14.0"
rustc_version = "0.4.0"
semver = "1.0.10"
serde = {version = "1.0.139", features = ['derive']}
serde = { version = "1.0.139", features = ['derive'] }
tee = "0.1.0"
toml = "0.5.6"
clap = { version = "3.2.12", features = ["derive"]}
clap = { version = "4.0.15", features = ["derive", "wrap_help"] }

107
README.md

@ -1,26 +1,97 @@ @@ -1,26 +1,97 @@
# cargo-3ds
Cargo command to work with Nintendo 3DS project binaries. Based on cargo-psp.
# Usage
## Usage
Use the nightly toolchain to build 3DS apps (either by using `rustup override nightly` for the project directory or by adding `+nightly` in the `cargo` invocation).
Available commands:
```
build build a 3dsx executable.
run build a 3dsx executable and send it to a device with 3dslink.
test build a 3dsx executable from unit/integration tests and send it to a device.
<cargo-command> execute some other Cargo command with 3ds options configured (ex. check or clippy).
```
Additional arguments will be passed through to `<cargo-command>`. Some that are supported include:
```txt
Commands:
build
Builds an executable suitable to run on a 3DS (3dsx)
run
Builds an executable and sends it to a device with `3dslink`
test
Builds a test executable and sends it to a device with `3dslink`
help
Print this message or the help of the given subcommand(s)
Options:
-h, --help
Print help information (use `-h` for a summary)
-V, --version
Print version information
```
[build | run | test] --release
test --no-run
Additional arguments will be passed through to the given subcommand.
See [passthrough arguments](#passthrough-arguments) for more details.
It is also possible to pass any other `cargo` command (e.g. `doc`, `check`),
and all its arguments will be passed through directly to `cargo` unmodified,
with the proper `RUSTFLAGS` and `--target` set for the 3DS target.
### Basic Examples
* `cargo 3ds build`
* `cargo 3ds check --verbose`
* `cargo 3ds run --release --example foo`
* `cargo 3ds test --no-run`
### Running executables
`cargo 3ds test` and `cargo 3ds run` use the `3dslink` tool to send built
executables to a device, and thus accept specific related arguments that correspond
to `3dslink` arguments:
```txt
-a, --address <ADDRESS>
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.
-0, --argv0 <ARGV0>
Set the 0th argument of the executable when running it. Corresponds to 3dslink's `--argv0` argument
-s, --server
Start the 3dslink server after sending the executable. Corresponds to 3dslink's `--server` argument
--retries <RETRIES>
Set the number of tries when connecting to the device to send the executable. Corresponds to 3dslink's `--retries` argument
```
Other flags and commands may work, but haven't been tested.
# Examples
`cargo 3ds build` \
`cargo 3ds run --release` \
`cargo 3ds test --no-run`
### Passthrough Arguments
Due to the way `cargo-3ds`, `cargo`, and `3dslink` parse arguments, there is
unfortunately some complexity required when invoking an executable with arguments.
All unrecognized arguments beginning with a dash (e.g. `--release`, `--features`,
etc.) will be passed through to `cargo` directly.
> **NOTE:** arguments for [running executables](#running-executables) must be
> specified *before* other unrecognized `cargo` arguments. Otherwise they will
> be treated as passthrough arguments which will most likely fail the resulting
> `cargo` command.
An optional `--` may be used to explicitly pass subsequent args to `cargo`, including
arguments to pass to the executable itself. To separate `cargo` arguments from
executable arguments, *another* `--` can be used. For example:
* `cargo 3ds run -- -- xyz`
Builds an executable and send it to a device to run it with the argument `xyz`.
* `cargo 3ds test --address 192.168.0.2 -- -- --test-arg 1`
Builds a test executable and attempts to send it to a device with the
address `192.168.0.2` and run it using the arguments `["--test-arg", "1"]`.
* `cargo 3ds test --address 192.168.0.2 --verbose -- --test-arg 1`
Build a test executable with `cargo test --verbose`, and attempts to send
it to a device with the address `192.168.0.2` and run it using the arguments
`["--test-arg", "1"]`.
This works without two `--` instances because `--verbose` begins the set of
`cargo` arguments and ends the set of 3DS-specific arguments.

337
src/command.rs

@ -1,36 +1,327 @@ @@ -1,36 +1,327 @@
use clap::{AppSettings, Args, Parser, ValueEnum};
use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
#[clap(name = "cargo")]
#[clap(bin_name = "cargo")]
#[derive(Parser, Debug)]
#[command(name = "cargo", bin_name = "cargo")]
pub enum Cargo {
#[clap(name = "3ds")]
#[command(name = "3ds")]
Input(Input),
}
#[derive(Args)]
#[clap(about)]
#[clap(global_setting(AppSettings::AllowLeadingHyphen))]
#[derive(Args, Debug)]
#[command(version, about)]
pub struct Input {
#[clap(value_enum)]
pub cmd: CargoCommand,
pub cargo_opts: Vec<String>,
#[command(subcommand)]
pub cmd: CargoCmd,
}
#[derive(ValueEnum, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum CargoCommand {
Build,
Run,
Test,
Check,
Clippy,
/// 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(RemainingArgs),
/// 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),
// 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 RUSTFLAGS set 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)]
#[arg(allow_hyphen_values = true)]
#[arg(global = true)]
#[arg(name = "CARGO_ARGS")]
args: Vec<String>,
}
#[derive(Parser, 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,
// The test command uses a superset of the same arguments as Run.
#[command(flatten)]
pub run_args: Run,
}
impl CargoCommand {
#[derive(Parser, 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 options.
#[command(flatten)]
pub cargo_args: RemainingArgs,
}
impl CargoCmd {
/// Whether or not this command should build a 3DSX executable file.
pub fn should_build_3dsx(&self) -> bool {
matches!(
self,
CargoCommand::Build | CargoCommand::Run | CargoCommand::Test
)
matches!(self, Self::Build(_) | Self::Run(_) | Self::Test(_))
}
/// Whether or not the resulting executable should be sent to the 3DS with
/// `3dslink`.
pub fn should_link_to_device(&self) -> bool {
match self {
CargoCmd::Test(test) => !test.no_run,
CargoCmd::Run(_) => true,
_ => false,
}
}
pub const DEFAULT_MESSAGE_FORMAT: &str = "json-render-diagnostics";
pub fn extract_message_format(&mut self) -> Result<Option<String>, String> {
Self::extract_message_format_from_args(match self {
CargoCmd::Build(args) => &mut args.args,
CargoCmd::Run(run) => &mut run.cargo_args.args,
CargoCmd::Test(test) => &mut test.run_args.cargo_args.args,
CargoCmd::Passthrough(args) => args,
})
}
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)
}
}
}
impl RemainingArgs {
/// Get the args to be passed to the executable itself (not `cargo`).
pub fn cargo_args(&self) -> &[String] {
self.split_args().0
}
/// Get the args to be passed to the executable itself (not `cargo`).
pub fn exe_args(&self) -> &[String] {
self.split_args().1
}
fn split_args(&self) -> (&[String], &[String]) {
if let Some(split) = self.args.iter().position(|s| s == "--") {
self.args.split_at(split + 1)
} else {
(&self.args[..], &[])
}
}
}
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.cargo_args.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
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[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(RemainingArgs {
args: args.iter().map(ToString::to_string).collect(),
});
assert_eq!(
cmd.extract_message_format().unwrap(),
expected.map(ToString::to_string)
);
if let CargoCmd::Build(args) = cmd {
assert_eq!(args.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(RemainingArgs {
args: args.iter().map(ToString::to_string).collect(),
});
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 Run { cargo_args, .. } =
Run::parse_from(std::iter::once(&"run").chain(param.input));
assert_eq!(cargo_args.cargo_args(), param.expected_cargo);
assert_eq!(cargo_args.exe_args(), param.expected_exe);
}
}
}

129
src/lib.rs

@ -1,85 +1,32 @@ @@ -1,85 +1,32 @@
extern crate core;
pub mod command;
use crate::command::{CargoCommand, Input};
use crate::command::CargoCmd;
use cargo_metadata::{Message, MetadataCommand};
use core::fmt;
use rustc_version::Channel;
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 tee::TeeReader;
const DEFAULT_MESSAGE_FORMAT: &str = "json-render-diagnostics";
/// Gets whether the executable should link the generated files to a 3ds
/// based on the parsed input.
pub fn get_should_link(input: &mut Input) -> bool {
// When running compile only commands, don't link the executable to the 3ds.
// Otherwise, link and run on the 3ds but do not run locally.
match input.cmd {
CargoCommand::Run => true,
CargoCommand::Test if !input.cargo_opts.contains(&"--no-run".to_string()) => {
input.cargo_opts.push("--no-run".to_string());
true
}
_ => false,
}
}
/// Extracts the user-defined message format and if there is none,
/// default to `json-render-diagnostics`.
pub fn get_message_format(input: &mut Input) -> String {
// Checks for a position within the args where '--message-format' is located
if let Some(pos) = input
.cargo_opts
.iter()
.position(|s| s.starts_with("--message-format"))
{
// Remove the arg from list
let arg = input.cargo_opts.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 {
input.cargo_opts.remove(pos)
};
// Non-json formats are not supported so the executable exits.
if !format.starts_with("json") {
eprintln!("error: non-JSON `message-format` is not supported");
process::exit(1);
} else {
format
}
} else {
// Default to 'json-render-diagnostics'
DEFAULT_MESSAGE_FORMAT.to_string()
}
}
/// Build the elf that will be used to create other 3ds files.
/// The command built from [`make_cargo_build_command`] is executed
/// and the messages from the spawned process are parsed and returned.
pub fn build_elf(
cmd: CargoCommand,
message_format: &str,
args: &Vec<String>,
) -> (ExitStatus, Vec<Message>) {
let mut command = make_cargo_build_command(cmd, message_format, args);
/// Build a command using [`make_cargo_build_command`] and execute it,
/// parsing and returning the messages from the spawned 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(cmd: &CargoCmd, message_format: Option<String>) -> (ExitStatus, Vec<Message>) {
let mut command = make_cargo_build_command(cmd, &message_format);
let mut process = command.spawn().unwrap();
let command_stdout = process.stdout.take().unwrap();
let mut tee_reader;
let mut stdout_reader;
let buf_reader: &mut dyn BufRead = if message_format == DEFAULT_MESSAGE_FORMAT {
let buf_reader: &mut dyn BufRead = if message_format.is_none() {
stdout_reader = BufReader::new(command_stdout);
&mut stdout_reader
} else {
@ -99,11 +46,7 @@ pub fn build_elf( @@ -99,11 +46,7 @@ pub fn build_elf(
/// Create the cargo build command, but don't execute it.
/// If there is no pre-built std detected in the sysroot, `build-std` is used.
pub fn make_cargo_build_command(
cmd: CargoCommand,
message_format: &str,
args: &Vec<String>,
) -> Command {
pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>) -> Command {
let rust_flags = env::var("RUSTFLAGS").unwrap_or_default()
+ &format!(
" -L{}/libctru/lib -lctru",
@ -113,28 +56,43 @@ pub fn make_cargo_build_command( @@ -113,28 +56,43 @@ pub fn make_cargo_build_command(
let sysroot = find_sysroot();
let mut command = Command::new(cargo);
let cmd = match cmd {
CargoCommand::Build | CargoCommand::Run => "build",
CargoCommand::Test => "test",
CargoCommand::Check => "check",
CargoCommand::Clippy => "clippy",
let cmd_str = match cmd {
CargoCmd::Build(_) | CargoCmd::Run(_) => "build",
CargoCmd::Test(_) => "test",
CargoCmd::Passthrough(cmd) => &cmd[0],
};
command
.env("RUSTFLAGS", rust_flags)
.arg(cmd)
.arg(cmd_str)
.arg("--target")
.arg("armv6k-nintendo-3ds")
.arg("--message-format")
.arg(message_format);
.arg(
message_format
.as_deref()
.unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT),
);
if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() {
eprintln!("No pre-build std found, using build-std");
command.arg("-Z").arg("build-std");
}
let cargo_args = match cmd {
CargoCmd::Build(cargo_args) => cargo_args.cargo_args(),
CargoCmd::Run(run) => run.cargo_args.cargo_args(),
CargoCmd::Test(test) => {
// We can't run 3DS executables on the host, so pass --no-run here and
// send the executable with 3dslink later, if the user wants
command.arg("--no-run");
test.run_args.cargo_args.cargo_args()
}
CargoCmd::Passthrough(other) => &other[1..],
};
command
.args(args)
.args(cargo_args)
.stdout(Stdio::piped())
.stdin(Stdio::inherit())
.stderr(Stdio::inherit());
@ -200,7 +158,7 @@ pub fn check_rust_version() { @@ -200,7 +158,7 @@ pub fn check_rust_version() {
/// Parses messages returned by the executed cargo command from [`build_elf`].
/// The returned [`CTRConfig`] is then used for further building in and execution
/// in [`build_smdh`], ['build_3dsx'], and [`link`].
/// in [`build_smdh`], [`build_3dsx`], and [`link`].
pub fn get_metadata(messages: &[Message]) -> CTRConfig {
let metadata = MetadataCommand::new()
.exec()
@ -249,7 +207,7 @@ pub fn get_metadata(messages: &[Message]) -> CTRConfig { @@ -249,7 +207,7 @@ pub fn get_metadata(messages: &[Message]) -> CTRConfig {
};
let author = match package.authors.as_slice() {
[name, ..] => name.to_owned(),
[name, ..] => name.clone(),
[] => String::from("Unspecified Author"), // as standard with the devkitPRO toolchain
};
@ -327,9 +285,16 @@ pub fn build_3dsx(config: &CTRConfig) { @@ -327,9 +285,16 @@ pub fn build_3dsx(config: &CTRConfig) {
/// Link the generated 3dsx to a 3ds to execute and test using `3dslink`.
/// This will fail if `3dslink` is not within the running directory or in a directory found in $PATH
pub fn link(config: &CTRConfig) {
pub fn link(config: &CTRConfig, cmd: &CargoCmd) {
let run_args = match cmd {
CargoCmd::Run(run) => run,
CargoCmd::Test(test) => &test.run_args,
_ => unreachable!(),
};
let mut process = Command::new("3dslink")
.arg(config.path_3dsx())
.args(run_args.get_3dslink_args())
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())

22
src/main.rs

@ -1,9 +1,8 @@ @@ -1,9 +1,8 @@
use cargo_3ds::command::Cargo;
use cargo_3ds::{
build_3dsx, build_elf, build_smdh, check_rust_version, get_message_format, get_metadata,
get_should_link, link,
};
use cargo_3ds::{build_3dsx, build_smdh, check_rust_version, get_metadata, link, run_cargo};
use clap::Parser;
use std::process;
fn main() {
@ -11,10 +10,15 @@ fn main() { @@ -11,10 +10,15 @@ fn main() {
let Cargo::Input(mut input) = Cargo::parse();
let should_link = get_should_link(&mut input);
let message_format = get_message_format(&mut input);
let message_format = match input.cmd.extract_message_format() {
Ok(fmt) => fmt,
Err(msg) => {
eprintln!("{msg}");
process::exit(1)
}
};
let (status, messages) = build_elf(input.cmd, &message_format, &input.cargo_opts);
let (status, messages) = run_cargo(&input.cmd, message_format);
if !status.success() {
process::exit(status.code().unwrap_or(1));
@ -33,8 +37,8 @@ fn main() { @@ -33,8 +37,8 @@ fn main() {
eprintln!("Building 3dsx: {}", app_conf.path_3dsx().display());
build_3dsx(&app_conf);
if should_link {
if input.cmd.should_link_to_device() {
eprintln!("Running 3dslink");
link(&app_conf);
link(&app_conf, &input.cmd);
}
}

Loading…
Cancel
Save