diff --git a/.gitignore b/.gitignore index f2f9e58..2da6cde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target -Cargo.lock \ No newline at end of file +Cargo.lock +.idea \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 7ac54bd..fbe3b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" cargo_metadata = "0.14.0" rustc_version = "0.4.0" semver = "1.0.10" -serde = "1.0.111" -serde_derive = "1.0.111" +serde = {version = "1.0.139", features = ['derive']} tee = "0.1.0" toml = "0.5.6" +clap = { version = "3.2.12", features = ["derive"]} \ No newline at end of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..df56fed --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,39 @@ +use clap::{ArgEnum, Parser}; +use std::fmt::{Display, Formatter}; + +#[derive(Parser)] +pub struct Input { + #[clap(arg_enum)] + pub cmd: CargoCommand, + pub cargo_opts: Vec, +} + +#[derive(ArgEnum, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum CargoCommand { + Build, + Run, + Test, + Check, + Clippy, +} + +impl CargoCommand { + pub fn should_build_3dsx(&self) -> bool { + matches!( + self, + CargoCommand::Build | CargoCommand::Run | CargoCommand::Test + ) + } +} + +impl Display for CargoCommand { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + CargoCommand::Build => write!(f, "build"), + CargoCommand::Run => write!(f, "run"), + CargoCommand::Test => write!(f, "test"), + CargoCommand::Check => write!(f, "check"), + CargoCommand::Clippy => write!(f, "clippy"), + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f3a4cd3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,348 @@ +extern crate core; + +pub mod commands; + +use crate::commands::CargoCommand; +use cargo_metadata::{Message, MetadataCommand}; +use core::fmt; +use rustc_version::Channel; +use semver::Version; +use serde::Deserialize; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus, Stdio}; +use std::{env, io, process}; +use tee::TeeReader; + +pub fn build_elf( + cmd: CargoCommand, + message_format: &str, + args: &Vec, +) -> (ExitStatus, Vec) { + let mut command = make_cargo_build_command(cmd, message_format, args); + let mut process = command.spawn().unwrap(); + let command_stdout = process.stdout.take().unwrap(); + + let mut tee_reader; + let mut stdout_reader; + let buf_reader: &mut dyn BufRead = if message_format == "json-render-diagnostics" { + stdout_reader = BufReader::new(command_stdout); + &mut stdout_reader + } else { + tee_reader = BufReader::new(TeeReader::new(command_stdout, io::stdout())); + &mut tee_reader + }; + + let messages = Message::parse_stream(buf_reader) + .collect::>() + .unwrap(); + + (process.wait().unwrap(), messages) +} + +fn make_cargo_build_command( + cmd: CargoCommand, + message_format: &str, + args: &Vec, +) -> 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") + ); + let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + let sysroot = find_sysroot(); + let mut command = Command::new(cargo); + + 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.to_string()) + .arg("--target") + .arg("armv6k-nintendo-3ds") + .arg("--message-format") + .arg(message_format) + .args(args) + .stdout(Stdio::piped()) + .stdin(Stdio::inherit()) + .stderr(Stdio::inherit()); + + command +} + +fn find_sysroot() -> PathBuf { + let sysroot = env::var("SYSROOT").ok().unwrap_or_else(|| { + let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); + + let output = Command::new(&rustc) + .arg("--print") + .arg("sysroot") + .output() + .unwrap_or_else(|_| panic!("Failed to run `{rustc} -- print sysroot`")); + String::from_utf8(output.stdout).expect("Failed to parse sysroot path into a UTF-8 string") + }); + + PathBuf::from(sysroot.trim()) +} + +pub fn check_rust_version() { + let rustc_version = rustc_version::version_meta().unwrap(); + + if rustc_version.channel > Channel::Nightly { + eprintln!("cargo-3ds requires a nightly rustc version."); + eprintln!( + "Please run `rustup override set nightly` to use nightly in the \ + current directory." + ); + process::exit(1); + } + + let old_version = MINIMUM_RUSTC_VERSION + > Version { + // Remove `-nightly` pre-release tag for comparison. + pre: semver::Prerelease::EMPTY, + ..rustc_version.semver.clone() + }; + + let old_commit = match rustc_version.commit_date { + None => false, + Some(date) => { + MINIMUM_COMMIT_DATE + > CommitDate::parse(&date).expect("could not parse `rustc --version` commit date") + } + }; + + if old_version || old_commit { + eprintln!( + "cargo-3ds requires rustc nightly version >= {}", + MINIMUM_COMMIT_DATE, + ); + eprintln!("Please run `rustup update nightly` to upgrade your nightly version"); + + process::exit(1); + } +} + +pub fn get_metadata(messages: &[Message]) -> CTRConfig { + let metadata = MetadataCommand::new() + .exec() + .expect("Failed to get cargo metadata"); + + let mut package = None; + let mut artifact = None; + + // Extract the final built executable. We may want to fail in cases where + // multiple executables, or none, were built? + for message in messages.iter().rev() { + if let Message::CompilerArtifact(art) = message { + if art.executable.is_some() { + package = Some(metadata[&art.package_id].clone()); + artifact = Some(art.clone()); + + break; + } + } + } + if package.is_none() || artifact.is_none() { + eprintln!("No executable found from build command output!"); + process::exit(1); + } + + let (package, artifact) = (package.unwrap(), artifact.unwrap()); + + let mut icon = String::from("./icon.png"); + + if !Path::new(&icon).exists() { + icon = format!( + "{}/libctru/default_icon.png", + env::var("DEVKITPRO").unwrap() + ); + } + + // for now assume a single "kind" since we only support one output artifact + let name = match artifact.target.kind[0].as_ref() { + "bin" | "lib" | "rlib" | "dylib" if artifact.target.test => { + format!("{} tests", artifact.target.name) + } + "example" => { + format!("{} - {} example", artifact.target.name, package.name) + } + _ => artifact.target.name, + }; + + let author = match package.authors.as_slice() { + [name, ..] => name.to_owned(), + [] => String::from("Unspecified Author"), // as standard with the devkitPRO toolchain + }; + + CTRConfig { + name, + author, + description: package + .description + .clone() + .unwrap_or_else(|| String::from("Homebrew Application")), + icon, + target_path: artifact.executable.unwrap().into(), + cargo_manifest_path: package.manifest_path.into(), + } +} + +pub fn build_smdh(config: &CTRConfig) { + let mut process = Command::new("smdhtool") + .arg("--create") + .arg(&config.name) + .arg(&config.description) + .arg(&config.author) + .arg(&config.icon) + .arg(config.path_smdh()) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .expect("smdhtool command failed, most likely due to 'smdhtool' not being in $PATH"); + + let status = process.wait().unwrap(); + + if !status.success() { + process::exit(status.code().unwrap_or(1)); + } +} + +pub fn build_3dsx(config: &CTRConfig) { + let mut command = Command::new("3dsxtool"); + let mut process = command + .arg(&config.target_path) + .arg(config.path_3dsx()) + .arg(format!("--smdh={}", config.path_smdh().to_string_lossy())); + + // If romfs directory exists, automatically include it + let (romfs_path, is_default_romfs) = get_romfs_path(config); + if romfs_path.is_dir() { + println!("Adding RomFS from {}", romfs_path.display()); + process = process.arg(format!("--romfs={}", romfs_path.to_string_lossy())); + } else if !is_default_romfs { + eprintln!( + "Could not find configured RomFS dir: {}", + romfs_path.display() + ); + process::exit(1); + } + + let mut process = process + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .expect("3dsxtool command failed, most likely due to '3dsxtool' not being in $PATH"); + + let status = process.wait().unwrap(); + + if !status.success() { + process::exit(status.code().unwrap_or(1)); + } +} + +pub fn link(config: &CTRConfig) { + let mut process = Command::new("3dslink") + .arg(config.path_3dsx()) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .unwrap(); + + let status = process.wait().unwrap(); + + if !status.success() { + process::exit(status.code().unwrap_or(1)); + } +} + +/// Read the `RomFS` path from the Cargo manifest. If it's unset, use the default. +/// The returned boolean is true when the default is used. +pub fn get_romfs_path(config: &CTRConfig) -> (PathBuf, bool) { + let manifest_path = &config.cargo_manifest_path; + let manifest_str = std::fs::read_to_string(manifest_path) + .unwrap_or_else(|e| panic!("Could not open {}: {e}", manifest_path.display())); + let manifest_data: toml::Value = + toml::de::from_str(&manifest_str).expect("Could not parse Cargo manifest as TOML"); + + // Find the romfs setting and compute the path + let mut is_default = false; + let romfs_dir_setting = manifest_data + .as_table() + .and_then(|table| table.get("package")) + .and_then(toml::Value::as_table) + .and_then(|table| table.get("metadata")) + .and_then(toml::Value::as_table) + .and_then(|table| table.get("cargo-3ds")) + .and_then(toml::Value::as_table) + .and_then(|table| table.get("romfs_dir")) + .and_then(toml::Value::as_str) + .unwrap_or_else(|| { + is_default = true; + "romfs" + }); + let mut romfs_path = manifest_path.clone(); + romfs_path.pop(); // Pop Cargo.toml + romfs_path.push(romfs_dir_setting); + + (romfs_path, is_default) +} + +#[derive(Deserialize, Default)] +pub struct CTRConfig { + name: String, + author: String, + description: String, + icon: String, + target_path: PathBuf, + cargo_manifest_path: PathBuf, +} + +impl CTRConfig { + pub fn path_3dsx(&self) -> PathBuf { + self.target_path.with_extension("3dsx") + } + + pub fn path_smdh(&self) -> PathBuf { + self.target_path.with_extension("smdh") + } +} + +#[derive(Ord, PartialOrd, PartialEq, Eq, Debug)] +pub struct CommitDate { + year: i32, + month: i32, + day: i32, +} + +impl CommitDate { + fn parse(date: &str) -> Option { + let mut iter = date.split('-'); + + let year = iter.next()?.parse().ok()?; + let month = iter.next()?.parse().ok()?; + let day = iter.next()?.parse().ok()?; + + Some(Self { year, month, day }) + } +} + +impl fmt::Display for CommitDate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day) + } +} + +const MINIMUM_COMMIT_DATE: CommitDate = CommitDate { + year: 2022, + month: 6, + day: 15, +}; +const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 63, 0); diff --git a/src/main.rs b/src/main.rs index 6d28c46..e3c480d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,501 +1,70 @@ -use cargo_metadata::{Message, MetadataCommand}; -use rustc_version::{Channel, Version}; -use std::io::{BufRead, BufReader}; -use std::path::{Path, PathBuf}; -use std::process::ExitStatus; -use std::{ - env, fmt, io, - process::{self, Command, Stdio}, -}; -use tee::TeeReader; - -#[derive(serde_derive::Deserialize, Default)] -struct CTRConfig { - name: String, - author: String, - description: String, - icon: String, - target_path: PathBuf, - cargo_manifest_path: PathBuf, -} - -impl CTRConfig { - fn path_3dsx(&self) -> PathBuf { - self.target_path.with_extension("3dsx") - } - - fn path_smdh(&self) -> PathBuf { - self.target_path.with_extension("smdh") - } -} - -#[derive(Ord, PartialOrd, PartialEq, Eq, Debug)] -struct CommitDate { - year: i32, - month: i32, - day: i32, -} - -impl CommitDate { - fn parse(date: &str) -> Option { - let mut iter = date.split('-'); - - let year = iter.next()?.parse().ok()?; - let month = iter.next()?.parse().ok()?; - let day = iter.next()?.parse().ok()?; - - Some(Self { year, month, day }) - } -} - -impl fmt::Display for CommitDate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day) - } -} - -const MINIMUM_COMMIT_DATE: CommitDate = CommitDate { - year: 2022, - month: 6, - day: 15, -}; -const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 63, 0); +use cargo_3ds::commands::{CargoCommand, Input}; +use cargo_3ds::{build_3dsx, build_elf, build_smdh, check_rust_version, get_metadata, link}; +use clap::Parser; +use std::process; fn main() { check_rust_version(); - if env::args().any(|arg| arg == "--help" || arg == "-h") { - print_usage(&mut io::stdout()); - return; - } - - // Get the command and collect the remaining arguments - let cargo_command = CargoCommand::from_args().unwrap_or_else(|| { - print_usage(&mut io::stderr()); - process::exit(2) - }); - - eprintln!("Running Cargo"); - let (status, messages) = cargo_command.build_elf(); - if !status.success() { - process::exit(status.code().unwrap_or(1)); - } - - if !cargo_command.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 cargo_command.should_link { - eprintln!("Running 3dslink"); - link(&app_conf); - } -} - -struct CargoCommand { - command: String, - should_link: bool, - args: Vec, - message_format: String, -} - -impl CargoCommand { - const DEFAULT_MESSAGE_FORMAT: &'static str = "json-render-diagnostics"; - - fn from_args() -> Option { - // Skip `cargo 3ds`. `cargo-3ds` isn't supported for now - let mut args = env::args().skip(2); - - let command = args.next()?; - let mut remaining_args: Vec = args.collect(); - - let (command, should_link) = match command.as_str() { - "run" => ("build".to_string(), true), - "test" => { - let no_run = String::from("--no-run"); - - if remaining_args.contains(&no_run) { - (command, false) - } else { - remaining_args.push(no_run); - (command, true) - } - } - _ => (command, false), - }; - - let message_format = match Self::extract_message_format(&mut remaining_args) { - Some(format) => { - if !format.starts_with("json") { - eprintln!("error: non-JSON `message-format` is not supported"); - process::exit(1); - } - format - } - None => Self::DEFAULT_MESSAGE_FORMAT.to_string(), - }; - - Some(Self { - command, - should_link, - args: remaining_args, - message_format, - }) - } - - fn extract_message_format(args: &mut Vec) -> Option { - for (i, arg) in args.iter().enumerate() { - if arg.starts_with("--message-format") { - return { - let arg = args.remove(i); - - if let Some((_, format)) = arg.split_once('=') { - Some(format.to_string()) - } else { - Some(args.remove(i)) - } - }; - } - } - - None - } - - fn build_elf(&self) -> (ExitStatus, Vec) { - let mut command = self.make_cargo_build_command(); - let mut process = command.spawn().unwrap(); - let command_stdout = process.stdout.take().unwrap(); - - let mut tee_reader; - let mut stdout_reader; - - let buf_reader: &mut dyn BufRead = if self.message_format == Self::DEFAULT_MESSAGE_FORMAT { - stdout_reader = BufReader::new(command_stdout); - &mut stdout_reader + let mut input: Input = Input::parse(); + + let should_link = input.cmd == CargoCommand::Build + || (input.cmd == CargoCommand::Test + && if input.cargo_opts.contains(&"--no-run".to_string()) { + false + } else { + input.cargo_opts.push("--no-run".to_string()); + true + }); + + let message_format = if let Some(pos) = input + .cargo_opts + .iter() + .position(|s| s.starts_with("--message-format")) + { + input.cargo_opts.remove(pos); + let format = if let Some((_, format)) = input + .cargo_opts + .get(pos) + .unwrap() + .to_string() + .split_once('=') + { + format.to_string() } else { - // The user presumably cares about the message format, so we should - // copy stuff to stdout like they expect. We can still extract the executable - // information out of it that we need for 3dsxtool etc. - tee_reader = BufReader::new(TeeReader::new(command_stdout, io::stdout())); - &mut tee_reader + input.cargo_opts.remove(pos).to_string() }; - - let messages = Message::parse_stream(buf_reader) - .collect::>() - .unwrap(); - - (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. - fn make_cargo_build_command(&self) -> Command { - let rustflags = env::var("RUSTFLAGS").unwrap_or_default() - + &format!(" -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 sysroot = Self::find_sysroot(); - let mut command = Command::new(cargo); - - 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("RUSTFLAGS", rustflags) - .arg(&self.command) - .arg("--target") - .arg("armv6k-nintendo-3ds") - .arg("--message-format") - .arg(&self.message_format) - .args(&self.args) - .stdout(Stdio::piped()) - .stdin(Stdio::inherit()) - .stderr(Stdio::inherit()); - - command - } - - /// Get the compiler's sysroot path - fn find_sysroot() -> PathBuf { - let sysroot = env::var("SYSROOT").ok().unwrap_or_else(|| { - // Get sysroot from rustc - let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); - - let output = Command::new(&rustc) - .arg("--print") - .arg("sysroot") - .output() - .unwrap_or_else(|_| panic!("Failed to run `{rustc} --print sysroot`")); - - String::from_utf8(output.stdout) - .expect("Failed to parse sysroot path into a UTF-8 string") - }); - - PathBuf::from(sysroot.trim()) - } - - fn should_build_3dsx(&self) -> bool { - matches!(self.command.as_str(), "build" | "run" | "test") - } -} - -fn print_usage(f: &mut impl io::Write) { - let invocation = { - let mut args = env::args(); - - // We do this to properly display `cargo-3ds` if invoked that way - let bin = args.next().unwrap(); - if let Some("3ds") = args.next().as_deref() { - "cargo 3ds".to_string() + if !format.starts_with("json") { + eprintln!("error: non-JSON `message-format` is not supported"); + process::exit(1); } else { - bin - } - }; - - writeln!( - f, - "{name}: {description}. - -Usage: - {invocation} build [CARGO_OPTS...] - {invocation} run [CARGO_OPTS...] - {invocation} test [CARGO_OPTS...] - {invocation} [CARGO_OPTS...] - {invocation} -h | --help - -Commands: - build build a 3dsx executable. - run build a 3dsx executable and send it to a device with 3dslink. - test build a 3dsx executable from unit/integration tests and send it to a device. - execute some other Cargo command with 3ds options configured (ex. check or clippy). - -Options: - -h --help Show this screen. - -Additional arguments will be passed through to ``. Some that are supported include: - - [build | run | test] --release - test --no-run - -Other flags may work, but haven't been tested. -", - name = env!("CARGO_BIN_NAME"), - description = env!("CARGO_PKG_DESCRIPTION"), - invocation = invocation, - ) - .unwrap(); -} - -fn check_rust_version() { - let rustc_version = rustc_version::version_meta().unwrap(); - - if rustc_version.channel > Channel::Nightly { - eprintln!("cargo-3ds requires a nightly rustc version."); - eprintln!( - "Please run `rustup override set nightly` to use nightly in the \ - current directory." - ); - process::exit(1); - } - - let old_version = MINIMUM_RUSTC_VERSION > Version { - // Remove `-nightly` pre-release tag for comparison. - pre: semver::Prerelease::EMPTY, - ..rustc_version.semver.clone() - }; - - let old_commit = match rustc_version.commit_date { - None => false, - Some(date) => { - MINIMUM_COMMIT_DATE - > CommitDate::parse(&date).expect("could not parse `rustc --version` commit date") - } - }; - - if old_version || old_commit { - eprintln!( - "cargo-3ds requires rustc nightly version >= {}", - MINIMUM_COMMIT_DATE, - ); - eprintln!("Please run `rustup update nightly` to upgrade your nightly version"); - - process::exit(1); - } -} - -fn get_metadata(messages: &[Message]) -> CTRConfig { - let metadata = MetadataCommand::new() - .exec() - .expect("Failed to get cargo metadata"); - - let mut package = None; - let mut artifact = None; - - // Extract the final built executable. We may want to fail in cases where - // multiple executables, or none, were built? - for message in messages.iter().rev() { - if let Message::CompilerArtifact(art) = message { - if art.executable.is_some() { - package = Some(metadata[&art.package_id].clone()); - artifact = Some(art.clone()); - - break; - } + format } - } - if package.is_none() || artifact.is_none() { - eprintln!("No executable found from build command output!"); - process::exit(1); - } - - let (package, artifact) = (package.unwrap(), artifact.unwrap()); - - let mut icon = String::from("./icon.png"); - - if !Path::new(&icon).exists() { - icon = format!( - "{}/libctru/default_icon.png", - env::var("DEVKITPRO").unwrap() - ); - } - - // for now assume a single "kind" since we only support one output artifact - let name = match artifact.target.kind[0].as_ref() { - "bin" | "lib" | "rlib" | "dylib" if artifact.target.test => { - format!("{} tests", artifact.target.name) - } - "example" => { - format!("{} - {} example", artifact.target.name, package.name) - } - _ => artifact.target.name, + } else { + "json-render-diagnostics".to_string() }; - let author = match package.authors.as_slice() { - [name, ..] => name.to_owned(), - [] => String::from("Unspecified Author"), // as standard with the devkitPRO toolchain - }; - - CTRConfig { - name, - author, - description: package - .description - .clone() - .unwrap_or_else(|| String::from("Homebrew Application")), - icon, - target_path: artifact.executable.unwrap().into(), - cargo_manifest_path: package.manifest_path.into(), - } -} - -fn build_smdh(config: &CTRConfig) { - let mut process = Command::new("smdhtool") - .arg("--create") - .arg(&config.name) - .arg(&config.description) - .arg(&config.author) - .arg(&config.icon) - .arg(config.path_smdh()) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .expect("smdhtool command failed, most likely due to 'smdhtool' not being in $PATH"); - - let status = process.wait().unwrap(); + let (status, messages) = build_elf(input.cmd, &message_format, &input.cargo_opts); if !status.success() { process::exit(status.code().unwrap_or(1)); } -} - -fn build_3dsx(config: &CTRConfig) { - let mut command = Command::new("3dsxtool"); - let mut process = command - .arg(&config.target_path) - .arg(config.path_3dsx()) - .arg(format!("--smdh={}", config.path_smdh().to_string_lossy())); - // If romfs directory exists, automatically include it - let (romfs_path, is_default_romfs) = get_romfs_path(config); - if romfs_path.is_dir() { - eprintln!("Adding RomFS from {}", romfs_path.display()); - process = process.arg(format!("--romfs={}", romfs_path.to_string_lossy())); - } else if !is_default_romfs { - eprintln!( - "Could not find configured RomFS dir: {}", - romfs_path.display() - ); - process::exit(1); + if !input.cmd.should_build_3dsx() { + return; } - let mut process = process - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .expect("3dsxtool command failed, most likely due to '3dsxtool' not being in $PATH"); - - let status = process.wait().unwrap(); - - if !status.success() { - process::exit(status.code().unwrap_or(1)); - } -} + println!("Getting metadata"); + let app_conf = get_metadata(&messages); -fn link(config: &CTRConfig) { - let mut process = Command::new("3dslink") - .arg(config.path_3dsx()) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .unwrap(); + println!("Building smdh:{}", app_conf.path_smdh().display()); + build_smdh(&app_conf); - let status = process.wait().unwrap(); + println!("Building 3dsx: {}", app_conf.path_3dsx().display()); + build_3dsx(&app_conf); - if !status.success() { - process::exit(status.code().unwrap_or(1)); + if should_link { + println!("Running 3dslink"); + link(&app_conf); } } - -/// Read the `RomFS` path from the Cargo manifest. If it's unset, use the default. -/// The returned boolean is true when the default is used. -fn get_romfs_path(config: &CTRConfig) -> (PathBuf, bool) { - let manifest_path = &config.cargo_manifest_path; - let manifest_str = std::fs::read_to_string(manifest_path) - .unwrap_or_else(|e| panic!("Could not open {}: {e}", manifest_path.display())); - let manifest_data: toml::Value = - toml::de::from_str(&manifest_str).expect("Could not parse Cargo manifest as TOML"); - - // Find the romfs setting and compute the path - let mut is_default = false; - let romfs_dir_setting = manifest_data - .as_table() - .and_then(|table| table.get("package")) - .and_then(toml::Value::as_table) - .and_then(|table| table.get("metadata")) - .and_then(toml::Value::as_table) - .and_then(|table| table.get("cargo-3ds")) - .and_then(toml::Value::as_table) - .and_then(|table| table.get("romfs_dir")) - .and_then(toml::Value::as_str) - .unwrap_or_else(|| { - is_default = true; - "romfs" - }); - let mut romfs_path = manifest_path.clone(); - romfs_path.pop(); // Pop Cargo.toml - romfs_path.push(romfs_dir_setting); - - (romfs_path, is_default) -}