refactor: simplify bun and uv installation scripts for improved clarity and functionality

- Removed the getLatestBunVersion and getLatestUvVersion functions to streamline version handling.
- Updated download functions to use temporary filenames and ensure proper cleanup of downloaded files.
- Enhanced directory management by ensuring the output directory is correctly referenced and cleaned up if empty.
- Refactored the install functions to directly use detected platform and architecture, improving readability and maintainability.
This commit is contained in:
kangfenmao 2025-03-17 17:25:56 +08:00
parent 6a187fd370
commit a83c153531
3 changed files with 94 additions and 218 deletions

View File

@ -24,55 +24,6 @@ const BUN_PACKAGES = {
'linux-musl-arm64': 'bun-linux-aarch64-musl.zip' 'linux-musl-arm64': 'bun-linux-aarch64-musl.zip'
} }
/**
* Fetches the latest version of bun from GitHub API
* @returns {Promise<string>} The latest version tag (without 'bun-v' prefix)
*/
async function getLatestBunVersion() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: '/repos/oven-sh/bun/releases/latest',
headers: {
'User-Agent': 'cherry-studio-install-script'
}
}
const req = https.get(options, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Request failed with status code ${res.statusCode}`))
return
}
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
try {
const release = JSON.parse(data)
// Remove the 'bun-v' prefix if present
const version = release.tag_name.startsWith('bun-v')
? release.tag_name.substring(5)
: release.tag_name.startsWith('v')
? release.tag_name.substring(1)
: release.tag_name
resolve(version)
} catch (error) {
reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
}
})
})
req.on('error', (error) => {
reject(new Error(`Failed to fetch latest version: ${error.message}`))
})
req.end()
})
}
/** /**
* Downloads a file from a URL with redirect handling * Downloads a file from a URL with redirect handling
* @param {string} url The URL to download from * @param {string} url The URL to download from
@ -129,34 +80,36 @@ async function downloadBunBinary(platform, arch, version = DEFAULT_BUN_VERSION,
} }
// Create output directory structure // Create output directory structure
const archDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
// Ensure directories exist // Ensure directories exist
fs.mkdirSync(archDir, { recursive: true }) fs.mkdirSync(binDir, { recursive: true })
// Download URL for the specific binary // Download URL for the specific binary
const downloadUrl = `${BUN_RELEASE_BASE_URL}/bun-v${version}/${packageName}` const downloadUrl = `${BUN_RELEASE_BASE_URL}/bun-v${version}/${packageName}`
const tempdir = os.tmpdir() const tempdir = os.tmpdir()
// Create a temporary file for the downloaded binary // Create a temporary file for the downloaded binary
const localFilename = path.join(tempdir, packageName) const tempFilename = path.join(tempdir, packageName)
try { try {
console.log(`Downloading bun ${version} for ${platformKey}...`) console.log(`Downloading bun ${version} for ${platformKey}...`)
console.log(`URL: ${downloadUrl}`) console.log(`URL: ${downloadUrl}`)
// Use the new download function // Use the new download function
await downloadWithRedirects(downloadUrl, localFilename) await downloadWithRedirects(downloadUrl, tempFilename)
// Extract the zip file using adm-zip // Extract the zip file using adm-zip
console.log(`Extracting ${packageName} to ${archDir}...`) console.log(`Extracting ${packageName} to ${binDir}...`)
const zip = new AdmZip(localFilename) const zip = new AdmZip(tempFilename)
zip.extractAllTo(tempdir, true) zip.extractAllTo(tempdir, true)
// Move files using Node.js fs // Move files using Node.js fs
const sourceDir = path.join(tempdir, packageName.split('.')[0]) const sourceDir = path.join(tempdir, packageName.split('.')[0])
const files = fs.readdirSync(sourceDir) const files = fs.readdirSync(sourceDir)
for (const file of files) { for (const file of files) {
const sourcePath = path.join(sourceDir, file) const sourcePath = path.join(sourceDir, file)
const destPath = path.join(archDir, file) const destPath = path.join(binDir, file)
fs.renameSync(sourcePath, destPath) fs.renameSync(sourcePath, destPath)
// Set executable permissions for non-Windows platforms // Set executable permissions for non-Windows platforms
@ -171,16 +124,29 @@ async function downloadBunBinary(platform, arch, version = DEFAULT_BUN_VERSION,
} }
// Clean up // Clean up
fs.unlinkSync(localFilename) fs.unlinkSync(tempFilename)
fs.rmdirSync(sourceDir, { recursive: true }) fs.rmSync(sourceDir, { recursive: true })
console.log(`Successfully installed bun ${version} for ${platformKey}`) console.log(`Successfully installed bun ${version} for ${platformKey}`)
return true return true
} catch (error) { } catch (error) {
console.error(`Error installing bun for ${platformKey}: ${error.message}`) console.error(`Error installing bun for ${platformKey}: ${error.message}`)
if (fs.existsSync(localFilename)) { // Clean up temporary file if it exists
fs.unlinkSync(localFilename) if (fs.existsSync(tempFilename)) {
fs.unlinkSync(tempFilename)
} }
// Check if binDir is empty and remove it if so
try {
const files = fs.readdirSync(binDir)
if (files.length === 0) {
fs.rmSync(binDir, { recursive: true })
console.log(`Removed empty directory: ${binDir}`)
}
} catch (cleanupError) {
console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`)
}
return false return false
} }
} }
@ -192,8 +158,9 @@ function detectPlatformAndArch() {
const platform = os.platform() const platform = os.platform()
const arch = os.arch() const arch = os.arch()
const isMusl = platform === 'linux' && detectIsMusl() const isMusl = platform === 'linux' && detectIsMusl()
const isBaseline = platform === 'win32'
return { platform, arch, isMusl } return { platform, arch, isMusl, isBaseline }
} }
/** /**
@ -213,61 +180,17 @@ function detectIsMusl() {
* Main function to install bun * Main function to install bun
*/ */
async function installBun() { async function installBun() {
const args = process.argv.slice(2)
const specifiedVersion = args.find((arg) => !arg.startsWith('--'))
// Get the latest version if no specific version is provided // Get the latest version if no specific version is provided
const version = specifiedVersion || (await getLatestBunVersion()) const version = DEFAULT_BUN_VERSION
console.log(`Using bun version: ${version}`) console.log(`Using bun version: ${version}`)
const specificPlatform = args.find((arg) => arg.startsWith('--platform='))?.split('=')[1] const { platform, arch, isMusl, isBaseline } = detectPlatformAndArch()
const specificArch = args.find((arg) => arg.startsWith('--arch='))?.split('=')[1]
const specificMusl = args.includes('--musl')
const specificBaseline = args.includes('--baseline')
const installAll = args.includes('--all')
if (installAll) { console.log(
console.log(`Installing all bun ${version} binaries...`) `Installing bun ${version} for ${platform}-${arch}${isMusl ? ' (MUSL)' : ''}${isBaseline ? ' (baseline)' : ''}...`
for (const platformKey in BUN_PACKAGES) { )
let platform,
arch,
isMusl = false,
isBaseline = false
if (platformKey.includes('-musl-')) { await downloadBunBinary(platform, arch, version, isMusl, isBaseline)
const [platformPart, archPart] = platformKey.split('-musl-')
platform = platformPart
isMusl = true
if (archPart.includes('-baseline')) {
;[arch] = archPart.split('-baseline')
isBaseline = true
} else {
arch = archPart
}
} else if (platformKey.includes('-baseline')) {
const [platformPart, archPart] = platformKey.split('-')
platform = platformPart
arch = archPart.replace('-baseline', '')
isBaseline = true
} else {
;[platform, arch] = platformKey.split('-')
}
await downloadBunBinary(platform, arch, version, isMusl, isBaseline)
}
} else {
const { platform, arch, isMusl } = detectPlatformAndArch()
const targetPlatform = specificPlatform || platform
const targetArch = specificArch || arch
const targetMusl = specificMusl || isMusl
const targetBaseline = specificBaseline || false
console.log(
`Installing bun ${version} for ${targetPlatform}-${targetArch}${targetMusl ? ' (MUSL)' : ''}${targetBaseline ? ' (baseline)' : ''}...`
)
await downloadBunBinary(targetPlatform, targetArch, version, targetMusl, targetBaseline)
}
} }
// Run the installation // Run the installation

View File

@ -4,6 +4,7 @@ const os = require('os')
const { execSync } = require('child_process') const { execSync } = require('child_process')
const https = require('https') const https = require('https')
const tar = require('tar') const tar = require('tar')
const AdmZip = require('adm-zip')
// Base URL for downloading uv binaries // Base URL for downloading uv binaries
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download' const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
@ -66,51 +67,6 @@ async function downloadWithRedirects(url, destinationPath) {
}) })
} }
/**
* Fetches the latest version of uv from GitHub API
* @returns {Promise<string>} The latest version tag (without 'v' prefix)
*/
async function getLatestUvVersion() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: '/repos/astral-sh/uv/releases/latest',
headers: {
'User-Agent': 'cherry-studio-install-script'
}
}
const req = https.get(options, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Request failed with status code ${res.statusCode}`))
return
}
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
try {
const release = JSON.parse(data)
// Remove the 'v' prefix if present
const version = release.tag_name.startsWith('v') ? release.tag_name.substring(1) : release.tag_name
resolve(version)
} catch (error) {
reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
}
})
})
req.on('error', (error) => {
reject(new Error(`Failed to fetch latest version: ${error.message}`))
})
req.end()
})
}
/** /**
* Downloads and extracts the uv binary for the specified platform and architecture * Downloads and extracts the uv binary for the specified platform and architecture
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux') * @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
@ -128,61 +84,82 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is
} }
// Create output directory structure // Create output directory structure
const archDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
// Ensure directories exist // Ensure directories exist
fs.mkdirSync(archDir, { recursive: true }) fs.mkdirSync(binDir, { recursive: true })
// Download URL for the specific binary // Download URL for the specific binary
const downloadUrl = `${UV_RELEASE_BASE_URL}/${version}/${packageName}` const downloadUrl = `${UV_RELEASE_BASE_URL}/${version}/${packageName}`
const tempdir = os.tmpdir() const tempdir = os.tmpdir()
const localFilename = path.join(tempdir, packageName) const tempFilename = path.join(tempdir, packageName)
try { try {
console.log(`Downloading uv ${version} for ${platformKey}...`) console.log(`Downloading uv ${version} for ${platformKey}...`)
console.log(`URL: ${downloadUrl}`) console.log(`URL: ${downloadUrl}`)
// Use the new download function await downloadWithRedirects(downloadUrl, tempFilename)
await downloadWithRedirects(downloadUrl, localFilename)
// Extract files using tar console.log(`Extracting ${packageName} to ${binDir}...`)
console.log(`Extracting ${packageName} to ${archDir}...`)
await tar.x({
file: localFilename,
cwd: tempdir,
// zip 文件也可以用 tar 解压
z: packageName.endsWith('.tar.gz') // 对于 .tar.gz 文件启用 gzip 解压
})
// Move files using Node.js fs // 根据文件扩展名选择解压方法
const sourceDir = path.join(tempdir, packageName.split('.')[0]) if (packageName.endsWith('.zip')) {
const files = fs.readdirSync(sourceDir) // 使用 adm-zip 处理 zip 文件
for (const file of files) { const zip = new AdmZip(tempFilename)
const sourcePath = path.join(sourceDir, file) zip.extractAllTo(binDir, true)
const destPath = path.join(archDir, file) fs.unlinkSync(tempFilename)
fs.renameSync(sourcePath, destPath) console.log(`Successfully installed uv ${version} for ${platform}-${arch}`)
return true
} else {
// tar.gz 文件的处理保持不变
await tar.x({
file: tempFilename,
cwd: tempdir,
z: true
})
// Set executable permissions for non-Windows platforms // Move files using Node.js fs
if (platform !== 'win32') { const sourceDir = path.join(tempdir, packageName.split('.')[0])
try { const files = fs.readdirSync(sourceDir)
// 755 permission: rwxr-xr-x for (const file of files) {
fs.chmodSync(destPath, '755') const sourcePath = path.join(sourceDir, file)
} catch (error) { const destPath = path.join(binDir, file)
console.warn(`Warning: Failed to set executable permissions: ${error.message}`) fs.renameSync(sourcePath, destPath)
// Set executable permissions for non-Windows platforms
if (platform !== 'win32') {
try {
fs.chmodSync(destPath, '755')
} catch (error) {
console.warn(`Warning: Failed to set executable permissions: ${error.message}`)
}
} }
} }
}
// Clean up // Clean up
fs.unlinkSync(localFilename) fs.unlinkSync(tempFilename)
fs.rmdirSync(sourceDir, { recursive: true }) fs.rmSync(sourceDir, { recursive: true })
}
console.log(`Successfully installed uv ${version} for ${platform}-${arch}`) console.log(`Successfully installed uv ${version} for ${platform}-${arch}`)
return true return true
} catch (error) { } catch (error) {
console.error(`Error installing uv for ${platformKey}: ${error.message}`) console.error(`Error installing uv for ${platformKey}: ${error.message}`)
if (fs.existsSync(localFilename)) {
fs.unlinkSync(localFilename) if (fs.existsSync(tempFilename)) {
fs.unlinkSync(tempFilename)
} }
// Check if binDir is empty and remove it if so
try {
const files = fs.readdirSync(binDir)
if (files.length === 0) {
fs.rmSync(binDir, { recursive: true })
console.log(`Removed empty directory: ${binDir}`)
}
} catch (cleanupError) {
console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`)
}
return false return false
} }
} }
@ -215,39 +192,15 @@ function detectIsMusl() {
* Main function to install uv * Main function to install uv
*/ */
async function installUv() { async function installUv() {
const args = process.argv.slice(2)
const specifiedVersion = args.find((arg) => !arg.startsWith('--'))
// Get the latest version if no specific version is provided // Get the latest version if no specific version is provided
const version = specifiedVersion || (await getLatestUvVersion()) const version = DEFAULT_UV_VERSION
console.log(`Using uv version: ${version}`) console.log(`Using uv version: ${version}`)
const specificPlatform = args.find((arg) => arg.startsWith('--platform='))?.split('=')[1] const { platform, arch, isMusl } = detectPlatformAndArch()
const specificArch = args.find((arg) => arg.startsWith('--arch='))?.split('=')[1]
const specificMusl = args.includes('--musl')
const installAll = args.includes('--all')
if (installAll) { console.log(`Installing uv ${version} for ${platform}-${arch}${isMusl ? ' (MUSL)' : ''}...`)
console.log(`Installing all uv ${version} binaries...`)
for (const platformKey in UV_PACKAGES) {
const [platformArch, musl] = platformKey.split('-musl-')
if (musl) {
const [platform, arch] = platformArch.split('-')
await downloadUvBinary(platform, arch, version, true)
} else {
const [platform, arch] = platformKey.split('-')
await downloadUvBinary(platform, arch, version, false)
}
}
} else {
const { platform, arch, isMusl } = detectPlatformAndArch()
const targetPlatform = specificPlatform || platform
const targetArch = specificArch || arch
const targetMusl = specificMusl || isMusl
console.log(`Installing uv ${version} for ${targetPlatform}-${targetArch}${targetMusl ? ' (MUSL)' : ''}...`) await downloadUvBinary(platform, arch, version, isMusl)
await downloadUvBinary(targetPlatform, targetArch, version, targetMusl)
}
} }
// Run the installation // Run the installation

View File

@ -39,7 +39,7 @@ export async function getBinaryPath(name: string): Promise<string> {
let cmd = process.platform === 'win32' ? `${name}.exe` : name let cmd = process.platform === 'win32' ? `${name}.exe` : name
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
const binariesDirExists = await fs.existsSync(binariesDir) const binariesDirExists = await fs.existsSync(binariesDir)
cmd = binariesDirExists ? path.join(binariesDir, name) : name cmd = binariesDirExists ? path.join(binariesDir, cmd) : name
return cmd return cmd
} }