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
24 changes: 10 additions & 14 deletions lib/core/updater/app_updater_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Map<String, String>> _resolveChecksums({
Expand Down
71 changes: 71 additions & 0 deletions lib/core/updater/update_asset_selection.dart
Original file line number Diff line number Diff line change
@@ -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<String> 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';
}
105 changes: 105 additions & 0 deletions test/core/updater/update_asset_selection_test.dart
Original file line number Diff line number Diff line change
@@ -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<String> 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', () {
const ctx = UpdateInstallContext(
environment: {'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', () {
const ctx = UpdateInstallContext(
environment: {},
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', () {
const ctx = UpdateInstallContext(
environment: {},
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', () {
const ctx = UpdateInstallContext(
environment: {},
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', () {
const ctx = UpdateInstallContext(
environment: {},
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');
});
});
}
Loading