diff --git a/src/command.rs b/src/command.rs index 7877819..847cd33 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,5 +1,11 @@ +use std::fs; +use std::io::Read; + +use cargo_metadata::Message; use clap::{Args, Parser, Subcommand}; +use crate::{CTRConfig, build_3dsx, build_smdh, get_metadata, link}; + #[derive(Parser, Debug)] #[command(name = "cargo", bin_name = "cargo")] pub enum Cargo { @@ -23,7 +29,7 @@ pub struct Input { #[command(allow_external_subcommands = true)] pub enum CargoCmd { /// Builds an executable suitable to run on a 3DS (3dsx). - Build(RemainingArgs), + Build(Build), /// Builds an executable and sends it to a device with `3dslink`. Run(Run), @@ -34,6 +40,9 @@ pub enum CargoCmd { /// unit tests (which require a custom test runner). Test(Test), + /// Sets up a new cargo project suitable to run on a 3DS. + New(New), + // 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. @@ -61,19 +70,10 @@ pub struct RemainingArgs { } #[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, - - /// If set, documentation tests will be built instead of unit tests. - /// This implies `--no-run`. - #[arg(long)] - pub doc: bool, - - // The test command uses a superset of the same arguments as Run. +pub struct Build { + // Passthrough cargo options. #[command(flatten)] - pub run_args: Run, + pub cargo_args: RemainingArgs, } #[derive(Parser, Debug)] @@ -101,12 +101,42 @@ pub struct Run { #[arg(long)] pub retries: Option, - // Passthrough cargo options. + // Passthrough `cargo build` options. + #[command(flatten)] + pub build_args: Build, +} + +#[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, +} + +#[derive(Parser, Debug)] +pub struct New { + /// If set, the built executable will not be sent to the device to run it. + #[arg(required = true)] + pub path: String, + + // The test command uses a superset of the same arguments as Run. #[command(flatten)] pub cargo_args: RemainingArgs, } impl CargoCmd { + /// Whether or not this command should compile any code, and thus needs import the custom environment configuration (e.g. RUSTFLAGS, target, std). + pub fn should_compile(&self) -> bool { + matches!( + self, + Self::Build(_) | Self::Run(_) | Self::Test(_) | Self::Passthrough(_) + ) + } + /// Whether or not this command should build a 3DSX executable file. pub fn should_build_3dsx(&self) -> bool { matches!( @@ -183,6 +213,31 @@ impl CargoCmd { Ok(None) } } + + /// Runs the custom callback *after* the cargo command, depending on the type of command launched. + /// + /// # Examples + /// + /// - `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]) { + let config = if self.should_build_3dsx() { + eprintln!("Getting metadata"); + + get_metadata(messages) + } else { + CTRConfig::default() + }; + + // Run callback only for commands that use it + match self { + Self::Build(cmd) => cmd.callback(&config), + Self::Run(cmd) => cmd.callback(&config), + Self::Test(cmd) => cmd.callback(&config), + Self::New(cmd) => cmd.callback(), + _ => (), + } + } } impl RemainingArgs { @@ -205,6 +260,19 @@ impl RemainingArgs { } } +impl Build { + /// Callback for `cargo 3ds build`. + /// + /// This callback handles building the application as a `.3dsx` file. + fn callback(&self, config: &CTRConfig) { + eprintln!("Building smdh:{}", config.path_smdh().display()); + build_smdh(config); + + eprintln!("Building 3dsx: {}", config.path_3dsx().display()); + build_3dsx(config); + } +} + impl Run { /// Get the args to pass to `3dslink` based on these options. pub fn get_3dslink_args(&self) -> Vec { @@ -226,7 +294,7 @@ impl Run { args.push("--server".to_string()); } - let exe_args = self.cargo_args.exe_args(); + let exe_args = self.build_args.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... @@ -246,6 +314,97 @@ impl Run { args } + + /// Callback for `cargo 3ds run`. + /// + /// This callback handles launching the application via `3dslink`. + fn callback(&self, config: &CTRConfig) { + // Run the normal "build" callback + self.build_args.callback(config); + + eprintln!("Running 3dslink"); + link(config, self); + } +} + +impl Test { + /// Callback for `cargo 3ds test`. + /// + /// This callback handles launching the application via `3dslink`. + fn callback(&self, config: &CTRConfig) { + if self.no_run { + // If the tests don't have to run, use the "build" callback + self.run_args.build_args.callback(config) + } else { + // If the tests have to run, use the "run" callback + self.run_args.callback(config) + } + } +} + +const TOML_CHANGES: &str = "ctru-rs = { git = \"https://github.com/rust3ds/ctru-rs\"} + +[package.metadata.cargo-3ds] +romfs_dir = \"romfs\" +"; + +const CUSTOM_MAIN_RS: &str = "use ctru::prelude::*; + +fn main() { + ctru::use_panic_handler(); + + 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 New { + /// Callback for `cargo 3ds new`. + /// + /// This callback handles the custom environment modifications when creating a new 3DS project. + fn callback(&self) { + // Commmit changes to the project only if is meant to be a binary + if self.cargo_args.args.contains(&"--lib".to_string()) { + return; + } + + // Attain a canonicalised path for the new project and it's TOML manifest + 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"); + + // Create the "romfs" directory + fs::create_dir(romfs_path).unwrap(); + + // Read the contents of `Cargo.toml` to a string + let mut buf = String::new(); + fs::File::open(&toml_path) + .unwrap() + .read_to_string(&mut buf) + .unwrap(); + + // Add the custom changes to the TOML + let buf = buf + TOML_CHANGES; + fs::write(&toml_path, buf).unwrap(); + + // Add the custom changes to the main.rs file + fs::write(main_rs_path, CUSTOM_MAIN_RS).unwrap(); + } } #[cfg(test)] @@ -281,8 +440,10 @@ mod tests { ]; for (args, expected) in CASES { - let mut cmd = CargoCmd::Build(RemainingArgs { - args: args.iter().map(ToString::to_string).collect(), + let mut cmd = CargoCmd::Build(Build { + cargo_args: RemainingArgs { + args: args.iter().map(ToString::to_string).collect(), + }, }); assert_eq!( @@ -290,8 +451,8 @@ mod tests { expected.map(ToString::to_string) ); - if let CargoCmd::Build(args) = cmd { - assert_eq!(args.args, vec!["--foo", "bar"]); + if let CargoCmd::Build(build) = cmd { + assert_eq!(build.cargo_args.args, vec!["--foo", "bar"]); } else { unreachable!(); } @@ -301,8 +462,10 @@ mod tests { #[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(), + let mut cmd = CargoCmd::Build(Build { + cargo_args: RemainingArgs { + args: args.iter().map(ToString::to_string).collect(), + }, }); assert!(cmd.extract_message_format().is_err()); @@ -339,11 +502,11 @@ mod tests { expected_exe: &["bar"], }, ] { - let Run { cargo_args, .. } = + let Run { build_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); + assert_eq!(build_args.cargo_args.cargo_args(), param.expected_cargo); + assert_eq!(build_args.cargo_args.exe_args(), param.expected_exe); } } } diff --git a/src/lib.rs b/src/lib.rs index 8fde305..13a3f91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ pub mod command; -use crate::command::CargoCmd; +use crate::command::{CargoCmd, Run}; use cargo_metadata::{Message, MetadataCommand}; use command::Test; @@ -21,7 +21,12 @@ 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(cmd: &CargoCmd, message_format: Option) -> (ExitStatus, Vec) { - let mut command = make_cargo_build_command(cmd, &message_format); + let mut command = if cmd.should_compile() { + make_cargo_build_command(cmd, &message_format) + } else { + make_cargo_generic_command(cmd) + }; + let mut process = command.spawn().unwrap(); let command_stdout = process.stdout.take().unwrap(); @@ -53,7 +58,7 @@ pub fn run_cargo(cmd: &CargoCmd, message_format: Option) -> (ExitStatus, (process.wait().unwrap(), messages) } -/// Create the cargo build command, but don't execute it. +/// Create a cargo command used for building based on the context. /// 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: &Option) -> Command { let rust_flags = env::var("RUSTFLAGS").unwrap_or_default() @@ -69,6 +74,7 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option) CargoCmd::Build(_) | CargoCmd::Run(_) => "build", CargoCmd::Test(_) => "test", CargoCmd::Passthrough(cmd) => &cmd[0], + _ => panic!("tried to build an executable using an unsupported cargo subcommand"), }; command @@ -89,8 +95,8 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option) } let cargo_args = match cmd { - CargoCmd::Build(cargo_args) => cargo_args.cargo_args(), - CargoCmd::Run(run) => run.cargo_args.cargo_args(), + CargoCmd::Build(build) => build.cargo_args.cargo_args(), + CargoCmd::Run(run) => run.build_args.cargo_args.cargo_args(), CargoCmd::Test(test) => { // We can't run 3DS executables on the host, so unconditionally pass // --no-run here and send the executable with 3dslink later, if the @@ -116,6 +122,39 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option) test.run_args.cargo_args.cargo_args() } CargoCmd::Passthrough(other) => &other[1..], + _ => panic!("tried to build an executable using an unsupported cargo subcommand"), + }; + + command + .args(cargo_args) + .stdout(Stdio::piped()) + .stdin(Stdio::inherit()) + .stderr(Stdio::inherit()); + + command +} + +/// Create a cargo command used for generic purposes based on the context. +pub fn make_cargo_generic_command(cmd: &CargoCmd) -> Command { + let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + let mut command = Command::new(cargo); + + let cmd_str = match cmd { + CargoCmd::New(_) => "new", + _ => panic!("tried to run an unsupported generic cargo subcommand"), + }; + + command.arg(cmd_str); + + let cargo_args = match cmd { + CargoCmd::New(new) => { + println!("{}", new.path); + + command.arg(&new.path); + + new.cargo_args.cargo_args() + } + _ => panic!("tried to build an executable using an unsupported cargo subcommand"), }; command @@ -180,7 +219,7 @@ pub fn check_rust_version() { } } -/// Parses messages returned by the executed cargo command from [`build_elf`]. +/// Parses messages returned by "build" cargo commands (such as `cargo 3ds build` or `cargo 3ds run`). /// The returned [`CTRConfig`] is then used for further building in and execution /// in [`build_smdh`], [`build_3dsx`], and [`link`]. pub fn get_metadata(messages: &[Message]) -> CTRConfig { @@ -309,13 +348,7 @@ 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, cmd: &CargoCmd) { - let run_args = match cmd { - CargoCmd::Run(run) => run, - CargoCmd::Test(test) => &test.run_args, - _ => unreachable!(), - }; - +pub fn link(config: &CTRConfig, run_args: &Run) { let mut process = Command::new("3dslink") .arg(config.path_3dsx()) .args(run_args.get_3dslink_args()) @@ -410,8 +443,8 @@ impl fmt::Display for CommitDate { } const MINIMUM_COMMIT_DATE: CommitDate = CommitDate { - year: 2022, - month: 6, - day: 15, + year: 2023, + month: 5, + day: 31, }; -const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 63, 0); +const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 70, 0); diff --git a/src/main.rs b/src/main.rs index e943262..bac5948 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use cargo_3ds::command::Cargo; -use cargo_3ds::{build_3dsx, build_smdh, check_rust_version, get_metadata, link, run_cargo}; +use cargo_3ds::{check_rust_version, run_cargo}; use clap::Parser; @@ -24,21 +24,5 @@ fn main() { process::exit(status.code().unwrap_or(1)); } - if !input.cmd.should_build_3dsx() { - return; - } - - eprintln!("Getting metadata"); - let app_conf = get_metadata(&messages); - - eprintln!("Building smdh:{}", app_conf.path_smdh().display()); - build_smdh(&app_conf); - - eprintln!("Building 3dsx: {}", app_conf.path_3dsx().display()); - build_3dsx(&app_conf); - - if input.cmd.should_link_to_device() { - eprintln!("Running 3dslink"); - link(&app_conf, &input.cmd); - } + input.cmd.run_callback(&messages); }