diff --git a/README.md b/README.md index 92dc166..15dc49c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Commands: Builds an executable and sends it to a device with `3dslink` test Builds a test executable and sends it to a device with `3dslink` + new + Sets up a new cargo project suitable to run on a 3DS help Print this message or the help of the given subcommand(s) @@ -30,7 +32,7 @@ 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. +with the proper `--target armv6k-nintendo-3ds` set. ### Basic Examples @@ -38,6 +40,7 @@ with the proper `RUSTFLAGS` and `--target` set for the 3DS target. * `cargo 3ds check --verbose` * `cargo 3ds run --release --example foo` * `cargo 3ds test --no-run` +* `cargo 3ds new my-new-project --edition 2021` ### Running executables diff --git a/src/command.rs b/src/command.rs index 7877819..7580419 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::{build_3dsx, build_smdh, get_metadata, link, CTRConfig}; + #[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,10 +40,13 @@ 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. - /// Run any other `cargo` command with RUSTFLAGS set for the 3DS. + /// Run any other `cargo` command with custom building tailored for the 3DS. #[command(external_subcommand, name = "COMMAND")] Passthrough(Vec), } @@ -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,99 @@ 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, + + /// 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. + #[command(flatten)] + pub run_args: Run, +} + +#[derive(Parser, Debug)] +pub struct New { + /// Path of the new project. + #[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 { + /// Returns the additional arguments run by the "official" cargo subcommand. + pub fn cargo_args(&self) -> Vec { + match self { + CargoCmd::Build(build) => build.cargo_args.cargo_args(), + CargoCmd::Run(run) => run.build_args.cargo_args.cargo_args(), + CargoCmd::Test(test) => { + let mut cargo_args = test.run_args.build_args.cargo_args.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 + if test.doc { + eprintln!("Documentation tests requested, no 3dsx will be built or run"); + + // https://github.com/rust-lang/cargo/issues/7040 + cargo_args.append(&mut vec![ + "--doc".to_string(), + "-Z".to_string(), + "doctest-xcompile".to_string(), + ]); + } else { + cargo_args.push("--no-run".to_string()); + } + + cargo_args + } + CargoCmd::New(new) => { + // We push the original path in the new command (we captured it in [`New`] to learn about the context) + 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), + } + } + + /// Returns the cargo subcommand run by `cargo-3ds` when handling a [`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`). + pub fn subcommand_name(&self) -> &str { + match self { + CargoCmd::Build(_) | CargoCmd::Run(_) => "build", + CargoCmd::Test(_) => "test", + CargoCmd::New(_) => "new", + CargoCmd::Passthrough(cmd) => &cmd[0], + } + } + + /// Whether or not this command should compile any code, and thus needs import the custom environment configuration (e.g. target spec). + 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!( @@ -129,10 +216,11 @@ impl CargoCmd { pub fn extract_message_format(&mut self) -> Result, String> { let cargo_args = match self { - Self::Build(args) => &mut args.args, - Self::Run(run) => &mut run.cargo_args.args, + Self::Build(build) => &mut build.cargo_args.args, + Self::Run(run) => &mut run.build_args.cargo_args.args, + Self::New(new) => &mut new.cargo_args.args, + Self::Test(test) => &mut test.run_args.build_args.cargo_args.args, Self::Passthrough(args) => args, - Self::Test(test) => &mut test.run_args.cargo_args.args, }; let format = Self::extract_message_format_from_args(cargo_args)?; @@ -183,28 +271,71 @@ 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]) { + // Process the metadata only for commands that have it/use it + 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 { /// Get the args to be passed to the executable itself (not `cargo`). - pub fn cargo_args(&self) -> &[String] { + pub fn cargo_args(&self) -> Vec { self.split_args().0 } /// Get the args to be passed to the executable itself (not `cargo`). - pub fn exe_args(&self) -> &[String] { + pub fn exe_args(&self) -> Vec { 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) + fn split_args(&self) -> (Vec, Vec) { + let mut args = self.args.clone(); + + if let Some(split) = args.iter().position(|s| s == "--") { + let second_half = args.split_off(split + 1); + + (args, second_half) } else { - (&self.args[..], &[]) + (args, Vec::new()) } } } +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 +357,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 +377,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 +503,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 +514,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 +525,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 +565,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..0af6431 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,8 @@ 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 = make_cargo_command(cmd, &message_format); + let mut process = command.spawn().unwrap(); let command_stdout = process.stdout.take().unwrap(); @@ -53,70 +54,47 @@ pub fn run_cargo(cmd: &CargoCmd, message_format: Option) -> (ExitStatus, (process.wait().unwrap(), messages) } -/// 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: &Option) -> Command { - let rust_flags = env::var("RUSTFLAGS").unwrap_or_default() - + &format!( - " -L{}/libctru/lib -lctru", - env::var("DEVKITPRO").expect("DEVKITPRO is not defined as an environment variable") - ); +/// Create a cargo command based on the context. +/// +/// 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) -> Command { let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - let sysroot = find_sysroot(); let mut command = Command::new(cargo); - let cmd_str = match cmd { - CargoCmd::Build(_) | CargoCmd::Run(_) => "build", - CargoCmd::Test(_) => "test", - CargoCmd::Passthrough(cmd) => &cmd[0], - }; + command.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. + if cmd.should_compile() { + command + .arg("--target") + .arg("armv6k-nintendo-3ds") + .arg("--message-format") + .arg( + message_format + .as_deref() + .unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT), + ); + + let sysroot = find_sysroot(); + if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() { + eprintln!("No pre-build std found, using build-std"); + command.arg("-Z").arg("build-std"); + } + } - command - .env("RUSTFLAGS", rust_flags) - .arg(cmd_str) - .arg("--target") - .arg("armv6k-nintendo-3ds") - .arg("--message-format") - .arg( - message_format - .as_deref() - .unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT), - ); + 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"; - if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() { - eprintln!("No pre-built std found, using build-std"); - command.arg("-Z").arg("build-std"); + command.env("RUSTDOCFLAGS", rustdoc_flags); } - 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 unconditionally pass - // --no-run here and send the executable with 3dslink later, if the - // user wants - - if test.doc { - eprintln!("Documentation tests requested, no 3dsx will be built or run"); - - // https://github.com/rust-lang/cargo/issues/7040 - command.args(["--doc", "-Z", "doctest-xcompile"]); - - // 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"; - - command.env("RUSTDOCFLAGS", rustdoc_flags); - } else { - command.arg("--no-run"); - } - - test.run_args.cargo_args.cargo_args() - } - CargoCmd::Passthrough(other) => &other[1..], - }; + let cargo_args = cmd.cargo_args(); command .args(cargo_args) @@ -180,7 +158,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 +287,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 +382,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); }