Browse Source

Callback refactor and cargo-new

pull/36/head
Andrea Ciliberti 2 years ago
parent
commit
9b6b79dff2
  1. 207
      src/command.rs
  2. 67
      src/lib.rs
  3. 20
      src/main.rs

207
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::{CTRConfig, build_3dsx, build_smdh, get_metadata, link};
#[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,6 +40,9 @@ 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.
@ -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,42 @@ 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,
// 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)] #[command(flatten)]
pub cargo_args: RemainingArgs, pub cargo_args: RemainingArgs,
} }
impl CargoCmd { 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. /// 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!(
@ -183,6 +213,31 @@ 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]) {
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 {
@ -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 { impl Run {
/// Get the args to pass to `3dslink` based on these options. /// Get the args to pass to `3dslink` based on these options.
pub fn get_3dslink_args(&self) -> Vec<String> { pub fn get_3dslink_args(&self) -> Vec<String> {
@ -226,7 +294,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 +314,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 +440,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 +451,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 +462,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 +502,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);
} }
} }
} }

67
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,12 @@ 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 = if cmd.should_compile() {
make_cargo_build_command(cmd, &message_format)
} else {
make_cargo_generic_command(cmd)
};
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,7 +58,7 @@ 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 used for building based on the context.
/// If there is no pre-built std detected in the sysroot, `build-std` is used. /// 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 { pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>) -> Command {
let rust_flags = env::var("RUSTFLAGS").unwrap_or_default() let rust_flags = env::var("RUSTFLAGS").unwrap_or_default()
@ -69,6 +74,7 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>)
CargoCmd::Build(_) | CargoCmd::Run(_) => "build", CargoCmd::Build(_) | CargoCmd::Run(_) => "build",
CargoCmd::Test(_) => "test", CargoCmd::Test(_) => "test",
CargoCmd::Passthrough(cmd) => &cmd[0], CargoCmd::Passthrough(cmd) => &cmd[0],
_ => panic!("tried to build an executable using an unsupported cargo subcommand"),
}; };
command command
@ -89,8 +95,8 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>)
} }
let cargo_args = match cmd { let cargo_args = match cmd {
CargoCmd::Build(cargo_args) => cargo_args.cargo_args(), CargoCmd::Build(build) => build.cargo_args.cargo_args(),
CargoCmd::Run(run) => run.cargo_args.cargo_args(), CargoCmd::Run(run) => run.build_args.cargo_args.cargo_args(),
CargoCmd::Test(test) => { CargoCmd::Test(test) => {
// We can't run 3DS executables on the host, so unconditionally pass // We can't run 3DS executables on the host, so unconditionally pass
// --no-run here and send the executable with 3dslink later, if the // --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<String>)
test.run_args.cargo_args.cargo_args() test.run_args.cargo_args.cargo_args()
} }
CargoCmd::Passthrough(other) => &other[1..], 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 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 /// 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 +348,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 +443,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