diff --git a/src/command.rs b/src/command.rs index 00972d3..0261474 100644 --- a/src/command.rs +++ b/src/command.rs @@ -23,7 +23,7 @@ pub struct Input { #[command(allow_external_subcommands = true)] pub enum CargoCmd { /// Builds an executable suitable to run on a 3DS (3dsx). - Build(CargoArgs), + Build(RemainingArgs), /// Builds an executable and sends it to a device with `3dslink`. Run(Run), @@ -43,7 +43,7 @@ pub enum CargoCmd { } #[derive(Args, Debug)] -pub struct CargoArgs { +pub struct RemainingArgs { /// Pass additional options through to the `cargo` command. /// /// To pass flags that start with `-`, you must use `--` to separate `cargo 3ds` @@ -101,12 +101,74 @@ pub struct Run { // Passthrough cargo options. #[command(flatten)] - pub cargo_args: CargoArgs, + pub cargo_args: RemainingArgs, } -impl CargoArgs { +impl CargoCmd { + /// Whether or not this command should build a 3DSX executable file. + pub fn should_build_3dsx(&self) -> bool { + 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, 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, + ) -> Result, 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=' 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_opts(&self) -> &[String] { + pub fn cargo_args(&self) -> &[String] { self.split_args().0 } @@ -116,17 +178,8 @@ impl CargoArgs { } fn split_args(&self) -> (&[String], &[String]) { - if let Some(split) = self - .args - .iter() - .position(|s| s == "--" || !s.starts_with('-')) - { - let split = if self.args[split] == "--" { - split + 1 - } else { - split - }; - self.args.split_at(split) + if let Some(split) = self.args.iter().position(|s| s == "--") { + self.args.split_at(split + 1) } else { (&self.args[..], &[]) } @@ -143,4 +196,89 @@ mod tests { fn verify_app() { Cargo::command().debug_assert(); } + + #[test] + fn extract_format() { + for (args, expected) in [ + (&["--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"), + ), + ] { + 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); + } + } } diff --git a/src/lib.rs b/src/lib.rs index 0aa7ddf..ba8d41a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,84 +1,32 @@ pub mod command; -use crate::command::{CargoCmd, 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 { - CargoCmd::Run(_) => true, - // CargoCmd::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 - todo!(); - // 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()[pos].clone(); // TODO - - // // Allows for usage of '--message-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()[pos].clone() // TODO - // }; - - // // Non-json formats are not supported so the executable exits. - // if format.starts_with("json") { - // format - // } else { - // eprintln!("error: non-JSON `message-format` is not supported"); - // process::exit(1); - // } - // } 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: CargoCmd, - message_format: &str, - args: &Vec, -) -> (ExitStatus, Vec) { - let mut command = make_cargo_build_command(cmd, message_format, args); +/// Built 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) -> (ExitStatus, Vec) { + 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 { @@ -98,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: CargoCmd, - message_format: &str, - args: &Vec, -) -> Command { +pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option) -> Command { let rust_flags = env::var("RUSTFLAGS").unwrap_or_default() + &format!( " -L{}/libctru/lib -lctru", @@ -112,32 +56,48 @@ pub fn make_cargo_build_command( let sysroot = find_sysroot(); let mut command = Command::new(cargo); - let cmd = match cmd { + let cmd_str = match cmd { CargoCmd::Build(_) | CargoCmd::Run(_) => "build", CargoCmd::Test(_) => "test", - CargoCmd::Passthrough(_) => todo!(), + 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()); - command + dbg!(command) } /// Finds the sysroot path of the current toolchain diff --git a/src/main.rs b/src/main.rs index a457646..ea2f47c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,8 @@ -use cargo_3ds::command::{Cargo, CargoCmd, Input, Run, Test}; -use cargo_3ds::{ - build_3dsx, build_elf, build_smdh, check_rust_version, get_message_format, get_metadata, - get_should_link, link, -}; -use clap::{CommandFactory, FromArgMatches, Parser}; +use cargo_3ds::command::Cargo; +use cargo_3ds::{build_3dsx, build_smdh, check_rust_version, get_metadata, link, run_cargo}; + +use clap::Parser; + use std::process; fn main() { @@ -11,45 +10,37 @@ fn main() { let Cargo::Input(mut input) = Cargo::parse(); - dbg!(&input); - - let cargo_args = match &input.cmd { - CargoCmd::Build(cargo_args) - | CargoCmd::Run(Run { cargo_args, .. }) - | CargoCmd::Test(Test { - run_args: Run { cargo_args, .. }, - .. - }) => cargo_args, - CargoCmd::Passthrough(other) => todo!(), + let message_format = match input.cmd.extract_message_format() { + Ok(fmt) => fmt, + Err(msg) => { + eprintln!("{msg}"); + process::exit(1) + } }; - dbg!(cargo_args.cargo_opts()); - dbg!(cargo_args.exe_args()); - - // let - // let message_format = get_message_format(&mut input); + let (status, messages) = run_cargo(&input.cmd, message_format); - // let (status, messages) = build_elf(input.cmd, &message_format, &input.cargo_opts); + if !status.success() { + process::exit(status.code().unwrap_or(1)); + } - // if !status.success() { - // process::exit(status.code().unwrap_or(1)); - // } + if !input.cmd.should_build_3dsx() { + return; + } - // if !input.cmd.should_build_3dsx() { - // return; - // } + eprintln!("Getting metadata"); + let app_conf = get_metadata(&messages); - // eprintln!("Getting metadata"); - // let app_conf = get_metadata(&messages); + eprintln!("Building smdh:{}", app_conf.path_smdh().display()); + build_smdh(&app_conf); - // eprintln!("Building smdh:{}", app_conf.path_smdh().display()); - // build_smdh(&app_conf); + eprintln!("Building 3dsx: {}", app_conf.path_3dsx().display()); + build_3dsx(&app_conf); - // eprintln!("Building 3dsx: {}", app_conf.path_3dsx().display()); - // build_3dsx(&app_conf); + if input.cmd.should_link_to_device() { + // TODO plumb in exe_args and various 3dslink args - // if should_link { - // eprintln!("Running 3dslink"); - // link(&app_conf); - // } + eprintln!("Running 3dslink"); + link(&app_conf); + } }