initial python packaging and nix flake

This commit is contained in:
ihaveahax
2026-01-08 17:42:35 -06:00
parent c61b2bf168
commit 09dbf134f1
14 changed files with 455 additions and 31 deletions

5
.gitignore vendored
View File

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

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

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. # 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). # 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. # You can find the full license text in LICENSE.md in the root of this project.
@@ -10,7 +10,7 @@ from argparse import ArgumentParser
from enum import Enum from enum import Enum
from glob import glob from glob import glob
import gzip import gzip
from os import makedirs, rename, scandir from os import makedirs, rename, scandir, environ
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
@@ -36,6 +36,8 @@ from pyctr.type.ncch import NCCHSection
from pyctr.type.tmd import TitleMetadataError from pyctr.type.tmd import TitleMetadataError
from pyctr.util import roundup from pyctr.util import roundup
from . import __version__
if platform == 'msys': if platform == 'msys':
platform = 'win32' platform = 'win32'
@@ -46,15 +48,21 @@ if is_windows:
else: else:
from os import statvfs from os import statvfs
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
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'
frozen = getattr(sys, 'frozen', False) frozen = getattr(sys, 'frozen', False)
script_dir: str script_dir: str
if frozen: if frozen:
script_dir = dirname(executable) script_dir = dirname(executable)
save3ds_fuse_path = join(script_dir, 'bin', save3ds_fuse_name)
else: else:
script_dir = dirname(__file__) script_dir = dirname(__file__)
save3ds_fuse_path = join(script_dir, 'bin', platform, save3ds_fuse_name)
# missing contents are replaced with 0xFFFFFFFF in the cmd file # missing contents are replaced with 0xFFFFFFFF in the cmd file
CMD_MISSING = b'\xff\xff\xff\xff' CMD_MISSING = b'\xff\xff\xff\xff'
@@ -281,12 +289,6 @@ class CustomInstall:
return isdir(sd_path) return isdir(sd_path)
def start(self): 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 isfile(save3ds_fuse_path):
self.log("Couldn't find " + save3ds_fuse_path, 2) self.log("Couldn't find " + save3ds_fuse_path, 2)
return None, False, 0 return None, False, 0
@@ -692,7 +694,7 @@ class CustomInstall:
return msg_with_type 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 = 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('cia', help='CIA files', nargs='+')
parser.add_argument('-m', '--movable', help='movable.sed file', required=True) parser.add_argument('-m', '--movable', help='movable.sed file', required=True)
@@ -703,7 +705,7 @@ if __name__ == "__main__":
parser.add_argument('--overwrite-saves', help='overwrite existing save files', action='store_true') 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') 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() args = parser.parse_args()
installer = CustomInstall(boot9=args.boot9, installer = CustomInstall(boot9=args.boot9,
@@ -751,3 +753,7 @@ if __name__ == "__main__":
installer.log(f'\n\nWarning: {application_count} installed applications were detected.\n' installer.log(f'\n\nWarning: {application_count} installed applications were detected.\n'
f'The HOME Menu will only show 300 icons.\n' f'The HOME Menu will only show 300 icons.\n'
f'Some applications (not updates or DLC) will need to be deleted.') f'Some applications (not updates or DLC) will need to be deleted.')
if __name__ == "__main__":
main()

View File

@@ -2,7 +2,7 @@
# This file is a part of custom-install.py. # 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). # 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. # 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.cia import CIAError
from pyctr.type.tmd import TitleMetadataError 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
if TYPE_CHECKING: if TYPE_CHECKING:
from os import PathLike from os import PathLike
@@ -497,7 +498,7 @@ class CustomInstallGUI(ttk.Frame):
self.status_label = ttk.Label(self, text='Waiting...') self.status_label = ttk.Label(self, text='Waiting...')
self.status_label.grid(row=5, column=0, sticky=tk.NSEW) 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: if is_windows and not taskbar:
self.log('Note: Could not load taskbar lib.') self.log('Note: Could not load taskbar lib.')
@@ -735,8 +736,13 @@ class CustomInstallGUI(ttk.Frame):
Thread(target=install).start() Thread(target=install).start()
def main():
window = tk.Tk() window = tk.Tk()
window.title(f'custom-install {CI_VERSION}') window.title(f'custom-install {__version__}')
frame = CustomInstallGUI(window) frame = CustomInstallGUI(window)
frame.pack(fill=tk.BOTH, expand=True) frame.pack(fill=tk.BOTH, expand=True)
window.mainloop() 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": 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
}

31
finalize/flake.nix Normal file
View File

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

93
flake.lock generated Normal file
View File

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

47
flake.nix Normal file
View File

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

72
package.nix Normal file
View File

@@ -0,0 +1,72 @@
{
lib,
pkgs,
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 = 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";
};
}

44
pyproject.toml Normal file
View File

@@ -0,0 +1,44 @@
[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}

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