Browse Source

Add support for `cargo test` (#11)

* Use cargo metadata to get exe paths

Include support for "test" subcommand

* Fix accidentally using wrong smdh file

* Update help text for `test`

`--release` is no longer special and just works like any other cargo
arg, so might as well take it out of the help text.

* Use cargo's exit code if possible

* Address PR comments

* Use PathBuf and more consistent Path methods

`.display()` for display, `.to_string_lossy()` for passing args. This
isn't perfect, but we'll just assume no UTF-8 pathnames for now.
pull/17/head
Ian Chamberlain 3 years ago committed by GitHub
parent
commit
b7452b4e54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 248
      src/main.rs

248
src/main.rs

@ -1,6 +1,7 @@
use cargo_metadata::{MetadataCommand, Package}; use cargo_metadata::{Message, MetadataCommand};
use rustc_version::{Channel, Version}; use rustc_version::{Channel, Version};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::{ use std::{
env, fmt, io, env, fmt, io,
process::{self, Command, Stdio}, process::{self, Command, Stdio},
@ -12,10 +13,20 @@ struct CTRConfig {
author: String, author: String,
description: String, description: String,
icon: String, icon: String,
target_path: String, target_path: PathBuf,
cargo_manifest_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)] #[derive(Ord, PartialOrd, PartialEq, Eq, Debug)]
struct CommitDate { struct CommitDate {
year: i32, year: i32,
@ -56,52 +67,108 @@ fn main() {
return; return;
} }
let optimization_level = if env::args().any(|arg| arg == "--release") {
String::from("release")
} else {
String::from("debug")
};
// Skip `cargo 3ds`
let mut args = env::args().skip(2);
// Get the command and collect the remaining arguments // Get the command and collect the remaining arguments
let command = args.next(); let cargo_command = CargoCommand::from_args().unwrap_or_else(|| {
let args: Vec<String> = args.collect();
let args: Vec<&str> = args.iter().map(String::as_str).collect();
let (command, must_link) = match command.as_deref() {
Some("link") => ("build", true),
Some(command) => (command, false),
None => {
print_usage(&mut io::stderr()); print_usage(&mut io::stderr());
process::exit(2) process::exit(2)
} });
};
eprintln!("Running Cargo"); eprintln!("Running Cargo");
build_elf(command, &args); let (status, messages) = cargo_command.build_elf();
if !status.success() {
process::exit(status.code().unwrap_or(1));
}
if command != "build" && !must_link { if !cargo_command.should_build_3dsx() {
// We only do more work if it's a build or build + 3dslink operation
return; return;
} }
eprintln!("Getting metadata"); eprintln!("Getting metadata");
let app_conf = get_metadata(&args, &optimization_level); let app_conf = get_metadata(&messages);
eprintln!("Building smdh"); eprintln!("Building smdh:{}", app_conf.path_smdh().display());
build_smdh(&app_conf); build_smdh(&app_conf);
eprintln!("Building 3dsx"); eprintln!("Building 3dsx: {}", app_conf.path_3dsx().display());
build_3dsx(&app_conf); build_3dsx(&app_conf);
if must_link { if cargo_command.should_link {
eprintln!("Running 3dslink"); eprintln!("Running 3dslink");
link(&app_conf); link(&app_conf);
} }
} }
struct CargoCommand {
command: String,
should_link: bool,
args: Vec<String>,
}
impl CargoCommand {
fn from_args() -> Option<Self> {
// 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<String> = args.collect();
let (command, should_link) = match command.as_str() {
"link" => ("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),
};
Some(Self {
command,
should_link,
args: remaining_args,
})
}
fn build_elf(&self) -> (ExitStatus, Vec<Message>) {
let rustflags = env::var("RUSTFLAGS").unwrap_or_default()
+ &format!(" -L{}/libctru/lib -lctru", env::var("DEVKITPRO").unwrap());
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut command = Command::new(cargo)
.arg(&self.command)
.arg("--message-format=json-render-diagnostics")
.arg("-Z")
.arg("build-std")
.arg("--target")
.arg("armv6k-nintendo-3ds")
.args(&self.args)
.env("RUSTFLAGS", rustflags)
.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let stdout_reader = std::io::BufReader::new(command.stdout.take().unwrap());
let messages = Message::parse_stream(stdout_reader)
.collect::<io::Result<Vec<Message>>>()
.unwrap();
(command.wait().unwrap(), messages)
}
fn should_build_3dsx(&self) -> bool {
matches!(self.command.as_str(), "build" | "link" | "test")
}
}
fn print_usage(f: &mut impl std::io::Write) { fn print_usage(f: &mut impl std::io::Write) {
let invocation = { let invocation = {
let mut args = std::env::args(); let mut args = std::env::args();
@ -120,21 +187,27 @@ fn print_usage(f: &mut impl std::io::Write) {
"{name}: {description}. "{name}: {description}.
Usage: Usage:
{invocation} build [--release] [CARGO_OPTS...] {invocation} build [CARGO_OPTS...]
{invocation} link [--release] [CARGO_OPTS...] {invocation} link [CARGO_OPTS...]
{invocation} test [CARGO_OPTS...]
{invocation} <cargo-command> [CARGO_OPTS...] {invocation} <cargo-command> [CARGO_OPTS...]
{invocation} -h | --help {invocation} -h | --help
Commands: Commands:
build build a 3dsx executable. build build a 3dsx executable.
link build a 3dsx executable and send it to a device with 3dslink. link 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.
<cargo-command> execute some other Cargo command with 3ds options configured (ex. check or clippy). <cargo-command> execute some other Cargo command with 3ds options configured (ex. check or clippy).
Options: Options:
-h --help Show this screen. -h --help Show this screen.
--release Build in release mode.
Additional arguments will be passed through to `cargo build`. Additional arguments will be passed through to `<cargo-command>`. Some that are supported include:
[build | link | test] --release
test --no-run
Other flags may work, but haven't been tested.
", ",
name = env!("CARGO_BIN_NAME"), name = env!("CARGO_BIN_NAME"),
description = env!("CARGO_PKG_DESCRIPTION"), description = env!("CARGO_PKG_DESCRIPTION"),
@ -176,71 +249,33 @@ fn check_rust_version() {
} }
} }
fn build_elf(command: &str, args: &[&str]) { fn get_metadata(messages: &[Message]) -> CTRConfig {
let rustflags = env::var("RUSTFLAGS").unwrap_or_default()
+ &format!(" -L{}/libctru/lib -lctru", env::var("DEVKITPRO").unwrap());
let mut process = Command::new("cargo")
.arg(command)
.arg("-Z")
.arg("build-std")
.arg("--target")
.arg("armv6k-nintendo-3ds")
.args(args)
.env("RUSTFLAGS", rustflags)
.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));
}
}
fn get_metadata(args: &[&str], opt_level: &str) -> CTRConfig {
let metadata = MetadataCommand::new() let metadata = MetadataCommand::new()
.exec() .exec()
.expect("Failed to get cargo metadata"); .expect("Failed to get cargo metadata");
let target_dir = &metadata.target_directory;
let package: &Package;
let binary_name: String;
let target_path: String;
// Check if we compiled the crate or an example
if let Some(example_pos) = args.iter().position(|arg| *arg == "--example") {
let example_name = *args.get(example_pos + 1).expect("No example given");
// Find the example's package
package = metadata
.packages
.iter()
.find(|pkg| {
pkg.targets.iter().any(|target| {
target.name == example_name && target.kind.iter().any(|kind| kind == "example")
})
})
.expect("Could not find package for example");
binary_name = format!("{} - {} example", example_name, package.name); let mut package = None;
target_path = format!( let mut artifact = None;
"{}/armv6k-nintendo-3ds/{}/examples/{}",
target_dir, opt_level, example_name // Extract the final built executable. We may want to fail in cases where
); // multiple executables, or none, were built?
} else { for message in messages.iter().rev() {
// Otherwise get the current/root crate if let Message::CompilerArtifact(art) = message {
package = metadata.root_package().expect("No root crate found"); if art.executable.is_some() {
binary_name = package.name.clone(); package = Some(metadata[&art.package_id].clone());
target_path = format!( artifact = Some(art.clone());
"{}/armv6k-nintendo-3ds/{}/{}",
target_dir, opt_level, package.name 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"); let mut icon = String::from("./icon.png");
if !Path::new(&icon).exists() { if !Path::new(&icon).exists() {
@ -250,16 +285,27 @@ fn get_metadata(args: &[&str], opt_level: &str) -> CTRConfig {
); );
} }
// 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,
};
CTRConfig { CTRConfig {
name: binary_name, name,
author: package.authors[0].clone(), author: package.authors[0].clone(),
description: package description: package
.description .description
.clone() .clone()
.unwrap_or_else(|| String::from("Homebrew Application")), .unwrap_or_else(|| String::from("Homebrew Application")),
icon, icon,
target_path, target_path: artifact.executable.unwrap().into(),
cargo_manifest_path: package.manifest_path.clone().into(), cargo_manifest_path: package.manifest_path.into(),
} }
} }
@ -270,7 +316,7 @@ fn build_smdh(config: &CTRConfig) {
.arg(&config.description) .arg(&config.description)
.arg(&config.author) .arg(&config.author)
.arg(&config.icon) .arg(&config.icon)
.arg(format!("{}.smdh", config.target_path)) .arg(config.path_smdh())
.stdin(Stdio::inherit()) .stdin(Stdio::inherit())
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
@ -287,15 +333,15 @@ fn build_smdh(config: &CTRConfig) {
fn build_3dsx(config: &CTRConfig) { fn build_3dsx(config: &CTRConfig) {
let mut command = Command::new("3dsxtool"); let mut command = Command::new("3dsxtool");
let mut process = command let mut process = command
.arg(format!("{}.elf", config.target_path)) .arg(&config.target_path)
.arg(format!("{}.3dsx", config.target_path)) .arg(config.path_3dsx())
.arg(format!("--smdh={}.smdh", config.target_path)); .arg(format!("--smdh={}", config.path_smdh().to_string_lossy()));
// If romfs directory exists, automatically include it // If romfs directory exists, automatically include it
let (romfs_path, is_default_romfs) = get_romfs_path(config); let (romfs_path, is_default_romfs) = get_romfs_path(config);
if romfs_path.is_dir() { if romfs_path.is_dir() {
println!("Adding RomFS from {}", romfs_path.display()); println!("Adding RomFS from {}", romfs_path.display());
process = process.arg(format!("--romfs={}", romfs_path.display())); process = process.arg(format!("--romfs={}", romfs_path.to_string_lossy()));
} else if !is_default_romfs { } else if !is_default_romfs {
eprintln!( eprintln!(
"Could not find configured RomFS dir: {}", "Could not find configured RomFS dir: {}",
@ -320,7 +366,7 @@ fn build_3dsx(config: &CTRConfig) {
fn link(config: &CTRConfig) { fn link(config: &CTRConfig) {
let mut process = Command::new("3dslink") let mut process = Command::new("3dslink")
.arg(format!("{}.3dsx", config.target_path)) .arg(config.path_3dsx())
.stdin(Stdio::inherit()) .stdin(Stdio::inherit())
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
@ -334,7 +380,7 @@ fn link(config: &CTRConfig) {
} }
} }
/// Read the RomFS path from the Cargo manifest. If it's unset, use the default. /// 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. /// The returned boolean is true when the default is used.
fn get_romfs_path(config: &CTRConfig) -> (PathBuf, bool) { fn get_romfs_path(config: &CTRConfig) -> (PathBuf, bool) {
let manifest_path = &config.cargo_manifest_path; let manifest_path = &config.cargo_manifest_path;

Loading…
Cancel
Save