Browse Source

Merge pull request #36 from rust3ds/feature/cargo-new

General refactoring and cargo-new
pull/38/head
Meziu 1 year ago committed by GitHub
parent
commit
6c22e506af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      README.md
  2. 290
      src/command.rs
  3. 74
      src/lib.rs
  4. 20
      src/main.rs

5
README.md

@ -14,6 +14,8 @@ Commands:
Builds an executable and sends it to a device with `3dslink` Builds an executable and sends it to a device with `3dslink`
test test
Builds a test executable and sends it to a device with `3dslink` 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 help
Print this message or the help of the given subcommand(s) 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`), 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, 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 ### Basic Examples
@ -38,6 +40,7 @@ with the proper `RUSTFLAGS` and `--target` set for the 3DS target.
* `cargo 3ds check --verbose` * `cargo 3ds check --verbose`
* `cargo 3ds run --release --example foo` * `cargo 3ds run --release --example foo`
* `cargo 3ds test --no-run` * `cargo 3ds test --no-run`
* `cargo 3ds new my-new-project --edition 2021`
### Running executables ### Running executables

290
src/command.rs

@ -1,5 +1,11 @@
use std::fs;
use std::io::Read;
use cargo_metadata::Message;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use crate::{build_3dsx, build_smdh, get_metadata, link, CTRConfig};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(name = "cargo", bin_name = "cargo")] #[command(name = "cargo", bin_name = "cargo")]
pub enum Cargo { pub enum Cargo {
@ -23,7 +29,7 @@ pub struct Input {
#[command(allow_external_subcommands = true)] #[command(allow_external_subcommands = true)]
pub enum CargoCmd { pub enum CargoCmd {
/// Builds an executable suitable to run on a 3DS (3dsx). /// 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`. /// Builds an executable and sends it to a device with `3dslink`.
Run(Run), Run(Run),
@ -34,10 +40,13 @@ pub enum CargoCmd {
/// unit tests (which require a custom test runner). /// unit tests (which require a custom test runner).
Test(Test), 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 // 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 // in help, but we might as well set them here in case a future version of clap
// does include them in help text. // 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")] #[command(external_subcommand, name = "COMMAND")]
Passthrough(Vec<String>), Passthrough(Vec<String>),
} }
@ -61,19 +70,10 @@ pub struct RemainingArgs {
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
pub struct Test { pub struct Build {
/// If set, the built executable will not be sent to the device to run it. // Passthrough cargo options.
#[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)] #[command(flatten)]
pub run_args: Run, pub cargo_args: RemainingArgs,
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@ -101,12 +101,99 @@ pub struct Run {
#[arg(long)] #[arg(long)]
pub retries: Option<usize>, pub retries: Option<usize>,
// 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)] #[command(flatten)]
pub cargo_args: RemainingArgs, pub cargo_args: RemainingArgs,
} }
impl CargoCmd { impl CargoCmd {
/// Returns the additional arguments run by the "official" cargo subcommand.
pub fn cargo_args(&self) -> Vec<String> {
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. /// Whether or not this command should build a 3DSX executable file.
pub fn should_build_3dsx(&self) -> bool { pub fn should_build_3dsx(&self) -> bool {
matches!( matches!(
@ -129,10 +216,11 @@ impl CargoCmd {
pub fn extract_message_format(&mut self) -> Result<Option<String>, String> { pub fn extract_message_format(&mut self) -> Result<Option<String>, String> {
let cargo_args = match self { let cargo_args = match self {
Self::Build(args) => &mut args.args, Self::Build(build) => &mut build.cargo_args.args,
Self::Run(run) => &mut run.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::Passthrough(args) => args,
Self::Test(test) => &mut test.run_args.cargo_args.args,
}; };
let format = Self::extract_message_format_from_args(cargo_args)?; let format = Self::extract_message_format_from_args(cargo_args)?;
@ -183,25 +271,68 @@ impl CargoCmd {
Ok(None) 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 { impl RemainingArgs {
/// Get the args to be passed to the executable itself (not `cargo`). /// Get the args to be passed to the executable itself (not `cargo`).
pub fn cargo_args(&self) -> &[String] { pub fn cargo_args(&self) -> Vec<String> {
self.split_args().0 self.split_args().0
} }
/// Get the args to be passed to the executable itself (not `cargo`). /// Get the args to be passed to the executable itself (not `cargo`).
pub fn exe_args(&self) -> &[String] { pub fn exe_args(&self) -> Vec<String> {
self.split_args().1 self.split_args().1
} }
fn split_args(&self) -> (&[String], &[String]) { fn split_args(&self) -> (Vec<String>, Vec<String>) {
if let Some(split) = self.args.iter().position(|s| s == "--") { let mut args = self.args.clone();
self.args.split_at(split + 1)
if let Some(split) = args.iter().position(|s| s == "--") {
let second_half = args.split_off(split + 1);
(args, second_half)
} else { } 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);
} }
} }
@ -226,7 +357,7 @@ impl Run {
args.push("--server".to_string()); 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() { if !exe_args.is_empty() {
// For some reason 3dslink seems to want 2 instances of `--`, one // For some reason 3dslink seems to want 2 instances of `--`, one
// in front of all of the args like this... // in front of all of the args like this...
@ -246,6 +377,97 @@ impl Run {
args 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)] #[cfg(test)]
@ -281,8 +503,10 @@ mod tests {
]; ];
for (args, expected) in CASES { for (args, expected) in CASES {
let mut cmd = CargoCmd::Build(RemainingArgs { let mut cmd = CargoCmd::Build(Build {
cargo_args: RemainingArgs {
args: args.iter().map(ToString::to_string).collect(), args: args.iter().map(ToString::to_string).collect(),
},
}); });
assert_eq!( assert_eq!(
@ -290,8 +514,8 @@ mod tests {
expected.map(ToString::to_string) expected.map(ToString::to_string)
); );
if let CargoCmd::Build(args) = cmd { if let CargoCmd::Build(build) = cmd {
assert_eq!(args.args, vec!["--foo", "bar"]); assert_eq!(build.cargo_args.args, vec!["--foo", "bar"]);
} else { } else {
unreachable!(); unreachable!();
} }
@ -301,8 +525,10 @@ mod tests {
#[test] #[test]
fn extract_format_err() { fn extract_format_err() {
for args in [&["--message-format=foo"][..], &["--message-format", "foo"]] { for args in [&["--message-format=foo"][..], &["--message-format", "foo"]] {
let mut cmd = CargoCmd::Build(RemainingArgs { let mut cmd = CargoCmd::Build(Build {
cargo_args: RemainingArgs {
args: args.iter().map(ToString::to_string).collect(), args: args.iter().map(ToString::to_string).collect(),
},
}); });
assert!(cmd.extract_message_format().is_err()); assert!(cmd.extract_message_format().is_err());
@ -339,11 +565,11 @@ mod tests {
expected_exe: &["bar"], expected_exe: &["bar"],
}, },
] { ] {
let Run { cargo_args, .. } = let Run { build_args, .. } =
Run::parse_from(std::iter::once(&"run").chain(param.input)); Run::parse_from(std::iter::once(&"run").chain(param.input));
assert_eq!(cargo_args.cargo_args(), param.expected_cargo); assert_eq!(build_args.cargo_args.cargo_args(), param.expected_cargo);
assert_eq!(cargo_args.exe_args(), param.expected_exe); assert_eq!(build_args.cargo_args.exe_args(), param.expected_exe);
} }
} }
} }

74
src/lib.rs

@ -1,6 +1,6 @@
pub mod command; pub mod command;
use crate::command::CargoCmd; use crate::command::{CargoCmd, Run};
use cargo_metadata::{Message, MetadataCommand}; use cargo_metadata::{Message, MetadataCommand};
use command::Test; use command::Test;
@ -21,7 +21,8 @@ use std::{env, io, process};
/// For commands that produce an executable output, this function will build the /// For commands that produce an executable output, this function will build the
/// `.elf` binary that can be used to create other 3ds files. /// `.elf` binary that can be used to create other 3ds files.
pub fn run_cargo(cmd: &CargoCmd, message_format: Option<String>) -> (ExitStatus, Vec<Message>) { 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 command = make_cargo_command(cmd, &message_format);
let mut process = command.spawn().unwrap(); let mut process = command.spawn().unwrap();
let command_stdout = process.stdout.take().unwrap(); let command_stdout = process.stdout.take().unwrap();
@ -53,27 +54,20 @@ pub fn run_cargo(cmd: &CargoCmd, message_format: Option<String>) -> (ExitStatus,
(process.wait().unwrap(), messages) (process.wait().unwrap(), messages)
} }
/// Create the cargo build command, but don't execute it. /// Create a cargo command 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<String>) -> Command { /// For "build" commands (which compile code, such as `cargo 3ds build` or `cargo 3ds clippy`),
let rust_flags = env::var("RUSTFLAGS").unwrap_or_default() /// if there is no pre-built std detected in the sysroot, `build-std` will be used instead.
+ &format!( pub fn make_cargo_command(cmd: &CargoCmd, message_format: &Option<String>) -> Command {
" -L{}/libctru/lib -lctru",
env::var("DEVKITPRO").expect("DEVKITPRO is not defined as an environment variable")
);
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let sysroot = find_sysroot();
let mut command = Command::new(cargo); let mut command = Command::new(cargo);
let cmd_str = match cmd { command.arg(cmd.subcommand_name());
CargoCmd::Build(_) | CargoCmd::Run(_) => "build",
CargoCmd::Test(_) => "test",
CargoCmd::Passthrough(cmd) => &cmd[0],
};
// 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 command
.env("RUSTFLAGS", rust_flags)
.arg(cmd_str)
.arg("--target") .arg("--target")
.arg("armv6k-nintendo-3ds") .arg("armv6k-nintendo-3ds")
.arg("--message-format") .arg("--message-format")
@ -83,25 +77,14 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>)
.unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT), .unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT),
); );
let sysroot = find_sysroot();
if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() { if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() {
eprintln!("No pre-built std found, using build-std"); eprintln!("No pre-build std found, using build-std");
command.arg("-Z").arg("build-std"); command.arg("-Z").arg("build-std");
} }
}
let cargo_args = match cmd { if matches!(cmd, CargoCmd::Test(_)) {
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: // Cargo doesn't like --no-run for doctests:
// https://github.com/rust-lang/rust/issues/87022 // https://github.com/rust-lang/rust/issues/87022
let rustdoc_flags = std::env::var("RUSTDOCFLAGS").unwrap_or_default() let rustdoc_flags = std::env::var("RUSTDOCFLAGS").unwrap_or_default()
@ -109,14 +92,9 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>)
+ " --no-run --persist-doctests target/doctests"; + " --no-run --persist-doctests target/doctests";
command.env("RUSTDOCFLAGS", rustdoc_flags); command.env("RUSTDOCFLAGS", rustdoc_flags);
} else {
command.arg("--no-run");
} }
test.run_args.cargo_args.cargo_args() let cargo_args = cmd.cargo_args();
}
CargoCmd::Passthrough(other) => &other[1..],
};
command command
.args(cargo_args) .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 /// 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 { 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`. /// 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 /// 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) { pub fn link(config: &CTRConfig, run_args: &Run) {
let run_args = match cmd {
CargoCmd::Run(run) => run,
CargoCmd::Test(test) => &test.run_args,
_ => unreachable!(),
};
let mut process = Command::new("3dslink") let mut process = Command::new("3dslink")
.arg(config.path_3dsx()) .arg(config.path_3dsx())
.args(run_args.get_3dslink_args()) .args(run_args.get_3dslink_args())
@ -410,8 +382,8 @@ impl fmt::Display for CommitDate {
} }
const MINIMUM_COMMIT_DATE: CommitDate = CommitDate { const MINIMUM_COMMIT_DATE: CommitDate = CommitDate {
year: 2022, year: 2023,
month: 6, month: 5,
day: 15, day: 31,
}; };
const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 63, 0); const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 70, 0);

20
src/main.rs

@ -1,5 +1,5 @@
use cargo_3ds::command::Cargo; 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; use clap::Parser;
@ -24,21 +24,5 @@ fn main() {
process::exit(status.code().unwrap_or(1)); process::exit(status.code().unwrap_or(1));
} }
if !input.cmd.should_build_3dsx() { input.cmd.run_callback(&messages);
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);
}
} }

Loading…
Cancel
Save