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.
|
Experimental script to automate the process of a manual title install for Nintendo 3DS. Originally created late June 2019.
|
||||||
|
|
||||||
## Summary
|
## 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.
|
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`
|
* Windows: `py -3 -m pip install --user -r requirements.txt`
|
||||||
* macOS/Linux: `python3 -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).
|
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.
|
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 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.cia import CIAReader, CIASection
|
||||||
from pyctr.type.ncch import NCCHSection
|
from pyctr.type.ncch import NCCHSection
|
||||||
from pyctr.util import roundup
|
from pyctr.util import roundup
|
||||||
@@ -134,7 +134,7 @@ class CustomInstall:
|
|||||||
|
|
||||||
cia: CIAReader
|
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.event = Events()
|
||||||
self.log_lines = [] # Stores all info messages for user to view
|
self.log_lines = [] # Stores all info messages for user to view
|
||||||
|
|
||||||
@@ -144,6 +144,7 @@ class CustomInstall:
|
|||||||
self.cias = cias
|
self.cias = cias
|
||||||
self.sd = sd
|
self.sd = sd
|
||||||
self.skip_contents = skip_contents
|
self.skip_contents = skip_contents
|
||||||
|
self.cifinish_out = cifinish_out
|
||||||
self.movable = movable
|
self.movable = movable
|
||||||
|
|
||||||
def copy_with_progress(self, src: BinaryIO, dst: BinaryIO, size: int, path: str):
|
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
|
# TODO: Move a lot of these into their own methods
|
||||||
self.log("Finding path to install to...")
|
self.log("Finding path to install to...")
|
||||||
[sd_path, id1s] = self.get_sd_path()
|
[sd_path, id1s] = self.get_sd_path()
|
||||||
try:
|
|
||||||
if len(id1s) > 1:
|
if len(id1s) > 1:
|
||||||
raise SDPathError(f'There are multiple id1 directories for id0 {crypto.id0.hex()}, '
|
raise SDPathError(f'There are multiple id1 directories for id0 {crypto.id0.hex()}, '
|
||||||
f'please remove extra directories')
|
f'please remove extra directories')
|
||||||
elif len(id1s) == 0:
|
elif len(id1s) == 0:
|
||||||
raise SDPathError(f'Could not find a suitable id1 directory for id0 {crypto.id0.hex()}')
|
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')
|
cifinish_path = join(self.sd, 'cifinish.bin')
|
||||||
sd_path = join(sd_path, id1s[0])
|
sd_path = join(sd_path, id1s[0])
|
||||||
title_info_entries = {}
|
title_info_entries = {}
|
||||||
cifinish_data = load_cifinish(cifinish_path)
|
cifinish_data = load_cifinish(cifinish_path)
|
||||||
|
|
||||||
|
load_seeddb(self.seeddb)
|
||||||
|
|
||||||
# Now loop through all provided cia files
|
# Now loop through all provided cia files
|
||||||
|
|
||||||
for c in self.cias:
|
for c in self.cias:
|
||||||
self.log('Reading ' + c)
|
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
|
self.cia = cia
|
||||||
|
|
||||||
tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16])
|
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)}'
|
title_root_cmd = f'/title/{"/".join(tid_parts)}'
|
||||||
content_root_cmd = title_root_cmd + '/content'
|
content_root_cmd = title_root_cmd + '/content'
|
||||||
|
|
||||||
|
if not self.skip_contents:
|
||||||
makedirs(join(content_root, 'cmd'), exist_ok=True)
|
makedirs(join(content_root, 'cmd'), exist_ok=True)
|
||||||
if cia.tmd.save_size:
|
if cia.tmd.save_size:
|
||||||
makedirs(join(title_root, 'data'), exist_ok=True)
|
makedirs(join(title_root, 'data'), exist_ok=True)
|
||||||
if is_dlc:
|
if is_dlc:
|
||||||
# create the separate directories for every 256 contents
|
# create the separate directories for every 256 contents
|
||||||
for x in range(((len(cia.content_info) - 1) // 256) + 1):
|
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
|
# maybe this will be changed in the future
|
||||||
tmd_id = 0
|
tmd_id = 0
|
||||||
|
|
||||||
tmd_filename = f'{tmd_id:08x}.tmd'
|
tmd_filename = f'{tmd_id:08x}.tmd'
|
||||||
|
|
||||||
if not self.skip_contents:
|
|
||||||
# write the tmd
|
# write the tmd
|
||||||
enc_path = content_root_cmd + '/' + tmd_filename
|
enc_path = content_root_cmd + '/' + tmd_filename
|
||||||
self.log(f'Writing {enc_path}...')
|
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)
|
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)
|
save_cifinish(cifinish_path, cifinish_data)
|
||||||
|
|
||||||
|
if title_info_entries:
|
||||||
with TemporaryDirectory(suffix='-custom-install') as tempdir:
|
with TemporaryDirectory(suffix='-custom-install') as tempdir:
|
||||||
# set up the common arguments for the two times we call save3ds_fuse
|
# set up the common arguments for the two times we call save3ds_fuse
|
||||||
save3ds_fuse_common_args = [
|
save3ds_fuse_common_args = [
|
||||||
@@ -398,6 +408,9 @@ class CustomInstall:
|
|||||||
self.log('FINAL STEP:\nRun custom-install-finalize through homebrew launcher.')
|
self.log('FINAL STEP:\nRun custom-install-finalize through homebrew launcher.')
|
||||||
self.log('This will install a ticket and seed if required.')
|
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):
|
def get_sd_path(self):
|
||||||
sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
|
sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
|
||||||
id1s = []
|
id1s = []
|
||||||
@@ -448,6 +461,7 @@ if __name__ == "__main__":
|
|||||||
parser.add_argument('-s', '--seeddb', help='seeddb file')
|
parser.add_argument('-s', '--seeddb', help='seeddb file')
|
||||||
parser.add_argument('--sd', help='path to SD root', required=True)
|
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('--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -456,6 +470,7 @@ if __name__ == "__main__":
|
|||||||
cias=args.cia,
|
cias=args.cia,
|
||||||
movable=args.movable,
|
movable=args.movable,
|
||||||
sd=args.sd,
|
sd=args.sd,
|
||||||
|
cifinish_out=args.cifinish_out,
|
||||||
skip_contents=(args.skip_contents or False))
|
skip_contents=(args.skip_contents or False))
|
||||||
|
|
||||||
def log_handle(msg, end='\n'):
|
def log_handle(msg, end='\n'):
|
||||||
|
|||||||
35
gui.py
35
gui.py
@@ -12,12 +12,6 @@ import tkinter.filedialog as tkfiledialog
|
|||||||
import style
|
import style
|
||||||
|
|
||||||
|
|
||||||
# BUTTON_COLOR =
|
|
||||||
# BUTTON_HIGHLIGHT_COLOR =
|
|
||||||
# BUTTON_FONT =
|
|
||||||
|
|
||||||
# Custom button
|
|
||||||
|
|
||||||
class themedFrame(tk.Frame):
|
class themedFrame(tk.Frame):
|
||||||
def __init__(self, frame, **kw):
|
def __init__(self, frame, **kw):
|
||||||
tk.Frame.__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)
|
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")
|
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 = 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,
|
cia_label = tk.Label(cia_container, text="cia paths - ", foreground=style.LABEL_COLOR,
|
||||||
background=style.BACKGROUND_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.")
|
"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 = 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 = 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 = 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()
|
self.skip_contents = tk.IntVar()
|
||||||
skip_contents_checkbutton = tk.Checkbutton(outer_frame, text="Skip contents? (only add title info)",
|
skip_contents_checkbutton = tk.Checkbutton(outer_frame, text="Skip contents? (only add title info)",
|
||||||
variable=self.skip_contents, background=style.BACKGROUND_COLOR,
|
variable=self.skip_contents, background=style.BACKGROUND_COLOR,
|
||||||
foreground=style.LABEL_COLOR, borderwidth=0, highlightthickness=0)
|
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",
|
console_label = tk.Label(outer_frame, text="Console:", background="black", foreground="white",
|
||||||
font=style.boldmonospace, borderwidth=0, highlightthickness=0)
|
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 = 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 = Button(outer_frame, self.run, text="run", font=style.boldmonospace)
|
||||||
run_button.place(relwidth=1, rely=1, y=- 22)
|
run_button.place(relwidth=1, rely=1, y=- 22)
|
||||||
|
|
||||||
@@ -365,6 +363,11 @@ class gui(tk.Tk):
|
|||||||
return
|
return
|
||||||
args_extra.extend(['--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 = []
|
cias = []
|
||||||
for i in range(0, self.cia_box.size()):
|
for i in range(0, self.cia_box.size()):
|
||||||
cias.append(self.cia_box.get(i).strip())
|
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"))]
|
(os.path.isfile(os.path.join(cia_dir_to_add, f)) and f.endswith(".cia"))]
|
||||||
if cias_to_add:
|
if cias_to_add:
|
||||||
for cia_to_add in 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):
|
def remove_cia(self):
|
||||||
index = self.cia_box.curselection()
|
index = self.cia_box.curselection()
|
||||||
@@ -413,8 +416,8 @@ class gui(tk.Tk):
|
|||||||
|
|
||||||
def execute_script(args_extra, printer):
|
def execute_script(args_extra, printer):
|
||||||
"""Wrapper function to pipe install script output to a 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")]
|
args = [sys.executable, '-u', os.path.join(os.path.dirname(__file__), "custominstall.py")]
|
||||||
|
try:
|
||||||
args.extend(args_extra)
|
args.extend(args_extra)
|
||||||
p = subprocess.Popen(args,
|
p = subprocess.Popen(args,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
@@ -427,7 +430,7 @@ def execute_script(args_extra, printer):
|
|||||||
printer(line)
|
printer(line)
|
||||||
p.wait()
|
p.wait()
|
||||||
except Exception as e:
|
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()
|
t = threader_object()
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pycryptodomex<=3.9.4
|
pycryptodomex<=3.9.4
|
||||||
events==0.3
|
events==0.3
|
||||||
pyctr==0.1.0
|
pyctr==0.4.1
|
||||||
|
|||||||
Reference in New Issue
Block a user