Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Changelog

## 1.7.4 (unreleased)

### Fixed

- **macOS crash on macOS 26**: Fixed SIGSEGV in `PyObjCClass_NewMetaClass` caused by
pyobjc-core 7.1 being incompatible with macOS 26's Objective-C runtime under Rosetta.
Upgraded pyobjc-core from 7.1 to 12.1.
- **Python 3.14 compatibility**: Fixed `ImportError` for `traceback._some_str` (private
API removed in 3.14), `AttributeError` for read-only `TracebackException.exc_type`
property, and `TypeError` from float-to-int conversion in `QSize`/`QPoint`/`setPointSize`
(implicit conversion removed in 3.14).
- **Slow GoTo quicksearch (Cmd+P)**: Cached `CoreServices.framework` loading which took
~800ms per Spotlight query with pyobjc-core 12.1. Pre-warms the framework at startup.
- **Frozen app: osxtrash not found**: Moved osxtrash `.so` to `Contents/Frameworks`
(where PyInstaller 6.x sets `sys._MEIPASS`) instead of `Contents/MacOS`.
- **Escape sequence warning** in `tutorial.py` docstring (`\F` is invalid in Python 3.14+).

### Changed

- **Upgraded all dependencies** for Python 3.9+ / 3.14 compatibility:
- PyInstaller: 4.4 -> 6.19.0
- pyobjc-core: 7.1 -> 12.1 (macOS)
- rsa: 3.4.2 -> 4.9
- boto3: 1.17.26 -> 1.35.99
- requests: 2.25.1 -> 2.32.3
- Send2Trash: 1.4.2/1.5.0 -> 1.8.3 (Windows/Linux)
- distro: 1.0.4 -> 1.9.0 (Linux)
- pywinpty: 0.5.7 -> 2.0.14 (Windows)
- pywin32: 300 -> 308 (Windows)
- PyQt5: 5.15.4 -> 5.15.11 (Windows, aligned with other platforms)
- **Updated fbs dependency syntax** from egg fragment to PEP 440 Direct URL format
for compatibility with modern pip.
- **Removed Python 3.5/3.6 compatibility workarounds**: Removed unnecessary
`try/except TypeError` around `Path.resolve(strict=True)` and updated
version-specific comments.
- **Reduced macOS app bundle size** from ~110MB to ~77MB by stripping unused
Qt frameworks (QtQml, QtQuick, QtWebSockets), unused Qt plugins, and
build-only dependencies (boto3/botocore) from the frozen bundle.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ A cross-platform dual-pane file manager.

## Development instructions

You currently need Python 3.9.
Python 3.9 or later is required (tested up to Python 3.14).

Install the requirements for your operating system. For example:

pip install -Ur requirements/ubuntu.txt
pip install -Ur requirements/mac.txt # macOS
pip install -Ur requirements/ubuntu.txt # Ubuntu/Debian
pip install -Ur requirements/arch.txt # Arch Linux
pip install -Ur requirements/fedora.txt # Fedora
pip install -Ur requirements/windows.txt # Windows

Then you can use `python build.py` to run, compile etc. fman. For example:

Expand All @@ -17,3 +21,13 @@ Then you can use `python build.py` to run, compile etc. fman. For example:
Call `python build.py` without arguments to see a list of available commands.
This uses [fman build system](https://build-system.fman.io/).

## Key dependencies

| Package | Version | Notes |
|---------|---------|-------|
| PyQt5 | 5.15.11 | GUI framework |
| PyInstaller | 6.19.0 | App freezing/compilation |
| pyobjc-core | 12.1 | macOS Objective-C bridge |
| fbs | 0.9.4 | Build system |

See `requirements/` for the full list per platform.
10 changes: 5 additions & 5 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
http://build-system.fman.io/pro/b5aab865-bd29-4f23-992c-0eb4f3a24f33/0.9.4#egg=fbs[sentry]
fbs[sentry] @ http://build-system.fman.io/pro/b5aab865-bd29-4f23-992c-0eb4f3a24f33/0.9.4
PyQt5==5.15.11
PyInstaller==4.4
rsa==3.4.2
PyInstaller==6.19.0
rsa==4.9
tinycss==0.4
boto3==1.17.26
requests==2.25.1
boto3==1.35.99
requests==2.32.3
4 changes: 2 additions & 2 deletions requirements/linux.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
-r base.txt
Send2Trash==1.5.0
distro==1.0.4
Send2Trash==1.8.3
distro==1.9.0
2 changes: 1 addition & 1 deletion requirements/mac.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
-r base.txt
osxtrash==1.6
pyobjc-core==7.1
pyobjc-core==12.1
8 changes: 4 additions & 4 deletions requirements/windows.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-r base.txt
PyQt5==5.15.4
PyQt5==5.15.11
# Note: Send2Trash 1.5.0 has no effect on Windows!
Send2Trash==1.4.2
Send2Trash==1.8.3
adodbapi==2.6.0.7
https://download.lfd.uci.edu/pythonlibs/w4tscw6k/pywinpty-0.5.7-cp39-cp39-win_amd64.whl
pywin32==300
pywinpty==2.0.14
pywin32==308
41 changes: 38 additions & 3 deletions src/build/python/build_impl/mac.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from time import sleep

import json
import os
import plistlib
import requests

Expand All @@ -31,24 +32,58 @@ def freeze():
remove(path('${core_plugin_in_freeze_dir}/Open Sans.ttf'))
# Similarly for Roboto Bold.ttf. It is only used on Windows:
remove(path('${core_plugin_in_freeze_dir}/Roboto Bold.ttf'))
_strip_unused_from_bundle()
copy_framework(
path('lib/mac/Sparkle-1.22.0/Sparkle.framework'),
path('${freeze_dir}/Contents/Frameworks/Sparkle.framework')
)
copy_python_library('osxtrash', path('${core_plugin_in_freeze_dir}'))
import osxtrash
so_name = basename(osxtrash.__file__)
# Move the .so file to the correct location according to macOS's app bundle
# structure, so it is codesigned:
# Move the .so to Frameworks (where PyInstaller 6.x sets sys._MEIPASS),
# so it's both importable and codesigned:
move(
path('${core_plugin_in_freeze_dir}/' + so_name),
path('${freeze_dir}/Contents/MacOS')
path('${freeze_dir}/Contents/Frameworks')
)
move(
path('${core_plugin_in_freeze_dir}/bin/mac/7za'),
path('${freeze_dir}/Contents/MacOS')
)

def _strip_unused_from_bundle():
frameworks = path('${freeze_dir}/Contents/Frameworks')
resources = path('${freeze_dir}/Contents/Resources')
# boto3/botocore are build-system-only deps, not used at runtime (~40MB):
for dir_name in ('boto3', 'botocore', 's3transfer'):
for base in (frameworks, resources):
dir_path = join(base, dir_name)
if os.path.islink(dir_path):
os.unlink(dir_path)
elif os.path.isdir(dir_path):
rmtree(dir_path)
# Remove unused Qt frameworks (fman only uses Core, Gui, Widgets,
# MacExtras, PrintSupport, Svg):
qt_lib = join(frameworks, 'PyQt5', 'Qt5', 'lib')
for unused_fw in (
'QtQml', 'QtQmlModels', 'QtQuick', 'QtWebSockets'
):
fw_path = join(qt_lib, unused_fw + '.framework')
if os.path.isdir(fw_path):
rmtree(fw_path)
# Remove unused Qt platform plugins:
qt_plugins = join(frameworks, 'PyQt5', 'Qt5', 'plugins')
for unused_plugin in (
'platforms/libqwebgl.dylib', 'platforms/libqminimal.dylib',
'platforms/libqoffscreen.dylib', 'bearer', 'generic',
'platformthemes'
):
p = join(qt_plugins, unused_plugin)
if os.path.isdir(p):
rmtree(p)
elif os.path.isfile(p):
remove(p)

@command
def sign():
app_dir = path('${freeze_dir}')
Expand Down
14 changes: 14 additions & 0 deletions src/main/python/fman/impl/application_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ def _load_plugins(self):
def fman_version(self):
return self.build_settings['version']
def on_main_window_shown(self):
if is_mac():
self._preload_core_services()
if self.updater:
self.updater.start()
if self.is_licensed:
Expand All @@ -104,6 +106,18 @@ def on_main_window_shown(self):
self.tour_controller.start(tutorial)
else:
self.splash_screen.exec()
def _preload_core_services(self):
from threading import Thread
def _load():
try:
from objc import loadBundle
loadBundle(
'CoreServices.framework', {},
bundle_identifier='com.apple.CoreServices'
)
except Exception:
pass
Thread(target=_load, daemon=True).start()
def on_main_window_close(self):
self.session_manager.on_close(self.main_window)
def on_quit(self):
Expand Down
2 changes: 1 addition & 1 deletion src/main/python/fman/impl/onboarding/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ def continue_from(src_url, showing_hidden_files=showing_hidden_files):
return []

def _upper_server(unc_path):
"""
r"""
\\server\Folder -> \\SERVER\Folder
"""
assert unc_path.startswith(r'\\'), unc_path
Expand Down
45 changes: 17 additions & 28 deletions src/main/python/fman/impl/plugins/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fman.impl.theme import ThemeError
from fman.impl.util import is_below_dir
from os.path import dirname, basename
from traceback import StackSummary, _some_str, extract_tb, TracebackException, \
from traceback import StackSummary, extract_tb, TracebackException, \
print_exception

import fman
Expand Down Expand Up @@ -66,7 +66,7 @@ def tb_filter(tb):

class TracebackExceptionWithTbFilter(TracebackException):
"""
Copied and adapted from Python 3.5.3's `TracebackException`. Adds one
Copied and adapted from Python's `TracebackException`. Adds one
additional constructor arg: `tb_filter`, a boolean predicate that determines
which traceback entries should be included.
"""
Expand All @@ -80,11 +80,18 @@ def __init__(
):
if _seen is None:
_seen = set()
_seen.add(exc_value)
_seen.add(id(exc_value))
# Let super handle all standard attributes (exc_type, _str,
# exceptions, etc.) — some became read-only properties in Python 3.14.
super().__init__(
exc_type, exc_value, exc_traceback,
limit=limit, lookup_lines=False,
capture_locals=capture_locals
)
# Override cause/context with tb_filter-aware versions
if (exc_value and exc_value.__cause__ is not None
and exc_value.__cause__ not in _seen):
# This differs from Python 3.5.3's implementation:
cause = TracebackExceptionWithTbFilter(
and id(exc_value.__cause__) not in _seen):
self.__cause__ = TracebackExceptionWithTbFilter(
type(exc_value.__cause__),
exc_value.__cause__,
exc_value.__cause__.__traceback__,
Expand All @@ -94,12 +101,9 @@ def __init__(
_seen=_seen,
tb_filter=tb_filter
)
else:
cause = None
if (exc_value and exc_value.__context__ is not None
and exc_value.__context__ not in _seen):
# This differs from Python 3.5.3's implementation:
context = TracebackExceptionWithTbFilter(
and id(exc_value.__context__) not in _seen):
self.__context__ = TracebackExceptionWithTbFilter(
type(exc_value.__context__),
exc_value.__context__,
exc_value.__context__.__traceback__,
Expand All @@ -109,31 +113,16 @@ def __init__(
_seen=_seen,
tb_filter=tb_filter
)
else:
context = None
self.exc_traceback = exc_traceback
self.__cause__ = cause
self.__context__ = context
# This differs from Python 3.5.3's implementation:
# Override stack with filtered version
self.stack = StackSummary.extract(
walk_tb_with_filtering(exc_traceback, tb_filter), limit=limit,
lookup_lines=lookup_lines, capture_locals=capture_locals
)
# This differs from Python 3.5.3's implementation:
if exc_value:
context = self.__context__
# Hide context when all its frames are hidden:
self.__suppress_context__ = exc_value.__suppress_context__ or \
(context and not context.stack.format())
else:
self.__suppress_context__ = False
self.exc_type = exc_type
self._str = _some_str(exc_value)
if exc_type and issubclass(exc_type, SyntaxError):
self.filename = exc_value.filename
self.lineno = str(exc_value.lineno)
self.text = exc_value.text
self.offset = exc_value.offset
self.msg = exc_value.msg
if lookup_lines:
self._load_lines()

Expand Down
10 changes: 5 additions & 5 deletions src/main/python/fman/impl/quicksearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def sizeHint(self):
width = max(width, w)
height += h
width += self._padding_left + self._padding_right
return QSize(width, height)
return QSize(int(width), int(height))
def _draw_background(self, painter):
self._proxy.drawPrimitive(
QStyle.PE_PanelItemViewItem, self._option, painter, self._widget
Expand All @@ -226,13 +226,13 @@ def _draw_title(self, painter):
highlight_formats = self._get_highlight_formats()
painter.setPen(self._css['title']['color'])
pos = self._option.rect.topLeft() \
+ QPoint(self._padding_left, self._padding_top)
+ QPoint(int(self._padding_left), int(self._padding_top))
layout.draw(painter, pos, highlight_formats)
def _draw_hint(self, painter):
if not self._hint:
return
font = QFont(self._option.font)
font.setPointSize(self._css['hint']['font-size_pts'])
font.setPointSize(int(self._css['hint']['font-size_pts']))
painter.setFont(font)
painter.setPen(self._css['hint']['color'])
rect = self._get_title_rect()
Expand All @@ -246,11 +246,11 @@ def _draw_description(self, painter):
layout.draw(painter, QPointF(title_rect.left(), title_rect.bottom()))
def _layout_title(self):
font = QFont(self._option.font)
font.setPointSize(self._css['title']['font-size_pts'])
font.setPointSize(int(self._css['title']['font-size_pts']))
return self._layout_text(self._title, font)
def _layout_description(self):
font = QFont(self._option.font)
font.setPointSize(self._css['description']['font-size_pts'])
font.setPointSize(int(self._css['description']['font-size_pts']))
return self._layout_text(self._description, font)
def _get_title_rect(self):
x = self._option.rect.x() + self._padding_left
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/base/Plugins/Core/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def get_str(self, url):
try:
timestamp = mtime.timestamp()
except OSError:
# This can occur in at least Python 3.6 on Windows. To reproduce:
# This can occur on Windows. To reproduce:
# datetime.min.timestamp()
# This raises `OSError: [Errno 22] Invalid argument`.
return ''
Expand Down
10 changes: 8 additions & 2 deletions src/main/resources/base/Plugins/Core/core/commands/goto.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,20 @@ def resolve(self, path):
return str(Path(path).resolve())
def samefile(self, f1, f2):
return os.path.samefile(f1, f2)
def find_folders_starting_with(self, pattern, timeout_secs=0.02):
if PLATFORM == 'Mac':
_core_services_ns = None
def _get_core_services_ns(self):
if self.__class__._core_services_ns is None:
from objc import loadBundle
ns = {}
loadBundle(
'CoreServices.framework', ns,
bundle_identifier='com.apple.CoreServices'
)
self.__class__._core_services_ns = ns
return self.__class__._core_services_ns
def find_folders_starting_with(self, pattern, timeout_secs=0.02):
if PLATFORM == 'Mac':
ns = self._get_core_services_ns()
pred = ns['NSPredicate'].predicateWithFormat_argumentArray_(
"kMDItemContentType == 'public.folder' && "
"kMDItemFSName BEGINSWITH[c] %@", [pattern]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,7 @@ def resolve(self, path):
return 'network://' + path[2:]
p = Path(path)
try:
try:
path = p.resolve(strict=True)
except TypeError:
# fman's "production Python version" is 3.6 but we want to be
# able to develop using 3.5 as well. So add this workaround for
# Python < 3.6:
path = p.resolve()
path = p.resolve(strict=True)
except FileNotFoundError:
if not p.exists():
raise
Expand Down