174 lines
6.0 KiB
JavaScript
174 lines
6.0 KiB
JavaScript
/**
|
||
* 构建机专用:下载 Windows 嵌入式 Python + FFmpeg,安装 requirements.txt。
|
||
* 产物写入仓库根目录 python/ 与 tools/ffmpeg/,随 Electron 打包分发;最终用户无需联网。
|
||
*/
|
||
import fs from 'fs'
|
||
import path from 'path'
|
||
import { spawnSync } from 'child_process'
|
||
import { fileURLToPath } from 'url'
|
||
|
||
const __filename = fileURLToPath(import.meta.url)
|
||
const __dirname = path.dirname(__filename)
|
||
const electronRoot = path.resolve(__dirname, '..')
|
||
const backendRoot = path.resolve(electronRoot, '..')
|
||
|
||
const PY_VER = '3.12.8'
|
||
const PYTHON_ZIP = `https://www.python.org/ftp/python/${PY_VER}/python-${PY_VER}-embed-amd64.zip`
|
||
const GET_PIP = 'https://bootstrap.pypa.io/get-pip.py'
|
||
/** BtbN 静态构建,单 zip 内含 bin/ffmpeg.exe */
|
||
const FFMPEG_ZIP =
|
||
'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip'
|
||
|
||
const runtimeDir = path.join(backendRoot, 'python')
|
||
const embedExe = path.join(runtimeDir, 'python.exe')
|
||
const toolsFfmpegBin = path.join(backendRoot, 'tools', 'ffmpeg', 'bin')
|
||
const ffmpegExe = path.join(toolsFfmpegBin, 'ffmpeg.exe')
|
||
const requirementsTxt = path.join(backendRoot, 'requirements.txt')
|
||
const tmpDir = path.join(electronRoot, '.tmp-prepare-runtime')
|
||
|
||
function rmrf(p) {
|
||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true })
|
||
}
|
||
|
||
function downloadFile(url, dest) {
|
||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||
const u = url.replace(/'/g, "''")
|
||
const d = dest.replace(/'/g, "''")
|
||
const r = spawnSync(
|
||
'powershell.exe',
|
||
[
|
||
'-NoProfile',
|
||
'-Command',
|
||
`$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${u}' -OutFile '${d}' -UseBasicParsing`,
|
||
],
|
||
{ stdio: 'inherit', windowsHide: true },
|
||
)
|
||
if (r.status !== 0) throw new Error(`下载失败: ${url}`)
|
||
}
|
||
|
||
function unzipWithPowerShell(zipPath, destDir) {
|
||
fs.mkdirSync(destDir, { recursive: true })
|
||
const ps = spawnSync(
|
||
'powershell.exe',
|
||
[
|
||
'-NoProfile',
|
||
'-Command',
|
||
`Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destDir.replace(/'/g, "''")}' -Force`,
|
||
],
|
||
{ stdio: 'inherit', windowsHide: true },
|
||
)
|
||
if (ps.status !== 0) throw new Error('Expand-Archive failed')
|
||
}
|
||
|
||
function enableEmbedSitePackages() {
|
||
const pthFiles = fs.readdirSync(runtimeDir).filter((f) => f.endsWith('._pth'))
|
||
for (const f of pthFiles) {
|
||
const p = path.join(runtimeDir, f)
|
||
const base = f.replace(/\._pth$/, '')
|
||
const zipLine = `${base}.zip`
|
||
// 嵌入式解释器必须显式启用 import site,Lib\\site-packages 才会进 sys.path(pip 才能用)
|
||
fs.writeFileSync(p, `${zipLine}\n.\n\nimport site\n`, 'utf8')
|
||
}
|
||
}
|
||
|
||
function hasWorkingEmbedPython() {
|
||
if (!fs.existsSync(embedExe)) return false
|
||
if (fs.existsSync(path.join(runtimeDir, 'pyvenv.cfg'))) return false
|
||
if (fs.existsSync(path.join(runtimeDir, 'Scripts', 'python.exe'))) return false
|
||
const r = spawnSync(embedExe, ['-c', 'import fastapi,uvicorn,requests,psutil'], {
|
||
cwd: backendRoot,
|
||
encoding: 'utf8',
|
||
windowsHide: true,
|
||
timeout: 60000,
|
||
})
|
||
return r.status === 0
|
||
}
|
||
|
||
function hasFfmpeg() {
|
||
return fs.existsSync(ffmpegExe)
|
||
}
|
||
|
||
function installEmbeddablePython() {
|
||
console.log('[prepare:runtime] 安装嵌入式 Python ' + PY_VER + ' …')
|
||
rmrf(tmpDir)
|
||
fs.mkdirSync(tmpDir, { recursive: true })
|
||
const zipPath = path.join(tmpDir, 'python-embed.zip')
|
||
downloadFile(PYTHON_ZIP, zipPath)
|
||
rmrf(runtimeDir)
|
||
fs.mkdirSync(runtimeDir, { recursive: true })
|
||
unzipWithPowerShell(zipPath, runtimeDir)
|
||
enableEmbedSitePackages()
|
||
|
||
const getPipPath = path.join(tmpDir, 'get-pip.py')
|
||
downloadFile(GET_PIP, getPipPath)
|
||
let pip = spawnSync(embedExe, [getPipPath, '--no-warn-script-location'], {
|
||
cwd: runtimeDir,
|
||
stdio: 'inherit',
|
||
windowsHide: true,
|
||
env: { ...process.env, PYTHONPATH: '' },
|
||
})
|
||
if (pip.status !== 0) throw new Error('get-pip failed')
|
||
|
||
pip = spawnSync(embedExe, ['-m', 'pip', 'install', '--upgrade', 'pip', 'setuptools', 'wheel'], {
|
||
cwd: backendRoot,
|
||
stdio: 'inherit',
|
||
windowsHide: true,
|
||
})
|
||
if (pip.status !== 0) throw new Error('pip upgrade failed')
|
||
|
||
if (!fs.existsSync(requirementsTxt)) throw new Error('Missing requirements.txt')
|
||
pip = spawnSync(embedExe, ['-m', 'pip', 'install', '-r', requirementsTxt], {
|
||
cwd: backendRoot,
|
||
stdio: 'inherit',
|
||
windowsHide: true,
|
||
})
|
||
if (pip.status !== 0) throw new Error('pip install -r requirements.txt failed')
|
||
console.log('[prepare:runtime] 嵌入式 Python + 依赖已就绪。')
|
||
}
|
||
|
||
function installFfmpeg() {
|
||
console.log('[prepare:runtime] 下载 FFmpeg(win64)…')
|
||
rmrf(tmpDir)
|
||
fs.mkdirSync(tmpDir, { recursive: true })
|
||
const zipPath = path.join(tmpDir, 'ffmpeg.zip')
|
||
downloadFile(FFMPEG_ZIP, zipPath)
|
||
const extractRoot = path.join(tmpDir, 'ff')
|
||
unzipWithPowerShell(zipPath, extractRoot)
|
||
const dirs = fs.readdirSync(extractRoot)
|
||
const top = dirs.length === 1 ? path.join(extractRoot, dirs[0]) : extractRoot
|
||
const binSrc = path.join(top, 'bin')
|
||
if (!fs.existsSync(path.join(binSrc, 'ffmpeg.exe'))) {
|
||
throw new Error('ffmpeg zip layout unexpected: missing bin/ffmpeg.exe')
|
||
}
|
||
rmrf(path.join(backendRoot, 'tools', 'ffmpeg'))
|
||
fs.mkdirSync(toolsFfmpegBin, { recursive: true })
|
||
for (const name of fs.readdirSync(binSrc)) {
|
||
fs.copyFileSync(path.join(binSrc, name), path.join(toolsFfmpegBin, name))
|
||
}
|
||
console.log('[prepare:runtime] FFmpeg 已安装到 tools/ffmpeg/bin/')
|
||
}
|
||
|
||
try {
|
||
if (!hasWorkingEmbedPython()) {
|
||
if (fs.existsSync(path.join(runtimeDir, 'pyvenv.cfg'))) {
|
||
console.log('[prepare:runtime] 移除旧 venv,改用嵌入式 Python …')
|
||
rmrf(runtimeDir)
|
||
}
|
||
installEmbeddablePython()
|
||
} else {
|
||
console.log('[prepare:runtime] 已存在可用的嵌入式 Python,跳过下载。')
|
||
}
|
||
|
||
if (!hasFfmpeg()) {
|
||
installFfmpeg()
|
||
} else {
|
||
console.log('[prepare:runtime] 已存在 tools/ffmpeg/bin/ffmpeg.exe,跳过下载。')
|
||
}
|
||
|
||
rmrf(tmpDir)
|
||
console.log('[prepare:runtime] 全部完成。')
|
||
} catch (e) {
|
||
console.error('[prepare:runtime] 失败:', e)
|
||
process.exit(1)
|
||
}
|