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
This commit is contained in:
Nguyen Ngoc Minh 2025-06-03 23:00:04 +07:00 committed by GitHub
parent 7dc51c5e0f
commit 9c825956e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 181 additions and 35 deletions

View File

@ -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/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/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 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 # 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 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

View File

@ -171,6 +171,9 @@ jobs:
- name: Build app - name: Build app
shell: bash shell: bash
run: | 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 make build-tauri
env: env:
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }} AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}

View File

@ -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=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=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=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_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=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" File /a "/oname=vulkan-1.dll" "D:\a\jan\jan\src-tauri\binaries\vulkan-1.dll"

View File

@ -74,7 +74,7 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["nsis", "app", "dmg", "deb", "appimage"], "targets": ["nsis", "app", "dmg", "deb", "appimage"],
"createUpdaterArtifacts": false, "createUpdaterArtifacts": true,
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",

View File

@ -10,7 +10,7 @@ import { useModelProvider } from '@/hooks/useModelProvider'
import { useAppUpdater } from '@/hooks/useAppUpdater' import { useAppUpdater } from '@/hooks/useAppUpdater'
import { abortDownload } from '@/services/models' import { abortDownload } from '@/services/models'
import { getProviders } from '@/services/providers' 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 { IconDownload, IconX } from '@tabler/icons-react'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
@ -21,10 +21,67 @@ export function DownloadManagement() {
const [isPopoverOpen, setIsPopoverOpen] = useState(false) const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const { downloads, updateProgress, removeDownload } = useDownloadStore() const { downloads, updateProgress, removeDownload } = useDownloadStore()
const { updateState } = useAppUpdater() const { updateState } = useAppUpdater()
const downloadCount = useMemo(
() => Object.keys(downloads).length, const [appUpdateState, setAppUpdateState] = useState({
[downloads] 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( const downloadProcesses = useMemo(
() => () =>
Object.values(downloads).map((download) => ({ Object.values(downloads).map((download) => ({
@ -38,14 +95,31 @@ export function DownloadManagement() {
) )
const overallProgress = useMemo(() => { const overallProgress = useMemo(() => {
const total = downloadProcesses.reduce((acc, download) => { const modelTotal = downloadProcesses.reduce((acc, download) => {
return acc + download.total return acc + download.total
}, 0) }, 0)
const current = downloadProcesses.reduce((acc, download) => { const modelCurrent = downloadProcesses.reduce((acc, download) => {
return acc + download.current return acc + download.current
}, 0) }, 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 return total > 0 ? current / total : 0
}, [downloadProcesses]) }, [
downloadProcesses,
appUpdateState.isDownloading,
appUpdateState.totalBytes,
appUpdateState.downloadedBytes,
])
const onFileDownloadUpdate = useCallback( const onFileDownloadUpdate = useCallback(
async (state: DownloadState) => { async (state: DownloadState) => {
@ -97,18 +171,34 @@ export function DownloadManagement() {
events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess) events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
events.on(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped) 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 () => { return () => {
console.debug('DownloadListener: unregistering event listeners...') console.debug('DownloadListener: unregistering event listeners...')
events.off(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate) events.off(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
events.off(DownloadEvent.onFileDownloadError, onFileDownloadError) events.off(DownloadEvent.onFileDownloadError, onFileDownloadError)
events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess) events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
events.off(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped) 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, onFileDownloadUpdate,
onFileDownloadError, onFileDownloadError,
onFileDownloadSuccess, onFileDownloadSuccess,
onFileDownloadStopped, onFileDownloadStopped,
onAppUpdateDownloadUpdate,
onAppUpdateDownloadSuccess,
onAppUpdateDownloadError,
]) ])
function renderGB(bytes: number): string { function renderGB(bytes: number): string {
@ -118,8 +208,7 @@ export function DownloadManagement() {
return ( return (
<> <>
{(downloadCount > 0 || {downloadCount > 0 && (
(updateState.isDownloading && updateState.downloadProgress > 0)) && (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
{isLeftPanelOpen ? ( {isLeftPanelOpen ? (
@ -162,8 +251,7 @@ export function DownloadManagement() {
<p className="text-xs text-main-view-fg/70">Downloading</p> <p className="text-xs text-main-view-fg/70">Downloading</p>
</div> </div>
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2"> <div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
{!updateState.isDownloading && {appUpdateState.isDownloading && (
updateState.downloadProgress > 0 && (
<div className="bg-main-view-fg/4 rounded-md p-2"> <div className="bg-main-view-fg/4 rounded-md p-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="truncate text-main-view-fg/80"> <p className="truncate text-main-view-fg/80">
@ -171,12 +259,12 @@ export function DownloadManagement() {
</p> </p>
</div> </div>
<Progress <Progress
value={updateState.downloadProgress * 100} value={appUpdateState.downloadProgress * 100}
className="my-2" className="my-2"
/> />
<p className="text-main-view-fg/60 text-xs"> <p className="text-main-view-fg/60 text-xs">
{`${renderGB(updateState.downloadedBytes)} / ${renderGB(updateState.totalBytes)}`}{' '} {`${renderGB(appUpdateState.downloadedBytes)} / ${renderGB(appUpdateState.totalBytes)}`}{' '}
GB ({Math.round(updateState.downloadProgress * 100)}%) GB ({Math.round(appUpdateState.downloadProgress * 100)}%)
</p> </p>
</div> </div>
)} )}

View File

@ -9,10 +9,13 @@ import { RenderMarkdown } from '../RenderMarkdown'
import { isDev } from '@/lib/utils' import { isDev } from '@/lib/utils'
const DialogAppUpdater = () => { const DialogAppUpdater = () => {
const { updateState, downloadAndInstallUpdate, checkForUpdate } = const {
useAppUpdater() updateState,
downloadAndInstallUpdate,
checkForUpdate,
setRemindMeLater,
} = useAppUpdater()
const [showReleaseNotes, setShowReleaseNotes] = useState(false) const [showReleaseNotes, setShowReleaseNotes] = useState(false)
const [remindMeLater, setRemindMeLater] = useState(false)
const handleUpdate = () => { const handleUpdate = () => {
downloadAndInstallUpdate() downloadAndInstallUpdate()
@ -35,7 +38,7 @@ const DialogAppUpdater = () => {
checkForUpdate() checkForUpdate()
}, [checkForUpdate]) }, [checkForUpdate])
if (remindMeLater) return null if (updateState.remindMeLater) return null
return ( return (
<> <>

View File

@ -1,6 +1,7 @@
import { isDev } from '@/lib/utils' import { isDev } from '@/lib/utils'
import { check, Update } from '@tauri-apps/plugin-updater' import { check, Update } from '@tauri-apps/plugin-updater'
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { events, AppEvent } from '@janhq/core'
export interface UpdateState { export interface UpdateState {
isUpdateAvailable: boolean isUpdateAvailable: boolean
@ -9,6 +10,7 @@ export interface UpdateState {
downloadProgress: number downloadProgress: number
downloadedBytes: number downloadedBytes: number
totalBytes: number totalBytes: number
remindMeLater: boolean
} }
export const useAppUpdater = () => { export const useAppUpdater = () => {
@ -19,10 +21,19 @@ export const useAppUpdater = () => {
downloadProgress: 0, downloadProgress: 0,
downloadedBytes: 0, downloadedBytes: 0,
totalBytes: 0, totalBytes: 0,
remindMeLater: false,
}) })
const checkForUpdate = useCallback(async () => { const checkForUpdate = useCallback(async (resetRemindMeLater = false) => {
try { try {
// Reset remindMeLater if requested (e.g., when called from settings)
if (resetRemindMeLater) {
setUpdateState((prev) => ({
...prev,
remindMeLater: false,
}))
}
if (!isDev()) { if (!isDev()) {
// Production mode - use actual Tauri updater // Production mode - use actual Tauri updater
const update = await check() const update = await check()
@ -45,6 +56,13 @@ export const useAppUpdater = () => {
return null return null
} }
} else {
setUpdateState((prev) => ({
...prev,
isUpdateAvailable: false,
updateInfo: null,
}))
return null
} }
} catch (error) { } catch (error) {
console.error('Error checking for updates:', 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 () => { const downloadAndInstallUpdate = useCallback(async () => {
if (!updateState.updateInfo) return if (!updateState.updateInfo) return
@ -79,6 +104,13 @@ export const useAppUpdater = () => {
totalBytes: contentLength, totalBytes: contentLength,
})) }))
console.log(`Started downloading ${contentLength} bytes`) console.log(`Started downloading ${contentLength} bytes`)
// Emit app update download started event
events.emit(AppEvent.onAppUpdateDownloadUpdate, {
progress: 0,
downloadedBytes: 0,
totalBytes: contentLength,
})
break break
case 'Progress': { case 'Progress': {
downloaded += event.data.chunkLength downloaded += event.data.chunkLength
@ -89,6 +121,13 @@ export const useAppUpdater = () => {
downloadedBytes: downloaded, downloadedBytes: downloaded,
})) }))
console.log(`Downloaded ${downloaded} from ${contentLength}`) console.log(`Downloaded ${downloaded} from ${contentLength}`)
// Emit app update download progress event
events.emit(AppEvent.onAppUpdateDownloadUpdate, {
progress: progress,
downloadedBytes: downloaded,
totalBytes: contentLength,
})
break break
} }
case 'Finished': case 'Finished':
@ -98,10 +137,15 @@ export const useAppUpdater = () => {
isDownloading: false, isDownloading: false,
downloadProgress: 1, downloadProgress: 1,
})) }))
// Emit app update download success event
events.emit(AppEvent.onAppUpdateDownloadSuccess, {})
break break
} }
}) })
await window.core?.api?.relaunch()
console.log('Update installed') console.log('Update installed')
} catch (error) { } catch (error) {
console.error('Error downloading update:', error) console.error('Error downloading update:', error)
@ -109,6 +153,11 @@ export const useAppUpdater = () => {
...prev, ...prev,
isDownloading: false, isDownloading: false,
})) }))
// Emit app update download error event
events.emit(AppEvent.onAppUpdateDownloadError, {
message: error instanceof Error ? error.message : 'Unknown error',
})
} }
}, [updateState.updateInfo]) }, [updateState.updateInfo])
@ -116,5 +165,6 @@ export const useAppUpdater = () => {
updateState, updateState,
checkForUpdate, checkForUpdate,
downloadAndInstallUpdate, downloadAndInstallUpdate,
setRemindMeLater,
} }
} }

View File

@ -63,7 +63,7 @@ const openFileTitle = (): string => {
function General() { function General() {
const { t } = useTranslation() const { t } = useTranslation()
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting() const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
const { checkForUpdate } = useAppUpdater() const { checkForUpdate, setRemindMeLater } = useAppUpdater()
const [janDataFolder, setJanDataFolder] = useState<string | undefined>() const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
const [isCopied, setIsCopied] = useState(false) const [isCopied, setIsCopied] = useState(false)
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null) const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null)
@ -181,10 +181,11 @@ function General() {
const handleCheckForUpdate = async () => { const handleCheckForUpdate = async () => {
setIsCheckingUpdate(true) setIsCheckingUpdate(true)
setRemindMeLater(false)
try { try {
if (isDev()) if (isDev())
return toast.info('You are running a development version of Jan!') return toast.info('You are running a development version of Jan!')
const update = await checkForUpdate() const update = await checkForUpdate(true)
if (!update) { if (!update) {
toast.info('You are using the latest version of Jan!') toast.info('You are using the latest version of Jan!')
} }