From 305144e0933156b5f1bec2c2729e9836a1ac0ba6 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 27 Jul 2026 04:26:38 +0300 Subject: [PATCH 1/2] feat(updater): prefer release asset matching install context AppImage runtimes pull .AppImage first; portable zip stays on zip; Windows Inno (unins000.exe) prefers *-windows-setup.exe. Closes #384 Co-authored-by: Cursor --- lib/core/updater/app_updater_service.dart | 24 ++-- lib/core/updater/update_asset_selection.dart | 71 ++++++++++++ .../updater/update_asset_selection_test.dart | 105 ++++++++++++++++++ 3 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 lib/core/updater/update_asset_selection.dart create mode 100644 test/core/updater/update_asset_selection_test.dart diff --git a/lib/core/updater/app_updater_service.dart b/lib/core/updater/app_updater_service.dart index 8cca2f77..28ec2aaa 100644 --- a/lib/core/updater/app_updater_service.dart +++ b/lib/core/updater/app_updater_service.dart @@ -10,6 +10,7 @@ import '../storage/app_settings.dart'; import 'github_releases_client.dart'; import 'installers/update_install_context.dart'; import 'sha256_checksums.dart'; +import 'update_asset_selection.dart'; import 'update_manifest.dart'; import 'update_platform_installer.dart'; import 'update_version.dart'; @@ -178,20 +179,15 @@ class AppUpdaterService { return context.isManagedPackage; } - /// Picks the platform zip for the current OS from [manifest]. - UpdateAsset? platformAssetFor(UpdateManifest manifest) { - final suffix = switch (Platform.operatingSystem) { - 'linux' => '-linux.zip', - 'windows' => '-windows.zip', - 'macos' => '-macos.zip', - _ => null, - }; - if (suffix == null) return null; - - for (final asset in manifest.assets) { - if (asset.name.endsWith(suffix)) return asset; - } - return null; + /// Picks the best Release asset for the current packaging context. + UpdateAsset? platformAssetFor( + UpdateManifest manifest, { + UpdateInstallContext? context, + }) { + return selectUpdateAsset( + manifest, + context ?? UpdateInstallContext.current(), + ); } Future> _resolveChecksums({ diff --git a/lib/core/updater/update_asset_selection.dart b/lib/core/updater/update_asset_selection.dart new file mode 100644 index 00000000..fdec6f2f --- /dev/null +++ b/lib/core/updater/update_asset_selection.dart @@ -0,0 +1,71 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:querya_desktop/core/updater/installers/update_install_context.dart'; +import 'package:querya_desktop/core/updater/update_manifest.dart'; + +/// Ordered filename suffixes to try when picking a GitHub Release asset. +/// +/// Prefer the packaging family of the running binary so portable users are not +/// switched onto installers (and vice versa). +List preferredUpdateAssetSuffixes( + UpdateInstallContext context, { + String? operatingSystem, + bool Function(String path)? fileExists, +}) { + final os = operatingSystem ?? Platform.operatingSystem; + final exists = fileExists ?? ((path) => File(path).existsSync()); + + switch (os) { + case 'linux': + if (context.isSnap || context.isFlatpak) { + // In-app install is blocked; still prefer zip if selection is asked. + return const ['-linux.zip']; + } + if (context.appImagePath != null) { + return const ['-linux.AppImage', '-linux.appimage', '-linux.zip']; + } + return const ['-linux.zip', '-linux.AppImage', '-linux.appimage']; + case 'windows': + if (_looksLikeWindowsSetupInstall(context, exists)) { + return const ['-windows-setup.exe', '-windows.zip']; + } + return const ['-windows.zip', '-windows-setup.exe']; + case 'macos': + return const ['-macos.zip']; + default: + return const []; + } +} + +/// Picks the first manifest asset whose name ends with a preferred suffix. +UpdateAsset? selectUpdateAsset( + UpdateManifest manifest, + UpdateInstallContext context, { + String? operatingSystem, + bool Function(String path)? fileExists, +}) { + final suffixes = preferredUpdateAssetSuffixes( + context, + operatingSystem: operatingSystem, + fileExists: fileExists, + ); + for (final suffix in suffixes) { + final needle = suffix.toLowerCase(); + for (final asset in manifest.assets) { + if (asset.name.toLowerCase().endsWith(needle)) return asset; + } + } + return null; +} + +bool _looksLikeWindowsSetupInstall( + UpdateInstallContext context, + bool Function(String path) fileExists, +) { + final root = p.dirname(context.resolvedExecutable); + // Inno Setup default uninstaller next to the app. + if (fileExists(p.join(root, 'unins000.exe'))) return true; + final channel = context.environment['QUERYA_INSTALL_CHANNEL']?.toLowerCase(); + return channel == 'installer' || channel == 'setup'; +} diff --git a/test/core/updater/update_asset_selection_test.dart b/test/core/updater/update_asset_selection_test.dart new file mode 100644 index 00000000..c6393c51 --- /dev/null +++ b/test/core/updater/update_asset_selection_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/updater/installers/update_install_context.dart'; +import 'package:querya_desktop/core/updater/update_asset_selection.dart'; +import 'package:querya_desktop/core/updater/update_manifest.dart'; + +UpdateManifest _manifest(List names) { + return UpdateManifest( + version: '0.5.0', + changelog: '', + assets: [ + for (final name in names) + UpdateAsset( + name: name, + downloadUrl: 'https://example.com/$name', + ), + ], + ); +} + +void main() { + group('selectUpdateAsset', () { + test('Linux AppImage runtime prefers .AppImage then zip', () { + final ctx = UpdateInstallContext( + environment: const {'APPIMAGE': '/opt/Querya.AppImage'}, + resolvedExecutable: '/tmp/.mount_Querya/querya_desktop', + ); + final asset = selectUpdateAsset( + _manifest([ + 'Querya-Desktop-0.5.0-linux.zip', + 'Querya-Desktop-0.5.0-linux.AppImage', + ]), + ctx, + operatingSystem: 'linux', + ); + expect(asset?.name, 'Querya-Desktop-0.5.0-linux.AppImage'); + }); + + test('Linux portable prefers zip over AppImage', () { + final ctx = UpdateInstallContext( + environment: const {}, + resolvedExecutable: '/home/u/Querya/querya_desktop', + ); + final asset = selectUpdateAsset( + _manifest([ + 'Querya-Desktop-0.5.0-linux.AppImage', + 'Querya-Desktop-0.5.0-linux.zip', + ]), + ctx, + operatingSystem: 'linux', + ); + expect(asset?.name, 'Querya-Desktop-0.5.0-linux.zip'); + }); + + test('Windows Inno install prefers setup.exe', () { + final ctx = UpdateInstallContext( + environment: const {}, + resolvedExecutable: r'C:\Program Files\Querya\querya_desktop.exe', + ); + final asset = selectUpdateAsset( + _manifest([ + 'Querya-Desktop-0.5.0-windows.zip', + 'Querya-Desktop-0.5.0-windows-setup.exe', + ]), + ctx, + operatingSystem: 'windows', + fileExists: (path) => path.toLowerCase().endsWith('unins000.exe'), + ); + expect(asset?.name, 'Querya-Desktop-0.5.0-windows-setup.exe'); + }); + + test('Windows portable prefers zip', () { + final ctx = UpdateInstallContext( + environment: const {}, + resolvedExecutable: r'D:\portable\querya_desktop.exe', + ); + final asset = selectUpdateAsset( + _manifest([ + 'Querya-Desktop-0.5.0-windows-setup.exe', + 'Querya-Desktop-0.5.0-windows.zip', + ]), + ctx, + operatingSystem: 'windows', + fileExists: (_) => false, + ); + expect(asset?.name, 'Querya-Desktop-0.5.0-windows.zip'); + }); + + test('macOS uses macos.zip', () { + final ctx = UpdateInstallContext( + environment: const {}, + resolvedExecutable: + '/Applications/querya_desktop.app/Contents/MacOS/querya_desktop', + ); + final asset = selectUpdateAsset( + _manifest([ + 'Querya-Desktop-0.5.0-macos.zip', + 'Querya-Desktop-0.5.0-linux.zip', + ]), + ctx, + operatingSystem: 'macos', + ); + expect(asset?.name, 'Querya-Desktop-0.5.0-macos.zip'); + }); + }); +} From 15224c03989e40099b3e17581162fc8c18723c2c Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 27 Jul 2026 04:31:39 +0300 Subject: [PATCH 2/2] fix(test): prefer_const_constructors in update asset selection tests Unblocks flutter analyze on #384 CI. --- .../updater/update_asset_selection_test.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/core/updater/update_asset_selection_test.dart b/test/core/updater/update_asset_selection_test.dart index c6393c51..8b6ad4db 100644 --- a/test/core/updater/update_asset_selection_test.dart +++ b/test/core/updater/update_asset_selection_test.dart @@ -20,8 +20,8 @@ UpdateManifest _manifest(List names) { void main() { group('selectUpdateAsset', () { test('Linux AppImage runtime prefers .AppImage then zip', () { - final ctx = UpdateInstallContext( - environment: const {'APPIMAGE': '/opt/Querya.AppImage'}, + const ctx = UpdateInstallContext( + environment: {'APPIMAGE': '/opt/Querya.AppImage'}, resolvedExecutable: '/tmp/.mount_Querya/querya_desktop', ); final asset = selectUpdateAsset( @@ -36,8 +36,8 @@ void main() { }); test('Linux portable prefers zip over AppImage', () { - final ctx = UpdateInstallContext( - environment: const {}, + const ctx = UpdateInstallContext( + environment: {}, resolvedExecutable: '/home/u/Querya/querya_desktop', ); final asset = selectUpdateAsset( @@ -52,8 +52,8 @@ void main() { }); test('Windows Inno install prefers setup.exe', () { - final ctx = UpdateInstallContext( - environment: const {}, + const ctx = UpdateInstallContext( + environment: {}, resolvedExecutable: r'C:\Program Files\Querya\querya_desktop.exe', ); final asset = selectUpdateAsset( @@ -69,8 +69,8 @@ void main() { }); test('Windows portable prefers zip', () { - final ctx = UpdateInstallContext( - environment: const {}, + const ctx = UpdateInstallContext( + environment: {}, resolvedExecutable: r'D:\portable\querya_desktop.exe', ); final asset = selectUpdateAsset( @@ -86,8 +86,8 @@ void main() { }); test('macOS uses macos.zip', () { - final ctx = UpdateInstallContext( - environment: const {}, + const ctx = UpdateInstallContext( + environment: {}, resolvedExecutable: '/Applications/querya_desktop.app/Contents/MacOS/querya_desktop', );