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

View File

@ -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 }}

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=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"

View File

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

View File

@ -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 && (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
{isLeftPanelOpen ? (
@ -162,24 +251,23 @@ export function DownloadManagement() {
<p className="text-xs text-main-view-fg/70">Downloading</p>
</div>
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
{!updateState.isDownloading &&
updateState.downloadProgress > 0 && (
<div className="bg-main-view-fg/4 rounded-md p-2">
<div className="flex items-center justify-between">
<p className="truncate text-main-view-fg/80">
App Update
</p>
</div>
<Progress
value={updateState.downloadProgress * 100}
className="my-2"
/>
<p className="text-main-view-fg/60 text-xs">
{`${renderGB(updateState.downloadedBytes)} / ${renderGB(updateState.totalBytes)}`}{' '}
GB ({Math.round(updateState.downloadProgress * 100)}%)
{appUpdateState.isDownloading && (
<div className="bg-main-view-fg/4 rounded-md p-2">
<div className="flex items-center justify-between">
<p className="truncate text-main-view-fg/80">
App Update
</p>
</div>
)}
<Progress
value={appUpdateState.downloadProgress * 100}
className="my-2"
/>
<p className="text-main-view-fg/60 text-xs">
{`${renderGB(appUpdateState.downloadedBytes)} / ${renderGB(appUpdateState.totalBytes)}`}{' '}
GB ({Math.round(appUpdateState.downloadProgress * 100)}%)
</p>
</div>
)}
{downloadProcesses.map((download) => (
<div className="bg-main-view-fg/4 rounded-md p-2">
<div className="flex items-center justify-between">

View File

@ -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 (
<>

View File

@ -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,
}
}

View File

@ -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<string | undefined>()
const [isCopied, setIsCopied] = useState(false)
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(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!')
}