From 9c825956e87de1a06f3574731c56ce1f536fddc8 Mon Sep 17 00:00:00 2001 From: Nguyen Ngoc Minh <91668012+Minh141120@users.noreply.github.com> Date: Tue, 3 Jun 2025 23:00:04 +0700 Subject: [PATCH] ci: tauri build macos (#5184) * ci: tauri build macos * chore: comment out electron builder .zip.sig file to s3 * chore: enable auto updater tauri * chore: comment out s3 upload mac.zip.sig * chore: handle remind me later state * chore: add dll file windows * chore: add debug step verbose * ci: add msvcp140_codecvt_ids.dll bundle windows * chore: update download progress * chore: update app updater UI * chore: remove log * chore: reload app after download app * chore: reset remindmelater --- .../workflows/template-tauri-build-macos.yml | 2 +- .../template-tauri-build-windows-x64.yml | 3 + src-tauri/tauri.bundle.windows.nsis.template | 3 +- src-tauri/tauri.conf.json | 2 +- web-app/src/containers/DownloadManegement.tsx | 138 ++++++++++++++---- web-app/src/containers/dialogs/AppUpdater.tsx | 11 +- web-app/src/hooks/useAppUpdater.ts | 52 ++++++- web-app/src/routes/settings/general.tsx | 5 +- 8 files changed, 181 insertions(+), 35 deletions(-) diff --git a/.github/workflows/template-tauri-build-macos.yml b/.github/workflows/template-tauri-build-macos.yml index e3c457433..d2cceedad 100644 --- a/.github/workflows/template-tauri-build-macos.yml +++ b/.github/workflows/template-tauri-build-macos.yml @@ -243,7 +243,7 @@ jobs: aws s3 cp ./macos/latest-mac.yml s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/latest-mac.yml aws s3 cp ./macos/beta-mac.yml s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/beta-mac.yml aws s3 cp ./macos/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip - aws s3 cp ./macos/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip.sig + # aws s3 cp ./macos/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip.sig # Upload for tauri updater aws s3 cp ./dmg/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_universal.dmg s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_universal.dmg diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml index 54c236485..3b824ee61 100644 --- a/.github/workflows/template-tauri-build-windows-x64.yml +++ b/.github/workflows/template-tauri-build-windows-x64.yml @@ -171,6 +171,9 @@ jobs: - name: Build app shell: bash run: | + curl -L -o ./src-tauri/binaries/vcomp140.dll https://catalog.jan.ai/vcomp140.dll + curl -L -o ./src-tauri/binaries/msvcp140_codecvt_ids.dll https://catalog.jan.ai/msvcp140_codecvt_ids.dll + ls ./src-tauri/binaries make build-tauri env: AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }} diff --git a/src-tauri/tauri.bundle.windows.nsis.template b/src-tauri/tauri.bundle.windows.nsis.template index ad037ebf8..2da569bac 100644 --- a/src-tauri/tauri.bundle.windows.nsis.template +++ b/src-tauri/tauri.bundle.windows.nsis.template @@ -640,7 +640,8 @@ Section Install File /a "/oname=cublasLt64_12.dll" "D:\a\jan\jan\src-tauri\binaries\cublasLt64_12.dll" File /a "/oname=cudart64_12.dll" "D:\a\jan\jan\src-tauri\binaries\cudart64_12.dll" File /a "/oname=msvcp140.dll" "D:\a\jan\jan\src-tauri\binaries\msvcp140.dll" - ; File /a "/oname=vcomp140.dll" "D:\a\jan\jan\src-tauri\binaries\vcomp140.dll" + File /a "/oname=vcomp140.dll" "D:\a\jan\jan\src-tauri\binaries\vcomp140.dll" + File /a "/oname=msvcp140_codecvt_ids.dll" "D:\a\jan\jan\src-tauri\binaries\msvcp140_codecvt_ids.dll" File /a "/oname=vcruntime140_1.dll" "D:\a\jan\jan\src-tauri\binaries\vcruntime140_1.dll" File /a "/oname=vcruntime140.dll" "D:\a\jan\jan\src-tauri\binaries\vcruntime140.dll" File /a "/oname=vulkan-1.dll" "D:\a\jan\jan\src-tauri\binaries\vulkan-1.dll" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 545873172..80f107dc4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -74,7 +74,7 @@ "bundle": { "active": true, "targets": ["nsis", "app", "dmg", "deb", "appimage"], - "createUpdaterArtifacts": false, + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/web-app/src/containers/DownloadManegement.tsx b/web-app/src/containers/DownloadManegement.tsx index f87b1e6be..2b66a022c 100644 --- a/web-app/src/containers/DownloadManegement.tsx +++ b/web-app/src/containers/DownloadManegement.tsx @@ -10,7 +10,7 @@ import { useModelProvider } from '@/hooks/useModelProvider' import { useAppUpdater } from '@/hooks/useAppUpdater' import { abortDownload } from '@/services/models' import { getProviders } from '@/services/providers' -import { DownloadEvent, DownloadState, events } from '@janhq/core' +import { DownloadEvent, DownloadState, events, AppEvent } from '@janhq/core' import { IconDownload, IconX } from '@tabler/icons-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' @@ -21,10 +21,67 @@ export function DownloadManagement() { const [isPopoverOpen, setIsPopoverOpen] = useState(false) const { downloads, updateProgress, removeDownload } = useDownloadStore() const { updateState } = useAppUpdater() - const downloadCount = useMemo( - () => Object.keys(downloads).length, - [downloads] + + const [appUpdateState, setAppUpdateState] = useState({ + isDownloading: false, + downloadProgress: 0, + downloadedBytes: 0, + totalBytes: 0, + }) + + useEffect(() => { + setAppUpdateState({ + isDownloading: updateState.isDownloading, + downloadProgress: updateState.downloadProgress, + downloadedBytes: updateState.downloadedBytes, + totalBytes: updateState.totalBytes, + }) + }, [updateState]) + + const onAppUpdateDownloadUpdate = useCallback( + (data: { + progress?: number + downloadedBytes?: number + totalBytes?: number + }) => { + setAppUpdateState((prev) => ({ + ...prev, + isDownloading: true, + downloadProgress: data.progress || 0, + downloadedBytes: data.downloadedBytes || 0, + totalBytes: data.totalBytes || 0, + })) + }, + [] ) + + const onAppUpdateDownloadSuccess = useCallback(() => { + setAppUpdateState((prev) => ({ + ...prev, + isDownloading: false, + downloadProgress: 1, + })) + toast.success('App Update Downloaded', { + description: 'The app update has been downloaded successfully.', + }) + }, []) + + const onAppUpdateDownloadError = useCallback(() => { + setAppUpdateState((prev) => ({ + ...prev, + isDownloading: false, + })) + toast.error('App Update Download Failed', { + description: 'Failed to download the app update. Please try again.', + }) + }, []) + + const downloadCount = useMemo(() => { + const modelDownloads = Object.keys(downloads).length + const appUpdateDownload = appUpdateState.isDownloading ? 1 : 0 + const total = modelDownloads + appUpdateDownload + return total + }, [downloads, appUpdateState.isDownloading]) const downloadProcesses = useMemo( () => Object.values(downloads).map((download) => ({ @@ -38,14 +95,31 @@ export function DownloadManagement() { ) const overallProgress = useMemo(() => { - const total = downloadProcesses.reduce((acc, download) => { + const modelTotal = downloadProcesses.reduce((acc, download) => { return acc + download.total }, 0) - const current = downloadProcesses.reduce((acc, download) => { + const modelCurrent = downloadProcesses.reduce((acc, download) => { return acc + download.current }, 0) + + // Include app update progress in overall calculation + const appUpdateTotal = appUpdateState.isDownloading + ? appUpdateState.totalBytes + : 0 + const appUpdateCurrent = appUpdateState.isDownloading + ? appUpdateState.downloadedBytes + : 0 + + const total = modelTotal + appUpdateTotal + const current = modelCurrent + appUpdateCurrent + return total > 0 ? current / total : 0 - }, [downloadProcesses]) + }, [ + downloadProcesses, + appUpdateState.isDownloading, + appUpdateState.totalBytes, + appUpdateState.downloadedBytes, + ]) const onFileDownloadUpdate = useCallback( async (state: DownloadState) => { @@ -97,18 +171,34 @@ export function DownloadManagement() { events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess) events.on(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped) + // Register app update event listeners + events.on(AppEvent.onAppUpdateDownloadUpdate, onAppUpdateDownloadUpdate) + events.on(AppEvent.onAppUpdateDownloadSuccess, onAppUpdateDownloadSuccess) + events.on(AppEvent.onAppUpdateDownloadError, onAppUpdateDownloadError) + return () => { console.debug('DownloadListener: unregistering event listeners...') events.off(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate) events.off(DownloadEvent.onFileDownloadError, onFileDownloadError) events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess) events.off(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped) + + // Unregister app update event listeners + events.off(AppEvent.onAppUpdateDownloadUpdate, onAppUpdateDownloadUpdate) + events.off( + AppEvent.onAppUpdateDownloadSuccess, + onAppUpdateDownloadSuccess + ) + events.off(AppEvent.onAppUpdateDownloadError, onAppUpdateDownloadError) } }, [ onFileDownloadUpdate, onFileDownloadError, onFileDownloadSuccess, onFileDownloadStopped, + onAppUpdateDownloadUpdate, + onAppUpdateDownloadSuccess, + onAppUpdateDownloadError, ]) function renderGB(bytes: number): string { @@ -118,8 +208,7 @@ export function DownloadManagement() { return ( <> - {(downloadCount > 0 || - (updateState.isDownloading && updateState.downloadProgress > 0)) && ( + {downloadCount > 0 && ( {isLeftPanelOpen ? ( @@ -162,24 +251,23 @@ export function DownloadManagement() {

Downloading

- {!updateState.isDownloading && - updateState.downloadProgress > 0 && ( -
-
-

- App Update -

-
- -

- {`${renderGB(updateState.downloadedBytes)} / ${renderGB(updateState.totalBytes)}`}{' '} - GB ({Math.round(updateState.downloadProgress * 100)}%) + {appUpdateState.isDownloading && ( +

+
+

+ App Update

- )} + +

+ {`${renderGB(appUpdateState.downloadedBytes)} / ${renderGB(appUpdateState.totalBytes)}`}{' '} + GB ({Math.round(appUpdateState.downloadProgress * 100)}%) +

+
+ )} {downloadProcesses.map((download) => (
diff --git a/web-app/src/containers/dialogs/AppUpdater.tsx b/web-app/src/containers/dialogs/AppUpdater.tsx index 5034df103..79ede8aa7 100644 --- a/web-app/src/containers/dialogs/AppUpdater.tsx +++ b/web-app/src/containers/dialogs/AppUpdater.tsx @@ -9,10 +9,13 @@ import { RenderMarkdown } from '../RenderMarkdown' import { isDev } from '@/lib/utils' const DialogAppUpdater = () => { - const { updateState, downloadAndInstallUpdate, checkForUpdate } = - useAppUpdater() + const { + updateState, + downloadAndInstallUpdate, + checkForUpdate, + setRemindMeLater, + } = useAppUpdater() const [showReleaseNotes, setShowReleaseNotes] = useState(false) - const [remindMeLater, setRemindMeLater] = useState(false) const handleUpdate = () => { downloadAndInstallUpdate() @@ -35,7 +38,7 @@ const DialogAppUpdater = () => { checkForUpdate() }, [checkForUpdate]) - if (remindMeLater) return null + if (updateState.remindMeLater) return null return ( <> diff --git a/web-app/src/hooks/useAppUpdater.ts b/web-app/src/hooks/useAppUpdater.ts index 19675ff0f..7b4245f74 100644 --- a/web-app/src/hooks/useAppUpdater.ts +++ b/web-app/src/hooks/useAppUpdater.ts @@ -1,6 +1,7 @@ import { isDev } from '@/lib/utils' import { check, Update } from '@tauri-apps/plugin-updater' import { useState, useCallback } from 'react' +import { events, AppEvent } from '@janhq/core' export interface UpdateState { isUpdateAvailable: boolean @@ -9,6 +10,7 @@ export interface UpdateState { downloadProgress: number downloadedBytes: number totalBytes: number + remindMeLater: boolean } export const useAppUpdater = () => { @@ -19,10 +21,19 @@ export const useAppUpdater = () => { downloadProgress: 0, downloadedBytes: 0, totalBytes: 0, + remindMeLater: false, }) - const checkForUpdate = useCallback(async () => { + const checkForUpdate = useCallback(async (resetRemindMeLater = false) => { try { + // Reset remindMeLater if requested (e.g., when called from settings) + if (resetRemindMeLater) { + setUpdateState((prev) => ({ + ...prev, + remindMeLater: false, + })) + } + if (!isDev()) { // Production mode - use actual Tauri updater const update = await check() @@ -45,6 +56,13 @@ export const useAppUpdater = () => { return null } + } else { + setUpdateState((prev) => ({ + ...prev, + isUpdateAvailable: false, + updateInfo: null, + })) + return null } } catch (error) { console.error('Error checking for updates:', error) @@ -58,6 +76,13 @@ export const useAppUpdater = () => { } }, []) + const setRemindMeLater = useCallback((remind: boolean) => { + setUpdateState((prev) => ({ + ...prev, + remindMeLater: remind, + })) + }, []) + const downloadAndInstallUpdate = useCallback(async () => { if (!updateState.updateInfo) return @@ -79,6 +104,13 @@ export const useAppUpdater = () => { totalBytes: contentLength, })) console.log(`Started downloading ${contentLength} bytes`) + + // Emit app update download started event + events.emit(AppEvent.onAppUpdateDownloadUpdate, { + progress: 0, + downloadedBytes: 0, + totalBytes: contentLength, + }) break case 'Progress': { downloaded += event.data.chunkLength @@ -89,6 +121,13 @@ export const useAppUpdater = () => { downloadedBytes: downloaded, })) console.log(`Downloaded ${downloaded} from ${contentLength}`) + + // Emit app update download progress event + events.emit(AppEvent.onAppUpdateDownloadUpdate, { + progress: progress, + downloadedBytes: downloaded, + totalBytes: contentLength, + }) break } case 'Finished': @@ -98,10 +137,15 @@ export const useAppUpdater = () => { isDownloading: false, downloadProgress: 1, })) + + // Emit app update download success event + events.emit(AppEvent.onAppUpdateDownloadSuccess, {}) break } }) + await window.core?.api?.relaunch() + console.log('Update installed') } catch (error) { console.error('Error downloading update:', error) @@ -109,6 +153,11 @@ export const useAppUpdater = () => { ...prev, isDownloading: false, })) + + // Emit app update download error event + events.emit(AppEvent.onAppUpdateDownloadError, { + message: error instanceof Error ? error.message : 'Unknown error', + }) } }, [updateState.updateInfo]) @@ -116,5 +165,6 @@ export const useAppUpdater = () => { updateState, checkForUpdate, downloadAndInstallUpdate, + setRemindMeLater, } } diff --git a/web-app/src/routes/settings/general.tsx b/web-app/src/routes/settings/general.tsx index f3aae9e6e..62ee09edd 100644 --- a/web-app/src/routes/settings/general.tsx +++ b/web-app/src/routes/settings/general.tsx @@ -63,7 +63,7 @@ const openFileTitle = (): string => { function General() { const { t } = useTranslation() const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting() - const { checkForUpdate } = useAppUpdater() + const { checkForUpdate, setRemindMeLater } = useAppUpdater() const [janDataFolder, setJanDataFolder] = useState() const [isCopied, setIsCopied] = useState(false) const [selectedNewPath, setSelectedNewPath] = useState(null) @@ -181,10 +181,11 @@ function General() { const handleCheckForUpdate = async () => { setIsCheckingUpdate(true) + setRemindMeLater(false) try { if (isDev()) return toast.info('You are running a development version of Jan!') - const update = await checkForUpdate() + const update = await checkForUpdate(true) if (!update) { toast.info('You are using the latest version of Jan!') }