From e82255d8a135ef500a531cd6b9e94237ba82a6d0 Mon Sep 17 00:00:00 2001 From: HRiggs Date: Mon, 15 Dec 2025 13:21:50 -0500 Subject: [PATCH] Disable cache --- .gitea/workflows/release.yml | 75 ++++++++++++++--- .../android/app/src/main/AndroidManifest.xml | 4 + flutter_app/build_sfx.ps1 | 81 +++++++++++++++++++ flutter_app/lib/services/update_service.dart | 54 ++++++++++--- flutter_app/pubspec.yaml | 2 +- flutter_app/windows/CMakeLists.txt | 4 + 6 files changed, 193 insertions(+), 27 deletions(-) create mode 100644 flutter_app/build_sfx.ps1 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index afb3531..8ea9824 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -110,15 +110,49 @@ jobs: } } - # Create portable (single exe) archive - if (Test-Path "build\windows\standalone\rmtpocketwatcher.exe") { - Compress-Archive -Path "build\windows\standalone\rmtpocketwatcher.exe" -DestinationPath "rmtPocketWatcher-Windows-Portable-v$version.zip" -CompressionLevel Optimal -Force - Write-Host "Created rmtPocketWatcher-Windows-Portable-v$version.zip" + # Build self-extracting portable exe (single file distribution) + Write-Host "Building self-extracting portable executable..." + .\build_sfx.ps1 + + # Copy SFX exe to root for upload + if (Test-Path "build\windows\sfx\rmtPocketWatcher-v$version-Portable.exe") { + Copy-Item "build\windows\sfx\rmtPocketWatcher-v$version-Portable.exe" "rmtPocketWatcher-Windows-Portable-v$version.exe" -Force + Write-Host "Created rmtPocketWatcher-Windows-Portable-v$version.exe" + } + + # Copy MSIX to root for easier upload + $msixFile = Get-ChildItem -Path "build\windows\x64\runner\Release" -Filter "*.msix" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($msixFile) { + Copy-Item $msixFile.FullName "rmtPocketWatcher-Windows-v$version.msix" -Force + Write-Host "Created rmtPocketWatcher-Windows-v$version.msix" + } + + # Export certificate for user installation (if certificate exists) + if (Test-Path "certificates\rmtPocketWatcher.pfx") { + Write-Host "Exporting certificate for user installation..." + try { + # Use PowerShell to export the certificate + $pfxPath = "certificates\rmtPocketWatcher.pfx" + $cerPath = "rmtPocketWatcher-Certificate.cer" + $certPassword = if ($env:CERT_PASSWORD) { $env:CERT_PASSWORD } else { "rmtPocketWatcher2024!" } + + # Load PFX and export public certificate + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pfxPath, $certPassword) + $certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) + [System.IO.File]::WriteAllBytes($cerPath, $certBytes) + + Write-Host "✅ Certificate exported: $cerPath" -ForegroundColor Green + } catch { + Write-Warning "Failed to export certificate: $($_.Exception.Message)" + } } # List created artifacts Write-Host "Artifacts created:" Get-ChildItem -Filter "*.zip" | ForEach-Object { Write-Host " - $($_.Name)" } + Get-ChildItem -Filter "*Portable*.exe" | ForEach-Object { Write-Host " - $($_.Name)" } + Get-ChildItem -Filter "*.msix" | ForEach-Object { Write-Host " - $($_.Name)" } + Get-ChildItem -Filter "*.cer" | ForEach-Object { Write-Host " - $($_.Name)" } - name: Upload Windows artifacts uses: actions/upload-artifact@v3 @@ -126,8 +160,9 @@ jobs: name: rmtPocketWatcher-Windows path: | flutter_app/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip - flutter_app/rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.zip - flutter_app/build/windows/x64/runner/Release/*.msix + flutter_app/rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.exe + flutter_app/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.msix + flutter_app/rmtPocketWatcher-Certificate.cer retention-days: 30 build-android: @@ -147,7 +182,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - cache: true + cache: false - name: Setup Android SDK uses: android-actions/setup-android@v3 @@ -169,6 +204,8 @@ jobs: run: | echo "WS_URL=$WS_URL" > .env echo "API_URL=$API_URL" >> .env + echo "Created .env file:" + cat .env | sed 's/=.*/=***/' # Show keys but mask values - name: Install dependencies working-directory: flutter_app @@ -178,7 +215,7 @@ jobs: - name: Build Android APK working-directory: flutter_app shell: bash - run: flutter build apk --release + run: flutter build apk --release --verbose - name: Rename APK working-directory: flutter_app @@ -226,8 +263,9 @@ jobs: ### Downloads - **Windows (Full)**: `rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip` - Complete standalone package - - **Windows (Portable)**: `rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.zip` - Single executable only - - **Windows (Installer)**: `*.msix` - Windows Store-style installer (if available) + - **Windows (Portable)**: `rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.exe` - Single self-extracting executable + - **Windows (Installer)**: `rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.msix` - Windows Store-style installer + - **Certificate**: `rmtPocketWatcher-Certificate.cer` - Required for signed executables (see installation notes) - **Android**: `rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk` ### Features @@ -239,8 +277,18 @@ jobs: - Vendor comparison tables ### Installation + + #### Certificate Installation (Required for signed executables) + If Windows shows "Unknown publisher" warnings, install the certificate first: + 1. Download `rmtPocketWatcher-Certificate.cer` + 2. Right-click → "Install Certificate" + 3. Choose "Local Machine" → "Place all certificates in the following store" + 4. Browse → Select "Trusted Root Certification Authorities" → OK + 5. Complete the installation + + #### Application Installation **Windows (Full)**: Extract the ZIP file and run `rmtpocketwatcher.exe` - includes all dependencies - **Windows (Portable)**: Single executable, no installation needed - just run `rmtpocketwatcher.exe` + **Windows (Portable)**: Just run the .exe - auto-extracts to AppData and launches **Windows (Installer)**: Double-click the MSIX file for Windows Store-style installation **Android**: Install the APK file (enable "Install from unknown sources") @@ -248,8 +296,9 @@ jobs: *Built with Flutter for cross-platform compatibility* files: | ./artifacts/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.zip - ./artifacts/rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.zip - ./artifacts/*.msix + ./artifacts/rmtPocketWatcher-Windows-Portable-v${{ needs.get-version.outputs.version }}.exe + ./artifacts/rmtPocketWatcher-Windows-v${{ needs.get-version.outputs.version }}.msix + ./artifacts/rmtPocketWatcher-Certificate.cer ./artifacts/rmtPocketWatcher-Android-v${{ needs.get-version.outputs.version }}.apk env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/flutter_app/android/app/src/main/AndroidManifest.xml b/flutter_app/android/app/src/main/AndroidManifest.xml index 92632d4..4c85c00 100644 --- a/flutter_app/android/app/src/main/AndroidManifest.xml +++ b/flutter_app/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,8 @@ + + + + diff --git a/flutter_app/build_sfx.ps1 b/flutter_app/build_sfx.ps1 new file mode 100644 index 0000000..1130744 --- /dev/null +++ b/flutter_app/build_sfx.ps1 @@ -0,0 +1,81 @@ +# rmtPocketWatcher Self-Extracting Executable Builder +# Creates a single .exe that extracts and runs the app + +$ErrorActionPreference = "Stop" + +$7zPath = "${env:ProgramFiles}\7-Zip\7z.exe" +if (-not (Test-Path $7zPath)) { + $7zPath = "${env:ProgramFiles(x86)}\7-Zip\7z.exe" +} +if (-not (Test-Path $7zPath)) { + Write-Error "7-Zip not found. Please install 7-Zip from https://7-zip.org" + exit 1 +} + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$StandaloneDir = Join-Path $ScriptDir "build\windows\standalone" +$OutputDir = Join-Path $ScriptDir "build\windows\sfx" +$SfxModule = "${env:ProgramFiles}\7-Zip\7z.sfx" +$PubspecPath = Join-Path $ScriptDir "pubspec.yaml" + +if (-not (Test-Path $StandaloneDir)) { + Write-Error "Standalone build not found. Run build_windows.ps1 first." + exit 1 +} + +Write-Host "Creating self-extracting executable..." -ForegroundColor Green + +# Create output directory +if (Test-Path $OutputDir) { Remove-Item -Recurse -Force $OutputDir } +New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null + +# Create SFX config file - silent extract to AppData and run +$SfxConfig = @" +;!@Install@!UTF-8! +Title="rmtPocketWatcher" +InstallPath="%LOCALAPPDATA%\\rmtPocketWatcher" +RunProgram="rmtpocketwatcher.exe" +GUIMode="2" +OverwriteMode="2" +;!@InstallEnd@! +"@ + +$SfxConfigPath = "$OutputDir\sfx_config.txt" +$SfxConfig | Out-File -FilePath $SfxConfigPath -Encoding UTF8 + +# Create 7z archive of standalone folder +$ArchivePath = "$OutputDir\app.7z" +Write-Host "Compressing application..." -ForegroundColor Yellow +& $7zPath a -t7z -mx=9 -mf=BCJ2 -r $ArchivePath "$StandaloneDir\*" | Out-Null + +if (-not (Test-Path $ArchivePath)) { + Write-Error "Failed to create archive" + exit 1 +} + +# Combine SFX module + config + archive +$Version = (Select-String -Path $PubspecPath -Pattern "^version: (.+)$").Matches[0].Groups[1].Value -replace '\+.*', '' +$SfxExePath = "$OutputDir\rmtPocketWatcher-v$Version-Portable.exe" + +Write-Host "Building self-extracting executable..." -ForegroundColor Yellow + +# Read binary files and concatenate +$sfxBytes = [System.IO.File]::ReadAllBytes($SfxModule) +$configBytes = [System.IO.File]::ReadAllBytes($SfxConfigPath) +$archiveBytes = [System.IO.File]::ReadAllBytes($ArchivePath) + +$outputStream = [System.IO.File]::Create($SfxExePath) +$outputStream.Write($sfxBytes, 0, $sfxBytes.Length) +$outputStream.Write($configBytes, 0, $configBytes.Length) +$outputStream.Write($archiveBytes, 0, $archiveBytes.Length) +$outputStream.Close() + +# Cleanup temp files +Remove-Item $SfxConfigPath -Force +Remove-Item $ArchivePath -Force + +$FileSize = [math]::Round((Get-Item $SfxExePath).Length / 1MB, 2) +Write-Host "`n✅ Self-extracting executable created!" -ForegroundColor Green +Write-Host " File: $SfxExePath" -ForegroundColor Cyan +Write-Host " Size: $FileSize MB" -ForegroundColor Cyan +Write-Host "`nThis single .exe can be distributed and run on any Windows PC." -ForegroundColor Yellow diff --git a/flutter_app/lib/services/update_service.dart b/flutter_app/lib/services/update_service.dart index 68056b5..444e28e 100644 --- a/flutter_app/lib/services/update_service.dart +++ b/flutter_app/lib/services/update_service.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; @@ -6,6 +7,20 @@ import 'package:xml/xml.dart'; class UpdateService { static const String _releasesRssUrl = 'https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases.rss'; + /// Check if the app was installed via MSIX (Windows Store-style installation) + static bool isInstalledViaMsix() { + if (!Platform.isWindows) return false; + + try { + // MSIX apps are installed in WindowsApps folder + final exePath = Platform.resolvedExecutable; + return exePath.contains('WindowsApps') || + exePath.contains('Program Files\\WindowsApps'); + } catch (e) { + return false; + } + } + /// Check if an update is available by comparing current version with latest release Future checkForUpdates() async { try { @@ -143,27 +158,34 @@ class UpdateService { final baseUrl = 'https://git.hudsonriggs.systems/LambdaBankingConglomerate/rmtPocketWatcher/releases/download/v$version'; return [ - // Windows Full Package + // Windows Full Package (ZIP with all DLLs) ReleaseAsset( name: 'rmtPocketWatcher-Windows-v$version.zip', downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-v$version.zip', - size: 0, // Unknown size from RSS - contentType: 'application/zip', - ), - // Windows Portable (single exe) - ReleaseAsset( - name: 'rmtPocketWatcher-Windows-Portable-v$version.zip', - downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-Portable-v$version.zip', size: 0, contentType: 'application/zip', ), + // Windows Portable (self-extracting EXE) + ReleaseAsset( + name: 'rmtPocketWatcher-Windows-Portable-v$version.exe', + downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-Portable-v$version.exe', + size: 0, + contentType: 'application/octet-stream', + ), // Windows MSIX Installer ReleaseAsset( - name: 'rmtPocketWatcher-v$version.msix', - downloadUrl: '$baseUrl/rmtPocketWatcher-v$version.msix', + name: 'rmtPocketWatcher-Windows-v$version.msix', + downloadUrl: '$baseUrl/rmtPocketWatcher-Windows-v$version.msix', size: 0, contentType: 'application/msix', ), + // Certificate for code signing verification + ReleaseAsset( + name: 'rmtPocketWatcher-Certificate.cer', + downloadUrl: '$baseUrl/rmtPocketWatcher-Certificate.cer', + size: 0, + contentType: 'application/x-x509-ca-cert', + ), // Android APK ReleaseAsset( name: 'rmtPocketWatcher-Android-v$version.apk', @@ -200,11 +222,17 @@ class UpdateInfo { switch (defaultTargetPlatform) { case TargetPlatform.windows: - // Prefer portable version for Windows - var portable = assets.where((asset) => asset.name.contains('Portable')).firstOrNull; + // If installed via MSIX, prefer MSIX for updates + if (UpdateService.isInstalledViaMsix()) { + var msix = assets.where((asset) => asset.name.endsWith('.msix')).firstOrNull; + if (msix != null) return msix; + } + + // Prefer portable self-extracting exe for non-MSIX installs + var portable = assets.where((asset) => asset.name.contains('Portable') && asset.name.endsWith('.exe')).firstOrNull; if (portable != null) return portable; - // Fall back to full Windows package + // Fall back to full Windows package (ZIP) var windows = assets.where((asset) => asset.name.contains('Windows') && asset.name.endsWith('.zip')).firstOrNull; if (windows != null) return windows; diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index ca0e8d2..93cc89f 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0 +version: 1.0.1 environment: sdk: '>=3.5.0 <4.0.0' diff --git a/flutter_app/windows/CMakeLists.txt b/flutter_app/windows/CMakeLists.txt index c423179..9a609b6 100644 --- a/flutter_app/windows/CMakeLists.txt +++ b/flutter_app/windows/CMakeLists.txt @@ -43,6 +43,10 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") + + # Use static runtime for truly portable builds (no VC++ Redistributable required) + set_property(TARGET ${TARGET} PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endfunction() # Flutter library and tool build rules.