Compare commits
81 Commits
fix/option
...
main
Author | SHA1 | Date |
---|---|---|
xenua | 8f00290b74 | 9 months ago |
Meziu | af2d8bfd79 | 11 months ago |
Ian Chamberlain | c6943cf953 | 11 months ago |
Ian Chamberlain | f6b9c6dc95 | 1 year ago |
Ian Chamberlain | 7d6ddb2ef8 | 1 year ago |
Ian Chamberlain | 2baf9cfc78 | 1 year ago |
Ian Chamberlain | 85ebef86aa | 1 year ago |
Ian Chamberlain | e1cf233cde | 1 year ago |
Ian Chamberlain | 545d853a80 | 1 year ago |
Ian Chamberlain | 2276df9d26 | 1 year ago |
Ian Chamberlain | 2931e5609e | 1 year ago |
Meziu | aa0d754d79 | 1 year ago |
Ian Chamberlain | 6b4fd53f20 | 1 year ago |
Ian Chamberlain | 0db0ad3b96 | 1 year ago |
Ian Chamberlain | ab30da9ee7 | 1 year ago |
Ian Chamberlain | cda49e09b6 | 1 year ago |
Ian Chamberlain | 48aa7933a4 | 1 year ago |
Ian Chamberlain | d374e7016d | 1 year ago |
Andrea Ciliberti | 268c45ae8f | 1 year ago |
Meziu | 1372661285 | 1 year ago |
Andrea Ciliberti | d2f50fcc84 | 1 year ago |
Andrea Ciliberti | 50530b112f | 1 year ago |
Meziu | 246753119d | 2 years ago |
Andrea Ciliberti | 757ce04e98 | 2 years ago |
Meziu | 3429e3ed9c | 2 years ago |
Andrea Ciliberti | c1f5e121f1 | 2 years ago |
Meziu | afc9046365 | 2 years ago |
Andrea Ciliberti | 70d5ec1843 | 2 years ago |
Andrea Ciliberti | 4560716fcc | 2 years ago |
Andrea Ciliberti | f5bee9a4e5 | 2 years ago |
Andrea Ciliberti | 17ce4002e8 | 2 years ago |
Andrea Ciliberti | ea28cbc788 | 2 years ago |
Andrea Ciliberti | 66eb19ed1f | 2 years ago |
Andrea Ciliberti | 7e16b8f123 | 2 years ago |
Meziu | 6c22e506af | 2 years ago |
Andrea Ciliberti | dd5ed8a480 | 2 years ago |
Andrea Ciliberti | abe088a78c | 2 years ago |
Andrea Ciliberti | a0fcbbc57e | 2 years ago |
Andrea Ciliberti | 9b483a8589 | 2 years ago |
Andrea Ciliberti | eaa93126f4 | 2 years ago |
Andrea Ciliberti | b899ff9d7e | 2 years ago |
Andrea Ciliberti | 9b6b79dff2 | 2 years ago |
Ian Chamberlain | 78a652fdfb | 2 years ago |
Ian Chamberlain | 6f4b74355a | 2 years ago |
Ian Chamberlain | 3e7665fb63 | 2 years ago |
Meziu | 804f63d0bc | 2 years ago |
Meziu | a1ca4f649a | 2 years ago |
Andrea Ciliberti | 318320200b | 2 years ago |
Andrea Ciliberti | 657eeccd1e | 2 years ago |
Ian Chamberlain | 5a06e64307 | 2 years ago |
Ian Chamberlain | e8535a130d | 2 years ago |
Meziu | 83032c6a71 | 2 years ago |
Ian Chamberlain | 329cb00d74 | 2 years ago |
Ian Chamberlain | dd8ae47296 | 2 years ago |
Ian Chamberlain | dbf1595def | 2 years ago |
Ian Chamberlain | 583fc67fc6 | 2 years ago |
Ian Chamberlain | c2560622e8 | 2 years ago |
Ian Chamberlain | cef69644e6 | 2 years ago |
Meziu | 7b70b6b26c | 2 years ago |
Mark Drobnak | cb3a9a19ff | 2 years ago |
Mark Drobnak | 7e99e1a9a5 | 2 years ago |
Mark Drobnak | 7723d93227 | 2 years ago |
Steve Cook | c7b6482f9c | 2 years ago |
Steve Cook | 84ac10371b | 3 years ago |
Steve Cook | 1a562d0c1a | 3 years ago |
Steve Cook | eb1d4345b7 | 3 years ago |
Steve Cook | 8d350a8c6b | 3 years ago |
Steve Cook | 9aa632622c | 3 years ago |
Steve Cook | 4c5929715c | 3 years ago |
Steve Cook | ef6a2f6724 | 3 years ago |
Steve Cook | 8eac835c4c | 3 years ago |
Steve Cook | 06bdf9f289 | 3 years ago |
Steve Cook | 239556abf8 | 3 years ago |
Steve Cook | 51917f8fcf | 3 years ago |
Steve Cook | f05b57f5ac | 3 years ago |
Steve Cook | 11855472df | 3 years ago |
Steve Cook | 3cb1169dcb | 3 years ago |
Steve Cook | cc03d78ba5 | 3 years ago |
Meziu | 9136456653 | 3 years ago |
Meziu | 7c8d450a4a | 3 years ago |
Mateo Cabanal | 880661157c | 3 years ago |
13 changed files with 2192 additions and 503 deletions
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
name: Setup |
||||
description: Set up CI environment for Rust + 3DS development |
||||
|
||||
inputs: |
||||
toolchain: |
||||
description: The Rust toolchain to use for the steps |
||||
required: true |
||||
default: nightly |
||||
|
||||
runs: |
||||
using: composite |
||||
steps: |
||||
# https://github.com/nektos/act/issues/917#issuecomment-1074421318 |
||||
- if: ${{ env.ACT }} |
||||
shell: bash |
||||
name: Hack container for local development |
||||
run: | |
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - |
||||
sudo apt-get install -y nodejs |
||||
|
||||
- name: Setup default Rust toolchain |
||||
# Use this helper action so we get matcher support |
||||
# https://github.com/actions-rust-lang/setup-rust-toolchain/pull/15 |
||||
uses: actions-rust-lang/setup-rust-toolchain@v1 |
||||
with: |
||||
components: clippy, rustfmt, rust-src |
||||
toolchain: ${{ inputs.toolchain }} |
||||
|
||||
- name: Install build tools for host |
||||
shell: bash |
||||
run: sudo apt-get update && sudo apt-get install -y build-essential |
||||
|
||||
- name: Set PATH to include devkitARM |
||||
shell: bash |
||||
# For some reason devkitARM/bin is not part of the default PATH in the container |
||||
run: echo "${DEVKITARM}/bin" >> $GITHUB_PATH |
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
name: CI |
||||
|
||||
on: |
||||
push: |
||||
branches: |
||||
- master |
||||
pull_request: |
||||
branches: |
||||
- master |
||||
workflow_dispatch: |
||||
|
||||
env: |
||||
# https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html |
||||
CARGO_UNSTABLE_SPARSE_REGISTRY: "true" |
||||
# actions-rust-lang/setup-rust-toolchain sets some default RUSTFLAGS |
||||
RUSTFLAGS: "" |
||||
|
||||
jobs: |
||||
lint: |
||||
strategy: |
||||
matrix: |
||||
toolchain: |
||||
- stable |
||||
|
||||
runs-on: ubuntu-latest |
||||
container: devkitpro/devkitarm |
||||
steps: |
||||
- name: Checkout branch |
||||
uses: actions/checkout@v2 |
||||
|
||||
- uses: ./.github/actions/setup |
||||
with: |
||||
toolchain: ${{ matrix.toolchain }} |
||||
|
||||
- name: Check formatting |
||||
run: cargo fmt --all --verbose -- --check |
||||
|
||||
- name: Cargo check |
||||
run: cargo clippy --color=always --verbose --all-targets |
||||
|
||||
project-build: |
||||
strategy: |
||||
matrix: |
||||
toolchain: |
||||
# Oldest supported nightly |
||||
- nightly-2023-06-01 |
||||
- nightly |
||||
|
||||
continue-on-error: ${{ matrix.toolchain == 'nightly' }} |
||||
runs-on: ubuntu-latest |
||||
container: devkitpro/devkitarm |
||||
steps: |
||||
- name: Checkout branch |
||||
uses: actions/checkout@v3 |
||||
|
||||
- uses: ./.github/actions/setup |
||||
with: |
||||
toolchain: ${{ matrix.toolchain }} |
||||
|
||||
- name: Install cargo-3ds |
||||
uses: actions-rs/cargo@v1 |
||||
with: |
||||
command: install |
||||
args: --locked --path . |
||||
|
||||
- name: Create new project |
||||
run: cargo 3ds new app --bin |
||||
|
||||
- name: Build project |
||||
working-directory: ./app |
||||
run: cargo 3ds build --release |
@ -0,0 +1,462 @@
@@ -0,0 +1,462 @@
|
||||
# This file is automatically @generated by Cargo. |
||||
# It is not intended for manual editing. |
||||
version = 3 |
||||
|
||||
[[package]] |
||||
name = "anstream" |
||||
version = "0.6.4" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" |
||||
dependencies = [ |
||||
"anstyle", |
||||
"anstyle-parse", |
||||
"anstyle-query", |
||||
"anstyle-wincon", |
||||
"colorchoice", |
||||
"utf8parse", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "anstyle" |
||||
version = "1.0.4" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" |
||||
|
||||
[[package]] |
||||
name = "anstyle-parse" |
||||
version = "0.2.2" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" |
||||
dependencies = [ |
||||
"utf8parse", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "anstyle-query" |
||||
version = "1.0.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" |
||||
dependencies = [ |
||||
"windows-sys 0.48.0", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "anstyle-wincon" |
||||
version = "3.0.1" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" |
||||
dependencies = [ |
||||
"anstyle", |
||||
"windows-sys 0.48.0", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "bitflags" |
||||
version = "2.4.1" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" |
||||
|
||||
[[package]] |
||||
name = "camino" |
||||
version = "1.1.6" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" |
||||
dependencies = [ |
||||
"serde", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "cargo-3ds" |
||||
version = "0.1.2" |
||||
dependencies = [ |
||||
"cargo_metadata", |
||||
"clap", |
||||
"rustc_version", |
||||
"semver", |
||||
"serde", |
||||
"serde_json", |
||||
"shlex", |
||||
"tee", |
||||
"toml", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "cargo-platform" |
||||
version = "0.1.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "e34637b3140142bdf929fb439e8aa4ebad7651ebf7b1080b3930aa16ac1459ff" |
||||
dependencies = [ |
||||
"serde", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "cargo_metadata" |
||||
version = "0.14.2" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" |
||||
dependencies = [ |
||||
"camino", |
||||
"cargo-platform", |
||||
"semver", |
||||
"serde", |
||||
"serde_json", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "clap" |
||||
version = "4.4.10" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" |
||||
dependencies = [ |
||||
"clap_builder", |
||||
"clap_derive", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "clap_builder" |
||||
version = "4.4.9" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" |
||||
dependencies = [ |
||||
"anstream", |
||||
"anstyle", |
||||
"clap_lex", |
||||
"strsim", |
||||
"terminal_size", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "clap_derive" |
||||
version = "4.4.7" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" |
||||
dependencies = [ |
||||
"heck", |
||||
"proc-macro2", |
||||
"quote", |
||||
"syn", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "clap_lex" |
||||
version = "0.6.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" |
||||
|
||||
[[package]] |
||||
name = "colorchoice" |
||||
version = "1.0.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" |
||||
|
||||
[[package]] |
||||
name = "errno" |
||||
version = "0.3.8" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" |
||||
dependencies = [ |
||||
"libc", |
||||
"windows-sys 0.52.0", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "heck" |
||||
version = "0.4.1" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" |
||||
|
||||
[[package]] |
||||
name = "itoa" |
||||
version = "1.0.9" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" |
||||
|
||||
[[package]] |
||||
name = "libc" |
||||
version = "0.2.150" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" |
||||
|
||||
[[package]] |
||||
name = "linux-raw-sys" |
||||
version = "0.4.11" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" |
||||
|
||||
[[package]] |
||||
name = "proc-macro2" |
||||
version = "1.0.70" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" |
||||
dependencies = [ |
||||
"unicode-ident", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "quote" |
||||
version = "1.0.33" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" |
||||
dependencies = [ |
||||
"proc-macro2", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "rustc_version" |
||||
version = "0.4.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" |
||||
dependencies = [ |
||||
"semver", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "rustix" |
||||
version = "0.38.25" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" |
||||
dependencies = [ |
||||
"bitflags", |
||||
"errno", |
||||
"libc", |
||||
"linux-raw-sys", |
||||
"windows-sys 0.48.0", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "ryu" |
||||
version = "1.0.15" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" |
||||
|
||||
[[package]] |
||||
name = "semver" |
||||
version = "1.0.20" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" |
||||
dependencies = [ |
||||
"serde", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "serde" |
||||
version = "1.0.193" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" |
||||
dependencies = [ |
||||
"serde_derive", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "serde_derive" |
||||
version = "1.0.193" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" |
||||
dependencies = [ |
||||
"proc-macro2", |
||||
"quote", |
||||
"syn", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "serde_json" |
||||
version = "1.0.108" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" |
||||
dependencies = [ |
||||
"itoa", |
||||
"ryu", |
||||
"serde", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "shlex" |
||||
version = "1.2.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" |
||||
|
||||
[[package]] |
||||
name = "strsim" |
||||
version = "0.10.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" |
||||
|
||||
[[package]] |
||||
name = "syn" |
||||
version = "2.0.39" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" |
||||
dependencies = [ |
||||
"proc-macro2", |
||||
"quote", |
||||
"unicode-ident", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "tee" |
||||
version = "0.1.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "37c12559dba7383625faaff75be24becf35bfc885044375bcab931111799a3da" |
||||
|
||||
[[package]] |
||||
name = "terminal_size" |
||||
version = "0.3.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" |
||||
dependencies = [ |
||||
"rustix", |
||||
"windows-sys 0.48.0", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "toml" |
||||
version = "0.5.11" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" |
||||
dependencies = [ |
||||
"serde", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "unicode-ident" |
||||
version = "1.0.12" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" |
||||
|
||||
[[package]] |
||||
name = "utf8parse" |
||||
version = "0.2.1" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" |
||||
|
||||
[[package]] |
||||
name = "windows-sys" |
||||
version = "0.48.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" |
||||
dependencies = [ |
||||
"windows-targets 0.48.5", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "windows-sys" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" |
||||
dependencies = [ |
||||
"windows-targets 0.52.0", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "windows-targets" |
||||
version = "0.48.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" |
||||
dependencies = [ |
||||
"windows_aarch64_gnullvm 0.48.5", |
||||
"windows_aarch64_msvc 0.48.5", |
||||
"windows_i686_gnu 0.48.5", |
||||
"windows_i686_msvc 0.48.5", |
||||
"windows_x86_64_gnu 0.48.5", |
||||
"windows_x86_64_gnullvm 0.48.5", |
||||
"windows_x86_64_msvc 0.48.5", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "windows-targets" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" |
||||
dependencies = [ |
||||
"windows_aarch64_gnullvm 0.52.0", |
||||
"windows_aarch64_msvc 0.52.0", |
||||
"windows_i686_gnu 0.52.0", |
||||
"windows_i686_msvc 0.52.0", |
||||
"windows_x86_64_gnu 0.52.0", |
||||
"windows_x86_64_gnullvm 0.52.0", |
||||
"windows_x86_64_msvc 0.52.0", |
||||
] |
||||
|
||||
[[package]] |
||||
name = "windows_aarch64_gnullvm" |
||||
version = "0.48.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" |
||||
|
||||
[[package]] |
||||
name = "windows_aarch64_gnullvm" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" |
||||
|
||||
[[package]] |
||||
name = "windows_aarch64_msvc" |
||||
version = "0.48.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" |
||||
|
||||
[[package]] |
||||
name = "windows_aarch64_msvc" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" |
||||
|
||||
[[package]] |
||||
name = "windows_i686_gnu" |
||||
version = "0.48.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" |
||||
|
||||
[[package]] |
||||
name = "windows_i686_gnu" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" |
||||
|
||||
[[package]] |
||||
name = "windows_i686_msvc" |
||||
version = "0.48.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" |
||||
|
||||
[[package]] |
||||
name = "windows_i686_msvc" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" |
||||
|
||||
[[package]] |
||||
name = "windows_x86_64_gnu" |
||||
version = "0.48.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" |
||||
|
||||
[[package]] |
||||
name = "windows_x86_64_gnu" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" |
||||
|
||||
[[package]] |
||||
name = "windows_x86_64_gnullvm" |
||||
version = "0.48.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" |
||||
|
||||
[[package]] |
||||
name = "windows_x86_64_gnullvm" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" |
||||
|
||||
[[package]] |
||||
name = "windows_x86_64_msvc" |
||||
version = "0.48.5" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" |
||||
|
||||
[[package]] |
||||
name = "windows_x86_64_msvc" |
||||
version = "0.52.0" |
||||
source = "registry+https://github.com/rust-lang/crates.io-index" |
||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" |
@ -1,17 +1,22 @@
@@ -1,17 +1,22 @@
|
||||
[package] |
||||
name = "cargo-3ds" |
||||
version = "0.1.0" |
||||
description = "Cargo wrapper for developing Rust-based Nintendo 3DS homebrew apps" |
||||
repository = "https://github.com/Meziu/cargo-3ds" |
||||
license = "MIT" |
||||
authors = ["Andrea Ciliberti <meziu210@icloud.com>"] |
||||
version = "0.1.2" |
||||
authors = ["Rust3DS Org", "Andrea Ciliberti <meziu210@icloud.com>"] |
||||
description = "Cargo wrapper for developing Nintendo 3DS homebrew apps" |
||||
repository = "https://git.xenua.me/rust3ds/cargo-3ds" |
||||
keywords = ["3ds", "homebrew"] |
||||
categories = ["command-line-utilities", "development-tools::cargo-plugins"] |
||||
exclude = [".github"] |
||||
license = "MIT OR Apache-2.0" |
||||
edition = "2021" |
||||
|
||||
[dependencies] |
||||
cargo_metadata = "0.14.0" |
||||
rustc_version = "0.4.0" |
||||
semver = "1.0.10" |
||||
serde = "1.0.111" |
||||
serde_derive = "1.0.111" |
||||
serde = { version = "1.0.139", features = ["derive"] } |
||||
tee = "0.1.0" |
||||
toml = "0.5.6" |
||||
clap = { version = "4.0.15", features = ["derive", "wrap_help"] } |
||||
shlex = "1.1.0" |
||||
serde_json = "1.0.108" |
||||
|
@ -0,0 +1,176 @@
@@ -0,0 +1,176 @@
|
||||
Apache License |
||||
Version 2.0, January 2004 |
||||
http://www.apache.org/licenses/ |
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
||||
1. Definitions. |
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, |
||||
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by |
||||
the copyright owner that is granting the License. |
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all |
||||
other entities that control, are controlled by, or are under common |
||||
control with that entity. For the purposes of this definition, |
||||
"control" means (i) the power, direct or indirect, to cause the |
||||
direction or management of such entity, whether by contract or |
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity |
||||
exercising permissions granted by this License. |
||||
|
||||
"Source" form shall mean the preferred form for making modifications, |
||||
including but not limited to software source code, documentation |
||||
source, and configuration files. |
||||
|
||||
"Object" form shall mean any form resulting from mechanical |
||||
transformation or translation of a Source form, including but |
||||
not limited to compiled object code, generated documentation, |
||||
and conversions to other media types. |
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or |
||||
Object form, made available under the License, as indicated by a |
||||
copyright notice that is included in or attached to the work |
||||
(an example is provided in the Appendix below). |
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object |
||||
form, that is based on (or derived from) the Work and for which the |
||||
editorial revisions, annotations, elaborations, or other modifications |
||||
represent, as a whole, an original work of authorship. For the purposes |
||||
of this License, Derivative Works shall not include works that remain |
||||
separable from, or merely link (or bind by name) to the interfaces of, |
||||
the Work and Derivative Works thereof. |
||||
|
||||
"Contribution" shall mean any work of authorship, including |
||||
the original version of the Work and any modifications or additions |
||||
to that Work or Derivative Works thereof, that is intentionally |
||||
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
or by an individual or Legal Entity authorized to submit on behalf of |
||||
the copyright owner. For the purposes of this definition, "submitted" |
||||
means any form of electronic, verbal, or written communication sent |
||||
to the Licensor or its representatives, including but not limited to |
||||
communication on electronic mailing lists, source code control systems, |
||||
and issue tracking systems that are managed by, or on behalf of, the |
||||
Licensor for the purpose of discussing and improving the Work, but |
||||
excluding communication that is conspicuously marked or otherwise |
||||
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
on behalf of whom a Contribution has been received by Licensor and |
||||
subsequently incorporated within the Work. |
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
copyright license to reproduce, prepare Derivative Works of, |
||||
publicly display, publicly perform, sublicense, and distribute the |
||||
Work and such Derivative Works in Source or Object form. |
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
(except as stated in this section) patent license to make, have made, |
||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
where such license applies only to those patent claims licensable |
||||
by such Contributor that are necessarily infringed by their |
||||
Contribution(s) alone or by combination of their Contribution(s) |
||||
with the Work to which such Contribution(s) was submitted. If You |
||||
institute patent litigation against any entity (including a |
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
or a Contribution incorporated within the Work constitutes direct |
||||
or contributory patent infringement, then any patent licenses |
||||
granted to You under this License for that Work shall terminate |
||||
as of the date such litigation is filed. |
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the |
||||
Work or Derivative Works thereof in any medium, with or without |
||||
modifications, and in Source or Object form, provided that You |
||||
meet the following conditions: |
||||
|
||||
(a) You must give any other recipients of the Work or |
||||
Derivative Works a copy of this License; and |
||||
|
||||
(b) You must cause any modified files to carry prominent notices |
||||
stating that You changed the files; and |
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works |
||||
that You distribute, all copyright, patent, trademark, and |
||||
attribution notices from the Source form of the Work, |
||||
excluding those notices that do not pertain to any part of |
||||
the Derivative Works; and |
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its |
||||
distribution, then any Derivative Works that You distribute must |
||||
include a readable copy of the attribution notices contained |
||||
within such NOTICE file, excluding those notices that do not |
||||
pertain to any part of the Derivative Works, in at least one |
||||
of the following places: within a NOTICE text file distributed |
||||
as part of the Derivative Works; within the Source form or |
||||
documentation, if provided along with the Derivative Works; or, |
||||
within a display generated by the Derivative Works, if and |
||||
wherever such third-party notices normally appear. The contents |
||||
of the NOTICE file are for informational purposes only and |
||||
do not modify the License. You may add Your own attribution |
||||
notices within Derivative Works that You distribute, alongside |
||||
or as an addendum to the NOTICE text from the Work, provided |
||||
that such additional attribution notices cannot be construed |
||||
as modifying the License. |
||||
|
||||
You may add Your own copyright statement to Your modifications and |
||||
may provide additional or different license terms and conditions |
||||
for use, reproduction, or distribution of Your modifications, or |
||||
for any such Derivative Works as a whole, provided Your use, |
||||
reproduction, and distribution of the Work otherwise complies with |
||||
the conditions stated in this License. |
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
any Contribution intentionally submitted for inclusion in the Work |
||||
by You to the Licensor shall be under the terms and conditions of |
||||
this License, without any additional terms or conditions. |
||||
Notwithstanding the above, nothing herein shall supersede or modify |
||||
the terms of any separate license agreement you may have executed |
||||
with Licensor regarding such Contributions. |
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade |
||||
names, trademarks, service marks, or product names of the Licensor, |
||||
except as required for reasonable and customary use in describing the |
||||
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
agreed to in writing, Licensor provides the Work (and each |
||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
implied, including, without limitation, any warranties or conditions |
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
appropriateness of using or redistributing the Work and assume any |
||||
risks associated with Your exercise of permissions under this License. |
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, |
||||
whether in tort (including negligence), contract, or otherwise, |
||||
unless required by applicable law (such as deliberate and grossly |
||||
negligent acts) or agreed to in writing, shall any Contributor be |
||||
liable to You for damages, including any direct, indirect, special, |
||||
incidental, or consequential damages of any character arising as a |
||||
result of this License or out of the use or inability to use the |
||||
Work (including but not limited to damages for loss of goodwill, |
||||
work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses), even if such Contributor |
||||
has been advised of the possibility of such damages. |
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing |
||||
the Work or Derivative Works thereof, You may choose to offer, |
||||
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
or other liability obligations and/or rights consistent with this |
||||
License. However, in accepting such obligations, You may act only |
||||
on Your own behalf and on Your sole responsibility, not on behalf |
||||
of any other Contributor, and only if You agree to indemnify, |
||||
defend, and hold each Contributor harmless for any liability |
||||
incurred by, or claims asserted against, such Contributor by reason |
||||
of your accepting any such warranty or additional liability. |
||||
|
||||
END OF TERMS AND CONDITIONS |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any |
||||
person obtaining a copy of this software and associated |
||||
documentation files (the "Software"), to deal in the |
||||
Software without restriction, including without |
||||
limitation the rights to use, copy, modify, merge, |
||||
publish, distribute, sublicense, and/or sell copies of |
||||
the Software, and to permit persons to whom the Software |
||||
is furnished to do so, subject to the following |
||||
conditions: |
||||
|
||||
The above copyright notice and this permission notice |
||||
shall be included in all copies or substantial portions |
||||
of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF |
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED |
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR |
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
DEALINGS IN THE SOFTWARE. |
@ -1,15 +1,130 @@
@@ -1,15 +1,130 @@
|
||||
# cargo-3ds |
||||
|
||||
Cargo command to work with Nintendo 3DS project binaries. Based on cargo-psp. |
||||
|
||||
# Usage |
||||
While you can set the nightly version of Rust as default for the project you're working on (`rustup override nightly`), my suggested method is: |
||||
`cargo +nightly 3ds`. \ |
||||
The commands are the same as cargo ("run" also uses 3dslink, so you can directly use `run` to compile and run on your system). |
||||
## Installation |
||||
|
||||
To install the latest release on <https://crates.io>: |
||||
|
||||
```sh |
||||
cargo install --locked cargo-3ds |
||||
``` |
||||
|
||||
To install the current `master` version of `cargo-3ds`: |
||||
|
||||
```sh |
||||
cargo install --locked --git https://github.com/rust3ds/cargo-3ds |
||||
``` |
||||
|
||||
## Usage |
||||
|
||||
Use the nightly toolchain to build 3DS apps (either by using `rustup override nightly` for the project directory or by adding `+nightly` in the `cargo` invocation). |
||||
|
||||
```txt |
||||
Commands: |
||||
build |
||||
Builds an executable suitable to run on a 3DS (3dsx) |
||||
run |
||||
Builds an executable and sends it to a device with `3dslink` |
||||
test |
||||
Builds a test executable and sends it to a device with `3dslink` |
||||
new |
||||
Sets up a new cargo project suitable to run on a 3DS |
||||
help |
||||
Print this message or the help of the given subcommand(s) |
||||
|
||||
Options: |
||||
-h, --help |
||||
Print help information (use `-h` for a summary) |
||||
|
||||
-V, --version |
||||
Print version information |
||||
``` |
||||
|
||||
Additional arguments will be passed through to the given subcommand. |
||||
See [passthrough arguments](#passthrough-arguments) for more details. |
||||
|
||||
It is also possible to pass any other `cargo` command (e.g. `doc`, `check`), |
||||
and all its arguments will be passed through directly to `cargo` unmodified, |
||||
with the proper `--target armv6k-nintendo-3ds` set. |
||||
|
||||
### Basic Examples |
||||
|
||||
* `cargo 3ds build` |
||||
* `cargo 3ds check --verbose` |
||||
* `cargo 3ds run --release --example foo` |
||||
* `cargo 3ds test --no-run` |
||||
* `cargo 3ds new my-new-project --edition 2021` |
||||
|
||||
### Running executables |
||||
|
||||
`cargo 3ds test` and `cargo 3ds run` use the `3dslink` tool to send built |
||||
executables to a device, and thus accept specific related arguments that correspond |
||||
to `3dslink` arguments: |
||||
|
||||
```txt |
||||
-a, --address <ADDRESS> |
||||
Specify the IP address of the device to send the executable to. |
||||
|
||||
Corresponds to 3dslink's `--address` arg, which defaults to automatically finding the device. |
||||
|
||||
-0, --argv0 <ARGV0> |
||||
Set the 0th argument of the executable when running it. Corresponds to 3dslink's `--argv0` argument |
||||
|
||||
-s, --server |
||||
Start the 3dslink server after sending the executable. Corresponds to 3dslink's `--server` argument |
||||
|
||||
--retries <RETRIES> |
||||
Set the number of tries when connecting to the device to send the executable. Corresponds to 3dslink's `--retries` argument |
||||
``` |
||||
|
||||
### Passthrough Arguments |
||||
|
||||
Due to the way `cargo-3ds`, `cargo`, and `3dslink` parse arguments, there is |
||||
unfortunately some complexity required when invoking an executable with arguments. |
||||
|
||||
All unrecognized arguments beginning with a dash (e.g. `--release`, `--features`, |
||||
etc.) will be passed through to `cargo` directly. |
||||
|
||||
> **NOTE:** arguments for [running executables](#running-executables) must be |
||||
> specified *before* other unrecognized `cargo` arguments. Otherwise they will |
||||
> be treated as passthrough arguments which will most likely fail the resulting |
||||
> `cargo` command. |
||||
|
||||
An optional `--` may be used to explicitly pass subsequent args to `cargo`, including |
||||
arguments to pass to the executable itself. To separate `cargo` arguments from |
||||
executable arguments, *another* `--` can be used. For example: |
||||
|
||||
* `cargo 3ds run -- -- xyz` |
||||
|
||||
Builds an executable and send it to a device to run it with the argument `xyz`. |
||||
|
||||
* `cargo 3ds test --address 192.168.0.2 -- -- --test-arg 1` |
||||
|
||||
Builds a test executable and attempts to send it to a device with the |
||||
address `192.168.0.2` and run it using the arguments `["--test-arg", "1"]`. |
||||
|
||||
* `cargo 3ds test --address 192.168.0.2 --verbose -- --test-arg 1` |
||||
|
||||
Build a test executable with `cargo test --verbose`, and attempts to send |
||||
it to a device with the address `192.168.0.2` and run it using the arguments |
||||
`["--test-arg", "1"]`. |
||||
|
||||
This works without two `--` instances because `--verbose` begins the set of |
||||
`cargo` arguments and ends the set of 3DS-specific arguments. |
||||
|
||||
### Caveats |
||||
|
||||
Due to the fact that only one executable at a time can be sent with `3dslink`, |
||||
by default only the "last" executable built will be used. If a `test` or `run` |
||||
command builds more than one binary, you may need to filter it in order to run |
||||
the executable you want. |
||||
|
||||
# Examples: |
||||
`cargo +nightly 3ds build` \ |
||||
`cargo +nightly 3ds run --release` |
||||
Doc tests sort of work, but `cargo-3ds` uses a number of unstable cargo and |
||||
rustdoc features to make them work, so the output won't be as pretty and will |
||||
require some manual workarounds to actually run the tests and see output from them. |
||||
For now, `cargo 3ds test --doc` will not build a 3dsx file or use `3dslink` at all. |
||||
|
||||
You can pass or not `--release` to build with debug symbols or not, and this works for both `build` and `run`. |
||||
## License |
||||
|
||||
Any other parameters you pass after the command will be passed during the compiling stage to `cargo`. |
||||
This project is distributed under the MIT license or the Apache-2.0 license. |
||||
|
@ -0,0 +1,694 @@
@@ -0,0 +1,694 @@
|
||||
use std::fs; |
||||
use std::io::Read; |
||||
use std::process::Stdio; |
||||
use std::sync::OnceLock; |
||||
|
||||
use cargo_metadata::Message; |
||||
use clap::{Args, Parser, Subcommand}; |
||||
|
||||
use crate::{build_3dsx, build_smdh, cargo, get_metadata, link, print_command, CTRConfig}; |
||||
|
||||
#[derive(Parser, Debug)] |
||||
#[command(name = "cargo", bin_name = "cargo")] |
||||
pub enum Cargo { |
||||
#[command(name = "3ds")] |
||||
Input(Input), |
||||
} |
||||
|
||||
#[derive(Args, Debug)] |
||||
#[command(version, about)] |
||||
pub struct Input { |
||||
#[command(subcommand)] |
||||
pub cmd: CargoCmd, |
||||
|
||||
/// Print the exact commands `cargo-3ds` is running. Note that this does not
|
||||
/// set the verbose flag for cargo itself. To set cargo's verbosity flag, add
|
||||
/// `-- -v` to the end of the command line.
|
||||
#[arg(long, short = 'v', global = true)] |
||||
pub verbose: bool, |
||||
|
||||
/// Set cargo configuration on the command line. This is equivalent to
|
||||
/// cargo's `--config` option.
|
||||
#[arg(long, global = true)] |
||||
pub config: Vec<String>, |
||||
} |
||||
|
||||
/// Run a cargo command. COMMAND will be forwarded to the real
|
||||
/// `cargo` with the appropriate arguments for the 3DS target.
|
||||
///
|
||||
/// If an unrecognized COMMAND is used, it will be passed through unmodified
|
||||
/// to `cargo` with the appropriate flags set for the 3DS target.
|
||||
#[derive(Subcommand, Debug)] |
||||
#[command(allow_external_subcommands = true)] |
||||
pub enum CargoCmd { |
||||
/// Builds an executable suitable to run on a 3DS (3dsx).
|
||||
Build(Build), |
||||
|
||||
/// Builds an executable and sends it to a device with `3dslink`.
|
||||
Run(Run), |
||||
|
||||
/// Builds a test executable and sends it to a device with `3dslink`.
|
||||
///
|
||||
/// This can be used with `--test` for integration tests, or `--lib` for
|
||||
/// unit tests (which require a custom test runner).
|
||||
Test(Test), |
||||
|
||||
/// Sets up a new cargo project suitable to run on a 3DS.
|
||||
New(New), |
||||
|
||||
// NOTE: it seems docstring + name for external subcommands are not rendered
|
||||
// in help, but we might as well set them here in case a future version of clap
|
||||
// does include them in help text.
|
||||
/// Run any other `cargo` command with custom building tailored for the 3DS.
|
||||
#[command(external_subcommand, name = "COMMAND")] |
||||
Passthrough(Vec<String>), |
||||
} |
||||
|
||||
#[derive(Args, Debug)] |
||||
pub struct RemainingArgs { |
||||
/// Pass additional options through to the `cargo` command.
|
||||
///
|
||||
/// All arguments after the first `--`, or starting with the first unrecognized
|
||||
/// option, will be passed through to `cargo` unmodified.
|
||||
///
|
||||
/// To pass arguments to an executable being run, a *second* `--` must be
|
||||
/// used to disambiguate cargo arguments from executable arguments.
|
||||
/// For example, `cargo 3ds run -- -- xyz` runs an executable with the argument
|
||||
/// `xyz`.
|
||||
#[arg(
|
||||
trailing_var_arg = true, |
||||
allow_hyphen_values = true, |
||||
value_name = "CARGO_ARGS" |
||||
)] |
||||
args: Vec<String>, |
||||
} |
||||
|
||||
#[derive(Args, Debug)] |
||||
pub struct Build { |
||||
#[arg(from_global)] |
||||
pub verbose: bool, |
||||
|
||||
// Passthrough cargo options.
|
||||
#[command(flatten)] |
||||
pub passthrough: RemainingArgs, |
||||
} |
||||
|
||||
#[derive(Args, Debug)] |
||||
pub struct Run { |
||||
/// Specify the IP address of the device to send the executable to.
|
||||
///
|
||||
/// Corresponds to 3dslink's `--address` arg, which defaults to automatically
|
||||
/// finding the device.
|
||||
#[arg(long, short = 'a')] |
||||
pub address: Option<std::net::Ipv4Addr>, |
||||
|
||||
/// Set the 0th argument of the executable when running it. Corresponds to
|
||||
/// 3dslink's `--argv0` argument.
|
||||
#[arg(long, short = '0')] |
||||
pub argv0: Option<String>, |
||||
|
||||
/// Start the 3dslink server after sending the executable. Corresponds to
|
||||
/// 3dslink's `--server` argument.
|
||||
#[arg(long, short = 's', default_value_t = false)] |
||||
pub server: bool, |
||||
|
||||
/// Set the number of tries when connecting to the device to send the executable.
|
||||
/// Corresponds to 3dslink's `--retries` argument.
|
||||
// Can't use `short = 'r'` because that would conflict with cargo's `--release/-r`
|
||||
#[arg(long)] |
||||
pub retries: Option<usize>, |
||||
|
||||
// Passthrough `cargo build` options.
|
||||
#[command(flatten)] |
||||
pub build_args: Build, |
||||
|
||||
#[arg(from_global)] |
||||
config: Vec<String>, |
||||
} |
||||
|
||||
#[derive(Args, Debug)] |
||||
pub struct Test { |
||||
/// If set, the built executable will not be sent to the device to run it.
|
||||
#[arg(long)] |
||||
pub no_run: bool, |
||||
|
||||
/// If set, documentation tests will be built instead of unit tests.
|
||||
/// This implies `--no-run`, unless Cargo's `target.armv6k-nintendo-3ds.runner`
|
||||
/// is configured.
|
||||
#[arg(long)] |
||||
pub doc: bool, |
||||
|
||||
// The test command uses a superset of the same arguments as Run.
|
||||
#[command(flatten)] |
||||
pub run_args: Run, |
||||
} |
||||
|
||||
#[derive(Args, Debug)] |
||||
pub struct New { |
||||
/// Path of the new project.
|
||||
#[arg(required = true)] |
||||
pub path: String, |
||||
|
||||
// The test command uses a superset of the same arguments as Run.
|
||||
#[command(flatten)] |
||||
pub cargo_args: RemainingArgs, |
||||
} |
||||
|
||||
impl CargoCmd { |
||||
/// Returns the additional arguments run by the "official" cargo subcommand.
|
||||
pub fn cargo_args(&self) -> Vec<String> { |
||||
match self { |
||||
CargoCmd::Build(build) => build.passthrough.cargo_args(), |
||||
CargoCmd::Run(run) => run.build_args.passthrough.cargo_args(), |
||||
CargoCmd::Test(test) => test.cargo_args(), |
||||
CargoCmd::New(new) => { |
||||
// We push the original path in the new command (we captured it in [`New`] to learn about the context)
|
||||
let mut cargo_args = new.cargo_args.cargo_args(); |
||||
cargo_args.push(new.path.clone()); |
||||
|
||||
cargo_args |
||||
} |
||||
CargoCmd::Passthrough(other) => other.clone().split_off(1), |
||||
} |
||||
} |
||||
|
||||
/// Returns the cargo subcommand run by `cargo-3ds` when handling a [`CargoCmd`].
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// This is not equivalent to the lowercase name of the [`CargoCmd`] variant.
|
||||
/// Commands may use different commands under the hood to function (e.g. [`CargoCmd::Run`] uses `build`
|
||||
/// if no custom runner is configured).
|
||||
pub fn subcommand_name(&self) -> &str { |
||||
match self { |
||||
CargoCmd::Build(_) => "build", |
||||
CargoCmd::Run(run) => { |
||||
if run.use_custom_runner() { |
||||
"run" |
||||
} else { |
||||
"build" |
||||
} |
||||
} |
||||
CargoCmd::Test(_) => "test", |
||||
CargoCmd::New(_) => "new", |
||||
CargoCmd::Passthrough(cmd) => &cmd[0], |
||||
} |
||||
} |
||||
|
||||
/// Whether or not this command should compile any code, and thus needs import the custom environment configuration (e.g. target spec).
|
||||
pub fn should_compile(&self) -> bool { |
||||
matches!( |
||||
self, |
||||
Self::Build(_) | Self::Run(_) | Self::Test(_) | Self::Passthrough(_) |
||||
) |
||||
} |
||||
|
||||
/// Whether or not this command should build a 3DSX executable file.
|
||||
pub fn should_build_3dsx(&self) -> bool { |
||||
match self { |
||||
Self::Build(_) | CargoCmd::Run(_) => true, |
||||
&Self::Test(Test { doc, .. }) => { |
||||
if doc { |
||||
eprintln!("Documentation tests requested, no 3dsx will be built"); |
||||
false |
||||
} else { |
||||
true |
||||
} |
||||
} |
||||
_ => false, |
||||
} |
||||
} |
||||
|
||||
/// Whether or not the resulting executable should be sent to the 3DS with
|
||||
/// `3dslink`.
|
||||
pub fn should_link_to_device(&self) -> bool { |
||||
match self { |
||||
Self::Test(Test { no_run: true, .. }) => false, |
||||
Self::Run(run) | Self::Test(Test { run_args: run, .. }) => !run.use_custom_runner(), |
||||
_ => false, |
||||
} |
||||
} |
||||
|
||||
pub const DEFAULT_MESSAGE_FORMAT: &'static str = "json-render-diagnostics"; |
||||
|
||||
pub fn extract_message_format(&mut self) -> Result<Option<String>, String> { |
||||
let cargo_args = match self { |
||||
Self::Build(build) => &mut build.passthrough.args, |
||||
Self::Run(run) => &mut run.build_args.passthrough.args, |
||||
Self::New(new) => &mut new.cargo_args.args, |
||||
Self::Test(test) => &mut test.run_args.build_args.passthrough.args, |
||||
Self::Passthrough(args) => args, |
||||
}; |
||||
|
||||
let format = Self::extract_message_format_from_args(cargo_args)?; |
||||
if format.is_some() { |
||||
return Ok(format); |
||||
} |
||||
|
||||
if let Self::Test(Test { doc: true, .. }) = self { |
||||
// We don't care about JSON output for doctests since we're not
|
||||
// building any 3dsx etc. Just use the default output as it's more
|
||||
// readable compared to DEFAULT_MESSAGE_FORMAT
|
||||
Ok(Some(String::from("human"))) |
||||
} else { |
||||
Ok(None) |
||||
} |
||||
} |
||||
|
||||
fn extract_message_format_from_args( |
||||
cargo_args: &mut Vec<String>, |
||||
) -> Result<Option<String>, String> { |
||||
// Checks for a position within the args where '--message-format' is located
|
||||
if let Some(pos) = cargo_args |
||||
.iter() |
||||
.position(|s| s.starts_with("--message-format")) |
||||
{ |
||||
// Remove the arg from list so we don't pass anything twice by accident
|
||||
let arg = cargo_args.remove(pos); |
||||
|
||||
// Allows for usage of '--message-format=<format>' and also using space separation.
|
||||
// Check for a '=' delimiter and use the second half of the split as the format,
|
||||
// otherwise remove next arg which is now at the same position as the original flag.
|
||||
let format = if let Some((_, format)) = arg.split_once('=') { |
||||
format.to_string() |
||||
} else { |
||||
// Also need to remove the argument to the --message-format option
|
||||
cargo_args.remove(pos) |
||||
}; |
||||
|
||||
// Non-json formats are not supported so the executable exits.
|
||||
if format.starts_with("json") { |
||||
Ok(Some(format)) |
||||
} else { |
||||
Err(String::from( |
||||
"error: non-JSON `message-format` is not supported", |
||||
)) |
||||
} |
||||
} else { |
||||
Ok(None) |
||||
} |
||||
} |
||||
|
||||
/// Runs the custom callback *after* the cargo command, depending on the type of command launched.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// - `cargo 3ds build` and other "build" commands will use their callbacks to build the final `.3dsx` file and link it.
|
||||
/// - `cargo 3ds new` and other generic commands will use their callbacks to make 3ds-specific changes to the environment.
|
||||
pub fn run_callback(&self, messages: &[Message]) { |
||||
// Process the metadata only for commands that have it/use it
|
||||
let config = if self.should_build_3dsx() { |
||||
eprintln!("Getting metadata"); |
||||
|
||||
Some(get_metadata(messages)) |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
// Run callback only for commands that use it
|
||||
match self { |
||||
Self::Build(cmd) => cmd.callback(&config), |
||||
Self::Run(cmd) => cmd.callback(&config), |
||||
Self::Test(cmd) => cmd.callback(&config), |
||||
Self::New(cmd) => cmd.callback(), |
||||
_ => (), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl RemainingArgs { |
||||
/// Get the args to be passed to `cargo`.
|
||||
pub fn cargo_args(&self) -> Vec<String> { |
||||
self.split_args().0 |
||||
} |
||||
|
||||
/// Get the args to be passed to the executable itself (not `cargo`).
|
||||
pub fn exe_args(&self) -> Vec<String> { |
||||
self.split_args().1 |
||||
} |
||||
|
||||
fn split_args(&self) -> (Vec<String>, Vec<String>) { |
||||
let mut args = self.args.clone(); |
||||
|
||||
if let Some(split) = args.iter().position(|s| s == "--") { |
||||
let second_half = args.split_off(split + 1); |
||||
// take off the "--" arg we found, we'll add one later if needed
|
||||
args.pop(); |
||||
|
||||
(args, second_half) |
||||
} else { |
||||
(args, Vec::new()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Build { |
||||
/// Callback for `cargo 3ds build`.
|
||||
///
|
||||
/// This callback handles building the application as a `.3dsx` file.
|
||||
fn callback(&self, config: &Option<CTRConfig>) { |
||||
if let Some(config) = config { |
||||
eprintln!("Building smdh: {}", config.path_smdh().display()); |
||||
build_smdh(config, self.verbose); |
||||
|
||||
eprintln!("Building 3dsx: {}", config.path_3dsx().display()); |
||||
build_3dsx(config, self.verbose); |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Run { |
||||
/// Get the args to pass to `3dslink` based on these options.
|
||||
pub fn get_3dslink_args(&self) -> Vec<String> { |
||||
let mut args = Vec::new(); |
||||
|
||||
if let Some(address) = self.address { |
||||
args.extend(["--address".to_string(), address.to_string()]); |
||||
} |
||||
|
||||
if let Some(argv0) = &self.argv0 { |
||||
args.extend(["--arg0".to_string(), argv0.clone()]); |
||||
} |
||||
|
||||
if let Some(retries) = self.retries { |
||||
args.extend(["--retries".to_string(), retries.to_string()]); |
||||
} |
||||
|
||||
if self.server { |
||||
args.push("--server".to_string()); |
||||
} |
||||
|
||||
let exe_args = self.build_args.passthrough.exe_args(); |
||||
if !exe_args.is_empty() { |
||||
// For some reason 3dslink seems to want 2 instances of `--`, one
|
||||
// in front of all of the args like this...
|
||||
args.extend(["--args".to_string(), "--".to_string()]); |
||||
|
||||
let mut escaped = false; |
||||
for arg in exe_args.iter().cloned() { |
||||
if arg.starts_with('-') && !escaped { |
||||
// And one before the first `-` arg that is passed in.
|
||||
args.extend(["--".to_string(), arg]); |
||||
escaped = true; |
||||
} else { |
||||
args.push(arg); |
||||
} |
||||
} |
||||
} |
||||
|
||||
args |
||||
} |
||||
|
||||
/// Callback for `cargo 3ds run`.
|
||||
///
|
||||
/// This callback handles launching the application via `3dslink`.
|
||||
fn callback(&self, config: &Option<CTRConfig>) { |
||||
// Run the normal "build" callback
|
||||
self.build_args.callback(config); |
||||
|
||||
if !self.use_custom_runner() { |
||||
if let Some(cfg) = config { |
||||
eprintln!("Running 3dslink"); |
||||
link(cfg, self, self.build_args.verbose); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Returns whether the cargo environment has `target.armv6k-nintendo-3ds.runner`
|
||||
/// configured. This will only be checked once during the lifetime of the program,
|
||||
/// and takes into account the usual ways Cargo looks for its
|
||||
/// [configuration](https://doc.rust-lang.org/cargo/reference/config.html):
|
||||
///
|
||||
/// - `.cargo/config.toml`
|
||||
/// - Environment variables
|
||||
/// - Command-line `--config` overrides
|
||||
pub fn use_custom_runner(&self) -> bool { |
||||
static HAS_RUNNER: OnceLock<bool> = OnceLock::new(); |
||||
|
||||
let &custom_runner_configured = HAS_RUNNER.get_or_init(|| { |
||||
let mut cmd = cargo(&self.config); |
||||
cmd.args([ |
||||
// https://github.com/rust-lang/cargo/issues/9301
|
||||
"-Z", |
||||
"unstable-options", |
||||
"config", |
||||
"get", |
||||
"target.armv6k-nintendo-3ds.runner", |
||||
]) |
||||
.stdout(Stdio::null()) |
||||
.stderr(Stdio::null()); |
||||
|
||||
if self.build_args.verbose { |
||||
print_command(&cmd); |
||||
} |
||||
|
||||
// `cargo config get` exits zero if the config exists, or nonzero otherwise
|
||||
cmd.status().map_or(false, |status| status.success()) |
||||
}); |
||||
|
||||
if self.build_args.verbose { |
||||
eprintln!( |
||||
"Custom runner is {}configured", |
||||
if custom_runner_configured { "" } else { "not " } |
||||
); |
||||
} |
||||
|
||||
custom_runner_configured |
||||
} |
||||
} |
||||
|
||||
impl Test { |
||||
/// Callback for `cargo 3ds test`.
|
||||
///
|
||||
/// This callback handles launching the application via `3dslink`.
|
||||
fn callback(&self, config: &Option<CTRConfig>) { |
||||
if self.no_run { |
||||
// If the tests don't have to run, use the "build" callback
|
||||
self.run_args.build_args.callback(config); |
||||
} else { |
||||
// If the tests have to run, use the "run" callback
|
||||
self.run_args.callback(config); |
||||
} |
||||
} |
||||
|
||||
fn should_run(&self) -> bool { |
||||
self.run_args.use_custom_runner() && !self.no_run |
||||
} |
||||
|
||||
/// The args to pass to the underlying `cargo test` command.
|
||||
fn cargo_args(&self) -> Vec<String> { |
||||
let mut cargo_args = self.run_args.build_args.passthrough.cargo_args(); |
||||
|
||||
// We can't run 3DS executables on the host, but we want to respect
|
||||
// the user's "runner" configuration if set.
|
||||
//
|
||||
// If doctests were requested, `--no-run` will be rejected on the
|
||||
// command line and must be set with RUSTDOCFLAGS instead:
|
||||
// https://github.com/rust-lang/rust/issues/87022
|
||||
|
||||
if self.doc { |
||||
cargo_args.extend([ |
||||
"--doc".into(), |
||||
// https://github.com/rust-lang/cargo/issues/7040
|
||||
"-Z".into(), |
||||
"doctest-xcompile".into(), |
||||
]); |
||||
} else if !self.should_run() { |
||||
cargo_args.push("--no-run".into()); |
||||
} |
||||
|
||||
cargo_args |
||||
} |
||||
|
||||
/// Flags to pass to rustdoc via RUSTDOCFLAGS
|
||||
pub(crate) fn rustdocflags(&self) -> &'static str { |
||||
if self.should_run() { |
||||
"" |
||||
} else { |
||||
// We don't support running doctests by default, but cargo doesn't like
|
||||
// --no-run for doctests, so we have to plumb it in via RUSTDOCFLAGS
|
||||
" --no-run" |
||||
} |
||||
} |
||||
} |
||||
|
||||
const TOML_CHANGES: &str = r#"ctru-rs = { git = "https://git.xenua.me/rust3ds/ctru-rs" } |
||||
|
||||
[package.metadata.cargo-3ds] |
||||
romfs_dir = "romfs" |
||||
"#; |
||||
|
||||
const CUSTOM_MAIN_RS: &str = r#"use ctru::prelude::*; |
||||
|
||||
fn main() { |
||||
let apt = Apt::new().unwrap(); |
||||
let mut hid = Hid::new().unwrap(); |
||||
let gfx = Gfx::new().unwrap(); |
||||
let _console = Console::new(gfx.top_screen.borrow_mut()); |
||||
|
||||
println!("Hello, World!"); |
||||
println!("\x1b[29;16HPress Start to exit"); |
||||
|
||||
while apt.main_loop() { |
||||
gfx.wait_for_vblank(); |
||||
|
||||
hid.scan_input(); |
||||
if hid.keys_down().contains(KeyPad::START) { |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
"#; |
||||
|
||||
impl New { |
||||
/// Callback for `cargo 3ds new`.
|
||||
///
|
||||
/// This callback handles the custom environment modifications when creating a new 3DS project.
|
||||
fn callback(&self) { |
||||
// Commmit changes to the project only if is meant to be a binary
|
||||
if self.cargo_args.args.contains(&"--lib".to_string()) { |
||||
return; |
||||
} |
||||
|
||||
// Attain a canonicalised path for the new project and it's TOML manifest
|
||||
let project_path = fs::canonicalize(&self.path).unwrap(); |
||||
let toml_path = project_path.join("Cargo.toml"); |
||||
let romfs_path = project_path.join("romfs"); |
||||
let main_rs_path = project_path.join("src/main.rs"); |
||||
|
||||
// Create the "romfs" directory
|
||||
fs::create_dir(romfs_path).unwrap(); |
||||
|
||||
// Read the contents of `Cargo.toml` to a string
|
||||
let mut buf = String::new(); |
||||
fs::File::open(&toml_path) |
||||
.unwrap() |
||||
.read_to_string(&mut buf) |
||||
.unwrap(); |
||||
|
||||
// Add the custom changes to the TOML
|
||||
let buf = buf + TOML_CHANGES; |
||||
fs::write(&toml_path, buf).unwrap(); |
||||
|
||||
// Add the custom changes to the main.rs file
|
||||
fs::write(main_rs_path, CUSTOM_MAIN_RS).unwrap(); |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use clap::CommandFactory; |
||||
|
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn verify_app() { |
||||
Cargo::command().debug_assert(); |
||||
} |
||||
|
||||
#[test] |
||||
fn extract_format() { |
||||
const CASES: &[(&[&str], Option<&str>)] = &[ |
||||
(&["--foo", "--message-format=json", "bar"], Some("json")), |
||||
(&["--foo", "--message-format", "json", "bar"], Some("json")), |
||||
( |
||||
&[ |
||||
"--foo", |
||||
"--message-format", |
||||
"json-render-diagnostics", |
||||
"bar", |
||||
], |
||||
Some("json-render-diagnostics"), |
||||
), |
||||
( |
||||
&["--foo", "--message-format=json-render-diagnostics", "bar"], |
||||
Some("json-render-diagnostics"), |
||||
), |
||||
(&["--foo", "bar"], None), |
||||
]; |
||||
|
||||
for (args, expected) in CASES { |
||||
let mut cmd = CargoCmd::Build(Build { |
||||
passthrough: RemainingArgs { |
||||
args: args.iter().map(ToString::to_string).collect(), |
||||
}, |
||||
verbose: false, |
||||
}); |
||||
|
||||
assert_eq!( |
||||
cmd.extract_message_format().unwrap(), |
||||
expected.map(ToString::to_string) |
||||
); |
||||
|
||||
if let CargoCmd::Build(build) = cmd { |
||||
assert_eq!(build.passthrough.args, vec!["--foo", "bar"]); |
||||
} else { |
||||
unreachable!(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn extract_format_err() { |
||||
for args in [&["--message-format=foo"][..], &["--message-format", "foo"]] { |
||||
let mut cmd = CargoCmd::Build(Build { |
||||
passthrough: RemainingArgs { |
||||
args: args.iter().map(ToString::to_string).collect(), |
||||
}, |
||||
verbose: false, |
||||
}); |
||||
|
||||
assert!(cmd.extract_message_format().is_err()); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn split_run_args() { |
||||
struct TestParam { |
||||
input: &'static [&'static str], |
||||
expected_cargo: &'static [&'static str], |
||||
expected_exe: &'static [&'static str], |
||||
} |
||||
|
||||
for param in [ |
||||
TestParam { |
||||
input: &["--example", "hello-world", "--no-default-features"], |
||||
expected_cargo: &["--example", "hello-world", "--no-default-features"], |
||||
expected_exe: &[], |
||||
}, |
||||
TestParam { |
||||
input: &["--example", "hello-world", "--", "--do-stuff", "foo"], |
||||
expected_cargo: &["--example", "hello-world"], |
||||
expected_exe: &["--do-stuff", "foo"], |
||||
}, |
||||
TestParam { |
||||
input: &["--lib", "--", "foo"], |
||||
expected_cargo: &["--lib"], |
||||
expected_exe: &["foo"], |
||||
}, |
||||
TestParam { |
||||
input: &["foo", "--", "bar"], |
||||
expected_cargo: &["foo"], |
||||
expected_exe: &["bar"], |
||||
}, |
||||
] { |
||||
let input: Vec<&str> = ["cargo", "3ds", "run"] |
||||
.iter() |
||||
.chain(param.input) |
||||
.copied() |
||||
.collect(); |
||||
|
||||
dbg!(&input); |
||||
let Cargo::Input(Input { |
||||
cmd: CargoCmd::Run(Run { build_args, .. }), |
||||
.. |
||||
}) = Cargo::try_parse_from(input).unwrap_or_else(|e| panic!("{e}")) |
||||
else { |
||||
panic!("parsed as something other than `run` subcommand") |
||||
}; |
||||
|
||||
assert_eq!(build_args.passthrough.cargo_args(), param.expected_cargo); |
||||
assert_eq!(build_args.passthrough.exe_args(), param.expected_exe); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,90 @@
@@ -0,0 +1,90 @@
|
||||
use std::error::Error; |
||||
use std::io::Read; |
||||
use std::process::{Command, Stdio}; |
||||
|
||||
use cargo_metadata::Target; |
||||
use serde::Deserialize; |
||||
|
||||
use crate::print_command; |
||||
|
||||
/// In lieu of <https://github.com/oli-obk/cargo_metadata/issues/107>
|
||||
/// and to avoid pulling in the real `cargo`
|
||||
/// [data structures](https://docs.rs/cargo/latest/cargo/core/compiler/unit_graph/type.UnitGraph.html)
|
||||
/// as a dependency, we define the subset of the build graph we care about.
|
||||
#[derive(Deserialize)] |
||||
pub struct UnitGraph { |
||||
pub version: i32, |
||||
pub units: Vec<Unit>, |
||||
} |
||||
|
||||
impl UnitGraph { |
||||
/// Collect the unit graph via Cargo's `--unit-graph` flag.
|
||||
/// This runs the same command as the actual build, except nothing is actually
|
||||
/// build and the graph is output instead.
|
||||
///
|
||||
/// See <https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#unit-graph>.
|
||||
pub fn from_cargo(cargo_cmd: &Command, verbose: bool) -> Result<Self, Box<dyn Error>> { |
||||
// Since Command isn't Clone, copy it "by hand", by copying its args and envs
|
||||
let mut cmd = Command::new(cargo_cmd.get_program()); |
||||
|
||||
let mut args = cargo_cmd.get_args(); |
||||
cmd.args(args.next()) |
||||
// These options must be added before any possible `--`, so the best
|
||||
// place is to just stick them immediately after the first arg (subcommand)
|
||||
.args(["-Z", "unstable-options", "--unit-graph"]) |
||||
.args(args) |
||||
.envs(cargo_cmd.get_envs().filter_map(|(k, v)| Some((k, v?)))) |
||||
.stdout(Stdio::piped()) |
||||
.stderr(Stdio::piped()); |
||||
|
||||
if verbose { |
||||
print_command(&cmd); |
||||
} |
||||
|
||||
let mut proc = cmd.spawn()?; |
||||
let stdout = proc.stdout.take().unwrap(); |
||||
let mut stderr = proc.stderr.take().unwrap(); |
||||
|
||||
let result: Self = serde_json::from_reader(stdout).map_err(|err| { |
||||
let mut stderr_str = String::new(); |
||||
let _ = stderr.read_to_string(&mut stderr_str); |
||||
|
||||
let _ = proc.wait(); |
||||
format!("unable to parse `--unit-graph` json: {err}\nstderr: `{stderr_str}`") |
||||
})?; |
||||
|
||||
let _status = proc.wait()?; |
||||
// TODO: with cargo 1.74.0-nightly (b4ddf95ad 2023-09-18),
|
||||
// `cargo run --unit-graph` panics at src/cargo/ops/cargo_run.rs:83:5
|
||||
// It seems to have been fixed as of cargo 1.76.0-nightly (71cd3a926 2023-11-20)
|
||||
// so maybe we can stop ignoring it once we bump the minimum toolchain version,
|
||||
// and certainly we should once `--unit-graph` is ever stabilized.
|
||||
//
|
||||
// if !status.success() {
|
||||
// return Err(format!("`cargo --unit-graph` exited with status {status:?}").into());
|
||||
// }
|
||||
|
||||
if result.version == 1 { |
||||
Ok(result) |
||||
} else { |
||||
Err(format!( |
||||
"unknown `cargo --unit-graph` output version {}", |
||||
result.version |
||||
))? |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Deserialize)] |
||||
pub struct Unit { |
||||
pub target: Target, |
||||
pub profile: Profile, |
||||
} |
||||
|
||||
/// This struct is very similar to [`cargo_metadata::ArtifactProfile`], but seems
|
||||
/// to have some slight differences so we define a different version. We only
|
||||
/// really care about `debuginfo` anyway.
|
||||
#[derive(Deserialize)] |
||||
pub struct Profile { |
||||
pub debuginfo: Option<u32>, |
||||
} |
@ -0,0 +1,490 @@
@@ -0,0 +1,490 @@
|
||||
pub mod command; |
||||
mod graph; |
||||
|
||||
use core::fmt; |
||||
use std::ffi::OsStr; |
||||
use std::io::{BufRead, BufReader}; |
||||
use std::path::{Path, PathBuf}; |
||||
use std::process::{Command, ExitStatus, Stdio}; |
||||
use std::{env, io, process}; |
||||
|
||||
use cargo_metadata::{Message, MetadataCommand}; |
||||
use command::{Input, Test}; |
||||
use rustc_version::Channel; |
||||
use semver::Version; |
||||
use tee::TeeReader; |
||||
|
||||
use crate::command::{CargoCmd, Run}; |
||||
use crate::graph::UnitGraph; |
||||
|
||||
/// Build a command using [`make_cargo_build_command`] and execute it,
|
||||
/// parsing and returning the messages from the spawned process.
|
||||
///
|
||||
/// For commands that produce an executable output, this function will build the
|
||||
/// `.elf` binary that can be used to create other 3ds files.
|
||||
pub fn run_cargo(input: &Input, message_format: Option<String>) -> (ExitStatus, Vec<Message>) { |
||||
let mut command = make_cargo_command(input, &message_format); |
||||
|
||||
if input.cmd.should_compile() { |
||||
let libctru = if should_use_ctru_debuginfo(&command, input.verbose) { |
||||
"ctrud" |
||||
} else { |
||||
"ctru" |
||||
}; |
||||
|
||||
let rustflags = command |
||||
.get_envs() |
||||
.find(|(var, _)| var == &OsStr::new("RUSTFLAGS")) |
||||
.and_then(|(_, flags)| flags) |
||||
.unwrap_or_default() |
||||
.to_string_lossy(); |
||||
|
||||
let rustflags = format!("{rustflags} -l{libctru}"); |
||||
|
||||
command.env("RUSTFLAGS", rustflags); |
||||
} |
||||
|
||||
if input.verbose { |
||||
print_command(&command); |
||||
} |
||||
|
||||
let mut process = command.spawn().unwrap(); |
||||
let command_stdout = process.stdout.take().unwrap(); |
||||
|
||||
let mut tee_reader; |
||||
let mut stdout_reader; |
||||
|
||||
let buf_reader: &mut dyn BufRead = match (message_format, &input.cmd) { |
||||
// The user presumably cares about the message format if set, so we should
|
||||
// copy stuff to stdout like they expect. We can still extract the executable
|
||||
// information out of it that we need for 3dsxtool etc.
|
||||
(Some(_), _) | |
||||
// Rustdoc unfortunately prints to stdout for compile errors, so
|
||||
// we also use a tee when building doc tests too.
|
||||
// Possibly related: https://github.com/rust-lang/rust/issues/75135
|
||||
(None, CargoCmd::Test(Test { doc: true, .. })) => { |
||||
tee_reader = BufReader::new(TeeReader::new(command_stdout, io::stdout())); |
||||
&mut tee_reader |
||||
} |
||||
_ => { |
||||
stdout_reader = BufReader::new(command_stdout); |
||||
&mut stdout_reader |
||||
} |
||||
}; |
||||
|
||||
let messages = Message::parse_stream(buf_reader) |
||||
.collect::<io::Result<_>>() |
||||
.unwrap(); |
||||
|
||||
(process.wait().unwrap(), messages) |
||||
} |
||||
|
||||
/// Ensure that we use the same `-lctru[d]` flag that `ctru-sys` is using in its build.
|
||||
fn should_use_ctru_debuginfo(cargo_cmd: &Command, verbose: bool) -> bool { |
||||
match UnitGraph::from_cargo(cargo_cmd, verbose) { |
||||
Ok(unit_graph) => { |
||||
let Some(unit) = unit_graph |
||||
.units |
||||
.iter() |
||||
.find(|unit| unit.target.name == "ctru-sys") |
||||
else { |
||||
eprintln!("Warning: unable to check if `ctru` debuginfo should be linked: `ctru-sys` not found"); |
||||
return false; |
||||
}; |
||||
|
||||
let debuginfo = unit.profile.debuginfo.unwrap_or(0); |
||||
debuginfo > 0 |
||||
} |
||||
Err(err) => { |
||||
eprintln!("Warning: unable to check if `ctru` debuginfo should be linked: {err}"); |
||||
false |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Create a cargo command based on the context.
|
||||
///
|
||||
/// For "build" commands (which compile code, such as `cargo 3ds build` or `cargo 3ds clippy`),
|
||||
/// if there is no pre-built std detected in the sysroot, `build-std` will be used instead.
|
||||
pub fn make_cargo_command(input: &Input, message_format: &Option<String>) -> Command { |
||||
let devkitpro = |
||||
env::var("DEVKITPRO").expect("DEVKITPRO is not defined as an environment variable"); |
||||
// TODO: should we actually prepend the user's RUSTFLAGS for linking order? not sure
|
||||
let rustflags = |
||||
env::var("RUSTFLAGS").unwrap_or_default() + &format!(" -L{devkitpro}/libctru/lib"); |
||||
|
||||
let cargo_cmd = &input.cmd; |
||||
|
||||
let mut command = cargo(&input.config); |
||||
command |
||||
.arg(cargo_cmd.subcommand_name()) |
||||
.env("RUSTFLAGS", rustflags); |
||||
|
||||
// Any command that needs to compile code will run under this environment.
|
||||
// Even `clippy` and `check` need this kind of context, so we'll just assume any other `Passthrough` command uses it too.
|
||||
if cargo_cmd.should_compile() { |
||||
command |
||||
.arg("--target") |
||||
.arg("armv6k-nintendo-3ds") |
||||
.arg("--message-format") |
||||
.arg( |
||||
message_format |
||||
.as_deref() |
||||
.unwrap_or(CargoCmd::DEFAULT_MESSAGE_FORMAT), |
||||
); |
||||
|
||||
let sysroot = find_sysroot(); |
||||
if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() { |
||||
eprintln!("No pre-build std found, using build-std"); |
||||
// Always building the test crate is not ideal, but we don't know if the
|
||||
// crate being built uses #![feature(test)], so we build it just in case.
|
||||
command.arg("-Z").arg("build-std=std,test"); |
||||
} |
||||
} |
||||
|
||||
if let CargoCmd::Test(test) = cargo_cmd { |
||||
// RUSTDOCFLAGS is simply ignored if --doc wasn't passed, so we always set it.
|
||||
let rustdoc_flags = std::env::var("RUSTDOCFLAGS").unwrap_or_default() + test.rustdocflags(); |
||||
command.env("RUSTDOCFLAGS", rustdoc_flags); |
||||
} |
||||
|
||||
command.args(cargo_cmd.cargo_args()); |
||||
|
||||
if let CargoCmd::Run(run) | CargoCmd::Test(Test { run_args: run, .. }) = &cargo_cmd { |
||||
if run.use_custom_runner() { |
||||
command |
||||
.arg("--") |
||||
.args(run.build_args.passthrough.exe_args()); |
||||
} |
||||
} |
||||
|
||||
command |
||||
.stdout(Stdio::piped()) |
||||
.stdin(Stdio::inherit()) |
||||
.stderr(Stdio::inherit()); |
||||
|
||||
command |
||||
} |
||||
|
||||
/// Build a `cargo` command with the given `--config` flags.
|
||||
fn cargo(config: &[String]) -> Command { |
||||
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); |
||||
let mut cmd = Command::new(cargo); |
||||
cmd.args(config.iter().map(|cfg| format!("--config={cfg}"))); |
||||
cmd |
||||
} |
||||
|
||||
fn print_command(command: &Command) { |
||||
let mut cmd_str = vec![command.get_program().to_string_lossy().to_string()]; |
||||
cmd_str.extend(command.get_args().map(|s| s.to_string_lossy().to_string())); |
||||
|
||||
eprintln!("Running command:"); |
||||
for (k, v) in command.get_envs() { |
||||
let v = v.map(|v| v.to_string_lossy().to_string()); |
||||
eprintln!( |
||||
" {}={} \\", |
||||
k.to_string_lossy(), |
||||
v.map_or_else(String::new, |s| shlex::quote(&s).to_string()) |
||||
); |
||||
} |
||||
eprintln!(" {}\n", shlex::join(cmd_str.iter().map(String::as_str))); |
||||
} |
||||
|
||||
/// Finds the sysroot path of the current toolchain
|
||||
pub fn find_sysroot() -> PathBuf { |
||||
let sysroot = env::var("SYSROOT").ok().unwrap_or_else(|| { |
||||
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); |
||||
|
||||
let output = Command::new(&rustc) |
||||
.arg("--print") |
||||
.arg("sysroot") |
||||
.output() |
||||
.unwrap_or_else(|_| panic!("Failed to run `{rustc} -- print sysroot`")); |
||||
String::from_utf8(output.stdout).expect("Failed to parse sysroot path into a UTF-8 string") |
||||
}); |
||||
|
||||
PathBuf::from(sysroot.trim()) |
||||
} |
||||
|
||||
/// Checks the current rust version and channel.
|
||||
/// Exits if the minimum requirement is not met.
|
||||
pub fn check_rust_version() { |
||||
let rustc_version = rustc_version::version_meta().unwrap(); |
||||
|
||||
if rustc_version.channel > Channel::Nightly { |
||||
eprintln!("cargo-3ds requires a nightly rustc version."); |
||||
eprintln!( |
||||
"Please run `rustup override set nightly` to use nightly in the \ |
||||
current directory, or use `cargo +nightly 3ds` to use it for a \ |
||||
single invocation." |
||||
); |
||||
process::exit(1); |
||||
} |
||||
|
||||
let old_version = MINIMUM_RUSTC_VERSION |
||||
> Version { |
||||
// Remove `-nightly` pre-release tag for comparison.
|
||||
pre: semver::Prerelease::EMPTY, |
||||
..rustc_version.semver.clone() |
||||
}; |
||||
|
||||
let old_commit = match rustc_version.commit_date { |
||||
None => false, |
||||
Some(date) => { |
||||
MINIMUM_COMMIT_DATE |
||||
> CommitDate::parse(&date).expect("could not parse `rustc --version` commit date") |
||||
} |
||||
}; |
||||
|
||||
if old_version || old_commit { |
||||
eprintln!("cargo-3ds requires rustc nightly version >= {MINIMUM_COMMIT_DATE}"); |
||||
eprintln!("Please run `rustup update nightly` to upgrade your nightly version"); |
||||
|
||||
process::exit(1); |
||||
} |
||||
} |
||||
|
||||
/// Parses messages returned by "build" cargo commands (such as `cargo 3ds build` or `cargo 3ds run`).
|
||||
/// The returned [`CTRConfig`] is then used for further building in and execution
|
||||
/// in [`build_smdh`], [`build_3dsx`], and [`link`].
|
||||
pub fn get_metadata(messages: &[Message]) -> CTRConfig { |
||||
let metadata = MetadataCommand::new() |
||||
.no_deps() |
||||
.exec() |
||||
.expect("Failed to get cargo metadata"); |
||||
|
||||
let mut package = None; |
||||
let mut artifact = None; |
||||
|
||||
// Extract the final built executable. We may want to fail in cases where
|
||||
// multiple executables, or none, were built?
|
||||
for message in messages.iter().rev() { |
||||
if let Message::CompilerArtifact(art) = message { |
||||
if art.executable.is_some() { |
||||
package = Some(metadata[&art.package_id].clone()); |
||||
artifact = Some(art.clone()); |
||||
|
||||
break; |
||||
} |
||||
} |
||||
} |
||||
if package.is_none() || artifact.is_none() { |
||||
eprintln!("No executable found from build command output!"); |
||||
process::exit(1); |
||||
} |
||||
|
||||
let (package, artifact) = (package.unwrap(), artifact.unwrap()); |
||||
|
||||
let mut icon = String::from("./icon.png"); |
||||
|
||||
if !Path::new(&icon).exists() { |
||||
icon = format!( |
||||
"{}/libctru/default_icon.png", |
||||
env::var("DEVKITPRO").unwrap() |
||||
); |
||||
} |
||||
|
||||
// for now assume a single "kind" since we only support one output artifact
|
||||
let name = match artifact.target.kind[0].as_ref() { |
||||
"bin" | "lib" | "rlib" | "dylib" if artifact.target.test => { |
||||
format!("{} tests", artifact.target.name) |
||||
} |
||||
"example" => { |
||||
format!("{} - {} example", artifact.target.name, package.name) |
||||
} |
||||
_ => artifact.target.name, |
||||
}; |
||||
|
||||
let author = match package.authors.as_slice() { |
||||
[name, ..] => name.clone(), |
||||
[] => String::from("Unspecified Author"), // as standard with the devkitPRO toolchain
|
||||
}; |
||||
|
||||
CTRConfig { |
||||
name, |
||||
author, |
||||
description: package |
||||
.description |
||||
.clone() |
||||
.unwrap_or_else(|| String::from("Homebrew Application")), |
||||
icon, |
||||
target_path: artifact.executable.unwrap().into(), |
||||
cargo_manifest_path: package.manifest_path.into(), |
||||
} |
||||
} |
||||
|
||||
/// Builds the smdh using `smdhtool`.
|
||||
/// This will fail if `smdhtool` is not within the running directory or in a directory found in $PATH
|
||||
pub fn build_smdh(config: &CTRConfig, verbose: bool) { |
||||
let mut command = Command::new("smdhtool"); |
||||
command |
||||
.arg("--create") |
||||
.arg(&config.name) |
||||
.arg(&config.description) |
||||
.arg(&config.author) |
||||
.arg(&config.icon) |
||||
.arg(config.path_smdh()) |
||||
.stdin(Stdio::inherit()) |
||||
.stdout(Stdio::inherit()) |
||||
.stderr(Stdio::inherit()); |
||||
|
||||
if verbose { |
||||
print_command(&command); |
||||
} |
||||
|
||||
let mut process = command |
||||
.spawn() |
||||
.expect("smdhtool command failed, most likely due to 'smdhtool' not being in $PATH"); |
||||
|
||||
let status = process.wait().unwrap(); |
||||
|
||||
if !status.success() { |
||||
process::exit(status.code().unwrap_or(1)); |
||||
} |
||||
} |
||||
|
||||
/// Builds the 3dsx using `3dsxtool`.
|
||||
/// This will fail if `3dsxtool` is not within the running directory or in a directory found in $PATH
|
||||
pub fn build_3dsx(config: &CTRConfig, verbose: bool) { |
||||
let mut command = Command::new("3dsxtool"); |
||||
command |
||||
.arg(&config.target_path) |
||||
.arg(config.path_3dsx()) |
||||
.arg(format!("--smdh={}", config.path_smdh().to_string_lossy())); |
||||
|
||||
// If romfs directory exists, automatically include it
|
||||
let (romfs_path, is_default_romfs) = get_romfs_path(config); |
||||
if romfs_path.is_dir() { |
||||
eprintln!("Adding RomFS from {}", romfs_path.display()); |
||||
command.arg(format!("--romfs={}", romfs_path.to_string_lossy())); |
||||
} else if !is_default_romfs { |
||||
eprintln!( |
||||
"Could not find configured RomFS dir: {}", |
||||
romfs_path.display() |
||||
); |
||||
process::exit(1); |
||||
} |
||||
|
||||
if verbose { |
||||
print_command(&command); |
||||
} |
||||
|
||||
let mut process = command |
||||
.stdin(Stdio::inherit()) |
||||
.stdout(Stdio::inherit()) |
||||
.stderr(Stdio::inherit()) |
||||
.spawn() |
||||
.expect("3dsxtool command failed, most likely due to '3dsxtool' not being in $PATH"); |
||||
|
||||
let status = process.wait().unwrap(); |
||||
|
||||
if !status.success() { |
||||
process::exit(status.code().unwrap_or(1)); |
||||
} |
||||
} |
||||
|
||||
/// Link the generated 3dsx to a 3ds to execute and test using `3dslink`.
|
||||
/// This will fail if `3dslink` is not within the running directory or in a directory found in $PATH
|
||||
pub fn link(config: &CTRConfig, run_args: &Run, verbose: bool) { |
||||
let mut command = Command::new("3dslink"); |
||||
command |
||||
.arg(config.path_3dsx()) |
||||
.args(run_args.get_3dslink_args()) |
||||
.stdin(Stdio::inherit()) |
||||
.stdout(Stdio::inherit()) |
||||
.stderr(Stdio::inherit()); |
||||
|
||||
if verbose { |
||||
print_command(&command); |
||||
} |
||||
|
||||
let status = command.spawn().unwrap().wait().unwrap(); |
||||
|
||||
if !status.success() { |
||||
process::exit(status.code().unwrap_or(1)); |
||||
} |
||||
} |
||||
|
||||
/// Read the `RomFS` path from the Cargo manifest. If it's unset, use the default.
|
||||
/// The returned boolean is true when the default is used.
|
||||
pub fn get_romfs_path(config: &CTRConfig) -> (PathBuf, bool) { |
||||
let manifest_path = &config.cargo_manifest_path; |
||||
let manifest_str = std::fs::read_to_string(manifest_path) |
||||
.unwrap_or_else(|e| panic!("Could not open {}: {e}", manifest_path.display())); |
||||
let manifest_data: toml::Value = |
||||
toml::de::from_str(&manifest_str).expect("Could not parse Cargo manifest as TOML"); |
||||
|
||||
// Find the romfs setting and compute the path
|
||||
let mut is_default = false; |
||||
let romfs_dir_setting = manifest_data |
||||
.as_table() |
||||
.and_then(|table| table.get("package")) |
||||
.and_then(toml::Value::as_table) |
||||
.and_then(|table| table.get("metadata")) |
||||
.and_then(toml::Value::as_table) |
||||
.and_then(|table| table.get("cargo-3ds")) |
||||
.and_then(toml::Value::as_table) |
||||
.and_then(|table| table.get("romfs_dir")) |
||||
.and_then(toml::Value::as_str) |
||||
.unwrap_or_else(|| { |
||||
is_default = true; |
||||
"romfs" |
||||
}); |
||||
let mut romfs_path = manifest_path.clone(); |
||||
romfs_path.pop(); // Pop Cargo.toml
|
||||
romfs_path.push(romfs_dir_setting); |
||||
|
||||
(romfs_path, is_default) |
||||
} |
||||
|
||||
#[derive(Default)] |
||||
pub struct CTRConfig { |
||||
name: String, |
||||
author: String, |
||||
description: String, |
||||
icon: String, |
||||
target_path: PathBuf, |
||||
cargo_manifest_path: PathBuf, |
||||
} |
||||
|
||||
impl CTRConfig { |
||||
pub fn path_3dsx(&self) -> PathBuf { |
||||
self.target_path.with_extension("3dsx") |
||||
} |
||||
|
||||
pub fn path_smdh(&self) -> PathBuf { |
||||
self.target_path.with_extension("smdh") |
||||
} |
||||
} |
||||
|
||||
#[derive(Ord, PartialOrd, PartialEq, Eq, Debug)] |
||||
pub struct CommitDate { |
||||
year: i32, |
||||
month: i32, |
||||
day: i32, |
||||
} |
||||
|
||||
impl CommitDate { |
||||
fn parse(date: &str) -> Option<Self> { |
||||
let mut iter = date.split('-'); |
||||
|
||||
let year = iter.next()?.parse().ok()?; |
||||
let month = iter.next()?.parse().ok()?; |
||||
let day = iter.next()?.parse().ok()?; |
||||
|
||||
Some(Self { year, month, day }) |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for CommitDate { |
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day) |
||||
} |
||||
} |
||||
|
||||
const MINIMUM_COMMIT_DATE: CommitDate = CommitDate { |
||||
year: 2023, |
||||
month: 5, |
||||
day: 31, |
||||
}; |
||||
const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 70, 0); |
@ -1,501 +1,27 @@
@@ -1,501 +1,27 @@
|
||||
use cargo_metadata::{Message, MetadataCommand}; |
||||
use rustc_version::{Channel, Version}; |
||||
use std::io::{BufRead, BufReader}; |
||||
use std::path::{Path, PathBuf}; |
||||
use std::process::ExitStatus; |
||||
use std::{ |
||||
env, fmt, io, |
||||
process::{self, Command, Stdio}, |
||||
}; |
||||
use tee::TeeReader; |
||||
use std::process; |
||||
|
||||
#[derive(serde_derive::Deserialize, Default)] |
||||
struct CTRConfig { |
||||
name: String, |
||||
author: String, |
||||
description: String, |
||||
icon: String, |
||||
target_path: PathBuf, |
||||
cargo_manifest_path: PathBuf, |
||||
} |
||||
|
||||
impl CTRConfig { |
||||
fn path_3dsx(&self) -> PathBuf { |
||||
self.target_path.with_extension("3dsx") |
||||
} |
||||
|
||||
fn path_smdh(&self) -> PathBuf { |
||||
self.target_path.with_extension("smdh") |
||||
} |
||||
} |
||||
|
||||
#[derive(Ord, PartialOrd, PartialEq, Eq, Debug)] |
||||
struct CommitDate { |
||||
year: i32, |
||||
month: i32, |
||||
day: i32, |
||||
} |
||||
|
||||
impl CommitDate { |
||||
fn parse(date: &str) -> Option<Self> { |
||||
let mut iter = date.split('-'); |
||||
|
||||
let year = iter.next()?.parse().ok()?; |
||||
let month = iter.next()?.parse().ok()?; |
||||
let day = iter.next()?.parse().ok()?; |
||||
|
||||
Some(Self { year, month, day }) |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for CommitDate { |
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
||||
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day) |
||||
} |
||||
} |
||||
|
||||
const MINIMUM_COMMIT_DATE: CommitDate = CommitDate { |
||||
year: 2022, |
||||
month: 6, |
||||
day: 15, |
||||
}; |
||||
const MINIMUM_RUSTC_VERSION: Version = Version::new(1, 63, 0); |
||||
use cargo_3ds::command::Cargo; |
||||
use cargo_3ds::{check_rust_version, run_cargo}; |
||||
use clap::Parser; |
||||
|
||||
fn main() { |
||||
check_rust_version(); |
||||
|
||||
if env::args().any(|arg| arg == "--help" || arg == "-h") { |
||||
print_usage(&mut io::stdout()); |
||||
return; |
||||
} |
||||
|
||||
// Get the command and collect the remaining arguments
|
||||
let cargo_command = CargoCommand::from_args().unwrap_or_else(|| { |
||||
print_usage(&mut io::stderr()); |
||||
process::exit(2) |
||||
}); |
||||
|
||||
eprintln!("Running Cargo"); |
||||
let (status, messages) = cargo_command.build_elf(); |
||||
if !status.success() { |
||||
process::exit(status.code().unwrap_or(1)); |
||||
} |
||||
|
||||
if !cargo_command.should_build_3dsx() { |
||||
return; |
||||
} |
||||
|
||||
eprintln!("Getting metadata"); |
||||
let app_conf = get_metadata(&messages); |
||||
|
||||
eprintln!("Building smdh:{}", app_conf.path_smdh().display()); |
||||
build_smdh(&app_conf); |
||||
|
||||
eprintln!("Building 3dsx: {}", app_conf.path_3dsx().display()); |
||||
build_3dsx(&app_conf); |
||||
|
||||
if cargo_command.should_link { |
||||
eprintln!("Running 3dslink"); |
||||
link(&app_conf); |
||||
} |
||||
} |
||||
|
||||
struct CargoCommand { |
||||
command: String, |
||||
should_link: bool, |
||||
args: Vec<String>, |
||||
message_format: String, |
||||
} |
||||
|
||||
impl CargoCommand { |
||||
const DEFAULT_MESSAGE_FORMAT: &'static str = "json-render-diagnostics"; |
||||
|
||||
fn from_args() -> Option<Self> { |
||||
// Skip `cargo 3ds`. `cargo-3ds` isn't supported for now
|
||||
let mut args = env::args().skip(2); |
||||
|
||||
let command = args.next()?; |
||||
let mut remaining_args: Vec<String> = args.collect(); |
||||
|
||||
let (command, should_link) = match command.as_str() { |
||||
"run" => ("build".to_string(), true), |
||||
"test" => { |
||||
let no_run = String::from("--no-run"); |
||||
|
||||
if remaining_args.contains(&no_run) { |
||||
(command, false) |
||||
} else { |
||||
remaining_args.push(no_run); |
||||
(command, true) |
||||
} |
||||
} |
||||
_ => (command, false), |
||||
}; |
||||
|
||||
let message_format = match Self::extract_message_format(&mut remaining_args) { |
||||
Some(format) => { |
||||
if !format.starts_with("json") { |
||||
eprintln!("error: non-JSON `message-format` is not supported"); |
||||
process::exit(1); |
||||
} |
||||
format |
||||
} |
||||
None => Self::DEFAULT_MESSAGE_FORMAT.to_string(), |
||||
}; |
||||
|
||||
Some(Self { |
||||
command, |
||||
should_link, |
||||
args: remaining_args, |
||||
message_format, |
||||
}) |
||||
} |
||||
|
||||
fn extract_message_format(args: &mut Vec<String>) -> Option<String> { |
||||
for (i, arg) in args.iter().enumerate() { |
||||
if arg.starts_with("--message-format") { |
||||
return { |
||||
let arg = args.remove(i); |
||||
|
||||
if let Some((_, format)) = arg.split_once('=') { |
||||
Some(format.to_string()) |
||||
} else { |
||||
Some(args.remove(i)) |
||||
} |
||||
}; |
||||
} |
||||
} |
||||
|
||||
None |
||||
} |
||||
|
||||
fn build_elf(&self) -> (ExitStatus, Vec<Message>) { |
||||
let mut command = self.make_cargo_build_command(); |
||||
let mut process = command.spawn().unwrap(); |
||||
let command_stdout = process.stdout.take().unwrap(); |
||||
|
||||
let mut tee_reader; |
||||
let mut stdout_reader; |
||||
let Cargo::Input(mut input) = Cargo::parse(); |
||||
|
||||
let buf_reader: &mut dyn BufRead = if self.message_format == Self::DEFAULT_MESSAGE_FORMAT { |
||||
stdout_reader = BufReader::new(command_stdout); |
||||
&mut stdout_reader |
||||
} else { |
||||
// The user presumably cares about the message format, so we should
|
||||
// copy stuff to stdout like they expect. We can still extract the executable
|
||||
// information out of it that we need for 3dsxtool etc.
|
||||
tee_reader = BufReader::new(TeeReader::new(command_stdout, io::stdout())); |
||||
&mut tee_reader |
||||
}; |
||||
|
||||
let messages = Message::parse_stream(buf_reader) |
||||
.collect::<io::Result<_>>() |
||||
.unwrap(); |
||||
|
||||
(process.wait().unwrap(), messages) |
||||
} |
||||
|
||||
/// Create the cargo build command, but don't execute it.
|
||||
/// If there is no pre-built std detected in the sysroot, `build-std` is used.
|
||||
fn make_cargo_build_command(&self) -> Command { |
||||
let rustflags = env::var("RUSTFLAGS").unwrap_or_default() |
||||
+ &format!(" -L{}/libctru/lib -lctru", env::var("DEVKITPRO").unwrap()); |
||||
let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); |
||||
let sysroot = Self::find_sysroot(); |
||||
let mut command = Command::new(cargo); |
||||
|
||||
if !sysroot.join("lib/rustlib/armv6k-nintendo-3ds").exists() { |
||||
eprintln!("No pre-built std found, using build-std"); |
||||
command.arg("-Z").arg("build-std"); |
||||
let message_format = match input.cmd.extract_message_format() { |
||||
Ok(fmt) => fmt, |
||||
Err(msg) => { |
||||
eprintln!("{msg}"); |
||||
process::exit(1) |
||||
} |
||||
|
||||
command |
||||
.env("RUSTFLAGS", rustflags) |
||||
.arg(&self.command) |
||||
.arg("--target") |
||||
.arg("armv6k-nintendo-3ds") |
||||
.arg("--message-format") |
||||
.arg(&self.message_format) |
||||
.args(&self.args) |
||||
.stdout(Stdio::piped()) |
||||
.stdin(Stdio::inherit()) |
||||
.stderr(Stdio::inherit()); |
||||
|
||||
command |
||||
} |
||||
|
||||
/// Get the compiler's sysroot path
|
||||
fn find_sysroot() -> PathBuf { |
||||
let sysroot = env::var("SYSROOT").ok().unwrap_or_else(|| { |
||||
// Get sysroot from rustc
|
||||
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); |
||||
|
||||
let output = Command::new(&rustc) |
||||
.arg("--print") |
||||
.arg("sysroot") |
||||
.output() |
||||
.unwrap_or_else(|_| panic!("Failed to run `{rustc} --print sysroot`")); |
||||
|
||||
String::from_utf8(output.stdout) |
||||
.expect("Failed to parse sysroot path into a UTF-8 string") |
||||
}); |
||||
|
||||
PathBuf::from(sysroot.trim()) |
||||
} |
||||
|
||||
fn should_build_3dsx(&self) -> bool { |
||||
matches!(self.command.as_str(), "build" | "run" | "test") |
||||
} |
||||
} |
||||
|
||||
fn print_usage(f: &mut impl io::Write) { |
||||
let invocation = { |
||||
let mut args = env::args(); |
||||
|
||||
// We do this to properly display `cargo-3ds` if invoked that way
|
||||
let bin = args.next().unwrap(); |
||||
if let Some("3ds") = args.next().as_deref() { |
||||
"cargo 3ds".to_string() |
||||
} else { |
||||
bin |
||||
} |
||||
}; |
||||
|
||||
writeln!( |
||||
f, |
||||
"{name}: {description}. |
||||
|
||||
Usage: |
||||
{invocation} build [CARGO_OPTS...] |
||||
{invocation} run [CARGO_OPTS...] |
||||
{invocation} test [CARGO_OPTS...] |
||||
{invocation} <cargo-command> [CARGO_OPTS...] |
||||
{invocation} -h | --help |
||||
|
||||
Commands: |
||||
build build a 3dsx executable. |
||||
run build a 3dsx executable and send it to a device with 3dslink. |
||||
test build a 3dsx executable from unit/integration tests and send it to a device. |
||||
<cargo-command> execute some other Cargo command with 3ds options configured (ex. check or clippy). |
||||
|
||||
Options: |
||||
-h --help Show this screen. |
||||
|
||||
Additional arguments will be passed through to `<cargo-command>`. Some that are supported include: |
||||
|
||||
[build | run | test] --release |
||||
test --no-run |
||||
|
||||
Other flags may work, but haven't been tested. |
||||
", |
||||
name = env!("CARGO_BIN_NAME"), |
||||
description = env!("CARGO_PKG_DESCRIPTION"), |
||||
invocation = invocation, |
||||
) |
||||
.unwrap(); |
||||
} |
||||
|
||||
fn check_rust_version() { |
||||
let rustc_version = rustc_version::version_meta().unwrap(); |
||||
|
||||
if rustc_version.channel > Channel::Nightly { |
||||
eprintln!("cargo-3ds requires a nightly rustc version."); |
||||
eprintln!( |
||||
"Please run `rustup override set nightly` to use nightly in the \ |
||||
current directory." |
||||
); |
||||
process::exit(1); |
||||
} |
||||
|
||||
let old_version = MINIMUM_RUSTC_VERSION > Version { |
||||
// Remove `-nightly` pre-release tag for comparison.
|
||||
pre: semver::Prerelease::EMPTY, |
||||
..rustc_version.semver.clone() |
||||
}; |
||||
|
||||
let old_commit = match rustc_version.commit_date { |
||||
None => false, |
||||
Some(date) => { |
||||
MINIMUM_COMMIT_DATE |
||||
> CommitDate::parse(&date).expect("could not parse `rustc --version` commit date") |
||||
} |
||||
}; |
||||
|
||||
if old_version || old_commit { |
||||
eprintln!( |
||||
"cargo-3ds requires rustc nightly version >= {}", |
||||
MINIMUM_COMMIT_DATE, |
||||
); |
||||
eprintln!("Please run `rustup update nightly` to upgrade your nightly version"); |
||||
|
||||
process::exit(1); |
||||
} |
||||
} |
||||
|
||||
fn get_metadata(messages: &[Message]) -> CTRConfig { |
||||
let metadata = MetadataCommand::new() |
||||
.exec() |
||||
.expect("Failed to get cargo metadata"); |
||||
|
||||
let mut package = None; |
||||
let mut artifact = None; |
||||
|
||||
// Extract the final built executable. We may want to fail in cases where
|
||||
// multiple executables, or none, were built?
|
||||
for message in messages.iter().rev() { |
||||
if let Message::CompilerArtifact(art) = message { |
||||
if art.executable.is_some() { |
||||
package = Some(metadata[&art.package_id].clone()); |
||||
artifact = Some(art.clone()); |
||||
|
||||
break; |
||||
} |
||||
} |
||||
} |
||||
if package.is_none() || artifact.is_none() { |
||||
eprintln!("No executable found from build command output!"); |
||||
process::exit(1); |
||||
} |
||||
|
||||
let (package, artifact) = (package.unwrap(), artifact.unwrap()); |
||||
|
||||
let mut icon = String::from("./icon.png"); |
||||
|
||||
if !Path::new(&icon).exists() { |
||||
icon = format!( |
||||
"{}/libctru/default_icon.png", |
||||
env::var("DEVKITPRO").unwrap() |
||||
); |
||||
} |
||||
|
||||
// for now assume a single "kind" since we only support one output artifact
|
||||
let name = match artifact.target.kind[0].as_ref() { |
||||
"bin" | "lib" | "rlib" | "dylib" if artifact.target.test => { |
||||
format!("{} tests", artifact.target.name) |
||||
} |
||||
"example" => { |
||||
format!("{} - {} example", artifact.target.name, package.name) |
||||
} |
||||
_ => artifact.target.name, |
||||
}; |
||||
|
||||
let author = match package.authors.as_slice() { |
||||
[name, ..] => name.to_owned(), |
||||
[] => String::from("Unspecified Author"), // as standard with the devkitPRO toolchain
|
||||
}; |
||||
|
||||
CTRConfig { |
||||
name, |
||||
author, |
||||
description: package |
||||
.description |
||||
.clone() |
||||
.unwrap_or_else(|| String::from("Homebrew Application")), |
||||
icon, |
||||
target_path: artifact.executable.unwrap().into(), |
||||
cargo_manifest_path: package.manifest_path.into(), |
||||
} |
||||
} |
||||
|
||||
fn build_smdh(config: &CTRConfig) { |
||||
let mut process = Command::new("smdhtool") |
||||
.arg("--create") |
||||
.arg(&config.name) |
||||
.arg(&config.description) |
||||
.arg(&config.author) |
||||
.arg(&config.icon) |
||||
.arg(config.path_smdh()) |
||||
.stdin(Stdio::inherit()) |
||||
.stdout(Stdio::inherit()) |
||||
.stderr(Stdio::inherit()) |
||||
.spawn() |
||||
.unwrap(); |
||||
|
||||
let status = process.wait().unwrap(); |
||||
let (status, messages) = run_cargo(&input, message_format); |
||||
|
||||
if !status.success() { |
||||
process::exit(status.code().unwrap_or(1)); |
||||
} |
||||
} |
||||
|
||||
fn build_3dsx(config: &CTRConfig) { |
||||
let mut command = Command::new("3dsxtool"); |
||||
let mut process = command |
||||
.arg(&config.target_path) |
||||
.arg(config.path_3dsx()) |
||||
.arg(format!("--smdh={}", config.path_smdh().to_string_lossy())); |
||||
|
||||
// If romfs directory exists, automatically include it
|
||||
let (romfs_path, is_default_romfs) = get_romfs_path(config); |
||||
if romfs_path.is_dir() { |
||||
eprintln!("Adding RomFS from {}", romfs_path.display()); |
||||
process = process.arg(format!("--romfs={}", romfs_path.to_string_lossy())); |
||||
} else if !is_default_romfs { |
||||
eprintln!( |
||||
"Could not find configured RomFS dir: {}", |
||||
romfs_path.display() |
||||
); |
||||
process::exit(1); |
||||
} |
||||
|
||||
let mut process = process |
||||
.stdin(Stdio::inherit()) |
||||
.stdout(Stdio::inherit()) |
||||
.stderr(Stdio::inherit()) |
||||
.spawn() |
||||
.unwrap(); |
||||
|
||||
let status = process.wait().unwrap(); |
||||
|
||||
if !status.success() { |
||||
process::exit(status.code().unwrap_or(1)); |
||||
} |
||||
} |
||||
|
||||
fn link(config: &CTRConfig) { |
||||
let mut process = Command::new("3dslink") |
||||
.arg(config.path_3dsx()) |
||||
.stdin(Stdio::inherit()) |
||||
.stdout(Stdio::inherit()) |
||||
.stderr(Stdio::inherit()) |
||||
.spawn() |
||||
.unwrap(); |
||||
|
||||
let status = process.wait().unwrap(); |
||||
|
||||
if !status.success() { |
||||
process::exit(status.code().unwrap_or(1)); |
||||
} |
||||
} |
||||
|
||||
/// Read the `RomFS` path from the Cargo manifest. If it's unset, use the default.
|
||||
/// The returned boolean is true when the default is used.
|
||||
fn get_romfs_path(config: &CTRConfig) -> (PathBuf, bool) { |
||||
let manifest_path = &config.cargo_manifest_path; |
||||
let manifest_str = std::fs::read_to_string(manifest_path) |
||||
.unwrap_or_else(|e| panic!("Could not open {}: {e}", manifest_path.display())); |
||||
let manifest_data: toml::Value = |
||||
toml::de::from_str(&manifest_str).expect("Could not parse Cargo manifest as TOML"); |
||||
|
||||
// Find the romfs setting and compute the path
|
||||
let mut is_default = false; |
||||
let romfs_dir_setting = manifest_data |
||||
.as_table() |
||||
.and_then(|table| table.get("package")) |
||||
.and_then(toml::Value::as_table) |
||||
.and_then(|table| table.get("metadata")) |
||||
.and_then(toml::Value::as_table) |
||||
.and_then(|table| table.get("cargo-3ds")) |
||||
.and_then(toml::Value::as_table) |
||||
.and_then(|table| table.get("romfs_dir")) |
||||
.and_then(toml::Value::as_str) |
||||
.unwrap_or_else(|| { |
||||
is_default = true; |
||||
"romfs" |
||||
}); |
||||
let mut romfs_path = manifest_path.clone(); |
||||
romfs_path.pop(); // Pop Cargo.toml
|
||||
romfs_path.push(romfs_dir_setting); |
||||
|
||||
(romfs_path, is_default) |
||||
input.cmd.run_callback(&messages); |
||||
} |
||||
|
Loading…
Reference in new issue