diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b321aabd..e0ca18c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -353,6 +353,44 @@ jobs: echo "Uploading $bin_file to GitHub release" gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file --clobber + echo "Signing portable binary" + pnpm tauri signer sign $bin_file + echo "Uploading signature to GitHub release" + gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file.sig --clobber + + - name: download and update latest.json for Windows portable release + if: matrix.config.os == 'windows-latest' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + curl -sL https://github.com/readest/readest/releases/latest/download/latest.json -o latest.json + + version=${{ needs.get-release.outputs.release_version }} + arch=${{ matrix.config.arch }} + + if [ "$arch" = "x86_64" ]; then + bin_file="Readest_${version}_x64-portable.exe" + platform_key="windows-x86_64-portable" + elif [ "$arch" = "aarch64" ]; then + bin_file="Readest_${version}_arm64-portable.exe" + platform_key="windows-aarch64-portable" + else + echo "Unknown architecture: $arch" + exit 1 + fi + + portable_url="https://github.com/readest/readest/releases/download/${{ needs.get-release.outputs.release_tag }}/$bin_file" + portable_sig=$(cat $bin_file.sig) + + jq --arg url "$portable_url" \ + --arg sig "$portable_sig" \ + --arg key "$platform_key" \ + '.platforms[$key] = {signature: $sig, url: $url}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json + + echo "Uploading updated latest.json to GitHub release" + gh release upload ${{ needs.get-release.outputs.release_tag }} latest.json --clobber + upload-to-r2: needs: [get-release, build-tauri] uses: ./.github/workflows/upload-to-r2.yml diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index d2a175d0..9f250e30 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -152,6 +152,16 @@ } ] }, + { + "identifier": "shell:allow-spawn", + "allow": [ + { + "name": "start-readest", + "cmd": "cmd", + "args": ["/C", "start", "", { "validator": "^.*Readest(.*)\\.exe$" }] + } + ] + }, "haptics:allow-vibrate", "haptics:allow-impact-feedback", "haptics:allow-notification-feedback", diff --git a/apps/readest-app/src/components/UpdaterWindow.tsx b/apps/readest-app/src/components/UpdaterWindow.tsx index b3725c27..6c6b05b5 100644 --- a/apps/readest-app/src/components/UpdaterWindow.tsx +++ b/apps/readest-app/src/components/UpdaterWindow.tsx @@ -5,8 +5,10 @@ import { useEnv } from '@/context/EnvContext'; import { useEffect, useState } from 'react'; import { type as osType, arch as osArch } from '@tauri-apps/plugin-os'; import { check, Update } from '@tauri-apps/plugin-updater'; -import { relaunch } from '@tauri-apps/plugin-process'; +import { relaunch, exit } from '@tauri-apps/plugin-process'; import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; +import { Command } from '@tauri-apps/plugin-shell'; +import { invoke } from '@tauri-apps/api/core'; import { isTauriAppPlatform } from '@/services/environment'; import { useTranslator } from '@/hooks/useTranslator'; import { useTranslation } from '@/hooks/useTranslation'; @@ -14,6 +16,7 @@ import { useSearchParams } from 'next/navigation'; import { getAppVersion } from '@/utils/version'; import { tauriDownload } from '@/utils/transfer'; import { installPackage } from '@/utils/bridge'; +import { join } from '@tauri-apps/api/path'; import { getLocale } from '@/utils/misc'; import { setLastShownReleaseNotesVersion } from '@/helpers/updater'; import { READEST_UPDATER_FILE, READEST_CHANGELOG_FILE } from '@/services/constants'; @@ -167,9 +170,76 @@ export const UpdaterContent = ({ } as GenericUpdate); } }; + const checkWindowsPortableUpdate = async () => { + if (!appService) return; + const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch; + const response = await fetch(READEST_UPDATER_FILE); + const data = await response.json(); + if (semver.gt(data.version, currentVersion)) { + const OS_ARCH = osArch(); + const platformKey = + OS_ARCH === 'x86_64' ? 'windows-x86_64-portable' : 'windows-aarch64-portable'; + const arch = OS_ARCH === 'x86_64' ? 'x64' : 'arm64'; + const downloadUrl = data.platforms[platformKey]?.url as string; + const execDir = await invoke('get_executable_dir'); + const exeFileName = `Readest_${data.version}_${arch}-portable.exe`; + const exeFilePath = await join(execDir, exeFileName); + setUpdate({ + currentVersion, + version: data.version, + date: data.pub_date, + body: data.notes, + downloadAndInstall: async (onEvent) => { + await new Promise(async (resolve, reject) => { + let downloaded = 0; + let total = 0; + await tauriDownload(downloadUrl, exeFilePath, (progress) => { + if (!onEvent) return; + if (!total && progress.total) { + total = progress.total; + onEvent({ + event: 'Started', + data: { contentLength: total }, + }); + } else if (downloaded > 0 && progress.progress === progress.total) { + console.log('Portable EXE downloaded to', exeFilePath); + onEvent?.({ event: 'Finished' }); + setTimeout(() => { + resolve(); + }, 1000); + } + + onEvent({ + event: 'Progress', + data: { chunkLength: progress.progress - downloaded }, + }); + downloaded = progress.progress; + }).catch((error) => { + console.error('Download failed:', error); + reject(error); + }); + }); + + try { + console.log('Launching new executable:', exeFilePath); + const command = Command.create('start-readest', ['/C', 'start', '', exeFilePath]); + await command.spawn(); + console.log('New executable launched, exiting current app...'); + setTimeout(async () => { + await exit(0); + }, 500); + } catch (error) { + console.error('Failed to launch new executable:', error); + } + }, + } as GenericUpdate); + } + }; const checkForUpdates = async () => { const OS_TYPE = osType(); - if (['macos', 'windows', 'linux'].includes(OS_TYPE)) { + if (appService?.isPortableApp && OS_TYPE === 'windows') { + checkWindowsPortableUpdate(); + } else if (['macos', 'windows', 'linux'].includes(OS_TYPE)) { checkDesktopUpdate(); } else if (OS_TYPE === 'android') { checkAndroidUpdate();