17 Commits

Author SHA1 Message Date
ihaveahax
f81734f293 remove old scripts for pre-package setup 2026-01-11 17:14:28 -06:00
ihaveahax
c81601fec0 format flake.nix 2026-01-11 17:09:49 -06:00
ihaveahax
6c70ee5780 update README for the python packaging update 2026-01-11 16:52:46 -06:00
ihaveahax
cf0fdcd9a1 flake.lock: Update
Flake lock file updates:

• Updated input 'finalize':
    'path:finalize?lastModified=1&narHash=sha256-qQn272f8Z4QhQV2reyd9v0GtypYgnTrOqEiewWnSGJY%3D' (1970-01-01)
  → 'path:finalize?lastModified=1&narHash=sha256-BZgu7%2B/RV9Gy1xo/icz5kd2fKCa3Zow%2BZz6MJWzpgMM%3D' (1970-01-01)
• Updated input 'hax-nur':
    'github:ihaveamac/nur-packages/f612d64a4136c3a4820e37ed50cefb6460dde857' (2026-01-01)
  → 'github:ihaveamac/nur-packages/8ebcd637fd5cd8e673c8e01ed408bf206f9d4f9b' (2026-01-11)
• Updated input 'hax-nur/treefmt-nix':
    'github:numtide/treefmt-nix/dec15f37015ac2e774c84d0952d57fcdf169b54d' (2025-12-30)
  → 'github:numtide/treefmt-nix/0c445aa21b01fd1d4bb58927f7b268568af87b20' (2026-01-10)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/16c7794d0a28b5a37904d55bcca36003b9109aaa' (2026-01-02)
  → 'github:NixOS/nixpkgs/3146c6aa9995e7351a398e17470e15305e6e18ff' (2026-01-10)
2026-01-11 16:45:19 -06:00
ihaveahax
9fba2ff88f finalize/flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/16c7794d0a28b5a37904d55bcca36003b9109aaa' (2026-01-02)
  → 'github:NixOS/nixpkgs/3146c6aa9995e7351a398e17470e15305e6e18ff' (2026-01-10)
2026-01-11 16:45:12 -06:00
ihaveahax
4d8a6de163 include custom-install-finalize.3dsx in package folder, add nix app to build and copy a new version 2026-01-11 16:44:59 -06:00
ihaveahax
69fc8bb39a finalize: add default outputs to flake 2026-01-11 16:32:46 -06:00
ihaveahax
a395c22aee actually include TaskbarLib.tlb and title.db.gz in package 2026-01-11 16:27:44 -06:00
ihaveahax
baf7490de0 update gitignore 2026-01-11 16:27:24 -06:00
ihaveahax
4b41703107 include TaskbarLib.tlb in package 2026-01-11 16:27:02 -06:00
ihaveahax
927ab5c669 add note if custom-install-finalize was not copied, print all messages to stdout in gui 2026-01-11 16:26:17 -06:00
ihaveahax
d656b1793c fix misplaced frozen variable setting 2026-01-08 18:31:01 -06:00
ihaveahax
50a7117aa9 move bin and title.db.gx to inside custominstall folder 2026-01-08 18:30:04 -06:00
ihaveahax
a0234e9b53 try to search for save3ds_fuse in PATH, if not found locally 2026-01-08 18:04:03 -06:00
ihaveahax
ffcf536d58 gui: search boot9strap folder too for input files (fixes #83) 2026-01-08 17:50:02 -06:00
ihaveahax
17aebb3256 fix corruption issue by moving corrupted files after closing them (fixes #86) 2026-01-08 17:47:18 -06:00
ihaveahax
09dbf134f1 initial python packaging and nix flake 2026-01-08 17:42:35 -06:00
27 changed files with 627 additions and 112 deletions

7
.gitignore vendored
View File

@@ -2,6 +2,7 @@
bin/linux/save3ds_fuse
cstins/
testing-class.py
*.local
# macOS
.DS_Store
@@ -11,12 +12,16 @@ testing-class.py
venv/
**/__pycache__/
*.pyc
*.egg-info/
*.whl
# JetBrains
.idea/
=======
*.pyc
/build/
/dist/
/custom-install-finalize.3dsx
result
result-*

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2019-2021 Ian Burgwin
Copyright (c) 2019 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

4
MANIFEST.in Normal file
View File

@@ -0,0 +1,4 @@
recursive-include custominstall/bin/*
include custominstall/title.db.gz
include custominstall/TaskbarLib.tlb
include custominstall/custom-install-finalize.3dsx

View File

@@ -12,19 +12,28 @@ Installs a title directly to an SD card for the Nintendo 3DS. Originally created
3. Extract and run ci-gui. Read `windows-quickstart.txt`.
### With installed Python
Note for Windows users: Enabling "Add Python 3.X to PATH" is **NOT** required! Python is installed with the `py` launcher by default.
> [!NOTE]
> 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. Download the repo ([zip link](https://github.com/ihaveamac/custom-install/archive/safe-install.zip) or `git clone`)
3. Install the packages:
* Windows: Double-click `windows-install-dependencies.py`
* Alternate manual method: `py -3 -m pip install --user -r requirements-win32.txt`
* macOS/Linux: `python3 -m pip install --user -r requirements.txt`
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.
2. Install the packages:
* Windows: `py -3 -m pip install --user --upgrade https://github.com/ihaveamac/custom-install/archive/refs/heads/python-package.zip`
* macOS/Linux: `python3 -m pip install --user --upgrade https://github.com/ihaveamac/custom-install/archive/refs/heads/python-package.zip`
To run the GUI:
* Windows: `py -3 -m custominstall.gui`
* macOS/Linux: `python3 -m custominstall.gui`
To run the command line version:
* Windows: `py -3 -m custominstall`
* macOS/Linux: `python3 -m custominstall`
## Setup
Linux users must build [wwylele/save3ds](https://github.com/wwylele/save3ds) and place `save3ds_fuse` in `bin/linux`. Install [rust using rustup](https://www.rust-lang.org/tools/install), then compile with: `cargo build --release --no-default-features`. The compiled binary is located in `target/release/save3ds_fuse`, copy it to `bin/linux`.
Linux users must build [wwylele/save3ds](https://github.com/wwylele/save3ds) and place `save3ds_fuse` in one of these places:
* A directory in `PATH`
* In `custominstall/bin/linux`
* Set the environment variable `CUSTOM_INSTALL_SAVE3DS_PATH` to the `save3ds_fuse` binary
movable.sed is required and can be provided with `-m` or `--movable`.
@@ -55,9 +64,9 @@ Use `-h` to view arguments.
Examples:
```
py -3 custominstall.py -b boot9.bin -m movable.sed --sd E:\ file.cia file2.cia
python3 custominstall.py -b boot9.bin -m movable.sed --sd /Volumes/GM9SD file.cia file2.cia
python3 custominstall.py -b boot9.bin -m movable.sed --sd /media/GM9SD file.cia file2.cia
py -3 -m custominstall -b boot9.bin -m movable.sed --sd E:\ file.cia file2.cia
python3 -m custominstall -b boot9.bin -m movable.sed --sd /Volumes/GM9SD file.cia file2.cia
python3 -m custominstall -b boot9.bin -m movable.sed --sd /media/GM9SD file.cia file2.cia
```
## GUI
@@ -66,26 +75,14 @@ A GUI is provided to make the process easier.
### GUI Setup
Linux users may need to install a Tk package:
- Ubuntu/Debian: `sudo apt install python3-tk`
- Manjaro/Arch: `sudo pacman -S tk`
Install the requirements listed in "Summary", then run `ci-gui.py`.
- Arch: `sudo pacman -S tk`
## Development
### Building Windows standalone
Using a 32-bit version of Python is recommended to build a version to be distributed.
A [virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/#creating-a-virtual-environment) is recommended to isolate the packages from system directories. The build script `make-standalone.bat` assumes that the dependencies are in PATH.
Install the dependencies, plus cx-Freeze. In a virtual environment, the specific Python version doesn't need to be requested.
```batch
pip install cx-freeze -r requirements-win32.txt
```
Copy `custom-install-finalize.3dsx` to the project root, this will be copied to the build directory and included in the final archive.
Run `make-standalone.bat`. This will run cxfreeze and make a standalone version at `dist\custom-install-standalone.zip`
> [!WARNING]
> This section is OUTDATED and currently does not work with the Python package setup.
## License/Credits
[save3ds by wwylele](https://github.com/wwylele/save3ds) is used to interact with the Title Database (details in `bin/README`).

10
custominstall/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
# 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

@@ -2,7 +2,7 @@
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019-2020 Ian Burgwin
# custom-install is 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.
@@ -10,12 +10,12 @@ from argparse import ArgumentParser
from enum import Enum
from glob import glob
import gzip
from os import makedirs, rename, scandir
from os import makedirs, rename, scandir, environ
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
from shutil import copyfile, copy2, rmtree, which
import sys
from sys import platform, executable
from tempfile import TemporaryDirectory
@@ -36,6 +36,8 @@ 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'
@@ -46,16 +48,28 @@ if is_windows:
else:
from os import statvfs
CI_VERSION = '2.1'
# used to run the save3ds_fuse binary next to the script
frozen = getattr(sys, 'frozen', False)
script_dir: str
frozen = getattr(sys, 'frozen', False)
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'
@@ -281,13 +295,7 @@ class CustomInstall:
return isdir(sd_path)
def start(self):
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):
if not (save3ds_fuse_path and isfile(save3ds_fuse_path)):
self.log("Couldn't find " + save3ds_fuse_path, 2)
return None, False, 0
@@ -692,7 +700,7 @@ class CustomInstall:
return msg_with_type
if __name__ == "__main__":
def 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)
@@ -703,7 +711,7 @@ if __name__ == "__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 {CI_VERSION} - https://github.com/ihaveamac/custom-install')
print(f'custom-install {__version__} - https://github.com/ihaveamac/custom-install')
args = parser.parse_args()
installer = CustomInstall(boot9=args.boot9,
@@ -751,3 +759,7 @@ if __name__ == "__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()

Binary file not shown.

View File

@@ -2,7 +2,7 @@
# This file is a part of custom-install.py.
#
# custom-install is copyright (c) 2019-2020 Ian Burgwin
# custom-install is 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.
@@ -25,7 +25,8 @@ from pyctr.type.cdn import CDNError
from pyctr.type.cia import CIAError
from pyctr.type.tmd import TitleMetadataError
from custominstall import CustomInstall, CI_VERSION, load_cifinish, InvalidCIFinishError, InstallStatus
from . import __version__
from .__main__ import CustomInstall, load_cifinish, InvalidCIFinishError, InstallStatus, save3ds_fuse_path
if TYPE_CHECKING:
from os import PathLike
@@ -229,8 +230,12 @@ class InstallResults(tk.Toplevel):
else:
message = 'Nothing was installed.'
if install_state['installed'] and copied_3dsx:
if install_state['installed']:
if copied_3dsx:
message += '\n\ncustom-install-finalize has been copied to the SD card.'
else:
message += ('\n\nNote: custom-install-finalize was not copied.\n'
'You can either manually copy the 3dsx to your SD card, or use GodMode9 to finish the install.')
if application_count >= 300:
message += (f'\n\nWarning: {application_count} installed applications were detected.\n'
@@ -335,7 +340,9 @@ 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, filename)])
sd_msed_path = find_first_file(
[join(f, "gm9", "out", filename), join(f, "boot9strap", filename), join(f, filename)]
)
if sd_msed_path:
self.log('Found ' + filename + ' on SD card at ' + sd_msed_path)
if filename.endswith('bin'):
@@ -497,7 +504,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 {CI_VERSION} - https://github.com/ihaveamac/custom-install', status=False)
self.log(f'custom-install {__version__} - https://github.com/ihaveamac/custom-install', status=False)
if is_windows and not taskbar:
self.log('Note: Could not load taskbar lib.')
@@ -598,6 +605,8 @@ class CustomInstallGUI(ttk.Frame):
if status:
self.status_label.config(text=line)
print(log_msg)
def show_error(self, message):
mb.showerror('Error', message, parent=self.parent)
@@ -735,8 +744,17 @@ class CustomInstallGUI(ttk.Frame):
Thread(target=install).start()
window = tk.Tk()
window.title(f'custom-install {CI_VERSION}')
frame = CustomInstallGUI(window)
frame.pack(fill=tk.BOTH, expand=True)
window.mainloop()
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()

34
default.nix Normal file
View File

@@ -0,0 +1,34 @@
{
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 Normal file
View File

@@ -0,0 +1,82 @@
{
"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": 1768032153,
"narHash": "sha256-6kD1MdY9fsE6FgSwdnx29hdH2UcBKs3/+JJleMShuJg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3146c6aa9995e7351a398e17470e15305e6e18ff",
"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
}

33
finalize/flake.nix Normal file
View File

@@ -0,0 +1,33 @@
{
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;
default = 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;
default = custom-install-finalize;
};
};
}

167
flake.lock generated Normal file
View File

@@ -0,0 +1,167 @@
{
"nodes": {
"devkitNix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"finalize",
"nixpkgs"
]
},
"locked": {
"lastModified": 1766539742,
"narHash": "sha256-F6OeM2LrLo2n+Xg5XU4udQR/vuWWrDMKxXRzNXE2ClQ=",
"owner": "bandithedoge",
"repo": "devkitNix",
"rev": "c97f9880737716085e78009cba6bf85ad104628b",
"type": "github"
},
"original": {
"owner": "bandithedoge",
"repo": "devkitNix",
"type": "github"
}
},
"finalize": {
"inputs": {
"devkitNix": "devkitNix",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1,
"narHash": "sha256-BZgu7+/RV9Gy1xo/icz5kd2fKCa3Zow+Zz6MJWzpgMM=",
"path": "finalize",
"type": "path"
},
"original": {
"path": "finalize",
"type": "path"
}
},
"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"
}
},
"hax-nur": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1768151313,
"narHash": "sha256-qcMLsdACTlFHltziBAsS1r09cVZyp5fUR16//mIhLIs=",
"owner": "ihaveamac",
"repo": "nur-packages",
"rev": "8ebcd637fd5cd8e673c8e01ed408bf206f9d4f9b",
"type": "github"
},
"original": {
"owner": "ihaveamac",
"ref": "master",
"repo": "nur-packages",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1768032153,
"narHash": "sha256-6kD1MdY9fsE6FgSwdnx29hdH2UcBKs3/+JJleMShuJg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3146c6aa9995e7351a398e17470e15305e6e18ff",
"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": {
"finalize": "finalize",
"hax-nur": "hax-nur",
"nixpkgs": "nixpkgs",
"pyctr": "pyctr"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"hax-nur",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768031762,
"narHash": "sha256-b2gJDJfi+TbA7Hu2sKip+1mWqya0GJaWrrXQjpbOVTU=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "0c445aa21b01fd1d4bb58927f7b268568af87b20",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

80
flake.nix Normal file
View File

@@ -0,0 +1,80 @@
{
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";
finalize.url = "path:finalize";
finalize.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
inputs@{
self,
nixpkgs,
pyctr,
hax-nur,
finalize,
}:
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}
);
apps = forAllSystems (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
gui = {
type = "app";
program = "${self.packages.${system}.custominstall}/bin/custominstall-gui";
};
}
// (
if system == "x86_64-linux" then
# this only works on x86_64-linux due to devkitNix only working there
{
update-finalize = {
type = "app";
program =
(pkgs.writeShellScript "update-finalize" ''
set -x
finalize=${inputs.finalize.packages.${system}.custom-install-finalize}/custom-install-finalize.3dsx
cp --no-preserve=mode,ownership,timestamps $finalize custominstall/custom-install-finalize.3dsx
'').outPath;
};
}
else
{ }
)
);
};
}

View File

@@ -1,13 +0,0 @@
mkdir build
mkdir dist
python setup-cxfreeze.py build_exe --build-exe=build\custom-install-standalone
mkdir build\custom-install-standalone\bin
copy TaskbarLib.tlb build\custom-install-standalone
copy bin\win32\save3ds_fuse.exe build\custom-install-standalone\bin
copy bin\README build\custom-install-standalone\bin
copy custom-install-finalize.3dsx build\custom-install-standalone
copy title.db.gz 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
python -m zipfile -c dist\custom-install-standalone.zip build\custom-install-standalone

74
package.nix Normal file
View File

@@ -0,0 +1,74 @@
{
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";
};
}

48
pyproject.toml Normal file
View File

@@ -0,0 +1,48 @@
[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", "TaskbarLib.tlb", "title.db.gz", "custom-install-finalize.3dsx"]

View File

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

View File

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

View File

@@ -1,19 +0,0 @@
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
)

View File

@@ -1,13 +0,0 @@
# This is meant to be double-clicked from File Explorer.
# This doesn't import pip as a module in case the way it's executed changes, which it has in the past.
# Instead we call it like we would in the command line.
from subprocess import run
from os.path import dirname, join
from sys import executable
root_dir = dirname(__file__)
run([executable, '-m', 'pip', 'install', '--user', '-r', join(root_dir, 'requirements-win32.txt')])
input('Press enter to close')