88 Commits

Author SHA1 Message Date
ihaveahax
c61b2bf168 update CONTRIBUTING.md 2026-01-07 22:38:29 -06:00
ihaveahax
c276dc82bc CONTRIBUTING.md 2025-09-30 13:50:01 -05:00
ihaveahax
e5725876e2 Merge pull request #76 from samuelplaca/compilation-flag-fix
Update cflags in Makefile of finalize
2024-04-17 19:10:12 -05:00
Samuel Plaça
2d78e0bc32 update deprecated cflags out of finalize Makefile
3ds.h currently prints a Warning when it detects the usage of compilation
flags -DARM11 -D_3DS, stating that -D__3DS__ should be used in their stead.
See 48967dc417
2024-04-17 13:47:28 -03:00
Ian Burgwin
9ab8236a78 add shebang to custominstall and ci-gui 2022-11-25 00:14:28 -08:00
ihaveahax
1be4221186 Merge pull request #65 from TaiAurori/safe-install
Correct README's file path for compiled save3ds_fuse
2022-05-17 02:06:21 -07:00
TaiAurori
6a770c40c0 README: correct save3ds_fuse compiled binary path 2022-05-16 10:42:01 -04:00
Ian Burgwin
da1a7393b0 README: point to correct branch 2022-04-25 13:25:35 -07:00
Ian Burgwin
6a5ca17a33 custominstall: fix --seeddb by loading it earlier 2022-04-02 11:28:54 -07:00
Ian Burgwin
8f90387a80 bump max allowed pyctr version 2022-04-02 11:24:00 -07:00
Ian Burgwin
83c6d07194 codesign save3ds_fuse mac binary 2022-02-04 17:31:29 -08:00
Ian Burgwin
a1b3cb059e update cargo command for linux 2022-01-30 20:27:30 -08:00
Ian Burgwin
68f6bfbb2e update save3ds_fuse binaries 2022-01-30 20:23:21 -08:00
Ian Burgwin
d12684d8bf version 2.1 2021-09-12 09:02:27 -07:00
Ian Burgwin
54ae8a504c check for id0 (closes #49) 2021-09-12 08:50:59 -07:00
Ian Burgwin
d97e11e4ec use setup script to build cx-freeze standalone 2021-07-26 11:31:34 -07:00
Ian Burgwin
c3448c388e ci-gui: use relative path when loading tcl (to fix non-latin characters in the absolute path), prevent taskbar lib errors from causing an exit 2021-07-13 07:08:36 -07:00
Ian Burgwin
4d7be0812e version 2.1b4 & finalize 1.6 2021-07-08 07:43:13 -07:00
Ian Burgwin
8c60eecec5 finalize: remove useless frees 2021-07-08 07:35:26 -07:00
Ian Burgwin
653569093d finalize: ensure *entries is initialized to NULL 2021-07-08 07:34:07 -07:00
Ian Burgwin
adccac9ee7 requirements-win32: bump comtypes to 1.1.10 2021-07-08 07:12:44 -07:00
Ian Burgwin
8629cbee8e requirements: bump pyctr to >=0.4,<0.6 2021-07-08 07:09:25 -07:00
Ian Burgwin
740844e57a Merge branch 'safe-install' of github.com:ihaveamac/custom-install into safe-install 2021-05-06 08:39:53 -07:00
Ian Burgwin
d847043045 requirements: relax pyctr requirement: >=0.4,<0.5 2021-05-06 08:39:33 -07:00
Ian Burgwin
217a508bf3 Merge pull request #47 from TimmSkiller/safe-install
Added checks for existing Ticket and entry in Title Database
2021-04-14 14:56:40 -07:00
TimmSkiller
42ec2d760a added missing break statement 2021-04-15 00:19:01 +03:00
TimmSkiller
938d8fd6aa Added check for existing ticket and title entry before finalizing 2021-04-15 00:16:15 +03:00
Ian Burgwin
ac0be9d61d custominstall: force utf-8 encoding for the output of save3ds_fuse (fixes #41 hopefully) 2021-03-22 06:55:40 -07:00
Ian Burgwin
d231e9c043 Merge pull request #43 from Jisxu/safe-install
auto input filename include  boot9.bin,seeddb.bin,movable.sed
2021-03-22 01:14:59 -07:00
Justin
9c3c4ce5f9 log filename 2021-03-22 15:59:54 +08:00
Justin
6a324b9388 add click callback function 2021-03-22 15:49:31 +08:00
Justin
647e56cf05 auto input filename include boot9.bin,seeddb.bin,movable.sed when they in <sdcard>:/gm9/out 2021-03-22 14:18:12 +08:00
Ian Burgwin
643e4e4976 custominstall: show 300 title warning (fixes #42) 2021-03-12 18:26:04 -08:00
Ian Burgwin
09ed0093df custominstall: post-release version bump 2021-03-12 18:24:10 -08:00
Ian Burgwin
9b7346c919 version 2.1b3 2021-03-08 18:16:58 -08:00
Ian Burgwin
38f5e2b0e6 custominstall: remove seek workaround for CDN contents (fixed in pyctr 0.4.6) 2021-03-08 18:08:32 -08:00
Ian Burgwin
f48e177604 bump pyctr -> 0.4.6, comtypes -> 1.1.8 2021-03-08 18:08:06 -08:00
Ian Burgwin
4ca2c59b5a custominstall: fix cdn content install 2021-02-24 15:19:48 -08:00
Ian Burgwin
7a68b23365 custominstall: create title.db and import.db if missing 2021-02-24 15:07:59 -08:00
Ian Burgwin
1dec5175ea add title.db.gz, add to standalone build 2021-02-24 14:17:17 -08:00
Ian Burgwin
4d223ed931 show warning if 300 titles are detected 2021-02-13 23:08:37 -08:00
Ian Burgwin
46a0d985a7 vesion 2.1b2 2021-02-12 20:58:13 -08:00
Ian Burgwin
37112682a0 show mid-install status per-title 2021-02-09 21:21:34 -08:00
Ian Burgwin
9c777adf26 ci-gui: ensure boot9 is loaded before allowing titles to be added, show better error message for missing seeddb, load seeddb before starting and any time a new one is selected 2021-02-09 20:24:34 -08:00
Ian Burgwin
b3eae08f27 custominstall: make boot9 and seeddb optional for CustomInstall.__init__ (for gui changes) 2021-02-09 20:22:24 -08:00
Ian Burgwin
5f49493dfb vesion 2.1b1 2021-02-09 15:41:02 -08:00
Ian Burgwin
fbc553f5c7 README: update description 2021-02-09 15:23:34 -08:00
Ian Burgwin
68d9026524 LICENSE: update copyright year 2021-02-09 15:16:44 -08:00
Ian Burgwin
46ac9cd809 ci-gui: show OK button in TitleFailReadResults and InstallResults windows 2021-02-09 14:49:28 -08:00
Ian Burgwin
40a8d2d684 ci-gui: prevent adding a file twice 2021-02-09 14:48:38 -08:00
Ian Burgwin
4ec5bce712 ci-gui: sort treeview by title name 2021-02-09 14:48:07 -08:00
Ian Burgwin
d27e181c40 ci-gui: show error reason for cdn titles 2021-02-09 00:01:59 -08:00
Ian Burgwin
8ed6ca54cc ci-gui: replace listview with treeview, load titles before adding to list and show reasons for failure, verify cifinish.bin after choosing SD card 2021-02-08 23:45:26 -08:00
Ian Burgwin
0dcaaedda7 custominstall: separate reader creation to get_reader, check against more errors 2021-02-08 23:43:40 -08:00
Ian Burgwin
f904049c06 custominstall: stop if cifinish.bin is corrupt 2021-02-08 23:43:13 -08:00
Ian Burgwin
7b121f5212 ci-gui: make InstallResults transient, adjust listbox frame height based on item count 2021-02-08 21:27:55 -08:00
Ian Burgwin
1b2b0d06db custominstall: block DSiWare from installing 2021-02-08 21:23:57 -08:00
Ian Burgwin
6623ffb439 ci-gui: show version in window title and console 2021-02-08 21:23:36 -08:00
Ian Burgwin
4733997132 custominstall: add CI_VERSION constant, print version at start 2021-02-08 21:22:06 -08:00
Ian Burgwin
9fc509489f ci-gui: enable buttons if an error occurs before installation 2021-02-08 20:55:51 -08:00
Ian Burgwin
2636c5923c custominstall: remove "Manually" from ArgumentParser description 2021-02-08 20:41:45 -08:00
Ian Burgwin
cfa46abea5 requirements: remove pycryptodome requirement (not used directly, only through pyctr) 2021-02-08 20:26:02 -08:00
Ian Burgwin
d91c567fc5 ci-gui: show (incomplete) more detailed install results window 2021-02-08 14:42:06 -08:00
Ian Burgwin
188be9b9d6 ci-gui: check for free space before installing 2021-02-08 14:41:30 -08:00
Ian Burgwin
616f9031b2 custominstall: check for free space before installing, move title size calculation to separate function 2021-02-08 14:39:50 -08:00
Ian Burgwin
b8bd9371dd finalize: delete cifinish if successful 2021-02-08 12:30:10 -08:00
Ian Burgwin
b69dfb0a46 finalize: close cifinish if successful 2021-02-08 12:29:35 -08:00
Ian Burgwin
e0573809bb custominstall: always use randomized temp install dir 2020-12-02 22:42:05 -08:00
Ian Burgwin
46ce6ab76c custominstall: verify content hashes 2020-12-02 22:29:53 -08:00
Ian Burgwin
fcf47e0564 ci-gui: prioritize failed message over installed 2020-12-02 22:29:20 -08:00
Ian Burgwin
a529ecf760 ci-gui: show list of installed and failed titles 2020-12-02 22:13:17 -08:00
Ian Burgwin
793d923240 custominstall: keep track of successful and failed installs 2020-12-02 22:13:01 -08:00
Ian Burgwin
918111dedf custominstall: remove debug line 2020-12-02 21:50:19 -08:00
Ian Burgwin
47f22313b4 custominstall: import into title.db after every title install 2020-12-02 21:49:38 -08:00
Ian Burgwin
5d60715d94 custominstall: write to temporary directory first, then move into place 2020-12-02 21:12:06 -08:00
Ian Burgwin
aad1accca3 custominstall: extract title.db before installing (untested)
This should help prevent installing a bunch of titles, only to find title.db doesn't exist.
2020-12-02 20:26:53 -08:00
Ian Burgwin
945b0a377b custominstall: don't fail if cifinish.bin is corrupted 2020-12-02 20:17:50 -08:00
Ian Burgwin
707b852db3 requirements: update versions 2020-12-02 20:08:52 -08:00
Ian Burgwin
794eb8750f custominstall: fix incorrect return value causing TypeError in ci-gui 2020-10-14 17:37:05 -07:00
Ian Burgwin
b34bba2543 ci-gui: print command line args if save3ds_fuse fails 2020-09-03 19:45:11 -07:00
Ian Burgwin
40cfd955cc custominstall: set platform to win32 if it's msys (close #2) 2020-07-28 09:32:18 -07:00
Ian Burgwin
bbcfb6fef1 ci-gui: support adding CDN folders 2020-07-28 06:33:18 -07:00
Ian Burgwin
1e3e15c969 custominstall: support CDN contents (close #27) 2020-07-28 06:21:22 -07:00
Ian Burgwin
48f92579ce custominstall: use bytes(cia.tmd) instead of reading tmd file directly 2020-07-28 06:15:27 -07:00
Ian Burgwin
06f70e37dc requirements: bump pyctr to 0.4.3 2020-07-28 06:01:13 -07:00
Ian Burgwin
44787ebc87 requirements: bump pyctr to 0.4.2 2020-07-28 04:51:03 -07:00
Ian Burgwin
399bb97238 custominstall: read all CIAs before installing
Prevents the issue of one having a corrupt header or something causing an issue in the middle of writing.
2020-07-28 02:33:08 -07:00
Ian Burgwin
6da2ed3343 ci-gui: use abspath when getting file parent
This should the initial directory setting for file dialogs work more reliably.
2020-07-28 01:29:59 -07:00
17 changed files with 1229 additions and 533 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,5 @@
.vscode/
bin/linux/save3ds_fuse
**/finalize
cstins/
testing-class.py
@@ -18,3 +17,6 @@ venv/
=======
*.pyc
/build/
/dist/
/custom-install-finalize.3dsx

9
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,9 @@
## 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,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2019 Ian Burgwin
Copyright (c) 2019-2021 Ian Burgwin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,7 +1,7 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)]() ![Releases](https://img.shields.io/github/downloads/ihaveamac/custom-install/total.svg)
# custom-install
Experimental script to automate the process of a manual title install for Nintendo 3DS. Originally created late June 2019.
Installs a title directly to an SD card for the Nintendo 3DS. Originally created late June 2019.
## Summary
@@ -15,7 +15,7 @@ Experimental script to automate the process of a manual title install for Ninten
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 repo ([zip link](https://github.com/ihaveamac/custom-install/archive/module-newer-gui.zip) or `git clone`)
2. Download the repo ([zip link](https://github.com/ihaveamac/custom-install/archive/safe-install.zip) or `git clone`)
3. Install the packages:
* Windows: Double-click `windows-install-dependencies.py`
* Alternate manual method: `py -3 -m pip install --user -r requirements-win32.txt`
@@ -24,7 +24,7 @@ Note for Windows users: Enabling "Add Python 3.X to PATH" is **NOT** required! P
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 `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`.
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 --release --no-default-features`. The compiled binary is located in `target/release/save3ds_fuse`, copy it to `bin/linux`.
movable.sed is required and can be provided with `-m` or `--movable`.

390
bin/Cargo.lock generated
View File

@@ -2,39 +2,49 @@
# It is not intended for manual editing.
[[package]]
name = "aes"
version = "0.3.2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
"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)",
]
[[package]]
name = "aes-soft"
version = "0.3.3"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
"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)",
"opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "aesni"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"block-cipher 0.7.1 (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.17"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"const-random 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"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)",
]
[[package]]
@@ -42,23 +52,28 @@ 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.7.3"
version = "0.8.0"
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.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.12.3 (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)",
]
[[package]]
name = "block-cipher-trait"
version = "0.6.2"
name = "block-cipher"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -80,7 +95,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.13.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -95,73 +110,75 @@ dependencies = [
[[package]]
name = "byteorder"
version = "1.3.2"
version = "1.3.4"
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 = "cmac"
version = "0.2.0"
name = "chrono"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
"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"
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)",
]
[[package]]
name = "const-random"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
"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)",
]
[[package]]
name = "const-random-macro"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
"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)",
]
[[package]]
name = "crypto-mac"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
"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)",
]
[[package]]
name = "dbl"
version = "0.2.1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "digest"
version = "0.8.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -174,27 +191,20 @@ name = "fuse"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (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.42 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "generic-array"
version = "0.12.3"
version = "0.14.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]]
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)",
"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)",
]
[[package]]
@@ -202,17 +212,17 @@ name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "getrandom"
version = "0.1.13"
version = "0.1.14"
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.65 (registry+https://github.com/rust-lang/crates.io-index)",
"wasi 0.7.0 (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)",
]
[[package]]
@@ -220,25 +230,31 @@ name = "hashbrown"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ahash 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)",
"ahash 0.2.18 (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.65"
version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libsave3ds"
version = "0.1.0"
dependencies = [
"aes 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"aes 0.4.0 (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.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)",
"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)",
]
[[package]]
@@ -259,12 +275,34 @@ dependencies = [
[[package]]
name = "lru"
version = "0.4.0"
version = "0.5.1"
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"
@@ -277,18 +315,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ppv-lite86"
version = "0.2.6"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "proc-macro-hack"
version = "0.5.11"
version = "0.5.16"
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"
@@ -298,14 +331,6 @@ 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"
@@ -314,32 +339,24 @@ dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "1.0.2"
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)",
]
[[package]]
name = "rand"
version = "0.7.2"
version = "0.7.3"
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)",
"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)",
"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.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -348,7 +365,7 @@ name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
"getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -364,31 +381,52 @@ 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.65 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"libsave3ds 0.1.0",
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
"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)",
]
[[package]]
name = "sha2"
version = "0.8.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
"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)",
"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 = "1.0.0"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -402,13 +440,22 @@ dependencies = [
]
[[package]]
name = "syn"
version = "1.0.8"
name = "termcolor"
version = "1.1.0"
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)",
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"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)",
]
[[package]]
@@ -417,23 +464,31 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "time"
version = "0.1.42"
name = "thread_local"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.71 (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.11.2"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-width"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -442,13 +497,26 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-xid"
version = "0.2.0"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "wasi"
version = "0.7.0"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -465,67 +533,85 @@ 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.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 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 autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"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 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-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.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 byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
"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 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 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.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 generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980"
"checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407"
"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
"checksum hashbrown 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8e6073d0ca812575946eb5f35ff68dbe519907b25c42530389ff946dc84c6ead"
"checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8"
"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 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.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26b0dca4ac5b5083c5169ab12205e6473df1c7659940e4978b94f363c6b54b22"
"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 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.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 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 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 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 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 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 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 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 syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
"checksum syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "661641ea2aa15845cddeb97dad000d22070bb5c1fb456b96c1cba883ec691e92"
"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 thread-scoped 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bcbb6aa301e5d3b0b5ef639c9a9c7e2f1c944f177b460c04dc24c69b1fa2bd99"
"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 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 unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
"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 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 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"

View File

@@ -1,8 +1,11 @@
save3ds_fuse for win32 and darwin built with commit 25f0b5ec2600ddff8f5d2acf7c89ac1c4e743972
save3ds_fuse for win32 and darwin built with commit 568b0597b17da0c8cfbd345bab27176cd84bd883
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`.
win32 binary built on Windows 10, version 21H2 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`.
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.

Binary file not shown.

BIN
bin/win32/save3ds_fuse.exe Executable file → Normal file

Binary file not shown.

472
ci-gui.py
View File

@@ -1,3 +1,5 @@
#!/usr/bin/env python3
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019-2020 Ian Burgwin
@@ -5,8 +7,8 @@
# 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, join, isfile, dirname
from sys import exc_info, platform
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
@@ -16,17 +18,28 @@ 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 custominstall import CustomInstall
from custominstall import CustomInstall, CI_VERSION, load_cifinish, InvalidCIFinishError, InstallStatus
if TYPE_CHECKING:
from typing import List
from os import PathLike
from typing import Dict, List, Union
is_windows = platform == 'win32'
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
@@ -34,10 +47,10 @@ if is_windows:
taskbar = cc.CreateObject('{56FDF344-FD6D-11D0-958A-006097C9A090}', interface=tbl.ITaskbarList3)
taskbar.HrInit()
except ModuleNotFoundError:
except (ModuleNotFoundError, UnicodeEncodeError, AttributeError):
pass
file_parent = dirname(__file__)
file_parent = dirname(abspath(__file__))
# automatically load boot9 if it's in the current directory
b9_paths.insert(0, join(file_parent, 'boot9.bin'))
@@ -52,6 +65,10 @@ except KeyError:
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):
@@ -63,6 +80,18 @@ 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):
@@ -94,13 +123,155 @@ class ConsoleFrame(ttk.Frame):
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'] and copied_3dsx:
message += '\n\ncustom-install-finalize has been copied to the SD card.'
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 = []
@@ -130,15 +301,27 @@ class CustomInstallGUI(ttk.Frame):
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)
sd_msed_path = find_first_file([join(f, 'gm9', 'out', 'movable.sed'), join(f, 'movable.sed')])
if sd_msed_path:
self.log('Found movable.sed on SD card at ' + sd_msed_path)
box = self.file_picker_textboxes['movable.sed']
box.delete('1.0', tk.END)
box.insert(tk.END, sd_msed_path)
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)
@@ -151,14 +334,25 @@ class CustomInstallGUI(ttk.Frame):
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, 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):
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)
@@ -173,59 +367,104 @@ class CustomInstallGUI(ttk.Frame):
self.file_picker_textboxes[type_name] = selected
create_required_file_picker('boot9', [('boot9 file', '*.bin')], default_b9_path, 1)
create_required_file_picker('seeddb', [('seeddb file', '*.bin')], default_seeddb_path, 2)
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
listbox_buttons = ttk.Frame(self)
listbox_buttons.grid(row=1, column=0)
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:
self.add_cia(f)
success, reason = self.add_cia(f)
if not success:
results[f] = reason
add_cias = ttk.Button(listbox_buttons, text='Add CIAs', command=add_cias_callback)
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'):
self.add_cia(f.path)
success, reason = self.add_cia(f.path)
if not success:
results[f] = reason
add_dirs = ttk.Button(listbox_buttons, text='Add folder', command=add_dirs_callback)
add_dirs.grid(row=0, column=1)
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():
indexes = self.cia_listbox.curselection()
n = 0
for i in indexes:
self.cia_listbox.delete(i - n)
n += 1
for entry in self.treeview.selection():
self.remove_cia(entry)
remove_selected = ttk.Button(listbox_buttons, text='Remove selected', command=remove_selected_callback)
remove_selected.grid(row=0, column=2)
remove_selected = ttk.Button(titlelist_buttons, text='Remove selected', command=remove_selected_callback)
remove_selected.grid(row=0, column=3)
# ---------------------------------------------------------------- #
# create listbox
listbox_frame = ttk.Frame(self)
listbox_frame.grid(row=2, column=0, sticky=tk.NSEW)
listbox_frame.rowconfigure(0, weight=1)
listbox_frame.columnconfigure(0, weight=1)
# 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)
cia_listbox_scrollbar = ttk.Scrollbar(listbox_frame, orient=tk.VERTICAL)
cia_listbox_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
treeview_scrollbar = ttk.Scrollbar(treeview_frame, orient=tk.VERTICAL)
treeview_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
self.cia_listbox = tk.Listbox(listbox_frame, highlightthickness=0, yscrollcommand=cia_listbox_scrollbar.set,
selectmode=tk.EXTENDED)
self.cia_listbox.grid(row=0, column=0, 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')
cia_listbox_scrollbar.config(command=self.cia_listbox.yview)
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
@@ -258,20 +497,73 @@ class CustomInstallGUI(ttk.Frame):
self.status_label = ttk.Label(self, text='Waiting...')
self.status_label.grid(row=5, column=0, sticky=tk.NSEW)
self.log('custom-install by ihaveamac', status=False)
self.log('https://github.com/ihaveamac/custom-install', status=False)
self.log(f'custom-install {CI_VERSION} - https://github.com/ihaveamac/custom-install', status=False)
if is_windows and not taskbar:
self.log('Note: comtypes module not found.')
self.log('Note: Could not load taskbar lib.')
self.log('Note: Progress will not be shown in the Windows taskbar.')
self.log('Ready.')
self.disable_during_install = (add_cias, add_dirs, remove_selected, start, *self.file_picker_textboxes.values())
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)
self.cia_listbox.insert(tk.END, 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:
@@ -316,25 +608,26 @@ class CustomInstallGUI(ttk.Frame):
mb.showinfo('Info', message, parent=self.parent)
def disable_buttons(self):
for b in self.disable_during_install:
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):
for b in self.disable_during_install:
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()
boot9 = self.file_picker_textboxes['boot9'].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 boot9:
self.show_error('boot9 is not specified.')
return
if not movable_sed:
self.show_error('movable.sed is not specified.')
return
@@ -344,24 +637,42 @@ class CustomInstallGUI(ttk.Frame):
'Continue?'):
return
self.disable_buttons()
self.log('Starting install...')
cias = self.cia_listbox.get(0, tk.END)
if not len(cias):
if not len(self.readers):
self.show_error('There are no titles added to install.')
return
installer = CustomInstall(boot9=boot9,
seeddb=seeddb,
movable=movable_sed,
cias=cias,
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(cias)
max_percentage = 100 * len(self.readers)
self.progressbar.config(maximum=max_percentage)
def ci_on_log_msg(message, *args, **kwargs):
@@ -392,27 +703,32 @@ class CustomInstallGUI(ttk.Frame):
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 taskbar:
taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL)
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 = installer.start(continue_on_fail=False)
if result is True:
self.log('Done!')
if copied_3dsx:
self.show_info('custom-install-finalize has been copied to the SD card.\n'
'To finish the install, run this on the console through the homebrew launcher.\n'
'This will install a ticket and seed if required.')
else:
self.show_info('To finish the install, run custom-install-finalize on the console.\n'
'This will install a ticket and seed if required.')
elif result is False:
self.show_error('An error occurred when trying to run save3ds_fuse.')
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(exc_info())
installer.event.on_error(sys.exc_info())
finally:
self.enable_buttons()
@@ -420,7 +736,7 @@ class CustomInstallGUI(ttk.Frame):
window = tk.Tk()
window.title('custom-install')
window.title(f'custom-install {CI_VERSION}')
frame = CustomInstallGUI(window)
frame.pack(fill=tk.BOTH, expand=True)
window.mainloop()

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env python3
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019-2020 Ian Burgwin
@@ -5,13 +7,17 @@
# 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, isfile
from enum import Enum
from glob import glob
import gzip
from os import makedirs, rename, scandir
from os.path import dirname, join, isdir, isfile
from random import randint
from hashlib import sha256
from locale import getpreferredencoding
from shutil import copyfile
from pprint import pformat
from shutil import copyfile, copy2, rmtree
import sys
from sys import platform, executable
from tempfile import TemporaryDirectory
from traceback import format_exception
from typing import BinaryIO, TYPE_CHECKING
@@ -19,22 +25,34 @@ import subprocess
if TYPE_CHECKING:
from os import PathLike
from typing import Union
from typing import List, Union, Tuple
from events import Events
from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb, get_seed
from pyctr.type.cia import CIAReader, CIASection
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
is_windows = sys.platform == 'win32'
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
CI_VERSION = '2.1'
# used to run the save3ds_fuse binary next to the script
frozen = getattr(sys, 'frozen', False)
script_dir: str
if frozen:
script_dir = dirname(sys.executable)
script_dir = dirname(executable)
else:
script_dir = dirname(__file__)
@@ -60,6 +78,35 @@ 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:
@@ -140,19 +187,32 @@ def save_cifinish(path: 'Union[PathLike, bytes, str]', data: dict):
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:
cia: CIAReader
def __init__(self, boot9, seeddb, movable, cias, sd, cifinish_out=None,
overwrite_saves=False, skip_contents=False):
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.cias = cias
self.readers: 'List[Tuple[Union[CDNReader, CIAReader], Union[PathLike, bytes, str]]]' = []
self.sd = sd
self.skip_contents = skip_contents
self.overwrite_saves = overwrite_saves
@@ -162,25 +222,74 @@ class CustomInstall:
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 = cipher.encrypt(src.read(READ_SIZE))
dst.write(data)
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)
def start(self, continue_on_fail=True):
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 frozen:
save3ds_fuse_path = join(script_dir, 'bin', 'save3ds_fuse')
else:
save3ds_fuse_path = join(script_dir, 'bin', sys.platform, 'save3ds_fuse')
save3ds_fuse_path = join(script_dir, 'bin', platform, 'save3ds_fuse')
if is_windows:
save3ds_fuse_path += '.exe'
if not isfile(save3ds_fuse_path):
self.log("Couldn't find " + save3ds_fuse_path, 2)
return None, False
return None, False, 0
crypto = self.crypto
# TODO: Move a lot of these into their own methods
@@ -191,293 +300,355 @@ class CustomInstall:
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')
sd_path = join(sd_path, id1s[0])
title_info_entries = {}
cifinish_data = load_cifinish(cifinish_path)
load_seeddb(self.seeddb)
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
# Now loop through all provided cia files
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()
for idx, c in enumerate(self.cias):
self.log('Reading ' + c)
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)
try:
cia = CIAReader(c)
except Exception as e:
self.event.on_error(sys.exc_info())
if continue_on_fail:
continue
else:
return None, False
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())
self.event.on_cia_start(idx)
e.seek(0)
e.write(cmac.digest())
self.cia = cia
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)
tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16])
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())
try:
self.log(f'Installing {cia.contents[0].exefs.icon.get_app_title().short_desc}...')
except:
self.log('Installing...')
e.seek(0)
e.write(cmac.digest())
sizes = [1] * 5
del tdb
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,
fire_event=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')
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')
if self.overwrite_saves or not isfile(out_path):
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))
else:
self.log(f'Not overwriting existing save at {enc_path}')
# 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
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
]
title_info_entries[cia.tmd.title_id] = b''.join(title_info_entry_data)
extra_kwargs = {}
if is_windows:
# hide console window
extra_kwargs['creationflags'] = 0x08000000 # CREATE_NO_WINDOW
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)}
# 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
# This is saved regardless if any titles were installed, so the file can be upgraded just in case.
save_cifinish(cifinish_path, cifinish_data)
install_state = {'installed': [], 'failed': []}
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 = [
save3ds_fuse_path,
'-b', crypto.b9_path,
'-m', self.movable,
'--sd', self.sd,
'--db', 'sdtitle',
tempdir
# 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
]
extra_kwargs = {}
if is_windows:
# hide console window
extra_kwargs['creationflags'] = 0x08000000 # CREATE_NO_WINDOW
self.event.update_status(path, InstallStatus.Finishing)
if isdir(title_root):
self.log(f'Removing original install at {title_root}...')
rmtree(title_root)
# 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=getpreferredencoding(),
**extra_kwargs)
if out.returncode:
for l in out.stdout.split('\n'):
self.log(l)
return False
makedirs(tidhigh_root, exist_ok=True)
rename(temp_title_root, title_root)
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)
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=getpreferredencoding(),
encoding='utf-8',
**extra_kwargs)
if out.returncode:
for l in out.stdout.split('\n'):
self.log(l)
return False, False
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)
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')
copied = False
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
# 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 True, copied
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.')
else:
self.log('Did not install any titles.', 2)
return None, False
return install_state, copied, application_count
def get_sd_path(self):
sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
@@ -522,7 +693,7 @@ class CustomInstall:
if __name__ == "__main__":
parser = ArgumentParser(description='Manually install a CIA to the SD card for a Nintendo 3DS system.')
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')
@@ -532,11 +703,11 @@ if __name__ == "__main__":
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 {CI_VERSION} - https://github.com/ihaveamac/custom-install')
args = parser.parse_args()
installer = CustomInstall(boot9=args.boot9,
seeddb=args.seeddb,
cias=args.cia,
movable=args.movable,
sd=args.sd,
overwrite_saves=args.overwrite_saves,
@@ -558,7 +729,25 @@ if __name__ == "__main__":
installer.event.update_percentage += percent_handle
installer.event.on_error += error
result, copied_3dsx = installer.start(continue_on_fail=False)
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.')

View File

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

View File

@@ -189,6 +189,7 @@ int load_cifinish(char* path, struct finish_db_entry_final **entries)
}
}
fclose(fp);
return header.title_count;
fail:
@@ -196,34 +197,99 @@ 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;
struct finish_db_entry_final *entries = NULL;
int title_count;
title_count = load_cifinish(CIFINISH_PATH, &entries);
if (title_count == -1)
u32 titles_read;
u32 tickets_read;
res = AM_GetTitleCount(MEDIATYPE_SD, &titles_read);
if (R_FAILED(res))
{
free(entries);
return;
}
if (title_count == 0)
{
printf("No titles to finalize.\n");
free(entries);
return;
}
//printf("Deleting %s...\n", CIFINISH_PATH);
//unlink(CIFINISH_PATH);
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;
}
title_count = load_cifinish(CIFINISH_PATH, &entries);
if (title_count == -1)
{
goto exit;
}
else if (title_count == 0)
{
printf("No titles to finalize.\n");
goto exit;
}
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);
@@ -233,8 +299,7 @@ void finalize_install(void)
{
printf("Failed to begin ticket install: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
free(entries);
return;
goto exit;
}
res = FSFILE_Write(ticketHandle, NULL, 0, &ticket_buf, sizeof(struct ticket_dumb), 0);
@@ -242,8 +307,7 @@ void finalize_install(void)
{
printf("Failed to write ticket: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
free(entries);
return;
goto exit;
}
res = AM_InstallTicketFinish(ticketHandle);
@@ -251,8 +315,7 @@ void finalize_install(void)
{
printf("Failed to finish ticket install: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
free(entries);
return;
goto exit;
}
if (entries[i].has_seed)
@@ -266,7 +329,15 @@ 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[])
@@ -275,7 +346,7 @@ int main(int argc, char* argv[])
gfxInitDefault();
consoleInit(GFX_TOP, NULL);
printf("custom-install-finalize v1.4\n");
printf("custom-install-finalize v1.6\n");
finalize_install();
// print this at the end in case it gets pushed off the screen

View File

@@ -1,11 +1,13 @@
mkdir build
mkdir dist
cxfreeze ci-gui.py --target-dir=build\custom-install-standalone --base-name=Win32GUI
python setup-cxfreeze.py build_exe --build-exe=build\custom-install-standalone
mkdir build\custom-install-standalone\bin
copy TaskbarLib.tlb build\custom-install-standalone
copy bin\win32\save3ds_fuse.exe build\custom-install-standalone\bin
copy bin\README build\custom-install-standalone\bin
copy custom-install-finalize.3dsx build\custom-install-standalone
copy title.db.gz build\custom-install-standalone
copy extras\windows-quickstart.txt build\custom-install-standalone
copy extras\run_with_cmd.bat build\custom-install-standalone
copy LICENSE.md build\custom-install-standalone
python -m zipfile -c dist\custom-install-standalone.zip build\custom-install-standalone

View File

@@ -1,2 +1,2 @@
-r requirements.txt
comtypes==1.1.7
comtypes==1.1.10

View File

@@ -1,3 +1,2 @@
pycryptodomex==3.9.8
events==0.3
pyctr==0.4.1
events==0.4
pyctr>=0.4,<0.7

19
setup-cxfreeze.py Normal file
View File

@@ -0,0 +1,19 @@
import sys
from cx_Freeze import setup, Executable
if sys.platform == 'win32':
executables = [
Executable('ci-gui.py', target_name='ci-gui-console'),
Executable('ci-gui.py', target_name='ci-gui', base='Win32GUI'),
]
else:
executables = [
Executable('ci-gui.py', target_name='ci-gui'),
]
setup(
name = "ci-gui",
version = "2.1b4",
description = "Installs a title directly to an SD card for the Nintendo 3DS",
executables = executables
)

BIN
title.db.gz Normal file

Binary file not shown.