18 Commits

Author SHA1 Message Date
Ian Burgwin
379da26a4e custominstall: fix loading seed (close #25) 2020-07-19 19:38:27 -07:00
Ian Burgwin
ed7fc99ff1 gui: properly print args on exception in execute_script 2020-07-12 21:16:25 -07:00
Ian Burgwin
3a5f554b58 gui: add full path when adding folder (close #23) 2020-07-12 21:12:59 -07:00
Ian Burgwin
ba5c5f19a7 Merge remote-tracking branch 'origin/module-new-gui' into module-new-gui 2020-07-12 21:08:15 -07:00
Ian Burgwin
c344ce3e7b allow changing cifinish output path 2020-07-12 21:07:51 -07:00
Ian Burgwin
13f706a0dc update to pyctr 0.4.1, catch exceptions when creating CIAReader 2020-07-12 21:07:26 -07:00
Ian Burgwin
3c99c7a9d9 Merge pull request #24 from uaevuon/module-new-gui
Change the order and the link in readme
2020-06-30 15:14:59 -07:00
uaevuon
238b7400e0 Change the order and link 2020-06-30 00:17:41 +09:00
Ian Burgwin
2319819bfa README: add notice about "Add Python 3.x to PATH" 2020-06-29 06:02:50 -07:00
Ian Burgwin
cb52b38ea7 custominstall: only makedirs if --skip-contents is not used, add exist_ok=True for DLC directories 2020-06-24 19:44:19 -07:00
Ian Burgwin
3dcee32145 Merge remote-tracking branch 'origin/module-new-gui' into module-new-gui 2020-06-24 19:41:59 -07:00
Ian Burgwin
647f21d32b Merge pull request #22 from LyfeOnEdge/patch-2
Clean up comments
2020-06-20 19:36:57 -07:00
Andrew (LyfeOnEdge) (ArcticGentoo)
58237a0ebe Clean up comments 2020-06-18 20:56:45 -07:00
Ian Burgwin
9f69a2195c Merge pull request #20 from JustSch/module-new-gui
added seeddb option to gui
2020-06-05 17:11:52 -07:00
Justin Schreiber
91e0fa24ad added seeddb option to gui 2020-05-27 19:34:11 -04:00
Ian Burgwin
b3365c47bd custominstall: don't extract or rebuild title.db if no titles were installed 2020-05-25 19:26:02 -07:00
Ian Burgwin
a515ca7e61 finalize: fix using pointers incorrectly, meaning only one title would work 2020-05-23 11:02:25 -07:00
Ian Burgwin
167a80ff11 gui: use list for args instead of creating a string 2020-05-22 02:43:11 -07:00
5 changed files with 116 additions and 88 deletions

View File

@@ -4,11 +4,13 @@
Experimental script to automate the process of a manual title install for Nintendo 3DS. Originally created late June 2019.
## Summary
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. Install the packages:
2. Download the repo ([zip link](https://github.com/ihaveamac/custom-install/archive/module-new-gui.zip) or `git clone`)
3. Install the packages:
* Windows: `py -3 -m pip install --user -r requirements.txt`
* macOS/Linux: `python3 -m pip install --user -r requirements.txt`
3. Download the repo ([zip link](https://github.com/ihaveamac/custom-install/archive/master.zip) or `git clone`)
4. Run `custominstall.py` with boot9.bin, movable.sed, path to the SD root, and CIA files to install (see Usage section).
5. Download and use [custom-install-finalize](https://github.com/ihaveamac/custom-install/releases) on the 3DS system to finish the install.

View File

@@ -20,7 +20,7 @@ if TYPE_CHECKING:
from events import Events
from pyctr.crypto import CryptoEngine, Keyslot
from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb
from pyctr.type.cia import CIAReader, CIASection
from pyctr.type.ncch import NCCHSection
from pyctr.util import roundup
@@ -134,7 +134,7 @@ class CustomInstall:
cia: CIAReader
def __init__(self, boot9, seeddb, movable, cias, sd, skip_contents=False):
def __init__(self, boot9, seeddb, movable, cias, sd, cifinish_out=None, skip_contents=False):
self.event = Events()
self.log_lines = [] # Stores all info messages for user to view
@@ -144,6 +144,7 @@ class CustomInstall:
self.cias = cias
self.sd = sd
self.skip_contents = skip_contents
self.cifinish_out = cifinish_out
self.movable = movable
def copy_with_progress(self, src: BinaryIO, dst: BinaryIO, size: int, path: str):
@@ -162,26 +163,33 @@ class CustomInstall:
# TODO: Move a lot of these into their own methods
self.log("Finding path to install to...")
[sd_path, id1s] = self.get_sd_path()
try:
if len(id1s) > 1:
raise SDPathError(f'There are multiple id1 directories for id0 {crypto.id0.hex()}, '
f'please remove extra directories')
elif len(id1s) == 0:
raise SDPathError(f'Could not find a suitable id1 directory for id0 {crypto.id0.hex()}')
except SDPathError:
self.log("")
if self.cifinish_out:
cifinish_path = self.cifinish_out
else:
cifinish_path = join(self.sd, 'cifinish.bin')
sd_path = join(sd_path, id1s[0])
title_info_entries = {}
cifinish_data = load_cifinish(cifinish_path)
load_seeddb(self.seeddb)
# Now loop through all provided cia files
for c in self.cias:
self.log('Reading ' + c)
cia = CIAReader(c, seeddb=self.seeddb)
try:
cia = CIAReader(c)
except Exception as e:
self.log(f'Failed to load file: {type(e).__name__}: {e}')
continue
self.cia = cia
tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16])
@@ -229,20 +237,20 @@ class CustomInstall:
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}'))
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'
if not self.skip_contents:
# write the tmd
enc_path = content_root_cmd + '/' + tmd_filename
self.log(f'Writing {enc_path}...')
@@ -367,10 +375,12 @@ class CustomInstall:
title_info_entries[cia.tmd.title_id] = b''.join(title_info_entry_data)
cifinish_data[int(cia.tmd.title_id, 16)] = {'seed': (cia.contents[0].seed if cia.contents[0].flags.uses_seed else None)}
cifinish_data[int(cia.tmd.title_id, 16)] = {'seed': (get_seed(cia.contents[0].program_id) if cia.contents[0].flags.uses_seed else None)}
# This is saved regardless if any titles were installed, so the file can be upgraded just in case.
save_cifinish(cifinish_path, cifinish_data)
if title_info_entries:
with TemporaryDirectory(suffix='-custom-install') as tempdir:
# set up the common arguments for the two times we call save3ds_fuse
save3ds_fuse_common_args = [
@@ -398,6 +408,9 @@ class CustomInstall:
self.log('FINAL STEP:\nRun custom-install-finalize through homebrew launcher.')
self.log('This will install a ticket and seed if required.')
else:
self.log('Did not install any titles.', 2)
def get_sd_path(self):
sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
id1s = []
@@ -448,6 +461,7 @@ if __name__ == "__main__":
parser.add_argument('-s', '--seeddb', help='seeddb file')
parser.add_argument('--sd', help='path to SD root', required=True)
parser.add_argument('--skip-contents', help="don't add contents, only add title info entry", action='store_true')
parser.add_argument('--cifinish-out', help='path for cifinish.bin file, defaults to (SD root)/cifinish.bin')
args = parser.parse_args()
@@ -456,6 +470,7 @@ if __name__ == "__main__":
cias=args.cia,
movable=args.movable,
sd=args.sd,
cifinish_out=args.cifinish_out,
skip_contents=(args.skip_contents or False))
def log_handle(msg, end='\n'):

View File

@@ -85,6 +85,8 @@ int load_cifinish(char* path, struct finish_db_entry_final **entries)
struct finish_db_entry_v2 v2;
struct finish_db_entry_v3 v3;
struct finish_db_entry_final *tmp;
int i;
size_t read;
@@ -114,6 +116,12 @@ int load_cifinish(char* path, struct finish_db_entry_final **entries)
}
*entries = calloc(header.title_count, sizeof(struct finish_db_entry_final));
if (!*entries) {
printf("Couldn't allocate memory.\n");
printf("This should never happen.\n");
goto fail;
}
tmp = *entries;
if (header.version == 1)
{
@@ -133,9 +141,9 @@ int load_cifinish(char* path, struct finish_db_entry_final **entries)
printf(" Is the file corrupt?\n");
goto fail;
}
entries[i]->has_seed = v1.has_seed;
entries[i]->title_id = v1.title_id;
memcpy(entries[i]->seed, v1.seed, 16);
tmp[i].has_seed = v1.has_seed;
tmp[i].title_id = v1.title_id;
memcpy(tmp[i].seed, v1.seed, 16);
}
} else if (header.version == 2) {
for (i = 0; i < header.title_count; i++)
@@ -154,9 +162,9 @@ int load_cifinish(char* path, struct finish_db_entry_final **entries)
printf(" Is the file corrupt?\n");
goto fail;
}
entries[i]->has_seed = v2.has_seed;
entries[i]->title_id = v2.title_id;
memcpy(entries[i]->seed, v2.seed, 16);
tmp[i].has_seed = v2.has_seed;
tmp[i].title_id = v2.title_id;
memcpy(tmp[i].seed, v2.seed, 16);
}
} else if (header.version == 3) {
for (i = 0; i < header.title_count; i++)
@@ -175,9 +183,9 @@ int load_cifinish(char* path, struct finish_db_entry_final **entries)
printf(" Is the file corrupt?\n");
goto fail;
}
entries[i]->has_seed = v3.has_seed;
entries[i]->title_id = v3.title_id;
memcpy(entries[i]->seed, v3.seed, 16);
tmp[i].has_seed = v3.has_seed;
tmp[i].title_id = v3.title_id;
memcpy(tmp[i].seed, v3.seed, 16);
}
}
@@ -267,7 +275,7 @@ int main(int argc, char* argv[])
gfxInitDefault();
consoleInit(GFX_TOP, NULL);
printf("custom-install-finalize v1.3\n");
printf("custom-install-finalize v1.4\n");
finalize_install();
// print this at the end in case it gets pushed off the screen

57
gui.py
View File

@@ -12,12 +12,6 @@ import tkinter.filedialog as tkfiledialog
import style
# BUTTON_COLOR =
# BUTTON_HIGHLIGHT_COLOR =
# BUTTON_FONT =
# Custom button
class themedFrame(tk.Frame):
def __init__(self, frame, **kw):
tk.Frame.__init__(self, frame, **kw)
@@ -304,9 +298,13 @@ class gui(tk.Tk):
self.boot9_box.place(relwidth=1, height=20, x=0, y=60)
CreateToolTip(self.boot9_box.xtainer, "Select the path to boot9.bin, this can be dumped from a 3ds")
self.seeddb_box = LabeledPathEntry(outer_frame, "Path to seeddb file -", filetypes=[('seeddb file', '*.bin')])
self.seeddb_box.place(relwidth=1, height=20, x=0, y=90)
CreateToolTip(self.seeddb_box.xtainer, "Select the path to seeddb.bin, this can retrieved from online")
# -------------------------------------------------
cia_container = themedFrame(outer_frame, borderwidth=0, highlightthickness=0)
cia_container.place(y=90, relwidth=1, height=115)
cia_container.place(y=120, relwidth=1, height=190)
cia_label = tk.Label(cia_container, text="cia paths - ", foreground=style.LABEL_COLOR,
background=style.BACKGROUND_COLOR)
@@ -318,31 +316,31 @@ class gui(tk.Tk):
"Select the cias you wish to install to the sd card. The `add folder` button will add all cias in the selected folder, but will not check subdirs. The `remove cia` button will remove the currently selected file from the listbox.")
add_cia_button = Button(cia_container, self.add_cia, text="add cia", font=style.monospace)
add_cia_button.place(relx=0, relwidth=0.333, height=20, y=92, width=- 6)
add_cia_button.place(relx=0, relwidth=0.333, height=20, y=95, width=- 6)
add_cia_folder_button = Button(cia_container, self.add_cia_folder, text="add folder", font=style.monospace)
add_cia_folder_button.place(relx=0.333, relwidth=0.333, height=20, y=92, x=+ 3, width=- 6)
add_cia_folder_button.place(relx=0.333, relwidth=0.333, height=20, y=95, x=+ 3, width=- 6)
remove_cia_button = Button(cia_container, self.remove_cia, text="remove cia", font=style.monospace)
remove_cia_button.place(relx=0.666, relwidth=0.333, height=20, y=92, x=+ 6, width=- 6)
remove_cia_button.place(relx=0.666, relwidth=0.333, height=20, y=95, x=+ 6, width=- 6)
# -------------------------------------------------
self.skip_contents = tk.IntVar()
skip_contents_checkbutton = tk.Checkbutton(outer_frame, text="Skip contents? (only add title info)",
variable=self.skip_contents, background=style.BACKGROUND_COLOR,
foreground=style.LABEL_COLOR, borderwidth=0, highlightthickness=0)
skip_contents_checkbutton.place(relwidth=1, y=205, height=20)
skip_contents_checkbutton.place(relwidth=1, y=239, height=20)
console_label = tk.Label(outer_frame, text="Console:", background="black", foreground="white",
font=style.boldmonospace, borderwidth=0, highlightthickness=0)
console_label.place(relwidth=1, height=20, y=230)
console_label.place(relwidth=1, height=20, y=260)
self.console = ScrolledText(outer_frame, background="black", foreground="white", highlightthickness=0)
self.console.place(relwidth=1, relheight=1, y=250, height=- 272)
self.console.place(relwidth=1, relheight=1, y=280, height=- 272)
run_button = Button(outer_frame, self.run, text="run", font=style.boldmonospace)
run_button.place(relwidth=1, rely=1, y=- 22)
def run(self):
argstring = ""
args_extra = []
self.output_to_console("-----------------------\nStarting...\n")
@@ -350,32 +348,38 @@ class gui(tk.Tk):
if not boot9:
self.output_to_console(
"Warning - boot9 not selected, if it's not set externally you may run into problems.\n")
argstring += f"-b {boot9} "
else:
args_extra.extend(['-b', boot9])
sed = self.sed_box.get()
if not sed:
self.output_to_console("Failed to run - No movable.sed selected.\n")
return
argstring += f"-m {sed} "
args_extra.extend(['-m', sed])
sd = self.sd_box.get().strip()
if not sd:
self.output_to_console("Failed to run - SD path not selected.\n")
return
argstring += f"--sd {sd} "
args_extra.extend(['--sd', sd])
seed = self.seeddb_box.get().strip()
if not seed:
self.output_to_console("Optional Seeddb not given - Certain CIAs May Require This!\n")
args_extra.extend(['--seeddb', seed])
cias = []
for i in range(0, self.cia_box.size()):
cias.append(self.cia_box.get(i).strip())
for cia in cias:
argstring += f" {cia}"
args_extra.append(cia)
if self.skip_contents.get():
argstring += "--skip-contents "
args_extra.append('--skip-contents')
print(f"Running custom-install.py with args {args}\n")
print(f"Running custom-install.py with args {args_extra}\n")
self.threader.do_async(execute_script, [argstring, self.output_to_console])
self.threader.do_async(execute_script, [args_extra, self.output_to_console])
def output_to_console(self, outstring):
self.console.insert('end', outstring)
@@ -393,7 +397,7 @@ class gui(tk.Tk):
(os.path.isfile(os.path.join(cia_dir_to_add, f)) and f.endswith(".cia"))]
if cias_to_add:
for cia_to_add in cias_to_add:
self.cia_box.insert('end', cia_to_add)
self.cia_box.insert('end', os.path.join(cia_dir_to_add, cia_to_add))
def remove_cia(self):
index = self.cia_box.curselection()
@@ -410,12 +414,11 @@ class gui(tk.Tk):
self.cia_box.select_set(0)
def execute_script(argstring, printer):
def execute_script(args_extra, printer):
"""Wrapper function to pipe install script output to a printer"""
try:
args = [sys.executable, '-u', os.path.join(os.path.dirname(__file__), "custominstall.py")]
for arg in argstring.split():
args.append(arg.strip())
try:
args.extend(args_extra)
p = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
@@ -427,7 +430,7 @@ def execute_script(argstring, printer):
printer(line)
p.wait()
except Exception as e:
printer(f"Error while executing script with args - {argstring} | Exception - {e}\n")
printer(f"Error while executing script with args - {args} | Exception - {e}\n")
t = threader_object()

View File

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