#!/usr/bin/env node const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const scriptDir = __dirname; const tauriDir = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(tauriDir, '..', '..'); const backendDir = path.join(repoRoot, 'backend'); const privacyCoreDir = path.join(repoRoot, 'privacy-core'); const outputDir = path.join(tauriDir, 'src-tauri', 'backend-runtime'); const venvMarkerPath = path.join(backendDir, '.venv-dir'); const releaseAttestationPath = path.join(backendDir, 'data', 'release_attestation.json'); const stagedReleaseAttestationPath = path.join( outputDir, 'data', 'release_attestation.json', ); const excludedNames = new Set([ '.env', '.pytest_cache', '.ruff_cache', '__pycache__', 'backend.egg-info', 'build', 'data', 'tests', 'timemachine', ]); const excludedFiles = new Set([ '.env.example', 'ais_cache.json', 'carrier_cache.json', 'cctv.db', 'dm_token_pepper.key', 'pytest.ini', ]); function backendPythonPath() { let venvDir = 'venv'; try { const persisted = fs.readFileSync(venvMarkerPath, 'utf8').trim(); if (persisted) { venvDir = persisted; } } catch {} if (process.platform === 'win32') { return path.join(backendDir, venvDir, 'Scripts', 'python.exe'); } return path.join(backendDir, venvDir, 'bin', 'python3'); } function shouldCopy(srcPath) { const relativePath = path.relative(backendDir, srcPath); if (!relativePath) return true; const parts = relativePath.split(path.sep); return parts.every((part, index) => { const isLeaf = index === parts.length - 1; if (excludedNames.has(part)) return false; if (isLeaf && excludedFiles.has(part)) return false; if (/^test_.*\.py$/i.test(part)) return false; return true; }); } function ensureRuntimePrereqs() { if (!fs.existsSync(path.join(backendDir, 'main.py'))) { throw new Error(`Missing backend/main.py at ${backendDir}`); } if (!fs.existsSync(backendPythonPath())) { throw new Error( `Missing bundled backend Python runtime at ${backendPythonPath()}. ` + 'Create the backend venv before packaging the desktop app.', ); } if (!fs.existsSync(path.join(backendDir, 'node_modules', 'ws'))) { throw new Error( `Missing backend/node_modules/ws at ${path.join(backendDir, 'node_modules', 'ws')}. ` + 'Install backend Node dependencies before packaging the desktop app.', ); } } function privacyCoreArtifactName() { if (process.platform === 'win32') return 'privacy_core.dll'; if (process.platform === 'darwin') return 'libprivacy_core.dylib'; return 'libprivacy_core.so'; } function privacyCoreArtifactPath() { return path.join(privacyCoreDir, 'target', 'release', privacyCoreArtifactName()); } function ensurePrivacyCoreArtifact() { const artifact = privacyCoreArtifactPath(); if (fs.existsSync(artifact)) { return artifact; } console.log('privacy-core release library missing; building it for desktop packaging...'); const result = spawnSync( 'cargo', ['build', '--release', '--manifest-path', path.join(privacyCoreDir, 'Cargo.toml')], { cwd: repoRoot, env: process.env, stdio: 'inherit', }, ); if (result.error || result.status !== 0) { throw new Error( 'Failed to build privacy-core release library. Install Rust/Cargo and rerun the desktop build.', ); } if (!fs.existsSync(artifact)) { throw new Error(`privacy-core build completed but artifact is missing: ${artifact}`); } return artifact; } function stageBackendRuntime() { fs.rmSync(outputDir, { recursive: true, force: true }); fs.cpSync(backendDir, outputDir, { recursive: true, filter: shouldCopy, }); stagePrivacyCoreArtifact(); stageReleaseAttestation(); } function stagePrivacyCoreArtifact() { const artifact = ensurePrivacyCoreArtifact(); const stagedPath = path.join(outputDir, path.basename(artifact)); fs.copyFileSync(artifact, stagedPath); } function stageReleaseAttestation() { if (!fs.existsSync(releaseAttestationPath)) { console.warn(`backend-runtime staged without release attestation: ${releaseAttestationPath}`); return; } fs.mkdirSync(path.dirname(stagedReleaseAttestationPath), { recursive: true }); fs.copyFileSync(releaseAttestationPath, stagedReleaseAttestationPath); } function writeBundleVersion() { const versionPath = path.join(outputDir, '.bundle-version'); const pkg = JSON.parse( fs.readFileSync(path.join(repoRoot, 'desktop-shell', 'package.json'), 'utf8'), ); fs.writeFileSync(versionPath, `${pkg.version || '0.0.0'}\n`, 'utf8'); } function fileCount(root) { let count = 0; for (const entry of fs.readdirSync(root, { withFileTypes: true })) { const fullPath = path.join(root, entry.name); if (entry.isDirectory()) { count += fileCount(fullPath); } else { count += 1; } } return count; } ensureRuntimePrereqs(); stageBackendRuntime(); writeBundleVersion(); console.log(`backend-runtime staged: ${fileCount(outputDir)} files`);