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
2 changes: 1 addition & 1 deletion docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Prepare the self-contained arm64 release candidate:
npm run release:rc:arm64
```

That command prepares portable Python, the pinned core pack, bundled FFmpeg/FFprobe, the frontend, and the release-configured Electron app. It runs the packaged runtime, backend, optional-capability, DMG, signing-readiness, and release-metadata gates, then writes `dist/release-candidate/SHA256SUMS.txt`, `dist/release-candidate/release-manifest.json`, and `dist/release-candidate/RELEASE_NOTES.md`.
That command prepares portable Python, the pinned core pack, bundled FFmpeg/FFprobe, the frontend, and the release-configured Electron app. The packaged Python is an execution-only private runtime: build/install tooling such as pip/ensurepip, development headers, venv/IDLE, and Tcl/Tk GUI assets are removed after the core pack has been assembled and verified. It runs the packaged runtime, backend, optional-capability, DMG, signing-readiness, and release-metadata gates, then writes `dist/release-candidate/SHA256SUMS.txt`, `dist/release-candidate/release-manifest.json`, and `dist/release-candidate/RELEASE_NOTES.md`.

Run the extended native model gate explicitly when requested:

Expand Down
23 changes: 23 additions & 0 deletions scripts/check-packaged-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ function inspectPackage(appPath) {
fail(`runtime manifest is not valid JSON: ${error.message}`);
}
const normalized = validateRuntimeManifest(manifest, { expectedTarget: 'darwin-arm64' });
if (
normalized.python.profile !== 'execution-only'
|| normalized.python.buildToolingBundled !== false
|| normalized.python.guiToolkitBundled !== false
) {
fail('packaged Python must declare the execution-only runtime profile without build or GUI tooling');
}
const constraints = manifest.packs?.constraints;
if (!constraints || typeof constraints.source !== 'string' || !/^[a-f0-9]{64}$/.test(constraints.sha256 || '')) {
fail('runtime manifest is missing valid core dependency constraints provenance');
Expand Down Expand Up @@ -195,6 +202,7 @@ function inspectPackage(appPath) {
}

const pythonPath = resolveResourcePath(resourcesPath, normalized.python.executable, 'Python executable');
const portablePythonRoot = path.dirname(path.dirname(pythonPath));
const backendRoot = resolveResourcePath(resourcesPath, normalized.backend.root, 'backend root');
const corePackRoot = resolveResourcePath(resourcesPath, normalized.packs.core, 'core pack root');
assertExecutable(pythonPath, 'Bundled Python');
Expand Down Expand Up @@ -233,6 +241,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 [
'include',
'lib/python3.11/site-packages',
'lib/python3.11/ensurepip',
'lib/python3.11/idlelib',
'lib/python3.11/tkinter',
'lib/libtcl9.0.dylib',
'lib/libtcl9tk9.0.dylib',
'bin/pip',
]) {
if (fs.existsSync(path.join(portablePythonRoot, ...relativePath.split('/')))) {
fail(`packaged portable Python still contains build-only tooling: ${relativePath}`);
}
}
const bundledModel = findResourceFile(resourcesPath, 'base.pt');
if (bundledModel) fail(`packaged Resources contains the Whisper model weight: ${path.relative(resourcesPath, bundledModel)}`);

Expand All @@ -249,6 +271,7 @@ function inspectPackage(appPath) {
console.log(`Backend: ${backendSize} bytes`);
console.log(`FFmpeg/bin: ${binSize} bytes`);
console.log(`Manifest: ${normalized.schema}, ${normalized.target.id}, Python ${normalized.python.version}`);
console.log('Python profile: execution-only; build tooling: No; GUI toolkit: No');
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`);
Expand Down
53 changes: 53 additions & 0 deletions scripts/prepare-python-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,37 @@ const coreConstraintsPath = path.join(root, 'runtime', 'core-darwin-arm64-py311.
const coreLockPath = path.join(root, 'runtime', 'core-darwin-arm64-py311.lock.txt');
// Torch imports torch.testing through its normal autograd import path.
const RUNTIME_REQUIRED_TEST_DIRECTORIES = new Set(['torch/testing']);
const PORTABLE_RUNTIME_BUILD_ONLY_PATHS = [
'include',
'lib/pkgconfig',
'lib/python3.11/site-packages',
'lib/python3.11/ensurepip',
'lib/python3.11/idlelib',
'lib/python3.11/pydoc_data',
'lib/python3.11/tkinter',
'lib/python3.11/turtledemo',
'lib/python3.11/lib2to3',
'lib/python3.11/venv',
'lib/python3.11/lib-dynload/_tkinter.cpython-311-darwin.so',
'lib/tcl9.0',
'lib/tcl9',
'lib/tk9.0',
'lib/itcl4.3.8',
'lib/thread3.0.6',
'lib/libtcl9.0.dylib',
'lib/libtcl9tk9.0.dylib',
'bin/2to3',
'bin/2to3-3.11',
'bin/idle3',
'bin/idle3.11',
'bin/pip',
'bin/pip3',
'bin/pip3.11',
'bin/pydoc3',
'bin/pydoc3.11',
'bin/python3-config',
'bin/python3.11-config',
];

function getOption(name) {
const index = process.argv.indexOf(name);
Expand Down Expand Up @@ -280,6 +311,9 @@ function writeManifest(relativeInterpreter, version) {
build: PYTHON_RUNTIME_ARTIFACT.build,
source: PYTHON_RUNTIME_ARTIFACT.source,
archiveSha256: PYTHON_RUNTIME_ARTIFACT.sha256,
profile: 'execution-only',
buildToolingBundled: false,
guiToolkitBundled: false,
},
backend: { root: 'backend' },
packs: {
Expand Down Expand Up @@ -394,6 +428,23 @@ function prunePackageTestArtifacts(directory) {
return removed;
}

function prunePortableRuntimeBuildTooling() {
const removed = { paths: 0, files: 0, bytes: 0 };
for (const relativePath of PORTABLE_RUNTIME_BUILD_ONLY_PATHS) {
const entryPath = path.join(runtimeRoot, ...relativePath.split('/'));
if (!fs.lstatSync(entryPath, { throwIfNoEntry: false })) fail(`expected portable build-only path is missing: ${relativePath}`);
const stats = directoryStats(entryPath);
fs.rmSync(entryPath, { recursive: true, force: false });
if (fs.lstatSync(entryPath, { throwIfNoEntry: false })) {
fail(`portable build-only path still exists after pruning: ${relativePath}`);
}
removed.paths += 1;
removed.files += stats.files;
removed.bytes += stats.bytes;
}
return removed;
}

async function main() {
verifyTarget();
fs.mkdirSync(buildRoot, { recursive: true });
Expand All @@ -408,9 +459,11 @@ async function main() {
console.log(`Core pack bytes before pruning: ${coreBefore.bytes}`);
const headersRemoved = pruneNonRuntimeCoreArtifacts();
const testsRemoved = prunePackageTestArtifacts(corePackRoot);
const portableBuildToolingRemoved = prunePortableRuntimeBuildTooling();
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(`Core pack files after pruning: ${coreAfter.files}`);
console.log(`Core pack bytes after pruning: ${coreAfter.bytes}`);
verifyCore(interpreterPath, relativeInterpreter);
Expand Down
14 changes: 14 additions & 0 deletions scripts/smoke-runtime-artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,21 @@ const runtimePreparation = fs.readFileSync(path.join(__dirname, 'prepare-python-
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, /profile:\s*['"]execution-only['"]/);
assert.match(runtimePreparation, /buildToolingBundled:\s*false/);
assert.match(runtimePreparation, /guiToolkitBundled:\s*false/);
assert.match(runtimePreparation, /RUNTIME_REQUIRED_TEST_DIRECTORIES\s*=\s*new Set\(\[['"]torch\/testing['"]\]\)/);
for (const expectedBuildOnlyPath of [
'lib/python3.11/site-packages',
'lib/python3.11/ensurepip',
'lib/python3.11/tkinter',
'lib/python3.11/lib-dynload/_tkinter.cpython-311-darwin.so',
'lib/libtcl9.0.dylib',
'lib/libtcl9tk9.0.dylib',
]) {
assert.ok(runtimePreparation.includes(`'${expectedBuildOnlyPath}'`), `portable runtime prune is missing ${expectedBuildOnlyPath}`);
}
assert.match(runtimePreparation, /['"]--no-compile['"]/);
assert.match(runtimePreparation, /['"]--no-build-isolation['"]/);
assert.match(runtimePreparation, /['"]--require-hashes['"]/);
Expand Down
Loading