This commit is contained in:
eric
2026-03-27 00:23:17 -05:00
parent 082bcc8805
commit aaf786b292
11 changed files with 564 additions and 56 deletions

View File

@@ -0,0 +1,173 @@
/**
* 构建机专用:下载 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 siteLib\\site-packages 才会进 sys.pathpip 才能用)
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] 下载 FFmpegwin64…')
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)
}