1pub mod command;
2mod graph;
3
4use std::ffi::OsStr;
5use std::io::{BufRead, BufReader};
6use std::path::PathBuf;
7use std::process::{Command, ExitStatus, Stdio};
8use std::{env, fmt, io, process};
9
10use camino::{Utf8Path, Utf8PathBuf};
11use cargo_metadata::{Artifact, Message, Package, TargetKind::*};
12use rustc_version::Channel;
13use semver::Version;
14use serde::Deserialize;
15use tee::TeeReader;
16
17use crate::command::{CargoCmd, Input, Run, Test};
18use crate::graph::UnitGraph;
19
20pub fn run_cargo(input: &Input, message_format: Option<String>) -> (ExitStatus, Vec<Message>) {
26 let mut command = make_cargo_command(input, &message_format);
27
28 if input.cmd.should_compile() {
30 let libctru = if should_use_ctru_debuginfo(&command, input.verbose) {
31 "ctrud"
32 } else {
33 "ctru"
34 };
35
36 let rustflags = command
37 .get_envs()
38 .find(|(var, _)| var == &OsStr::new("RUSTFLAGS"))
39 .and_then(|(_, flags)| flags)
40 .unwrap_or_default()
41 .to_string_lossy();
42
43 let rustflags = format!("{rustflags} -l{libctru}");
44
45 command.env("RUSTFLAGS", rustflags);
46 }
47
48 if input.verbose {
49 print_command(&command);
50 }
51
52 let mut process = command.spawn().unwrap();
53 let command_stdout = process.stdout.take().unwrap();
54
55 let mut tee_reader;
56 let mut stdout_reader;
57
58 let buf_reader: &mut dyn BufRead = match (message_format, &input.cmd) {
59 (Some(_), _) |
63 (None, CargoCmd::Test(Test { doc: true, .. })) => {
67 tee_reader = BufReader::new(TeeReader::new(command_stdout, io::stdout()));
68 &mut tee_reader
69 }
70 _ => {
71 stdout_reader = BufReader::new(command_stdout);
72 &mut stdout_reader
73 }
74 };
75
76 let messages = Message::parse_stream(buf_reader)
77 .collect::<io::Result<_>>()
78 .unwrap();
79
80 (process.wait().unwrap(), messages)
81}
82
83fn should_use_ctru_debuginfo(cargo_cmd: &Command, verbose: bool) -> bool {
85 match UnitGraph::from_cargo(cargo_cmd, verbose) {
86 Ok(unit_graph) => {
87 let Some(unit) = unit_graph
88 .units
89 .iter()
90 .find(|unit| unit.target.name == "ctru_sys")
91 else {
92 eprintln!(
93 "Warning: unable to check if `ctru` debuginfo should be linked: `ctru-sys` not found"
94 );
95 return false;
96 };
97
98 let debuginfo = unit.profile.debuginfo.unwrap_or(0);
99 debuginfo > 0
100 }
101 Err(err) => {
102 eprintln!("Warning: unable to check if `ctru` debuginfo should be linked: {err}");
103 false
104 }
105 }
106}
107
108pub(crate) fn make_cargo_command(input: &Input, message_format: &Option<String>) -> Command {
113 let devkitpro =
114 env::var("DEVKITPRO").expect("DEVKITPRO is not defined as an environment variable");
115 let rustflags =
117 env::var("RUSTFLAGS").unwrap_or_default() + &format!(" -L{devkitpro}/libctru/lib");
118
119 let cargo_cmd = &input.cmd;
120
121 let mut command = cargo(&input.config);
122 command
123 .arg(cargo_cmd.subcommand_name())
124 .env("RUSTFLAGS", rustflags);
125
126 if cargo_cmd.should_compile() {
129 command
130 .arg("--target")
131 .arg("armv6k-nintendo-3ds")
132 .arg("--message-format")
133 .arg(
134 message_format
135 .as_deref()
136 .unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT),
137 );
138
139 let sysroot = find_sysroot();
140 if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() {
141 if input.verbose {
144 eprintln!("No pre-build std found, using build-std");
145 }
146
147 command.arg("-Z").arg("build-std=std,test");
150 }
151 }
152
153 if let CargoCmd::Test(test) = cargo_cmd {
154 let rustdoc_flags = std::env::var("RUSTDOCFLAGS").unwrap_or_default() + test.rustdocflags();
156 command.env("RUSTDOCFLAGS", rustdoc_flags);
157 }
158
159 command.args(cargo_cmd.cargo_args());
160
161 if let CargoCmd::Run(run) | CargoCmd::Test(Test { run_args: run, .. }) = &cargo_cmd {
162 if run.use_custom_runner() {
163 command
164 .arg("--")
165 .args(run.build_args.passthrough.exe_args());
166 }
167 }
168
169 command
170 .stdout(Stdio::piped())
171 .stdin(Stdio::inherit())
172 .stderr(Stdio::inherit());
173
174 command
175}
176
177fn cargo(config: &[String]) -> Command {
179 let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
180 let mut cmd = Command::new(cargo);
181 cmd.args(config.iter().map(|cfg| format!("--config={cfg}")));
182 cmd
183}
184
185fn print_command(command: &Command) {
186 let mut cmd_str = vec![command.get_program().to_string_lossy().to_string()];
187 cmd_str.extend(command.get_args().map(|s| s.to_string_lossy().to_string()));
188
189 eprintln!("Running command:");
190 for (k, v) in command.get_envs() {
191 let v = v.map(|v| v.to_string_lossy().to_string());
192 eprintln!(
193 " {}={} \\",
194 k.to_string_lossy(),
195 v.map_or_else(String::new, |s| shlex::try_quote(&s).unwrap().to_string())
196 );
197 }
198 eprintln!(
199 " {}\n",
200 shlex::try_join(cmd_str.iter().map(String::as_str)).unwrap()
201 );
202}
203
204pub(crate) fn find_sysroot() -> PathBuf {
206 let sysroot = env::var("SYSROOT").ok().unwrap_or_else(|| {
207 let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
208
209 let output = Command::new(&rustc)
210 .arg("--print")
211 .arg("sysroot")
212 .output()
213 .unwrap_or_else(|_| panic!("Failed to run `{rustc} -- print sysroot`"));
214 String::from_utf8(output.stdout).expect("Failed to parse sysroot path into a UTF-8 string")
215 });
216
217 PathBuf::from(sysroot.trim())
218}
219
220pub fn check_rust_version(input: &Input) {
223 let rustc_version = rustc_version::version_meta().unwrap();
224
225 if rustc_version.channel > Channel::Nightly && input.cmd.should_compile() {
228 eprintln!("building with cargo-3ds requires a nightly rustc version.");
229 eprintln!(
230 "Please run `rustup override set nightly` to use nightly in the \
231 current directory, or use `cargo +nightly 3ds` to use it for a \
232 single invocation."
233 );
234 process::exit(1);
235 }
236
237 let old_version = MINIMUM_RUSTC_VERSION
238 > Version {
239 pre: semver::Prerelease::EMPTY,
241 ..rustc_version.semver.clone()
242 };
243
244 let old_commit = match rustc_version.commit_date {
245 None => false,
246 Some(date) => {
247 MINIMUM_COMMIT_DATE
248 > CommitDate::parse(&date).expect("could not parse `rustc --version` commit date")
249 }
250 };
251
252 if old_version || old_commit {
253 eprintln!("cargo-3ds requires rustc nightly version >= {MINIMUM_COMMIT_DATE}");
254 eprintln!("Please run `rustup update nightly` to upgrade your nightly version");
255
256 process::exit(1);
257 }
258}
259
260pub(crate) fn get_artifact_config(package: Package, artifact: Artifact) -> CTRConfig {
264 let name = match artifact.target.kind[0] {
267 Bin | Lib | RLib | DyLib if artifact.profile.test => {
268 format!("{} tests", artifact.target.name)
269 }
270 Example => {
271 format!("{} - {} example", artifact.target.name, package.name)
272 }
273 _ => artifact.target.name,
274 };
275
276 let config = package
280 .metadata
281 .get("cargo-3ds")
282 .and_then(|c| CTRConfig::deserialize(c).ok())
283 .unwrap_or_default();
284
285 CTRConfig {
286 name,
287 authors: config.authors.or(Some(package.authors)),
288 description: config.description.or(package.description),
289 manifest_dir: package.manifest_path.parent().unwrap().into(),
290 target_path: artifact.executable.unwrap(),
291 ..config
292 }
293}
294
295pub(crate) fn build_3dsx(config: &CTRConfig, verbose: bool) {
298 let mut command = Command::new("3dsxtool");
299 command
300 .arg(&config.target_path)
301 .arg(config.path_3dsx())
302 .arg(format!("--smdh={}", config.path_smdh()));
303
304 let romfs = config.romfs_dir();
305 if romfs.is_dir() {
306 eprintln!("Adding RomFS from {romfs}");
307 command.arg(format!("--romfs={romfs}"));
308 } else if config.romfs_dir.is_some() {
309 eprintln!("Could not find configured RomFS dir: {romfs}");
310 process::exit(1);
311 }
312
313 if verbose {
314 print_command(&command);
315 }
316
317 let mut process = command
318 .stdin(Stdio::inherit())
319 .stdout(Stdio::inherit())
320 .stderr(Stdio::inherit())
321 .spawn()
322 .expect("3dsxtool command failed, most likely due to '3dsxtool' not being in $PATH");
323
324 let status = process.wait().unwrap();
325
326 if !status.success() {
327 process::exit(status.code().unwrap_or(1));
328 }
329}
330
331pub(crate) fn link(config: &CTRConfig, run_args: &Run, verbose: bool) {
334 let mut command = Command::new("3dslink");
335 command
336 .arg(config.path_3dsx())
337 .args(run_args.get_3dslink_args())
338 .stdin(Stdio::inherit())
339 .stdout(Stdio::inherit())
340 .stderr(Stdio::inherit());
341
342 if verbose {
343 print_command(&command);
344 }
345
346 let status = command.spawn().unwrap().wait().unwrap();
347
348 if !status.success() {
349 process::exit(status.code().unwrap_or(1));
350 }
351}
352
353#[derive(Default, Debug, Deserialize, PartialEq, Eq)]
354pub struct CTRConfig {
355 authors: Option<Vec<String>>,
359
360 description: Option<String>,
366
367 icon_path: Option<Utf8PathBuf>,
370
371 #[serde(alias = "romfs-dir")]
375 romfs_dir: Option<Utf8PathBuf>,
376
377 #[serde(skip)]
381 name: String,
382 #[serde(skip)]
383 target_path: Utf8PathBuf,
384 #[serde(skip)]
385 manifest_dir: Utf8PathBuf,
386}
387
388impl CTRConfig {
389 pub(crate) fn path_3dsx(&self) -> Utf8PathBuf {
391 self.target_path.with_extension("3dsx")
392 }
393
394 pub(crate) fn path_smdh(&self) -> Utf8PathBuf {
396 self.target_path.with_extension("smdh")
397 }
398
399 pub(crate) fn romfs_dir(&self) -> Utf8PathBuf {
401 self.manifest_dir
402 .join(self.romfs_dir.as_deref().unwrap_or(Utf8Path::new("romfs")))
403 }
404
405 const DEFAULT_AUTHOR: &'static str = "Unspecified Author";
407 const DEFAULT_DESCRIPTION: &'static str = "Homebrew Application";
408
409 pub(crate) fn build_smdh(&self, verbose: bool) {
412 let description = self
413 .description
414 .as_deref()
415 .unwrap_or(Self::DEFAULT_DESCRIPTION);
416
417 let publisher = if let Some(authors) = self.authors.as_ref() {
418 authors.join(", ")
419 } else {
420 Self::DEFAULT_AUTHOR.to_string()
421 };
422
423 let icon_path = self.icon_path().unwrap_or_else(|err_path| {
424 eprintln!("Icon at {err_path} does not exist");
425 process::exit(1);
426 });
427
428 let mut command = Command::new("smdhtool");
429 command
430 .arg("--create")
431 .arg(&self.name)
432 .arg(description)
433 .arg(publisher)
434 .arg(icon_path)
435 .arg(self.path_smdh())
436 .stdin(Stdio::inherit())
437 .stdout(Stdio::inherit())
438 .stderr(Stdio::inherit());
439
440 if verbose {
441 print_command(&command);
442 }
443
444 let mut process = command
445 .spawn()
446 .expect("smdhtool command failed, most likely due to 'smdhtool' not being in $PATH");
447
448 let status = process.wait().unwrap();
449
450 if !status.success() {
451 process::exit(status.code().unwrap_or(1));
452 }
453 }
454
455 fn icon_path(&self) -> Result<Utf8PathBuf, Utf8PathBuf> {
462 let path = if let Some(path) = &self.icon_path {
463 self.manifest_dir.join(path)
464 } else {
465 let path = self.manifest_dir.join("icon.png");
466 if path.exists() {
467 return Ok(path);
468 }
469
470 Utf8PathBuf::from(env::var("DEVKITPRO").unwrap())
471 .join("libctru")
472 .join("default_icon.png")
473 };
474
475 if path.exists() { Ok(path) } else { Err(path) }
476 }
477}
478
479#[derive(Ord, PartialOrd, PartialEq, Eq, Debug)]
480pub struct CommitDate {
481 year: i32,
482 month: i32,
483 day: i32,
484}
485
486impl CommitDate {
487 fn parse(date: &str) -> Option<Self> {
488 let mut iter = date.split('-');
489
490 let year = iter.next()?.parse().ok()?;
491 let month = iter.next()?.parse().ok()?;
492 let day = iter.next()?.parse().ok()?;
493
494 Some(Self { year, month, day })
495 }
496}
497
498impl fmt::Display for CommitDate {
499 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
500 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
501 }
502}
503
504const MINIMUM_COMMIT_DATE: CommitDate = CommitDate {
505 year: 2023,
506 month: 5,
507 day: 31,
508};
509const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 70, 0);