Files
leon-ai--leon/scripts/setup/setup-runtime-binary.js
T
2026-07-13 12:44:08 +08:00

182 lines
4.5 KiB
JavaScript

import fs from 'node:fs'
import path from 'node:path'
import { FileHelper } from '@/helpers/file-helper'
import { SystemHelper } from '@/helpers/system-helper'
import { createSetupStatus } from './setup-status'
const MOVE_FALLBACK_ERROR_CODES = new Set(['EXDEV', 'EPERM', 'EBUSY', 'EACCES'])
function readManifest(manifestPath) {
if (!fs.existsSync(manifestPath)) {
return null
}
try {
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
} catch {
return null
}
}
async function removePath(targetPath) {
await fs.promises.rm(targetPath, { recursive: true, force: true })
}
function isMoveFallbackError(error) {
return (
error instanceof Error &&
'code' in error &&
MOVE_FALLBACK_ERROR_CODES.has(error.code)
)
}
async function movePath(sourcePath, destinationPath) {
try {
await fs.promises.rename(sourcePath, destinationPath)
} catch (error) {
if (!isMoveFallbackError(error)) {
throw error
}
await fs.promises.cp(sourcePath, destinationPath, {
recursive: true,
force: true
})
await removePath(sourcePath)
}
}
async function moveDirectoryContents(sourcePath, destinationPath) {
await fs.promises.mkdir(destinationPath, { recursive: true })
const entries = await fs.promises.readdir(sourcePath, {
withFileTypes: true
})
await Promise.all(
entries.map((entry) =>
movePath(
path.join(sourcePath, entry.name),
path.join(destinationPath, entry.name)
)
)
)
}
async function cleanInstallDirectory(basePath) {
await fs.promises.mkdir(basePath, { recursive: true })
const entries = await fs.promises.readdir(basePath, {
withFileTypes: true
})
await Promise.all(
entries
.filter((entry) => entry.name !== 'versions.json')
.map((entry) => removePath(path.join(basePath, entry.name)))
)
}
async function getFlattenedExtractionRoot(extractionPath) {
const entries = await fs.promises.readdir(extractionPath, {
withFileTypes: true
})
if (entries.length === 1 && entries[0]?.isDirectory()) {
return path.join(extractionPath, entries[0].name)
}
return extractionPath
}
/**
* Install a portable runtime into Leon's managed `bin/` directory from either
* an archive asset or a direct executable download.
*/
export async function setupRuntimeBinary({
name,
version,
basePath,
installPath,
manifestPath,
binaryPath,
downloadURL,
archiveFileName = null
}) {
const status = createSetupStatus(`Downloading and setting up ${name}...`).start()
const manifest = readManifest(manifestPath)
if (manifest?.version === version && fs.existsSync(binaryPath)) {
status.succeed(`${name}: ${version}`)
return
}
await cleanInstallDirectory(basePath)
try {
status.pause()
if (archiveFileName) {
const archivePath = path.join(basePath, archiveFileName)
const extractionPath = await fs.promises.mkdtemp(
path.join(basePath, `${name.toLowerCase()}-extract-`)
)
try {
await FileHelper.downloadFile(downloadURL, archivePath, {
cliProgress: true,
parallelStreams: 3,
skipExisting: false
})
status.text = `Installing ${name}...`
status.start()
await FileHelper.extractArchive(archivePath, extractionPath)
const flattenedRootPath = await getFlattenedExtractionRoot(extractionPath)
await moveDirectoryContents(flattenedRootPath, installPath)
} finally {
await Promise.all([
fs.promises.rm(archivePath, { force: true }),
fs.promises.rm(extractionPath, { recursive: true, force: true })
])
}
} else {
await fs.promises.mkdir(path.dirname(binaryPath), { recursive: true })
await FileHelper.downloadFile(downloadURL, binaryPath, {
cliProgress: true,
parallelStreams: 3,
skipExisting: false
})
status.text = `Finalizing ${name}...`
status.start()
}
if (!SystemHelper.isWindows()) {
await fs.promises.chmod(binaryPath, 0o755)
}
if (!fs.existsSync(binaryPath)) {
throw new Error(`Cannot find ${name} binary at "${binaryPath}"`)
}
await FileHelper.createManifestFile(manifestPath, name.toLowerCase(), version, {
os: SystemHelper.getInformation().type,
architecture: SystemHelper.getInformation().cpuArchitecture
})
status.succeed(`${name}: ${version}`)
} catch (error) {
if (status.isSpinning) {
status.fail(`Failed to set up ${name}`)
}
throw new Error(`Failed to set up ${name}: ${error}`)
}
}