/* eslint-disable @typescript-eslint/explicit-function-return-type */ 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') let arch = process.arch const platform = process.platform if (process.argv.slice(2).length !== 0) { arch = process.argv.slice(2)[0].replace('--', '') } /* ======= 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 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}"`) } /** * 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 } } /** * 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' } }) 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 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-lite.dat` }) const resolveASN = () => resolveResource({ file: 'ASN.mmdb', downloadURL: `https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb` }) const resolveEnableLoopback = () => resolveResource({ file: 'enableLoopback.exe', downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe` }) 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/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: 'mmdb', func: resolveMmdb, retry: 5 }, { name: 'geosite', func: resolveGeosite, retry: 5 }, { name: 'geoip', func: resolveGeoIP, retry: 5 }, { name: 'asn', func: resolveASN, retry: 5 }, { name: 'font', func: resolveFont, retry: 5 }, { name: 'enableLoopback', func: resolveEnableLoopback, retry: 5, winOnly: 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() 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) throw err } } return runTask() } runTask() runTask()