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: @@ -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. @@ -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. @@ -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

290
src/command.rs

@ -1,5 +1,11 @@ @@ -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 { @@ -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 { @@ -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<String>),
}
@ -61,19 +70,10 @@ pub struct RemainingArgs { @@ -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 { @@ -101,12 +101,99 @@ pub struct Run {
#[arg(long)]
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)]
pub cargo_args: RemainingArgs,
}
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.
pub fn should_build_3dsx(&self) -> bool {
matches!(
@ -129,10 +216,11 @@ impl CargoCmd { @@ -129,10 +216,11 @@ impl CargoCmd {
pub fn extract_message_format(&mut self) -> Result<Option<String>, 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,26 +271,69 @@ impl CargoCmd { @@ -183,26 +271,69 @@ 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<String> {
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<String> {
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<String>, Vec<String>) {
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 {
@ -226,7 +357,7 @@ impl Run { @@ -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 { @@ -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 { @@ -281,8 +503,10 @@ mod tests {
];
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(),
},
});
assert_eq!(
@ -290,8 +514,8 @@ mod tests { @@ -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 { @@ -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 {
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 { @@ -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);
}
}
}

74
src/lib.rs

@ -1,6 +1,6 @@ @@ -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}; @@ -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<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 command_stdout = process.stdout.take().unwrap();
@ -53,27 +54,20 @@ pub fn run_cargo(cmd: &CargoCmd, message_format: Option<String>) -> (ExitStatus, @@ -53,27 +54,20 @@ pub fn run_cargo(cmd: &CargoCmd, message_format: Option<String>) -> (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<String>) -> 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<String>) -> 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
.env("RUSTFLAGS", rust_flags)
.arg(cmd_str)
.arg("--target")
.arg("armv6k-nintendo-3ds")
.arg("--message-format")
@ -83,25 +77,14 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>) @@ -83,25 +77,14 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>)
.unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT),
);
let sysroot = find_sysroot();
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");
}
}
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"]);
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()
@ -109,14 +92,9 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>) @@ -109,14 +92,9 @@ pub fn make_cargo_build_command(cmd: &CargoCmd, message_format: &Option<String>)
+ " --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() { @@ -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) { @@ -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 { @@ -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);

20
src/main.rs

@ -1,5 +1,5 @@ @@ -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() { @@ -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);
}

Loading…
Cancel
Save