use std::fs;
use std::io::Read;
use std::process::{self, Stdio};
use std::sync::OnceLock;
use cargo_metadata::{Message, Metadata};
use clap::{Args, Parser, Subcommand};
use crate::{build_3dsx, cargo, get_artifact_config, 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,
#[arg(long, short = 'v', global = true)]
pub verbose: bool,
#[arg(long, global = true)]
pub config: Vec<String>,
}
#[derive(Subcommand, Debug)]
#[command(allow_external_subcommands = true)]
pub enum CargoCmd {
Build(Build),
Run(Run),
Test(Test),
New(New),
#[command(external_subcommand, name = "COMMAND")]
Passthrough(Vec<String>),
}
#[derive(Args, Debug)]
pub struct RemainingArgs {
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
value_name = "CARGO_ARGS"
)]
args: Vec<String>,
}
#[allow(unused_variables)]
trait Callbacks {
fn build_callback(&self, config: &CTRConfig) {}
fn run_callback(&self, config: &CTRConfig) {}
}
#[derive(Args, Debug)]
pub struct Build {
#[arg(from_global)]
pub verbose: bool,
#[command(flatten)]
pub passthrough: RemainingArgs,
}
#[derive(Args, Debug)]
pub struct Run {
#[arg(long, short = 'a')]
pub address: Option<std::net::Ipv4Addr>,
#[arg(long, short = '0')]
pub argv0: Option<String>,
#[arg(long, short = 's', default_value_t = false)]
pub server: bool,
#[arg(long)]
pub retries: Option<usize>,
#[command(flatten)]
pub build_args: Build,
#[arg(from_global)]
config: Vec<String>,
}
#[derive(Args, Debug)]
pub struct Test {
#[arg(long)]
pub no_run: bool,
#[arg(long)]
pub doc: bool,
#[command(flatten)]
pub run_args: Run,
}
#[derive(Args, Debug)]
pub struct New {
#[arg(required = true)]
pub path: String,
#[command(flatten)]
pub cargo_args: RemainingArgs,
}
impl CargoCmd {
pub(crate) 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) => {
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),
}
}
pub(crate) 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],
}
}
pub(crate) fn should_compile(&self) -> bool {
matches!(
self,
Self::Build(_) | Self::Run(_) | Self::Test(_) | Self::Passthrough(_)
)
}
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,
}
}
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 {
Ok(Some(String::from("human")))
} else {
Ok(None)
}
}
fn extract_message_format_from_args(
cargo_args: &mut Vec<String>,
) -> Result<Option<String>, String> {
if let Some(pos) = cargo_args
.iter()
.position(|s| s.starts_with("--message-format"))
{
let arg = cargo_args.remove(pos);
let format = if let Some((_, format)) = arg.split_once('=') {
format.to_string()
} else {
cargo_args.remove(pos)
};
if format.starts_with("json") {
Ok(Some(format))
} else {
Err(String::from(
"error: non-JSON `message-format` is not supported",
))
}
} else {
Ok(None)
}
}
pub fn run_callbacks(&self, messages: &[Message], metadata: Option<&Metadata>) {
let configs = metadata
.map(|metadata| self.build_callbacks(messages, metadata))
.unwrap_or_default();
let config = match self {
_ if configs.len() == 1 => configs.into_iter().next().unwrap(),
Self::Test(Test { no_run: true, .. }) => return,
Self::Test(Test { run_args: run, .. }) | Self::Run(run) if run.use_custom_runner() => {
return
}
Self::New(_) => CTRConfig::default(),
Self::Test(_) | Self::Run(_) => {
let paths: Vec<_> = configs.into_iter().map(|c| c.path_3dsx()).collect();
let names: Vec<_> = paths.iter().filter_map(|p| p.file_name()).collect();
eprintln!(
"Error: expected exactly one (1) executable to run, got {}: {names:?}",
paths.len(),
);
process::exit(1);
}
_ => return,
};
self.run_callback(&config);
}
fn build_callbacks(&self, messages: &[Message], metadata: &Metadata) -> Vec<CTRConfig> {
let max_artifact_count = metadata.packages.iter().map(|pkg| pkg.targets.len()).sum();
let mut configs = Vec::with_capacity(max_artifact_count);
for message in messages {
let Message::CompilerArtifact(artifact) = message else {
continue;
};
if artifact.executable.is_none()
|| !metadata.workspace_members.contains(&artifact.package_id)
{
continue;
}
let package = &metadata[&artifact.package_id];
let config = get_artifact_config(package.clone(), artifact.clone());
self.build_callback(&config);
configs.push(config);
}
configs
}
fn inner_callback(&self) -> Option<&dyn Callbacks> {
match self {
Self::Build(cmd) => Some(cmd),
Self::Run(cmd) => Some(cmd),
Self::Test(cmd) => Some(cmd),
_ => None,
}
}
}
impl Callbacks for CargoCmd {
fn build_callback(&self, config: &CTRConfig) {
if let Some(cb) = self.inner_callback() {
cb.build_callback(config);
}
}
fn run_callback(&self, config: &CTRConfig) {
if let Some(cb) = self.inner_callback() {
cb.run_callback(config);
}
}
}
impl RemainingArgs {
pub(crate) fn cargo_args(&self) -> Vec<String> {
self.split_args().0
}
pub(crate) 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);
args.pop();
(args, second_half)
} else {
(args, Vec::new())
}
}
}
impl Callbacks for Build {
fn build_callback(&self, config: &CTRConfig) {
eprintln!("Building smdh: {}", config.path_smdh());
config.build_smdh(self.verbose);
eprintln!("Building 3dsx: {}", config.path_3dsx());
build_3dsx(config, self.verbose);
}
}
impl Callbacks for Run {
fn build_callback(&self, config: &CTRConfig) {
self.build_args.build_callback(config);
}
fn run_callback(&self, config: &CTRConfig) {
if !self.use_custom_runner() {
eprintln!("Running 3dslink");
link(config, self, self.build_args.verbose);
}
}
}
impl Run {
pub(crate) 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() {
args.extend(["--args".to_string(), "--".to_string()]);
let mut escaped = false;
for arg in exe_args.iter().cloned() {
if arg.starts_with('-') && !escaped {
args.extend(["--".to_string(), arg]);
escaped = true;
} else {
args.push(arg);
}
}
}
args
}
pub(crate) 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([
"-Z",
"unstable-options",
"config",
"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 custom_runner_configured { "" } else { "not " }
);
}
custom_runner_configured
}
}
impl Callbacks for Test {
fn build_callback(&self, config: &CTRConfig) {
self.run_args.build_callback(config);
}
fn run_callback(&self, config: &CTRConfig) {
if !self.no_run {
self.run_args.run_callback(config);
}
}
}
impl Test {
fn should_run(&self) -> bool {
self.run_args.use_custom_runner() && !self.no_run
}
fn cargo_args(&self) -> Vec<String> {
let mut cargo_args = self.run_args.build_args.passthrough.cargo_args();
if self.doc {
cargo_args.extend([
"--doc".into(),
"-Z".into(),
"doctest-xcompile".into(),
]);
} else if !self.should_run() {
cargo_args.push("--no-run".into());
}
cargo_args
}
pub(crate) fn rustdocflags(&self) -> &'static str {
if self.should_run() {
""
} else {
" --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 Callbacks for New {
fn run_callback(&self, _: &CTRConfig) {
if self.cargo_args.args.contains(&"--lib".to_string()) {
return;
}
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");
let dummy_romfs_path = romfs_path.join("PUT_YOUR_ROMFS_FILES_HERE.txt");
fs::create_dir(romfs_path).unwrap();
fs::File::create(dummy_romfs_path).unwrap();
let mut buf = String::new();
fs::File::open(&toml_path)
.unwrap()
.read_to_string(&mut buf)
.unwrap();
let buf = buf + TOML_CHANGES;
fs::write(&toml_path, buf).unwrap();
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);
}
}
}