153 lines
6.7 KiB
JavaScript
153 lines
6.7 KiB
JavaScript
import { execFile } from 'node:child_process'
|
|
import { access, chmod, cp, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
|
|
import { constants } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { promisify } from 'node:util'
|
|
import { build } from 'esbuild'
|
|
|
|
const run = promisify(execFile)
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
const projectDir = resolve(scriptDir, '..')
|
|
const releaseRoot = join(projectDir, 'release', 'connector')
|
|
const platformName = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'windows' : process.platform
|
|
const packageDir = join(releaseRoot, `${platformName}-${process.arch}`)
|
|
const executableName = process.platform === 'win32' ? 'SpineParticleConnector.exe' : 'SpineParticleConnector'
|
|
const executablePath = join(packageDir, executableName)
|
|
const launcherSource = join(projectDir, 'connector', 'launchers', platformName)
|
|
const nodeTarget = `node${process.versions.node.split('.')[0]}`
|
|
|
|
function seaUnavailable(error) {
|
|
return /single executable application is disabled/i.test(String(error?.stderr || '') + String(error?.stdout || '') + String(error))
|
|
}
|
|
|
|
async function downloadOfficialSeaNode(stagingDir) {
|
|
const version = process.versions.node
|
|
const architecture = process.arch === 'arm64' ? 'arm64' : process.arch === 'x64' ? 'x64' : null
|
|
if (!architecture || !['darwin', 'win32'].includes(process.platform)) {
|
|
throw new Error('当前系统的 Node 不支持 SEA。请安装 Node 官方发行版,或通过 SEA_NODE_BINARY 指定可用的 Node 可执行文件。')
|
|
}
|
|
|
|
const osName = process.platform === 'darwin' ? 'darwin' : 'win'
|
|
const extension = process.platform === 'darwin' ? 'tar.gz' : 'zip'
|
|
const archiveName = `node-v${version}-${osName}-${architecture}.${extension}`
|
|
const archivePath = join(stagingDir, archiveName)
|
|
const url = `https://nodejs.org/dist/v${version}/${archiveName}`
|
|
console.log(`当前 Node 禁用了 SEA,正在临时使用 Node 官方构建器:${url}`)
|
|
const response = await fetch(url, { signal: AbortSignal.timeout(120_000) })
|
|
if (!response.ok) throw new Error(`无法下载 Node 官方构建器(${response.status}):${url}`)
|
|
await writeFile(archivePath, Buffer.from(await response.arrayBuffer()))
|
|
|
|
if (process.platform === 'darwin') await run('tar', ['-xzf', archivePath, '-C', stagingDir])
|
|
else await run('powershell.exe', ['-NoProfile', '-Command', `Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${stagingDir.replace(/'/g, "''")}' -Force`])
|
|
|
|
const binary = process.platform === 'darwin'
|
|
? join(stagingDir, `node-v${version}-${osName}-${architecture}`, 'bin', 'node')
|
|
: join(stagingDir, `node-v${version}-${osName}-${architecture}`, 'node.exe')
|
|
await access(binary, constants.X_OK)
|
|
return binary
|
|
}
|
|
|
|
async function buildSea(configPath, stagingDir) {
|
|
const configuredBinary = process.env.SEA_NODE_BINARY
|
|
const candidates = configuredBinary ? [configuredBinary, process.execPath] : [process.execPath]
|
|
let lastError
|
|
for (const binary of candidates) {
|
|
try {
|
|
await run(binary, ['--build-sea', configPath])
|
|
return
|
|
} catch (error) {
|
|
lastError = error
|
|
if (!seaUnavailable(error)) throw error
|
|
}
|
|
}
|
|
const officialBinary = await downloadOfficialSeaNode(stagingDir)
|
|
try {
|
|
await run(officialBinary, ['--build-sea', configPath])
|
|
} catch (error) {
|
|
throw new Error(`Node 官方构建器无法生成 SEA:${error instanceof Error ? error.message : error}`, { cause: lastError })
|
|
}
|
|
}
|
|
|
|
async function createPortableArchive() {
|
|
const archivePath = join(releaseRoot, `SpineParticleConnector-${platformName}-${process.arch}.zip`)
|
|
await rm(archivePath, { force: true })
|
|
if (process.platform === 'darwin') {
|
|
await run('zip', ['-q', '-r', archivePath, basename(packageDir)], { cwd: releaseRoot })
|
|
} else if (process.platform === 'win32') {
|
|
const escapedSource = packageDir.replace(/'/g, "''")
|
|
const escapedTarget = archivePath.replace(/'/g, "''")
|
|
await run('powershell.exe', ['-NoProfile', '-Command', `Compress-Archive -LiteralPath '${escapedSource}' -DestinationPath '${escapedTarget}' -Force`])
|
|
} else {
|
|
return null
|
|
}
|
|
return archivePath
|
|
}
|
|
|
|
async function main() {
|
|
const stagingDir = await mkdtemp(join(tmpdir(), 'spine-particle-sea-'))
|
|
try {
|
|
const bundlePath = join(stagingDir, 'spine-connector.cjs')
|
|
const configPath = join(stagingDir, 'sea-config.json')
|
|
|
|
await build({
|
|
entryPoints: [join(projectDir, 'connector', 'spine-connector.mjs')],
|
|
outfile: bundlePath,
|
|
bundle: true,
|
|
platform: 'node',
|
|
format: 'cjs',
|
|
target: nodeTarget,
|
|
legalComments: 'none',
|
|
})
|
|
|
|
// The release directory is generated output. Recreate this exact package folder
|
|
// so a rebuild cannot retain files from an older connector version.
|
|
await rm(packageDir, { recursive: true, force: true })
|
|
await mkdir(packageDir, { recursive: true })
|
|
await writeFile(configPath, `${JSON.stringify({
|
|
main: bundlePath,
|
|
mainFormat: 'commonjs',
|
|
output: executablePath,
|
|
disableExperimentalSEAWarning: true,
|
|
useCodeCache: false,
|
|
}, null, 2)}\n`, 'utf8')
|
|
await buildSea(configPath, stagingDir)
|
|
|
|
if (process.platform !== 'win32') await chmod(executablePath, 0o755)
|
|
if (process.platform === 'darwin') {
|
|
// SEA modifies a copy of Node. Re-sign it locally so macOS will execute it.
|
|
await run('codesign', ['--force', '--sign', '-', '--timestamp=none', executablePath])
|
|
}
|
|
|
|
await cp(launcherSource, packageDir, { recursive: true, force: true })
|
|
if (process.platform !== 'win32') {
|
|
await Promise.all([
|
|
chmod(join(packageDir, '启动Spine连接器.command'), 0o755),
|
|
chmod(join(packageDir, '停止Spine连接器.command'), 0o755),
|
|
])
|
|
}
|
|
await cp(join(projectDir, 'connector', 'DISTRIBUTION.md'), join(packageDir, '使用说明.md'), { force: true })
|
|
await writeFile(join(packageDir, 'package.json'), `${JSON.stringify({
|
|
name: 'SpineParticleConnector',
|
|
connectorVersion: '1.0.0',
|
|
platform: process.platform,
|
|
architecture: process.arch,
|
|
executable: basename(executablePath),
|
|
builtWithNode: process.version,
|
|
}, null, 2)}\n`, 'utf8')
|
|
|
|
const archivePath = await createPortableArchive()
|
|
console.log(`已生成独立连接器:${packageDir}`)
|
|
console.log(`可执行文件:${executablePath}`)
|
|
if (archivePath) console.log(`分发压缩包:${archivePath}`)
|
|
} finally {
|
|
await rm(stagingDir, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`构建独立连接器失败:${error instanceof Error ? error.message : error}`)
|
|
process.exitCode = 1
|
|
})
|