167 lines
4.6 KiB
JavaScript
167 lines
4.6 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { spawnSync } from 'node:child_process'
|
|
import {
|
|
access,
|
|
copyFile,
|
|
cp,
|
|
mkdir,
|
|
readFile,
|
|
rm,
|
|
writeFile
|
|
} from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import sharp from 'sharp'
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
const outputRoot = path.join(root, 'dist-windows')
|
|
const stageRoot = path.join(outputRoot, 'staging')
|
|
const runtimeRoot = path.join(stageRoot, 'runtime')
|
|
const appRoot = path.join(runtimeRoot, 'app')
|
|
const nodeDir = path.join(runtimeRoot, 'node')
|
|
const ffmpegDir = path.join(runtimeRoot, 'ffmpeg')
|
|
const buildDir = path.join(stageRoot, 'build')
|
|
const cacheDir = path.join(outputRoot, 'cache')
|
|
const nodeVersion = '22.23.3'
|
|
|
|
function run(command, args, cwd = root) {
|
|
const result = spawnSync(command, args, {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
shell: process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(command)
|
|
})
|
|
if (result.error) throw result.error
|
|
if (result.status !== 0) {
|
|
throw new Error(`${command} ${args.join(' ')} 失败,退出代码 ${result.status}`)
|
|
}
|
|
}
|
|
|
|
async function exists(file) {
|
|
try {
|
|
await access(file)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function download(file, url) {
|
|
if (await exists(file)) return
|
|
await mkdir(path.dirname(file), { recursive: true })
|
|
console.log(`下载 ${url}`)
|
|
const response = await fetch(url)
|
|
if (!response.ok) throw new Error(`下载失败:${response.status} ${url}`)
|
|
await writeFile(file, Buffer.from(await response.arrayBuffer()))
|
|
}
|
|
|
|
async function prepareNode() {
|
|
const cacheFile = path.join(cacheDir, `node-v${nodeVersion}-win-x64.exe`)
|
|
const nodeExe = path.join(nodeDir, 'node.exe')
|
|
await download(
|
|
cacheFile,
|
|
`https://nodejs.org/dist/v${nodeVersion}/win-x64/node.exe`
|
|
)
|
|
|
|
const sums = await (await fetch(
|
|
`https://nodejs.org/dist/v${nodeVersion}/SHASUMS256.txt`
|
|
)).text()
|
|
const expected = sums
|
|
.split(/\r?\n/)
|
|
.map(line => line.trim().split(/\s+/))
|
|
.find(parts => parts[1] === 'win-x64/node.exe')?.[0]
|
|
const actual = createHash('sha256')
|
|
.update(await readFile(cacheFile))
|
|
.digest('hex')
|
|
if (!expected || actual !== expected) {
|
|
throw new Error('内置 Node.js 校验失败')
|
|
}
|
|
|
|
await mkdir(nodeDir, { recursive: true })
|
|
await copyFile(cacheFile, nodeExe)
|
|
run(nodeExe, ['--version'])
|
|
}
|
|
|
|
async function findFfmpegDir() {
|
|
const candidates = [
|
|
process.env.FFMPEG_DIR,
|
|
'C:\\ffmpeg-8.0.1-essentials_build\\bin'
|
|
].filter(Boolean)
|
|
|
|
for (const candidate of candidates) {
|
|
if (
|
|
await exists(path.join(candidate, 'ffmpeg.exe'))
|
|
&& await exists(path.join(candidate, 'ffprobe.exe'))
|
|
) {
|
|
return candidate
|
|
}
|
|
}
|
|
|
|
const located = spawnSync('where.exe', ['ffmpeg.exe'], { encoding: 'utf8' })
|
|
const first = located.stdout?.trim().split(/\r?\n/)[0]
|
|
if (first && await exists(path.join(path.dirname(first), 'ffprobe.exe'))) {
|
|
return path.dirname(first)
|
|
}
|
|
|
|
throw new Error('未找到 ffmpeg.exe 和 ffprobe.exe,请设置 FFMPEG_DIR')
|
|
}
|
|
|
|
async function main() {
|
|
run('npm.cmd', ['run', 'build'])
|
|
await rm(stageRoot, { recursive: true, force: true })
|
|
await Promise.all([
|
|
mkdir(appRoot, { recursive: true }),
|
|
mkdir(buildDir, { recursive: true })
|
|
])
|
|
|
|
await cp(path.join(root, '.output'), path.join(appRoot, '.output'), {
|
|
recursive: true,
|
|
dereference: true
|
|
})
|
|
await copyFile(path.join(root, 'package.json'), path.join(appRoot, 'package.json'))
|
|
await copyFile(
|
|
path.join(root, 'package-lock.json'),
|
|
path.join(appRoot, 'package-lock.json')
|
|
)
|
|
await cp(path.join(root, 'server', 'cli'), path.join(appRoot, 'server', 'cli'), {
|
|
recursive: true
|
|
})
|
|
|
|
console.log('安装 Windows 运行时生产依赖')
|
|
run(
|
|
'npm.cmd',
|
|
['ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'],
|
|
appRoot
|
|
)
|
|
|
|
await prepareNode()
|
|
|
|
const sourceFfmpeg = await findFfmpegDir()
|
|
await mkdir(ffmpegDir, { recursive: true })
|
|
await copyFile(
|
|
path.join(sourceFfmpeg, 'ffmpeg.exe'),
|
|
path.join(ffmpegDir, 'ffmpeg.exe')
|
|
)
|
|
await copyFile(
|
|
path.join(sourceFfmpeg, 'ffprobe.exe'),
|
|
path.join(ffmpegDir, 'ffprobe.exe')
|
|
)
|
|
run(path.join(ffmpegDir, 'ffmpeg.exe'), ['-version'])
|
|
|
|
await sharp(path.join(root, 'app', 'assets', 'image', 'logo-light.png'))
|
|
.resize(512, 512, {
|
|
fit: 'contain',
|
|
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
|
})
|
|
.png()
|
|
.toFile(path.join(buildDir, 'icon.png'))
|
|
|
|
console.log(`Windows 运行时已准备到 ${runtimeRoot}`)
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exit(1)
|
|
})
|