From 09dbf134f1ad2ab462dd0f8c2457838f37ca085c Mon Sep 17 00:00:00 2001 From: ihaveahax Date: Thu, 8 Jan 2026 17:42:35 -0600 Subject: [PATCH] initial python packaging and nix flake --- .gitignore | 5 +- LICENSE.md | 2 +- custominstall/__init__.py | 10 ++ custominstall.py => custominstall/__main__.py | 40 ++++---- ci-gui.py => custominstall/gui.py | 22 +++-- default.nix | 34 +++++++ finalize/flake.lock | 82 ++++++++++++++++ finalize/flake.nix | 31 +++++++ flake.lock | 93 +++++++++++++++++++ flake.nix | 47 ++++++++++ package.nix | 72 ++++++++++++++ pyproject.toml | 44 +++++++++ requirements-win32.txt | 2 - requirements.txt | 2 - 14 files changed, 455 insertions(+), 31 deletions(-) create mode 100644 custominstall/__init__.py rename custominstall.py => custominstall/__main__.py (97%) rename ci-gui.py => custominstall/gui.py (98%) create mode 100644 default.nix create mode 100644 finalize/flake.lock create mode 100644 finalize/flake.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 package.nix create mode 100644 pyproject.toml delete mode 100644 requirements-win32.txt delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 183e378..643072c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,12 +11,15 @@ testing-class.py venv/ **/__pycache__/ *.pyc +*.egg-info/ # JetBrains .idea/ ======= -*.pyc /build/ /dist/ /custom-install-finalize.3dsx + +result +result-* diff --git a/LICENSE.md b/LICENSE.md index 649e4fe..80948cd 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -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 diff --git a/custominstall/__init__.py b/custominstall/__init__.py new file mode 100644 index 0000000..6e612c8 --- /dev/null +++ b/custominstall/__init__.py @@ -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' diff --git a/custominstall.py b/custominstall/__main__.py similarity index 97% rename from custominstall.py rename to custominstall/__main__.py index 5580c18..67816b3 100644 --- a/custominstall.py +++ b/custominstall/__main__.py @@ -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,7 +10,7 @@ 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 @@ -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,15 +48,21 @@ 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 -if frozen: - script_dir = dirname(executable) +if 'CUSTOM_INSTALL_SAVE3DS_PATH' in environ: + save3ds_fuse_path = environ['CUSTOM_INSTALL_SAVE3DS_PATH'] else: - script_dir = dirname(__file__) + save3ds_fuse_name = 'save3ds_fuse' + if is_windows: + save3ds_fuse_name += '.exe' + frozen = getattr(sys, 'frozen', False) + script_dir: str + if frozen: + script_dir = dirname(executable) + save3ds_fuse_path = join(script_dir, 'bin', save3ds_fuse_name) + else: + 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 CMD_MISSING = b'\xff\xff\xff\xff' @@ -281,12 +289,6 @@ 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): self.log("Couldn't find " + save3ds_fuse_path, 2) return None, False, 0 @@ -692,7 +694,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 +705,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 +753,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() diff --git a/ci-gui.py b/custominstall/gui.py similarity index 98% rename from ci-gui.py rename to custominstall/gui.py index 8b82fc3..b09a842 100644 --- a/ci-gui.py +++ b/custominstall/gui.py @@ -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 if TYPE_CHECKING: from os import PathLike @@ -497,7 +498,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.') @@ -735,8 +736,13 @@ 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(): + 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() diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..a20c846 --- /dev/null +++ b/default.nix @@ -0,0 +1,34 @@ +{ + pkgs ? import { }, + # 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; + }; +} diff --git a/finalize/flake.lock b/finalize/flake.lock new file mode 100644 index 0000000..5df65be --- /dev/null +++ b/finalize/flake.lock @@ -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 +} diff --git a/finalize/flake.nix b/finalize/flake.nix new file mode 100644 index 0000000..99b3e2f --- /dev/null +++ b/finalize/flake.nix @@ -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; + }; + }; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1b116ed --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..58e4d63 --- /dev/null +++ b/flake.nix @@ -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} + ); + }; +} diff --git a/package.nix b/package.nix new file mode 100644 index 0000000..5cabbc5 --- /dev/null +++ b/package.nix @@ -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"; + }; +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a468b99 --- /dev/null +++ b/pyproject.toml @@ -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} diff --git a/requirements-win32.txt b/requirements-win32.txt deleted file mode 100644 index 5a2e5c5..0000000 --- a/requirements-win32.txt +++ /dev/null @@ -1,2 +0,0 @@ --r requirements.txt -comtypes==1.1.10 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0ff0bb9..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -events==0.4 -pyctr>=0.4,<0.7