Files
clash-party/scripts/prepare.mjs
2026-09-18 22:22:40 +08:00

607 lines
18 KiB
JavaScript
Raw 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.

import fs from 'fs'
import AdmZip from 'adm-zip'
import path from 'path'
import zlib from 'zlib'
import { extract } from 'tar'
import { execSync } from 'child_process'
const cwd = process.cwd()
const TEMP_DIR = path.join(cwd, 'node_modules/.temp')
const platform = process.platform
// 目标架构解析优先级:命令行参数 > npm_config_arch 环境变量 > 当前进程架构
// CI 交叉编译(如 ubuntu x64 宿主构建 arm64 包)时,若参数未传递到位,
// 会误用宿主机的 x64 架构下载 sidecar导致 arm64 包内混入 x64 二进制
const SUPPORTED_ARCHS = ['x64', 'ia32', 'arm64']
function resolveTargetArch() {
const args = process.argv.slice(2)
if (args.length !== 0) {
return args[0].replace('--', '')
}
const npmArch = process.env.npm_config_arch
if (npmArch && SUPPORTED_ARCHS.includes(npmArch)) {
return npmArch
}
return process.arch
}
const arch = resolveTargetArch()
/* ======= mihomo alpha======= */
const MIHOMO_ALPHA_VERSION_URL =
'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt'
const MIHOMO_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`
let MIHOMO_ALPHA_VERSION
const MIHOMO_ALPHA_MAP = {
'win32-x64': 'mihomo-windows-amd64-compatible',
'win32-ia32': 'mihomo-windows-386',
'win32-arm64': 'mihomo-windows-arm64',
'darwin-x64': 'mihomo-darwin-amd64-compatible',
'darwin-arm64': 'mihomo-darwin-arm64',
'linux-x64': 'mihomo-linux-amd64-compatible',
'linux-arm64': 'mihomo-linux-arm64'
}
// Fetch the latest alpha release version from the version.txt file
async function getLatestAlphaVersion() {
try {
const response = await fetch(MIHOMO_ALPHA_VERSION_URL, {
method: 'GET'
})
let v = await response.text()
MIHOMO_ALPHA_VERSION = v.trim() // Trim to remove extra whitespaces
console.log(`Latest alpha version: ${MIHOMO_ALPHA_VERSION}`)
} catch (error) {
console.error('Error fetching latest alpha version:', error.message)
process.exit(1)
}
}
/* ======= mihomo smart ======= */
const MIHOMO_SMART_VERSION_URL =
'https://github.com/vernesong/mihomo/releases/download/Prerelease-Alpha/version.txt'
const MIHOMO_SMART_URL_PREFIX = `https://github.com/vernesong/mihomo/releases/download/Prerelease-Alpha`
let MIHOMO_SMART_VERSION
const MIHOMO_SMART_MAP = {
'win32-x64': 'mihomo-windows-amd64-v2-go120',
'win32-ia32': 'mihomo-windows-386-go120',
'win32-arm64': 'mihomo-windows-arm64',
'darwin-x64': 'mihomo-darwin-amd64-v2-go120',
'darwin-arm64': 'mihomo-darwin-arm64',
'linux-x64': 'mihomo-linux-amd64-v2-go120',
'linux-arm64': 'mihomo-linux-arm64'
}
async function getLatestSmartVersion() {
try {
const response = await fetch(MIHOMO_SMART_VERSION_URL, {
method: 'GET'
})
let v = await response.text()
MIHOMO_SMART_VERSION = v.trim() // Trim to remove extra whitespaces
console.log(`Latest smart version: ${MIHOMO_SMART_VERSION}`)
} catch (error) {
console.error('Error fetching latest smart version:', error.message)
process.exit(1)
}
}
/* ======= mihomo release ======= */
const MIHOMO_VERSION_URL =
'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
const MIHOMO_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`
let MIHOMO_VERSION
const MIHOMO_MAP = {
'win32-x64': 'mihomo-windows-amd64-compatible',
'win32-ia32': 'mihomo-windows-386',
'win32-arm64': 'mihomo-windows-arm64',
'darwin-x64': 'mihomo-darwin-amd64-compatible',
'darwin-arm64': 'mihomo-darwin-arm64',
'linux-x64': 'mihomo-linux-amd64-compatible',
'linux-arm64': 'mihomo-linux-arm64'
}
// Fetch the latest release version from the version.txt file
async function getLatestReleaseVersion() {
try {
const response = await fetch(MIHOMO_VERSION_URL, {
method: 'GET'
})
let v = await response.text()
MIHOMO_VERSION = v.trim() // Trim to remove extra whitespaces
console.log(`Latest release version: ${MIHOMO_VERSION}`)
} catch (error) {
console.error('Error fetching latest release version:', error.message)
process.exit(1)
}
}
/*
* check available
*/
if (!MIHOMO_MAP[`${platform}-${arch}`]) {
throw new Error(`unsupported platform "${platform}-${arch}"`)
}
if (!MIHOMO_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(`unsupported platform "${platform}-${arch}"`)
}
if (!MIHOMO_SMART_MAP[`${platform}-${arch}`]) {
throw new Error(`unsupported platform "${platform}-${arch}"`)
}
/**
* core info
*/
function MihomoAlpha() {
const name = MIHOMO_ALPHA_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz'
const downloadURL = `${MIHOMO_ALPHA_URL_PREFIX}/${name}-${MIHOMO_ALPHA_VERSION}.${urlExt}`
const exeFile = `${name}${isWin ? '.exe' : ''}`
const zipFile = `${name}-${MIHOMO_ALPHA_VERSION}.${urlExt}`
return {
name: 'mihomo-alpha',
targetFile: `mihomo-alpha${isWin ? '.exe' : ''}`,
exeFile,
zipFile,
downloadURL
}
}
function mihomo() {
const name = MIHOMO_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz'
const downloadURL = `${MIHOMO_URL_PREFIX}/${MIHOMO_VERSION}/${name}-${MIHOMO_VERSION}.${urlExt}`
const exeFile = `${name}${isWin ? '.exe' : ''}`
const zipFile = `${name}-${MIHOMO_VERSION}.${urlExt}`
return {
name: 'mihomo',
targetFile: `mihomo${isWin ? '.exe' : ''}`,
exeFile,
zipFile,
downloadURL
}
}
function mihomoSmart() {
const name = MIHOMO_SMART_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz'
const downloadURL = `${MIHOMO_SMART_URL_PREFIX}/${name}-${MIHOMO_SMART_VERSION}.${urlExt}`
const exeFile = `${name}${isWin ? '.exe' : ''}`
const zipFile = `${name}-${MIHOMO_SMART_VERSION}.${urlExt}`
return {
name: 'mihomo-smart',
targetFile: `mihomo-smart${isWin ? '.exe' : ''}`,
exeFile,
zipFile,
downloadURL
}
}
/**
* download sidecar and rename
*/
async function resolveSidecar(binInfo) {
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo
const sidecarDir = path.join(cwd, 'extra', 'sidecar')
const sidecarPath = path.join(sidecarDir, targetFile)
fs.mkdirSync(sidecarDir, { recursive: true })
if (fs.existsSync(sidecarPath)) {
fs.rmSync(sidecarPath)
}
const tempDir = path.join(TEMP_DIR, name)
const tempZip = path.join(tempDir, zipFile)
const tempExe = path.join(tempDir, exeFile)
fs.mkdirSync(tempDir, { recursive: true })
try {
if (!fs.existsSync(tempZip)) {
await downloadFile(downloadURL, tempZip)
}
if (zipFile.endsWith('.zip')) {
const zip = new AdmZip(tempZip)
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName)
})
zip.extractAllTo(tempDir, true)
fs.renameSync(tempExe, sidecarPath)
console.log(`[INFO]: "${name}" unzip finished`)
} else if (zipFile.endsWith('.tgz')) {
// tgz
fs.mkdirSync(tempDir, { recursive: true })
await extract({
cwd: tempDir,
file: tempZip
})
const files = fs.readdirSync(tempDir)
console.log(`[DEBUG]: "${name}" files in tempDir:`, files)
const extractedFile = files.find((file) => file.startsWith('虚空终端-'))
if (extractedFile) {
const extractedFilePath = path.join(tempDir, extractedFile)
fs.renameSync(extractedFilePath, sidecarPath)
console.log(`[INFO]: "${name}" file renamed to "${sidecarPath}"`)
execSync(`chmod 755 ${sidecarPath}`)
console.log(`[INFO]: "${name}" chmod binary finished`)
} else {
throw new Error(`Expected file not found in ${tempDir}`)
}
} else {
// gz
const readStream = fs.createReadStream(tempZip)
const writeStream = fs.createWriteStream(sidecarPath)
await new Promise((resolve, reject) => {
const onError = (error) => {
console.error(`[ERROR]: "${name}" gz failed:`, error.message)
reject(error)
}
readStream
.pipe(zlib.createGunzip().on('error', onError))
.pipe(writeStream)
.on('finish', () => {
console.log(`[INFO]: "${name}" gunzip finished`)
execSync(`chmod 755 ${sidecarPath}`)
console.log(`[INFO]: "${name}" chmod binary finished`)
resolve()
})
.on('error', onError)
})
}
} catch (err) {
// 需要删除文件
fs.rmSync(sidecarPath)
throw err
} finally {
fs.rmSync(tempDir, { recursive: true })
}
}
/**
* download the file to the extra dir
*/
async function resolveResource(binInfo) {
const { file, downloadURL } = binInfo
const resDir = path.join(cwd, 'extra', 'files')
const targetPath = path.join(resDir, file)
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath)
}
fs.mkdirSync(resDir, { recursive: true })
await downloadFile(downloadURL, targetPath)
console.log(`[INFO]: ${file} finished`)
}
/**
* download file and save to `path`
*/
async function downloadFile(url, path) {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/octet-stream' }
})
if (!response.ok) {
throw new Error(`download failed: ${response.status} ${response.statusText} for "${url}"`)
}
const buffer = await response.arrayBuffer()
fs.writeFileSync(path, new Uint8Array(buffer))
console.log(`[INFO]: download finished "${url}"`)
}
const resolveMmdb = () =>
resolveResource({
file: 'country.mmdb',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb`
})
const resolveMetadb = () =>
resolveResource({
file: 'geoip.metadb',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb`
})
const resolveGeosite = () =>
resolveResource({
file: 'geosite.dat',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`
})
const resolveGeoIP = () =>
resolveResource({
file: 'geoip.dat',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`
})
const resolveASN = () =>
resolveResource({
file: 'ASN.mmdb',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb`
})
const resolveBundleMRS = () =>
resolveResource({
file: 'BundleMRS.7z',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/BundleMRS.7z`
})
const resolveEnableLoopback = () =>
resolveResource({
file: 'enableLoopback.exe',
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`
})
/* ======= sysproxy-rs ======= */
const SYSPROXY_RS_URL_PREFIX =
'https://github.com/mihomo-party-org/sysproxy-rs-opti/releases/latest/download'
// ELF e_machine 编号,用于校验下载的 .node 确为目标架构
const ELF_MACHINE = {
x64: 62, // EM_X86_64
ia32: 3, // EM_386
arm64: 183 // EM_AARCH64
}
function assertElfMachine(filePath, targetArch) {
const fd = fs.openSync(filePath, 'r')
const header = Buffer.alloc(20)
try {
fs.readSync(fd, header, 0, 20, 0)
} finally {
fs.closeSync(fd)
}
if (header.toString('ascii', 0, 4) !== '\x7fELF') {
throw new Error(`not a valid ELF file: ${filePath}`)
}
const machine = header.readUInt16LE(18)
const expected = ELF_MACHINE[targetArch]
if (expected !== undefined && machine !== expected) {
throw new Error(
`architecture mismatch for "${filePath}": expected ${targetArch} (ELF machine ${expected}), got ${machine}`
)
}
}
function getSysproxyNodeName() {
// 检测是否为 musl 系统(与 src/native/sysproxy/index.js 保持一致)
const isMusl = (() => {
if (platform !== 'linux') return false
try {
const output = execSync('ldd --version 2>&1 || true').toString()
return output.includes('musl')
} catch {
return false
}
})()
const isWin7Build = process.env.LEGACY_BUILD === 'true'
switch (platform) {
case 'win32':
if (arch === 'x64')
return isWin7Build ? 'sysproxy.win32-x64-msvc-win7.node' : 'sysproxy.win32-x64-msvc.node'
if (arch === 'arm64') return 'sysproxy.win32-arm64-msvc.node'
if (arch === 'ia32')
return isWin7Build ? 'sysproxy.win32-ia32-msvc-win7.node' : 'sysproxy.win32-ia32-msvc.node'
break
case 'darwin':
if (arch === 'x64') return 'sysproxy.darwin-x64.node'
if (arch === 'arm64') return 'sysproxy.darwin-arm64.node'
break
case 'linux':
if (isMusl) {
if (arch === 'x64') return 'sysproxy.linux-x64-musl.node'
if (arch === 'arm64') return 'sysproxy.linux-arm64-musl.node'
} else {
if (arch === 'x64') return 'sysproxy.linux-x64-gnu.node'
if (arch === 'arm64') return 'sysproxy.linux-arm64-gnu.node'
}
break
}
throw new Error(`Unsupported platform for sysproxy-rs: ${platform}-${arch}`)
}
const resolveSysproxy = async () => {
const nodeName = getSysproxyNodeName()
const sidecarDir = path.join(cwd, 'extra', 'sidecar')
const targetPath = path.join(sidecarDir, nodeName)
fs.mkdirSync(sidecarDir, { recursive: true })
// 清理其他平台的 .node 文件
const files = fs.readdirSync(sidecarDir)
for (const file of files) {
if (file.endsWith('.node') && file !== nodeName) {
fs.rmSync(path.join(sidecarDir, file))
console.log(`[INFO]: removed ${file}`)
}
}
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath)
}
await downloadFile(`${SYSPROXY_RS_URL_PREFIX}/${nodeName}`, targetPath)
// 校验 .node 确为目标架构 ELF避免交叉编译时把宿主机架构产物误打进包仅 Linux
if (platform === 'linux') {
assertElfMachine(targetPath, arch)
}
console.log(`[INFO]: ${nodeName} finished`)
}
const resolveMonitor = async () => {
const tempDir = path.join(TEMP_DIR, 'TrafficMonitor')
const tempZip = path.join(tempDir, `${arch}.zip`)
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
await downloadFile(
`https://github.com/mihomo-party-org/mihomo-party-run/releases/download/monitor/${arch}.zip`,
tempZip
)
const zip = new AdmZip(tempZip)
const resDir = path.join(cwd, 'extra', 'files')
const targetPath = path.join(resDir, 'TrafficMonitor')
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath, { recursive: true })
}
zip.extractAllTo(targetPath, true)
console.log(`[INFO]: TrafficMonitor finished`)
}
const resolve7zip = () =>
resolveResource({
file: '7za.exe',
downloadURL: `https://github.com/develar/7zip-bin/raw/master/win/${arch}/7za.exe`
})
const resolveSubstore = () =>
resolveResource({
file: 'sub-store.bundle.cjs',
downloadURL:
'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js'
})
const resolveHelper = () =>
resolveResource({
file: 'party.mihomo.helper',
downloadURL: `https://github.com/mihomo-party-org/mihomo-party-helper/releases/download/${arch}/party.mihomo.helper`
})
const resolveSubstoreFrontend = async () => {
const tempDir = path.join(TEMP_DIR, 'substore-frontend')
const tempZip = path.join(tempDir, 'dist.zip')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
await downloadFile(
'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
tempZip
)
const zip = new AdmZip(tempZip)
const resDir = path.join(cwd, 'extra', 'files')
const targetPath = path.join(resDir, 'sub-store-frontend')
if (fs.existsSync(targetPath)) {
fs.rmSync(targetPath, { recursive: true })
}
zip.extractAllTo(resDir, true)
fs.renameSync(path.join(resDir, 'dist'), targetPath)
console.log(`[INFO]: sub-store-frontend finished`)
}
const resolveFont = async () => {
const targetPath = path.join(cwd, 'src', 'renderer', 'src', 'assets', 'NotoColorEmoji.ttf')
if (fs.existsSync(targetPath)) {
return
}
await downloadFile(
'https://github.com/googlefonts/noto-emoji/raw/main/2D/fonts/NotoColorEmoji.ttf',
targetPath
)
console.log(`[INFO]: NotoColorEmoji.ttf finished`)
}
const tasks = [
{
name: 'mihomo-alpha',
func: () => getLatestAlphaVersion().then(() => resolveSidecar(MihomoAlpha())),
retry: 5
},
{
name: 'mihomo',
func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())),
retry: 5
},
{
name: 'mihomo-smart',
func: () => getLatestSmartVersion().then(() => resolveSidecar(mihomoSmart())),
retry: 5
},
{ name: 'mmdb', func: resolveMmdb, retry: 5 },
{ name: 'metadb', func: resolveMetadb, retry: 5 },
{ name: 'geosite', func: resolveGeosite, retry: 5 },
{ name: 'geoip', func: resolveGeoIP, retry: 5 },
{ name: 'asn', func: resolveASN, retry: 5 },
{ name: 'bundlemrs', func: resolveBundleMRS, retry: 5 },
{
name: 'font',
func: resolveFont,
retry: 5
},
{
name: 'enableLoopback',
func: resolveEnableLoopback,
retry: 5,
winOnly: true
},
{
name: 'sysproxy',
func: resolveSysproxy,
retry: 5
},
{
name: 'monitor',
func: resolveMonitor,
retry: 5,
winOnly: true
},
{
name: 'substore',
func: resolveSubstore,
retry: 5
},
{
name: 'substorefrontend',
func: resolveSubstoreFrontend,
retry: 5
},
{
name: '7zip',
func: resolve7zip,
retry: 5,
winOnly: true
},
{
name: 'helper',
func: resolveHelper,
retry: 5,
darwinOnly: true
}
]
async function runTask() {
const task = tasks.shift()
if (!task) return
if (task.winOnly && platform !== 'win32') return runTask()
if (task.linuxOnly && platform !== 'linux') return runTask()
if (task.unixOnly && platform === 'win32') return runTask()
if (task.darwinOnly && platform !== 'darwin') return runTask()
for (let i = 0; i < task.retry; i++) {
try {
await task.func()
break
} catch (err) {
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message)
if (i === task.retry - 1) {
if (task.optional) {
console.log(`[WARN]: Optional task::${task.name} failed, skipping...`)
break
} else {
throw err
}
}
}
}
return runTask()
}
runTask()
runTask()