pub mod command;
mod graph;
use std::ffi::OsStr;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Command, ExitStatus, Stdio};
use std::{env, fmt, io, process};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::{Artifact, Message, Package};
use rustc_version::Channel;
use semver::Version;
use serde::Deserialize;
use tee::TeeReader;
use crate::command::{CargoCmd, Input, Run, Test};
use crate::graph::UnitGraph;
pub fn run_cargo(input: &Input, message_format: Option<String>) -> (ExitStatus, Vec<Message>) {
let mut command = make_cargo_command(input, &message_format);
if input.cmd.should_compile() {
let libctru = if should_use_ctru_debuginfo(&command, input.verbose) {
"ctrud"
} else {
"ctru"
};
let rustflags = command
.get_envs()
.find(|(var, _)| var == &OsStr::new("RUSTFLAGS"))
.and_then(|(_, flags)| flags)
.unwrap_or_default()
.to_string_lossy();
let rustflags = format!("{rustflags} -l{libctru}");
command.env("RUSTFLAGS", rustflags);
}
if input.verbose {
print_command(&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 = match (message_format, &input.cmd) {
(Some(_), _) |
(None, CargoCmd::Test(Test { doc: true, .. })) => {
tee_reader = BufReader::new(TeeReader::new(command_stdout, io::stdout()));
&mut tee_reader
}
_ => {
stdout_reader = BufReader::new(command_stdout);
&mut stdout_reader
}
};
let messages = Message::parse_stream(buf_reader)
.collect::<io::Result<_>>()
.unwrap();
(process.wait().unwrap(), messages)
}
fn should_use_ctru_debuginfo(cargo_cmd: &Command, verbose: bool) -> bool {
match UnitGraph::from_cargo(cargo_cmd, verbose) {
Ok(unit_graph) => {
let Some(unit) = unit_graph
.units
.iter()
.find(|unit| unit.target.name == "ctru_sys")
else {
eprintln!("Warning: unable to check if `ctru` debuginfo should be linked: `ctru-sys` not found");
return false;
};
let debuginfo = unit.profile.debuginfo.unwrap_or(0);
debuginfo > 0
}
Err(err) => {
eprintln!("Warning: unable to check if `ctru` debuginfo should be linked: {err}");
false
}
}
}
pub(crate) fn make_cargo_command(input: &Input, message_format: &Option<String>) -> Command {
let devkitpro =
env::var("DEVKITPRO").expect("DEVKITPRO is not defined as an environment variable");
let rustflags =
env::var("RUSTFLAGS").unwrap_or_default() + &format!(" -L{devkitpro}/libctru/lib");
let cargo_cmd = &input.cmd;
let mut command = cargo(&input.config);
command
.arg(cargo_cmd.subcommand_name())
.env("RUSTFLAGS", rustflags);
if cargo_cmd.should_compile() {
command
.arg("--target")
.arg("armv6k-nintendo-3ds")
.arg("--message-format")
.arg(
message_format
.as_deref()
.unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT),
);
let sysroot = find_sysroot();
if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() {
if input.verbose {
eprintln!("No pre-build std found, using build-std");
}
command.arg("-Z").arg("build-std=std,test");
}
}
if let CargoCmd::Test(test) = cargo_cmd {
let rustdoc_flags = std::env::var("RUSTDOCFLAGS").unwrap_or_default() + test.rustdocflags();
command.env("RUSTDOCFLAGS", rustdoc_flags);
}
command.args(cargo_cmd.cargo_args());
if let CargoCmd::Run(run) | CargoCmd::Test(Test { run_args: run, .. }) = &cargo_cmd {
if run.use_custom_runner() {
command
.arg("--")
.args(run.build_args.passthrough.exe_args());
}
}
command
.stdout(Stdio::piped())
.stdin(Stdio::inherit())
.stderr(Stdio::inherit());
command
}
fn cargo(config: &[String]) -> Command {
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut cmd = Command::new(cargo);
cmd.args(config.iter().map(|cfg| format!("--config={cfg}")));
cmd
}
fn print_command(command: &Command) {
let mut cmd_str = vec![command.get_program().to_string_lossy().to_string()];
cmd_str.extend(command.get_args().map(|s| s.to_string_lossy().to_string()));
eprintln!("Running command:");
for (k, v) in command.get_envs() {
let v = v.map(|v| v.to_string_lossy().to_string());
eprintln!(
" {}={} \\",
k.to_string_lossy(),
v.map_or_else(String::new, |s| shlex::try_quote(&s).unwrap().to_string())
);
}
eprintln!(
" {}\n",
shlex::try_join(cmd_str.iter().map(String::as_str)).unwrap()
);
}
pub(crate) 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(input: &Input) {
let rustc_version = rustc_version::version_meta().unwrap();
if rustc_version.channel > Channel::Nightly && input.cmd.should_compile() {
eprintln!("building with cargo-3ds requires a nightly rustc version.");
eprintln!(
"Please run `rustup override set nightly` to use nightly in the \
current directory, or use `cargo +nightly 3ds` to use it for a \
single invocation."
);
process::exit(1);
}
let old_version = MINIMUM_RUSTC_VERSION
> Version {
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(crate) fn get_artifact_config(package: Package, artifact: Artifact) -> CTRConfig {
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 config = package
.metadata
.get("cargo-3ds")
.and_then(|c| CTRConfig::deserialize(c).ok())
.unwrap_or_default();
CTRConfig {
name,
authors: config.authors.or(Some(package.authors)),
description: config.description.or(package.description),
manifest_dir: package.manifest_path.parent().unwrap().into(),
target_path: artifact.executable.unwrap(),
..config
}
}
pub(crate) fn build_3dsx(config: &CTRConfig, verbose: bool) {
let mut command = Command::new("3dsxtool");
command
.arg(&config.target_path)
.arg(config.path_3dsx())
.arg(format!("--smdh={}", config.path_smdh()));
let romfs = config.romfs_dir();
if romfs.is_dir() {
eprintln!("Adding RomFS from {romfs}");
command.arg(format!("--romfs={romfs}"));
} else if config.romfs_dir.is_some() {
eprintln!("Could not find configured RomFS dir: {romfs}");
process::exit(1);
}
if verbose {
print_command(&command);
}
let mut process = command
.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(crate) fn link(config: &CTRConfig, run_args: &Run, verbose: bool) {
let mut command = Command::new("3dslink");
command
.arg(config.path_3dsx())
.args(run_args.get_3dslink_args())
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
if verbose {
print_command(&command);
}
let status = command.spawn().unwrap().wait().unwrap();
if !status.success() {
process::exit(status.code().unwrap_or(1));
}
}
#[derive(Default, Debug, Deserialize, PartialEq, Eq)]
pub struct CTRConfig {
authors: Option<Vec<String>>,
description: Option<String>,
icon_path: Option<Utf8PathBuf>,
#[serde(alias = "romfs-dir")]
romfs_dir: Option<Utf8PathBuf>,
#[serde(skip)]
name: String,
#[serde(skip)]
target_path: Utf8PathBuf,
#[serde(skip)]
manifest_dir: Utf8PathBuf,
}
impl CTRConfig {
pub(crate) fn path_3dsx(&self) -> Utf8PathBuf {
self.target_path.with_extension("3dsx")
}
pub(crate) fn path_smdh(&self) -> Utf8PathBuf {
self.target_path.with_extension("smdh")
}
pub(crate) fn romfs_dir(&self) -> Utf8PathBuf {
self.manifest_dir
.join(self.romfs_dir.as_deref().unwrap_or(Utf8Path::new("romfs")))
}
const DEFAULT_AUTHOR: &'static str = "Unspecified Author";
const DEFAULT_DESCRIPTION: &'static str = "Homebrew Application";
pub(crate) fn build_smdh(&self, verbose: bool) {
let description = self
.description
.as_deref()
.unwrap_or(Self::DEFAULT_DESCRIPTION);
let publisher = if let Some(authors) = self.authors.as_ref() {
authors.join(", ")
} else {
Self::DEFAULT_AUTHOR.to_string()
};
let icon_path = self.icon_path().unwrap_or_else(|err_path| {
eprintln!("Icon at {err_path} does not exist");
process::exit(1);
});
let mut command = Command::new("smdhtool");
command
.arg("--create")
.arg(&self.name)
.arg(description)
.arg(publisher)
.arg(icon_path)
.arg(self.path_smdh())
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
if verbose {
print_command(&command);
}
let mut process = command
.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));
}
}
fn icon_path(&self) -> Result<Utf8PathBuf, Utf8PathBuf> {
let path = if let Some(path) = &self.icon_path {
self.manifest_dir.join(path)
} else {
let path = self.manifest_dir.join("icon.png");
if path.exists() {
return Ok(path);
}
Utf8PathBuf::from(env::var("DEVKITPRO").unwrap())
.join("libctru")
.join("default_icon.png")
};
if path.exists() {
Ok(path)
} else {
Err(path)
}
}
}
#[derive(Ord, PartialOrd, PartialEq, Eq, Debug)]
pub struct CommitDate {
year: i32,
month: i32,
day: i32,
}
impl CommitDate {
fn parse(date: &str) -> Option<Self> {
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: 2023,
month: 5,
day: 31,
};
const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 70, 0);