├── .gitignore ├── src ├── main.rs ├── config.rs ├── graph.rs ├── lib.rs └── command.rs ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── LICENSE-APACHE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, process}; 2 | 3 | use cargo_nds::command::Cargo; 4 | use cargo_nds::{check_rust_version, run_cargo}; 5 | use clap::Parser; 6 | 7 | fn main() { 8 | check_rust_version(); 9 | let Cargo::Input(mut input) = Cargo::parse(); 10 | 11 | let message_format = match input.cmd.extract_message_format() { 12 | Ok(fmt) => fmt, 13 | Err(msg) => { 14 | eprintln!("{msg}"); 15 | process::exit(1) 16 | } 17 | }; 18 | 19 | let (status, messages) = run_cargo(&input, message_format); 20 | 21 | if !status.success() { 22 | process::exit(status.code().unwrap_or(1)); 23 | } 24 | 25 | input.cmd.run_callback(&messages); 26 | } 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-nds" 3 | version = "0.1.2" 4 | authors = ["Rustnds Org", "Andrea Ciliberti "] 5 | description = "Cargo wrapper for developing Nintendo nds homebrew apps" 6 | repository = "https://github.com/rustnds/cargo-nds" 7 | keywords = ["nds", "homebrew"] 8 | categories = ["command-line-utilities", "development-tools::cargo-plugins"] 9 | exclude = [".github"] 10 | license = "MIT OR Apache-2.0" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | cargo_metadata = "0.18.1" 15 | rustc_version = "0.4.0" 16 | semver = "1.0.10" 17 | serde = { version = "1.0.139", features = ["derive"] } 18 | tee = "0.1.0" 19 | toml = "0.8.12" 20 | clap = { version = "4.0.15", features = ["derive", "wrap_help"] } 21 | shlex = "1.1.0" 22 | serde_json = "1.0.108" 23 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::io::ErrorKind; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::NDSConfig; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Default)] 8 | pub struct Config { 9 | pub name: [Option; 3], 10 | pub icon: Option, 11 | } 12 | 13 | impl Config { 14 | pub fn try_load(nds_config: &NDSConfig) -> std::io::Result { 15 | let mut path = nds_config.cargo_manifest_path.clone(); 16 | path.pop(); 17 | path.push("nds.toml"); 18 | 19 | match std::fs::exists(&path) { 20 | Ok(true) => {} 21 | Ok(false) => return Ok(Self::default()), 22 | Err(e) => return Err(e), 23 | } 24 | 25 | let config = std::fs::read_to_string(&path)?; 26 | 27 | let config: Config = toml::from_str(&config) 28 | .map_err(|e| std::io::Error::new(ErrorKind::Other, e.message()))?; 29 | 30 | Ok(config) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/graph.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::io::Read; 3 | use std::process::{Command, Stdio}; 4 | 5 | use cargo_metadata::Target; 6 | use serde::Deserialize; 7 | 8 | use crate::print_command; 9 | 10 | /// In lieu of 11 | /// and to avoid pulling in the real `cargo` 12 | /// [data structures](https://docs.rs/cargo/latest/cargo/core/compiler/unit_graph/type.UnitGraph.html) 13 | /// as a dependency, we define the subset of the build graph we care about. 14 | #[derive(Deserialize)] 15 | pub struct UnitGraph { 16 | pub version: i32, 17 | pub units: Vec, 18 | } 19 | 20 | impl UnitGraph { 21 | /// Collect the unit graph via Cargo's `--unit-graph` flag. 22 | /// This runs the same command as the actual build, except nothing is actually 23 | /// build and the graph is output instead. 24 | /// 25 | /// See . 26 | pub fn from_cargo(cargo_cmd: &Command, verbose: bool) -> Result> { 27 | // Since Command isn't Clone, copy it "by hand", by copying its args and envs 28 | let mut cmd = Command::new(cargo_cmd.get_program()); 29 | 30 | let mut args = cargo_cmd.get_args(); 31 | cmd.args(args.next()) 32 | // These options must be added before any possible `--`, so the best 33 | // place is to just stick them immediately after the first arg (subcommand) 34 | .args(["-Z", "unstable-options", "--unit-graph"]) 35 | .args(args) 36 | .envs(cargo_cmd.get_envs().filter_map(|(k, v)| Some((k, v?)))) 37 | .stdout(Stdio::piped()) 38 | .stderr(Stdio::piped()); 39 | 40 | if verbose { 41 | print_command(&cmd); 42 | } 43 | 44 | let mut proc = cmd.spawn()?; 45 | let stdout = proc.stdout.take().unwrap(); 46 | let mut stderr = proc.stderr.take().unwrap(); 47 | 48 | let result: Self = serde_json::from_reader(stdout).map_err(|err| { 49 | let mut stderr_str = String::new(); 50 | let _ = stderr.read_to_string(&mut stderr_str); 51 | 52 | let _ = proc.wait(); 53 | format!("unable to parse `--unit-graph` json: {err}\nstderr: `{stderr_str}`") 54 | })?; 55 | 56 | let _status = proc.wait()?; 57 | // TODO: with cargo 1.74.0-nightly (b4ddf95ad 2023-09-18), 58 | // `cargo run --unit-graph` panics at src/cargo/ops/cargo_run.rs:83:5 59 | // It seems to have been fixed as of cargo 1.76.0-nightly (71cd3a926 2023-11-20) 60 | // so maybe we can stop ignoring it once we bump the minimum toolchain version, 61 | // and certainly we should once `--unit-graph` is ever stabilized. 62 | // 63 | // if !status.success() { 64 | // return Err(format!("`cargo --unit-graph` exited with status {status:?}").into()); 65 | // } 66 | 67 | if result.version == 1 { 68 | Ok(result) 69 | } else { 70 | Err(format!( 71 | "unknown `cargo --unit-graph` output version {}", 72 | result.version 73 | ))? 74 | } 75 | } 76 | } 77 | 78 | #[derive(Deserialize)] 79 | pub struct Unit { 80 | pub target: Target, 81 | pub profile: Profile, 82 | } 83 | 84 | /// This struct is very similar to [`cargo_metadata::ArtifactProfile`], but seems 85 | /// to have some slight differences so we define a different version. We only 86 | /// really care about `debuginfo` anyway. 87 | #[derive(Deserialize)] 88 | pub struct Profile { 89 | pub debuginfo: Option, 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-nds 2 | 3 | cargo-nds is a Cargo command to work with Nintendo DS project binaries. Based on cargo-3ds (https://github.com/rust3ds/cargo-3ds). 4 | 5 | Relies on libnds-sys as a dependency https://github.com/SeleDreams/libnds-sys 6 | 7 | ## Installation 8 | 9 | To install the current `master` version of `cargo-nds`: 10 | 11 | ```sh 12 | cargo install --git https://github.com/SeleDreams/cargo-nds.git 13 | ``` 14 | Before attempting to use it, make sure you installed the BlocksDS toolchain ! 15 | 16 | Follow the installation instructions available here : https://blocksds.github.io/docs/setup/options/ 17 | 18 | You will need to set the WONDERFUL_TOOLCHAIN and BLOCKSDS environment variables and have the directory of arm-none-eabi-gcc as well as ndstool in your PATH 19 | 20 | arm-none-eabi-gcc is located at $WONDERFUL_TOOLCHAIN/toolchain/gcc-arm-none-eabi/bin 21 | 22 | ndstool is located at $BLOCKSDS/tools/ndstool 23 | 24 | to use ndslink, please check this repository https://github.com/devkitPro/install-dsilink 25 | 26 | ## Usage 27 | 28 | https://github.com/SeleDreams/cargo-nds/assets/16335601/a0be4450-d253-4dd4-9dca-71adfe489de5 29 | 30 | Use the nightly toolchain to build DS apps (either by using `rustup override nightly` for the project directory or by adding `+nightly` in the `cargo` invocation). 31 | 32 | ```txt 33 | Commands: 34 | build 35 | Builds an executable suitable to run on a DS (nds) 36 | run 37 | Builds an executable and sends it to a device with `dslink` 38 | test 39 | Builds a test executable and sends it to a device with `dslink` 40 | new 41 | Sets up a new cargo project suitable to run on a DS 42 | help 43 | Print this message or the help of the given subcommand(s) 44 | 45 | Options: 46 | -h, --help 47 | Print help information (use `-h` for a summary) 48 | 49 | -V, --version 50 | Print version information 51 | ``` 52 | 53 | Additional arguments will be passed through to the given subcommand. 54 | See [passthrough arguments](#passthrough-arguments) for more details. 55 | 56 | It is also possible to pass any other `cargo` command (e.g. `doc`, `check`), 57 | and all its arguments will be passed through directly to `cargo` unmodified, 58 | with the proper `--target armv5te-nintendo-ds.json` set. 59 | 60 | ### Basic Examples 61 | 62 | * `cargo nds build` 63 | * `cargo nds check --verbose` 64 | * `cargo nds run --release --example foo` 65 | * `cargo nds test --no-run` 66 | * `cargo nds new my-new-project --edition 2021` 67 | * `cargo nds init .` 68 | ### Running executables 69 | 70 | `cargo nds test` and `cargo nds run` use the `dslink` tool to send built 71 | executables to a device. 72 | 73 | ### Caveats 74 | 75 | Due to the fact that only one executable at a time can be sent with `dslink`, 76 | by default only the "last" executable built will be used. If a `test` or `run` 77 | command builds more than one binary, you may need to filter it in order to run 78 | the executable you want. 79 | 80 | Doc tests sort of work, but `cargo-nds` uses a number of unstable cargo and 81 | rustdoc features to make them work, so the output won't be as pretty and will 82 | require some manual workarounds to actually run the tests and see output from them. 83 | For now, `cargo nds test --doc` will not build a nds file or use `dslink` at all. 84 | 85 | For the time being, only arm9 homebrews can be built. I am still thinking about the best way to integrate arm7 support to the workflow. 86 | 87 | The default arm7 binary of blocksds will be bundled in the nds file. 88 | 89 | ## License 90 | 91 | This project is distributed under the MIT license or the Apache-2.0 license. 92 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "utf8parse", 17 | ] 18 | 19 | [[package]] 20 | name = "anstyle" 21 | version = "1.0.4" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 24 | 25 | [[package]] 26 | name = "anstyle-parse" 27 | version = "0.2.2" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 30 | dependencies = [ 31 | "utf8parse", 32 | ] 33 | 34 | [[package]] 35 | name = "anstyle-query" 36 | version = "1.0.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 39 | dependencies = [ 40 | "windows-sys 0.48.0", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-wincon" 45 | version = "3.0.1" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 48 | dependencies = [ 49 | "anstyle", 50 | "windows-sys 0.48.0", 51 | ] 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "2.4.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 58 | 59 | [[package]] 60 | name = "camino" 61 | version = "1.1.6" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" 64 | dependencies = [ 65 | "serde", 66 | ] 67 | 68 | [[package]] 69 | name = "cargo-nds" 70 | version = "0.1.2" 71 | dependencies = [ 72 | "cargo_metadata", 73 | "clap", 74 | "rustc_version", 75 | "semver", 76 | "serde", 77 | "serde_json", 78 | "shlex", 79 | "tee", 80 | "toml", 81 | ] 82 | 83 | [[package]] 84 | name = "cargo-platform" 85 | version = "0.1.5" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff" 88 | dependencies = [ 89 | "serde", 90 | ] 91 | 92 | [[package]] 93 | name = "cargo_metadata" 94 | version = "0.18.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" 97 | dependencies = [ 98 | "camino", 99 | "cargo-platform", 100 | "semver", 101 | "serde", 102 | "serde_json", 103 | "thiserror", 104 | ] 105 | 106 | [[package]] 107 | name = "clap" 108 | version = "4.4.10" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" 111 | dependencies = [ 112 | "clap_builder", 113 | "clap_derive", 114 | ] 115 | 116 | [[package]] 117 | name = "clap_builder" 118 | version = "4.4.9" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" 121 | dependencies = [ 122 | "anstream", 123 | "anstyle", 124 | "clap_lex", 125 | "strsim", 126 | "terminal_size", 127 | ] 128 | 129 | [[package]] 130 | name = "clap_derive" 131 | version = "4.4.7" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 134 | dependencies = [ 135 | "heck", 136 | "proc-macro2", 137 | "quote", 138 | "syn", 139 | ] 140 | 141 | [[package]] 142 | name = "clap_lex" 143 | version = "0.6.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 146 | 147 | [[package]] 148 | name = "colorchoice" 149 | version = "1.0.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 152 | 153 | [[package]] 154 | name = "equivalent" 155 | version = "1.0.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 158 | 159 | [[package]] 160 | name = "errno" 161 | version = "0.3.8" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 164 | dependencies = [ 165 | "libc", 166 | "windows-sys 0.52.0", 167 | ] 168 | 169 | [[package]] 170 | name = "hashbrown" 171 | version = "0.14.5" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 174 | 175 | [[package]] 176 | name = "heck" 177 | version = "0.4.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 180 | 181 | [[package]] 182 | name = "indexmap" 183 | version = "2.2.6" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 186 | dependencies = [ 187 | "equivalent", 188 | "hashbrown", 189 | ] 190 | 191 | [[package]] 192 | name = "itoa" 193 | version = "1.0.9" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 196 | 197 | [[package]] 198 | name = "libc" 199 | version = "0.2.150" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 202 | 203 | [[package]] 204 | name = "linux-raw-sys" 205 | version = "0.4.11" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" 208 | 209 | [[package]] 210 | name = "memchr" 211 | version = "2.7.2" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 214 | 215 | [[package]] 216 | name = "proc-macro2" 217 | version = "1.0.81" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 220 | dependencies = [ 221 | "unicode-ident", 222 | ] 223 | 224 | [[package]] 225 | name = "quote" 226 | version = "1.0.36" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 229 | dependencies = [ 230 | "proc-macro2", 231 | ] 232 | 233 | [[package]] 234 | name = "rustc_version" 235 | version = "0.4.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 238 | dependencies = [ 239 | "semver", 240 | ] 241 | 242 | [[package]] 243 | name = "rustix" 244 | version = "0.38.25" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" 247 | dependencies = [ 248 | "bitflags", 249 | "errno", 250 | "libc", 251 | "linux-raw-sys", 252 | "windows-sys 0.48.0", 253 | ] 254 | 255 | [[package]] 256 | name = "ryu" 257 | version = "1.0.15" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 260 | 261 | [[package]] 262 | name = "semver" 263 | version = "1.0.20" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" 266 | dependencies = [ 267 | "serde", 268 | ] 269 | 270 | [[package]] 271 | name = "serde" 272 | version = "1.0.193" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 275 | dependencies = [ 276 | "serde_derive", 277 | ] 278 | 279 | [[package]] 280 | name = "serde_derive" 281 | version = "1.0.193" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 284 | dependencies = [ 285 | "proc-macro2", 286 | "quote", 287 | "syn", 288 | ] 289 | 290 | [[package]] 291 | name = "serde_json" 292 | version = "1.0.108" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 295 | dependencies = [ 296 | "itoa", 297 | "ryu", 298 | "serde", 299 | ] 300 | 301 | [[package]] 302 | name = "serde_spanned" 303 | version = "0.6.5" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 306 | dependencies = [ 307 | "serde", 308 | ] 309 | 310 | [[package]] 311 | name = "shlex" 312 | version = "1.2.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" 315 | 316 | [[package]] 317 | name = "strsim" 318 | version = "0.10.0" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 321 | 322 | [[package]] 323 | name = "syn" 324 | version = "2.0.60" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 327 | dependencies = [ 328 | "proc-macro2", 329 | "quote", 330 | "unicode-ident", 331 | ] 332 | 333 | [[package]] 334 | name = "tee" 335 | version = "0.1.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "37c12559dba7383625faaff75be24becf35bfc885044375bcab931111799a3da" 338 | 339 | [[package]] 340 | name = "terminal_size" 341 | version = "0.3.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 344 | dependencies = [ 345 | "rustix", 346 | "windows-sys 0.48.0", 347 | ] 348 | 349 | [[package]] 350 | name = "thiserror" 351 | version = "1.0.59" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" 354 | dependencies = [ 355 | "thiserror-impl", 356 | ] 357 | 358 | [[package]] 359 | name = "thiserror-impl" 360 | version = "1.0.59" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" 363 | dependencies = [ 364 | "proc-macro2", 365 | "quote", 366 | "syn", 367 | ] 368 | 369 | [[package]] 370 | name = "toml" 371 | version = "0.8.12" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" 374 | dependencies = [ 375 | "serde", 376 | "serde_spanned", 377 | "toml_datetime", 378 | "toml_edit", 379 | ] 380 | 381 | [[package]] 382 | name = "toml_datetime" 383 | version = "0.6.5" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 386 | dependencies = [ 387 | "serde", 388 | ] 389 | 390 | [[package]] 391 | name = "toml_edit" 392 | version = "0.22.12" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" 395 | dependencies = [ 396 | "indexmap", 397 | "serde", 398 | "serde_spanned", 399 | "toml_datetime", 400 | "winnow", 401 | ] 402 | 403 | [[package]] 404 | name = "unicode-ident" 405 | version = "1.0.12" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 408 | 409 | [[package]] 410 | name = "utf8parse" 411 | version = "0.2.1" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 414 | 415 | [[package]] 416 | name = "windows-sys" 417 | version = "0.48.0" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 420 | dependencies = [ 421 | "windows-targets 0.48.5", 422 | ] 423 | 424 | [[package]] 425 | name = "windows-sys" 426 | version = "0.52.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 429 | dependencies = [ 430 | "windows-targets 0.52.0", 431 | ] 432 | 433 | [[package]] 434 | name = "windows-targets" 435 | version = "0.48.5" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 438 | dependencies = [ 439 | "windows_aarch64_gnullvm 0.48.5", 440 | "windows_aarch64_msvc 0.48.5", 441 | "windows_i686_gnu 0.48.5", 442 | "windows_i686_msvc 0.48.5", 443 | "windows_x86_64_gnu 0.48.5", 444 | "windows_x86_64_gnullvm 0.48.5", 445 | "windows_x86_64_msvc 0.48.5", 446 | ] 447 | 448 | [[package]] 449 | name = "windows-targets" 450 | version = "0.52.0" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 453 | dependencies = [ 454 | "windows_aarch64_gnullvm 0.52.0", 455 | "windows_aarch64_msvc 0.52.0", 456 | "windows_i686_gnu 0.52.0", 457 | "windows_i686_msvc 0.52.0", 458 | "windows_x86_64_gnu 0.52.0", 459 | "windows_x86_64_gnullvm 0.52.0", 460 | "windows_x86_64_msvc 0.52.0", 461 | ] 462 | 463 | [[package]] 464 | name = "windows_aarch64_gnullvm" 465 | version = "0.48.5" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 468 | 469 | [[package]] 470 | name = "windows_aarch64_gnullvm" 471 | version = "0.52.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 474 | 475 | [[package]] 476 | name = "windows_aarch64_msvc" 477 | version = "0.48.5" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 480 | 481 | [[package]] 482 | name = "windows_aarch64_msvc" 483 | version = "0.52.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 486 | 487 | [[package]] 488 | name = "windows_i686_gnu" 489 | version = "0.48.5" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 492 | 493 | [[package]] 494 | name = "windows_i686_gnu" 495 | version = "0.52.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 498 | 499 | [[package]] 500 | name = "windows_i686_msvc" 501 | version = "0.48.5" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 504 | 505 | [[package]] 506 | name = "windows_i686_msvc" 507 | version = "0.52.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 510 | 511 | [[package]] 512 | name = "windows_x86_64_gnu" 513 | version = "0.48.5" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 516 | 517 | [[package]] 518 | name = "windows_x86_64_gnu" 519 | version = "0.52.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 522 | 523 | [[package]] 524 | name = "windows_x86_64_gnullvm" 525 | version = "0.48.5" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 528 | 529 | [[package]] 530 | name = "windows_x86_64_gnullvm" 531 | version = "0.52.0" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 534 | 535 | [[package]] 536 | name = "windows_x86_64_msvc" 537 | version = "0.48.5" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 540 | 541 | [[package]] 542 | name = "windows_x86_64_msvc" 543 | version = "0.52.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 546 | 547 | [[package]] 548 | name = "winnow" 549 | version = "0.6.7" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" 552 | dependencies = [ 553 | "memchr", 554 | ] 555 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | mod config; 3 | mod graph; 4 | 5 | use core::fmt; 6 | use std::ffi::OsStr; 7 | use std::io::{BufRead, BufReader}; 8 | use std::path::{Path, PathBuf}; 9 | use std::process::{Command, ExitStatus, Stdio}; 10 | use std::{env, io, process}; 11 | 12 | use cargo_metadata::{Message, MetadataCommand}; 13 | use command::{Input, Test}; 14 | use config::Config; 15 | use rustc_version::Channel; 16 | use semver::Version; 17 | use tee::TeeReader; 18 | 19 | use crate::command::{CargoCmd, Run}; 20 | use crate::graph::UnitGraph; 21 | 22 | /// Build a command using [`make_cargo_build_command`] and execute it, 23 | /// parsing and returning the messages from the spawned process. 24 | /// 25 | /// For commands that produce an executable output, this function will build the 26 | /// `.elf` binary that can be used to create other nds files. 27 | pub fn run_cargo(input: &Input, message_format: Option) -> (ExitStatus, Vec) { 28 | let mut command = make_cargo_command(input, &message_format); 29 | 30 | if input.verbose { 31 | print_command(&command); 32 | } 33 | 34 | let mut process = command.spawn().unwrap(); 35 | let command_stdout = process.stdout.take().unwrap(); 36 | 37 | let mut tee_reader; 38 | let mut stdout_reader; 39 | 40 | let buf_reader: &mut dyn BufRead = match (message_format, &input.cmd) { 41 | // The user presumably cares about the message format if set, so we should 42 | // copy stuff to stdout like they expect. We can still extract the executable 43 | // information out of it that we need for ndstool etc. 44 | (Some(_), _) | 45 | // Rustdoc unfortunately prints to stdout for compile errors, so 46 | // we also use a tee when building doc tests too. 47 | // Possibly related: https://github.com/rust-lang/rust/issues/75135 48 | (None, CargoCmd::Test(Test { doc: true, .. })) => { 49 | tee_reader = BufReader::new(TeeReader::new(command_stdout, io::stdout())); 50 | &mut tee_reader 51 | } 52 | _ => { 53 | stdout_reader = BufReader::new(command_stdout); 54 | &mut stdout_reader 55 | } 56 | }; 57 | 58 | let messages = Message::parse_stream(buf_reader) 59 | .collect::>() 60 | .unwrap(); 61 | 62 | (process.wait().unwrap(), messages) 63 | } 64 | 65 | /// Create a cargo command based on the context. 66 | /// 67 | /// For "build" commands (which compile code, such as `cargo nds build` or `cargo nds clippy`), 68 | /// if there is no pre-built std detected in the sysroot, `build-std` will be used instead. 69 | pub fn make_cargo_command(input: &Input, message_format: &Option) -> Command { 70 | let blocksds = 71 | env::var("BLOCKSDS").unwrap_or("/opt/wonderful/thirdparty/blocksds/core".to_owned()); 72 | let rustflags = format!("-C link-args=-specs={blocksds}/sys/crts/ds_arm9.specs"); 73 | 74 | let cargo_cmd = &input.cmd; 75 | 76 | let mut command = cargo(&input.config); 77 | command 78 | .arg(cargo_cmd.subcommand_name()) 79 | .env("RUSTFLAGS", rustflags); 80 | 81 | // Any command that needs to compile code will run under this environment. 82 | // Even `clippy` and `check` need this kind of context, so we'll just assume any other `Passthrough` command uses it too. 83 | if cargo_cmd.should_compile() { 84 | command 85 | .arg("--target") 86 | .arg("armv5te-nintendo-ds.json") 87 | .arg("-Z") 88 | .arg("build-std=core,alloc") 89 | .arg("--message-format") 90 | .arg( 91 | message_format 92 | .as_deref() 93 | .unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT), 94 | ); 95 | } 96 | 97 | if let CargoCmd::Test(test) = cargo_cmd { 98 | // RUSTDOCFLAGS is simply ignored if --doc wasn't passed, so we always set it. 99 | let rustdoc_flags = std::env::var("RUSTDOCFLAGS").unwrap_or_default() + test.rustdocflags(); 100 | command.env("RUSTDOCFLAGS", rustdoc_flags); 101 | } 102 | 103 | command.args(cargo_cmd.cargo_args()); 104 | 105 | if let CargoCmd::Run(run) | CargoCmd::Test(Test { run_args: run, .. }) = &cargo_cmd { 106 | if run.use_custom_runner() { 107 | command 108 | .arg("--") 109 | .args(run.build_args.passthrough.exe_args()); 110 | } 111 | } 112 | 113 | command 114 | .stdout(Stdio::piped()) 115 | .stdin(Stdio::inherit()) 116 | .stderr(Stdio::inherit()); 117 | 118 | command 119 | } 120 | 121 | /// Build a `cargo` command with the given `--config` flags. 122 | fn cargo(config: &[String]) -> Command { 123 | let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); 124 | let mut cmd = Command::new(cargo); 125 | cmd.args(config.iter().map(|cfg| format!("--config={cfg}"))); 126 | cmd 127 | } 128 | 129 | fn print_command(command: &Command) { 130 | let mut cmd_str = vec![command.get_program().to_string_lossy().to_string()]; 131 | cmd_str.extend(command.get_args().map(|s| s.to_string_lossy().to_string())); 132 | 133 | eprintln!("Running command:"); 134 | for (k, v) in command.get_envs() { 135 | let v = v.map(|v| v.to_string_lossy().to_string()); 136 | eprintln!( 137 | " {}={} \\", 138 | k.to_string_lossy(), 139 | v.map_or_else(String::new, |s| shlex::quote(&s).to_string()) 140 | ); 141 | } 142 | eprintln!(" {}\n", shlex::join(cmd_str.iter().map(String::as_str))); 143 | } 144 | 145 | /// Finds the sysroot path of the current toolchain 146 | pub fn find_sysroot() -> PathBuf { 147 | let sysroot = env::var("SYSROOT").ok().unwrap_or_else(|| { 148 | let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); 149 | 150 | let output = Command::new(&rustc) 151 | .arg("--print") 152 | .arg("sysroot") 153 | .output() 154 | .unwrap_or_else(|_| panic!("Failed to run `{rustc} -- print sysroot`")); 155 | String::from_utf8(output.stdout).expect("Failed to parse sysroot path into a UTF-8 string") 156 | }); 157 | 158 | PathBuf::from(sysroot.trim()) 159 | } 160 | 161 | /// Checks the current rust version and channel. 162 | /// Exits if the minimum requirement is not met. 163 | pub fn check_rust_version() { 164 | let rustc_version = rustc_version::version_meta().unwrap(); 165 | 166 | if rustc_version.channel > Channel::Nightly { 167 | eprintln!("cargo-nds requires a nightly rustc version."); 168 | eprintln!( 169 | "Please run `rustup override set nightly` to use nightly in the \ 170 | current directory, or use `cargo +nightly nds` to use it for a \ 171 | single invocation." 172 | ); 173 | process::exit(1); 174 | } 175 | 176 | let old_version = MINIMUM_RUSTC_VERSION 177 | > Version { 178 | // Remove `-nightly` pre-release tag for comparison. 179 | pre: semver::Prerelease::EMPTY, 180 | ..rustc_version.semver.clone() 181 | }; 182 | 183 | let old_commit = match rustc_version.commit_date { 184 | None => false, 185 | Some(date) => { 186 | MINIMUM_COMMIT_DATE 187 | > CommitDate::parse(&date).expect("could not parse `rustc --version` commit date") 188 | } 189 | }; 190 | 191 | if old_version || old_commit { 192 | eprintln!("cargo-nds requires rustc nightly version >= {MINIMUM_COMMIT_DATE}"); 193 | eprintln!("Please run `rustup update nightly` to upgrade your nightly version"); 194 | 195 | process::exit(1); 196 | } 197 | } 198 | 199 | /// Parses messages returned by "build" cargo commands (such as `cargo nds build` or `cargo nds run`). 200 | /// The returned [`CTRConfig`] is then used for further building in and execution 201 | /// in [`build_nds`], and [`link`]. 202 | pub fn get_metadata(messages: &[Message]) -> NDSConfig { 203 | let metadata = MetadataCommand::new() 204 | .no_deps() 205 | .exec() 206 | .expect("Failed to get cargo metadata"); 207 | 208 | let mut package = None; 209 | let mut artifact = None; 210 | 211 | // Extract the final built executable. We may want to fail in cases where 212 | // multiple executables, or none, were built? 213 | for message in messages.iter().rev() { 214 | if let Message::CompilerArtifact(art) = message { 215 | if art.executable.is_some() { 216 | package = Some(metadata[&art.package_id].clone()); 217 | artifact = Some(art.clone()); 218 | 219 | break; 220 | } 221 | } 222 | } 223 | if package.is_none() || artifact.is_none() { 224 | eprintln!("No executable found from build command output!"); 225 | process::exit(1); 226 | } 227 | 228 | let (package, artifact) = (package.unwrap(), artifact.unwrap()); 229 | 230 | let mut icon = String::from("./icon.bmp"); 231 | 232 | if !Path::new(&icon).exists() { 233 | icon = format!("{}/sys/icon.bmp", env::var("BLOCKSDS").unwrap()); 234 | } 235 | 236 | // for now assume a single "kind" since we only support one output artifact 237 | let name = match artifact.target.kind[0].as_ref() { 238 | "bin" | "lib" | "rlib" | "dylib" if artifact.target.test => { 239 | format!("{} tests", artifact.target.name) 240 | } 241 | "example" => { 242 | format!("{} - {} example", artifact.target.name, package.name) 243 | } 244 | _ => artifact.target.name, 245 | }; 246 | 247 | let author = match package.authors.as_slice() { 248 | [name, ..] => name.clone(), 249 | [] => String::from("Unspecified Author"), // as standard with the devkitPRO toolchain 250 | }; 251 | 252 | NDSConfig { 253 | name: name, 254 | author: author, 255 | description: package 256 | .description 257 | .clone() 258 | .unwrap_or_else(|| String::from("Homebrew Application")), 259 | icon: icon, 260 | target_path: artifact.executable.unwrap().into(), 261 | cargo_manifest_path: package.manifest_path.into(), 262 | } 263 | } 264 | 265 | /// Builds the nds using `ndstool`. 266 | /// This will fail if `ndstool` is not within the running directory or in a directory found in $PATH 267 | pub fn build_nds(config: &NDSConfig, verbose: bool) { 268 | let mut command = Command::new("ndstool"); 269 | let name = get_name(config); 270 | 271 | let output_config = Config::try_load(config).expect("Failed to load nds.toml"); 272 | 273 | let banner_text = if output_config.name.iter().any(|i| i.is_some()) { 274 | output_config 275 | .name 276 | .into_iter() 277 | .map(|i| i.unwrap_or_default()) 278 | .collect::>() 279 | .join(";") 280 | } else { 281 | format!( 282 | "{};{};{}", 283 | name.0.file_name().unwrap().to_string_lossy(), 284 | &config.description, 285 | &config.author 286 | ) 287 | }; 288 | 289 | let icon = get_icon_path(config); 290 | 291 | command 292 | .arg("-c") 293 | .arg(config.path_nds()) 294 | .arg("-9") 295 | .arg(config.path_arm9()) 296 | .arg("-7") 297 | .arg(config.path_arm7()) 298 | .arg("-b") 299 | .arg(&icon) 300 | .arg(banner_text); 301 | 302 | // If romfs directory exists, automatically include it 303 | let (romfs_path, is_default_romfs) = get_romfs_path(config); 304 | if romfs_path.is_dir() { 305 | eprintln!("Adding RomFS from {}", romfs_path.display()); 306 | command.arg("-d").arg(&romfs_path); 307 | } else if !is_default_romfs { 308 | eprintln!( 309 | "Could not find configured RomFS dir: {}", 310 | romfs_path.display() 311 | ); 312 | process::exit(1); 313 | } 314 | 315 | if verbose { 316 | print_command(&command); 317 | } 318 | 319 | let mut process = command 320 | .stdin(Stdio::inherit()) 321 | .stdout(Stdio::inherit()) 322 | .stderr(Stdio::inherit()) 323 | .spawn() 324 | .expect("ndstool command failed, most likely due to 'ndstool' not being in $PATH"); 325 | 326 | let status = process.wait().unwrap(); 327 | 328 | if !status.success() { 329 | process::exit(status.code().unwrap_or(1)); 330 | } 331 | } 332 | 333 | /// Link the generated nds to a ds to execute and test using `dslink`. 334 | /// This will fail if `dslink` is not within the running directory or in a directory found in $PATH 335 | pub fn link(config: &NDSConfig, run_args: &Run, verbose: bool) { 336 | let mut command = Command::new("dslink"); 337 | command 338 | .args(run_args.get_dslink_args()) 339 | .arg(config.path_nds()) 340 | .stdin(Stdio::inherit()) 341 | .stdout(Stdio::inherit()) 342 | .stderr(Stdio::inherit()); 343 | 344 | if verbose { 345 | print_command(&command); 346 | } 347 | 348 | let status = command.spawn().unwrap().wait().unwrap(); 349 | 350 | if !status.success() { 351 | process::exit(status.code().unwrap_or(1)); 352 | } 353 | } 354 | 355 | /// Read the `RomFS` path from the Cargo manifest. If it's unset, use the default. 356 | /// The returned boolean is true when the default is used. 357 | pub fn get_romfs_path(config: &NDSConfig) -> (PathBuf, bool) { 358 | let manifest_path = &config.cargo_manifest_path; 359 | let manifest_str = std::fs::read_to_string(manifest_path) 360 | .unwrap_or_else(|e| panic!("Could not open {}: {e}", manifest_path.display())); 361 | let manifest_data: toml::Value = 362 | toml::de::from_str(&manifest_str).expect("Could not parse Cargo manifest as TOML"); 363 | 364 | // Find the romfs setting and compute the path 365 | let mut is_default = false; 366 | let romfs_dir_setting = manifest_data 367 | .as_table() 368 | .and_then(|table| table.get("package")) 369 | .and_then(toml::Value::as_table) 370 | .and_then(|table| table.get("metadata")) 371 | .and_then(toml::Value::as_table) 372 | .and_then(|table| table.get("nds")) 373 | .and_then(toml::Value::as_table) 374 | .and_then(|table| table.get("romfs")) 375 | .and_then(toml::Value::as_str) 376 | .unwrap_or_else(|| { 377 | is_default = true; 378 | "romfs" 379 | }); 380 | let mut romfs_path = manifest_path.clone(); 381 | romfs_path.pop(); // Pop Cargo.toml 382 | romfs_path.push(romfs_dir_setting); 383 | 384 | (romfs_path, is_default) 385 | } 386 | 387 | /// Read the `RomFS` path from the Cargo manifest. If it's unset, use the default. 388 | /// The returned boolean is true when the default is used. 389 | pub fn get_name(config: &NDSConfig) -> (PathBuf, bool) { 390 | let manifest_path = &config.cargo_manifest_path; 391 | let manifest_str = std::fs::read_to_string(manifest_path) 392 | .unwrap_or_else(|e| panic!("Could not open {}: {e}", manifest_path.display())); 393 | let manifest_data: toml::Value = 394 | toml::de::from_str(&manifest_str).expect("Could not parse Cargo manifest as TOML"); 395 | 396 | // Find the romfs setting and compute the path 397 | let mut is_default = false; 398 | let name_setting = manifest_data 399 | .as_table() 400 | .and_then(|table| table.get("package")) 401 | .and_then(toml::Value::as_table) 402 | .and_then(|table| table.get("name")) 403 | .and_then(toml::Value::as_str) 404 | .unwrap_or_else(|| { 405 | is_default = true; 406 | "No Name" 407 | }); 408 | let mut name = manifest_path.clone(); 409 | name.pop(); // Pop Cargo.toml 410 | name.push(name_setting); 411 | 412 | (name, is_default) 413 | } 414 | 415 | /// Read the `icon` path from the Cargo manifest. If it's unset, use the default. 416 | /// The returned boolean is true when the default is used. 417 | pub fn get_icon_path(config: &NDSConfig) -> PathBuf { 418 | let manifest_path = &config.cargo_manifest_path; 419 | 420 | let config = Config::try_load(config).expect("Failed to load nds.toml"); 421 | match config.icon { 422 | Some(icon) => { 423 | let mut icon_path = manifest_path.clone(); 424 | icon_path.pop(); // Pop Cargo.toml 425 | icon_path.push(icon); 426 | icon_path 427 | } 428 | None => "/opt/wonderful/thirdparty/blocksds/core/sys/icon.bmp".into(), 429 | } 430 | } 431 | 432 | #[derive(Default, Debug)] 433 | pub struct NDSConfig { 434 | name: String, 435 | author: String, 436 | description: String, 437 | icon: String, 438 | target_path: PathBuf, 439 | cargo_manifest_path: PathBuf, 440 | } 441 | 442 | impl NDSConfig { 443 | pub fn path_nds(&self) -> PathBuf { 444 | self.target_path.with_extension("").with_extension("nds") 445 | } 446 | pub fn path_arm9(&self) -> PathBuf { 447 | self.target_path 448 | .with_extension("") 449 | .with_extension("arm9.elf") 450 | } 451 | pub fn path_arm7(&self) -> PathBuf { 452 | let arm7 = self 453 | .target_path 454 | .with_extension("") 455 | .with_extension("arm7.elf"); 456 | if arm7.exists() { 457 | return arm7; 458 | } 459 | let blocksds = 460 | env::var("BLOCKSDS").unwrap_or("/opt/wonderful/thirdparty/blocksds/core".to_owned()); 461 | PathBuf::from(format!("{}/sys/default_arm7/arm7.elf", blocksds)) 462 | } 463 | } 464 | 465 | #[derive(Ord, PartialOrd, PartialEq, Eq, Debug)] 466 | pub struct CommitDate { 467 | year: i32, 468 | month: i32, 469 | day: i32, 470 | } 471 | 472 | impl CommitDate { 473 | fn parse(date: &str) -> Option { 474 | let mut iter = date.split('-'); 475 | 476 | let year = iter.next()?.parse().ok()?; 477 | let month = iter.next()?.parse().ok()?; 478 | let day = iter.next()?.parse().ok()?; 479 | 480 | Some(Self { year, month, day }) 481 | } 482 | } 483 | 484 | impl fmt::Display for CommitDate { 485 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 486 | write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day) 487 | } 488 | } 489 | 490 | const MINIMUM_COMMIT_DATE: CommitDate = CommitDate { 491 | year: 2023, 492 | month: 5, 493 | day: 31, 494 | }; 495 | const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 70, 0); 496 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::process::Stdio; 3 | use std::sync::OnceLock; 4 | use std::{env, fs}; 5 | 6 | use cargo_metadata::Message; 7 | use clap::{Args, Parser, Subcommand}; 8 | 9 | use crate::{build_nds, cargo, get_metadata, link, print_command, NDSConfig}; 10 | 11 | #[derive(Parser, Debug)] 12 | #[command(name = "cargo", bin_name = "cargo")] 13 | pub enum Cargo { 14 | #[command(name = "nds")] 15 | Input(Input), 16 | } 17 | 18 | #[derive(Args, Debug)] 19 | #[command(version, about)] 20 | pub struct Input { 21 | #[command(subcommand)] 22 | pub cmd: CargoCmd, 23 | 24 | /// Print the exact commands `cargo-nds` is running. Note that this does not 25 | /// set the verbose flag for cargo itself. To set cargo's verbosity flag, add 26 | /// `-- -v` to the end of the command line. 27 | #[arg(long, short = 'v', global = true)] 28 | pub verbose: bool, 29 | 30 | /// Set cargo configuration on the command line. This is equivalent to 31 | /// cargo's `--config` option. 32 | #[arg(long, global = true)] 33 | pub config: Vec, 34 | } 35 | 36 | /// Run a cargo command. COMMAND will be forwarded to the real 37 | /// `cargo` with the appropriate arguments for the nds target. 38 | /// 39 | /// If an unrecognized COMMAND is used, it will be passed through unmodified 40 | /// to `cargo` with the appropriate flags set for the nds target. 41 | #[derive(Subcommand, Debug)] 42 | #[command(allow_external_subcommands = true)] 43 | pub enum CargoCmd { 44 | /// Builds an executable suitable to run on a DS (nds). 45 | Build(Build), 46 | 47 | /// Builds an executable and sends it to a device with `dslink`. 48 | Run(Run), 49 | 50 | /// Builds a test executable and sends it to a device with `dslink`. 51 | /// 52 | /// This can be used with `--test` for integration tests, or `--lib` for 53 | /// unit tests (which require a custom test runner). 54 | Test(Test), 55 | 56 | /// Sets up a new cargo project suitable to run on a DS. 57 | New(New), 58 | 59 | Init(Init), 60 | 61 | // NOTE: it seems docstring + name for external subcommands are not rendered 62 | // in help, but we might as well set them here in case a future version of clap 63 | // does include them in help text. 64 | /// Run any other `cargo` command with custom building tailored for the nds. 65 | #[command(external_subcommand, name = "COMMAND")] 66 | Passthrough(Vec), 67 | } 68 | 69 | #[derive(Args, Debug)] 70 | pub struct RemainingArgs { 71 | /// Pass additional options through to the `cargo` command. 72 | /// 73 | /// All arguments after the first `--`, or starting with the first unrecognized 74 | /// option, will be passed through to `cargo` unmodified. 75 | /// 76 | /// To pass arguments to an executable being run, a *second* `--` must be 77 | /// used to disambiguate cargo arguments from executable arguments. 78 | /// For example, `cargo nds run -- -- xyz` runs an executable with the argument 79 | /// `xyz`. 80 | #[arg( 81 | trailing_var_arg = true, 82 | allow_hyphen_values = true, 83 | value_name = "CARGO_ARGS" 84 | )] 85 | args: Vec, 86 | } 87 | 88 | #[derive(Args, Debug)] 89 | pub struct Build { 90 | #[arg(from_global)] 91 | pub verbose: bool, 92 | 93 | // Passthrough cargo options. 94 | #[command(flatten)] 95 | pub passthrough: RemainingArgs, 96 | } 97 | 98 | #[derive(Args, Debug)] 99 | pub struct Run { 100 | /// Specify the IP address of the device to send the executable to. 101 | /// 102 | /// Corresponds to ndslink's `--address` arg, which defaults to automatically 103 | /// finding the device. 104 | #[arg(long, short = 'a')] 105 | pub address: Option, 106 | 107 | /// Set the 0th argument of the executable when running it. Corresponds to 108 | /// ndslink's `--argv0` argument. 109 | #[arg(long, short = '0')] 110 | pub argv0: Option, 111 | 112 | /// Start the ndslink server after sending the executable. Corresponds to 113 | /// ndslink's `--server` argument. 114 | #[arg(long, short = 's', default_value_t = false)] 115 | pub server: bool, 116 | 117 | /// Set the number of tries when connecting to the device to send the executable. 118 | /// Corresponds to ndslink's `--retries` argument. 119 | // Can't use `short = 'r'` because that would conflict with cargo's `--release/-r` 120 | #[arg(long)] 121 | pub retries: Option, 122 | 123 | // Passthrough `cargo build` options. 124 | #[command(flatten)] 125 | pub build_args: Build, 126 | 127 | #[arg(from_global)] 128 | config: Vec, 129 | } 130 | 131 | #[derive(Args, Debug)] 132 | pub struct Test { 133 | /// If set, the built executable will not be sent to the device to run it. 134 | #[arg(long)] 135 | pub no_run: bool, 136 | 137 | /// If set, documentation tests will be built instead of unit tests. 138 | /// This implies `--no-run`, unless Cargo's `target.armv6k-nintendo-nds.runner` 139 | /// is configured. 140 | #[arg(long)] 141 | pub doc: bool, 142 | 143 | // The test command uses a superset of the same arguments as Run. 144 | #[command(flatten)] 145 | pub run_args: Run, 146 | } 147 | 148 | #[derive(Args, Debug)] 149 | pub struct New { 150 | /// Path of the new project. 151 | #[arg(required = true)] 152 | pub path: String, 153 | 154 | // The test command uses a superset of the same arguments as Run. 155 | #[command(flatten)] 156 | pub cargo_args: RemainingArgs, 157 | } 158 | 159 | #[derive(Args, Debug)] 160 | pub struct Init { 161 | /// Path of the new project. 162 | #[arg(required = false)] 163 | pub path: String, 164 | 165 | // The test command uses a superset of the same arguments as Run. 166 | #[command(flatten)] 167 | pub cargo_args: RemainingArgs, 168 | } 169 | 170 | impl CargoCmd { 171 | /// Returns the additional arguments run by the "official" cargo subcommand. 172 | pub fn cargo_args(&self) -> Vec { 173 | match self { 174 | CargoCmd::Build(build) => build.passthrough.cargo_args(), 175 | CargoCmd::Run(run) => run.build_args.passthrough.cargo_args(), 176 | CargoCmd::Test(test) => test.cargo_args(), 177 | CargoCmd::New(new) => { 178 | // We push the original path in the new command (we captured it in [`New`] to learn about the context) 179 | let mut cargo_args = new.cargo_args.cargo_args(); 180 | cargo_args.push(new.path.clone()); 181 | 182 | cargo_args 183 | } 184 | CargoCmd::Init(init) => { 185 | // We push the original path in the init command (we captured it in [`Init`] to learn about the context) 186 | let mut cargo_args = init.cargo_args.cargo_args(); 187 | cargo_args.push(init.path.clone()); 188 | 189 | cargo_args 190 | } 191 | CargoCmd::Passthrough(other) => other.clone().split_off(1), 192 | } 193 | } 194 | 195 | /// Returns the cargo subcommand run by `cargo-nds` when handling a [`CargoCmd`]. 196 | /// 197 | /// # Notes 198 | /// 199 | /// This is not equivalent to the lowercase name of the [`CargoCmd`] variant. 200 | /// Commands may use different commands under the hood to function (e.g. [`CargoCmd::Run`] uses `build` 201 | /// if no custom runner is configured). 202 | pub fn subcommand_name(&self) -> &str { 203 | match self { 204 | CargoCmd::Build(_) => "build", 205 | CargoCmd::Run(run) => { 206 | if run.use_custom_runner() { 207 | "run" 208 | } else { 209 | "build" 210 | } 211 | } 212 | CargoCmd::Test(_) => "test", 213 | CargoCmd::New(_) => "new", 214 | CargoCmd::Init(_) => "init", 215 | CargoCmd::Passthrough(cmd) => &cmd[0], 216 | } 217 | } 218 | 219 | /// Whether or not this command should compile any code, and thus needs import the custom environment configuration (e.g. target spec). 220 | pub fn should_compile(&self) -> bool { 221 | matches!( 222 | self, 223 | Self::Build(_) | Self::Run(_) | Self::Test(_) | Self::Passthrough(_) 224 | ) 225 | } 226 | 227 | /// Whether or not this command should build a ndsX executable file. 228 | pub fn should_build_ndsx(&self) -> bool { 229 | match self { 230 | Self::Build(_) | CargoCmd::Run(_) => true, 231 | &Self::Test(Test { doc, .. }) => { 232 | if doc { 233 | eprintln!("Documentation tests requested, no ndsx will be built"); 234 | false 235 | } else { 236 | true 237 | } 238 | } 239 | _ => false, 240 | } 241 | } 242 | 243 | /// Whether or not the resulting executable should be sent to the nds with 244 | /// `ndslink`. 245 | pub fn should_link_to_device(&self) -> bool { 246 | match self { 247 | Self::Test(Test { no_run: true, .. }) => false, 248 | Self::Run(run) | Self::Test(Test { run_args: run, .. }) => !run.use_custom_runner(), 249 | _ => false, 250 | } 251 | } 252 | 253 | pub const DEFAULT_MESSAGE_FORMAT: &'static str = "json-render-diagnostics"; 254 | 255 | pub fn extract_message_format(&mut self) -> Result, String> { 256 | let cargo_args = match self { 257 | Self::Build(build) => &mut build.passthrough.args, 258 | Self::Run(run) => &mut run.build_args.passthrough.args, 259 | Self::New(new) => &mut new.cargo_args.args, 260 | Self::Init(init) => &mut init.cargo_args.args, 261 | Self::Test(test) => &mut test.run_args.build_args.passthrough.args, 262 | Self::Passthrough(args) => args, 263 | }; 264 | 265 | let format = Self::extract_message_format_from_args(cargo_args)?; 266 | if format.is_some() { 267 | return Ok(format); 268 | } 269 | 270 | if let Self::Test(Test { doc: true, .. }) = self { 271 | // We don't care about JSON output for doctests since we're not 272 | // building any ndsx etc. Just use the default output as it's more 273 | // readable compared to DEFAULT_MESSAGE_FORMAT 274 | Ok(Some(String::from("human"))) 275 | } else { 276 | Ok(None) 277 | } 278 | } 279 | 280 | fn extract_message_format_from_args( 281 | cargo_args: &mut Vec, 282 | ) -> Result, String> { 283 | // Checks for a position within the args where '--message-format' is located 284 | if let Some(pos) = cargo_args 285 | .iter() 286 | .position(|s| s.starts_with("--message-format")) 287 | { 288 | // Remove the arg from list so we don't pass anything twice by accident 289 | let arg = cargo_args.remove(pos); 290 | 291 | // Allows for usage of '--message-format=' and also using space separation. 292 | // Check for a '=' delimiter and use the second half of the split as the format, 293 | // otherwise remove next arg which is now at the same position as the original flag. 294 | let format = if let Some((_, format)) = arg.split_once('=') { 295 | format.to_string() 296 | } else { 297 | // Also need to remove the argument to the --message-format option 298 | cargo_args.remove(pos) 299 | }; 300 | 301 | // Non-json formats are not supported so the executable exits. 302 | if format.starts_with("json") { 303 | Ok(Some(format)) 304 | } else { 305 | Err(String::from( 306 | "error: non-JSON `message-format` is not supported", 307 | )) 308 | } 309 | } else { 310 | Ok(None) 311 | } 312 | } 313 | 314 | /// Runs the custom callback *after* the cargo command, depending on the type of command launched. 315 | /// 316 | /// # Examples 317 | /// 318 | /// - `cargo nds build` and other "build" commands will use their callbacks to build the final `.ndsx` file and link it. 319 | /// - `cargo nds new` and other generic commands will use their callbacks to make nds-specific changes to the environment. 320 | pub fn run_callback(&self, messages: &[Message]) { 321 | // Process the metadata only for commands that have it/use it 322 | let config = if self.should_build_ndsx() { 323 | eprintln!("Getting metadata"); 324 | 325 | Some(get_metadata(messages)) 326 | } else { 327 | None 328 | }; 329 | 330 | // Run callback only for commands that use it 331 | match self { 332 | Self::Build(cmd) => cmd.callback(&config), 333 | Self::Run(cmd) => cmd.callback(&config), 334 | Self::Test(cmd) => cmd.callback(&config), 335 | Self::New(cmd) => cmd.callback(), 336 | Self::Init(cmd) => cmd.callback(), 337 | _ => (), 338 | } 339 | } 340 | } 341 | 342 | impl RemainingArgs { 343 | /// Get the args to be passed to `cargo`. 344 | pub fn cargo_args(&self) -> Vec { 345 | self.split_args().0 346 | } 347 | 348 | /// Get the args to be passed to the executable itself (not `cargo`). 349 | pub fn exe_args(&self) -> Vec { 350 | self.split_args().1 351 | } 352 | 353 | fn split_args(&self) -> (Vec, Vec) { 354 | let mut args = self.args.clone(); 355 | 356 | if let Some(split) = args.iter().position(|s| s == "--") { 357 | let second_half = args.split_off(split + 1); 358 | // take off the "--" arg we found, we'll add one later if needed 359 | args.pop(); 360 | 361 | (args, second_half) 362 | } else { 363 | (args, Vec::new()) 364 | } 365 | } 366 | } 367 | 368 | impl Build { 369 | /// Callback for `cargo nds build`. 370 | /// 371 | /// This callback handles building the application as a `.ndsx` file. 372 | fn callback(&self, config: &Option) { 373 | if let Some(config) = config { 374 | eprintln!("Building nds: {}", config.path_nds().display()); 375 | build_nds(config, self.verbose); 376 | } 377 | } 378 | } 379 | 380 | impl Run { 381 | /// Get the args to pass to `ndslink` based on these options. 382 | pub fn get_dslink_args(&self) -> Vec { 383 | let mut args = Vec::new(); 384 | 385 | if let Some(address) = self.address { 386 | args.extend(["-a".to_string(), address.to_string()]); 387 | } 388 | 389 | args 390 | } 391 | 392 | /// Callback for `cargo nds run`. 393 | /// 394 | /// This callback handles launching the application via `dslink`. 395 | fn callback(&self, config: &Option) { 396 | // Run the normal "build" callback 397 | self.build_args.callback(config); 398 | 399 | if !self.use_custom_runner() { 400 | if let Some(cfg) = config { 401 | eprintln!("Running dslink"); 402 | link(cfg, self, self.build_args.verbose); 403 | } 404 | } 405 | } 406 | 407 | /// Returns whether the cargo environment has `target.armv6k-nintendo-nds.runner` 408 | /// configured. This will only be checked once during the lifetime of the program, 409 | /// and takes into account the usual ways Cargo looks for its 410 | /// [configuration](https://doc.rust-lang.org/cargo/reference/config.html): 411 | /// 412 | /// - `.cargo/config.toml` 413 | /// - Environment variables 414 | /// - Command-line `--config` overrides 415 | pub fn use_custom_runner(&self) -> bool { 416 | static HAS_RUNNER: OnceLock = OnceLock::new(); 417 | 418 | let &custom_runner_configured = HAS_RUNNER.get_or_init(|| { 419 | let blocksds = env::var("BLOCKSDS") 420 | .unwrap_or("/opt/wonderful/thirdparty/blocksds/core".to_owned()); 421 | env::set_var( 422 | "RUSTFLAGS", 423 | format!("-C link-args=-specs={blocksds}/sys/crts/ds_arm9.specs"), 424 | ); 425 | 426 | let mut cmd = cargo(&self.config); 427 | cmd.args([ 428 | // https://github.com/rust-lang/cargo/issues/9301 429 | "-Z", 430 | "build-std=core,alloc", 431 | "--target", 432 | "./armv5te-nintendo-ds.json", 433 | ]) 434 | .stdout(Stdio::null()) 435 | .stderr(Stdio::null()); 436 | 437 | if self.build_args.verbose { 438 | print_command(&cmd); 439 | } 440 | 441 | // `cargo config get` exits zero if the config exists, or nonzero otherwise 442 | cmd.status().map_or(false, |status| status.success()) 443 | }); 444 | 445 | if self.build_args.verbose { 446 | eprintln!( 447 | "Custom runner is {}configured", 448 | if custom_runner_configured { "" } else { "not " } 449 | ); 450 | } 451 | 452 | custom_runner_configured 453 | } 454 | } 455 | 456 | impl Test { 457 | /// Callback for `cargo nds test`. 458 | /// 459 | /// This callback handles launching the application via `ndslink`. 460 | fn callback(&self, config: &Option) { 461 | if self.no_run { 462 | // If the tests don't have to run, use the "build" callback 463 | self.run_args.build_args.callback(config); 464 | } else { 465 | // If the tests have to run, use the "run" callback 466 | self.run_args.callback(config); 467 | } 468 | } 469 | 470 | fn should_run(&self) -> bool { 471 | self.run_args.use_custom_runner() && !self.no_run 472 | } 473 | 474 | /// The args to pass to the underlying `cargo test` command. 475 | fn cargo_args(&self) -> Vec { 476 | let mut cargo_args = self.run_args.build_args.passthrough.cargo_args(); 477 | 478 | // We can't run nds executables on the host, but we want to respect 479 | // the user's "runner" configuration if set. 480 | // 481 | // If doctests were requested, `--no-run` will be rejected on the 482 | // command line and must be set with RUSTDOCFLAGS instead: 483 | // https://github.com/rust-lang/rust/issues/87022 484 | 485 | if self.doc { 486 | cargo_args.extend([ 487 | "--doc".into(), 488 | // https://github.com/rust-lang/cargo/issues/7040 489 | "-Z".into(), 490 | "doctest-xcompile".into(), 491 | ]); 492 | } else if !self.should_run() { 493 | cargo_args.push("--no-run".into()); 494 | } 495 | 496 | cargo_args 497 | } 498 | 499 | /// Flags to pass to rustdoc via RUSTDOCFLAGS 500 | pub(crate) fn rustdocflags(&self) -> &'static str { 501 | if self.should_run() { 502 | "" 503 | } else { 504 | // We don't support running doctests by default, but cargo doesn't like 505 | // --no-run for doctests, so we have to plumb it in via RUSTDOCFLAGS 506 | " --no-run" 507 | } 508 | } 509 | } 510 | 511 | const TOML_CHANGES: &str = r#"libnds_sys = { git = "https://github.com/SeleDreams/libnds-sys.git" } 512 | 513 | [package.metadata.nds] 514 | romfs_dir = "romfs" 515 | "#; 516 | 517 | const TARGET_JSON: &str = r#"{ 518 | "abi": "eabi", 519 | "arch": "arm", 520 | "data-layout": "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64", 521 | "env" : "picolibc", 522 | "exe-suffix" : ".arm9.elf", 523 | "is-builtin": false, 524 | "linker": "arm-none-eabi-gcc", 525 | "llvm-target": "armv5te-none-gnu", 526 | "llvm-floatabi": "soft", 527 | "relocation-model": "static", 528 | "target-endian": "little", 529 | "target-pointer-width": "32", 530 | "target-c-int-width": "32", 531 | "executables": true, 532 | "linker-flavor": "gcc", 533 | "max-atomic-width": 32, 534 | "disable-redzone": true, 535 | "emit-debug-gdb-scripts": false, 536 | "features" : "+soft-float,+strict-align,+atomics-32", 537 | "panic-strategy" : "abort", 538 | "linker-is-gnu": true, 539 | "target-family": [ 540 | "unix" 541 | ], 542 | "no-default-libraries": false, 543 | "main-needs-argc-argv":"false", 544 | "pre-link-args": { 545 | "gcc": [ 546 | "--data-sections", 547 | "-march=armv5te", 548 | "-mthumb", 549 | "-mcpu=arm946e-s+nofp", 550 | "-mthumb-interwork", 551 | "-Wl,-Map,target/arm9.map", 552 | "-Wl,--gc-sections" 553 | ] 554 | }, 555 | "post-link-args" : { 556 | "gcc": [ 557 | "-Wl,--no-warn-rwx-segments", 558 | "-Wl,--allow-multiple-definition" 559 | ] 560 | }, 561 | "late-link-args": { 562 | "gcc": [ 563 | "-lgcc" 564 | ] 565 | }, 566 | "vendor" : "nintendo", 567 | "os" : "nintendo_ds_arm9" 568 | } 569 | "#; 570 | 571 | const CUSTOM_MAIN_RS: &str = r#"#![no_main] 572 | #![no_std] 573 | extern crate alloc; 574 | use libnds_sys::arm9_bindings::*; 575 | use libnds_sys::*; 576 | use core::ffi::*; 577 | #[unsafe(no_mangle)] 578 | extern "C" fn main() -> c_int 579 | { 580 | unsafe 581 | { 582 | consoleDemoInit(); 583 | println!("Hello World!"); 584 | loop { 585 | swiWaitForVBlank(); 586 | scanKeys(); 587 | let keys = keysHeld(); 588 | if (keys & KEY_START) > 0 589 | { 590 | break; 591 | } 592 | } 593 | } 594 | return 0; 595 | } 596 | "#; 597 | 598 | const CUSTOM_CARGO_CONFIG: &str = r#"[profile.release] 599 | codegen-units = 1 600 | opt-level=3 601 | debug-assertions=false 602 | strip = "debuginfo" 603 | lto = true 604 | overflow-checks=false 605 | 606 | [profile.dev] 607 | codegen-units = 1 608 | debug=2 609 | opt-level=3 610 | debug-assertions=false 611 | lto = true 612 | overflow-checks=false 613 | strip = false 614 | "#; 615 | 616 | impl New { 617 | /// Callback for `cargo nds new`. 618 | /// 619 | /// This callback handles the custom environment modifications when creating a new nds project. 620 | fn callback(&self) { 621 | // Commmit changes to the project only if is meant to be a binary 622 | if self.cargo_args.args.contains(&"--lib".to_string()) { 623 | return; 624 | } 625 | 626 | // Attain a canonicalised path for the new project and it's TOML manifest 627 | let project_path = fs::canonicalize(&self.path).unwrap(); 628 | let toml_path = project_path.join("Cargo.toml"); 629 | let romfs_path = project_path.join("romfs"); 630 | let main_rs_path = project_path.join("src/main.rs"); 631 | let target_json_path = project_path.join("armv5te-nintendo-ds.json"); 632 | let config_path = project_path.join(".cargo/config.toml"); 633 | 634 | // Create the "romfs" directory 635 | fs::create_dir(romfs_path).unwrap(); 636 | 637 | // Read the contents of `Cargo.toml` to a string 638 | let mut buf = String::new(); 639 | fs::File::open(&toml_path) 640 | .unwrap() 641 | .read_to_string(&mut buf) 642 | .unwrap(); 643 | 644 | // Add the custom changes to the TOML 645 | let buf = buf + TOML_CHANGES; 646 | fs::write(&toml_path, buf).unwrap(); 647 | 648 | // Add the custom changes to the main.rs file 649 | fs::write(main_rs_path, CUSTOM_MAIN_RS).unwrap(); 650 | 651 | fs::write(target_json_path, TARGET_JSON).unwrap(); 652 | fs::create_dir(project_path.join(".cargo")).unwrap(); 653 | fs::write(config_path, CUSTOM_CARGO_CONFIG).unwrap(); 654 | } 655 | } 656 | 657 | impl Init { 658 | /// Callback for `cargo nds new`. 659 | /// 660 | /// This callback handles the custom environment modifications when creating a new nds project. 661 | fn callback(&self) { 662 | // Commmit changes to the project only if is meant to be a binary 663 | if self.cargo_args.args.contains(&"--lib".to_string()) { 664 | return; 665 | } 666 | 667 | // Attain a canonicalised path for the new project and it's TOML manifest 668 | let project_path = fs::canonicalize(&self.path).unwrap(); 669 | let toml_path = project_path.join("Cargo.toml"); 670 | let romfs_path = project_path.join("romfs"); 671 | let main_rs_path = project_path.join("src/main.rs"); 672 | let target_json_path = project_path.join("armv5te-nintendo-ds.json"); 673 | let config_path = project_path.join(".cargo/config.toml"); 674 | 675 | // Create the "romfs" directory 676 | fs::create_dir(romfs_path).unwrap(); 677 | 678 | // Read the contents of `Cargo.toml` to a string 679 | let mut buf = String::new(); 680 | fs::File::open(&toml_path) 681 | .unwrap() 682 | .read_to_string(&mut buf) 683 | .unwrap(); 684 | 685 | // Add the custom changes to the TOML 686 | let buf = buf + TOML_CHANGES; 687 | fs::write(&toml_path, buf).unwrap(); 688 | 689 | // Add the custom changes to the main.rs file 690 | fs::write(main_rs_path, CUSTOM_MAIN_RS).unwrap(); 691 | 692 | fs::write(target_json_path, TARGET_JSON).unwrap(); 693 | fs::create_dir(project_path.join(".cargo")).unwrap(); 694 | fs::write(config_path, CUSTOM_CARGO_CONFIG).unwrap(); 695 | } 696 | } 697 | 698 | #[cfg(test)] 699 | mod tests { 700 | use clap::CommandFactory; 701 | 702 | use super::*; 703 | 704 | #[test] 705 | fn verify_app() { 706 | Cargo::command().debug_assert(); 707 | } 708 | 709 | #[test] 710 | fn extract_format() { 711 | const CASES: &[(&[&str], Option<&str>)] = &[ 712 | (&["--foo", "--message-format=json", "bar"], Some("json")), 713 | (&["--foo", "--message-format", "json", "bar"], Some("json")), 714 | ( 715 | &[ 716 | "--foo", 717 | "--message-format", 718 | "json-render-diagnostics", 719 | "bar", 720 | ], 721 | Some("json-render-diagnostics"), 722 | ), 723 | ( 724 | &["--foo", "--message-format=json-render-diagnostics", "bar"], 725 | Some("json-render-diagnostics"), 726 | ), 727 | (&["--foo", "bar"], None), 728 | ]; 729 | 730 | for (args, expected) in CASES { 731 | let mut cmd = CargoCmd::Build(Build { 732 | passthrough: RemainingArgs { 733 | args: args.iter().map(ToString::to_string).collect(), 734 | }, 735 | verbose: false, 736 | }); 737 | 738 | assert_eq!( 739 | cmd.extract_message_format().unwrap(), 740 | expected.map(ToString::to_string) 741 | ); 742 | 743 | if let CargoCmd::Build(build) = cmd { 744 | assert_eq!(build.passthrough.args, vec!["--foo", "bar"]); 745 | } else { 746 | unreachable!(); 747 | } 748 | } 749 | } 750 | 751 | #[test] 752 | fn extract_format_err() { 753 | for args in [&["--message-format=foo"][..], &["--message-format", "foo"]] { 754 | let mut cmd = CargoCmd::Build(Build { 755 | passthrough: RemainingArgs { 756 | args: args.iter().map(ToString::to_string).collect(), 757 | }, 758 | verbose: false, 759 | }); 760 | 761 | assert!(cmd.extract_message_format().is_err()); 762 | } 763 | } 764 | 765 | #[test] 766 | fn split_run_args() { 767 | struct TestParam { 768 | input: &'static [&'static str], 769 | expected_cargo: &'static [&'static str], 770 | expected_exe: &'static [&'static str], 771 | } 772 | 773 | for param in [ 774 | TestParam { 775 | input: &["--example", "hello-world", "--no-default-features"], 776 | expected_cargo: &["--example", "hello-world", "--no-default-features"], 777 | expected_exe: &[], 778 | }, 779 | TestParam { 780 | input: &["--example", "hello-world", "--", "--do-stuff", "foo"], 781 | expected_cargo: &["--example", "hello-world"], 782 | expected_exe: &["--do-stuff", "foo"], 783 | }, 784 | TestParam { 785 | input: &["--lib", "--", "foo"], 786 | expected_cargo: &["--lib"], 787 | expected_exe: &["foo"], 788 | }, 789 | TestParam { 790 | input: &["foo", "--", "bar"], 791 | expected_cargo: &["foo"], 792 | expected_exe: &["bar"], 793 | }, 794 | ] { 795 | let input: Vec<&str> = ["cargo", "nds", "run"] 796 | .iter() 797 | .chain(param.input) 798 | .copied() 799 | .collect(); 800 | 801 | let Cargo::Input(Input { 802 | cmd: CargoCmd::Run(Run { build_args, .. }), 803 | .. 804 | }) = Cargo::try_parse_from(input).unwrap_or_else(|e| panic!("{e}")) 805 | else { 806 | panic!("parsed as something other than `run` subcommand") 807 | }; 808 | 809 | assert_eq!(build_args.passthrough.cargo_args(), param.expected_cargo); 810 | assert_eq!(build_args.passthrough.exe_args(), param.expected_exe); 811 | } 812 | } 813 | } 814 | --------------------------------------------------------------------------------