mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-09-16 14:15:29 +02:00
508 lines
15 KiB
JavaScript
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,
|
|
};
|