21 Commits
v2.1b3 ... v2.1

Author SHA1 Message Date
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
8 changed files with 166 additions and 35 deletions

3
.gitignore vendored
View File

@@ -18,3 +18,6 @@ venv/
======= =======
*.pyc *.pyc
/build/
/dist/
/custom-install-finalize.3dsx

View File

@@ -6,7 +6,7 @@
from os import environ, scandir from os import environ, scandir
from os.path import abspath, basename, dirname, join, isfile from os.path import abspath, basename, dirname, join, isfile
from sys import exc_info, platform import sys
from threading import Thread, Lock from threading import Thread, Lock
from time import strftime from time import strftime
from traceback import format_exception from traceback import format_exception
@@ -29,9 +29,15 @@ if TYPE_CHECKING:
from os import PathLike from os import PathLike
from typing import Dict, List, Union from typing import Dict, List, Union
is_windows = platform == 'win32' frozen = getattr(sys, 'frozen', None)
is_windows = sys.platform == 'win32'
taskbar = None taskbar = None
if is_windows: 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: try:
import comtypes.client as cc import comtypes.client as cc
@@ -39,7 +45,7 @@ if is_windows:
taskbar = cc.CreateObject('{56FDF344-FD6D-11D0-958A-006097C9A090}', interface=tbl.ITaskbarList3) taskbar = cc.CreateObject('{56FDF344-FD6D-11D0-958A-006097C9A090}', interface=tbl.ITaskbarList3)
taskbar.HrInit() taskbar.HrInit()
except ModuleNotFoundError: except (ModuleNotFoundError, UnicodeEncodeError, AttributeError):
pass pass
file_parent = dirname(abspath(__file__)) file_parent = dirname(abspath(__file__))
@@ -306,12 +312,14 @@ class CustomInstallGUI(ttk.Frame):
sd_selected.delete('1.0', tk.END) sd_selected.delete('1.0', tk.END)
sd_selected.insert(tk.END, f) sd_selected.insert(tk.END, f)
sd_msed_path = find_first_file([join(f, 'gm9', 'out', 'movable.sed'), join(f, 'movable.sed')]) for filename in ['boot9.bin', 'seeddb.bin', 'movable.sed']:
if sd_msed_path: path = auto_input_filename(self, f, filename)
self.log('Found movable.sed on SD card at ' + sd_msed_path) if filename == 'boot9.bin':
box = self.file_picker_textboxes['movable.sed'] self.check_b9_loaded()
box.delete('1.0', tk.END) self.enable_buttons()
box.insert(tk.END, sd_msed_path) if filename == 'seeddb.bin':
load_seeddb(path)
sd_type_label = ttk.Label(file_pickers, text='SD root') sd_type_label = ttk.Label(file_pickers, text='SD root')
sd_type_label.grid(row=0, column=0) sd_type_label.grid(row=0, column=0)
@@ -324,6 +332,16 @@ class CustomInstallGUI(ttk.Frame):
self.file_picker_textboxes['sd'] = sd_selected 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. # This feels so wrong.
def create_required_file_picker(type_name, types, default, row, callback=lambda filename: None): def create_required_file_picker(type_name, types, default, row, callback=lambda filename: None):
def internal_callback(): def internal_callback():
@@ -480,7 +498,7 @@ class CustomInstallGUI(ttk.Frame):
self.log(f'custom-install {CI_VERSION} - 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: 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('Note: Progress will not be shown in the Windows taskbar.')
self.log('Ready.') self.log('Ready.')
@@ -624,7 +642,6 @@ class CustomInstallGUI(ttk.Frame):
for path in self.readers.keys(): for path in self.readers.keys():
self.update_status(path, InstallStatus.Waiting) self.update_status(path, InstallStatus.Waiting)
self.disable_buttons() self.disable_buttons()
self.log('Starting install...')
if taskbar: if taskbar:
taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL) taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL)
@@ -634,6 +651,16 @@ class CustomInstallGUI(ttk.Frame):
skip_contents=self.skip_contents_var.get() == 1, skip_contents=self.skip_contents_var.get() == 1,
overwrite_saves=self.overwrite_saves_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 # use the treeview which has been sorted alphabetically
readers_final = [] readers_final = []
for k in self.treeview.get_children(): for k in self.treeview.get_children():
@@ -699,7 +726,7 @@ class CustomInstallGUI(ttk.Frame):
"Either title.db doesn't exist, or save3ds_fuse couldn't be run.") "Either title.db doesn't exist, or save3ds_fuse couldn't be run.")
self.open_console() self.open_console()
except: except:
installer.event.on_error(exc_info()) installer.event.on_error(sys.exc_info())
finally: finally:
self.enable_buttons() self.enable_buttons()

View File

@@ -12,7 +12,6 @@ from os import makedirs, rename, scandir
from os.path import dirname, join, isdir, isfile from os.path import dirname, join, isdir, isfile
from random import randint from random import randint
from hashlib import sha256 from hashlib import sha256
from locale import getpreferredencoding
from pprint import pformat from pprint import pformat
from shutil import copyfile, copy2, rmtree from shutil import copyfile, copy2, rmtree
import sys import sys
@@ -45,7 +44,7 @@ if is_windows:
else: else:
from os import statvfs from os import statvfs
CI_VERSION = '2.1b3' CI_VERSION = '2.1'
# used to run the save3ds_fuse binary next to the script # used to run the save3ds_fuse binary next to the script
frozen = getattr(sys, 'frozen', False) frozen = getattr(sys, 'frozen', False)
@@ -272,6 +271,10 @@ class CustomInstall:
free_space = get_free_space(self.sd) free_space = get_free_space(self.sd)
return total_size, free_space 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): def start(self):
if frozen: if frozen:
save3ds_fuse_path = join(script_dir, 'bin', 'save3ds_fuse') save3ds_fuse_path = join(script_dir, 'bin', 'save3ds_fuse')
@@ -365,7 +368,7 @@ class CustomInstall:
out = subprocess.run(save3ds_fuse_common_args + ['-x'], out = subprocess.run(save3ds_fuse_common_args + ['-x'],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
encoding=getpreferredencoding(), encoding='utf-8',
**extra_kwargs) **extra_kwargs)
if out.returncode: if out.returncode:
for l in out.stdout.split('\n'): for l in out.stdout.split('\n'):
@@ -606,7 +609,7 @@ class CustomInstall:
out = subprocess.run(save3ds_fuse_common_args + ['-i'], out = subprocess.run(save3ds_fuse_common_args + ['-i'],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
encoding=getpreferredencoding(), encoding='utf-8',
**extra_kwargs) **extra_kwargs)
if out.returncode: if out.returncode:
for l in out.stdout.split('\n'): for l in out.stdout.split('\n'):
@@ -724,6 +727,10 @@ if __name__ == "__main__":
installer.event.update_percentage += percent_handle installer.event.update_percentage += percent_handle
installer.event.on_error += error installer.event.on_error += error
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) installer.prepare_titles(args.cia)
if not args.skip_contents: if not args.skip_contents:
@@ -734,7 +741,11 @@ if __name__ == "__main__":
f'Free space: {free_space / (1024 * 1024):0.2f} MiB') f'Free space: {free_space / (1024 * 1024):0.2f} MiB')
sys.exit(1) sys.exit(1)
result, copied_3dsx = installer.start() result, copied_3dsx, application_count = installer.start()
if result is False: if result is False:
# save3ds_fuse failed # save3ds_fuse failed
installer.log('NOTE: Once save3ds_fuse is fixed, run the same command again with --skip-contents') 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

@@ -197,31 +197,99 @@ fail:
return -1; 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) void finalize_install(void)
{ {
Result res; Result res;
Handle ticketHandle; Handle ticketHandle;
struct ticket_dumb ticket_buf; struct ticket_dumb ticket_buf;
struct finish_db_entry_final *entries; struct finish_db_entry_final *entries = NULL;
int title_count; int title_count;
title_count = load_cifinish(CIFINISH_PATH, &entries); u32 titles_read;
if (title_count == -1) u32 tickets_read;
res = AM_GetTitleCount(MEDIATYPE_SD, &titles_read);
if (R_FAILED(res))
{ {
free(entries);
return; return;
} }
if (title_count == 0)
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"); printf("No titles to finalize.\n");
free(entries); goto exit;
return;
} }
memcpy(&ticket_buf, basetik_bin, basetik_bin_size); memcpy(&ticket_buf, basetik_bin, basetik_bin_size);
Result exist_res = 0;
for (int i = 0; i < title_count; ++i) 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); printf("Finalizing %016llx...\n", entries[i].title_id);
ticket_buf.title_id_be = __builtin_bswap64(entries[i].title_id); ticket_buf.title_id_be = __builtin_bswap64(entries[i].title_id);
@@ -231,8 +299,7 @@ void finalize_install(void)
{ {
printf("Failed to begin ticket install: %08lx\n", res); printf("Failed to begin ticket install: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle); AM_InstallTicketAbort(ticketHandle);
free(entries); goto exit;
return;
} }
res = FSFILE_Write(ticketHandle, NULL, 0, &ticket_buf, sizeof(struct ticket_dumb), 0); res = FSFILE_Write(ticketHandle, NULL, 0, &ticket_buf, sizeof(struct ticket_dumb), 0);
@@ -240,8 +307,7 @@ void finalize_install(void)
{ {
printf("Failed to write ticket: %08lx\n", res); printf("Failed to write ticket: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle); AM_InstallTicketAbort(ticketHandle);
free(entries); goto exit;
return;
} }
res = AM_InstallTicketFinish(ticketHandle); res = AM_InstallTicketFinish(ticketHandle);
@@ -249,8 +315,7 @@ void finalize_install(void)
{ {
printf("Failed to finish ticket install: %08lx\n", res); printf("Failed to finish ticket install: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle); AM_InstallTicketAbort(ticketHandle);
free(entries); goto exit;
return;
} }
if (entries[i].has_seed) if (entries[i].has_seed)
@@ -267,7 +332,12 @@ void finalize_install(void)
printf("Deleting %s...\n", CIFINISH_PATH); printf("Deleting %s...\n", CIFINISH_PATH);
unlink(CIFINISH_PATH); unlink(CIFINISH_PATH);
exit:
free(entries); free(entries);
free(installed_ticket_ids);
free(installed_title_ids);
return;
} }
int main(int argc, char* argv[]) int main(int argc, char* argv[])
@@ -276,7 +346,7 @@ int main(int argc, char* argv[])
gfxInitDefault(); gfxInitDefault();
consoleInit(GFX_TOP, NULL); consoleInit(GFX_TOP, NULL);
printf("custom-install-finalize v1.5\n"); printf("custom-install-finalize v1.6\n");
finalize_install(); finalize_install();
// print this at the end in case it gets pushed off the screen // print this at the end in case it gets pushed off the screen

View File

@@ -1,6 +1,6 @@
mkdir build mkdir build
mkdir dist 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 mkdir build\custom-install-standalone\bin
copy TaskbarLib.tlb build\custom-install-standalone copy TaskbarLib.tlb build\custom-install-standalone
copy bin\win32\save3ds_fuse.exe build\custom-install-standalone\bin copy bin\win32\save3ds_fuse.exe build\custom-install-standalone\bin
@@ -8,5 +8,6 @@ copy bin\README build\custom-install-standalone\bin
copy custom-install-finalize.3dsx build\custom-install-standalone copy custom-install-finalize.3dsx build\custom-install-standalone
copy title.db.gz build\custom-install-standalone copy title.db.gz build\custom-install-standalone
copy extras\windows-quickstart.txt 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 copy LICENSE.md build\custom-install-standalone
python -m zipfile -c dist\custom-install-standalone.zip 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 -r requirements.txt
comtypes==1.1.8 comtypes==1.1.10

View File

@@ -1,2 +1,2 @@
events==0.4 events==0.4
pyctr==0.4.6 pyctr>=0.4,<0.6

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
)