@ -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 - 3 ds ]
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 ; 16 HPress 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 ) ;
}
}
}