mirror of
https://github.com/ihaveamac/custom-install.git
synced 2026-01-21 14:06:02 +00:00
Compare commits
16 Commits
finalize-1
...
module-new
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379da26a4e | ||
|
|
ed7fc99ff1 | ||
|
|
3a5f554b58 | ||
|
|
ba5c5f19a7 | ||
|
|
c344ce3e7b | ||
|
|
13f706a0dc | ||
|
|
3c99c7a9d9 | ||
|
|
238b7400e0 | ||
|
|
2319819bfa | ||
|
|
cb52b38ea7 | ||
|
|
3dcee32145 | ||
|
|
647f21d32b | ||
|
|
58237a0ebe | ||
|
|
9f69a2195c | ||
|
|
91e0fa24ad | ||
|
|
b3365c47bd |
@@ -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.
|
||||
|
||||
|
||||
111
custominstall.py
111
custominstall.py
@@ -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 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()}')
|
||||
|
||||
cifinish_path = join(self.sd, 'cifinish.bin')
|
||||
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'
|
||||
|
||||
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}'))
|
||||
|
||||
# maybe this will be changed in the future
|
||||
tmd_id = 0
|
||||
|
||||
tmd_filename = f'{tmd_id:08x}.tmd'
|
||||
|
||||
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}...')
|
||||
@@ -367,36 +375,41 @@ 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)
|
||||
|
||||
with TemporaryDirectory(suffix='-custom-install') as tempdir:
|
||||
# set up the common arguments for the two times we call save3ds_fuse
|
||||
save3ds_fuse_common_args = [
|
||||
join(script_dir, 'bin', platform, 'save3ds_fuse'),
|
||||
'-b', crypto.b9_path,
|
||||
'-m', self.movable,
|
||||
'--sd', self.sd,
|
||||
'--db', 'sdtitle',
|
||||
tempdir
|
||||
]
|
||||
if title_info_entries:
|
||||
with TemporaryDirectory(suffix='-custom-install') as tempdir:
|
||||
# set up the common arguments for the two times we call save3ds_fuse
|
||||
save3ds_fuse_common_args = [
|
||||
join(script_dir, 'bin', platform, 'save3ds_fuse'),
|
||||
'-b', crypto.b9_path,
|
||||
'-m', self.movable,
|
||||
'--sd', self.sd,
|
||||
'--db', 'sdtitle',
|
||||
tempdir
|
||||
]
|
||||
|
||||
# extract the title database to add our own entry to
|
||||
self.log('Extracting Title Database...')
|
||||
subprocess.run(save3ds_fuse_common_args + ['-x'])
|
||||
# extract the title database to add our own entry to
|
||||
self.log('Extracting Title Database...')
|
||||
subprocess.run(save3ds_fuse_common_args + ['-x'])
|
||||
|
||||
for title_id, entry in title_info_entries.items():
|
||||
# write the title info entry to the temp directory
|
||||
with open(join(tempdir, title_id), 'wb') as o:
|
||||
o.write(entry)
|
||||
for title_id, entry in title_info_entries.items():
|
||||
# write the title info entry to the temp directory
|
||||
with open(join(tempdir, title_id), 'wb') as o:
|
||||
o.write(entry)
|
||||
|
||||
# import the directory, now including our title
|
||||
self.log('Importing into Title Database...')
|
||||
subprocess.run(save3ds_fuse_common_args + ['-i'])
|
||||
# import the directory, now including our title
|
||||
self.log('Importing into Title Database...')
|
||||
subprocess.run(save3ds_fuse_common_args + ['-i'])
|
||||
|
||||
self.log('FINAL STEP:\nRun custom-install-finalize through homebrew launcher.')
|
||||
self.log('This will install a ticket and seed if required.')
|
||||
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())
|
||||
@@ -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'):
|
||||
|
||||
35
gui.py
35
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,26 +316,26 @@ 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)
|
||||
|
||||
@@ -365,6 +363,11 @@ class gui(tk.Tk):
|
||||
return
|
||||
args_extra.extend(['--sd', sd])
|
||||
|
||||
seed = self.seeddb_box.get().strip()
|
||||
if not seed:
|
||||
self.output_to_console("Optional Seeddb not given - Certain CIAs May Require This!\n")
|
||||
args_extra.extend(['--seeddb', seed])
|
||||
|
||||
cias = []
|
||||
for i in range(0, self.cia_box.size()):
|
||||
cias.append(self.cia_box.get(i).strip())
|
||||
@@ -394,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()
|
||||
@@ -413,8 +416,8 @@ class gui(tk.Tk):
|
||||
|
||||
def execute_script(args_extra, printer):
|
||||
"""Wrapper function to pipe install script output to a printer"""
|
||||
args = [sys.executable, '-u', os.path.join(os.path.dirname(__file__), "custominstall.py")]
|
||||
try:
|
||||
args = [sys.executable, '-u', os.path.join(os.path.dirname(__file__), "custominstall.py")]
|
||||
args.extend(args_extra)
|
||||
p = subprocess.Popen(args,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -427,7 +430,7 @@ def execute_script(args_extra, 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