Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions scripts/check-packaged-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function run(command, args, options = {}) {
env: options.env || process.env,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
...(options.maxBuffer ? { maxBuffer: options.maxBuffer } : {}),
});
if (result.error || result.status !== 0) {
fail(`${command} ${args.join(' ')} failed: ${(result.error?.message || result.stderr || result.stdout || '').trim()}`);
Expand Down Expand Up @@ -231,7 +232,10 @@ function inspectPackage(appPath) {
fail(`Python version ${versionOutput} does not match manifest ${normalized.python.version}`);
}
run(pythonPath, ['-c', [
'import fastapi, uvicorn, pydantic, requests, torch, whisper',
'import fastapi, uvicorn, pydantic, requests, numpy as np, torch, whisper',
'a = np.ones((32, 32)); b = a @ a; assert float(b[0, 0]) == 32.0',
'from numba import njit; f = njit(lambda x: x + 1); assert f(2) == 3',
'x = torch.tensor([1.0, 2.0]); y = x * 2; assert y.tolist() == [2.0, 4.0]',
'from importlib.metadata import version, distributions; assert version("openai-whisper") == "20250625"',
'import main, sys; forbidden = ("whisperx", "nemo", "pyannote.audio", "df", "mediapipe", "cv2", "openai", "anthropic", "moviepy"); assert all(name not in sys.modules for name in forbidden)',
'installed = {(d.metadata.get("Name") or "").lower() for d in distributions()}; assert not installed.intersection({"whisperx", "nemo-toolkit", "pyannote.audio", "deepfilternet", "mediapipe", "opencv-python", "openai", "anthropic", "moviepy"})',
Expand All @@ -241,6 +245,20 @@ function inspectPackage(appPath) {
if (!/arm64/i.test(architecture)) fail(`bundled Python architecture is not arm64: ${architecture.trim()}`);
const forbidden = findForbiddenResourceEntry(resourcesPath);
if (forbidden) fail(`packaged Resources contains forbidden developer/cache data: ${forbidden}`);
for (const relativePath of [
'llvmlite/binding/libllvmlite.dylib',
'numpy/.dylibs/libopenblas64_.0.dylib',
'torch/lib/libtorch_cpu.dylib',
'torch/lib/libtorch_python.dylib',
]) {
const nativePath = path.join(corePackRoot, ...relativePath.split('/'));
if (!fs.existsSync(nativePath)) fail(`packaged native runtime target is missing: ${relativePath}`);
run('/usr/bin/codesign', ['--verify', '--strict', nativePath]);
const symbols = run('/usr/bin/nm', ['-m', nativePath], { maxBuffer: 16 * 1024 * 1024 }).stdout || '';
if (/\bnon-external\b/i.test(symbols)) {
fail(`packaged native runtime target still contains local symbols after strip -x: ${relativePath}`);
}
}
for (const relativePath of [
'include',
'lib/python3.11/site-packages',
Expand Down Expand Up @@ -275,7 +293,7 @@ function inspectPackage(appPath) {
console.log(`Core constraints: ${constraints.source}, SHA-256 ${constraints.sha256}`);
console.log(`Core dependency lock: ${dependencyLock.source}, SHA-256 ${dependencyLock.sha256}, requireHashes=true, buildIsolation=false`);
console.log(`Model manifest: ${modelManifest.id}, ${modelManifest.revision}, ${modelManifest.expectedBytes} bytes`);
console.log('Core imports: fastapi, uvicorn, pydantic, requests, torch, whisper==20250625');
console.log('Core execution probe: NumPy/OpenBLAS, Numba/llvmlite, Torch, Whisper==20250625');
console.log('Optional packages bundled: No');
console.log('Backend import: passed');
console.log('Model weight in Resources: No');
Expand Down
34 changes: 33 additions & 1 deletion scripts/prepare-python-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const PORTABLE_RUNTIME_BUILD_ONLY_PATHS = [
'bin/python3-config',
'bin/python3.11-config',
];
const NATIVE_RUNTIME_STRIP_TARGETS = [
'llvmlite/binding/libllvmlite.dylib',
'numpy/.dylibs/libopenblas64_.0.dylib',
'torch/lib/libtorch_cpu.dylib',
'torch/lib/libtorch_python.dylib',
];

function getOption(name) {
const index = process.argv.indexOf(name);
Expand Down Expand Up @@ -272,7 +278,9 @@ function verifyCore(interpreterPath, relativeInterpreter) {
SCRIPTCUT_RUNTIME_MANIFEST_SCHEMA: 'scriptcut.runtime.v1',
});
const probe = [
'import fastapi, uvicorn, pydantic, requests, torch, whisper',
'import fastapi, uvicorn, pydantic, requests, numpy as np, torch, whisper',
'a = np.ones((32, 32)); b = a @ a; assert float(b[0, 0]) == 32.0',
'from numba import njit; f = njit(lambda x: x + 1); assert f(2) == 3',
'x = torch.tensor([1.0, 2.0]); y = x * 2; assert y.tolist() == [2.0, 4.0]',
'from importlib.metadata import version; assert version("openai-whisper") == "20250625"',
'import importlib.util, sys; import main; forbidden = ("whisperx", "nemo", "pyannote.audio", "df", "mediapipe", "cv2", "openai", "anthropic", "moviepy"); assert all(name not in sys.modules for name in forbidden)',
Expand Down Expand Up @@ -445,6 +453,28 @@ function prunePortableRuntimeBuildTooling() {
return removed;
}

function stripNativeRuntimeSymbols() {
const optimized = { files: 0, bytesBefore: 0, bytesAfter: 0 };
for (const relativePath of NATIVE_RUNTIME_STRIP_TARGETS) {
const filePath = path.join(corePackRoot, ...relativePath.split('/'));
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
fail(`expected native strip target is missing: ${relativePath}`);
}
const before = fs.statSync(filePath).size;
run('/usr/bin/strip', ['-x', filePath]);
// strip invalidates the wheel's Mach-O signature. Restore an ad-hoc staging
// signature so local probes can load it; electron-builder signs it again.
run('/usr/bin/codesign', ['--force', '--sign', '-', filePath]);
run('/usr/bin/codesign', ['--verify', '--strict', filePath]);
const after = fs.statSync(filePath).size;
if (after >= before) fail(`native symbol stripping did not reduce ${relativePath}: ${before} -> ${after}`);
optimized.files += 1;
optimized.bytesBefore += before;
optimized.bytesAfter += after;
}
return optimized;
}

async function main() {
verifyTarget();
fs.mkdirSync(buildRoot, { recursive: true });
Expand All @@ -460,10 +490,12 @@ async function main() {
const headersRemoved = pruneNonRuntimeCoreArtifacts();
const testsRemoved = prunePackageTestArtifacts(corePackRoot);
const portableBuildToolingRemoved = prunePortableRuntimeBuildTooling();
const nativeSymbols = stripNativeRuntimeSymbols();
const coreAfter = directoryStats(corePackRoot);
console.log(`Torch headers removed: ${headersRemoved.files}`);
console.log(`Package test artifacts removed: ${testsRemoved.directories} directories, ${testsRemoved.files} files, ${testsRemoved.bytes} bytes`);
console.log(`Portable Python build-only tooling removed: ${portableBuildToolingRemoved.paths} paths, ${portableBuildToolingRemoved.files} files, ${portableBuildToolingRemoved.bytes} bytes`);
console.log(`Native runtime symbols stripped: ${nativeSymbols.files} files, ${nativeSymbols.bytesBefore - nativeSymbols.bytesAfter} bytes removed`);
console.log(`Core pack files after pruning: ${coreAfter.files}`);
console.log(`Core pack bytes after pruning: ${coreAfter.bytes}`);
verifyCore(interpreterPath, relativeInterpreter);
Expand Down
20 changes: 20 additions & 0 deletions scripts/smoke-runtime-artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ for (const constraint of constraintPins) {
assert.ok(lockLines.some((line) => line.startsWith(`${normalized} --hash=sha256:`)), `core lock is missing artifact hash for: ${constraint}`);
}
const runtimePreparation = fs.readFileSync(path.join(__dirname, 'prepare-python-runtime.js'), 'utf8');
const packagedRuntimeCheck = fs.readFileSync(path.join(__dirname, 'check-packaged-runtime.js'), 'utf8');
assert.match(runtimePreparation, /function\s+pruneNonRuntimeCoreArtifacts\s*\(/);
assert.match(runtimePreparation, /function\s+prunePythonBytecode\s*\(/);
assert.match(runtimePreparation, /function\s+prunePackageTestArtifacts\s*\(/);
assert.match(runtimePreparation, /function\s+prunePortableRuntimeBuildTooling\s*\(/);
assert.match(runtimePreparation, /function\s+stripNativeRuntimeSymbols\s*\(/);
assert.match(runtimePreparation, /profile:\s*['"]execution-only['"]/);
assert.match(runtimePreparation, /buildToolingBundled:\s*false/);
assert.match(runtimePreparation, /guiToolkitBundled:\s*false/);
Expand All @@ -51,6 +53,24 @@ for (const expectedBuildOnlyPath of [
]) {
assert.ok(runtimePreparation.includes(`'${expectedBuildOnlyPath}'`), `portable runtime prune is missing ${expectedBuildOnlyPath}`);
}
for (const expectedNativeStripTarget of [
'llvmlite/binding/libllvmlite.dylib',
'numpy/.dylibs/libopenblas64_.0.dylib',
'torch/lib/libtorch_cpu.dylib',
'torch/lib/libtorch_python.dylib',
]) {
assert.ok(runtimePreparation.includes(`'${expectedNativeStripTarget}'`), `native strip target is missing ${expectedNativeStripTarget}`);
assert.ok(packagedRuntimeCheck.includes(`'${expectedNativeStripTarget}'`), `packaged runtime verification is missing ${expectedNativeStripTarget}`);
}
assert.match(runtimePreparation, /['"]\/usr\/bin\/strip['"],\s*\[['"]-x['"]/);
assert.match(runtimePreparation, /['"]\/usr\/bin\/codesign['"],\s*\[['"]--force['"],\s*['"]--sign['"],\s*['"]-['"]/);
assert.match(runtimePreparation, /from numba import njit; f = njit/);
assert.match(packagedRuntimeCheck, /from numba import njit; f = njit/);
assert.match(packagedRuntimeCheck, /np\.ones\(\(32, 32\)\)/);
assert.match(packagedRuntimeCheck, /torch\.tensor\(\[1\.0, 2\.0\]\)/);
assert.match(packagedRuntimeCheck, /['"]\/usr\/bin\/codesign['"],\s*\[['"]--verify['"],\s*['"]--strict['"]/);
assert.match(packagedRuntimeCheck, /['"]\/usr\/bin\/nm['"],\s*\[['"]-m['"]/);
assert.match(packagedRuntimeCheck, /\\bnon-external\\b/);
assert.match(runtimePreparation, /['"]--no-compile['"]/);
assert.match(runtimePreparation, /['"]--no-build-isolation['"]/);
assert.match(runtimePreparation, /['"]--require-hashes['"]/);
Expand Down
Loading