@@ -69,6 +69,7 @@ function main() {
6969 }
7070
7171 const chaosEngineBundle = stageVendoredChaosEngine ( pythonInfo . executable ) ;
72+ const bundledOptionalPackages = stageOptionalRuntimePackages ( pythonInfo . executable ) ;
7273 validateBundledProjectImports ( pythonInfo . executable ) ;
7374 const llamaWarnings = stageLlamaBinaries ( ) ;
7475 maybeSignEmbeddedRuntime ( ) ;
@@ -88,6 +89,7 @@ function main() {
8889 llamaCli : fs . existsSync ( path . join ( binDest , binaryName ( "llama-cli" ) ) ) ? `bin/${ binaryName ( "llama-cli" ) } ` : null ,
8990 pythonVersion : pythonInfo . versionTag ,
9091 bundledCacheStrategies : chaosEngineBundle ? [ "chaosengine" ] : [ ] ,
92+ bundledOptionalPackages : bundledOptionalPackages ,
9193 warnings : llamaWarnings ,
9294 } ;
9395
@@ -169,6 +171,7 @@ function validateBundledPythonPackages(pythonBinary) {
169171 "requirements = {" ,
170172 " 'desktop': [('fastapi', 'fastapi'), ('huggingface_hub', 'huggingface_hub'), ('psutil', 'psutil'), ('uvicorn', 'uvicorn')]," ,
171173 " 'images': [('accelerate', 'accelerate'), ('diffusers', 'diffusers'), ('huggingface_hub', 'huggingface_hub'), ('PIL', 'pillow'), ('safetensors', 'safetensors'), ('torch', 'torch')]," ,
174+ " 'inference': [('dflash_mlx', 'dflash-mlx'), ('turboquant', 'turboquant'), ('turboquant_mlx', 'turboquant-mlx-full')]," ,
172175 "}" ,
173176 "missing = {" ,
174177 " group: [label for module, label in modules if importlib.util.find_spec(module) is None]" ,
@@ -182,14 +185,28 @@ function validateBundledPythonPackages(pythonBinary) {
182185 encoding : "utf8" ,
183186 } ) . trim ( ) ;
184187 const missing = JSON . parse ( payload ) ;
185- const flatMissing = Object . entries ( missing )
188+
189+ // Inference packages are optional — warn but never block the build.
190+ const optionalGroups = new Set ( [ "inference" ] ) ;
191+ const requiredMissing = Object . entries ( missing )
192+ . filter ( ( [ group ] ) => ! optionalGroups . has ( group ) )
193+ . flatMap ( ( [ group , values ] ) => values . length ? values . map ( ( value ) => `${ group } :${ value } ` ) : [ ] ) ;
194+ const optionalMissing = Object . entries ( missing )
195+ . filter ( ( [ group ] ) => optionalGroups . has ( group ) )
186196 . flatMap ( ( [ group , values ] ) => values . length ? values . map ( ( value ) => `${ group } :${ value } ` ) : [ ] ) ;
187- if ( flatMissing . length === 0 ) {
197+
198+ if ( optionalMissing . length ) {
199+ console . warn (
200+ `[stage-runtime] info: optional inference packages not in build venv (${ optionalMissing . join ( ", " ) } ). ` +
201+ `DFlash/TurboQuant will require user install via the Setup page.` ,
202+ ) ;
203+ }
204+ if ( requiredMissing . length === 0 ) {
188205 return ;
189206 }
190207
191208 const message =
192- `Embedded Python is missing required runtime packages (${ flatMissing . join ( ", " ) } ). ` +
209+ `Embedded Python is missing required runtime packages (${ requiredMissing . join ( ", " ) } ). ` +
193210 `Install them into the build venv with: ${ pythonBinary } -m pip install -e ".[desktop,images]"` ;
194211 if ( strict ) {
195212 throw new Error ( message ) ;
@@ -278,6 +295,82 @@ function resolveChaosEngineVendor() {
278295 } ;
279296}
280297
298+ function stageOptionalRuntimePackages ( pythonBinary ) {
299+ // Pre-install optional runtime packages into the staged site-packages
300+ // so that DFlash, TurboQuant, and RotorQuant work out of the box for
301+ // new users without requiring manual pip installs via the Setup page.
302+ //
303+ // Each entry: [pip package name, import name used for verification]
304+ const optionalPackages = [
305+ [ "dflash-mlx" , "dflash_mlx" ] ,
306+ [ "turboquant" , "turboquant" ] ,
307+ [ "turboquant-mlx-full" , "turboquant_mlx" ] ,
308+ ] ;
309+
310+ const installed = [ ] ;
311+ const skipped = [ ] ;
312+
313+ for ( const [ pipName , importName ] of optionalPackages ) {
314+ // Check if already available in the build venv
315+ const checkScript = `import importlib.util; exit(0 if importlib.util.find_spec("${ importName } ") else 1)` ;
316+ let available = false ;
317+ try {
318+ execFileSync ( pythonBinary , [ "-c" , checkScript ] , {
319+ cwd : workspaceRoot ,
320+ stdio : "ignore" ,
321+ } ) ;
322+ available = true ;
323+ } catch {
324+ available = false ;
325+ }
326+
327+ if ( ! available ) {
328+ const message = `Optional package "${ pipName } " not found in build venv — skipping bundle` ;
329+ if ( strict ) {
330+ console . warn ( `[stage-runtime] warning: ${ message } . Install with: ${ pythonBinary } -m pip install ${ pipName } ` ) ;
331+ }
332+ skipped . push ( pipName ) ;
333+ continue ;
334+ }
335+
336+ try {
337+ console . log ( `[stage-runtime] bundling optional package: ${ pipName } ` ) ;
338+ execFileSync (
339+ pythonBinary ,
340+ [
341+ "-m" , "pip" , "install" ,
342+ "--disable-pip-version-check" ,
343+ "--no-deps" ,
344+ "--no-compile" ,
345+ "--upgrade" ,
346+ "--target" , sitePackagesDest ,
347+ pipName ,
348+ ] ,
349+ {
350+ cwd : workspaceRoot ,
351+ stdio : "inherit" ,
352+ } ,
353+ ) ;
354+ installed . push ( pipName ) ;
355+ } catch ( err ) {
356+ const message = `Failed to bundle optional package "${ pipName } ": ${ err . message } ` ;
357+ if ( strict ) {
358+ throw new Error ( message ) ;
359+ }
360+ console . warn ( `[stage-runtime] warning: ${ message } ` ) ;
361+ skipped . push ( pipName ) ;
362+ }
363+ }
364+
365+ if ( installed . length ) {
366+ console . log ( `[stage-runtime] bundled optional packages: ${ installed . join ( ", " ) } ` ) ;
367+ }
368+ if ( skipped . length ) {
369+ console . log ( `[stage-runtime] skipped optional packages (not in build venv): ${ skipped . join ( ", " ) } ` ) ;
370+ }
371+ return installed ;
372+ }
373+
281374function stageLlamaBinaries ( ) {
282375 const warnings = [ ] ;
283376 const sourceDir = process . env . CHAOSENGINE_LLAMA_BIN_DIR || defaultLlamaBinDir ( ) ;
0 commit comments