From 403df319077f9980e5d22a1e4c1cbaeca63361ae Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:03:20 +0100 Subject: [PATCH] feat: native auto-update close #521 --- .github/workflows/build.yml | 128 +++++++++++++++++- lib/screens/main_screen.dart | 6 + lib/screens/settings/settings_screen.dart | 25 ++++ lib/services/update_service.dart | 67 +++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 10 ++ macos/Runner/Info.plist | 4 + pubspec.lock | 32 +++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + windows/runner/Runner.rc | 6 + windows/runner/resources/dsa_pub.pem | 36 +++++ 13 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 windows/runner/resources/dsa_pub.pem diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a52e5bc4..b6302a16 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -269,16 +269,34 @@ jobs: ln -s /Applications dmg-staging/Applications hdiutil create -format ULMO -srcfolder dmg-staging -volname "Plezy" plezy-macos.dmg + - name: Sign DMG for Sparkle (EdDSA) + if: ${{ secrets.SPARKLE_PRIVATE_KEY != '' }} + env: + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + run: | + SIGN_BIN="macos/Pods/Sparkle/bin/sign_update" + SIGNATURE=$(echo "$SPARKLE_PRIVATE_KEY" | "$SIGN_BIN" plezy-macos.dmg --ed-key-file - -p) + echo "MACOS_ED_SIGNATURE=$SIGNATURE" >> $GITHUB_ENV + echo "MACOS_DMG_SIZE=$(stat -f%z plezy-macos.dmg)" >> $GITHUB_ENV + - name: Attest macOS DMG uses: actions/attest-build-provenance@v2 with: subject-path: plezy-macos.dmg + - name: Write macOS signature metadata + run: | + echo "${{ env.MACOS_ED_SIGNATURE }}" > macos-ed-signature.txt + echo "${{ env.MACOS_DMG_SIZE }}" > macos-dmg-size.txt + - name: Upload macOS DMG uses: actions/upload-artifact@v4 with: name: macos-dmg - path: plezy-macos.dmg + path: | + plezy-macos.dmg + macos-ed-signature.txt + macos-dmg-size.txt build-windows-x64: runs-on: windows-latest @@ -361,6 +379,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + cache: true + + - name: Install dependencies + run: flutter pub get + - name: Download x64 build uses: actions/download-artifact@v4 with: @@ -376,6 +403,18 @@ jobs: - name: Build installer and portables run: .\windows\build-installer.ps1 -X64BuildDir "build-x64" -Arm64BuildDir "build-arm64" + - name: Sign installer for WinSparkle (DSA) + if: ${{ secrets.WINSPARKLE_DSA_PRIVATE_KEY != '' }} + env: + WINSPARKLE_DSA_PRIVATE_KEY: ${{ secrets.WINSPARKLE_DSA_PRIVATE_KEY }} + shell: pwsh + run: | + $env:WINSPARKLE_DSA_PRIVATE_KEY | Out-File -Encoding ascii dsa_priv.pem + $output = dart run auto_updater:sign_update plezy-windows-installer.exe + Remove-Item dsa_priv.pem + $output | Out-File -Encoding ascii win-dsa-signature.txt + (Get-Item plezy-windows-installer.exe).Length | Out-File -Encoding ascii win-installer-size.txt + - name: Attest Windows artifacts uses: actions/attest-build-provenance@v2 with: @@ -400,7 +439,10 @@ jobs: uses: actions/upload-artifact@v4 with: name: windows-installer - path: plezy-windows-installer.exe + path: | + plezy-windows-installer.exe + win-dsa-signature.txt + win-installer-size.txt build-linux-x64: runs-on: ubuntu-latest @@ -634,6 +676,11 @@ jobs: permissions: contents: write steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: pubspec.yaml + sparse-checkout-cone-mode: false + - name: Download all artifacts uses: actions/download-artifact@v4 with: @@ -642,6 +689,82 @@ jobs: - name: Display structure of downloaded files run: ls -R artifacts + - name: Read version from pubspec.yaml + id: version + run: | + VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Generate appcast.xml + run: | + VERSION="${{ steps.version.outputs.version }}" + + # Read macOS signature metadata (may be empty if secrets not configured) + MACOS_ED_SIG="" + MACOS_DMG_SIZE="0" + if [ -f artifacts/macos-dmg/macos-ed-signature.txt ]; then + MACOS_ED_SIG=$(cat artifacts/macos-dmg/macos-ed-signature.txt) + fi + if [ -f artifacts/macos-dmg/macos-dmg-size.txt ]; then + MACOS_DMG_SIZE=$(cat artifacts/macos-dmg/macos-dmg-size.txt) + fi + + # Read Windows signature metadata (may be empty if secrets not configured) + WIN_DSA_SIG="" + WIN_INSTALLER_SIZE="0" + if [ -f artifacts/windows-installer/win-dsa-signature.txt ]; then + WIN_DSA_SIG=$(cat artifacts/windows-installer/win-dsa-signature.txt) + fi + if [ -f artifacts/windows-installer/win-installer-size.txt ]; then + WIN_INSTALLER_SIZE=$(cat artifacts/windows-installer/win-installer-size.txt) + fi + + cat > appcast.xml << 'XMLEOF' + + + + Plezy Updates + XMLEOF + + # macOS entry + if [ -n "$MACOS_ED_SIG" ]; then + cat >> appcast.xml << XMLEOF + + Version ${VERSION} + ${VERSION} + macos + + + XMLEOF + fi + + # Windows entry + if [ -n "$WIN_DSA_SIG" ]; then + cat >> appcast.xml << XMLEOF + + Version ${VERSION} + ${VERSION} + windows-x64 + + + XMLEOF + fi + + cat >> appcast.xml << 'XMLEOF' + + + XMLEOF + + # Clean up leading whitespace from heredoc indentation + sed -i 's/^ //' appcast.xml + + echo "Generated appcast.xml:" + cat appcast.xml + - name: Create Release uses: softprops/action-gh-release@v1 with: @@ -656,6 +779,7 @@ jobs: artifacts/windows-installer/plezy-windows-installer.exe artifacts/linux-x64/* artifacts/linux-arm64/* + appcast.xml draft: true prerelease: false generate_release_notes: true diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index fd59bd0b..449150dd 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -195,6 +195,12 @@ class _MainScreenState extends State with RouteAware, WindowListener if (!mounted) return; + // Native updater (Sparkle/WinSparkle) handles everything — skip Flutter dialog + if (UpdateService.useNativeUpdater) { + await UpdateService.checkForUpdatesNative(inBackground: true); + return; + } + try { final updateInfo = await UpdateService.checkForUpdatesOnStartup(); diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index e178786c..a7dc66a0 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -931,6 +931,31 @@ class _SettingsScreenState extends State with FocusableTab { } Widget _buildUpdateSection() { + // Native updater: simple tile that triggers Sparkle/WinSparkle native dialog + if (UpdateService.useNativeUpdater) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + t.settings.updates, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ListTile( + focusNode: _focusTracker.get(_kCheckForUpdates), + leading: const AppIcon(Symbols.system_update_rounded, fill: 1), + title: Text(t.settings.checkForUpdates), + trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1), + onTap: () => UpdateService.checkForUpdatesNative(inBackground: false), + ), + ], + ), + ); + } + final hasUpdate = _updateInfo != null && _updateInfo!['hasUpdate'] == true; return Card( diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart index 2cb52419..0b0d7dc6 100644 --- a/lib/services/update_service.dart +++ b/lib/services/update_service.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:auto_updater/auto_updater.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:logger/logger.dart'; @@ -5,9 +8,14 @@ import 'package:shared_preferences/shared_preferences.dart'; /// Service to check for new versions on GitHub /// Only enabled when ENABLE_UPDATE_CHECK build flag is set +/// +/// On macOS (non-Homebrew) and installed Windows: delegates to Sparkle/WinSparkle +/// via auto_updater for native update dialogs and in-app installs. +/// On all other platforms: falls back to GitHub API check + browser link dialog. class UpdateService { static final Logger _logger = Logger(); static const String _githubRepo = 'edde746/plezy'; + static const String _feedUrl = 'https://github.com/edde746/plezy/releases/latest/download/appcast.xml'; // SharedPreferences keys static const String _keySkippedVersion = 'update_skipped_version'; @@ -16,11 +24,70 @@ class UpdateService { // Check cooldown: 6 hours static const Duration _checkCooldown = Duration(hours: 6); + static bool _nativeUpdaterInitialized = false; + /// Check if update checking is enabled via build flag static bool get isUpdateCheckEnabled { return const bool.fromEnvironment('ENABLE_UPDATE_CHECK', defaultValue: false); } + /// Whether the native auto_updater (Sparkle/WinSparkle) should be used. + /// True on macOS (non-Homebrew) and installed Windows (has uninstaller). + static bool get useNativeUpdater { + if (!isUpdateCheckEnabled) return false; + if (Platform.isMacOS) return !_isHomebrewInstall(); + if (Platform.isWindows) return _isInstalledApp(); + return false; + } + + /// Initialize the native auto_updater (Sparkle/WinSparkle). + /// Call once at startup if [useNativeUpdater] is true. + static Future initNativeUpdater() async { + if (_nativeUpdaterInitialized) return; + _nativeUpdaterInitialized = true; + + try { + await autoUpdater.setFeedURL(_feedUrl); + // 6 hours in seconds + await autoUpdater.setScheduledCheckInterval(_checkCooldown.inSeconds); + } catch (e) { + _logger.e('Failed to initialize native auto updater: $e'); + } + } + + /// Trigger a background update check via Sparkle/WinSparkle. + /// Only shows UI if an update is found. + static Future checkForUpdatesNative({bool inBackground = true}) async { + if (!_nativeUpdaterInitialized) await initNativeUpdater(); + try { + await autoUpdater.checkForUpdates(inBackground: inBackground); + } catch (e) { + _logger.e('Native update check failed: $e'); + } + } + + /// Check if the macOS app was installed via Homebrew. + /// Homebrew casks live under /opt/homebrew/Caskroom/ or /usr/local/Caskroom/. + static bool _isHomebrewInstall() { + try { + final execPath = Platform.resolvedExecutable; + return execPath.contains('/Caskroom/') || execPath.contains('/homebrew/'); + } catch (_) { + return false; + } + } + + /// Check if the Windows app is an installed copy (not portable). + /// The Inno Setup installer places unins000.exe next to the executable. + static bool _isInstalledApp() { + try { + final exeDir = File(Platform.resolvedExecutable).parent.path; + return File('$exeDir\\unins000.exe').existsSync(); + } catch (_) { + return false; + } + } + /// Skip a specific version static Future skipVersion(String version) async { final prefs = await SharedPreferences.getInstance(); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 718fe9a5..a9d35d16 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import auto_updater_macos import connectivity_plus import device_info_plus import file_picker @@ -23,6 +24,7 @@ import wakelock_plus import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AutoUpdaterMacosPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index d0b49723..650efab9 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,7 @@ PODS: + - auto_updater_macos (0.0.1): + - FlutterMacOS + - Sparkle - connectivity_plus (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): @@ -22,6 +25,7 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - Sparkle (2.9.0) - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS @@ -60,6 +64,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - auto_updater_macos (from `Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) @@ -80,9 +85,12 @@ DEPENDENCIES: SPEC REPOS: trunk: + - Sparkle - sqlite3 EXTERNAL SOURCES: + auto_updater_macos: + :path: Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos device_info_plus: @@ -119,6 +127,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: + auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a @@ -130,6 +139,7 @@ SPEC CHECKSUMS: path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + Sparkle: f4355f9ebbe9b7d932df4980d70f13922ac97b2a sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index bcd707b5..b9238fc3 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -30,5 +30,9 @@ NSApplication FLTEnableMergedPlatformUIThread + SUFeedURL + https://github.com/edde746/plezy/releases/latest/download/appcast.xml + SUPublicEDKey + q7t6eu1txRrqv1PeWDCRV95rFxnmD/D3GZWbUPMwUyU= diff --git a/pubspec.lock b/pubspec.lock index acb9df56..bccc1d78 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + auto_updater: + dependency: "direct main" + description: + name: auto_updater + sha256: "74fd008b021d15e4fc50fb5f1923870e6374ae831bb1168241c23bc35a4e898b" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + auto_updater_macos: + dependency: transitive + description: + name: auto_updater_macos + sha256: ef9de0daf38d782d2243fe131503a9404d62df762f5386e5360020e43c01867f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + auto_updater_platform_interface: + dependency: transitive + description: + name: auto_updater_platform_interface + sha256: ed5dff8cea18c58bc5ac787ff9122fedd1644272e02015d28c9b592f8078c5dc + url: "https://pub.dev" + source: hosted + version: "1.0.0" + auto_updater_windows: + dependency: transitive + description: + name: auto_updater_windows + sha256: "2bba20a71eee072f49b7267fedd5c4f1406c4b1b1e5b83932c634dbab75b80c9" + url: "https://pub.dev" + source: hosted + version: "1.0.0" background_downloader: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index bf1c46b5..f8c24a9a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: mobile_scanner: ^6.0.2 android_intent_plus: ^5.0.2 background_downloader: ^9.5.2 + auto_updater: ^1.0.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index bc3ebff2..6eeee254 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -15,6 +16,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AutoUpdaterWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AutoUpdaterWindowsPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); OsMediaControlsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index db77f229..85ff0401 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + auto_updater_windows connectivity_plus os_media_controls screen_retriever_windows diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 265245f9..7f732849 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -105,6 +105,12 @@ BEGIN END END +///////////////////////////////////////////////////////////////////////////// +// +// WinSparkle DSA public key for update signature verification +// +DSAPub DSAPEM "resources\\dsa_pub.pem" + #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// diff --git a/windows/runner/resources/dsa_pub.pem b/windows/runner/resources/dsa_pub.pem new file mode 100644 index 00000000..88668a46 --- /dev/null +++ b/windows/runner/resources/dsa_pub.pem @@ -0,0 +1,36 @@ +-----BEGIN PUBLIC KEY----- +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 +IdoSn7v/pXbD9haYAkYFhXifYUUE/g== +-----END PUBLIC KEY-----