Compare commits

...

13 Commits

  1. 8
      BUILDING.md
  2. 2
      README.md
  3. 2
      ninfs/__init__.py
  4. 23
      ninfs/gui/__init__.py
  5. 2
      ninfs/gui/about.py
  6. 46
      ninfs/gui/wizardcontainer.py
  7. 28
      ninfs/mount/_common.py
  8. 9
      ninfs/mount/exefs.py
  9. 1
      requirements.txt
  10. 14
      scripts/make-dmg-mac.sh
  11. 13
      scripts/make-icons.sh
  12. 12
      scripts/make-zip-win.bat
  13. 8
      setup-cxfreeze.py
  14. 2
      setup.py
  15. 12
      standalone.spec

8
BUILDING.md

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
This is still being worked on (as of October 25, 2020).
This is still being worked on (as of January 26, 2022).
# Windows
@ -7,7 +7,7 @@ This expects Python 3.8 32-bit to be installed. @@ -7,7 +7,7 @@ This expects Python 3.8 32-bit to be installed.
Install the dependencies:
```batch
py -3.8-32 -m pip install --user cx-Freeze==6.6 -r requirements.txt
py -3.8-32 -m pip install --user --upgrade cx-Freeze==6.10 -r requirements.txt
```
Build the exe:
@ -30,13 +30,13 @@ scripts\make-inst-win.bat @@ -30,13 +30,13 @@ scripts\make-inst-win.bat
* `py -3 setup.py sdist` - build source distribution
# macOS
This needs Python built with universal2 to produce a build with a working GUI. Despite that it currently doesn't produce a universal build (https://github.com/pyinstaller/pyinstaller/issues/5315).
This needs Python built with universal2 to produce a build with a working GUI. A universal2 build will be made.
Set up a venv, activate it, and install the requirements:
```sh
python3.9 -m venv venv39
source venv39/bin/activate
pip install pyinstaller certifi -r requirements.txt
pip install --upgrade pyinstaller certifi -r requirements.txt
```
Build the icns:

2
README.md

@ -76,7 +76,7 @@ No standalone build is available at the moment. @@ -76,7 +76,7 @@ No standalone build is available at the moment.
#### Install with existing Python
* Install the latest version of Python 3. The recommended way is [Homebrew](https://brew.sh). You can also use an installer from [python.org](https://www.python.org/downloads/) or a tool like [pyenv](https://github.com/pyenv/pyenv).
* Install the latest version of [FUSE for macOS](https://github.com/osxfuse/osxfuse/releases/latest).
* Install the latest version of [macFUSE](https://github.com/osxfuse/osxfuse/releases/latest).
* Install ninfs with `python3 -m pip install --upgrade https://github.com/ihaveamac/ninfs/archive/2.0.zip`
### Linux

2
ninfs/__init__.py

@ -7,4 +7,4 @@ @@ -7,4 +7,4 @@
__author__ = 'ihaveamac'
__copyright__ = 'Copyright (c) 2017-2021 Ian Burgwin'
__license__ = 'MIT'
__version__ = '2.0a6'
__version__ = '2.0a7'

23
ninfs/gui/__init__.py

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
# Copyright (c) 2017-2021 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.
import os
import sys
import tkinter as tk
import tkinter.ttk as ttk
@ -155,8 +155,8 @@ class NinfsGUI(tk.Tk): @@ -155,8 +155,8 @@ class NinfsGUI(tk.Tk):
webbrowser.open('http://www.secfs.net/winfsp/rel/')
elif is_mac:
res = mb.askyesno('Failed to load libfuse',
'Failed to load libfuse. FUSE for macOS needs to be installed.\n\n'
'Would you like to open the FUSE for macOS download page?\n'
'Failed to load libfuse. macFUSE needs to be installed.\n\n'
'Would you like to open the macFUSE download page?\n'
'https://osxfuse.github.io')
if res:
webbrowser.open('https://osxfuse.github.io')
@ -204,8 +204,21 @@ class NinfsGUI(tk.Tk): @@ -204,8 +204,21 @@ class NinfsGUI(tk.Tk):
wizard_window.change_frame(WizardTypeSelector)
wizard_window.focus()
def mount(self, mounttype: 'str', cmdargs: 'List[str]', mountpoint: str, callback_success: 'Callable',
callback_failed: 'Callable'):
def mount(self, mounttype: str, cmdargs: 'List[str]', mountpoint: str, callback_success: 'Callable',
callback_failed: 'Callable', is_drive_letter: bool):
if is_windows:
if not is_drive_letter:
# the directory must first be deleted before winfsp can mount to it
try:
os.rmdir(mountpoint)
except FileNotFoundError:
pass
except Exception:
exc_list = ['Failed to delete directory:', mountpoint, '']
exc_list.extend(format_exc().splitlines())
callback_failed(None, exc_list)
return
args = [executable]
if not frozen:
args.append(dirname(dirname(__file__)))

2
ninfs/gui/about.py

@ -85,7 +85,7 @@ class NinfsAbout(tk.Toplevel): @@ -85,7 +85,7 @@ class NinfsAbout(tk.Toplevel):
'https://github.com/Legrandin/pycryptodome', 'PyCryptodome - multiple licenses'),
(f'pyctr {pyctr_version}', 'pyctr', 'https://github.com/ihaveamac/pyctr',
'pyctr - Copyright (c) 2017-2021 Ian Burgwin'),
('haccrypto 0.1.1', 'haccrypto.md', 'https://github.com/luigoalma/haccrypto',
('haccrypto 0.1.2', 'haccrypto.md', 'https://github.com/luigoalma/haccrypto',
'haccrypto - Copyright (c) 2017-2021 Ian Burgwin & Copyright (c) 2020-2021 Luis Marques')
]

46
ninfs/gui/wizardcontainer.py

@ -3,12 +3,13 @@ @@ -3,12 +3,13 @@
# Copyright (c) 2017-2021 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.
import os
from datetime import datetime
from sys import platform
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.filedialog as fd
import tkinter.messagebox as mb
from typing import TYPE_CHECKING
import mountinfo
@ -18,7 +19,7 @@ from .outputviewer import OutputViewer @@ -18,7 +19,7 @@ from .outputviewer import OutputViewer
from .setupwizard import *
if TYPE_CHECKING:
from typing import List, Type
from typing import List, Optional, Type
from . import NinfsGUI
wizard_bases = {
@ -156,7 +157,10 @@ class WizardMountPointSelector(WizardBase): @@ -156,7 +157,10 @@ class WizardMountPointSelector(WizardBase):
self.adv_options = {'user_access': 'none'}
if platform == 'win32':
# special case for nand types here, which should not be mounted to a drive letter
if platform == 'win32' and not cmdargs[0].startswith('nand'):
self.is_drive_letter = True
drive_letters = [x + ':' for x in get_unused_drives()]
container, drive_selector, drive_selector_var = self.make_option_menu('Select the drive letter to use:',
@ -168,6 +172,8 @@ class WizardMountPointSelector(WizardBase): @@ -168,6 +172,8 @@ class WizardMountPointSelector(WizardBase):
self.wizardcontainer.set_next_enabled(True)
else:
self.is_drive_letter = False
def callback(*_):
mount_point = self.mount_point_var.get().strip()
self.wizardcontainer.set_next_enabled(mount_point)
@ -176,8 +182,10 @@ class WizardMountPointSelector(WizardBase): @@ -176,8 +182,10 @@ class WizardMountPointSelector(WizardBase):
container, mount_textbox, mount_textbox_var = self.make_directory_picker(labeltext, 'Select mountpoint')
container.pack(fill=tk.X, expand=True)
adv_options_button = ttk.Button(self, text='Advanced mount options', command=self.show_advanced_options)
adv_options_button.pack(fill=tk.X, expand=True)
# no advanced options needed on windows yet
if platform != 'win32':
adv_options_button = ttk.Button(self, text='Advanced mount options', command=self.show_advanced_options)
adv_options_button.pack(fill=tk.X, expand=True)
mount_textbox_var.trace_add('write', callback)
@ -190,15 +198,20 @@ class WizardMountPointSelector(WizardBase): @@ -190,15 +198,20 @@ class WizardMountPointSelector(WizardBase):
self.adv_options.update(adv_options_window.get_options())
def next_pressed(self):
if platform == 'win32' and not self.is_drive_letter:
if len(os.listdir(self.mount_point_var.get())) != 0:
mb.showerror('ninfs', 'Directory must be empty.')
return
extra_args = []
if self.adv_options['user_access'] != 'none':
extra_args.extend(('-o', self.adv_options['user_access']))
self.wizardcontainer.mount(self.mounttype, self.cmdargs + extra_args, self.mount_point_var.get())
self.wizardcontainer.mount(self.mounttype, self.cmdargs + extra_args, self.mount_point_var.get(),
self.is_drive_letter)
class WizardMountStep(WizardBase):
def __init__(self, parent: 'tk.BaseWidget' = None, *, wizardcontainer: 'WizardContainer', mounttype: 'str',
cmdargs: 'List[str]', mountpoint: 'str'):
def __init__(self, parent: 'tk.BaseWidget' = None, *, wizardcontainer: 'WizardContainer', mounttype: str,
cmdargs: 'List[str]', mountpoint: str, is_drive_letter: bool):
super().__init__(parent, wizardcontainer=wizardcontainer)
self.wizardcontainer.set_cancel_enabled(False)
@ -208,7 +221,8 @@ class WizardMountStep(WizardBase): @@ -208,7 +221,8 @@ class WizardMountStep(WizardBase):
label = ttk.Label(self, text='Starting mount process...')
label.pack(fill=tk.X, expand=True)
self.wizardcontainer.parent.mount(mounttype, cmdargs, mountpoint, self.callback_success, self.callback_failed)
self.wizardcontainer.parent.mount(mounttype, cmdargs, mountpoint, self.callback_success, self.callback_failed,
is_drive_letter=is_drive_letter)
def callback_success(self):
opened = open_directory(self.mountpoint)
@ -222,12 +236,13 @@ class WizardMountStep(WizardBase): @@ -222,12 +236,13 @@ class WizardMountStep(WizardBase):
class WizardFailedMount(WizardBase):
def __init__(self, parent: 'tk.BaseWidget' = None, *, wizardcontainer: 'WizardContainer', returncode: int,
output: 'List[str]', kind: str):
def __init__(self, parent: 'tk.BaseWidget' = None, *, wizardcontainer: 'WizardContainer',
returncode: 'Optional[int]', output: 'List[str]', kind: str):
super().__init__(parent, wizardcontainer=wizardcontainer)
output.append('')
output.append(f'Return code was {returncode}')
if returncode:
output.append('')
output.append(f'Return code was {returncode}')
self.returncode = returncode
self.output = output
@ -322,8 +337,9 @@ class WizardContainer(tk.Toplevel): @@ -322,8 +337,9 @@ class WizardContainer(tk.Toplevel):
self.next_button.configure(text='Mount')
self.change_frame(WizardMountPointSelector, cmdargs=cmdargs, mounttype=mounttype)
def mount(self, mounttype: 'str', cmdargs: 'List[str]', mountpoint: str):
self.change_frame(WizardMountStep, mounttype=mounttype, cmdargs=cmdargs, mountpoint=mountpoint)
def mount(self, mounttype: 'str', cmdargs: 'List[str]', mountpoint: str, is_drive_letter: bool):
self.change_frame(WizardMountStep, mounttype=mounttype, cmdargs=cmdargs, mountpoint=mountpoint,
is_drive_letter=is_drive_letter)
def change_frame(self, target: 'Type[WizardBase]', *args, **kwargs):
if self.current_frame:

28
ninfs/mount/_common.py

@ -333,13 +333,12 @@ class SplitFileHandler(BufferedIOBase): @@ -333,13 +333,12 @@ class SplitFileHandler(BufferedIOBase):
class RawDeviceHandler(BufferedIOBase):
"""Handler for easier IO access with raw devices by aligning reads and writes to the sector size."""
"""(NYI) Handler for easier IO access with raw devices by aligning reads and writes to the sector size."""
_seek = 0
def __init__(self, fh: 'BinaryIO', mode: str = 'rb+', sector_size: int = 0x200):
def __init__(self, fh: 'BinaryIO', sector_size: int = 0x200):
self._fh = fh
self.mode = mode
self._sector_size = sector_size
@_raise_if_closed
@ -352,7 +351,8 @@ class RawDeviceHandler(BufferedIOBase): @@ -352,7 +351,8 @@ class RawDeviceHandler(BufferedIOBase):
self._seek = max(self._seek + seek, 0)
elif whence == 2:
# this doesn't work...
raise Exception
# maybe if the size is known, this could be based off that instead?
raise NotImplementedError("can't seek from the ending on a block device")
return self._seek
@_raise_if_closed
@ -361,12 +361,26 @@ class RawDeviceHandler(BufferedIOBase): @@ -361,12 +361,26 @@ class RawDeviceHandler(BufferedIOBase):
@_raise_if_closed
def readable(self) -> bool:
return True
return self._fh.readable()
@_raise_if_closed
def writable(self) -> bool:
return True
return self._fh.writable()
@_raise_if_closed
def seekable(self) -> bool:
return True
return self._fh.seekable()
@_raise_if_closed
def read(self, size: int = -1) -> bytes:
if size == -1:
raise NotImplementedError("can't read without a specified size")
before = self._seek % self._sector_size
after = 0
total = before + size
if total % self._sector_size:
after = self._sector_size - (total % self._sector_size)
total += after
self._seek += size

9
ninfs/mount/exefs.py

@ -10,12 +10,14 @@ Mounts Executable Filesystem (ExeFS) files, creating a virtual filesystem of the @@ -10,12 +10,14 @@ Mounts Executable Filesystem (ExeFS) files, creating a virtual filesystem of the
import logging
from errno import ENOENT
from itertools import chain
from io import BytesIO
from stat import S_IFDIR, S_IFREG
from sys import argv
from threading import Lock
from typing import TYPE_CHECKING
import png
from pyctr.type.exefs import ExeFSReader, ExeFSFileNotFoundError, CodeDecompressionError
from pyctr.type.smdh import SMDH, InvalidSMDHError
@ -77,8 +79,11 @@ class ExeFSMount(LoggingMixIn, Operations): @@ -77,8 +79,11 @@ class ExeFSMount(LoggingMixIn, Operations):
icon_small = BytesIO()
icon_large = BytesIO()
self.reader.icon.icon_small.save(icon_small, 'png')
self.reader.icon.icon_large.save(icon_large, 'png')
def load_to_pypng(array, w, h):
return png.from_array((chain.from_iterable(x) for x in array), 'RGB', {'width': w, 'height': h})
load_to_pypng(self.reader.icon.icon_small_array, 24, 24).write(icon_small)
load_to_pypng(self.reader.icon.icon_large_array, 48, 48).write(icon_large)
icon_small_size = icon_small.seek(0, 2)
icon_large_size = icon_large.seek(0, 2)

1
requirements.txt

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
pyctr>=0.5.1,<0.7
haccrypto>=0.1
pycryptodomex>=3.9,<4
pypng>=0.0.21

14
scripts/make-dmg-mac.sh

@ -1,14 +1,14 @@ @@ -1,14 +1,14 @@
#!/bin/sh
NV=`python3 -c 'import ninfs.__init__ as i; print(i.__version__)'`
NV=$(python3 -c 'import ninfs.__init__ as i; print(i.__version__)')
DMGDIR=build/dmg/ninfs-$NV
rm -rf $DMGDIR
rm -rf "$DMGDIR"
set -e -u
mkdir -p $DMGDIR
mkdir -p "$DMGDIR"
cp -rpc dist/ninfs.app $DMGDIR/ninfs.app
ln -s /Applications $DMGDIR/Applications
cp resources/MacGettingStarted.pdf $DMGDIR/Getting\ Started.pdf
cp -rpc dist/ninfs.app "$DMGDIR/ninfs.app"
ln -s /Applications "$DMGDIR/Applications"
cp resources/MacGettingStarted.pdf "$DMGDIR/Getting Started.pdf"
hdiutil create -format UDZO -srcfolder $DMGDIR -fs HFS+ dist/ninfs-$NV-macos.dmg -ov
hdiutil create -format UDZO -srcfolder "$DMGDIR" -fs HFS+ "dist/ninfs-$NV-macos.dmg" -ov

13
scripts/make-icons.sh

@ -1,7 +1,14 @@ @@ -1,7 +1,14 @@
#!/bin/sh
if [[ `uname -s` = Darwin ]]; then
mkdir build
rm -r build/ninfs.iconset > /dev/null
# check for imagemagick
if ! convert > /dev/null 2>&1; then
echo "convert not found, please install ImageMagick"
exit
fi
if [ "$(uname -s)" = Darwin ]; then
mkdir build 2> /dev/null
rm -r build/ninfs.iconset 2> /dev/null
mkdir build/ninfs.iconset
cp ninfs/gui/data/16x16.png build/ninfs.iconset/icon_16x16.png

12
scripts/make-zip-win.bat

@ -4,11 +4,13 @@ set OUTDIR=build\zipbuild\ninfs-%VERSION% @@ -4,11 +4,13 @@ set OUTDIR=build\zipbuild\ninfs-%VERSION%
mkdir dist
rmdir /s /q build\zipbuild
mkdir %OUTDIR% || exit /b
mkdir build
mkdir build\zipbuild
mkdir %OUTDIR%
copy LICENSE.md %OUTDIR% || exit /b
copy README.md %OUTDIR% || exit /b
copy LICENSE.md %OUTDIR%
copy README.md %OUTDIR%
xcopy /s /e /i /y build\exe.win32-3.8 %OUTDIR% || exit /b
xcopy /s /e /i /y build\exe.win32-3.8 %OUTDIR%
py -m zipfile -c dist\ninfs-%VERSION%-win32.zip %OUTDIR% || exit /b
py -m zipfile -c dist\ninfs-%VERSION%-win32.zip %OUTDIR%

8
setup-cxfreeze.py

@ -38,15 +38,9 @@ if sys.platform == 'win32': @@ -38,15 +38,9 @@ if sys.platform == 'win32':
executables.append(Executable('ninfs/winpathmodify.py',
target_name='winpathmodify'))
# based on https://github.com/Legrandin/pycryptodome/blob/b3a394d0837ff92919d35d01de9952b8809e802d/setup.py
with open('ninfs/__init__.py', 'r', encoding='utf-8') as f:
for line in f:
if line.startswith('__version__'):
version = eval(line.split('=')[1])
setup(
name='ninfs',
version=version,
version='2.0',
description='FUSE filesystem Python scripts for Nintendo console files',
options={'build_exe': build_exe_options},
executables=executables

2
setup.py

@ -39,7 +39,7 @@ setup( @@ -39,7 +39,7 @@ setup(
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
],
install_requires=['pycryptodomex>=3.9,<4', 'pyctr>=0.5.1,<0.7', 'haccrypto>=0.1'],
install_requires=['pycryptodomex>=3.9,<4', 'pyctr>=0.5.1,<0.7', 'haccrypto>=0.1', 'pypng>=0.0.21'],
python_requires='>=3.6.1',
# fusepy should be added here once the main repo has a new release with Windows support.
entry_points={'gui_scripts': ['ninfsw = ninfs.main:gui'],

12
standalone.spec

@ -8,6 +8,8 @@ sys.path.insert(0, os.getcwd()) @@ -8,6 +8,8 @@ sys.path.insert(0, os.getcwd())
from ninfs import mountinfo
import haccrypto
mount_module_paths = [f'mount.{x}' for x in mountinfo.types.keys()]
imports = [
@ -24,7 +26,8 @@ imports = [ @@ -24,7 +26,8 @@ imports = [
a = Analysis(['ninfs/_frozen_main.py'],
pathex=['./ninfs'],
binaries=[],
# this is bugging the shit out of me
binaries=[(os.path.join(os.path.dirname(haccrypto.__file__), 'libcrypto.1.1.dylib'), 'haccrypto')],
datas=[('ninfs/gui/data', 'guidata')],
hiddenimports=imports,
hookspath=[],
@ -45,7 +48,8 @@ exe = EXE(pyz, @@ -45,7 +48,8 @@ exe = EXE(pyz,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False)
console=False,
target_arch='universal2')
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
@ -62,7 +66,7 @@ app = BUNDLE(coll, @@ -62,7 +66,7 @@ app = BUNDLE(coll,
'LSMinimumSystemVersion': '10.12.6',
#'NSRequiresAquaSystemAppearance': True,
#'NSHighResolutionCapable': True,
'CFBundleShortVersionString': '2.0a6',
'CFBundleVersion': '2002',
'CFBundleShortVersionString': '2.0a7',
'CFBundleVersion': '2003',
}
)

Loading…
Cancel
Save