Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ quote-style = "single"
[tool.coverage.report]
skip_empty = true

[tool.pyrefly]
search_path = ["synodic_client/..."]

[tool.pdm.version]
source = "scm"

Expand Down
5 changes: 4 additions & 1 deletion synodic_client/application/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_()

Expand Down
27 changes: 21 additions & 6 deletions synodic_client/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions tool/pyinstaller/synodic.spec
Original file line number Diff line number Diff line change
@@ -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,
)
Loading