From 036ad656d75cd5822669957b6b287f7e25d78664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 08:40:40 +0100 Subject: [PATCH 01/14] fix: upgrade pyobjc-core 7.1 -> 10.3.1 to fix SIGSEGV on macOS 26 pyobjc-core 7.1 (built for CPython 3.6) crashes in PyObjCClass_NewMetaClass when loading Objective-C classes on macOS 26.3.1 under Rosetta. Version 10.3.1 has native Apple Silicon support and modern macOS compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) --- requirements/mac.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/mac.txt b/requirements/mac.txt index c879ef83..e0ec7075 100644 --- a/requirements/mac.txt +++ b/requirements/mac.txt @@ -1,3 +1,3 @@ -r base.txt osxtrash==1.6 -pyobjc-core==7.1 \ No newline at end of file +pyobjc-core==10.3.1 \ No newline at end of file From 2ffd12a39bd5e365984db09e876d5585b6d0b136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 08:41:20 +0100 Subject: [PATCH 02/14] chore: upgrade base dependencies for Python 3.9 compatibility PyInstaller 4.4 -> 6.11.1 (EOL fix) rsa 3.4.2 -> 4.9 (security) boto3 1.17.26 -> 1.35.99 (3 years of fixes) requests 2.25.1 -> 2.32.3 (security patches) Co-Authored-By: Claude Opus 4.6 (1M context) --- requirements/base.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 03ffc86e..951df5dc 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,7 +1,7 @@ http://build-system.fman.io/pro/b5aab865-bd29-4f23-992c-0eb4f3a24f33/0.9.4#egg=fbs[sentry] PyQt5==5.15.11 -PyInstaller==4.4 -rsa==3.4.2 +PyInstaller==6.11.1 +rsa==4.9 tinycss==0.4 -boto3==1.17.26 -requests==2.25.1 \ No newline at end of file +boto3==1.35.99 +requests==2.32.3 \ No newline at end of file From 3593b9134965e78fb95129c855e88bb9d2cdd9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 08:41:50 +0100 Subject: [PATCH 03/14] chore: upgrade Windows dependencies PyQt5 5.15.4 -> 5.15.11 (align with base) Send2Trash 1.4.2 -> 1.8.3 pywinpty 0.5.7 (wheel) -> 2.0.14 (PyPI) pywin32 300 -> 308 Co-Authored-By: Claude Opus 4.6 (1M context) --- requirements/windows.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/windows.txt b/requirements/windows.txt index d63bc13c..e6dfd14d 100644 --- a/requirements/windows.txt +++ b/requirements/windows.txt @@ -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 \ No newline at end of file +pywinpty==2.0.14 +pywin32==308 \ No newline at end of file From 50849aaede4beb8396e8b9b9fc7678cb1eaaf13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 08:42:37 +0100 Subject: [PATCH 04/14] chore: upgrade Linux dependencies Send2Trash 1.5.0 -> 1.8.3 distro 1.0.4 -> 1.9.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- requirements/linux.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/linux.txt b/requirements/linux.txt index 1cdd27e6..7ee521d2 100644 --- a/requirements/linux.txt +++ b/requirements/linux.txt @@ -1,3 +1,3 @@ -r base.txt -Send2Trash==1.5.0 -distro==1.0.4 \ No newline at end of file +Send2Trash==1.8.3 +distro==1.9.0 \ No newline at end of file From e1b1a5ff63c8079015cafe73aaabeace1776826a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 08:47:08 +0100 Subject: [PATCH 05/14] chore: remove Python 3.5/3.6 compatibility workarounds Python 3.9 is the minimum version. Remove unnecessary try/except for Path.resolve(strict=True) and update version references in comments. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/python/fman/impl/plugins/error.py | 10 +++++----- src/main/resources/base/Plugins/Core/core/__init__.py | 2 +- .../base/Plugins/Core/core/fs/local/__init__.py | 8 +------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/python/fman/impl/plugins/error.py b/src/main/python/fman/impl/plugins/error.py index 3228ad46..f7a74d42 100644 --- a/src/main/python/fman/impl/plugins/error.py +++ b/src/main/python/fman/impl/plugins/error.py @@ -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. """ @@ -83,7 +83,7 @@ def __init__( _seen.add(exc_value) 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: + # This differs from stdlib's implementation: cause = TracebackExceptionWithTbFilter( type(exc_value.__cause__), exc_value.__cause__, @@ -98,7 +98,7 @@ def __init__( 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: + # This differs from stdlib's implementation: context = TracebackExceptionWithTbFilter( type(exc_value.__context__), exc_value.__context__, @@ -114,12 +114,12 @@ def __init__( self.exc_traceback = exc_traceback self.__cause__ = cause self.__context__ = context - # This differs from Python 3.5.3's implementation: + # This differs from stdlib's implementation: 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: + # This differs from stdlib's implementation: if exc_value: # Hide context when all its frames are hidden: self.__suppress_context__ = exc_value.__suppress_context__ or \ diff --git a/src/main/resources/base/Plugins/Core/core/__init__.py b/src/main/resources/base/Plugins/Core/core/__init__.py index 17ab484d..478b81e4 100644 --- a/src/main/resources/base/Plugins/Core/core/__init__.py +++ b/src/main/resources/base/Plugins/Core/core/__init__.py @@ -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 '' diff --git a/src/main/resources/base/Plugins/Core/core/fs/local/__init__.py b/src/main/resources/base/Plugins/Core/core/fs/local/__init__.py index 108ed2a3..b3fbf022 100644 --- a/src/main/resources/base/Plugins/Core/core/fs/local/__init__.py +++ b/src/main/resources/base/Plugins/Core/core/fs/local/__init__.py @@ -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 From 6a0363cc4706768a4909c8cff27ef4386c9867cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 09:02:25 +0100 Subject: [PATCH 06/14] fix: bump PyInstaller to 6.19.0 and pyobjc-core to 12.1 for Python 3.14 compat PyInstaller 6.11.1 requires Python <3.14; 6.19.0 supports 3.14. pyobjc-core 10.3.1 uses pkg_resources (removed in 3.14); 12.1 works. Also fix fbs egg fragment syntax for modern pip (PEP 440 Direct URL). Co-Authored-By: Claude Opus 4.6 (1M context) --- requirements/base.txt | 4 ++-- requirements/mac.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 951df5dc..a2bddc2e 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,6 +1,6 @@ -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==6.11.1 +PyInstaller==6.19.0 rsa==4.9 tinycss==0.4 boto3==1.35.99 diff --git a/requirements/mac.txt b/requirements/mac.txt index e0ec7075..7ce5dbda 100644 --- a/requirements/mac.txt +++ b/requirements/mac.txt @@ -1,3 +1,3 @@ -r base.txt osxtrash==1.6 -pyobjc-core==10.3.1 \ No newline at end of file +pyobjc-core==12.1 \ No newline at end of file From e0cb91a5cb6ef289a0da3333188379bea1234035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 09:17:31 +0100 Subject: [PATCH 07/14] fix: replace private _some_str import removed in Python 3.14 traceback._some_str was a private function removed in Python 3.14. Replace with inline safe-str conversion (try/except). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/python/fman/impl/plugins/error.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/python/fman/impl/plugins/error.py b/src/main/python/fman/impl/plugins/error.py index f7a74d42..12d199d1 100644 --- a/src/main/python/fman/impl/plugins/error.py +++ b/src/main/python/fman/impl/plugins/error.py @@ -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 @@ -127,7 +127,10 @@ def __init__( else: self.__suppress_context__ = False self.exc_type = exc_type - self._str = _some_str(exc_value) + try: + self._str = str(exc_value) + except Exception: + self._str = '' % type(exc_value).__name__ if exc_type and issubclass(exc_type, SyntaxError): self.filename = exc_value.filename self.lineno = str(exc_value.lineno) From 778b538f4a29efc399ec5651eaa530cb5a88f062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 09:19:45 +0100 Subject: [PATCH 08/14] fix: use raw docstring to silence invalid escape sequence warning \F in the docstring is not a valid escape sequence and will become an error in a future Python version. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/python/fman/impl/onboarding/tutorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/python/fman/impl/onboarding/tutorial.py b/src/main/python/fman/impl/onboarding/tutorial.py index 0dfb6c53..73d3b628 100644 --- a/src/main/python/fman/impl/onboarding/tutorial.py +++ b/src/main/python/fman/impl/onboarding/tutorial.py @@ -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 From 952289fe3e2011ef1b3a587e701e6fe95e2db88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 09:31:07 +0100 Subject: [PATCH 09/14] fix: replace private _some_str import removed in Python 3.14 traceback._some_str was a private function removed in Python 3.14. Also refactor TracebackExceptionWithTbFilter to call super().__init__() instead of reimplementing it, fixing exc_type read-only property and missing exceptions attribute in Python 3.14. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/python/fman/impl/plugins/error.py | 52 ++++++---------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/src/main/python/fman/impl/plugins/error.py b/src/main/python/fman/impl/plugins/error.py index 12d199d1..afd506d4 100644 --- a/src/main/python/fman/impl/plugins/error.py +++ b/src/main/python/fman/impl/plugins/error.py @@ -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 stdlib'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__, @@ -94,49 +101,16 @@ 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 stdlib's implementation: - context = TracebackExceptionWithTbFilter( - type(exc_value.__context__), - exc_value.__context__, - exc_value.__context__.__traceback__, - limit=limit, - lookup_lines=False, - capture_locals=capture_locals, - _seen=_seen, - tb_filter=tb_filter - ) - else: - context = None - self.exc_traceback = exc_traceback - self.__cause__ = cause - self.__context__ = context - # This differs from stdlib'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 stdlib'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 - try: - self._str = str(exc_value) - except Exception: - self._str = '' % type(exc_value).__name__ - 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() From 7979ce5c773debc0c92d31ef2c93742e6f313603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 09:31:22 +0100 Subject: [PATCH 10/14] fix: move osxtrash .so to Frameworks dir for PyInstaller 6.x compat PyInstaller 6.x sets sys._MEIPASS to Contents/Frameworks, not Contents/MacOS. Move osxtrash .so there so it's importable at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/build/python/build_impl/mac.py | 6 +++--- src/main/python/fman/impl/plugins/error.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/build/python/build_impl/mac.py b/src/build/python/build_impl/mac.py index abeba510..714de907 100644 --- a/src/build/python/build_impl/mac.py +++ b/src/build/python/build_impl/mac.py @@ -38,11 +38,11 @@ def freeze(): 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'), diff --git a/src/main/python/fman/impl/plugins/error.py b/src/main/python/fman/impl/plugins/error.py index afd506d4..8b19d965 100644 --- a/src/main/python/fman/impl/plugins/error.py +++ b/src/main/python/fman/impl/plugins/error.py @@ -101,6 +101,18 @@ def __init__( _seen=_seen, tb_filter=tb_filter ) + if (exc_value and exc_value.__context__ is not None + and id(exc_value.__context__) not in _seen): + self.__context__ = TracebackExceptionWithTbFilter( + type(exc_value.__context__), + exc_value.__context__, + exc_value.__context__.__traceback__, + limit=limit, + lookup_lines=False, + capture_locals=capture_locals, + _seen=_seen, + tb_filter=tb_filter + ) # Override stack with filtered version self.stack = StackSummary.extract( walk_tb_with_filtering(exc_traceback, tb_filter), limit=limit, From 5f7bf5212c25211577e869155e574a8e8ae1474c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 10:53:27 +0100 Subject: [PATCH 11/14] fix: cast CSS float values to int for PyQt5 on Python 3.14 Python 3.14 removed implicit float-to-int conversion in C extensions. QSize, QPoint, and setPointSize now reject float arguments. Cast CSS-derived values to int explicitly. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/python/fman/impl/quicksearch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/python/fman/impl/quicksearch.py b/src/main/python/fman/impl/quicksearch.py index cba8f3a3..79314844 100644 --- a/src/main/python/fman/impl/quicksearch.py +++ b/src/main/python/fman/impl/quicksearch.py @@ -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 @@ -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() @@ -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 From 8990786e86b1d1e4049ca0cb1bfa051cd9ab31c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 11:35:34 +0100 Subject: [PATCH 12/14] fix: cache CoreServices loadBundle to fix slow GoTo quicksearch loadBundle('CoreServices.framework') takes ~800ms on first call with pyobjc-core 12.1. Cache the namespace in LocalFileSystem so subsequent GoTo queries don't re-load the framework. Also pre-warm the framework in a background thread at app startup. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/python/fman/impl/application_context.py | 14 ++++++++++++++ .../base/Plugins/Core/core/commands/goto.py | 10 ++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/python/fman/impl/application_context.py b/src/main/python/fman/impl/application_context.py index b3372366..8744eaad 100644 --- a/src/main/python/fman/impl/application_context.py +++ b/src/main/python/fman/impl/application_context.py @@ -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: @@ -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): diff --git a/src/main/resources/base/Plugins/Core/core/commands/goto.py b/src/main/resources/base/Plugins/Core/core/commands/goto.py index db242c3c..94fd8ad3 100644 --- a/src/main/resources/base/Plugins/Core/core/commands/goto.py +++ b/src/main/resources/base/Plugins/Core/core/commands/goto.py @@ -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] From 3db4ef299538e0668355559aa92f3be7649ec1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 11:50:24 +0100 Subject: [PATCH 13/14] chore: strip unused deps from frozen macOS bundle (110MB -> 77MB) Remove boto3/botocore/s3transfer (build-only, not runtime: ~40MB). Remove unused Qt frameworks: QtQml, QtQmlModels, QtQuick, QtWebSockets. Remove unused Qt plugins: webgl, minimal, offscreen, bearer, generic, platformthemes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/build/python/build_impl/mac.py | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/build/python/build_impl/mac.py b/src/build/python/build_impl/mac.py index 714de907..d8925b23 100644 --- a/src/build/python/build_impl/mac.py +++ b/src/build/python/build_impl/mac.py @@ -11,6 +11,7 @@ from time import sleep import json +import os import plistlib import requests @@ -31,6 +32,7 @@ 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') @@ -49,6 +51,39 @@ def freeze(): 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}') From 7fbcf8a80b1d42e081df2d07332a17e318d054f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Datkowski?= Date: Wed, 25 Mar 2026 11:59:01 +0100 Subject: [PATCH 14/14] docs: update README and add CHANGELOG for 1.7.4 Update Python version requirement (3.9+, tested to 3.14), add per-platform install commands and key dependencies table. Add CHANGELOG documenting all fixes and dependency upgrades. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ README.md | 18 ++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a76d15b9 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index d0d171ca..ee2fd216 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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.