arcane-os 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ import {ArcaneError,ERROR_CODES,normalizeError,throwIfAborted} from '../errors.m
9
9
  import {createEventQueue} from '../event-queue.mjs';
10
10
  import {runProcess} from '../process.mjs';
11
11
  import {extractLocalAIArchive} from './archive.mjs';
12
+ import {normalizeImageRuntimeRequirement} from './config.mjs';
12
13
 
13
14
  const is=new Is(false);
14
15
  const installations=new Map();
@@ -19,12 +20,13 @@ function selectRuntimes(runtimes){
19
20
  }
20
21
  const selected=new Map();
21
22
  for(const item of runtimes){
22
- const runtime=is.string(item)?{id:item}:item;
23
- if(!runtime||!['llama.cpp','ollama','nemo-speech','onnx'].includes(runtime.id)){
23
+ const requirement=is.string(item)?{id:item}:item;
24
+ const runtime=requirement?.id==='stable-diffusion.cpp'?normalizeImageRuntimeRequirement(requirement):requirement;
25
+ if(!runtime||!['llama.cpp','ollama','nemo-speech','onnx','stable-diffusion.cpp'].includes(runtime.id)){
24
26
  throw new ArcaneError(ERROR_CODES.targetUnavailable,`Local AI runtime installation is unavailable for ${String(runtime?.id)}.`);
25
27
  }
26
28
  const prior=selected.get(runtime.id);
27
- if(prior&&(prior.version!==runtime.version||prior.url!==runtime.url)){
29
+ if(prior&&(prior.version!==runtime.version||prior.url!==runtime.url||(runtime.id==='stable-diffusion.cpp'&&prior.backend!==runtime.backend))){
28
30
  throw new ArcaneError(ERROR_CODES.usage,`Select one ${runtime.id} runtime version and URL for this operation.`);
29
31
  }
30
32
  if(runtime.version!==undefined&&(!is.string(runtime.version)||!runtime.version)){
@@ -82,6 +84,9 @@ async function upstreamResponse(url,signal){
82
84
  }
83
85
 
84
86
  async function selectRelease(runtime,platform,architecture,signal){
87
+ if (runtime.id === 'stable-diffusion.cpp') {
88
+ return selectImageRelease(runtime, platform, architecture, signal);
89
+ }
85
90
  if(runtime.url){
86
91
  const name=path.posix.basename(new URL(runtime.url).pathname);
87
92
  return {version:runtime.version??'custom',url:runtime.url,name};
@@ -109,6 +114,243 @@ async function selectRelease(runtime,platform,architecture,signal){
109
114
  return {version:release.tag_name,url:asset.browser_download_url,name:asset.name};
110
115
  }
111
116
 
117
+ async function imageBackendCandidate(backend, platform, signal) {
118
+ if (backend !== 'auto') return backend;
119
+ if (platform === 'darwin') return 'metal';
120
+ // This selects a distribution candidate. The worker probes actual devices.
121
+ return 'cpu';
122
+ }
123
+
124
+ function imageAssetSuffix(platform, architecture, backend) {
125
+ if (platform === 'win32' && architecture === 'x64') {
126
+ if (backend === 'cpu') return '-bin-win-cpu-x64.zip';
127
+ }
128
+ if (platform === 'linux' && architecture === 'x64') {
129
+ if (backend === 'cpu') return '-bin-Linux-Ubuntu-24.04-x86_64.zip';
130
+ }
131
+ if (platform === 'darwin' && ['x64', 'arm64'].includes(architecture) && ['metal', 'cpu'].includes(backend)) {
132
+ // This upstream archive contains the universal arm64/x86_64 build.
133
+ return '-bin-Darwin-macOS-26.6.2-arm64.zip';
134
+ }
135
+ return null;
136
+ }
137
+
138
+ async function selectImageRelease(runtime, platform, architecture, signal) {
139
+ const backend = runtime.url ? runtime.backend : await imageBackendCandidate(runtime.backend, platform, signal);
140
+ if (runtime.url) {
141
+ return {
142
+ version: runtime.version,
143
+ backend: runtime.backend,
144
+ assets: [
145
+ {
146
+ backend,
147
+ url: runtime.url,
148
+ name: path.posix.basename(new URL(runtime.url).pathname)
149
+ }
150
+ ],
151
+ sharedCPU: backend === 'auto'
152
+ };
153
+ }
154
+ const selection = runtime.version === 'latest' ? 'latest' : `tags/${encodeURIComponent(runtime.version)}`;
155
+ const response = await upstreamResponse(`https://api.github.com/repos/leejet/stable-diffusion.cpp/releases/${selection}`, signal);
156
+ const release = await response.json();
157
+ const backends = backend === 'cpu' || platform === 'darwin' ? [backend] : [backend, 'cpu'];
158
+ const assets = backends.map(
159
+ function selectImageArchive(selectedBackend) {
160
+ const suffix = imageAssetSuffix(platform, architecture, selectedBackend);
161
+ const asset = suffix && release.assets?.find(
162
+ function matchingImageArchive(candidate) {
163
+ return candidate.name.endsWith(suffix);
164
+ }
165
+ );
166
+ if (!asset) {
167
+ throw new ArcaneError(ERROR_CODES.targetUnavailable, `stable-diffusion.cpp ${release.tag_name} has no selected ${platform}/${architecture} ${selectedBackend} archive.`);
168
+ }
169
+ const selected = {backend: selectedBackend, url: asset.browser_download_url, name: asset.name};
170
+ return selected;
171
+ }
172
+ );
173
+ return {version: release.tag_name, backend: runtime.backend, assets, sharedCPU: backend === 'metal'};
174
+ }
175
+
176
+ async function imageLibraryPath(root, platform, signal) {
177
+ const filename = platform === 'win32' ? 'stable-diffusion.dll' : platform === 'darwin' ? 'libstable-diffusion.dylib' : 'libstable-diffusion.so';
178
+ const directories = [root];
179
+ for (const directory of directories) {
180
+ throwIfAborted(signal);
181
+ const entries = await readdir(
182
+ directory,
183
+ {withFileTypes: true}
184
+ );
185
+ for (const entry of entries) {
186
+ const selected = path.join(directory, entry.name);
187
+ if (entry.name === filename && (entry.isFile() || entry.isSymbolicLink())) return selected;
188
+ if (entry.isDirectory()) directories.push(selected);
189
+ }
190
+ }
191
+ throw new ArcaneError(ERROR_CODES.operationFailed, `The stable-diffusion.cpp archive does not contain ${filename}.`);
192
+ }
193
+
194
+ async function extractImageAsset(asset, root, signal, onEvent) {
195
+ const parent = path.dirname(root);
196
+ const archive = path.join(parent, asset.name);
197
+ await onEvent(
198
+ {
199
+ type: 'local-ai.install.downloading',
200
+ message: `Downloading ${asset.name}.`,
201
+ data: {id: 'stable-diffusion.cpp', asset: asset.name}
202
+ }
203
+ );
204
+ const response = await upstreamResponse(asset.url, signal);
205
+ await pipeline(
206
+ Readable.fromWeb(response.body),
207
+ createWriteStream(
208
+ archive,
209
+ {flags: 'wx'}
210
+ ),
211
+ {signal}
212
+ );
213
+ await onEvent(
214
+ {
215
+ type: 'local-ai.install.extracting',
216
+ message: `Extracting ${asset.name}.`,
217
+ data: {id: 'stable-diffusion.cpp', asset: asset.name}
218
+ }
219
+ );
220
+ await extractLocalAIArchive(
221
+ {archive, directory: root, signal, onEvent}
222
+ );
223
+ await unlink(archive);
224
+ }
225
+
226
+ async function installImageBinding(root, directory, platform, architecture, signal, onEvent) {
227
+ const prefix = path.join(root, 'binding');
228
+ await mkdir(
229
+ prefix,
230
+ {recursive: true}
231
+ );
232
+ await writeFile(
233
+ path.join(prefix, 'package.json'),
234
+ '{"private":true}\n',
235
+ {flag: 'wx', signal}
236
+ );
237
+ await onEvent(
238
+ {
239
+ type: 'local-ai.install.installing',
240
+ message: `Installing koffi 3.3.2 for ${platform}/${architecture}.`,
241
+ data: {id: 'stable-diffusion.cpp', package: 'koffi', version: '3.3.2', platform, architecture}
242
+ }
243
+ );
244
+ await runProcess(
245
+ 'npm',
246
+ [
247
+ 'install', '--prefix', prefix, '--global=false', '--save-exact',
248
+ '--omit=dev', '--include=optional', '--ignore-scripts', '--no-audit', '--no-fund',
249
+ '--registry=https://registry.npmjs.org/', '--cache', path.join(directory, 'npm-cache'),
250
+ `--os=${platform}`, `--cpu=${architecture}`, 'koffi@3.3.2'
251
+ ],
252
+ {cwd: prefix, signal, onEvent}
253
+ );
254
+ const packagePath = path.join(prefix, 'package.json');
255
+ const bindingModulePath = createRequire(packagePath).resolve('koffi');
256
+ const platformPackagePath = path.join(prefix, 'node_modules', '@koromix', `koffi-${platform}-${architecture}`, 'package.json');
257
+ await stat(platformPackagePath);
258
+ return bindingModulePath;
259
+ }
260
+
261
+ async function installImageDistribution(release, {root, directory, platform, architecture, signal, onEvent}) {
262
+ await mkdir(
263
+ root,
264
+ {recursive: true}
265
+ );
266
+ const jobs = release.assets.map(
267
+ async function prepareImageVariant(asset) {
268
+ const variantRoot = path.join(root, asset.backend);
269
+ await extractImageAsset(asset, variantRoot, signal, onEvent);
270
+ const libraryPath = await imageLibraryPath(variantRoot, platform, signal);
271
+ return {backend: asset.backend, root: variantRoot, libraryPath};
272
+ }
273
+ );
274
+ const bindingJob = installImageBinding(root, directory, platform, architecture, signal, onEvent);
275
+ jobs.push(bindingJob);
276
+ const outcomes = await Promise.allSettled(jobs);
277
+ const failures = outcomes.filter(
278
+ function imagePreparationFailed(outcome) {
279
+ return outcome.status === 'rejected';
280
+ }
281
+ );
282
+ if (failures.length === 1) throw failures[0].reason;
283
+ if (failures.length > 1) {
284
+ throw new AggregateError(
285
+ failures.map(
286
+ function imagePreparationError(outcome) {
287
+ return outcome.reason;
288
+ }
289
+ ),
290
+ 'Local image runtime preparation failed.'
291
+ );
292
+ }
293
+ const bindingModulePath = outcomes.pop().value;
294
+ const variants = outcomes.map(
295
+ function preparedImageVariant(outcome) {
296
+ return outcome.value;
297
+ }
298
+ );
299
+ if (release.sharedCPU) {
300
+ variants.push(
301
+ {...variants[0], backend: 'cpu'}
302
+ );
303
+ }
304
+ return {
305
+ version: release.version,
306
+ backend: release.backend,
307
+ libraryPath: variants[0].libraryPath,
308
+ bindingModulePath,
309
+ variants
310
+ };
311
+ }
312
+
313
+ function imageModelRecords(models) {
314
+ return models.map(
315
+ function preparedImageModel(model) {
316
+ const resources = {};
317
+ for (const [role, resource] of Object.entries(model.resources)) {
318
+ const location = is.string(resource) ? resource : resource?.path;
319
+ resources[role] = is.string(location) ? path.resolve(location) : resource;
320
+ }
321
+ return {...model, resources};
322
+ }
323
+ );
324
+ }
325
+
326
+ function bundledImagePath(location, runtimeRoot, relativeRoot) {
327
+ const relative = path.relative(runtimeRoot, location);
328
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
329
+ // The model resource owner reports external-path incompatibility.
330
+ return location;
331
+ }
332
+ return path.posix.join(relativeRoot, relative.split(path.sep).join('/'));
333
+ }
334
+
335
+ function bundledImageModelResource(resource, descriptor, model, role, runtimeRoot, relativeRoot) {
336
+ if (descriptor && !is.string(descriptor) && is.string(descriptor.url)) {
337
+ // A portable descriptor selects its original URL for post-launch asset
338
+ // preparation rather than carrying this host's working projection.
339
+ const portable = {...descriptor};
340
+ delete portable.path;
341
+ return portable;
342
+ }
343
+ if (!is.string(resource)) return resource;
344
+ const bundled = bundledImagePath(resource, runtimeRoot, relativeRoot);
345
+ if (bundled === resource) {
346
+ throw new ArcaneError(
347
+ ERROR_CODES.usage,
348
+ `Image model ${model.id} resource ${role} is an external native working path and is not portable in this runtime bundle. Prepare it through modelAssets after launch or include it through the explicit nativeResources workflow.`
349
+ );
350
+ }
351
+ return bundled;
352
+ }
353
+
112
354
  async function installedRuntime(base,runtime,platform,architecture,signal){
113
355
  let entries;
114
356
  try{
@@ -117,6 +359,9 @@ async function installedRuntime(base,runtime,platform,architecture,signal){
117
359
  if(error.code==='ENOENT')return null;
118
360
  throw error;
119
361
  }
362
+ const imageBackend = runtime.id === 'stable-diffusion.cpp' && !runtime.url
363
+ ? await imageBackendCandidate(runtime.backend, platform, signal)
364
+ : runtime.backend;
120
365
  for(const entry of entries){
121
366
  throwIfAborted(signal);
122
367
  if(!entry.isDirectory())continue;
@@ -128,9 +373,38 @@ async function installedRuntime(base,runtime,platform,architecture,signal){
128
373
  throw error;
129
374
  }
130
375
  if(installation.requestVersion!==(runtime.version??null)||installation.requestUrl!==(runtime.url??null))continue;
376
+ if (runtime.id === 'stable-diffusion.cpp' && installation.requestBackend !== runtime.backend) continue;
131
377
  const record=installation.runtime;
132
378
  if(record?.id!==runtime.id||record.platform!==platform||record.architecture!==architecture)continue;
133
379
  try{
380
+ if (runtime.id === 'stable-diffusion.cpp') {
381
+ const locations = [record.libraryPath, record.bindingModulePath];
382
+ if (!Array.isArray(record.variants) || record.variants.length === 0) continue;
383
+ if (record.variants[0].backend !== imageBackend) continue;
384
+ for (const variant of record.variants) locations.push(variant.libraryPath);
385
+ const missingLocation = locations.some(
386
+ function missingImageLocation(location) {
387
+ return !is.string(location);
388
+ }
389
+ );
390
+ if (missingLocation) continue;
391
+ const platformPackagePath = path.join(record.root, 'binding', 'node_modules', '@koromix', `koffi-${platform}-${architecture}`, 'package.json');
392
+ locations.push(platformPackagePath);
393
+ const entries = await Promise.all(
394
+ locations.map(
395
+ function inspectImageLocation(location) {
396
+ return stat(location);
397
+ }
398
+ )
399
+ );
400
+ const filesPresent = entries.every(
401
+ function imageFilePresent(info) {
402
+ return info.isFile();
403
+ }
404
+ );
405
+ if (filesPresent) return record;
406
+ continue;
407
+ }
134
408
  if(runtime.id==='onnx'){
135
409
  if(!is.string(record.root)||!is.string(record.modulePath))continue;
136
410
  const locations=await onnxRuntimeModule(record.root,platform,architecture,signal);
@@ -242,7 +516,8 @@ async function installONNXPackage(runtime,{root,directory,platform,architecture,
242
516
  }
243
517
 
244
518
  async function installRuntime(runtime,{directory,platform,architecture,signal,onEvent}){
245
- const base=path.join(directory,runtime.id,`${platform}-${architecture}`,encodeURIComponent(runtime.version??'default'));
519
+ const runtimeBase=path.join(directory,runtime.id,`${platform}-${architecture}`,encodeURIComponent(runtime.version??'default'));
520
+ const base=runtime.id==='stable-diffusion.cpp'?path.join(runtimeBase,runtime.backend):runtimeBase;
246
521
  await onEvent({type:'local-ai.install.starting',message:`Preparing ${runtime.id} for ${platform}/${architecture}.`,data:{id:runtime.id,platform,architecture}});
247
522
  const installed=await installedRuntime(base,runtime,platform,architecture,signal);
248
523
  if(installed){
@@ -257,12 +532,17 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
257
532
  await mkdir(base,{recursive:true});
258
533
  const attempt=await mkdtemp(path.join(base,'install-'));
259
534
  const root=path.join(attempt,'runtime');
260
- const archive=release?path.join(attempt,release.name):null;
535
+ const archive=release&&runtime.id!=='stable-diffusion.cpp'?path.join(attempt,release.name):null;
261
536
  let completed=false;
262
537
  try{
263
538
  let locations;
264
539
  if(runtime.id==='onnx'){
265
540
  locations=await installONNXPackage(runtime,{root,directory,platform,architecture,signal,onEvent});
541
+ } else if (runtime.id === 'stable-diffusion.cpp') {
542
+ locations = await installImageDistribution(
543
+ release,
544
+ {root, directory, platform, architecture, signal, onEvent}
545
+ );
266
546
  }else{
267
547
  await onEvent({type:'local-ai.install.downloading',message:`Downloading ${runtime.id} ${release.version}.`,data:{id:runtime.id,version:release.version,platform,architecture}});
268
548
  const response=await upstreamResponse(release.url,signal);
@@ -276,7 +556,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
276
556
  const record={id:runtime.id,platform,architecture,root,...locations,...(runtime.version?{requestedVersion:runtime.version}:{})};
277
557
  throwIfAborted(signal);
278
558
  if(archive)await unlink(archive);
279
- await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,runtime:record},null,2)}\n`,{flag:'wx',signal});
559
+ await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,...(runtime.id==='stable-diffusion.cpp'?{requestBackend:runtime.backend}:{}),runtime:record},null,2)}\n`,{flag:'wx',signal});
280
560
  completed=true;
281
561
  await onEvent({type:'local-ai.install.completed',message:`${runtime.id} ${record.version} is available.`,data:record});
282
562
  return record;
@@ -296,7 +576,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
296
576
 
297
577
  function shareInstallation(runtime,options){
298
578
  throwIfAborted(options.signal);
299
- const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null]);
579
+ const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null,runtime.id==='stable-diffusion.cpp'?runtime.backend:null]);
300
580
  let entry=installations.get(key);
301
581
  if(entry?.controller.signal.aborted){
302
582
  // The prior cancelled attempt owns its cleanup. A later request waits
@@ -392,7 +672,11 @@ export async function ensureLocalAIRuntimes({runtimes=[],directory,platform=proc
392
672
  if(failures.length===1)throw failures[0].reason;
393
673
  if(failures.length>1)throw new AggregateError(failures.map(function runtimeFailure(outcome){return outcome.reason;}),'Local AI runtime preparation failed.');
394
674
  throwIfAborted(operationSignal);
395
- return outcomes.map(function completedRuntime(outcome){return outcome.value;});
675
+ return outcomes.map(function completedRuntime(outcome,index){
676
+ const record=outcome.value;
677
+ if(record.id==='stable-diffusion.cpp')return {...record,models:imageModelRecords(selected[index].models)};
678
+ return record;
679
+ });
396
680
  }finally{
397
681
  clearInterval(heartbeat);
398
682
  await events.drain();
@@ -441,12 +725,44 @@ export async function bundleLocalAIRuntimes({runtimes=[],directory,outputRoot,pl
441
725
  const files=[];
442
726
  const relative=`runtime/local-ai/${runtime.id}`;
443
727
  const destination=path.join(root,relative);
444
- await copyRuntimeTree(runtime.root,destination,root,files,signal);
445
728
  const bundled={...runtime,root:relative};
446
- for(const field of ['executable','modulePath','includeDirectory','libraryDirectory','binaryDirectory','cmakeDirectory']){
729
+ for(const field of ['executable','modulePath','includeDirectory','libraryDirectory','binaryDirectory','cmakeDirectory','libraryPath','bindingModulePath']){
447
730
  if(runtime[field]===undefined)continue;
448
731
  bundled[field]=path.posix.join(relative,path.relative(runtime.root,runtime[field]).split(path.sep).join('/'));
449
732
  }
733
+ if (runtime.id === 'stable-diffusion.cpp') {
734
+ const requirement = selected.find(
735
+ function selectedImageRequirement(candidate) {
736
+ return candidate.id === runtime.id;
737
+ }
738
+ );
739
+ bundled.variants = runtime.variants.map(
740
+ function bundledImageVariant(variant) {
741
+ return {
742
+ ...variant,
743
+ root: bundledImagePath(variant.root, runtime.root, relative),
744
+ libraryPath: bundledImagePath(variant.libraryPath, runtime.root, relative)
745
+ };
746
+ }
747
+ );
748
+ bundled.models = runtime.models.map(
749
+ function bundledImageModel(model, index) {
750
+ const resources = {};
751
+ for (const [role, resource] of Object.entries(model.resources)) {
752
+ resources[role] = bundledImageModelResource(
753
+ resource,
754
+ requirement.models[index].resources[role],
755
+ model,
756
+ role,
757
+ runtime.root,
758
+ relative
759
+ );
760
+ }
761
+ return {...model, resources};
762
+ }
763
+ );
764
+ }
765
+ await copyRuntimeTree(runtime.root,destination,root,files,signal);
450
766
  return {runtime:bundled,files};
451
767
  }));
452
768
  const failures=outcomes.filter(function failedBundle(outcome){return outcome.status==='rejected';});
@@ -150,6 +150,9 @@ export async function copyCoreRuntime(root, files, signal, {
150
150
  }
151
151
 
152
152
  export function coreEntrySource(application, version, services, {localAI, runtimes = [], packagedWeb = false} = {}) {
153
+ const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
154
+ return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
155
+ }) ?? false;
153
156
  const imports = services.map(function serviceImport(service, index) {
154
157
  const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
155
158
  return `import createService${index} from ${JSON.stringify(specifier)};`;
@@ -159,8 +162,20 @@ export function coreEntrySource(application, version, services, {localAI, runtim
159
162
  return ` createService${index}(JSON.parse(${JSON.stringify(JSON.stringify(service.options === undefined ? {} : service.options))}), context)`;
160
163
  });
161
164
  if (localAI !== undefined) {
165
+ const chatConfiguration = {
166
+ ...localAI,
167
+ runtimes: localAI.runtimes.filter(function selectedChat(requirement) {
168
+ return (typeof requirement === 'string' ? requirement : requirement.id) !== 'stable-diffusion.cpp';
169
+ })
170
+ };
162
171
  imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
163
- definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), context)`);
172
+ definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), context)`);
173
+ }
174
+ if (imageSelected) {
175
+ imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
176
+ imports.push("import {createModelAssetService} from 'arcane-os/core/model-assets';");
177
+ definitions.push(' modelAssets');
178
+ definitions.push(` createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets})`);
164
179
  }
165
180
  if (packagedWeb) {
166
181
  imports.push("import {createPackagedWebService} from 'arcane-os/core/packaged-web';");
@@ -185,15 +200,26 @@ export function coreEntrySource(application, version, services, {localAI, runtim
185
200
  ...(localAI === undefined ? [] : [
186
201
  ` runtimes: JSON.parse(${JSON.stringify(JSON.stringify(runtimes))}).map(function runtimeLocation(runtime) {`,
187
202
  ' const resolved = {...runtime};',
188
- " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory']) {",
203
+ " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory', 'libraryPath', 'bindingModulePath']) {",
189
204
  ' if (runtime[field] !== undefined) resolved[field] = runtimePath(runtime[field]);',
190
205
  ' }',
206
+ ' if (runtime.variants) resolved.variants = runtime.variants.map(function nativeVariant(variant) {',
207
+ ' return {...variant, root: runtimePath(variant.root), libraryPath: runtimePath(variant.libraryPath)};',
208
+ ' });',
209
+ ' if (runtime.models) resolved.models = runtime.models.map(function nativeModel(model) {',
210
+ ' const resources = {};',
211
+ ' for (const [role, resource] of Object.entries(model.resources)) {',
212
+ " resources[role] = typeof resource === 'string' ? runtimePath(resource) : resource;",
213
+ ' }',
214
+ ' return {...model, resources};',
215
+ ' });',
191
216
  ' return resolved;',
192
217
  ' }),'
193
218
  ]),
194
219
  ' ...await readCoreLaunchContext()',
195
220
  '};',
196
221
  '',
222
+ ...(imageSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
197
223
  'const host = startCoreHost({',
198
224
  ` application: JSON.parse(${JSON.stringify(JSON.stringify(application))}),`,
199
225
  ` version: ${JSON.stringify(version)},`,