1 Commits

Author SHA1 Message Date
Ian Burgwin
379da26a4e custominstall: fix loading seed (close #25) 2020-07-19 19:38:27 -07:00
33 changed files with 1143 additions and 2461 deletions

11
.gitignore vendored
View File

@@ -1,8 +1,8 @@
.vscode/
bin/linux/save3ds_fuse
**/finalize
cstins/
testing-class.py
*.local
# macOS
.DS_Store
@@ -12,16 +12,9 @@ testing-class.py
venv/
**/__pycache__/
*.pyc
*.egg-info/
*.whl
# JetBrains
.idea/
=======
/build/
/dist/
/custom-install-finalize.3dsx
result
result-*
*.pyc

View File

@@ -1,9 +0,0 @@
## This is my personal project
I make this project in my free time whenever I feel like it. I make no promises about reading issues or pull requests on a timely basis, or that I will fix certain issues or merge pull requests (soon or ever).
If you are making a significant addition and you intend for it to be implemented in my repository, you should talk to me first, because putting it in my repo means I have to maintain it. Please keep in mind the above paragraph. Maybe keep your own fork if you need something.
## No AI-generated content
This project, like all my projects, employs a strict zero-tolarance policy against any content generated by artificial intelligence, for any reason. Do not use it for issues, pull requests, comments, or anything else. Any content found to be the result of generative AI will be deleted, and the user likely blocked.

View File

@@ -1,4 +0,0 @@
recursive-include custominstall/bin/*
include custominstall/title.db.gz
include custominstall/TaskbarLib.tlb
include custominstall/custom-install-finalize.3dsx

View File

@@ -1,39 +1,21 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)]() ![Releases](https://img.shields.io/github/downloads/ihaveamac/custom-install/total.svg)
# custom-install
Installs a title directly to an SD card for the Nintendo 3DS. Originally created late June 2019.
Experimental script to automate the process of a manual title install for Nintendo 3DS. Originally created late June 2019.
## Summary
### Windows standalone
Note for Windows users: Enabling "Add Python 3.X to PATH" is **NOT** required! Python is installed with the `py` launcher by default.
1. [Dump boot9.bin and movable.sed](https://ihaveamac.github.io/dump.html) from a 3DS system.
2. Download the [latest releases](https://github.com/ihaveamac/custom-install/releases).
3. Extract and run ci-gui. Read `windows-quickstart.txt`.
### With installed Python
> [!NOTE]
> Windows users: Enabling "Add Python 3.X to PATH" is **NOT** required! Python is installed with the `py` launcher by default.
1. [Dump boot9.bin and movable.sed](https://ihaveamac.github.io/dump.html) from a 3DS system.
2. Install the packages:
* Windows: `py -3 -m pip install --user --upgrade https://github.com/ihaveamac/custom-install/archive/refs/heads/python-package.zip`
* macOS/Linux: `python3 -m pip install --user --upgrade https://github.com/ihaveamac/custom-install/archive/refs/heads/python-package.zip`
To run the GUI:
* Windows: `py -3 -m custominstall.gui`
* macOS/Linux: `python3 -m custominstall.gui`
To run the command line version:
* Windows: `py -3 -m custominstall`
* macOS/Linux: `python3 -m custominstall`
2. Download the repo ([zip link](https://github.com/ihaveamac/custom-install/archive/module-new-gui.zip) or `git clone`)
3. Install the packages:
* Windows: `py -3 -m pip install --user -r requirements.txt`
* macOS/Linux: `python3 -m pip install --user -r requirements.txt`
4. Run `custominstall.py` with boot9.bin, movable.sed, path to the SD root, and CIA files to install (see Usage section).
5. Download and use [custom-install-finalize](https://github.com/ihaveamac/custom-install/releases) on the 3DS system to finish the install.
## Setup
Linux users must build [wwylele/save3ds](https://github.com/wwylele/save3ds) and place `save3ds_fuse` in one of these places:
* A directory in `PATH`
* In `custominstall/bin/linux`
* Set the environment variable `CUSTOM_INSTALL_SAVE3DS_PATH` to the `save3ds_fuse` binary
Linux users must build [wwylele/save3ds](https://github.com/wwylele/save3ds) and place `save3ds_fuse` in `bin/linux`. Install [rust using rustup](https://www.rust-lang.org/tools/install), then compile with: `cargo build`. The compiled binary is located in `target/debug/save3ds_fuse`, copy it to `bin/linux`.
movable.sed is required and can be provided with `-m` or `--movable`.
@@ -64,31 +46,27 @@ Use `-h` to view arguments.
Examples:
```
py -3 -m custominstall -b boot9.bin -m movable.sed --sd E:\ file.cia file2.cia
python3 -m custominstall -b boot9.bin -m movable.sed --sd /Volumes/GM9SD file.cia file2.cia
python3 -m custominstall -b boot9.bin -m movable.sed --sd /media/GM9SD file.cia file2.cia
py -3 custominstall.py -b boot9.bin -m movable.sed --sd E:\ file.cia file2.cia
python3 custominstall.py -b boot9.bin -m movable.sed --sd /Volumes/GM9SD file.cia file2.cia
python3 custominstall.py -b boot9.bin -m movable.sed --sd /media/GM9SD file.cia file2.cia
```
## GUI
A GUI is provided to make the process easier.
GUI wrapper to easily manage your apps. (More will go here...)
![GUI](https://raw.githubusercontent.com/LyfeOnEdge/custom-install/master/docu/main.png)
### GUI Setup
Linux users may need to install a Tk package:
- Ubuntu/Debian: `sudo apt install python3-tk`
- Arch: `sudo pacman -S tk`
## Development
### Building Windows standalone
> [!WARNING]
> This section is OUTDATED and currently does not work with the Python package setup.
- Manjaro/Arch: `sudo pacman -S tk`
- Mac: Sometimes the default tkinter libs that ship with mac don't work, you can get them on the python site - `https://www.python.org/downloads/mac-osx/`
- Windows: Install python - `Remember to install tcl/tk when doing a custom installation`
## License/Credits
[save3ds by wwylele](https://github.com/wwylele/save3ds) is used to interact with the Title Database (details in `bin/README`).
Thanks to @LyfeOnEdge from the [brewtools Discord](https://brewtools.dev) for designing the GUI. Special thanks to CrafterPika and archbox for testing.
Thanks to @nek0bit for redesigning `custominstall.py` to work as a module, and for implementing an earlier GUI.
Thanks to @LyfeOnEdge from the [brewtools Discord](https://brewtools.dev) for designing the second version of the GUI. Special thanks to CrafterPika and archbox for testing.
Thanks to @BpyH64 for [researching how to generate the cmacs](https://github.com/d0k3/GodMode9/issues/340#issuecomment-487916606).

View File

@@ -2,49 +2,39 @@
# It is not intended for manual editing.
[[package]]
name = "aes"
version = "0.4.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aes-soft 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"aesni 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "aes-soft"
version = "0.4.0"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "aesni"
version = "0.7.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ahash"
version = "0.2.18"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "atty"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"const-random 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -52,28 +42,23 @@ name = "autocfg"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "block-buffer"
version = "0.8.0"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "block-cipher"
version = "0.7.1"
name = "block-cipher-trait"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -95,7 +80,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byte_struct_derive 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -110,75 +95,73 @@ dependencies = [
[[package]]
name = "byteorder"
version = "1.3.4"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "c2-chacha"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "chrono"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cmac"
version = "0.3.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"crypto-mac 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"dbl 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"dbl 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "const-random"
version = "0.1.8"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)",
"const-random-macro 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "const-random-macro"
version = "0.1.8"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crypto-mac"
version = "0.8.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"subtle 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dbl"
version = "0.3.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "digest"
version = "0.9.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -191,20 +174,27 @@ name = "fuse"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
"thread-scoped 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "generic-array"
version = "0.14.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "generic-array"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -212,17 +202,17 @@ name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "getrandom"
version = "0.1.14"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
"wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -230,31 +220,25 @@ name = "hashbrown"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ahash 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
"ahash 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)",
"autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "lazy_static"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.71"
version = "0.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libsave3ds"
version = "0.1.0"
dependencies = [
"aes 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"byte_struct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cmac 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"lru 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"sha2 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cmac 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lru 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -275,34 +259,12 @@ dependencies = [
[[package]]
name = "lru"
version = "0.5.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"hashbrown 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-integer"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-traits"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "opaque-debug"
version = "0.2.3"
@@ -315,13 +277,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ppv-lite86"
version = "0.2.8"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "proc-macro-hack"
version = "0.5.16"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro2"
@@ -331,6 +298,14 @@ dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro2"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "0.6.13"
@@ -340,23 +315,31 @@ dependencies = [
]
[[package]]
name = "rand"
version = "0.7.3"
name = "quote"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -365,7 +348,7 @@ name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -381,52 +364,31 @@ name = "redox_syscall"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "redox_termios"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "save3ds_fuse"
version = "0.1.0"
dependencies = [
"fuse 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
"libsave3ds 0.1.0",
"stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sha2"
version = "0.9.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-buffer 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"digest 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
"fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "stderrlog"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "subtle"
version = "2.2.3"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -440,22 +402,13 @@ dependencies = [
]
[[package]]
name = "termcolor"
version = "1.1.0"
name = "syn"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termion"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -463,32 +416,24 @@ name = "thread-scoped"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "thread_local"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time"
version = "0.1.43"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "typenum"
version = "1.12.0"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-width"
version = "0.1.7"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -497,26 +442,13 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unreachable"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "void"
version = "1.0.2"
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -533,85 +465,67 @@ name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum aes 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f7001367fde4c768a19d1029f0a8be5abd9308e1119846d5bd9ad26297b8faf5"
"checksum aes-soft 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4925647ee64e5056cf231608957ce7c81e12d6d6e316b9ce1404778cc1d35fa7"
"checksum aesni 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d050d39b0b7688b3a3254394c3e30a9d66c41dcf9b05b0e2dbdc623f6505d264"
"checksum ahash 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "6f33b5018f120946c1dcf279194f238a9f146725593ead1c08fa47ff22b0b5d3"
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
"checksum aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "54eb1d8fe354e5fc611daf4f2ea97dd45a765f4f1e4512306ec183ae2e8f20c9"
"checksum aes-soft 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cfd7e7ae3f9a1fb5c03b389fc6bb9a51400d0c13053f0dca698c832bfd893a0d"
"checksum aesni 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100"
"checksum ahash 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "2f00e10d4814aa20900e7948174384f79f1317f24f0ba7494e735111653fc330"
"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
"checksum block-buffer 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dbcf92448676f82bb7a334c58bbce8b0d43580fb5362a9d608b18879d12a3d31"
"checksum block-cipher 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10"
"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
"checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774"
"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
"checksum byte_struct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bde2e17424d6d3042b950f39de519dfd398c2e08adb1402d3fc10232a17564e"
"checksum byte_struct_derive 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7fb6eccde50afec044557d1f1b8776168b7040255390eefffb39fcfd1ab40b2e"
"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
"checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
"checksum cmac 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9f8f8ba8b9640e29213f152015694e78208e601adf91c72b698460633b15715"
"checksum const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a"
"checksum const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a"
"checksum crypto-mac 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
"checksum dbl 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2735145c3b9ba15f2d7a3ae8cdafcbc8c98a7bef7f62afe9d08bd99fbf7130de"
"checksum digest 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
"checksum cmac 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6f4a435124bcc292eba031f1f725d7abacdaf13cbf9f935450e8c45aa9e96cad"
"checksum const-random 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7b641a8c9867e341f3295564203b1c250eb8ce6cb6126e007941f78c4d2ed7fe"
"checksum const-random-macro 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c750ec12b83377637110d5a57f5ae08e895b06c4b16e2bdbf1a94ef717428c59"
"checksum crypto-mac 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
"checksum dbl 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28dc203b75decc900220c4d9838e738d08413e663c26826ba92b669bed1d0795"
"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
"checksum fuse 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80e57070510966bfef93662a81cb8aa2b1c7db0964354fa9921434f04b9e8660"
"checksum generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980"
"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
"checksum generic-array 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd"
"checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407"
"checksum hashbrown 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead"
"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
"checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
"checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8"
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
"checksum lru 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28e0c685219cd60e49a2796bba7e4fe6523e10daca4fd721e84e7f905093d60c"
"checksum num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
"checksum num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
"checksum lru 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26b0dca4ac5b5083c5169ab12205e6473df1c7659940e4978b94f363c6b54b22"
"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
"checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"
"checksum proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"
"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
"checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27"
"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
"checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
"checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412"
"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum sha2 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "72377440080fd008550fe9b441e854e43318db116f90181eef92e9ae9aedab48"
"checksum stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32e5ee9b90a5452c570a0b0ac1c99ae9498db7e56e33d74366de7f2a7add7f25"
"checksum subtle 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1"
"checksum sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4d8bfd0e469f417657573d8451fb33d16cfe0989359b93baf3a1ffc639543d"
"checksum subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
"checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
"checksum termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905"
"checksum syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "661641ea2aa15845cddeb97dad000d22070bb5c1fb456b96c1cba883ec691e92"
"checksum thread-scoped 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bcbb6aa301e5d3b0b5ef639c9a9c7e2f1c944f177b460c04dc24c69b1fa2bd99"
"checksum thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14"
"checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
"checksum typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
"checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9"
"checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20"
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
"checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

8
bin/README Normal file
View File

@@ -0,0 +1,8 @@
save3ds_fuse for win32 and darwin built with commit 25f0b5ec2600ddff8f5d2acf7c89ac1c4e743972
in repository https://github.com/wwylele/save3ds
win32 binary built on Windows 10, version 1903 64-bit with `cargo build --release --target=i686-pc-windows-msvc`.
darwin binary built on macOS 10.15.1 with `cd save3ds_fuse && cargo build --no-default-features --release`.
linux binary must be provided by the user.

BIN
bin/darwin/save3ds_fuse Executable file

Binary file not shown.

BIN
bin/win32/save3ds_fuse.exe Executable file

Binary file not shown.

485
custominstall.py Normal file
View File

@@ -0,0 +1,485 @@
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019-2020 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
from argparse import ArgumentParser
from os import makedirs, scandir
from os.path import dirname, join
from random import randint
from hashlib import sha256
from sys import platform
from tempfile import TemporaryDirectory
from typing import BinaryIO, TYPE_CHECKING
import subprocess
if TYPE_CHECKING:
from os import PathLike
from typing import Union
from events import Events
from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb
from pyctr.type.cia import CIAReader, CIASection
from pyctr.type.ncch import NCCHSection
from pyctr.util import roundup
# used to run the save3ds_fuse binary next to the script
script_dir: str = dirname(__file__)
# missing contents are replaced with 0xFFFFFFFF in the cmd file
CMD_MISSING = b'\xff\xff\xff\xff'
# the size of each file and directory in a title's contents are rounded up to this
TITLE_ALIGN_SIZE = 0x8000
# size to read at a time when copying files
READ_SIZE = 0x200000
# version for cifinish.bin
CIFINISH_VERSION = 3
# Placeholder for SDPathErrors
class SDPathError(Exception):
pass
class InvalidCIFinishError(Exception):
pass
def load_cifinish(path: 'Union[PathLike, bytes, str]'):
try:
with open(path, 'rb') as f:
header = f.read(0x10)
if header[0:8] != b'CIFINISH':
raise InvalidCIFinishError('CIFINISH magic not found')
version = int.from_bytes(header[0x8:0xC], 'little')
count = int.from_bytes(header[0xC:0x10], 'little')
data = {}
for _ in range(count):
if version == 1:
# ignoring the titlekey and common key index, since it's not useful in this scenario
raw_entry = f.read(0x30)
if len(raw_entry) != 0x30:
raise InvalidCIFinishError(f'title entry is not 0x30 (version {version})')
title_magic = raw_entry[0xA:0x10]
title_id = int.from_bytes(raw_entry[0:8], 'little')
has_seed = raw_entry[0x9]
seed = raw_entry[0x20:0x30]
elif version == 2:
# this is assuming the "wrong" version created by an earlier version of this script
# there wasn't a version of custom-install-finalize that really accepted this version
raw_entry = f.read(0x20)
if len(raw_entry) != 0x20:
raise InvalidCIFinishError(f'title entry is not 0x20 (version {version})')
title_magic = raw_entry[0:6]
title_id = int.from_bytes(raw_entry[0x6:0xE], 'little')
has_seed = raw_entry[0xE]
seed = raw_entry[0x10:0x20]
elif version == 3:
raw_entry = f.read(0x20)
if len(raw_entry) != 0x20:
raise InvalidCIFinishError(f'title entry is not 0x20 (version {version})')
title_magic = raw_entry[0:6]
title_id = int.from_bytes(raw_entry[0x8:0x10], 'little')
has_seed = raw_entry[0x6]
seed = raw_entry[0x10:0x20]
else:
raise InvalidCIFinishError(f'unknown version {version}')
if title_magic == b'TITLE\0':
data[title_id] = {'seed': seed if has_seed else None}
return data
except FileNotFoundError:
# allow the caller to easily create a new database in the same place where an existing one would be updated
return {}
def save_cifinish(path: 'Union[PathLike, bytes, str]', data: dict):
with open(path, 'wb') as out:
entries = sorted(data.items())
out.write(b'CIFINISH')
out.write(CIFINISH_VERSION.to_bytes(4, 'little'))
out.write(len(entries).to_bytes(4, 'little'))
for tid, data in entries:
finalize_entry_data = [
# magic
b'TITLE\0',
# has seed
bool(data['seed']).to_bytes(1, 'little'),
# padding
b'\0',
# title id
tid.to_bytes(8, 'little'),
# seed, if needed
(data['seed'] if data['seed'] else (b'\0' * 0x10))
]
out.write(b''.join(finalize_entry_data))
class CustomInstall:
cia: CIAReader
def __init__(self, boot9, seeddb, movable, cias, sd, cifinish_out=None, skip_contents=False):
self.event = Events()
self.log_lines = [] # Stores all info messages for user to view
self.crypto = CryptoEngine(boot9=boot9)
self.crypto.setup_sd_key_from_file(movable)
self.seeddb = seeddb
self.cias = cias
self.sd = sd
self.skip_contents = skip_contents
self.cifinish_out = cifinish_out
self.movable = movable
def copy_with_progress(self, src: BinaryIO, dst: BinaryIO, size: int, path: str):
left = size
cipher = self.crypto.create_ctr_cipher(Keyslot.SD, self.crypto.sd_path_to_iv(path))
while left > 0:
to_read = min(READ_SIZE, left)
data = cipher.encrypt(src.read(READ_SIZE))
dst.write(data)
left -= to_read
total_read = size - left
self.event.update_percentage((total_read / size) * 100, total_read / 1048576, size / 1048576)
def start(self):
crypto = self.crypto
# TODO: Move a lot of these into their own methods
self.log("Finding path to install to...")
[sd_path, id1s] = self.get_sd_path()
if len(id1s) > 1:
raise SDPathError(f'There are multiple id1 directories for id0 {crypto.id0.hex()}, '
f'please remove extra directories')
elif len(id1s) == 0:
raise SDPathError(f'Could not find a suitable id1 directory for id0 {crypto.id0.hex()}')
if self.cifinish_out:
cifinish_path = self.cifinish_out
else:
cifinish_path = join(self.sd, 'cifinish.bin')
sd_path = join(sd_path, id1s[0])
title_info_entries = {}
cifinish_data = load_cifinish(cifinish_path)
load_seeddb(self.seeddb)
# Now loop through all provided cia files
for c in self.cias:
self.log('Reading ' + c)
try:
cia = CIAReader(c)
except Exception as e:
self.log(f'Failed to load file: {type(e).__name__}: {e}')
continue
self.cia = cia
tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16])
try:
self.log(f'Installing {cia.contents[0].exefs.icon.get_app_title().short_desc}...')
except:
self.log('Installing...')
sizes = [1] * 5
if cia.tmd.save_size:
# one for the data directory, one for the 00000001.sav file
sizes.extend((1, cia.tmd.save_size))
for record in cia.content_info:
sizes.append(record.size)
# this calculates the size to put in the Title Info Entry
title_size = sum(roundup(x, TITLE_ALIGN_SIZE) for x in sizes)
# checks if this is dlc, which has some differences
is_dlc = tid_parts[0] == '0004008c'
# this checks if it has a manual (index 1) and is not DLC
has_manual = (not is_dlc) and (1 in cia.contents)
# this gets the extdata id from the extheader, stored in the storage info area
try:
with cia.contents[0].open_raw_section(NCCHSection.ExtendedHeader) as e:
e.seek(0x200 + 0x30)
extdata_id = e.read(8)
except KeyError:
# not an executable title
extdata_id = b'\0' * 8
# cmd content id, starts with 1 for non-dlc contents
cmd_id = len(cia.content_info) if is_dlc else 1
cmd_filename = f'{cmd_id:08x}.cmd'
# get the title root where all the contents will be
title_root = join(sd_path, 'title', *tid_parts)
content_root = join(title_root, 'content')
# generate the path used for the IV
title_root_cmd = f'/title/{"/".join(tid_parts)}'
content_root_cmd = title_root_cmd + '/content'
if not self.skip_contents:
makedirs(join(content_root, 'cmd'), exist_ok=True)
if cia.tmd.save_size:
makedirs(join(title_root, 'data'), exist_ok=True)
if is_dlc:
# create the separate directories for every 256 contents
for x in range(((len(cia.content_info) - 1) // 256) + 1):
makedirs(join(content_root, f'{x:08x}'), exist_ok=True)
# maybe this will be changed in the future
tmd_id = 0
tmd_filename = f'{tmd_id:08x}.tmd'
# write the tmd
enc_path = content_root_cmd + '/' + tmd_filename
self.log(f'Writing {enc_path}...')
with cia.open_raw_section(CIASection.TitleMetadata) as s:
with open(join(content_root, tmd_filename), 'wb') as o:
self.copy_with_progress(s, o, cia.sections[CIASection.TitleMetadata].size, enc_path)
# write each content
for co in cia.content_info:
content_filename = co.id + '.app'
if is_dlc:
dir_index = format((co.cindex // 256), '08x')
enc_path = content_root_cmd + f'/{dir_index}/{content_filename}'
out_path = join(content_root, dir_index, content_filename)
else:
enc_path = content_root_cmd + '/' + content_filename
out_path = join(content_root, content_filename)
self.log(f'Writing {enc_path}...')
with cia.open_raw_section(co.cindex) as s, open(out_path, 'wb') as o:
self.copy_with_progress(s, o, co.size, enc_path)
# generate a blank save
if cia.tmd.save_size:
enc_path = title_root_cmd + '/data/00000001.sav'
out_path = join(title_root, 'data', '00000001.sav')
cipher = crypto.create_ctr_cipher(Keyslot.SD, crypto.sd_path_to_iv(enc_path))
# in a new save, the first 0x20 are all 00s. the rest can be random
data = cipher.encrypt(b'\0' * 0x20)
self.log(f'Generating blank save at {enc_path}...')
with open(out_path, 'wb') as o:
o.write(data)
o.write(b'\0' * (cia.tmd.save_size - 0x20))
# generate and write cmd
enc_path = content_root_cmd + '/cmd/' + cmd_filename
out_path = join(content_root, 'cmd', cmd_filename)
self.log(f'Generating {enc_path}')
highest_index = 0
content_ids = {}
for record in cia.content_info:
highest_index = record.cindex
with cia.open_raw_section(record.cindex) as s:
s.seek(0x100)
cmac_data = s.read(0x100)
id_bytes = bytes.fromhex(record.id)[::-1]
cmac_data += record.cindex.to_bytes(4, 'little') + id_bytes
cmac_ncch = crypto.create_cmac_object(Keyslot.CMACSDNAND)
cmac_ncch.update(sha256(cmac_data).digest())
content_ids[record.cindex] = (id_bytes, cmac_ncch.digest())
# add content IDs up to the last one
ids_by_index = [CMD_MISSING] * (highest_index + 1)
installed_ids = []
cmacs = []
for x in range(len(ids_by_index)):
try:
info = content_ids[x]
except KeyError:
# "MISSING CONTENT!"
# The 3DS does generate a cmac for missing contents, but I don't know how it works.
# It doesn't matter anyway, the title seems to be fully functional.
cmacs.append(bytes.fromhex('4D495353494E4720434F4E54454E5421'))
else:
ids_by_index[x] = info[0]
cmacs.append(info[1])
installed_ids.append(info[0])
installed_ids.sort(key=lambda x: int.from_bytes(x, 'little'))
final = (cmd_id.to_bytes(4, 'little')
+ len(ids_by_index).to_bytes(4, 'little')
+ len(installed_ids).to_bytes(4, 'little')
+ (1).to_bytes(4, 'little'))
cmac_cmd_header = crypto.create_cmac_object(Keyslot.CMACSDNAND)
cmac_cmd_header.update(final)
final += cmac_cmd_header.digest()
final += b''.join(ids_by_index)
final += b''.join(installed_ids)
final += b''.join(cmacs)
cipher = crypto.create_ctr_cipher(Keyslot.SD, crypto.sd_path_to_iv(enc_path))
self.log(f'Writing {enc_path}')
with open(out_path, 'wb') as o:
o.write(cipher.encrypt(final))
# this starts building the title info entry
title_info_entry_data = [
# title size
title_size.to_bytes(8, 'little'),
# title type, seems to usually be 0x40
0x40.to_bytes(4, 'little'),
# title version
int(cia.tmd.title_version).to_bytes(2, 'little'),
# ncch version
cia.contents[0].version.to_bytes(2, 'little'),
# flags_0, only checking if there is a manual
(1 if has_manual else 0).to_bytes(4, 'little'),
# tmd content id, always starting with 0
(0).to_bytes(4, 'little'),
# cmd content id
cmd_id.to_bytes(4, 'little'),
# flags_1, only checking save data
(1 if cia.tmd.save_size else 0).to_bytes(4, 'little'),
# extdataid low
extdata_id[0:4],
# reserved
b'\0' * 4,
# flags_2, only using a common value
0x100000000.to_bytes(8, 'little'),
# product code
cia.contents[0].product_code.encode('ascii').ljust(0x10, b'\0'),
# reserved
b'\0' * 0x10,
# unknown
randint(0, 0xFFFFFFFF).to_bytes(4, 'little'),
# reserved
b'\0' * 0x2c
]
title_info_entries[cia.tmd.title_id] = b''.join(title_info_entry_data)
cifinish_data[int(cia.tmd.title_id, 16)] = {'seed': (get_seed(cia.contents[0].program_id) if cia.contents[0].flags.uses_seed else None)}
# This is saved regardless if any titles were installed, so the file can be upgraded just in case.
save_cifinish(cifinish_path, cifinish_data)
if title_info_entries:
with TemporaryDirectory(suffix='-custom-install') as tempdir:
# set up the common arguments for the two times we call save3ds_fuse
save3ds_fuse_common_args = [
join(script_dir, 'bin', platform, 'save3ds_fuse'),
'-b', crypto.b9_path,
'-m', self.movable,
'--sd', self.sd,
'--db', 'sdtitle',
tempdir
]
# extract the title database to add our own entry to
self.log('Extracting Title Database...')
subprocess.run(save3ds_fuse_common_args + ['-x'])
for title_id, entry in title_info_entries.items():
# write the title info entry to the temp directory
with open(join(tempdir, title_id), 'wb') as o:
o.write(entry)
# import the directory, now including our title
self.log('Importing into Title Database...')
subprocess.run(save3ds_fuse_common_args + ['-i'])
self.log('FINAL STEP:\nRun custom-install-finalize through homebrew launcher.')
self.log('This will install a ticket and seed if required.')
else:
self.log('Did not install any titles.', 2)
def get_sd_path(self):
sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
id1s = []
for d in scandir(sd_path):
if d.is_dir() and len(d.name) == 32:
try:
# check if the name can be converted to hex
# I'm not sure what the 3DS does if there is a folder that is not a 32-char hex string.
bytes.fromhex(d.name)
except ValueError:
continue
else:
id1s.append(d.name)
return [sd_path, id1s]
def log(self, message, mtype=0, errorname=None, end='\n'):
"""Logs an Message with a type. Format is similar to python errors
There are 3 types of errors, indexed accordingly
type 0 = Message
type 1 = Warning
type 2 = Error
optionally, errorname can be a custom name as a string to identify errors easily
"""
if errorname:
errorname += ": "
else:
# No errorname provided
errorname = ""
types = [
"", # Type 0
"Warning: ", # Type 1
"Error: " # Type 2
]
# Example: "Warning: UninformativeError: An error occured, try again.""
msg_with_type = types[mtype] + errorname + str(message)
self.log_lines.append(msg_with_type)
self.event.on_log_msg(msg_with_type, end=end)
return msg_with_type
if __name__ == "__main__":
parser = ArgumentParser(description='Manually install a CIA to the SD card for a Nintendo 3DS system.')
parser.add_argument('cia', help='CIA files', nargs='+')
parser.add_argument('-m', '--movable', help='movable.sed file', required=True)
parser.add_argument('-b', '--boot9', help='boot9 file')
parser.add_argument('-s', '--seeddb', help='seeddb file')
parser.add_argument('--sd', help='path to SD root', required=True)
parser.add_argument('--skip-contents', help="don't add contents, only add title info entry", action='store_true')
parser.add_argument('--cifinish-out', help='path for cifinish.bin file, defaults to (SD root)/cifinish.bin')
args = parser.parse_args()
installer = CustomInstall(boot9=args.boot9,
seeddb=args.seeddb,
cias=args.cia,
movable=args.movable,
sd=args.sd,
cifinish_out=args.cifinish_out,
skip_contents=(args.skip_contents or False))
def log_handle(msg, end='\n'):
print(msg, end=end)
def percent_handle(total_percent, total_read, size):
installer.log(f' {total_percent:>5.1f}% {total_read:>.1f} MiB / {size:.1f} MiB\r', end='')
installer.event.on_log_msg += log_handle
installer.event.update_percentage += percent_handle
installer.start()

Binary file not shown.

View File

@@ -1,10 +0,0 @@
# This file is a part of custom-install.
#
# Copyright (c) 2019 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
__author__ = 'ihaveahax'
__copyright__ = 'Copyright (c) 2019 Ian Burgwin'
__license__ = 'MIT'
__version__ = '2.1'

View File

@@ -1,765 +0,0 @@
#!/usr/bin/env python3
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
from argparse import ArgumentParser
from enum import Enum
from glob import glob
import gzip
from os import makedirs, rename, scandir, environ
from os.path import dirname, join, isdir, isfile
from random import randint
from hashlib import sha256
from pprint import pformat
from shutil import copyfile, copy2, rmtree, which
import sys
from sys import platform, executable
from tempfile import TemporaryDirectory
from traceback import format_exception
from typing import BinaryIO, TYPE_CHECKING
import subprocess
if TYPE_CHECKING:
from os import PathLike
from typing import List, Union, Tuple
from events import Events
from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb, get_seed
from pyctr.type.cdn import CDNReader, CDNError
from pyctr.type.cia import CIAReader, CIAError
from pyctr.type.ncch import NCCHSection
from pyctr.type.tmd import TitleMetadataError
from pyctr.util import roundup
from . import __version__
if platform == 'msys':
platform = 'win32'
is_windows = platform == 'win32'
if is_windows:
from ctypes import c_wchar_p, pointer, c_ulonglong, windll
else:
from os import statvfs
script_dir: str
frozen = getattr(sys, 'frozen', False)
if frozen:
script_dir = dirname(executable)
else:
script_dir = dirname(__file__)
# used to run the save3ds_fuse binary next to the script
if 'CUSTOM_INSTALL_SAVE3DS_PATH' in environ:
save3ds_fuse_path = environ['CUSTOM_INSTALL_SAVE3DS_PATH']
else:
save3ds_fuse_name = 'save3ds_fuse'
if is_windows:
save3ds_fuse_name += '.exe'
if frozen:
save3ds_fuse_path = join(script_dir, 'bin', save3ds_fuse_name)
else:
save3ds_fuse_path = join(script_dir, 'bin', platform, save3ds_fuse_name)
if not isfile(save3ds_fuse_path):
save3ds_fuse_path = which('save3ds_fuse')
# missing contents are replaced with 0xFFFFFFFF in the cmd file
CMD_MISSING = b'\xff\xff\xff\xff'
# the size of each file and directory in a title's contents are rounded up to this
TITLE_ALIGN_SIZE = 0x8000
# size to read at a time when copying files
READ_SIZE = 0x200000
# version for cifinish.bin
CIFINISH_VERSION = 3
# Placeholder for SDPathErrors
class SDPathError(Exception):
pass
class InvalidCIFinishError(Exception):
pass
class InstallStatus(Enum):
Waiting = 0
Starting = 1
Writing = 2
Finishing = 3
Done = 4
Failed = 5
def get_free_space(path: 'Union[PathLike, bytes, str]'):
if is_windows:
lpSectorsPerCluster = c_ulonglong(0)
lpBytesPerSector = c_ulonglong(0)
lpNumberOfFreeClusters = c_ulonglong(0)
lpTotalNumberOfClusters = c_ulonglong(0)
ret = windll.kernel32.GetDiskFreeSpaceW(c_wchar_p(path), pointer(lpSectorsPerCluster),
pointer(lpBytesPerSector),
pointer(lpNumberOfFreeClusters),
pointer(lpTotalNumberOfClusters))
if not ret:
raise WindowsError
free_blocks = lpNumberOfFreeClusters.value * lpSectorsPerCluster.value
free_bytes = free_blocks * lpBytesPerSector.value
else:
stv = statvfs(path)
free_bytes = stv.f_bavail * stv.f_frsize
return free_bytes
def load_cifinish(path: 'Union[PathLike, bytes, str]'):
try:
with open(path, 'rb') as f:
header = f.read(0x10)
if header[0:8] != b'CIFINISH':
raise InvalidCIFinishError('CIFINISH magic not found')
version = int.from_bytes(header[0x8:0xC], 'little')
count = int.from_bytes(header[0xC:0x10], 'little')
data = {}
for _ in range(count):
if version == 1:
# ignoring the titlekey and common key index, since it's not useful in this scenario
raw_entry = f.read(0x30)
if len(raw_entry) != 0x30:
raise InvalidCIFinishError(f'title entry is not 0x30 (version {version})')
title_magic = raw_entry[0xA:0x10]
title_id = int.from_bytes(raw_entry[0:8], 'little')
has_seed = raw_entry[0x9]
seed = raw_entry[0x20:0x30]
elif version == 2:
# this is assuming the "wrong" version created by an earlier version of this script
# there wasn't a version of custom-install-finalize that really accepted this version
raw_entry = f.read(0x20)
if len(raw_entry) != 0x20:
raise InvalidCIFinishError(f'title entry is not 0x20 (version {version})')
title_magic = raw_entry[0:6]
title_id = int.from_bytes(raw_entry[0x6:0xE], 'little')
has_seed = raw_entry[0xE]
seed = raw_entry[0x10:0x20]
elif version == 3:
raw_entry = f.read(0x20)
if len(raw_entry) != 0x20:
raise InvalidCIFinishError(f'title entry is not 0x20 (version {version})')
title_magic = raw_entry[0:6]
title_id = int.from_bytes(raw_entry[0x8:0x10], 'little')
has_seed = raw_entry[0x6]
seed = raw_entry[0x10:0x20]
else:
raise InvalidCIFinishError(f'unknown version {version}')
if title_magic == b'TITLE\0':
data[title_id] = {'seed': seed if has_seed else None}
return data
except FileNotFoundError:
# allow the caller to easily create a new database in the same place where an existing one would be updated
return {}
def save_cifinish(path: 'Union[PathLike, bytes, str]', data: dict):
with open(path, 'wb') as out:
entries = sorted(data.items())
out.write(b'CIFINISH')
out.write(CIFINISH_VERSION.to_bytes(4, 'little'))
out.write(len(entries).to_bytes(4, 'little'))
for tid, data in entries:
finalize_entry_data = [
# magic
b'TITLE\0',
# has seed
bool(data['seed']).to_bytes(1, 'little'),
# padding
b'\0',
# title id
tid.to_bytes(8, 'little'),
# seed, if needed
(data['seed'] if data['seed'] else (b'\0' * 0x10))
]
out.write(b''.join(finalize_entry_data))
def get_install_size(title: 'Union[CIAReader, CDNReader]'):
sizes = [1] * 5
if title.tmd.save_size:
# one for the data directory, one for the 00000001.sav file
sizes.extend((1, title.tmd.save_size))
for record in title.content_info:
sizes.append(record.size)
# this calculates the size to put in the Title Info Entry
title_size = sum(roundup(x, TITLE_ALIGN_SIZE) for x in sizes)
return title_size
class CustomInstall:
def __init__(self, *, movable, sd, cifinish_out=None, overwrite_saves=False, skip_contents=False,
boot9=None, seeddb=None):
self.event = Events()
self.log_lines = [] # Stores all info messages for user to view
self.crypto = CryptoEngine(boot9=boot9)
self.crypto.setup_sd_key_from_file(movable)
self.seeddb = seeddb
self.readers: 'List[Tuple[Union[CDNReader, CIAReader], Union[PathLike, bytes, str]]]' = []
self.sd = sd
self.skip_contents = skip_contents
self.overwrite_saves = overwrite_saves
self.cifinish_out = cifinish_out
self.movable = movable
def copy_with_progress(self, src: BinaryIO, dst: BinaryIO, size: int, path: str, fire_event: bool = True):
left = size
cipher = self.crypto.create_ctr_cipher(Keyslot.SD, self.crypto.sd_path_to_iv(path))
hasher = sha256()
while left > 0:
to_read = min(READ_SIZE, left)
data = src.read(READ_SIZE)
hasher.update(data)
dst.write(cipher.encrypt(data))
left -= to_read
total_read = size - left
if fire_event:
self.event.update_percentage((total_read / size) * 100, total_read / 1048576, size / 1048576)
return hasher.digest()
@staticmethod
def get_reader(path: 'Union[PathLike, bytes, str]'):
if isdir(path):
# try the default tmd file
reader = CDNReader(join(path, 'tmd'))
else:
try:
reader = CIAReader(path)
except CIAError:
# if there was an error with parsing the CIA header,
# the file would be tried in CDNReader next (assuming it's a tmd)
# any other error should be propagated to the caller
reader = CDNReader(path)
return reader
def prepare_titles(self, paths: 'List[PathLike]'):
if self.seeddb:
load_seeddb(self.seeddb)
readers = []
for path in paths:
self.log(f'Reading {path}')
try:
reader = self.get_reader(path)
except (CIAError, CDNError, TitleMetadataError):
self.log(f"Couldn't read {path}, likely corrupt or not a CIA or CDN title")
continue
if reader.tmd.title_id.startswith('00048'): # DSiWare
self.log(f'Skipping {reader.tmd.title_id} - DSiWare is not supported')
continue
readers.append((reader, path))
self.readers = readers
def check_size(self):
total_size = 0
for r, _ in self.readers:
total_size += get_install_size(r)
free_space = get_free_space(self.sd)
return total_size, free_space
def check_for_id0(self):
sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
return isdir(sd_path)
def start(self):
if not (save3ds_fuse_path and isfile(save3ds_fuse_path)):
self.log("Couldn't find " + save3ds_fuse_path, 2)
return None, False, 0
crypto = self.crypto
# TODO: Move a lot of these into their own methods
self.log("Finding path to install to...")
[sd_path, id1s] = self.get_sd_path()
if len(id1s) > 1:
raise SDPathError(f'There are multiple id1 directories for id0 {crypto.id0.hex()}, '
f'please remove extra directories')
elif len(id1s) == 0:
raise SDPathError(f'Could not find a suitable id1 directory for id0 {crypto.id0.hex()}')
id1 = id1s[0]
sd_path = join(sd_path, id1)
if self.cifinish_out:
cifinish_path = self.cifinish_out
else:
cifinish_path = join(self.sd, 'cifinish.bin')
try:
cifinish_data = load_cifinish(cifinish_path)
except InvalidCIFinishError as e:
self.log(f'{type(e).__qualname__}: {e}')
self.log(f'{cifinish_path} was corrupt!\n'
f'This could mean an issue with the SD card or the filesystem. Please check it for errors.\n'
f'It is also possible, though less likely, to be an issue with custom-install.\n'
f'Exiting now to prevent possible issues. If you want to try again, delete cifinish.bin from the SD card and re-run custom-install.')
return None, False, 0
db_path = join(sd_path, 'dbs')
titledb_path = join(db_path, 'title.db')
importdb_path = join(db_path, 'import.db')
if not isfile(titledb_path):
makedirs(db_path, exist_ok=True)
with gzip.open(join(script_dir, 'title.db.gz')) as f:
tdb = f.read()
self.log(f'Creating title.db...')
with open(titledb_path, 'wb') as o:
with self.crypto.create_ctr_io(Keyslot.SD, o, self.crypto.sd_path_to_iv('/dbs/title.db')) as e:
e.write(tdb)
cmac = crypto.create_cmac_object(Keyslot.CMACSDNAND)
cmac_data = [b'CTR-9DB0', 0x2.to_bytes(4, 'little'), tdb[0x100:0x200]]
cmac.update(sha256(b''.join(cmac_data)).digest())
e.seek(0)
e.write(cmac.digest())
self.log(f'Creating import.db...')
with open(importdb_path, 'wb') as o:
with self.crypto.create_ctr_io(Keyslot.SD, o, self.crypto.sd_path_to_iv('/dbs/import.db')) as e:
e.write(tdb)
cmac = crypto.create_cmac_object(Keyslot.CMACSDNAND)
cmac_data = [b'CTR-9DB0', 0x3.to_bytes(4, 'little'), tdb[0x100:0x200]]
cmac.update(sha256(b''.join(cmac_data)).digest())
e.seek(0)
e.write(cmac.digest())
del tdb
with TemporaryDirectory(suffix='-custom-install') as tempdir:
# set up the common arguments for the two times we call save3ds_fuse
save3ds_fuse_common_args = [
save3ds_fuse_path,
'-b', crypto.b9_path,
'-m', self.movable,
'--sd', self.sd,
'--db', 'sdtitle',
tempdir
]
extra_kwargs = {}
if is_windows:
# hide console window
extra_kwargs['creationflags'] = 0x08000000 # CREATE_NO_WINDOW
# extract the title database to add our own entry to
self.log('Extracting Title Database...')
out = subprocess.run(save3ds_fuse_common_args + ['-x'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding='utf-8',
**extra_kwargs)
if out.returncode:
for l in out.stdout.split('\n'):
self.log(l)
self.log('Command line:')
for l in pformat(out.args).split('\n'):
self.log(l)
return None, False, 0
install_state = {'installed': [], 'failed': []}
# Now loop through all provided cia files
for idx, info in enumerate(self.readers):
cia, path = info
self.event.on_cia_start(idx)
self.event.update_status(path, InstallStatus.Starting)
temp_title_root = join(self.sd, f'ci-install-temp-{cia.tmd.title_id}-{randint(0, 0xFFFFFFFF):08x}')
makedirs(temp_title_root, exist_ok=True)
tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16])
try:
display_title = f'{cia.contents[0].exefs.icon.get_app_title().short_desc} - {cia.tmd.title_id}'
except:
display_title = cia.tmd.title_id
self.log(f'Installing {display_title}...')
title_size = get_install_size(cia)
# checks if this is dlc, which has some differences
is_dlc = tid_parts[0] == '0004008c'
# this checks if it has a manual (index 1) and is not DLC
has_manual = (not is_dlc) and (1 in cia.contents)
# this gets the extdata id from the extheader, stored in the storage info area
try:
with cia.contents[0].open_raw_section(NCCHSection.ExtendedHeader) as e:
e.seek(0x200 + 0x30)
extdata_id = e.read(8)
except KeyError:
# not an executable title
extdata_id = b'\0' * 8
# cmd content id, starts with 1 for non-dlc contents
cmd_id = len(cia.content_info) if is_dlc else 1
cmd_filename = f'{cmd_id:08x}.cmd'
# this is where the final directory will be moved
tidhigh_root = join(sd_path, 'title', tid_parts[0])
# get the title root where all the contents will be
title_root = join(sd_path, 'title', *tid_parts)
content_root = join(title_root, 'content')
# generate the path used for the IV
title_root_cmd = f'/title/{"/".join(tid_parts)}'
content_root_cmd = title_root_cmd + '/content'
temp_content_root = join(temp_title_root, 'content')
if not self.skip_contents:
self.event.update_status(path, InstallStatus.Writing)
makedirs(join(temp_content_root, 'cmd'), exist_ok=True)
if cia.tmd.save_size:
makedirs(join(temp_title_root, 'data'), exist_ok=True)
if is_dlc:
# create the separate directories for every 256 contents
for x in range(((len(cia.content_info) - 1) // 256) + 1):
makedirs(join(temp_content_root, f'{x:08x}'), exist_ok=True)
# maybe this will be changed in the future
tmd_id = 0
tmd_filename = f'{tmd_id:08x}.tmd'
# write the tmd
tmd_enc_path = content_root_cmd + '/' + tmd_filename
self.log(f'Writing {tmd_enc_path}...')
with open(join(temp_content_root, tmd_filename), 'wb') as o:
with self.crypto.create_ctr_io(Keyslot.SD, o, self.crypto.sd_path_to_iv(tmd_enc_path)) as e:
e.write(bytes(cia.tmd))
# in case the contents are corrupted
do_continue = False
# write each content
for co in cia.content_info:
content_filename = co.id + '.app'
if is_dlc:
dir_index = format((co.cindex // 256), '08x')
content_enc_path = content_root_cmd + f'/{dir_index}/{content_filename}'
content_out_path = join(temp_content_root, dir_index, content_filename)
else:
content_enc_path = content_root_cmd + '/' + content_filename
content_out_path = join(temp_content_root, content_filename)
self.log(f'Writing {content_enc_path}...')
with cia.open_raw_section(co.cindex) as s, open(content_out_path, 'wb') as o:
result_hash = self.copy_with_progress(s, o, co.size, content_enc_path)
if result_hash != co.hash:
self.log(f'WARNING: Hash does not match for {content_enc_path}!')
install_state['failed'].append(display_title)
rename(temp_title_root, temp_title_root + '-corrupted')
do_continue = True
self.event.update_status(path, InstallStatus.Failed)
break
if do_continue:
continue
# generate a blank save
if cia.tmd.save_size:
sav_enc_path = title_root_cmd + '/data/00000001.sav'
tmp_sav_out_path = join(temp_title_root, 'data', '00000001.sav')
sav_out_path = join(title_root, 'data', '00000001.sav')
if self.overwrite_saves or not isfile(sav_out_path):
cipher = crypto.create_ctr_cipher(Keyslot.SD, crypto.sd_path_to_iv(sav_enc_path))
# in a new save, the first 0x20 are all 00s. the rest can be random
data = cipher.encrypt(b'\0' * 0x20)
self.log(f'Generating blank save at {sav_enc_path}...')
with open(tmp_sav_out_path, 'wb') as o:
o.write(data)
o.write(b'\0' * (cia.tmd.save_size - 0x20))
else:
self.log(f'Copying original save file from {sav_enc_path}...')
copy2(sav_out_path, tmp_sav_out_path)
# generate and write cmd
cmd_enc_path = content_root_cmd + '/cmd/' + cmd_filename
cmd_out_path = join(temp_content_root, 'cmd', cmd_filename)
self.log(f'Generating {cmd_enc_path}')
highest_index = 0
content_ids = {}
for record in cia.content_info:
highest_index = record.cindex
with cia.open_raw_section(record.cindex) as s:
s.seek(0x100)
cmac_data = s.read(0x100)
id_bytes = bytes.fromhex(record.id)[::-1]
cmac_data += record.cindex.to_bytes(4, 'little') + id_bytes
cmac_ncch = crypto.create_cmac_object(Keyslot.CMACSDNAND)
cmac_ncch.update(sha256(cmac_data).digest())
content_ids[record.cindex] = (id_bytes, cmac_ncch.digest())
# add content IDs up to the last one
ids_by_index = [CMD_MISSING] * (highest_index + 1)
installed_ids = []
cmacs = []
for x in range(len(ids_by_index)):
try:
info = content_ids[x]
except KeyError:
# "MISSING CONTENT!"
# The 3DS does generate a cmac for missing contents, but I don't know how it works.
# It doesn't matter anyway, the title seems to be fully functional.
cmacs.append(bytes.fromhex('4D495353494E4720434F4E54454E5421'))
else:
ids_by_index[x] = info[0]
cmacs.append(info[1])
installed_ids.append(info[0])
installed_ids.sort(key=lambda x: int.from_bytes(x, 'little'))
final = (cmd_id.to_bytes(4, 'little')
+ len(ids_by_index).to_bytes(4, 'little')
+ len(installed_ids).to_bytes(4, 'little')
+ (1).to_bytes(4, 'little'))
cmac_cmd_header = crypto.create_cmac_object(Keyslot.CMACSDNAND)
cmac_cmd_header.update(final)
final += cmac_cmd_header.digest()
final += b''.join(ids_by_index)
final += b''.join(installed_ids)
final += b''.join(cmacs)
cipher = crypto.create_ctr_cipher(Keyslot.SD, crypto.sd_path_to_iv(cmd_enc_path))
self.log(f'Writing {cmd_enc_path}')
with open(cmd_out_path, 'wb') as o:
o.write(cipher.encrypt(final))
# this starts building the title info entry
title_info_entry_data = [
# title size
title_size.to_bytes(8, 'little'),
# title type, seems to usually be 0x40
0x40.to_bytes(4, 'little'),
# title version
int(cia.tmd.title_version).to_bytes(2, 'little'),
# ncch version
cia.contents[0].version.to_bytes(2, 'little'),
# flags_0, only checking if there is a manual
(1 if has_manual else 0).to_bytes(4, 'little'),
# tmd content id, always starting with 0
(0).to_bytes(4, 'little'),
# cmd content id
cmd_id.to_bytes(4, 'little'),
# flags_1, only checking save data
(1 if cia.tmd.save_size else 0).to_bytes(4, 'little'),
# extdataid low
extdata_id[0:4],
# reserved
b'\0' * 4,
# flags_2, only using a common value
0x100000000.to_bytes(8, 'little'),
# product code
cia.contents[0].product_code.encode('ascii').ljust(0x10, b'\0'),
# reserved
b'\0' * 0x10,
# unknown
randint(0, 0xFFFFFFFF).to_bytes(4, 'little'),
# reserved
b'\0' * 0x2c
]
self.event.update_status(path, InstallStatus.Finishing)
if isdir(title_root):
self.log(f'Removing original install at {title_root}...')
rmtree(title_root)
makedirs(tidhigh_root, exist_ok=True)
rename(temp_title_root, title_root)
cifinish_data[int(cia.tmd.title_id, 16)] = {'seed': (get_seed(cia.contents[0].program_id) if cia.contents[0].flags.uses_seed else None)}
# This is saved regardless if any titles were installed, so the file can be upgraded just in case.
save_cifinish(cifinish_path, cifinish_data)
with open(join(tempdir, cia.tmd.title_id), 'wb') as o:
o.write(b''.join(title_info_entry_data))
# import the directory, now including our title
self.log('Importing into Title Database...')
out = subprocess.run(save3ds_fuse_common_args + ['-i'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding='utf-8',
**extra_kwargs)
if out.returncode:
for l in out.stdout.split('\n'):
self.log(l)
self.log('Command line:')
for l in pformat(out.args).split('\n'):
self.log(l)
install_state['failed'].append(display_title)
self.event.update_status(path, InstallStatus.Failed)
else:
install_state['installed'].append(display_title)
self.event.update_status(path, InstallStatus.Done)
copied = False
# launchable applications, not DLC or update data
application_count = len(glob(join(tempdir, '00040000*')))
if install_state['installed']:
if application_count >= 300:
self.log(f'{application_count} installed applications were detected.', 1)
self.log('The HOME Menu will only show 300 icons.', 1)
self.log('Some applications (not updates or DLC) will need to be deleted.', 1)
finalize_3dsx_orig_path = join(script_dir, 'custom-install-finalize.3dsx')
hb_dir = join(self.sd, '3ds')
finalize_3dsx_path = join(hb_dir, 'custom-install-finalize.3dsx')
if isfile(finalize_3dsx_orig_path):
self.log('Copying finalize program to ' + finalize_3dsx_path)
makedirs(hb_dir, exist_ok=True)
copyfile(finalize_3dsx_orig_path, finalize_3dsx_path)
copied = True
self.log('FINAL STEP:')
self.log('Run custom-install-finalize through homebrew launcher.')
self.log('This will install a ticket and seed if required.')
if copied:
self.log('custom-install-finalize has been copied to the SD card.')
return install_state, copied, application_count
def get_sd_path(self):
sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
id1s = []
for d in scandir(sd_path):
if d.is_dir() and len(d.name) == 32:
try:
# check if the name can be converted to hex
# I'm not sure what the 3DS does if there is a folder that is not a 32-char hex string.
bytes.fromhex(d.name)
except ValueError:
continue
else:
id1s.append(d.name)
return [sd_path, id1s]
def log(self, message, mtype=0, errorname=None, end='\n'):
"""Logs an Message with a type. Format is similar to python errors
There are 3 types of errors, indexed accordingly
type 0 = Message
type 1 = Warning
type 2 = Error
optionally, errorname can be a custom name as a string to identify errors easily
"""
if errorname:
errorname += ": "
else:
# No errorname provided
errorname = ""
types = [
"", # Type 0
"Warning: ", # Type 1
"Error: " # Type 2
]
# Example: "Warning: UninformativeError: An error occured, try again.""
msg_with_type = types[mtype] + errorname + str(message)
self.log_lines.append(msg_with_type)
self.event.on_log_msg(msg_with_type, end=end)
return msg_with_type
def main():
parser = ArgumentParser(description='Install a CIA to the SD card for a Nintendo 3DS system.')
parser.add_argument('cia', help='CIA files', nargs='+')
parser.add_argument('-m', '--movable', help='movable.sed file', required=True)
parser.add_argument('-b', '--boot9', help='boot9 file')
parser.add_argument('-s', '--seeddb', help='seeddb file')
parser.add_argument('--sd', help='path to SD root', required=True)
parser.add_argument('--skip-contents', help="don't add contents, only add title info entry", action='store_true')
parser.add_argument('--overwrite-saves', help='overwrite existing save files', action='store_true')
parser.add_argument('--cifinish-out', help='path for cifinish.bin file, defaults to (SD root)/cifinish.bin')
print(f'custom-install {__version__} - https://github.com/ihaveamac/custom-install')
args = parser.parse_args()
installer = CustomInstall(boot9=args.boot9,
seeddb=args.seeddb,
movable=args.movable,
sd=args.sd,
overwrite_saves=args.overwrite_saves,
cifinish_out=args.cifinish_out,
skip_contents=(args.skip_contents or False))
def log_handle(msg, end='\n'):
print(msg, end=end)
def percent_handle(total_percent, total_read, size):
installer.log(f' {total_percent:>5.1f}% {total_read:>.1f} MiB / {size:.1f} MiB\r', end='')
def error(exc):
for line in format_exception(*exc):
for line2 in line.split('\n')[:-1]:
installer.log(line2)
installer.event.on_log_msg += log_handle
installer.event.update_percentage += percent_handle
installer.event.on_error += error
if not installer.check_for_id0():
installer.event.on_error(f'Could not find id0 directory {installer.crypto.id0.hex()} '
f'inside Nintendo 3DS directory.')
installer.prepare_titles(args.cia)
if not args.skip_contents:
total_size, free_space = installer.check_size()
if total_size > free_space:
installer.event.on_log_msg(f'Not enough free space.\n'
f'Combined title install size: {total_size / (1024 * 1024):0.2f} MiB\n'
f'Free space: {free_space / (1024 * 1024):0.2f} MiB')
sys.exit(1)
result, copied_3dsx, application_count = installer.start()
if result is False:
# save3ds_fuse failed
installer.log('NOTE: Once save3ds_fuse is fixed, run the same command again with --skip-contents')
if application_count >= 300:
installer.log(f'\n\nWarning: {application_count} installed applications were detected.\n'
f'The HOME Menu will only show 300 icons.\n'
f'Some applications (not updates or DLC) will need to be deleted.')
if __name__ == "__main__":
main()

View File

@@ -1,11 +0,0 @@
save3ds_fuse for win32 and darwin built with commit 568b0597b17da0c8cfbd345bab27176cd84bd883
in repository https://github.com/wwylele/save3ds
win32 binary built on Windows 10, version 21H2 64-bit with `cargo build --release --target=i686-pc-windows-msvc`.
darwin binary built on macOS 12.2 with:
* `cargo build --target=aarch64-apple-darwin --no-default-features --release`
* `cargo build --target=x86_64-apple-darwin --no-default-features --release`
* Then a universal binary is built: `lipo -create -output save3ds_fuse-universal2 target/aarch64-apple-darwin/release/save3ds_fuse target/x86_64-apple-darwin/release/save3ds_fuse`
linux binary must be provided by the user.

View File

@@ -1,760 +0,0 @@
#!/usr/bin/env python3
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
from os import environ, scandir
from os.path import abspath, basename, dirname, join, isfile
import sys
from threading import Thread, Lock
from time import strftime
from traceback import format_exception
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.filedialog as fd
import tkinter.messagebox as mb
from typing import TYPE_CHECKING
from pyctr.crypto import MissingSeedError, CryptoEngine, load_seeddb
from pyctr.crypto.engine import b9_paths
from pyctr.util import config_dirs
from pyctr.type.cdn import CDNError
from pyctr.type.cia import CIAError
from pyctr.type.tmd import TitleMetadataError
from . import __version__
from .__main__ import CustomInstall, load_cifinish, InvalidCIFinishError, InstallStatus, save3ds_fuse_path
if TYPE_CHECKING:
from os import PathLike
from typing import Dict, List, Union
frozen = getattr(sys, 'frozen', None)
is_windows = sys.platform == 'win32'
taskbar = None
if is_windows:
if frozen:
# attempt to fix loading tcl/tk when running from a path with non-latin characters
tkinter_path = dirname(tk.__file__)
tcl_path = join(tkinter_path, 'tcl8.6')
environ['TCL_LIBRARY'] = 'lib/tkinter/tcl8.6'
try:
import comtypes.client as cc
tbl = cc.GetModule('TaskbarLib.tlb')
taskbar = cc.CreateObject('{56FDF344-FD6D-11D0-958A-006097C9A090}', interface=tbl.ITaskbarList3)
taskbar.HrInit()
except (ModuleNotFoundError, UnicodeEncodeError, AttributeError):
pass
file_parent = dirname(abspath(__file__))
# automatically load boot9 if it's in the current directory
b9_paths.insert(0, join(file_parent, 'boot9.bin'))
b9_paths.insert(0, join(file_parent, 'boot9_prot.bin'))
seeddb_paths = [join(x, 'seeddb.bin') for x in config_dirs]
try:
seeddb_paths.insert(0, environ['SEEDDB_PATH'])
except KeyError:
pass
# automatically load seeddb if it's in the current directory
seeddb_paths.insert(0, join(file_parent, 'seeddb.bin'))
def clamp(n, smallest, largest):
return max(smallest, min(n, largest))
def find_first_file(paths):
for p in paths:
if isfile(p):
return p
# find boot9, seeddb, and movable.sed to auto-select in the gui
default_b9_path = find_first_file(b9_paths)
default_seeddb_path = find_first_file(seeddb_paths)
default_movable_sed_path = find_first_file([join(file_parent, 'movable.sed')])
if default_seeddb_path:
load_seeddb(default_seeddb_path)
statuses = {
InstallStatus.Waiting: 'Waiting',
InstallStatus.Starting: 'Starting',
InstallStatus.Writing: 'Writing',
InstallStatus.Finishing: 'Finishing',
InstallStatus.Done: 'Done',
InstallStatus.Failed: 'Failed',
}
class ConsoleFrame(ttk.Frame):
def __init__(self, parent: tk.BaseWidget = None, starting_lines: 'List[str]' = None):
super().__init__(parent)
self.parent = parent
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
self.text = tk.Text(self, highlightthickness=0, wrap='word', yscrollcommand=scrollbar.set)
self.text.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar.config(command=self.text.yview)
if starting_lines:
for l in starting_lines:
self.text.insert(tk.END, l + '\n')
self.text.see(tk.END)
self.text.configure(state=tk.DISABLED)
def log(self, *message, end='\n', sep=' '):
self.text.configure(state=tk.NORMAL)
self.text.insert(tk.END, sep.join(message) + end)
self.text.see(tk.END)
self.text.configure(state=tk.DISABLED)
def simple_listbox_frame(parent, title: 'str', items: 'List[str]'):
frame = ttk.LabelFrame(parent, text=title)
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL)
scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
box = tk.Listbox(frame, highlightthickness=0, yscrollcommand=scrollbar.set, selectmode=tk.EXTENDED)
box.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar.config(command=box.yview)
box.insert(tk.END, *items)
box.config(height=clamp(len(items), 3, 10))
return frame
class TitleReadFailResults(tk.Toplevel):
def __init__(self, parent: tk.Tk = None, *, failed: 'Dict[str, str]'):
super().__init__(parent)
self.parent = parent
self.wm_withdraw()
self.wm_transient(self.parent)
self.grab_set()
self.wm_title('Failed to add titles')
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
outer_container = ttk.Frame(self)
outer_container.grid(sticky=tk.NSEW)
outer_container.rowconfigure(0, weight=0)
outer_container.rowconfigure(1, weight=1)
outer_container.columnconfigure(0, weight=1)
message_label = ttk.Label(outer_container, text="Some titles couldn't be added.")
message_label.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=10)
treeview_frame = ttk.Frame(outer_container)
treeview_frame.grid(row=1, column=0, sticky=tk.NSEW)
treeview_frame.rowconfigure(0, weight=1)
treeview_frame.columnconfigure(0, weight=1)
treeview_scrollbar = ttk.Scrollbar(treeview_frame, orient=tk.VERTICAL)
treeview_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
treeview = ttk.Treeview(treeview_frame, yscrollcommand=treeview_scrollbar.set)
treeview.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10))
treeview.configure(columns=('filepath', 'reason'), show='headings')
treeview.column('filepath', width=200, anchor=tk.W)
treeview.heading('filepath', text='File path')
treeview.column('reason', width=400, anchor=tk.W)
treeview.heading('reason', text='Reason')
treeview_scrollbar.configure(command=treeview.yview)
for path, reason in failed.items():
treeview.insert('', tk.END, text=path, iid=path, values=(basename(path), reason))
ok_frame = ttk.Frame(outer_container)
ok_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10))
ok_frame.rowconfigure(0, weight=1)
ok_frame.columnconfigure(0, weight=1)
ok_button = ttk.Button(ok_frame, text='OK', command=self.destroy)
ok_button.grid(row=0, column=0)
self.wm_deiconify()
class InstallResults(tk.Toplevel):
def __init__(self, parent: tk.Tk = None, *, install_state: 'Dict[str, List[str]]', copied_3dsx: bool,
application_count: int):
super().__init__(parent)
self.parent = parent
self.wm_withdraw()
self.wm_transient(self.parent)
self.grab_set()
self.wm_title('Install results')
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
outer_container = ttk.Frame(self)
outer_container.grid(sticky=tk.NSEW)
outer_container.rowconfigure(0, weight=0)
outer_container.columnconfigure(0, weight=1)
if install_state['failed'] and install_state['installed']:
# some failed and some worked
message = ('Some titles were installed, some failed. Please check the output for more details.\n'
'The ones that were installed can be finished with custom-install-finalize.')
elif install_state['failed'] and not install_state['installed']:
# all failed
message = 'All titles failed to install. Please check the output for more details.'
elif install_state['installed'] and not install_state['failed']:
# all worked
message = 'All titles were installed.'
else:
message = 'Nothing was installed.'
if install_state['installed']:
if copied_3dsx:
message += '\n\ncustom-install-finalize has been copied to the SD card.'
else:
message += ('\n\nNote: custom-install-finalize was not copied.\n'
'You can either manually copy the 3dsx to your SD card, or use GodMode9 to finish the install.')
if application_count >= 300:
message += (f'\n\nWarning: {application_count} installed applications were detected.\n'
f'The HOME Menu will only show 300 icons.\n'
f'Some applications (not updates or DLC) will need to be deleted.')
message_label = ttk.Label(outer_container, text=message)
message_label.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=10)
if install_state['installed']:
outer_container.rowconfigure(1, weight=1)
frame = simple_listbox_frame(outer_container, 'Installed', install_state['installed'])
frame.grid(row=1, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10))
if install_state['failed']:
outer_container.rowconfigure(2, weight=1)
frame = simple_listbox_frame(outer_container, 'Failed', install_state['failed'])
frame.grid(row=2, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10))
ok_frame = ttk.Frame(outer_container)
ok_frame.grid(row=3, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10))
ok_frame.rowconfigure(0, weight=1)
ok_frame.columnconfigure(0, weight=1)
ok_button = ttk.Button(ok_frame, text='OK', command=self.destroy)
ok_button.grid(row=0, column=0)
self.wm_deiconify()
class CustomInstallGUI(ttk.Frame):
console = None
b9_loaded = False
def __init__(self, parent: tk.Tk = None):
super().__init__(parent)
self.parent = parent
# readers to give to CustomInstall at the install
self.readers = {}
self.lock = Lock()
self.log_messages = []
self.hwnd = None # will be set later
self.rowconfigure(2, weight=1)
self.columnconfigure(0, weight=1)
if taskbar:
# this is so progress can be shown in the taskbar
def setup_tab():
self.hwnd = int(parent.wm_frame(), 16)
taskbar.ActivateTab(self.hwnd)
self.after(100, setup_tab)
# ---------------------------------------------------------------- #
# create file pickers for base files
file_pickers = ttk.Frame(self)
file_pickers.grid(row=0, column=0, sticky=tk.EW)
file_pickers.columnconfigure(1, weight=1)
self.file_picker_textboxes = {}
def sd_callback():
f = fd.askdirectory(parent=parent, title='Select SD root (the directory or drive that contains '
'"Nintendo 3DS")', initialdir=file_parent, mustexist=True)
if f:
cifinish_path = join(f, 'cifinish.bin')
try:
load_cifinish(cifinish_path)
except InvalidCIFinishError:
self.show_error(f'{cifinish_path} was corrupt!\n\n'
f'This could mean an issue with the SD card or the filesystem. Please check it for errors.\n'
f'It is also possible, though less likely, to be an issue with custom-install.\n\n'
f'Stopping now to prevent possible issues. If you want to try again, delete cifinish.bin from the SD card and re-run custom-install.')
return
sd_selected.delete('1.0', tk.END)
sd_selected.insert(tk.END, f)
for filename in ['boot9.bin', 'seeddb.bin', 'movable.sed']:
path = auto_input_filename(self, f, filename)
if filename == 'boot9.bin':
self.check_b9_loaded()
self.enable_buttons()
if filename == 'seeddb.bin':
load_seeddb(path)
sd_type_label = ttk.Label(file_pickers, text='SD root')
sd_type_label.grid(row=0, column=0)
sd_selected = tk.Text(file_pickers, wrap='none', height=1)
sd_selected.grid(row=0, column=1, sticky=tk.EW)
sd_button = ttk.Button(file_pickers, text='...', command=sd_callback)
sd_button.grid(row=0, column=2)
self.file_picker_textboxes['sd'] = sd_selected
def auto_input_filename(self, f, filename):
sd_msed_path = find_first_file(
[join(f, "gm9", "out", filename), join(f, "boot9strap", filename), join(f, filename)]
)
if sd_msed_path:
self.log('Found ' + filename + ' on SD card at ' + sd_msed_path)
if filename.endswith('bin'):
filename = filename.split('.')[0]
box = self.file_picker_textboxes[filename]
box.delete('1.0', tk.END)
box.insert(tk.END, sd_msed_path)
return sd_msed_path
# This feels so wrong.
def create_required_file_picker(type_name, types, default, row, callback=lambda filename: None):
def internal_callback():
f = fd.askopenfilename(parent=parent, title='Select ' + type_name, filetypes=types,
initialdir=file_parent)
if f:
selected.delete('1.0', tk.END)
selected.insert(tk.END, f)
callback(f)
type_label = ttk.Label(file_pickers, text=type_name)
type_label.grid(row=row, column=0)
selected = tk.Text(file_pickers, wrap='none', height=1)
selected.grid(row=row, column=1, sticky=tk.EW)
if default:
selected.insert(tk.END, default)
button = ttk.Button(file_pickers, text='...', command=internal_callback)
button.grid(row=row, column=2)
self.file_picker_textboxes[type_name] = selected
def b9_callback(path: 'Union[PathLike, bytes, str]'):
self.check_b9_loaded()
self.enable_buttons()
def seeddb_callback(path: 'Union[PathLike, bytes, str]'):
load_seeddb(path)
create_required_file_picker('boot9', [('boot9 file', '*.bin')], default_b9_path, 1, b9_callback)
create_required_file_picker('seeddb', [('seeddb file', '*.bin')], default_seeddb_path, 2, seeddb_callback)
create_required_file_picker('movable.sed', [('movable.sed file', '*.sed')], default_movable_sed_path, 3)
# ---------------------------------------------------------------- #
# create buttons to add cias
titlelist_buttons = ttk.Frame(self)
titlelist_buttons.grid(row=1, column=0)
def add_cias_callback():
files = fd.askopenfilenames(parent=parent, title='Select CIA files', filetypes=[('CIA files', '*.cia')],
initialdir=file_parent)
results = {}
for f in files:
success, reason = self.add_cia(f)
if not success:
results[f] = reason
if results:
title_read_fail_window = TitleReadFailResults(self.parent, failed=results)
title_read_fail_window.focus()
self.sort_treeview()
add_cias = ttk.Button(titlelist_buttons, text='Add CIAs', command=add_cias_callback)
add_cias.grid(row=0, column=0)
def add_cdn_callback():
d = fd.askdirectory(parent=parent, title='Select folder containing title contents in CDN format',
initialdir=file_parent)
if d:
if isfile(join(d, 'tmd')):
success, reason = self.add_cia(d)
if not success:
self.show_error(f"Couldn't add {basename(d)}: {reason}")
else:
self.sort_treeview()
else:
self.show_error('tmd file not found in the CDN directory:\n' + d)
add_cdn = ttk.Button(titlelist_buttons, text='Add CDN title folder', command=add_cdn_callback)
add_cdn.grid(row=0, column=1)
def add_dirs_callback():
d = fd.askdirectory(parent=parent, title='Select folder containing CIA files', initialdir=file_parent)
if d:
results = {}
for f in scandir(d):
if f.name.lower().endswith('.cia'):
success, reason = self.add_cia(f.path)
if not success:
results[f] = reason
if results:
title_read_fail_window = TitleReadFailResults(self.parent, failed=results)
title_read_fail_window.focus()
self.sort_treeview()
add_dirs = ttk.Button(titlelist_buttons, text='Add folder', command=add_dirs_callback)
add_dirs.grid(row=0, column=2)
def remove_selected_callback():
for entry in self.treeview.selection():
self.remove_cia(entry)
remove_selected = ttk.Button(titlelist_buttons, text='Remove selected', command=remove_selected_callback)
remove_selected.grid(row=0, column=3)
# ---------------------------------------------------------------- #
# create treeview
treeview_frame = ttk.Frame(self)
treeview_frame.grid(row=2, column=0, sticky=tk.NSEW)
treeview_frame.rowconfigure(0, weight=1)
treeview_frame.columnconfigure(0, weight=1)
treeview_scrollbar = ttk.Scrollbar(treeview_frame, orient=tk.VERTICAL)
treeview_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
self.treeview = ttk.Treeview(treeview_frame, yscrollcommand=treeview_scrollbar.set)
self.treeview.grid(row=0, column=0, sticky=tk.NSEW)
self.treeview.configure(columns=('filepath', 'titleid', 'titlename', 'status'), show='headings')
self.treeview.column('filepath', width=200, anchor=tk.W)
self.treeview.heading('filepath', text='File path')
self.treeview.column('titleid', width=70, anchor=tk.W)
self.treeview.heading('titleid', text='Title ID')
self.treeview.column('titlename', width=150, anchor=tk.W)
self.treeview.heading('titlename', text='Title name')
self.treeview.column('status', width=20, anchor=tk.W)
self.treeview.heading('status', text='Status')
treeview_scrollbar.configure(command=self.treeview.yview)
# ---------------------------------------------------------------- #
# create progressbar
self.progressbar = ttk.Progressbar(self, orient=tk.HORIZONTAL, mode='determinate')
self.progressbar.grid(row=3, column=0, sticky=tk.NSEW)
# ---------------------------------------------------------------- #
# create start and console buttons
control_frame = ttk.Frame(self)
control_frame.grid(row=4, column=0)
self.skip_contents_var = tk.IntVar()
skip_contents_checkbox = ttk.Checkbutton(control_frame, text='Skip contents (only add to title database)',
variable=self.skip_contents_var)
skip_contents_checkbox.grid(row=0, column=0)
self.overwrite_saves_var = tk.IntVar()
overwrite_saves_checkbox = ttk.Checkbutton(control_frame, text='Overwrite existing saves',
variable=self.overwrite_saves_var)
overwrite_saves_checkbox.grid(row=0, column=1)
show_console = ttk.Button(control_frame, text='Show console', command=self.open_console)
show_console.grid(row=0, column=2)
start = ttk.Button(control_frame, text='Start install', command=self.start_install)
start.grid(row=0, column=3)
self.status_label = ttk.Label(self, text='Waiting...')
self.status_label.grid(row=5, column=0, sticky=tk.NSEW)
self.log(f'custom-install {__version__} - https://github.com/ihaveamac/custom-install', status=False)
if is_windows and not taskbar:
self.log('Note: Could not load taskbar lib.')
self.log('Note: Progress will not be shown in the Windows taskbar.')
self.log('Ready.')
self.require_boot9 = (add_cias, add_cdn, add_dirs, remove_selected, start)
self.disable_buttons()
self.check_b9_loaded()
self.enable_buttons()
if not self.b9_loaded:
self.log('Note: boot9 was not auto-detected. Please choose it before adding any titles.')
def sort_treeview(self):
l = [(self.treeview.set(k, 'titlename'), k) for k in self.treeview.get_children()]
# sort by title name
l.sort(key=lambda x: x[0].lower())
for idx, pair in enumerate(l):
self.treeview.move(pair[1], '', idx)
def check_b9_loaded(self):
if not self.b9_loaded:
boot9 = self.file_picker_textboxes['boot9'].get('1.0', tk.END).strip()
try:
tmp_crypto = CryptoEngine(boot9=boot9)
self.b9_loaded = tmp_crypto.b9_keys_set
except:
return False
return self.b9_loaded
def update_status(self, path: 'Union[PathLike, bytes, str]', status: InstallStatus):
self.treeview.set(path, 'status', statuses[status])
def add_cia(self, path):
if not self.check_b9_loaded():
# this shouldn't happen
return False, 'Please choose boot9 first'
path = abspath(path)
if path in self.readers:
return False, 'File already in list'
try:
reader = CustomInstall.get_reader(path)
except (CIAError, CDNError, TitleMetadataError):
return False, 'Failed to read as a CIA or CDN title, probably corrupt'
except MissingSeedError:
return False, 'Latest seeddb.bin is required, check the README for details'
except Exception as e:
return False, f'Exception occurred: {type(e).__name__}: {e}'
if reader.tmd.title_id.startswith('00048'):
return False, 'DSiWare is not supported'
try:
title_name = reader.contents[0].exefs.icon.get_app_title().short_desc
except:
title_name = '(No title)'
self.treeview.insert('', tk.END, text=path, iid=path,
values=(path, reader.tmd.title_id, title_name, statuses[InstallStatus.Waiting]))
self.readers[path] = reader
return True, ''
def remove_cia(self, path):
self.treeview.delete(path)
del self.readers[path]
def open_console(self):
if self.console:
self.console.parent.lift()
self.console.focus()
else:
console_window = tk.Toplevel()
console_window.title('custom-install Console')
self.console = ConsoleFrame(console_window, self.log_messages)
self.console.pack(fill=tk.BOTH, expand=True)
def close():
with self.lock:
try:
console_window.destroy()
except:
pass
self.console = None
console_window.focus()
console_window.protocol('WM_DELETE_WINDOW', close)
def log(self, line, status=True):
with self.lock:
log_msg = f"{strftime('%H:%M:%S')} - {line}"
self.log_messages.append(log_msg)
if self.console:
self.console.log(log_msg)
if status:
self.status_label.config(text=line)
print(log_msg)
def show_error(self, message):
mb.showerror('Error', message, parent=self.parent)
def ask_warning(self, message):
return mb.askokcancel('Warning', message, parent=self.parent)
def show_info(self, message):
mb.showinfo('Info', message, parent=self.parent)
def disable_buttons(self):
for b in self.require_boot9:
b.config(state=tk.DISABLED)
for b in self.file_picker_textboxes.values():
b.config(state=tk.DISABLED)
def enable_buttons(self):
if self.b9_loaded:
for b in self.require_boot9:
b.config(state=tk.NORMAL)
for b in self.file_picker_textboxes.values():
b.config(state=tk.NORMAL)
def start_install(self):
sd_root = self.file_picker_textboxes['sd'].get('1.0', tk.END).strip()
seeddb = self.file_picker_textboxes['seeddb'].get('1.0', tk.END).strip()
movable_sed = self.file_picker_textboxes['movable.sed'].get('1.0', tk.END).strip()
if not sd_root:
self.show_error('SD root is not specified.')
return
if not movable_sed:
self.show_error('movable.sed is not specified.')
return
if not seeddb:
if not self.ask_warning('seeddb was not specified. Titles that require it will fail to install.\n'
'Continue?'):
return
if not len(self.readers):
self.show_error('There are no titles added to install.')
return
for path in self.readers.keys():
self.update_status(path, InstallStatus.Waiting)
self.disable_buttons()
if taskbar:
taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL)
installer = CustomInstall(movable=movable_sed,
sd=sd_root,
skip_contents=self.skip_contents_var.get() == 1,
overwrite_saves=self.overwrite_saves_var.get() == 1)
if not installer.check_for_id0():
self.show_error(f'id0 {installer.crypto.id0.hex()} was not found inside "Nintendo 3DS" on the SD card.\n'
f'\n'
f'Before using custom-install, you should use this SD card on the appropriate console.\n'
f'\n'
f'Otherwise, make sure the correct movable.sed is being used.')
return
self.log('Starting install...')
# use the treeview which has been sorted alphabetically
readers_final = []
for k in self.treeview.get_children():
filepath = self.treeview.set(k, 'filepath')
readers_final.append((self.readers[filepath], filepath))
installer.readers = readers_final
finished_percent = 0
max_percentage = 100 * len(self.readers)
self.progressbar.config(maximum=max_percentage)
def ci_on_log_msg(message, *args, **kwargs):
# ignoring end
self.log(message)
def ci_update_percentage(total_percent, total_read, size):
self.progressbar.config(value=total_percent + finished_percent)
if taskbar:
taskbar.SetProgressValue(self.hwnd, int(total_percent + finished_percent), max_percentage)
def ci_on_error(exc):
if taskbar:
taskbar.SetProgressState(self.hwnd, tbl.TBPF_ERROR)
for line in format_exception(*exc):
for line2 in line.split('\n')[:-1]:
installer.log(line2)
self.show_error('An error occurred during installation.')
self.open_console()
def ci_on_cia_start(idx):
nonlocal finished_percent
finished_percent = idx * 100
if taskbar:
taskbar.SetProgressValue(self.hwnd, finished_percent, max_percentage)
installer.event.on_log_msg += ci_on_log_msg
installer.event.update_percentage += ci_update_percentage
installer.event.on_error += ci_on_error
installer.event.on_cia_start += ci_on_cia_start
installer.event.update_status += self.update_status
if self.skip_contents_var.get() != 1:
total_size, free_space = installer.check_size()
if total_size > free_space:
self.show_error(f'Not enough free space.\n'
f'Combined title install size: {total_size / (1024 * 1024):0.2f} MiB\n'
f'Free space: {free_space / (1024 * 1024):0.2f} MiB')
self.enable_buttons()
return
def install():
try:
result, copied_3dsx, application_count = installer.start()
if result:
result_window = InstallResults(self.parent,
install_state=result,
copied_3dsx=copied_3dsx,
application_count=application_count)
result_window.focus()
elif result is None:
self.show_error("An error occurred when trying to run save3ds_fuse.\n"
"Either title.db doesn't exist, or save3ds_fuse couldn't be run.")
self.open_console()
except:
installer.event.on_error(sys.exc_info())
finally:
self.enable_buttons()
Thread(target=install).start()
def main():
if not (save3ds_fuse_path and isfile(save3ds_fuse_path)):
mb.showerror('Error', "Couldn't find save3ds_fuse. Please place it PATH.")
return
window = tk.Tk()
window.title(f'custom-install {__version__}')
frame = CustomInstallGUI(window)
frame.pack(fill=tk.BOTH, expand=True)
window.mainloop()
if __name__ == '__main__':
main()

Binary file not shown.

View File

@@ -1,34 +0,0 @@
{
pkgs ? import <nixpkgs> { },
# just so i can use the same pinned version as the flake...
pyctr ? (
let
flakeLock = builtins.fromJSON (builtins.readFile ./flake.lock);
pyctr-repo = import (builtins.fetchTarball (
with flakeLock.nodes.pyctr.locked;
{
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz";
}
)) { inherit pkgs; };
in
pyctr-repo.pyctr
),
save3ds ? (
let
flakeLock = builtins.fromJSON (builtins.readFile ./flake.lock);
hax-nur-repo = import (builtins.fetchTarball (
with flakeLock.nodes.hax-nur.locked;
{
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz";
}
)) { inherit pkgs; };
in
hax-nur-repo.save3ds
),
}:
rec {
custominstall = pkgs.python3Packages.callPackage ./package.nix {
inherit pyctr save3ds;
};
}

BIN
docu/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,7 +0,0 @@
Run ci-gui to bring up the custom-install gui.
Select your SD card root, boot9, seeddb, and movable.sed files.
In some cases these will be automatically selected for you.
Add the CIA files and click "Start install".
Once it's finished, start up the homebrew launcher and run custom-install-finalize to finish the process.

View File

@@ -54,7 +54,7 @@ CFLAGS := -g -Wall -O2 -mword-relocations \
-fomit-frame-pointer -ffunction-sections \
$(ARCH)
CFLAGS += $(INCLUDE) -D__3DS__
CFLAGS += $(INCLUDE) -DARM11 -D_3DS
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11

82
finalize/flake.lock generated
View File

@@ -1,82 +0,0 @@
{
"nodes": {
"devkitNix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1766539742,
"narHash": "sha256-F6OeM2LrLo2n+Xg5XU4udQR/vuWWrDMKxXRzNXE2ClQ=",
"owner": "bandithedoge",
"repo": "devkitNix",
"rev": "c97f9880737716085e78009cba6bf85ad104628b",
"type": "github"
},
"original": {
"owner": "bandithedoge",
"repo": "devkitNix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1768032153,
"narHash": "sha256-6kD1MdY9fsE6FgSwdnx29hdH2UcBKs3/+JJleMShuJg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3146c6aa9995e7351a398e17470e15305e6e18ff",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devkitNix": "devkitNix",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,33 +0,0 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
devkitNix.url = "github:bandithedoge/devkitNix";
devkitNix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, devkitNix }: let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ devkitNix.overlays.default ]; };
in {
devShells.x86_64-linux = rec {
custom-install-finalize = pkgs.mkShell.override { stdenv = pkgs.devkitNix.stdenvARM; } {};
cif = custom-install-finalize;
default = custom-install-finalize;
};
packages.x86_64-linux = rec {
custom-install-finalize = pkgs.devkitNix.stdenvARM.mkDerivation rec {
name = "custom-install-finalize";
src = builtins.path { path = ./.; name = name; };
makeFlags = [ "TARGET=${name}" ];
installPhase = ''
mkdir $out
cp ${name}.3dsx $out
'';
};
cif = custom-install-finalize;
default = custom-install-finalize;
};
};
}

View File

@@ -189,7 +189,6 @@ int load_cifinish(char* path, struct finish_db_entry_final **entries)
}
}
fclose(fp);
return header.title_count;
fail:
@@ -197,99 +196,34 @@ fail:
return -1;
}
Result check_title_exist(u64 title_id, u64 *ticket_ids, u32 ticket_ids_length, u64 *title_ids, u32 title_ids_length)
{
Result ret = -2;
for (u32 i = 0; i < ticket_ids_length; i++)
{
if (ticket_ids[i] == title_id)
{
ret++;
break;
}
}
for (u32 i = 0; i < title_ids_length; i++)
{
if (title_ids[i] == title_id)
{
ret++;
break;
}
}
return ret;
}
void finalize_install(void)
{
Result res;
Handle ticketHandle;
struct ticket_dumb ticket_buf;
struct finish_db_entry_final *entries = NULL;
int title_count;
u32 titles_read;
u32 tickets_read;
res = AM_GetTitleCount(MEDIATYPE_SD, &titles_read);
if (R_FAILED(res))
{
return;
}
res = AM_GetTicketCount(&tickets_read);
if (R_FAILED(res))
{
return;
}
u64 *installed_ticket_ids = malloc(sizeof(u64) * tickets_read );
u64 *installed_title_ids = malloc(sizeof(u64) * titles_read );
res = AM_GetTitleList(&titles_read, MEDIATYPE_SD, titles_read, installed_title_ids);
if (R_FAILED(res))
{
goto exit;
}
res = AM_GetTicketList(&tickets_read, tickets_read, 0, installed_ticket_ids);
if (R_FAILED(res))
{
goto exit;
}
struct finish_db_entry_final *entries;
int title_count;
title_count = load_cifinish(CIFINISH_PATH, &entries);
if (title_count == -1)
{
goto exit;
free(entries);
return;
}
else if (title_count == 0)
if (title_count == 0)
{
printf("No titles to finalize.\n");
goto exit;
free(entries);
return;
}
//printf("Deleting %s...\n", CIFINISH_PATH);
//unlink(CIFINISH_PATH);
memcpy(&ticket_buf, basetik_bin, basetik_bin_size);
Result exist_res = 0;
for (int i = 0; i < title_count; ++i)
{
exist_res = check_title_exist(entries[i].title_id, installed_ticket_ids, tickets_read, installed_title_ids, titles_read);
if (R_SUCCEEDED(exist_res))
{
printf("No need to finalize %016llx, skipping...\n", entries[i].title_id);
continue;
}
printf("Finalizing %016llx...\n", entries[i].title_id);
ticket_buf.title_id_be = __builtin_bswap64(entries[i].title_id);
@@ -299,7 +233,8 @@ void finalize_install(void)
{
printf("Failed to begin ticket install: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
goto exit;
free(entries);
return;
}
res = FSFILE_Write(ticketHandle, NULL, 0, &ticket_buf, sizeof(struct ticket_dumb), 0);
@@ -307,7 +242,8 @@ void finalize_install(void)
{
printf("Failed to write ticket: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
goto exit;
free(entries);
return;
}
res = AM_InstallTicketFinish(ticketHandle);
@@ -315,7 +251,8 @@ void finalize_install(void)
{
printf("Failed to finish ticket install: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
goto exit;
free(entries);
return;
}
if (entries[i].has_seed)
@@ -329,15 +266,7 @@ void finalize_install(void)
}
}
printf("Deleting %s...\n", CIFINISH_PATH);
unlink(CIFINISH_PATH);
exit:
free(entries);
free(installed_ticket_ids);
free(installed_title_ids);
return;
}
int main(int argc, char* argv[])
@@ -346,7 +275,7 @@ int main(int argc, char* argv[])
gfxInitDefault();
consoleInit(GFX_TOP, NULL);
printf("custom-install-finalize v1.6\n");
printf("custom-install-finalize v1.4\n");
finalize_install();
// print this at the end in case it gets pushed off the screen

167
flake.lock generated
View File

@@ -1,167 +0,0 @@
{
"nodes": {
"devkitNix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"finalize",
"nixpkgs"
]
},
"locked": {
"lastModified": 1766539742,
"narHash": "sha256-F6OeM2LrLo2n+Xg5XU4udQR/vuWWrDMKxXRzNXE2ClQ=",
"owner": "bandithedoge",
"repo": "devkitNix",
"rev": "c97f9880737716085e78009cba6bf85ad104628b",
"type": "github"
},
"original": {
"owner": "bandithedoge",
"repo": "devkitNix",
"type": "github"
}
},
"finalize": {
"inputs": {
"devkitNix": "devkitNix",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1,
"narHash": "sha256-BZgu7+/RV9Gy1xo/icz5kd2fKCa3Zow+Zz6MJWzpgMM=",
"path": "finalize",
"type": "path"
},
"original": {
"path": "finalize",
"type": "path"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"hax-nur": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1768151313,
"narHash": "sha256-qcMLsdACTlFHltziBAsS1r09cVZyp5fUR16//mIhLIs=",
"owner": "ihaveamac",
"repo": "nur-packages",
"rev": "8ebcd637fd5cd8e673c8e01ed408bf206f9d4f9b",
"type": "github"
},
"original": {
"owner": "ihaveamac",
"ref": "master",
"repo": "nur-packages",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1768032153,
"narHash": "sha256-6kD1MdY9fsE6FgSwdnx29hdH2UcBKs3/+JJleMShuJg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3146c6aa9995e7351a398e17470e15305e6e18ff",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pyctr": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1763515957,
"narHash": "sha256-S0qzooGQN5tkbIVgijVZ9umvBC1dYbdPN97tks5SbwE=",
"owner": "ihaveamac",
"repo": "pyctr",
"rev": "eb8d4d06ce7339727d3f72b40f45ec3260336058",
"type": "github"
},
"original": {
"owner": "ihaveamac",
"ref": "master",
"repo": "pyctr",
"type": "github"
}
},
"root": {
"inputs": {
"finalize": "finalize",
"hax-nur": "hax-nur",
"nixpkgs": "nixpkgs",
"pyctr": "pyctr"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"hax-nur",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768031762,
"narHash": "sha256-b2gJDJfi+TbA7Hu2sKip+1mWqya0GJaWrrXQjpbOVTU=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "0c445aa21b01fd1d4bb58927f7b268568af87b20",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,80 +0,0 @@
{
description = "custominstall";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
pyctr.url = "github:ihaveamac/pyctr/master";
pyctr.inputs.nixpkgs.follows = "nixpkgs";
hax-nur.url = "github:ihaveamac/nur-packages/master";
hax-nur.inputs.nixpkgs.follows = "nixpkgs";
finalize.url = "path:finalize";
finalize.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
inputs@{
self,
nixpkgs,
pyctr,
hax-nur,
finalize,
}:
let
systems = [
"x86_64-linux"
"i686-linux"
"x86_64-darwin"
"aarch64-darwin"
"aarch64-linux"
"armv6l-linux"
"armv7l-linux"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
in
{
legacyPackages = forAllSystems (
system:
(import ./default.nix {
pkgs = import nixpkgs { inherit system; };
pyctr = pyctr.packages.${system}.pyctr;
save3ds = hax-nur.packages.${system}.save3ds;
})
// {
default = self.legacyPackages.${system}.custominstall;
}
);
packages = forAllSystems (
system: nixpkgs.lib.filterAttrs (_: v: nixpkgs.lib.isDerivation v) self.legacyPackages.${system}
);
apps = forAllSystems (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
gui = {
type = "app";
program = "${self.packages.${system}.custominstall}/bin/custominstall-gui";
};
}
// (
if system == "x86_64-linux" then
# this only works on x86_64-linux due to devkitNix only working there
{
update-finalize = {
type = "app";
program =
(pkgs.writeShellScript "update-finalize" ''
set -x
finalize=${inputs.finalize.packages.${system}.custom-install-finalize}/custom-install-finalize.3dsx
cp --no-preserve=mode,ownership,timestamps $finalize custominstall/custom-install-finalize.3dsx
'').outPath;
};
}
else
{ }
)
);
};
}

438
gui.py Normal file
View File

@@ -0,0 +1,438 @@
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019-2020 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
# A gui for custom-install.py
# By LyfeOnEdge
import os, sys, platform, subprocess, threading
import tkinter as tk
import tkinter.filedialog as tkfiledialog
import style
class themedFrame(tk.Frame):
def __init__(self, frame, **kw):
tk.Frame.__init__(self, frame, **kw)
if not (kw.get("background") or kw.get("bg")):
self.configure(bg=style.BACKGROUND_COLOR)
if not kw.get("borderwidth"):
self.configure(borderwidth=0)
if not kw.get("highlightthickness"):
self.configure(highlightthickness=0)
class Button(tk.Label):
"""Cross-platform button"""
def __init__(self, frame, callback, **kw):
self.callback = callback
self.background = "#aaaaaa"
self.selected = False
tk.Label.__init__(self, frame, **kw)
self.configure(anchor="center")
self.configure(background=self.background)
self.configure(highlightthickness=1)
if not "font" in kw.keys():
self.configure(font=style.BUTTON_FONT)
self.configure(highlightbackground="#999999")
self.bind('<Button-1>', self.on_click)
# Use callback when our makeshift "button" clicked
def on_click(self, event=None):
self.configure(background="#dddddd")
if not self.selected:
self.after(100, self.on_click_color_change)
if self.callback:
self.callback()
# Function to set the button's image
def setimage(self, image):
self.configure(image=image)
# Function to set the button's text
def settext(self, text):
self.configure(text=text)
def deselect(self):
self.selected = False
self.configure(background=self.background)
def on_click_color_change(self):
if not self.selected:
self.configure(background=self.background)
class PathEntry(tk.Entry):
"""Tkinter entry widget with a button to set the file path using tkinter's file dialog"""
def __init__(self, frame, dir=False, filetypes=None, *args, **kw):
self.dir = dir
self.filetypes = filetypes
container = themedFrame(frame)
self.button = Button(container, self.set_path, text="...")
self.button.place(relheight=1, relx=1, x=- style.BUTTONSIZE, width=style.BUTTONSIZE)
tk.Entry.__init__(self, container, *args, **kw)
self.text_var = tk.StringVar()
self.configure(textvariable=self.text_var)
self.configure(background=style.ENTRY_COLOR)
self.configure(foreground=style.ENTRY_FOREGROUND)
self.configure(borderwidth=0)
self.configure(highlightthickness=2)
self.configure(highlightbackground=style.BUTTON_COLOR)
super().place(relwidth=1, relheight=1, width=- style.BUTTONSIZE)
self.container = container
def clear(self):
self.text_var.set("")
def set(self, string):
self.text_var.set(string)
def get_var(self):
return self.text_var
def get(self):
return self.text_var.get()
def place(self, **kw):
self.container.place(**kw)
def set_path(self):
if not self.dir:
self.set(tkfiledialog.askopenfilename(filetypes=self.filetypes))
else:
self.set(tkfiledialog.askdirectory())
class LabeledPathEntry(PathEntry):
"""Gives the PathEntry class a label"""
def __init__(self, frame, text, *args, **kw):
self.xtainer = themedFrame(frame)
label = tk.Label(self.xtainer, text=text, background=style.BACKGROUND_COLOR, foreground=style.LABEL_COLOR)
label.place(width=label.winfo_reqwidth(), relheight=1)
PathEntry.__init__(self, self.xtainer, *args, **kw)
PathEntry.place(self, relwidth=1, relheight=1, width=- (label.winfo_reqwidth() + 5),
x=label.winfo_reqwidth() + 5)
def place(self, **kw):
self.xtainer.place(**kw)
class AutoScroll(object):
def __init__(self, master):
try:
vsb = tk.Scrollbar(master, orient='vertical', command=self.yview)
except:
pass
hsb = tk.Scrollbar(master, orient='horizontal', command=self.xview)
try:
self.configure(yscrollcommand=self._autoscroll(vsb))
except:
pass
self.configure(xscrollcommand=self._autoscroll(hsb))
self.grid(column=0, row=0, sticky='nsew')
try:
vsb.grid(column=1, row=0, sticky='ns')
except:
pass
hsb.grid(column=0, row=1, sticky='ew')
master.grid_columnconfigure(0, weight=1)
master.grid_rowconfigure(0, weight=1)
methods = tk.Pack.__dict__.keys() | tk.Grid.__dict__.keys() \
| tk.Place.__dict__.keys()
for m in methods:
if m[0] != '_' and m not in ('config', 'configure'):
setattr(self, m, getattr(master, m))
@staticmethod
def _autoscroll(sbar):
'''Hide and show scrollbar as needed.'''
def wrapped(first, last):
first, last = float(first), float(last)
if first <= 0 and last >= 1:
sbar.grid_remove()
else:
sbar.grid()
sbar.set(first, last)
return wrapped
def __str__(self):
return str(self.master)
def _create_container(func):
'''Creates a tk Frame with a given master, and use this new frame to
place the scrollbars and the widget.'''
def wrapped(cls, master, **kw):
container = themedFrame(master)
container.bind('<Enter>', lambda e: _bound_to_mousewheel(e, container))
container.bind(
'<Leave>', lambda e: _unbound_to_mousewheel(e, container))
return func(cls, container, **kw)
return wrapped
def _bound_to_mousewheel(event, widget):
child = widget.winfo_children()[0]
if platform.system() == 'Windows' or platform.system() == 'Darwin':
child.bind_all('<MouseWheel>', lambda e: _on_mousewheel(e, child))
child.bind_all('<Shift-MouseWheel>',
lambda e: _on_shiftmouse(e, child))
else:
child.bind_all('<Button-4>', lambda e: _on_mousewheel(e, child))
child.bind_all('<Button-5>', lambda e: _on_mousewheel(e, child))
child.bind_all('<Shift-Button-4>', lambda e: _on_shiftmouse(e, child))
child.bind_all('<Shift-Button-5>', lambda e: _on_shiftmouse(e, child))
def _unbound_to_mousewheel(event, widget):
if platform.system() == 'Windows' or platform.system() == 'Darwin':
widget.unbind_all('<MouseWheel>')
widget.unbind_all('<Shift-MouseWheel>')
else:
widget.unbind_all('<Button-4>')
widget.unbind_all('<Button-5>')
widget.unbind_all('<Shift-Button-4>')
widget.unbind_all('<Shift-Button-5>')
def _on_mousewheel(event, widget):
if platform.system() == 'Windows':
widget.yview_scroll(-1 * int(event.delta / 120), 'units')
elif platform.system() == 'Darwin':
widget.yview_scroll(-1 * int(event.delta), 'units')
else:
if event.num == 4:
widget.yview_scroll(-1, 'units')
elif event.num == 5:
widget.yview_scroll(1, 'units')
class ScrolledText(AutoScroll, tk.Text):
@_create_container
def __init__(self, master, **kw):
tk.Text.__init__(self, master, **kw)
AutoScroll.__init__(self, master)
# from https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter
class CreateToolTip(object):
'''
create a tooltip for a given widget
'''
def __init__(self, widget, text='widget info'):
self.widget = widget
self.text = text
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.close)
def enter(self, event=None):
x = y = 0
x, y, cx, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx()
y += self.widget.winfo_rooty() + 20
# creates a toplevel window
self.tw = tk.Toplevel(self.widget)
# Leaves only the label and removes the app window
self.tw.wm_overrideredirect(True)
self.tw.wm_geometry("+%d+%d" % (x, y))
label = tk.Label(self.tw, text=self.text, justify='left',
background='gray', foreground=style.LABEL_COLOR,
relief='solid', borderwidth=2,
font=("times", "12", "normal"),
wraplength=self.widget.winfo_width())
label.pack(ipadx=1)
def close(self, event=None):
if self.tw:
self.tw.destroy()
class threader_object:
"""an object to be declared outside of tk root so
things can be called asyncronously (you cannot start
a new thread from within a tkinter callback so you
must call it from an object that exists outside)"""
def do_async(self, func, arglist=[]):
threading.Thread(target=func, args=arglist).start()
class gui(tk.Tk):
def __init__(self, threader):
self.threader = threader
tk.Tk.__init__(self)
self.minsize(300, 400)
self.title("custom-install gui")
self.f = themedFrame(self)
self.f.place(relwidth=1, relheight=1)
outer_frame = themedFrame(self.f)
outer_frame.place(relwidth=1, relheight=1, x=+ style.STANDARD_OFFSET, width=- 2 * style.STANDARD_OFFSET,
y=+ style.STANDARD_OFFSET, height=- 2 * style.STANDARD_OFFSET)
self.sd_box = LabeledPathEntry(outer_frame, "Path to SD root -", dir=True)
self.sd_box.place(relwidth=1, height=20, x=0)
CreateToolTip(self.sd_box.xtainer, "Select the root of the sd card you wish to install the cias to.")
self.sed_box = LabeledPathEntry(outer_frame, "Path to movable.sed file -", filetypes=[('sed file', '*.sed')])
self.sed_box.place(relwidth=1, height=20, x=0, y=30)
CreateToolTip(self.sed_box.xtainer, "Select movable.sed file, this can be dumped from a 3ds")
self.boot9_box = LabeledPathEntry(outer_frame, "Path to boot9 file -", filetypes=[('boot9 file', '*.bin')])
self.boot9_box.place(relwidth=1, height=20, x=0, y=60)
CreateToolTip(self.boot9_box.xtainer, "Select the path to boot9.bin, this can be dumped from a 3ds")
self.seeddb_box = LabeledPathEntry(outer_frame, "Path to seeddb file -", filetypes=[('seeddb file', '*.bin')])
self.seeddb_box.place(relwidth=1, height=20, x=0, y=90)
CreateToolTip(self.seeddb_box.xtainer, "Select the path to seeddb.bin, this can retrieved from online")
# -------------------------------------------------
cia_container = themedFrame(outer_frame, borderwidth=0, highlightthickness=0)
cia_container.place(y=120, relwidth=1, height=190)
cia_label = tk.Label(cia_container, text="cia paths - ", foreground=style.LABEL_COLOR,
background=style.BACKGROUND_COLOR)
cia_label.place(relwidth=1, height=20)
self.cia_box = tk.Listbox(cia_container, highlightthickness=0, bg=style.ENTRY_COLOR,
foreground=style.ENTRY_FOREGROUND)
self.cia_box.place(relwidth=1, height=70, y=20)
CreateToolTip(cia_label,
"Select the cias you wish to install to the sd card. The `add folder` button will add all cias in the selected folder, but will not check subdirs. The `remove cia` button will remove the currently selected file from the listbox.")
add_cia_button = Button(cia_container, self.add_cia, text="add cia", font=style.monospace)
add_cia_button.place(relx=0, relwidth=0.333, height=20, y=95, width=- 6)
add_cia_folder_button = Button(cia_container, self.add_cia_folder, text="add folder", font=style.monospace)
add_cia_folder_button.place(relx=0.333, relwidth=0.333, height=20, y=95, x=+ 3, width=- 6)
remove_cia_button = Button(cia_container, self.remove_cia, text="remove cia", font=style.monospace)
remove_cia_button.place(relx=0.666, relwidth=0.333, height=20, y=95, x=+ 6, width=- 6)
# -------------------------------------------------
self.skip_contents = tk.IntVar()
skip_contents_checkbutton = tk.Checkbutton(outer_frame, text="Skip contents? (only add title info)",
variable=self.skip_contents, background=style.BACKGROUND_COLOR,
foreground=style.LABEL_COLOR, borderwidth=0, highlightthickness=0)
skip_contents_checkbutton.place(relwidth=1, y=239, height=20)
console_label = tk.Label(outer_frame, text="Console:", background="black", foreground="white",
font=style.boldmonospace, borderwidth=0, highlightthickness=0)
console_label.place(relwidth=1, height=20, y=260)
self.console = ScrolledText(outer_frame, background="black", foreground="white", highlightthickness=0)
self.console.place(relwidth=1, relheight=1, y=280, height=- 272)
run_button = Button(outer_frame, self.run, text="run", font=style.boldmonospace)
run_button.place(relwidth=1, rely=1, y=- 22)
def run(self):
args_extra = []
self.output_to_console("-----------------------\nStarting...\n")
boot9 = self.boot9_box.get()
if not boot9:
self.output_to_console(
"Warning - boot9 not selected, if it's not set externally you may run into problems.\n")
else:
args_extra.extend(['-b', boot9])
sed = self.sed_box.get()
if not sed:
self.output_to_console("Failed to run - No movable.sed selected.\n")
return
args_extra.extend(['-m', sed])
sd = self.sd_box.get().strip()
if not sd:
self.output_to_console("Failed to run - SD path not selected.\n")
return
args_extra.extend(['--sd', sd])
seed = self.seeddb_box.get().strip()
if not seed:
self.output_to_console("Optional Seeddb not given - Certain CIAs May Require This!\n")
args_extra.extend(['--seeddb', seed])
cias = []
for i in range(0, self.cia_box.size()):
cias.append(self.cia_box.get(i).strip())
for cia in cias:
args_extra.append(cia)
if self.skip_contents.get():
args_extra.append('--skip-contents')
print(f"Running custom-install.py with args {args_extra}\n")
self.threader.do_async(execute_script, [args_extra, self.output_to_console])
def output_to_console(self, outstring):
self.console.insert('end', outstring)
self.console.see('end')
def add_cia(self):
cia_to_add = tkfiledialog.askopenfilename(filetypes=[('cia file', '*.cia')])
if cia_to_add:
self.cia_box.insert('end', cia_to_add)
def add_cia_folder(self):
cia_dir_to_add = tkfiledialog.askdirectory()
if cia_dir_to_add:
cias_to_add = [f for f in os.listdir(cia_dir_to_add) if
(os.path.isfile(os.path.join(cia_dir_to_add, f)) and f.endswith(".cia"))]
if cias_to_add:
for cia_to_add in cias_to_add:
self.cia_box.insert('end', os.path.join(cia_dir_to_add, cia_to_add))
def remove_cia(self):
index = self.cia_box.curselection()
if index:
self.cia_box.delete(index)
if self.cia_box.size():
self.cia_box.select_clear(0, 'end')
if self.cia_box.size() > 1:
try:
self.cia_box.select_set(index)
except:
pass
else:
self.cia_box.select_set(0)
def execute_script(args_extra, printer):
"""Wrapper function to pipe install script output to a printer"""
args = [sys.executable, '-u', os.path.join(os.path.dirname(__file__), "custominstall.py")]
try:
args.extend(args_extra)
p = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
)
with p.stdout:
for line in iter(p.stdout.readline, b''):
printer(line)
p.wait()
except Exception as e:
printer(f"Error while executing script with args - {args} | Exception - {e}\n")
t = threader_object()
window = gui(t)
window.mainloop()

View File

@@ -1,74 +0,0 @@
{
lib,
pkgs,
python,
callPackage,
buildPythonApplication,
fetchPypi,
pyctr,
pycryptodomex,
pypng,
tkinter,
setuptools,
events,
stdenv,
save3ds,
withGUI ? true,
}:
let
save3ds_no_fuse = save3ds.override { withFUSE = false; };
in
buildPythonApplication rec {
pname = "custominstall";
version = "2.1";
pyproject = true;
src = builtins.path {
path = ./.;
name = "custominstall";
filter =
path: type:
!(builtins.elem (baseNameOf path) [
"build"
"dist"
"localtest"
"__pycache__"
"v"
".git"
"_build"
"custominstall.egg-info"
]);
};
doCheck = false;
build-system = [ setuptools ];
propagatedBuildInputs =
[
pyctr
pycryptodomex
setuptools
events
]
++ lib.optionals (withGUI) [
tkinter
];
makeWrapperArgs = [ "--set CUSTOM_INSTALL_SAVE3DS_PATH ${save3ds_no_fuse}/bin/save3ds_fuse" ];
preFixup = ''
rm -r $out/lib/${python.libPrefix}/site-packages/custominstall/bin
${lib.optionalString (!withGUI) "rm $out/bin/custominstall-gui"}
'';
meta = with lib; {
description = "Installs a title directly to an SD card for the Nintendo 3DS";
homepage = "https://github.com/ihaveamac/custom-install";
license = licenses.mit;
platforms = platforms.unix;
mainProgram = "custominstall";
};
}

View File

@@ -1,48 +0,0 @@
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "custominstall"
description = "Installs a title directly to an SD card for the Nintendo 3DS"
authors = [
{ name = "Ian Burgwin", email = "ian@ianburgwin.net" },
]
readme = "README.md"
license = {text = "MIT"}
dynamic = ["version"]
requires-python = ">= 3.8"
classifiers = [
"Topic :: Utilities",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"pyctr>=0.7.6,<0.9",
"setuptools>=61.0.0",
"events>=0.4",
"comtypes>=1.4.12; os_name == 'nt'",
]
[project.gui-scripts]
custominstall-gui = "custominstall.gui:main"
[project.scripts]
custominstall = "custominstall.__main__:main"
[tool.setuptools.dynamic]
version = {attr = "custominstall.__version__"}
[tool.setuptools.packages]
find = {namespaces = false}
# is it even possible to make these OS-specific with pyproject.toml?
[tool.setuptools.package-data]
custominstall = ["bin/darwin/save3ds_fuse", "bin/win32/save3ds_fuse.exe", "TaskbarLib.tlb", "title.db.gz", "custom-install-finalize.3dsx"]

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
pycryptodomex<=3.9.4
events==0.3
pyctr==0.4.1

18
style.py Normal file
View File

@@ -0,0 +1,18 @@
STANDARD_OFFSET = 10 #Offset to place everything
BUTTONSIZE = 30
monospace = ("Monospace",10)
boldmonospace = ("Monospace",10,"bold")
BUTTON_FONT = monospace
BACKGROUND_COLOR = "#20232a"
BUTTON_COLOR = "#aaaaaa"
ENTRY_COLOR = "#373940"
ENTRY_FOREGROUND = "black"
LABEL_COLOR = "#61dafb"