From 771988a6a61d55eab88803ea51d4e240bb1f8d25 Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 28 Jan 2026 18:14:34 -0800 Subject: [PATCH 1/2] Enable Pyrefly --- pyproject.toml | 3 +++ synodic_client/application/qt.py | 5 ++++- synodic_client/updater.py | 27 +++++++++++++++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 54e7ddb..e961d6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,9 @@ quote-style = "single" [tool.coverage.report] skip_empty = true +[tool.pyrefly] +search_path = ["synodic_client/..."] + [tool.pdm.version] source = "scm" diff --git a/synodic_client/application/qt.py b/synodic_client/application/qt.py index 566a0f5..1df55c8 100644 --- a/synodic_client/application/qt.py +++ b/synodic_client/application/qt.py @@ -44,7 +44,10 @@ def application() -> None: screen = Screen() - app.tray = TrayScreen(app, client, icon, screen.window) + # Store tray screen as instance attribute using object.__setattr__ + # to avoid type checking issues with dynamic attributes + tray_screen = TrayScreen(app, client, icon, screen.window) + object.__setattr__(app, 'tray', tray_screen) app.exec_() diff --git a/synodic_client/updater.py b/synodic_client/updater.py index e5bb809..3e3f470 100644 --- a/synodic_client/updater.py +++ b/synodic_client/updater.py @@ -133,7 +133,7 @@ def check_for_update(self) -> UpdateInfo: result = self._porringer.update.check(params) if result.available and result.latest_version: - latest = Version(result.latest_version) + latest = Version(str(result.latest_version)) self._update_info = UpdateInfo( available=True, current_version=self._current_version, @@ -239,7 +239,8 @@ def apply_update(self) -> bool: except Exception as e: logger.exception('Failed to apply update') self._state = UpdateState.ROLLBACK_REQUIRED - self._update_info.error = str(e) + if self._update_info is not None: + self._update_info.error = str(e) return False def rollback(self) -> bool: @@ -341,9 +342,13 @@ def _get_bundled_root_metadata(self) -> Path | None: Path to root.json if bundled, None otherwise """ if self.is_frozen: - # PyInstaller bundle - bundle_dir = Path(sys._MEIPASS) # type: ignore[attr-defined] - root_path = bundle_dir / 'data' / 'tuf_root.json' + # PyInstaller bundle - _MEIPASS is set by PyInstaller at runtime + meipass = getattr(sys, '_MEIPASS', None) + if meipass is not None: + bundle_dir = Path(meipass) + root_path = bundle_dir / 'data' / 'tuf_root.json' + else: + return None else: # Development mode root_path = Path(__file__).parent.parent / 'data' / 'tuf_root.json' @@ -401,6 +406,10 @@ def _apply_frozen_update(self) -> bool: backup_path = self._get_backup_path() new_exe = self._downloaded_path + if new_exe is None: + logger.error('No downloaded executable found') + return False + # Create backup of current executable logger.info('Creating backup: %s -> %s', current_exe, backup_path) shutil.copy2(current_exe, backup_path) @@ -448,9 +457,15 @@ def _apply_windows_update(self, current_exe: Path, new_exe: Path, backup_path: P script_path.write_text(script_content) # Schedule the script to run + # Windows-specific process creation flags + flags = 0 + if sys.platform == 'win32': + # CREATE_NEW_CONSOLE = 0x00000200, DETACHED_PROCESS = 0x00000008 + flags = 0x00000200 | 0x00000008 + subprocess.Popen( ['cmd', '/c', str(script_path)], - creationflags=subprocess.CREATE_NEW_CONSOLE | subprocess.DETACHED_PROCESS, + creationflags=flags, ) self._state = UpdateState.APPLIED From 0016f5a214287c7b6df57ea6420a477dda997cea Mon Sep 17 00:00:00 2001 From: Asher Norland Date: Wed, 28 Jan 2026 18:16:27 -0800 Subject: [PATCH 2/2] Add Spec File --- .gitignore | 2 ++ tool/pyinstaller/synodic.spec | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tool/pyinstaller/synodic.spec diff --git a/.gitignore b/.gitignore index 5394dee..4a42255 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ MANIFEST # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec +# Exception: track our custom spec file +!tool/pyinstaller/synodic.spec # Installer logs pip-log.txt diff --git a/tool/pyinstaller/synodic.spec b/tool/pyinstaller/synodic.spec new file mode 100644 index 0000000..afc9243 --- /dev/null +++ b/tool/pyinstaller/synodic.spec @@ -0,0 +1,52 @@ +# -*- mode: python ; coding: utf-8 -*- + +from PyInstaller.utils.hooks import collect_all, copy_metadata + +# Collect porringer and its plugins with metadata +datas = [('../../data', 'data')] +hiddenimports = [] + +# Add porringer metadata so entry points work +datas += copy_metadata('porringer') + +# Add TUF metadata for secure updates +datas += copy_metadata('tuf') + +# Add your plugin packages here as you add them to dependencies +# Example: datas += copy_metadata('porringer-plugin-name') +# Example: hiddenimports += ['porringer_plugin_name'] + +a = Analysis( + ['../../synodic_client/application/qt.py'], + pathex=[], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='synodic', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +)