import { execFileSync, spawn } from 'node:child_process' import { access } from 'node:fs/promises' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' export const developmentServiceDirectoryEnvironment = 'CLASH_VERGE_DEV_SERVICE_DIR' const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') const serviceRepository = resolve( repositoryRoot, '..', 'clash-verge-service-ipc', ) const serviceManifest = join(serviceRepository, 'Cargo.toml') export const developmentServiceWatchPaths = [ 'src', 'resources', 'Cargo.toml', 'Cargo.lock', ].map((name) => join(serviceRepository, name)) function run(command, args, options = {}) { return new Promise((resolvePromise, reject) => { const child = spawn(command, args, { env: options.env ?? process.env, stdio: options.stdio ?? 'inherit', windowsHide: true, }) let stdout = '' child.stdout?.setEncoding('utf8') child.stdout?.on('data', (chunk) => { stdout += chunk }) child.once('error', reject) child.once('close', (code, signal) => { if (code === 0) { resolvePromise(stdout) return } const error = new Error( `${command} failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`, ) error.exitCode = code reject(error) }) }) } async function checkDevelopmentServiceVersion() { const [guiOutput, daemonOutput] = await Promise.all([ run( 'cargo', [ 'metadata', '--locked', '--format-version=1', '--manifest-path', join(repositoryRoot, 'Cargo.toml'), ], { stdio: ['inherit', 'pipe', 'inherit'] }, ), run( 'cargo', [ 'metadata', '--locked', '--no-deps', '--format-version=1', '--manifest-path', serviceManifest, ], { stdio: ['inherit', 'pipe', 'inherit'] }, ), ]) const gui = JSON.parse(guiOutput) const daemon = JSON.parse(daemonOutput).packages.find( (pkg) => resolve(pkg.manifest_path) === serviceManifest, ) const app = gui.packages.find( (pkg) => resolve(pkg.manifest_path) === join(repositoryRoot, 'src-tauri', 'Cargo.toml'), ) const dependency = gui.resolve?.nodes .find((node) => node.id === app?.id) ?.deps.find((dep) => dep.name === 'clash_verge_service_ipc') const sdk = gui.packages.find((pkg) => pkg.id === dependency?.pkg) if (!daemon || !sdk) throw new Error('Cargo did not resolve the development Service and GUI SDK') if (daemon.version !== sdk.version) { throw new Error( `Development Service ${daemon.version} does not match the resolved GUI SDK ${sdk.version} (${sdk.manifest_path}). ` + 'Align the GUI dependency explicitly before starting development. Building the sibling daemon does not update the SDK.', ) } } export async function prepareDevelopmentService() { await access(serviceManifest) await checkDevelopmentServiceVersion() const output = await run( 'cargo', [ 'build', '--manifest-path', serviceManifest, '--target-dir', process.env.CARGO_TARGET_DIR || join(serviceRepository, 'target', 'development'), '--features', 'standalone,client,development-channel', '--bins', '--message-format=json-render-diagnostics', ], { stdio: ['inherit', 'pipe', 'inherit'] }, ) const artifacts = new Map() for (const line of output.split(/\r?\n/).filter(Boolean)) { const artifact = JSON.parse(line) if ( artifact.reason === 'compiler-artifact' && resolve(artifact.manifest_path) === serviceManifest && artifact.target.kind.includes('bin') && artifact.executable ) { artifacts.set(artifact.target.name, resolve(artifact.executable)) } } const executables = [ 'clash-verge-service', 'clash-verge-service-install', 'clash-verge-service-uninstall', 'service-integration-driver', ].map((name) => { const executable = artifacts.get(name) if (!executable) throw new Error(`Cargo did not report the ${name} executable`) return executable }) const serviceDirectory = dirname(executables[0]) for (const executable of executables) { if (dirname(executable) !== serviceDirectory) { throw new Error( 'Development service tools were built in different directories', ) } await access(executable) } return serviceDirectory } export async function ensureDevelopmentService({ platform = process.platform, } = {}) { const serviceDirectory = await prepareDevelopmentService() const extension = platform === 'win32' ? '.exe' : '' const installer = join( serviceDirectory, `clash-verge-service-install${extension}`, ) const host = execFileSync('rustc', ['-vV'], { encoding: 'utf8' }).match( /^host: (.+)$/m, )?.[1] if (!host) throw new Error('rustc did not report its host target') const args = ['--prepare-install', '--ensure'] for (const name of ['verge-mihomo', 'verge-mihomo-alpha']) { args.push( '--core', `${name}${extension}`, join( repositoryRoot, 'src-tauri', 'sidecar', `${name}-${host}${extension}`, ), ) } await run(installer, args) return serviceDirectory }