Files
Shadowbroker/desktop-shell/tauri-skeleton/scripts/build-backend-runtime.cjs
T

508 lines
15 KiB
JavaScript

#!/usr/bin/env node
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
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 runtimeLayoutVersion = 2;
const windowsEmbeddedPython = Object.freeze({
version: '3.11.9',
major: 3,
minor: 11,
arch: 'x64',
archiveName: 'python-3.11.9-embed-amd64.zip',
sha256: '009d6bf7e3b2ddca3d784fa09f90fe54336d5b60f0e0f305c37f400bf83cfd3b',
});
const excludedNames = new Set([
'.env',
'.pytest_cache',
'.ruff_cache',
'__pycache__',
'backend.egg-info',
'build',
'data',
'tests',
'timemachine',
]);
const excludedFiles = new Set([
'.env.example',
'.venv-dir',
'ais_cache.json',
'carrier_cache.json',
'cctv.db',
'dm_token_pepper.key',
'pytest.ini',
]);
const conventionalVenvNames = new Set(['venv', '.venv', 'venv-repair', '.venv-repair']);
function selectedVenvDirName() {
let venvDir = 'venv';
try {
const persisted = fs.readFileSync(venvMarkerPath, 'utf8').trim();
if (persisted) {
venvDir = persisted;
}
} catch {}
if (path.isAbsolute(venvDir) || path.basename(venvDir) !== venvDir) {
throw new Error(`Invalid backend venv directory marker: ${venvDir}`);
}
return venvDir;
}
function backendPythonPath() {
if (process.env.SHADOWBROKER_BACKEND_PYTHON) {
return path.resolve(process.env.SHADOWBROKER_BACKEND_PYTHON);
}
const venvDir = selectedVenvDirName();
if (process.platform === 'win32') {
return path.join(backendDir, venvDir, 'Scripts', 'python.exe');
}
return path.join(backendDir, venvDir, 'bin', 'python3');
}
function shouldCopyBackendPath(
srcPath,
platform = process.platform,
venvDirName = selectedVenvDirName(),
) {
const relativePath = path.relative(backendDir, srcPath);
if (!relativePath) return true;
const parts = relativePath.split(path.sep);
if (
platform === 'win32' &&
(parts[0] === venvDirName || conventionalVenvNames.has(parts[0]))
) {
return false;
}
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 shouldCopySitePackagePath(sitePackagesRoot, srcPath) {
const relativePath = path.relative(sitePackagesRoot, srcPath);
if (!relativePath) return true;
const parts = relativePath.split(path.sep);
if (parts.includes('__pycache__')) return false;
if (parts.some((part) => /^backend-.*\.dist-info$/i.test(part))) return false;
const leaf = parts.at(-1);
if (/\.(?:egg-link|pth|pyc)$/i.test(leaf)) return false;
if (leaf.toLowerCase() === 'direct_url.json') 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 backend build interpreter 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 readBuildPythonInfo() {
const python = backendPythonPath();
const code = [
'import json, platform, sys, sysconfig',
"print(json.dumps({'major': sys.version_info.major, 'minor': sys.version_info.minor, " +
"'micro': sys.version_info.micro, 'machine': platform.machine(), " +
"'prefix': sys.prefix, 'base_prefix': sys.base_prefix, " +
"'purelib': sysconfig.get_paths()['purelib']}))",
].join('; ');
const result = spawnSync(python, ['-I', '-c', code], {
cwd: backendDir,
encoding: 'utf8',
});
if (result.error || result.status !== 0) {
throw new Error(`Failed to inspect backend build interpreter: ${result.stderr || result.error}`);
}
return JSON.parse(result.stdout.trim());
}
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 sha256File(filePath) {
const hash = crypto.createHash('sha256');
hash.update(fs.readFileSync(filePath));
return hash.digest('hex');
}
async function downloadFile(url, destination) {
const response = await fetch(url, { redirect: 'follow' });
if (!response.ok) {
throw new Error(`Failed to download ${url}: HTTP ${response.status}`);
}
const temporary = `${destination}.partial-${process.pid}`;
fs.writeFileSync(temporary, Buffer.from(await response.arrayBuffer()));
fs.renameSync(temporary, destination);
}
async function windowsEmbeddedPythonArchive() {
const configured = process.env.SHADOWBROKER_PYTHON_EMBED_ZIP;
const cacheDir = path.join(os.tmpdir(), 'shadowbroker-python-embed');
const archivePath = configured
? path.resolve(configured)
: path.join(cacheDir, windowsEmbeddedPython.archiveName);
if (!fs.existsSync(archivePath)) {
if (configured) {
throw new Error(`SHADOWBROKER_PYTHON_EMBED_ZIP does not exist: ${archivePath}`);
}
fs.mkdirSync(cacheDir, { recursive: true });
const url =
`https://www.python.org/ftp/python/${windowsEmbeddedPython.version}/` +
windowsEmbeddedPython.archiveName;
console.log(`Downloading verified embedded Python ${windowsEmbeddedPython.version}...`);
await downloadFile(url, archivePath);
}
const actualHash = sha256File(archivePath);
if (actualHash !== windowsEmbeddedPython.sha256) {
throw new Error(
`Embedded Python archive SHA-256 mismatch for ${archivePath}: ` +
`expected ${windowsEmbeddedPython.sha256}, got ${actualHash}`,
);
}
return archivePath;
}
function extractZipWithPowerShell(archivePath, destination) {
fs.mkdirSync(destination, { recursive: true });
const env = {
...process.env,
SHADOWBROKER_EMBED_ARCHIVE: archivePath,
SHADOWBROKER_EMBED_DESTINATION: destination,
};
const result = spawnSync(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-Command',
"$ErrorActionPreference='Stop'; Expand-Archive -LiteralPath $env:SHADOWBROKER_EMBED_ARCHIVE -DestinationPath $env:SHADOWBROKER_EMBED_DESTINATION -Force",
],
{ env, encoding: 'utf8' },
);
if (result.error || result.status !== 0) {
throw new Error(`Failed to extract embedded Python: ${result.stderr || result.error}`);
}
}
function configureEmbeddedPythonPath(pythonRoot) {
const pthPath = path.join(
pythonRoot,
`python${windowsEmbeddedPython.major}${windowsEmbeddedPython.minor}._pth`,
);
if (!fs.existsSync(pthPath)) {
throw new Error(`Embedded Python path file is missing: ${pthPath}`);
}
fs.writeFileSync(
pthPath,
[
`python${windowsEmbeddedPython.major}${windowsEmbeddedPython.minor}.zip`,
'.',
'Lib',
'Lib\\site-packages',
'..',
'import site',
'',
].join('\n'),
'utf8',
);
}
async function stageWindowsEmbeddedPython() {
if (process.arch !== windowsEmbeddedPython.arch) {
throw new Error(
`Windows desktop packaging currently supports ${windowsEmbeddedPython.arch}, not ${process.arch}`,
);
}
const buildInfo = readBuildPythonInfo();
if (
buildInfo.major !== windowsEmbeddedPython.major ||
buildInfo.minor !== windowsEmbeddedPython.minor
) {
throw new Error(
`Backend venv uses Python ${buildInfo.major}.${buildInfo.minor}; ` +
`desktop packaging requires Python ${windowsEmbeddedPython.major}.${windowsEmbeddedPython.minor}.x ` +
'so compiled extension modules match the embedded runtime.',
);
}
if (!fs.existsSync(buildInfo.purelib)) {
throw new Error(`Backend site-packages directory is missing: ${buildInfo.purelib}`);
}
const pythonRoot = path.join(outputDir, 'python');
const archivePath = await windowsEmbeddedPythonArchive();
extractZipWithPowerShell(archivePath, pythonRoot);
configureEmbeddedPythonPath(pythonRoot);
const stagedSitePackages = path.join(pythonRoot, 'Lib', 'site-packages');
fs.mkdirSync(stagedSitePackages, { recursive: true });
fs.cpSync(buildInfo.purelib, stagedSitePackages, {
recursive: true,
filter: (srcPath) => shouldCopySitePackagePath(buildInfo.purelib, srcPath),
});
fs.writeFileSync(
path.join(outputDir, '.runtime-layout.json'),
`${JSON.stringify(
{
layout: 'embedded-python',
layoutVersion: runtimeLayoutVersion,
python: 'python/python.exe',
pythonVersion: windowsEmbeddedPython.version,
},
null,
2,
)}\n`,
'utf8',
);
return buildInfo;
}
function walkFiles(root) {
const files = [];
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) {
files.push(...walkFiles(entryPath));
} else {
files.push(entryPath);
}
}
return files;
}
function assertPortableWindowsBundle(root, buildInfo) {
const forbiddenPaths = [repoRoot, backendDir, buildInfo.prefix, buildInfo.base_prefix]
.filter(Boolean)
.map((value) => path.resolve(value).toLowerCase());
for (const filePath of walkFiles(root)) {
const leaf = path.basename(filePath).toLowerCase();
if (leaf === 'pyvenv.cfg' || /\.(?:egg-link|pth)$/i.test(leaf)) {
throw new Error(`Non-portable virtualenv metadata was staged: ${filePath}`);
}
const stat = fs.statSync(filePath);
if (stat.size > 2 * 1024 * 1024) continue;
const bytes = fs.readFileSync(filePath);
if (bytes.includes(0)) continue;
const text = bytes.toString('utf8').toLowerCase();
const leakedPath = forbiddenPaths.find((value) => text.includes(value));
if (leakedPath) {
throw new Error(`Build-machine path leaked into staged runtime: ${filePath}`);
}
}
}
function verifyRelocatableWindowsRuntime() {
const probeDir = `${outputDir}-portability-probe-${process.pid}`;
if (fs.existsSync(probeDir)) {
throw new Error(`Portability probe path already exists: ${probeDir}`);
}
fs.renameSync(outputDir, probeDir);
let result;
try {
const env = { ...process.env };
delete env.PYTHONHOME;
delete env.PYTHONPATH;
result = spawnSync(
path.join(probeDir, 'python', 'python.exe'),
[
'-I',
'-c',
[
'import sys',
'import fastapi, uvicorn, cryptography, numpy, orjson, pydantic',
'assert sys.prefix == sys.base_prefix',
'print(sys.executable)',
].join('; '),
],
{ cwd: probeDir, env, encoding: 'utf8' },
);
} finally {
fs.renameSync(probeDir, outputDir);
}
if (result.error || result.status !== 0) {
throw new Error(`Relocated embedded Python smoke test failed: ${result.stderr || result.error}`);
}
}
async function stageBackendRuntime() {
fs.rmSync(outputDir, { recursive: true, force: true });
fs.cpSync(backendDir, outputDir, {
recursive: true,
filter: shouldCopyBackendPath,
});
let buildInfo = null;
if (process.platform === 'win32') {
buildInfo = await stageWindowsEmbeddedPython();
}
stagePrivacyCoreArtifact();
stageReleaseAttestation();
stageStartScripts();
if (process.platform === 'win32') {
assertPortableWindowsBundle(outputDir, buildInfo);
verifyRelocatableWindowsRuntime();
}
}
function stageStartScripts() {
const scripts = ['start.bat', 'start.sh'];
for (const name of scripts) {
const src = path.join(repoRoot, name);
if (!fs.existsSync(src)) {
console.warn(`backend-runtime staged without ${name} (not at repo root)`);
continue;
}
const dst = path.join(outputDir, name);
fs.copyFileSync(src, dst);
if (name.endsWith('.sh') && process.platform !== 'win32') {
try {
fs.chmodSync(dst, 0o755);
} catch {
/* best-effort; not fatal on filesystems that don't honor chmod */
}
}
}
}
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'}-runtime-${runtimeLayoutVersion}\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;
}
async function main() {
ensureRuntimePrereqs();
await stageBackendRuntime();
writeBundleVersion();
console.log(`backend-runtime staged: ${fileCount(outputDir)} files`);
}
if (require.main === module) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}
module.exports = {
assertPortableWindowsBundle,
configureEmbeddedPythonPath,
shouldCopyBackendPath,
shouldCopySitePackagePath,
windowsEmbeddedPython,
};