Files
clash-verge-rev/scripts/prebuild.mjs
T
wonfen 3253828bf0 feat/fix: service core staging (#7899)
* feat(service): hand upgraded cores to the service through the installer

The service now only executes administrator-staged core copies, so an
in-app core upgrade has to deliver the new bytes there before the
restart asks for them; otherwise a service-mode restart keeps running
the previous core while the UI reports the new version.

After publishing the downloaded core, service-mode upgrades invoke the
elevated service installer with --install-core <path> --sha256 <hex>.
The digest is computed over the published file and rides the elevated
command line, which no other local account can alter, so a file
swapped after hashing is refused rather than staged. Sidecar-mode
upgrades run the file directly and prompt for nothing.

Rollback distinguishes staging that never ran, was refused, or
succeeded: only a successful staging obliges the rollback to restore
the approved copy too (deliberately not gated on the running mode,
which reads NotRunning after the very crash being rolled back), and a
refused staging restores the file so a retry does not see the new
version on disk and conclude there is nothing to do.

Requires clash-verge-service-ipc >= 2.6.7. The pinned dependency and
the packaged service release must move together with this change, or
the old installer silently treats --install-core as a plain install.

* feat(installer): attest bundled cores to the service at install time

prebuild computes sha256 digests of the downloaded mihomo sidecars
into packages/windows/core-hashes.nsh (gitignored), included through
the NSIS installerHooks path. After unpacking and before starting the
service, the installer stages both cores into the service's approved
directory with those digests, so what gets attested is exactly what
this installer shipped: a core swapped on disk in a user-writable
install directory is refused, not published. Failure is logged and
does not abort the install; a later elevated service install can
stage the cores again.

* chore(deps): require clash-verge-service-ipc v2.6.8

v2.6.8 is the first service whose installer understands
--install-core --sha256; the staging this branch introduces is inert
against anything older, because the old installer treats the flag as a
plain service installation and reports success. The service release and
this pin therefore have to ship in the same app release.

* fix(service): heap-allocate the core hashing buffer

clippy::large_stack_arrays denies the 64 KiB stack array.
2026-09-09 18:25:25 +08:00

758 lines
23 KiB
JavaScript

import { execFileSync, execSync } from 'child_process'
import { createHash } from 'crypto'
import fs from 'fs'
import fsp from 'fs/promises'
import path from 'path'
import zlib from 'zlib'
import AdmZip from 'adm-zip'
import { glob } from 'glob'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { extract } from 'tar'
import { resolveServiceRelease } from './service-release.mjs'
import { log_debug, log_error, log_info, log_success } from './utils.mjs'
/** Prepares platform resources, caching versions and unchanged files unless `--force` is used. */
const cwd = process.cwd()
const TEMP_DIR = path.join(cwd, 'node_modules/.verge')
const FORCE = process.argv.includes('--force') || process.argv.includes('-f')
const VERSION_CACHE_FILE = path.join(TEMP_DIR, '.version_cache.json')
const HASH_CACHE_FILE = path.join(TEMP_DIR, '.hash_cache.json')
const PLATFORM_MAP = {
'x86_64-pc-windows-msvc': 'win32',
'i686-pc-windows-msvc': 'win32',
'aarch64-pc-windows-msvc': 'win32',
'x86_64-apple-darwin': 'darwin',
'aarch64-apple-darwin': 'darwin',
'x86_64-unknown-linux-gnu': 'linux',
'i686-unknown-linux-gnu': 'linux',
'aarch64-unknown-linux-gnu': 'linux',
'armv7-unknown-linux-gnueabihf': 'linux',
'riscv64gc-unknown-linux-gnu': 'linux',
'loongarch64-unknown-linux-gnu': 'linux',
}
const ARCH_MAP = {
'x86_64-pc-windows-msvc': 'x64',
'i686-pc-windows-msvc': 'ia32',
'aarch64-pc-windows-msvc': 'arm64',
'x86_64-apple-darwin': 'x64',
'aarch64-apple-darwin': 'arm64',
'x86_64-unknown-linux-gnu': 'x64',
'i686-unknown-linux-gnu': 'ia32',
'aarch64-unknown-linux-gnu': 'arm64',
'armv7-unknown-linux-gnueabihf': 'arm',
'riscv64gc-unknown-linux-gnu': 'riscv64',
'loongarch64-unknown-linux-gnu': 'loong64',
}
const arg1 = process.argv.slice(2)[0]
const arg2 = process.argv.slice(2)[1]
const target = arg1 === '--force' || arg1 === '-f' ? arg2 : arg1
if (process.env.CI && !target) {
throw new Error('prebuild requires an explicit target triple in CI')
}
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process
const SIDECAR_HOST = target
? target
: execSync('rustc -vV')
.toString()
.match(/(?<=host: ).+(?=\s*)/g)[0]
const RESOURCES_DIR = path.join(cwd, 'src-tauri', 'resources')
const SIDECAR_DIR = path.join(cwd, 'src-tauri', 'sidecar')
// Linux service binaries are bundled as externalBin sidecars (see tauri.linux.conf.json)
const SERVICE_DIR = platform === 'linux' ? SIDECAR_DIR : RESOURCES_DIR
// Version cache
async function loadVersionCache() {
try {
if (fs.existsSync(VERSION_CACHE_FILE)) {
const data = await fsp.readFile(VERSION_CACHE_FILE, 'utf-8')
return JSON.parse(data)
}
} catch (err) {
log_debug('Failed to load version cache:', err.message)
}
return {}
}
async function saveVersionCache(cache) {
try {
await fsp.mkdir(TEMP_DIR, { recursive: true })
await fsp.writeFile(VERSION_CACHE_FILE, JSON.stringify(cache, null, 2))
log_debug('Version cache saved')
} catch (err) {
log_debug('Failed to save version cache:', err.message)
}
}
async function getCachedVersion(key) {
const cache = await loadVersionCache()
const cached = cache[key]
if (cached && Date.now() - cached.timestamp < 3600000) {
log_info(`Using cached version for ${key}: ${cached.version}`)
return cached.version
}
return null
}
async function setCachedVersion(key, version) {
const cache = await loadVersionCache()
cache[key] = { version, timestamp: Date.now() }
await saveVersionCache(cache)
}
// File hash cache
async function calculateFileHash(filePath) {
try {
const fileBuffer = await fsp.readFile(filePath)
const hashSum = createHash('sha256')
hashSum.update(fileBuffer)
return hashSum.digest('hex')
} catch (ignoreErr) {
return null
}
}
async function loadHashCache() {
try {
if (fs.existsSync(HASH_CACHE_FILE)) {
const data = await fsp.readFile(HASH_CACHE_FILE, 'utf-8')
return JSON.parse(data)
}
} catch (err) {
log_debug('Failed to load hash cache:', err.message)
}
return {}
}
async function saveHashCache(cache) {
try {
await fsp.mkdir(TEMP_DIR, { recursive: true })
await fsp.writeFile(HASH_CACHE_FILE, JSON.stringify(cache, null, 2))
log_debug('Hash cache saved')
} catch (err) {
log_debug('Failed to save hash cache:', err.message)
}
}
async function hasFileChanged(filePath, targetPath) {
if (FORCE) return true
if (!fs.existsSync(targetPath)) return true
const hashCache = await loadHashCache()
const sourceHash = await calculateFileHash(filePath)
const targetHash = await calculateFileHash(targetPath)
if (!sourceHash || !targetHash) return true
const cacheKey = targetPath
const cachedHash = hashCache[cacheKey]
if (cachedHash === sourceHash && sourceHash === targetHash) {
return false
}
return true
}
async function updateHashCache(targetPath) {
const hashCache = await loadHashCache()
const hash = await calculateFileHash(targetPath)
if (hash) {
hashCache[targetPath] = hash
await saveHashCache(hashCache)
}
}
// Mihomo release maps
const META_ALPHA_VERSION_URL =
'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt'
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`
let META_ALPHA_VERSION
const META_VERSION_URL =
'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`
let META_VERSION
const META_ALPHA_MAP = {
'win32-x64': 'mihomo-windows-amd64-v2',
'win32-ia32': 'mihomo-windows-386',
'win32-arm64': 'mihomo-windows-arm64',
'darwin-x64': 'mihomo-darwin-amd64-v1-go122',
'darwin-arm64': 'mihomo-darwin-arm64-go122',
'linux-x64': 'mihomo-linux-amd64-v2',
'linux-ia32': 'mihomo-linux-386',
'linux-arm64': 'mihomo-linux-arm64',
'linux-arm': 'mihomo-linux-armv7',
'linux-riscv64': 'mihomo-linux-riscv64',
'linux-loong64': 'mihomo-linux-loong64',
}
const META_MAP = {
'win32-x64': 'mihomo-windows-amd64-v2',
'win32-ia32': 'mihomo-windows-386',
'win32-arm64': 'mihomo-windows-arm64',
'darwin-x64': 'mihomo-darwin-amd64-v2-go122',
'darwin-arm64': 'mihomo-darwin-arm64-go122',
'linux-x64': 'mihomo-linux-amd64-v2',
'linux-ia32': 'mihomo-linux-386',
'linux-arm64': 'mihomo-linux-arm64',
'linux-arm': 'mihomo-linux-armv7',
'linux-riscv64': 'mihomo-linux-riscv64',
'linux-loong64': 'mihomo-linux-loong64',
}
// Release discovery
async function getLatestAlphaVersion() {
if (!FORCE) {
const cached = await getCachedVersion('META_ALPHA_VERSION')
if (cached) {
META_ALPHA_VERSION = cached
return
}
}
const options = {}
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy)
try {
const response = await fetch(META_ALPHA_VERSION_URL, {
...options,
method: 'GET',
})
if (!response.ok)
throw new Error(
`Failed to fetch ${META_ALPHA_VERSION_URL}: ${response.status}`,
)
META_ALPHA_VERSION = (await response.text()).trim()
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`)
await setCachedVersion('META_ALPHA_VERSION', META_ALPHA_VERSION)
} catch (err) {
log_error('Error fetching latest alpha version:', err.message)
process.exit(1)
}
}
async function getLatestReleaseVersion() {
if (!FORCE) {
const cached = await getCachedVersion('META_VERSION')
if (cached) {
META_VERSION = cached
return
}
}
const options = {}
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy)
try {
const response = await fetch(META_VERSION_URL, {
...options,
method: 'GET',
})
if (!response.ok)
throw new Error(`Failed to fetch ${META_VERSION_URL}: ${response.status}`)
META_VERSION = (await response.text()).trim()
log_info(`Latest release version: ${META_VERSION}`)
await setCachedVersion('META_VERSION', META_VERSION)
} catch (err) {
log_error('Error fetching latest release version:', err.message)
process.exit(1)
}
}
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`)
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(`clash meta alpha unsupported platform "${platform}-${arch}"`)
}
function clashMetaAlpha() {
const name = META_ALPHA_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz'
return {
name: 'verge-mihomo-alpha',
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,
exeFile: `${name}${isWin ? '.exe' : ''}`,
zipFile: `${name}-${META_ALPHA_VERSION}.${urlExt}`,
downloadURL: `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`,
}
}
function clashMeta() {
const name = META_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz'
return {
name: 'verge-mihomo',
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,
exeFile: `${name}${isWin ? '.exe' : ''}`,
zipFile: `${name}-${META_VERSION}.${urlExt}`,
downloadURL: `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`,
}
}
async function downloadFile(url, outPath) {
const options = {}
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy)
const response = await fetch(url, {
...options,
method: 'GET',
headers: { 'Content-Type': 'application/octet-stream' },
})
if (!response.ok) {
const body = await response.text().catch(() => '')
await fsp.mkdir(path.dirname(outPath), { recursive: true })
await fsp.writeFile(outPath, body)
throw new Error(`Failed to download ${url}: status ${response.status}`)
}
const buf = Buffer.from(await response.arrayBuffer())
await fsp.mkdir(path.dirname(outPath), { recursive: true })
if (url.endsWith('.gz') || url.endsWith('.tgz')) {
if (!(buf[0] === 0x1f && buf[1] === 0x8b)) {
await fsp.writeFile(outPath, buf)
throw new Error(
`Downloaded file for ${url} is not a valid gzip (magic mismatch).`,
)
}
} else if (url.endsWith('.zip')) {
if (!(buf[0] === 0x50 && buf[1] === 0x4b)) {
await fsp.writeFile(outPath, buf)
throw new Error(
`Downloaded file for ${url} is not a valid zip (magic mismatch).`,
)
}
}
await fsp.writeFile(outPath, buf)
log_success(`download finished: ${url}`)
}
async function resolveSidecar(binInfo) {
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo
const sidecarPath = path.join(SIDECAR_DIR, targetFile)
await fsp.mkdir(SIDECAR_DIR, { recursive: true })
if (!FORCE && fs.existsSync(sidecarPath)) {
log_success(`"${name}" already exists, skipping download`)
return
}
const tempDir = path.join(TEMP_DIR, name)
const tempZip = path.join(tempDir, zipFile)
const tempExe = path.join(tempDir, exeFile)
await fsp.mkdir(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) => {
log_debug(`"${name}" entry: ${entry.entryName}`)
})
zip.extractAllTo(tempDir, true)
if (fs.existsSync(tempExe)) {
await fsp.rename(tempExe, sidecarPath)
} else {
const files = await fsp.readdir(tempDir)
const candidate = files.find(
(f) =>
f === path.basename(exeFile) ||
f.endsWith('.exe') ||
!f.includes('.'),
)
if (!candidate)
throw new Error(`Expected binary not found in ${tempDir}`)
await fsp.rename(path.join(tempDir, candidate), sidecarPath)
}
if (platform !== 'win32') await fsp.chmod(sidecarPath, 0o755)
log_success(`unzip finished: "${name}"`)
} else if (zipFile.endsWith('.tgz')) {
await extract({ cwd: tempDir, file: tempZip })
const files = await fsp.readdir(tempDir)
log_debug(`"${name}" extracted files:`, files)
let extracted = files.find(
(f) =>
f === path.basename(exeFile) ||
f.startsWith('虚空终端-') ||
!f.includes('.'),
)
if (!extracted) extracted = files[0]
if (!extracted) throw new Error(`Expected file not found in ${tempDir}`)
await fsp.rename(path.join(tempDir, extracted), sidecarPath)
await fsp.chmod(sidecarPath, 0o755)
log_success(`tgz processed: "${name}"`)
} else {
const readStream = fs.createReadStream(tempZip)
const writeStream = fs.createWriteStream(sidecarPath)
await new Promise((resolve, reject) => {
readStream
.pipe(zlib.createGunzip())
.on('error', (e) => {
log_error(`gunzip error for ${name}:`, e.message)
reject(e)
})
.pipe(writeStream)
.on('finish', () => {
if (platform !== 'win32')
execFileSync('chmod', ['755', sidecarPath])
resolve()
})
.on('error', (e) => {
log_error(`write stream error for ${name}:`, e.message)
reject(e)
})
})
log_success(`gz binary processed: "${name}"`)
}
} catch (err) {
await fsp.rm(sidecarPath, { recursive: true, force: true })
throw err
} finally {
await fsp.rm(tempDir, { recursive: true, force: true })
}
}
async function resolveResource(binInfo) {
const { file, downloadURL, localPath, dir } = binInfo
const baseDir = dir ?? RESOURCES_DIR
const targetPath = path.join(baseDir, file)
if (!FORCE && fs.existsSync(targetPath) && !downloadURL && !localPath) {
log_success(`"${file}" already exists, skipping`)
return
}
if (downloadURL) {
if (!FORCE && fs.existsSync(targetPath)) {
log_success(`"${file}" already exists, skipping download`)
return
}
await fsp.mkdir(baseDir, { recursive: true })
await downloadFile(downloadURL, targetPath)
await updateHashCache(targetPath)
}
if (localPath) {
if (!(await hasFileChanged(localPath, targetPath))) {
return
}
await fsp.mkdir(baseDir, { recursive: true })
await fsp.copyFile(localPath, targetPath)
await updateHashCache(targetPath)
log_success(`Copied file: ${file}`)
}
log_success(`${file} finished`)
}
// Windows NSIS plugin
const resolvePlugin = async () => {
const url =
'https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip'
const tempDir = path.join(TEMP_DIR, 'SimpleSC')
const tempZip = path.join(
tempDir,
'NSIS_Simple_Service_Plugin_Unicode_1.30.zip',
)
const tempDll = path.join(tempDir, 'SimpleSC.dll')
const pluginDir = path.join(process.env.APPDATA || '', 'Local/NSIS')
const pluginPath = path.join(pluginDir, 'SimpleSC.dll')
await fsp.mkdir(pluginDir, { recursive: true })
await fsp.mkdir(tempDir, { recursive: true })
if (!FORCE && fs.existsSync(pluginPath)) return
try {
if (!fs.existsSync(tempZip)) {
await downloadFile(url, tempZip)
}
const zip = new AdmZip(tempZip)
zip
.getEntries()
.forEach((entry) => log_debug(`"SimpleSC" entry`, entry.entryName))
zip.extractAllTo(tempDir, true)
if (fs.existsSync(tempDll)) {
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true })
log_success(`unzip finished: "SimpleSC"`)
} else {
const files = await fsp.readdir(tempDir)
const dll = files.find((f) => f.toLowerCase().endsWith('.dll'))
if (dll) {
await fsp.cp(path.join(tempDir, dll), pluginPath, {
recursive: true,
force: true,
})
log_success(`unzip finished: "SimpleSC" (found ${dll})`)
} else {
throw new Error('SimpleSC.dll not found in zip')
}
}
} finally {
await fsp.rm(tempDir, { recursive: true, force: true })
}
}
// Service executable permissions
const resolveServicePermission = async () => {
const serviceExecutables = [
'clash-verge-service*',
'clash-verge-service-install*',
'clash-verge-service-uninstall*',
]
const hashCache = await loadHashCache()
let hasChanges = false
for (const f of serviceExecutables) {
const files = glob.sync(path.join(SERVICE_DIR, f))
for (const filePath of files) {
if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath)
const cacheKey = `${filePath}_chmod`
if (!FORCE && hashCache[cacheKey] === currentHash) {
continue
}
try {
execSync(`chmod 755 ${filePath}`)
log_success(`chmod finished: "${filePath}"`)
} catch (e) {
log_error(`chmod failed for ${filePath}:`, e.message)
}
hashCache[cacheKey] = currentHash
hasChanges = true
}
}
}
if (hasChanges) {
await saveHashCache(hashCache)
}
}
// Other resources
const SERVICE_BINARIES = [
'clash-verge-service',
'clash-verge-service-install',
'clash-verge-service-uninstall',
]
function serviceFileInfo(name) {
const ext = platform === 'win32' ? '.exe' : ''
const suffix = platform === 'linux' ? '-' + SIDECAR_HOST : ''
return {
sourceFile: `${name}${ext}`,
targetFile: `${name}${suffix}${ext}`,
}
}
async function findExtractedFile(dir, fileName) {
const entries = await fsp.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const entryPath = path.join(dir, entry.name)
if (entry.isFile() && entry.name === fileName) return entryPath
if (entry.isDirectory()) {
const found = await findExtractedFile(entryPath, fileName)
if (found) return found
}
}
return null
}
async function resolveServiceBundle() {
const files = SERVICE_BINARIES.map((name) => {
const info = serviceFileInfo(name)
return {
...info,
targetPath: path.join(SERVICE_DIR, info.targetFile),
}
})
const cargoManifest = await fsp.readFile(
path.join(cwd, 'src-tauri', 'Cargo.toml'),
'utf8',
)
const { archiveFile, downloadURL } = resolveServiceRelease(
cargoManifest,
SIDECAR_HOST,
platform,
)
const tempDir = path.join(TEMP_DIR, 'clash-verge-service-ipc')
const tempArchive = path.join(tempDir, archiveFile)
await fsp.mkdir(tempDir, { recursive: true })
await fsp.mkdir(SERVICE_DIR, { recursive: true })
try {
await downloadFile(downloadURL, tempArchive)
if (platform === 'win32') {
const zip = new AdmZip(tempArchive)
zip
.getEntries()
.forEach((entry) =>
log_debug('"clash-verge-service-ipc" entry:', entry.entryName),
)
zip.extractAllTo(tempDir, true)
} else {
await extract({ cwd: tempDir, file: tempArchive })
}
for (const { sourceFile, targetFile, targetPath } of files) {
const extractedFile = await findExtractedFile(tempDir, sourceFile)
if (!extractedFile) {
throw new Error(`Expected binary ${sourceFile} not found in archive`)
}
await fsp.copyFile(extractedFile, targetPath)
if (platform !== 'win32') await fsp.chmod(targetPath, 0o755)
await updateHashCache(targetPath)
log_success(`Extracted service file: ${targetFile}`)
}
log_success(`service bundle finished: ${archiveFile}`)
} finally {
await fsp.rm(tempDir, { recursive: true, force: true })
}
}
/// The NSIS installer publishes the bundled cores into the service's approved directory and must
/// attest exactly the bytes it unpacked. The digests are computed here, where the sidecars land,
/// and reach installer.nsi through the installerHooks include (see tauri.windows.conf.json).
const CORE_HASHES_NSH = path.join(
cwd,
'src-tauri',
'packages',
'windows',
'core-hashes.nsh',
)
async function resolveCoreHashes() {
const lines = []
for (const [define, name] of [
['MIHOMO_SHA256', 'verge-mihomo'],
['MIHOMO_ALPHA_SHA256', 'verge-mihomo-alpha'],
]) {
const sidecar = path.join(SIDECAR_DIR, `${name}-${SIDECAR_HOST}.exe`)
const digest = createHash('sha256')
.update(await fsp.readFile(sidecar))
.digest('hex')
lines.push(`!define ${define} "${digest}"`)
}
await fsp.writeFile(CORE_HASHES_NSH, lines.join('\n') + '\n')
log_success(`Generated ${CORE_HASHES_NSH}`)
}
const resolveMmdb = () =>
resolveResource({
file: 'Country.mmdb',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb`,
})
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 resolveEnableLoopback = () =>
resolveResource({
file: 'enableLoopback.exe',
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
})
const resolveSetDnsScript = () =>
resolveResource({
file: 'set_dns.sh',
localPath: path.join(cwd, 'scripts/set_dns.sh'),
})
const resolveUnSetDnsScript = () =>
resolveResource({
file: 'unset_dns.sh',
localPath: path.join(cwd, 'scripts/unset_dns.sh'),
})
const tasks = [
{
name: 'verge-mihomo-alpha',
func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5,
},
{
name: 'verge-mihomo',
func: () =>
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5,
},
// After both sidecar tasks: it hashes what they downloaded.
{ name: 'core_hashes', func: resolveCoreHashes, retry: 1, winOnly: true },
{ name: 'plugin', func: resolvePlugin, retry: 5, winOnly: true },
{ name: 'service', func: resolveServiceBundle, retry: 5 },
{ name: 'mmdb', func: resolveMmdb, retry: 5 },
{ name: 'geosite', func: resolveGeosite, retry: 5 },
{ name: 'geoip', func: resolveGeoIP, retry: 5 },
{
name: 'enableLoopback',
func: resolveEnableLoopback,
retry: 5,
winOnly: true,
},
{
name: 'service_chmod',
func: resolveServicePermission,
retry: 5,
unixOnly: platform === 'linux' || platform === 'darwin',
},
{
name: 'set_dns_script',
func: resolveSetDnsScript,
retry: 5,
macosOnly: true,
},
{
name: 'unset_dns_script',
func: resolveUnSetDnsScript,
retry: 5,
macosOnly: true,
},
]
async function runTask() {
const task = tasks.shift()
if (!task) return
if (task.unixOnly && platform === 'win32') return runTask()
if (task.winOnly && platform !== 'win32') return runTask()
if (task.macosOnly && platform !== 'darwin') return runTask()
if (task.linuxOnly && platform !== 'linux') return runTask()
for (let i = 0; i < task.retry; i++) {
try {
await task.func()
break
} catch (err) {
log_error(`task::${task.name} try ${i} ==`, err.message)
if (i === task.retry - 1) throw err
}
}
return runTask()
}
runTask()