mirror of
https://github.com/ihaveamac/custom-install.git
synced 2026-01-21 14:06:02 +00:00
Compare commits
18 Commits
finalize-1
...
module-new
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379da26a4e | ||
|
|
ed7fc99ff1 | ||
|
|
3a5f554b58 | ||
|
|
ba5c5f19a7 | ||
|
|
c344ce3e7b | ||
|
|
13f706a0dc | ||
|
|
3c99c7a9d9 | ||
|
|
238b7400e0 | ||
|
|
2319819bfa | ||
|
|
cb52b38ea7 | ||
|
|
3dcee32145 | ||
|
|
647f21d32b | ||
|
|
58237a0ebe | ||
|
|
9f69a2195c | ||
|
|
91e0fa24ad | ||
|
|
b3365c47bd | ||
|
|
a515ca7e61 | ||
|
|
167a80ff11 |
@@ -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.
|
||||
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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
57
gui.py
@@ -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()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pycryptodomex<=3.9.4
|
||||
events==0.3
|
||||
pyctr==0.1.0
|
||||
pyctr==0.4.1
|
||||
|
||||
Reference in New Issue
Block a user