3 Commits

Author SHA1 Message Date
ihaveahax
5cf735e55f Merge pull request #88 from RoiKlevansky/feat/finalize-piepline
Add pipeline for "finalize"
2026-03-07 15:47:57 -06:00
Roi Klevansky
6a79b6ca86 ci(finalize): initial pipeline for finalize 2026-03-06 21:44:26 +02:00
Roi Klevansky
46361111ba ref(finalize): use updated libctru function names 2026-03-06 21:39:13 +02:00
23 changed files with 75 additions and 487 deletions

32
.github/workflows/build-finalize.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Build finalize (3DS)
on:
push:
paths:
- 'finalize/**'
pull_request:
paths:
- 'finalize/**'
jobs:
build:
runs-on: ubuntu-latest
container:
image: devkitpro/devkitarm:20260219
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build finalize module
working-directory: finalize
run: make
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: finalize-3dsx
path: |
finalize/custom-install-finalize.3dsx
finalize/custom-install-finalize.elf
finalize/custom-install-finalize.smdh

5
.gitignore vendored
View File

@@ -11,15 +11,12 @@ testing-class.py
venv/
**/__pycache__/
*.pyc
*.egg-info/
# JetBrains
.idea/
=======
*.pyc
/build/
/dist/
/custom-install-finalize.3dsx
result
result-*

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2019 Ian Burgwin
Copyright (c) 2019-2021 Ian Burgwin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,2 +0,0 @@
recursive-include custominstall/bin/*
include custominstall/title.db.gz

View File

@@ -2,7 +2,7 @@
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019 Ian Burgwin
# custom-install is copyright (c) 2019-2020 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
@@ -25,8 +25,7 @@ from pyctr.type.cdn import CDNError
from pyctr.type.cia import CIAError
from pyctr.type.tmd import TitleMetadataError
from . import __version__
from .__main__ import CustomInstall, load_cifinish, InvalidCIFinishError, InstallStatus, save3ds_fuse_path
from custominstall import CustomInstall, CI_VERSION, load_cifinish, InvalidCIFinishError, InstallStatus
if TYPE_CHECKING:
from os import PathLike
@@ -336,9 +335,7 @@ class CustomInstallGUI(ttk.Frame):
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, "boot9strap", filename), join(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'):
@@ -500,7 +497,7 @@ class CustomInstallGUI(ttk.Frame):
self.status_label = ttk.Label(self, text='Waiting...')
self.status_label.grid(row=5, column=0, sticky=tk.NSEW)
self.log(f'custom-install {__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:
self.log('Note: Could not load taskbar lib.')
@@ -738,17 +735,8 @@ class CustomInstallGUI(ttk.Frame):
Thread(target=install).start()
def main():
if not (save3ds_fuse_path and isfile(save3ds_fuse_path)):
mb.showerror('Error', "Couldn't find save3ds_fuse. Please place it PATH.")
return
window = tk.Tk()
window.title(f'custom-install {__version__}')
frame = CustomInstallGUI(window)
frame.pack(fill=tk.BOTH, expand=True)
window.mainloop()
if __name__ == '__main__':
main()
window = tk.Tk()
window.title(f'custom-install {CI_VERSION}')
frame = CustomInstallGUI(window)
frame.pack(fill=tk.BOTH, expand=True)
window.mainloop()

View File

@@ -2,7 +2,7 @@
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019 Ian Burgwin
# custom-install is copyright (c) 2019-2020 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
@@ -10,12 +10,12 @@ from argparse import ArgumentParser
from enum import Enum
from glob import glob
import gzip
from os import makedirs, rename, scandir, environ
from os import makedirs, rename, scandir
from os.path import dirname, join, isdir, isfile
from random import randint
from hashlib import sha256
from pprint import pformat
from shutil import copyfile, copy2, rmtree, which
from shutil import copyfile, copy2, rmtree
import sys
from sys import platform, executable
from tempfile import TemporaryDirectory
@@ -36,8 +36,6 @@ from pyctr.type.ncch import NCCHSection
from pyctr.type.tmd import TitleMetadataError
from pyctr.util import roundup
from . import __version__
if platform == 'msys':
platform = 'win32'
@@ -48,28 +46,16 @@ if is_windows:
else:
from os import statvfs
script_dir: str
CI_VERSION = '2.1'
# used to run the save3ds_fuse binary next to the script
frozen = getattr(sys, 'frozen', False)
script_dir: str
if frozen:
script_dir = dirname(executable)
else:
script_dir = dirname(__file__)
# used to run the save3ds_fuse binary next to the script
if 'CUSTOM_INSTALL_SAVE3DS_PATH' in environ:
save3ds_fuse_path = environ['CUSTOM_INSTALL_SAVE3DS_PATH']
else:
save3ds_fuse_name = 'save3ds_fuse'
if is_windows:
save3ds_fuse_name += '.exe'
if frozen:
save3ds_fuse_path = join(script_dir, 'bin', save3ds_fuse_name)
else:
save3ds_fuse_path = join(script_dir, 'bin', platform, save3ds_fuse_name)
if not isfile(save3ds_fuse_path):
save3ds_fuse_path = which('save3ds_fuse')
# missing contents are replaced with 0xFFFFFFFF in the cmd file
CMD_MISSING = b'\xff\xff\xff\xff'
@@ -295,7 +281,13 @@ class CustomInstall:
return isdir(sd_path)
def start(self):
if not (save3ds_fuse_path and isfile(save3ds_fuse_path)):
if frozen:
save3ds_fuse_path = join(script_dir, 'bin', 'save3ds_fuse')
else:
save3ds_fuse_path = join(script_dir, 'bin', platform, 'save3ds_fuse')
if is_windows:
save3ds_fuse_path += '.exe'
if not isfile(save3ds_fuse_path):
self.log("Couldn't find " + save3ds_fuse_path, 2)
return None, False, 0
@@ -481,13 +473,13 @@ class CustomInstall:
self.log(f'Writing {content_enc_path}...')
with cia.open_raw_section(co.cindex) as s, open(content_out_path, 'wb') as o:
result_hash = self.copy_with_progress(s, o, co.size, content_enc_path)
if result_hash != co.hash:
self.log(f'WARNING: Hash does not match for {content_enc_path}!')
install_state['failed'].append(display_title)
rename(temp_title_root, temp_title_root + '-corrupted')
do_continue = True
self.event.update_status(path, InstallStatus.Failed)
break
if result_hash != co.hash:
self.log(f'WARNING: Hash does not match for {content_enc_path}!')
install_state['failed'].append(display_title)
rename(temp_title_root, temp_title_root + '-corrupted')
do_continue = True
self.event.update_status(path, InstallStatus.Failed)
break
if do_continue:
continue
@@ -700,7 +692,7 @@ class CustomInstall:
return msg_with_type
def main():
if __name__ == "__main__":
parser = ArgumentParser(description='Install a CIA to the SD card for a Nintendo 3DS system.')
parser.add_argument('cia', help='CIA files', nargs='+')
parser.add_argument('-m', '--movable', help='movable.sed file', required=True)
@@ -711,7 +703,7 @@ def main():
parser.add_argument('--overwrite-saves', help='overwrite existing save files', action='store_true')
parser.add_argument('--cifinish-out', help='path for cifinish.bin file, defaults to (SD root)/cifinish.bin')
print(f'custom-install {__version__} - https://github.com/ihaveamac/custom-install')
print(f'custom-install {CI_VERSION} - https://github.com/ihaveamac/custom-install')
args = parser.parse_args()
installer = CustomInstall(boot9=args.boot9,
@@ -759,7 +751,3 @@ def main():
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.')
if __name__ == "__main__":
main()

View File

@@ -1,10 +0,0 @@
# This file is a part of custom-install.
#
# Copyright (c) 2019 Ian Burgwin
# This file is licensed under The MIT License (MIT).
# You can find the full license text in LICENSE.md in the root of this project.
__author__ = 'ihaveahax'
__copyright__ = 'Copyright (c) 2019 Ian Burgwin'
__license__ = 'MIT'
__version__ = '2.1'

View File

@@ -1,34 +0,0 @@
{
pkgs ? import <nixpkgs> { },
# just so i can use the same pinned version as the flake...
pyctr ? (
let
flakeLock = builtins.fromJSON (builtins.readFile ./flake.lock);
pyctr-repo = import (builtins.fetchTarball (
with flakeLock.nodes.pyctr.locked;
{
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz";
}
)) { inherit pkgs; };
in
pyctr-repo.pyctr
),
save3ds ? (
let
flakeLock = builtins.fromJSON (builtins.readFile ./flake.lock);
hax-nur-repo = import (builtins.fetchTarball (
with flakeLock.nodes.hax-nur.locked;
{
url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz";
}
)) { inherit pkgs; };
in
hax-nur-repo.save3ds
),
}:
rec {
custominstall = pkgs.python3Packages.callPackage ./package.nix {
inherit pyctr save3ds;
};
}

82
finalize/flake.lock generated
View File

@@ -1,82 +0,0 @@
{
"nodes": {
"devkitNix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1766539742,
"narHash": "sha256-F6OeM2LrLo2n+Xg5XU4udQR/vuWWrDMKxXRzNXE2ClQ=",
"owner": "bandithedoge",
"repo": "devkitNix",
"rev": "c97f9880737716085e78009cba6bf85ad104628b",
"type": "github"
},
"original": {
"owner": "bandithedoge",
"repo": "devkitNix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devkitNix": "devkitNix",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,31 +0,0 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
devkitNix.url = "github:bandithedoge/devkitNix";
devkitNix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, devkitNix }: let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ devkitNix.overlays.default ]; };
in {
devShells.x86_64-linux = rec {
custom-install-finalize = pkgs.mkShell.override { stdenv = pkgs.devkitNix.stdenvARM; } {};
cif = custom-install-finalize;
};
packages.x86_64-linux = rec {
custom-install-finalize = pkgs.devkitNix.stdenvARM.mkDerivation rec {
name = "custom-install-finalize";
src = builtins.path { path = ./.; name = name; };
makeFlags = [ "TARGET=${name}" ];
installPhase = ''
mkdir $out
cp ${name}.3dsx $out
'';
};
cif = custom-install-finalize;
};
};
}

View File

@@ -294,11 +294,11 @@ void finalize_install(void)
ticket_buf.title_id_be = __builtin_bswap64(entries[i].title_id);
res = AM_InstallTicketBegin(&ticketHandle);
res = AMNET_InstallTicketBegin(&ticketHandle);
if (R_FAILED(res))
{
printf("Failed to begin ticket install: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
AMNET_InstallTicketAbort(ticketHandle);
goto exit;
}
@@ -306,15 +306,15 @@ void finalize_install(void)
if (R_FAILED(res))
{
printf("Failed to write ticket: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
AMNET_InstallTicketAbort(ticketHandle);
goto exit;
}
res = AM_InstallTicketFinish(ticketHandle);
res = AMNET_InstallTicketFinish(ticketHandle);
if (R_FAILED(res))
{
printf("Failed to finish ticket install: %08lx\n", res);
AM_InstallTicketAbort(ticketHandle);
AMNET_InstallTicketAbort(ticketHandle);
goto exit;
}

93
flake.lock generated
View File

@@ -1,93 +0,0 @@
{
"nodes": {
"hax-nur": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1767302708,
"narHash": "sha256-uCSEH/PR5/JxwuMayB4fMcOhOCT7I6BzWp7EtEYYjFQ=",
"owner": "ihaveamac",
"repo": "nur-packages",
"rev": "f612d64a4136c3a4820e37ed50cefb6460dde857",
"type": "github"
},
"original": {
"owner": "ihaveamac",
"ref": "master",
"repo": "nur-packages",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pyctr": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1763515957,
"narHash": "sha256-S0qzooGQN5tkbIVgijVZ9umvBC1dYbdPN97tks5SbwE=",
"owner": "ihaveamac",
"repo": "pyctr",
"rev": "eb8d4d06ce7339727d3f72b40f45ec3260336058",
"type": "github"
},
"original": {
"owner": "ihaveamac",
"ref": "master",
"repo": "pyctr",
"type": "github"
}
},
"root": {
"inputs": {
"hax-nur": "hax-nur",
"nixpkgs": "nixpkgs",
"pyctr": "pyctr"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"hax-nur",
"nixpkgs"
]
},
"locked": {
"lastModified": 1767122417,
"narHash": "sha256-yOt/FTB7oSEKQH9EZMFMeuldK1HGpQs2eAzdS9hNS/o=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "dec15f37015ac2e774c84d0952d57fcdf169b54d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,47 +0,0 @@
{
description = "custominstall";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
pyctr.url = "github:ihaveamac/pyctr/master";
pyctr.inputs.nixpkgs.follows = "nixpkgs";
hax-nur.url = "github:ihaveamac/nur-packages/master";
hax-nur.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
inputs@{
self,
nixpkgs,
pyctr,
hax-nur,
}:
let
systems = [
"x86_64-linux"
"i686-linux"
"x86_64-darwin"
"aarch64-darwin"
"aarch64-linux"
"armv6l-linux"
"armv7l-linux"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
in
{
legacyPackages = forAllSystems (
system:
(import ./default.nix {
pkgs = import nixpkgs { inherit system; };
pyctr = pyctr.packages.${system}.pyctr;
save3ds = hax-nur.packages.${system}.save3ds;
})
// {
default = self.legacyPackages.${system}.custominstall;
}
);
packages = forAllSystems (
system: nixpkgs.lib.filterAttrs (_: v: nixpkgs.lib.isDerivation v) self.legacyPackages.${system}
);
};
}

View File

@@ -1,74 +0,0 @@
{
lib,
pkgs,
python,
callPackage,
buildPythonApplication,
fetchPypi,
pyctr,
pycryptodomex,
pypng,
tkinter,
setuptools,
events,
stdenv,
save3ds,
withGUI ? true,
}:
let
save3ds_no_fuse = save3ds.override { withFUSE = false; };
in
buildPythonApplication rec {
pname = "custominstall";
version = "2.1";
pyproject = true;
src = builtins.path {
path = ./.;
name = "custominstall";
filter =
path: type:
!(builtins.elem (baseNameOf path) [
"build"
"dist"
"localtest"
"__pycache__"
"v"
".git"
"_build"
"custominstall.egg-info"
]);
};
doCheck = false;
build-system = [ setuptools ];
propagatedBuildInputs =
[
pyctr
pycryptodomex
setuptools
events
]
++ lib.optionals (withGUI) [
tkinter
];
makeWrapperArgs = [ "--set CUSTOM_INSTALL_SAVE3DS_PATH ${save3ds_no_fuse}/bin/save3ds_fuse" ];
preFixup = ''
rm -r $out/lib/${python.libPrefix}/site-packages/custominstall/bin
${lib.optionalString (!withGUI) "rm $out/bin/custominstall-gui"}
'';
meta = with lib; {
description = "Installs a title directly to an SD card for the Nintendo 3DS";
homepage = "https://github.com/ihaveamac/custom-install";
license = licenses.mit;
platforms = platforms.unix;
mainProgram = "custominstall";
};
}

View File

@@ -1,48 +0,0 @@
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "custominstall"
description = "Installs a title directly to an SD card for the Nintendo 3DS"
authors = [
{ name = "Ian Burgwin", email = "ian@ianburgwin.net" },
]
readme = "README.md"
license = {text = "MIT"}
dynamic = ["version"]
requires-python = ">= 3.8"
classifiers = [
"Topic :: Utilities",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"pyctr>=0.7.6,<0.9",
"setuptools>=61.0.0",
"events>=0.4",
"comtypes>=1.4.12; os_name == 'nt'",
]
[project.gui-scripts]
custominstall-gui = "custominstall.gui:main"
[project.scripts]
custominstall = "custominstall.__main__:main"
[tool.setuptools.dynamic]
version = {attr = "custominstall.__version__"}
[tool.setuptools.packages]
find = {namespaces = false}
# is it even possible to make these OS-specific with pyproject.toml?
[tool.setuptools.package-data]
custominstall = ["bin/darwin/save3ds_fuse", "bin/win32/save3ds_fuse.exe"]

2
requirements-win32.txt Normal file
View File

@@ -0,0 +1,2 @@
-r requirements.txt
comtypes==1.1.10

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
events==0.4
pyctr>=0.4,<0.7