Files
2026-05-20 23:50:43 -05:00

182 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 构建机专用:下载 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..\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
enableEmbedSitePackages()
const r = spawnSync(
embedExe,
[
'-c',
'import importlib.util,fastapi,uvicorn,requests,psutil,src; assert importlib.util.find_spec("web2")',
],
{
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)
}