arcane-os 0.54.1 → 0.56.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +35 -28
  3. package/browser-runtime/core/client.mjs +552 -0
  4. package/browser-runtime/core/contracts.mjs +85 -0
  5. package/docs/architecture.md +31 -23
  6. package/docs/compatibility.md +10 -1
  7. package/docs/consumer-updates.md +82 -0
  8. package/docs/platform-targets.md +20 -11
  9. package/docs/publishing.md +41 -18
  10. package/docs/reference/README.md +19 -7
  11. package/docs/reference/availability-and-normalization.md +15 -5
  12. package/docs/reference/behavioral-testing.md +7 -6
  13. package/docs/reference/cli.md +12 -4
  14. package/docs/reference/core-client.md +163 -0
  15. package/docs/reference/core-native-packaging.md +149 -0
  16. package/docs/reference/core-runtime.md +185 -0
  17. package/docs/reference/inventory/package-api.json +271 -5
  18. package/docs/reference/protocols.md +38 -14
  19. package/docs/reference/pwa.md +17 -0
  20. package/docs/reference/sdk-api.md +772 -15
  21. package/package.json +10 -3
  22. package/schemas/arcane-app.schema.json +10 -0
  23. package/schemas/arcane-package.schema.json +10 -0
  24. package/schemas/native-build-plan.schema.json +8 -0
  25. package/src/app-descriptor.mjs +18 -1
  26. package/src/app-layout.mjs +6 -2
  27. package/src/cli/main.mjs +8 -6
  28. package/src/core/host.mjs +23 -0
  29. package/src/core/hosts/arcane-core-loader.cjs +2 -0
  30. package/src/core/hosts/linux/arcane_core_process.c +568 -0
  31. package/src/core/hosts/linux/arcane_core_process.h +127 -0
  32. package/src/core/hosts/sea-launcher.cjs +17 -0
  33. package/src/core/hosts/windows/ArcaneCoreProcess.cs +416 -0
  34. package/src/core/runtime.mjs +345 -0
  35. package/src/core/stdio.mjs +209 -0
  36. package/src/core-classic-source.mjs +43 -0
  37. package/src/import-map.mjs +62 -0
  38. package/src/native/portable-layout.mjs +184 -0
  39. package/src/native/portable-provider.mjs +211 -0
  40. package/src/native-plan.mjs +10 -4
  41. package/src/native-provider-loader.mjs +15 -4
  42. package/src/packager/core.mjs +131 -32
  43. package/src/process.mjs +8 -4
  44. package/src/targets/index.mjs +3 -1
  45. package/src/toolchain.mjs +23 -6
@@ -13,6 +13,7 @@ import {
13
13
  import path from 'node:path';
14
14
  import {pathToFileURL} from 'node:url';
15
15
  import {appRelativeRoot,resolveAppRoot,resolvePackageOutputRoot} from '../app-layout.mjs';
16
+ import {TARGET_IDS} from '../constants.mjs';
16
17
  import {readInstalledSdkLayout} from '../sdk-runtime-layout.mjs';
17
18
  import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
18
19
  import {materializeNativeModules} from './native-modules.mjs';
@@ -20,6 +21,7 @@ import {
20
21
  applyPwaEntryReferences,
21
22
  inspectImportMapHtml,
22
23
  readWorkspaceAssetVersion,
24
+ removePwaEntryReferences,
23
25
  resolveAssetReference,
24
26
  rewriteAssetReferences,
25
27
  versionAssetUrl
@@ -319,7 +321,7 @@ function normalizeOptionalRecord(value,label){
319
321
  export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.join(appRelativeRoot(rootConfig,appId),APP_CONFIG_NAME)){
320
322
  assertOnlyKeys(value,new Set([
321
323
  'schemaVersion','id','displayName','version','entry','strategy','security',
322
- 'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory','documents'
324
+ 'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory','documents','nativeResources'
323
325
  ]),`${appId}/${APP_CONFIG_NAME}`);
324
326
  if(value.schemaVersion!==1)fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
325
327
  if(!is.string(value.id)||value.id!==appId||!APP_ID_PATTERN.test(value.id)){
@@ -334,6 +336,15 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
334
336
  );
335
337
  const include=validatePathList(value.include,`${appId}/${APP_CONFIG_NAME}.include`,{required:true});
336
338
  const exclude=validatePathList(value.exclude??[],`${appId}/${APP_CONFIG_NAME}.exclude`);
339
+ let nativeResources;
340
+ if(value.nativeResources!==undefined){
341
+ const label=`${appId}/${APP_CONFIG_NAME}.nativeResources`;
342
+ assertOnlyKeys(value.nativeResources,new Set(['include','exclude']),label);
343
+ nativeResources={
344
+ include:validatePathList(value.nativeResources.include,`${label}.include`),
345
+ exclude:validatePathList(value.nativeResources.exclude??[],`${label}.exclude`)
346
+ };
347
+ }
337
348
  if(include.some(allowed=>sameOrDescendant(APP_CONFIG_NAME,allowed))){
338
349
  fail(`${appId}/${APP_CONFIG_NAME}.include must not expose the authored package configuration.`);
339
350
  }
@@ -381,6 +392,7 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
381
392
  )}),
382
393
  include,
383
394
  exclude,
395
+ ...(nativeResources===undefined?{}:{nativeResources}),
384
396
  shared:[...value.shared],
385
397
  ...(adapter===undefined?{}:{adapter}),
386
398
  configPath
@@ -422,7 +434,8 @@ async function assertContainedRealPath(root,candidate,label){
422
434
  }
423
435
  }
424
436
 
425
- async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory}={}){
437
+ async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory,target='browser'}={}){
438
+ if(!TARGET_IDS.includes(target))fail(`Unsupported package target: ${String(target)}.`);
426
439
  const workspaceRoot=await realDirectory(normalizeWorkspaceRoot(requestedWorkspaceRoot),'Workspace root');
427
440
  const rootConfigPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
428
441
  const rootConfig=validateRootConfig(await readJson(rootConfigPath,ROOT_CONFIG_NAME),rootConfigPath);
@@ -435,14 +448,19 @@ async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory}={}){
435
448
  if(outputDirectory!==undefined){
436
449
  config.outputDirectory=normalizeRelativePath(outputDirectory,'package outputDirectory');
437
450
  }
451
+ const outputRoot=resolvePackageOutputRoot(workspaceRoot,rootConfig,config,{target,outputDirectory});
452
+ if(target!=='browser'&&config.nativeResources?.include.length>0){
453
+ config.outputDirectory=path.relative(workspaceRoot,outputRoot).split(path.sep).join('/');
454
+ }
438
455
  return {
439
456
  workspaceRoot,
440
457
  rootConfig,
441
458
  appsRoot,
442
459
  appRoot,
443
460
  appId,
461
+ target,
444
462
  config,
445
- outputRoot:resolvePackageOutputRoot(workspaceRoot,rootConfig,config)
463
+ outputRoot
446
464
  };
447
465
  }
448
466
 
@@ -493,6 +511,11 @@ async function assertPackageOutputLocation(context){
493
511
  for(const selected of context.config.include){
494
512
  assertSelectedInput(context.appRoot,selected,context.config.exclude,context.appId);
495
513
  }
514
+ if(context.target!=='browser'&&context.config.nativeResources){
515
+ for(const selected of context.config.nativeResources.include){
516
+ assertSelectedInput(context.appRoot,selected,context.config.nativeResources.exclude,`${context.appId} native`);
517
+ }
518
+ }
496
519
  for(const sharedId of context.config.shared){
497
520
  for(const route of context.rootConfig.sharedPayloads[sharedId]){
498
521
  const sourceRoot=path.resolve(context.workspaceRoot,route.source);
@@ -525,6 +548,7 @@ async function collectSelectedPath({
525
548
  reject,
526
549
  records,
527
550
  destinations,
551
+ coalescedRecords,
528
552
  signal,
529
553
  label,
530
554
  allowRoot=false
@@ -554,6 +578,7 @@ async function collectSelectedPath({
554
578
  reject,
555
579
  records,
556
580
  destinations,
581
+ coalescedRecords,
557
582
  signal,
558
583
  label
559
584
  });
@@ -566,9 +591,15 @@ async function collectSelectedPath({
566
591
  fail(`${label} overlaps the generated release manifest.`);
567
592
  }
568
593
  const key=pathKey(normalizedDestination);
569
- if(destinations.has(key))fail(`Package destination collision: ${normalizedDestination}.`);
594
+ if(destinations.has(key)){
595
+ const existing=coalescedRecords?.get(key);
596
+ if(existing?.source===absolute&&existing.destination===normalizedDestination)return;
597
+ fail(`Package destination collision: ${normalizedDestination}.`);
598
+ }
570
599
  destinations.add(key);
571
- records.push({source:absolute,destination:normalizedDestination});
600
+ const record={source:absolute,destination:normalizedDestination};
601
+ records.push(record);
602
+ coalescedRecords?.set(key,record);
572
603
  }
573
604
 
574
605
  async function collectPackageRecords(context,{signal}={}){
@@ -612,6 +643,29 @@ async function collectPackageRecords(context,{signal}={}){
612
643
  }
613
644
  }
614
645
  }
646
+ if(context.target!=='browser'&&context.config.nativeResources){
647
+ const nativeRecords=[];
648
+ const coalescedRecords=new Map(records.map(record=>[pathKey(record.destination),record]));
649
+ for(const selected of context.config.nativeResources.include){
650
+ await collectSelectedPath({
651
+ sourceRoot:context.appRoot,
652
+ selected,
653
+ destination:appPackagePath(context,selected),
654
+ excludes:context.config.nativeResources.exclude,
655
+ reject:isAppSourceForbidden,
656
+ records:nativeRecords,
657
+ destinations,
658
+ coalescedRecords,
659
+ signal,
660
+ label:`${context.appId} nativeResources`
661
+ });
662
+ }
663
+ for(const record of nativeRecords){
664
+ record.appRelativePath=path.relative(context.appRoot,record.source).split(path.sep).join('/');
665
+ record.nativeOnly=true;
666
+ records.push(record);
667
+ }
668
+ }
615
669
  records.sort((left,right)=>compareText(left.destination,right.destination));
616
670
  if(!records.some(record=>pathKey(record.destination)===pathKey(appPackagePath(context, context.config.entry)))){
617
671
  fail(`Package entry is missing from the selected files: ${context.config.entry}.`);
@@ -670,7 +724,7 @@ async function optionalDescriptor(context){
670
724
 
671
725
  async function inspectContext(context,{signal,records}={}){
672
726
  const selectedRecords=records??await collectPackageRecords(context,{signal});
673
- const documents=await browserDocuments(selectedRecords,context.config.entry,context.config.documents);
727
+ const documents=await browserDocuments(selectedRecords.filter(record=>!record.nativeOnly),context.config.entry,context.config.documents);
674
728
  return {
675
729
  appId:context.appId,
676
730
  displayName:context.config.displayName,
@@ -682,6 +736,7 @@ async function inspectContext(context,{signal,records}={}){
682
736
  ...(context.config.pwa===undefined?{}:{pwa:copyJson(context.config.pwa)}),
683
737
  include:[...context.config.include],
684
738
  exclude:[...context.config.exclude],
739
+ ...(context.config.nativeResources===undefined?{}:{nativeResources:copyJson(context.config.nativeResources)}),
685
740
  shared:[...context.config.shared],
686
741
  ...(context.config.security===undefined?{}:{security:copyJson(context.config.security)}),
687
742
  ...(context.config.localAIModelPolicy===undefined?{}:{
@@ -723,9 +778,9 @@ export async function discoverApps({workspaceRoot:requestedWorkspaceRoot}={}){
723
778
  return apps;
724
779
  }
725
780
 
726
- export async function inspectApp({workspaceRoot,appId,signal}={}){
781
+ export async function inspectApp({workspaceRoot,appId,signal,target,outputDirectory}={}){
727
782
  throwIfAborted(signal);
728
- const context=await loadContext(workspaceRoot,appId);
783
+ const context=await loadContext(workspaceRoot,appId,{target,outputDirectory});
729
784
  return inspectContext(context,{signal});
730
785
  }
731
786
 
@@ -845,7 +900,8 @@ async function replaceDirectory(stagingRoot,outputRoot){
845
900
  }
846
901
 
847
902
  async function packageWithContext(context,options={}){
848
- const {signal,onEvent,browserPwa=true,moduleFormat='import-map'}=options;
903
+ const {signal,onEvent,moduleFormat='import-map',browserPwa:requestedBrowserPwa=true}=options;
904
+ const browserPwa=context.target==='browser'&&requestedBrowserPwa;
849
905
  if(!['import-map','native'].includes(moduleFormat)){
850
906
  throw new TypeError('packageApp moduleFormat must be "import-map" or "native".');
851
907
  }
@@ -853,7 +909,9 @@ async function packageWithContext(context,options={}){
853
909
  const appPath=appRelativeRoot(context.rootConfig,context.appId);
854
910
  const entryPath=appPackagePath(context,context.config.entry);
855
911
  const records=await collectPackageRecords(context,{signal});
856
- const rootDocument = records.some(
912
+ const browserRecords=records.filter(record=>!record.nativeOnly);
913
+ const nativeRecords=records.filter(record=>record.nativeOnly);
914
+ const rootDocument = browserRecords.some(
857
915
  function selectedRootIndex(record) {
858
916
  return record.destination === 'index.html';
859
917
  }
@@ -900,7 +958,7 @@ async function packageWithContext(context,options={}){
900
958
  let promoted=false;
901
959
  try{
902
960
  async function copyBase() {
903
- await copyRecords(records,stagingRoot,{signal,onEvent});
961
+ await copyRecords(browserRecords,stagingRoot,{signal,onEvent});
904
962
  if (!rootDocument) {
905
963
  await writePackageLauncher(context, stagingRoot);
906
964
  }
@@ -912,6 +970,8 @@ async function packageWithContext(context,options={}){
912
970
  workspaceRoot:context.workspaceRoot,
913
971
  appRoot:context.appRoot,
914
972
  outputRoot:stagingRoot,
973
+ target:context.target,
974
+ browserPwa,
915
975
  copyBase,
916
976
  signal,
917
977
  onEvent
@@ -920,12 +980,57 @@ async function packageWithContext(context,options={}){
920
980
  await copyBase();
921
981
  }
922
982
  let files=await listOutputFiles(stagingRoot,{signal});
983
+ const entryUrl=new URL(packageResourceUrl(entryPath),'http://arcane.invalid/');
984
+ const selectedFiles=new Set(files);
985
+ const explicitDocumentPaths=context.config.documents===undefined?null:new Set(
986
+ inspected.browserDocuments.map(function selectedDocumentPath(document){return document.packagePath;})
987
+ );
988
+ const pwaDocumentPaths=new Set(explicitDocumentPaths??[entryPath]);
989
+ for(const selected of explicitDocumentPaths===null?context.config.include:[]){
990
+ const selectedPath=appPackagePath(context,selected);
991
+ if(/\.html?$/iu.test(selected)&&selectedFiles.has(selectedPath))pwaDocumentPaths.add(selectedPath);
992
+ }
993
+ for(const document of inspected.browserDocuments){
994
+ const appDocument=context.config.include.some(function includesAppDocument(selected){
995
+ return sameOrDescendant(document.path,selected);
996
+ });
997
+ if(appDocument&&(appPath===''||document.managedMaps.length>0)&&selectedFiles.has(document.packagePath)){
998
+ pwaDocumentPaths.add(document.packagePath);
999
+ }
1000
+ }
1001
+ if (!pwaEnabled) {
1002
+ // Remove source-generated registration before native processing can follow it.
1003
+ const removals = await Promise.allSettled(
1004
+ [...pwaDocumentPaths].map(
1005
+ async function removeStagedPwaReferences(documentPath) {
1006
+ throwIfAborted(signal);
1007
+ const filePath = path.join(stagingRoot, ...documentPath.split('/'));
1008
+ const source = await readFile(filePath, 'utf8');
1009
+ const content = removePwaEntryReferences(
1010
+ source,
1011
+ {
1012
+ documentUrl: new URL(packageResourceUrl(documentPath), entryUrl.origin),
1013
+ manifestUrl: new URL(`/${PWA_MANIFEST_NAME}`, entryUrl.origin)
1014
+ }
1015
+ );
1016
+ if (content !== source) await writeFile(filePath, content, 'utf8');
1017
+ }
1018
+ )
1019
+ );
1020
+ // Settle every stage write before a failure can trigger stage cleanup.
1021
+ const errors = [];
1022
+ for (const removal of removals) {
1023
+ if (removal.status === 'rejected') errors.push(removal.reason);
1024
+ }
1025
+ if (errors.length === 1) throw errors[0];
1026
+ if (errors.length > 1) throw new AggregateError(errors, 'Selected PWA reference removal failed.');
1027
+ }
923
1028
  if(moduleFormat==='native'){
924
1029
  files=await materializeNativeModules({
925
1030
  stagingRoot,
926
1031
  files,
927
1032
  documents:inspected.browserDocuments.map(function documentPath(document){return document.packagePath;}),
928
- sharedFiles:records.filter(function sharedRecord(record){return record.appRelativePath===undefined;})
1033
+ sharedFiles:browserRecords.filter(function sharedRecord(record){return record.appRelativePath===undefined;})
929
1034
  .map(function sharedPath(record){return record.destination;}),
930
1035
  signal,
931
1036
  onEvent
@@ -937,27 +1042,10 @@ async function packageWithContext(context,options={}){
937
1042
  const inventory=new Set(files);
938
1043
  const offlineInventory=pwaEnabled?new Set(selectPwaFiles(files,context.config.pwa,appPath)):null;
939
1044
  const offlineReferences=new Set();
940
- const entryUrl=new URL(packageResourceUrl(entryPath),'http://arcane.invalid/');
941
1045
  const pwaDocumentSources=new Map();
942
- const explicitDocumentPaths=context.config.documents===undefined?null:new Set(
943
- inspected.browserDocuments.map(function selectedDocumentPath(document){return document.packagePath;})
944
- );
945
1046
  if(pwaEnabled){
946
- const documentPaths=new Set(explicitDocumentPaths??[entryPath]);
947
- for(const selected of explicitDocumentPaths===null?context.config.include:[]){
948
- const selectedPath=appPackagePath(context,selected);
949
- if(/\.html?$/iu.test(selected)&&inventory.has(selectedPath))documentPaths.add(selectedPath);
950
- }
951
- for(const document of inspected.browserDocuments){
952
- const appDocument=context.config.include.some(function includesAppDocument(selected){
953
- return sameOrDescendant(document.path,selected);
954
- });
955
- if(appDocument&&(appPath===''||document.managedMaps.length>0)&&inventory.has(document.packagePath)){
956
- documentPaths.add(document.packagePath);
957
- }
958
- }
959
1047
  await Promise.all(
960
- [...documentPaths].map(
1048
+ [...pwaDocumentPaths].map(
961
1049
  async function readPwaDocument(documentPath) {
962
1050
  const source = await readFile(
963
1051
  path.join(stagingRoot, ...documentPath.split('/')),
@@ -978,8 +1066,8 @@ async function packageWithContext(context,options={}){
978
1066
  const documentUrl=entryDocument?.bases[0]?.href
979
1067
  ?new URL(entryDocument.bases[0].href,entryUrl):entryUrl;
980
1068
  const pending=[{file:entryPath,documentUrl}];
981
- const sharedFiles=new Set(records.filter(record=>record.appRelativePath===undefined).map(record=>record.destination));
982
- const applicationFiles=new Set(records.filter(function applicationRecord(record){
1069
+ const sharedFiles=new Set(browserRecords.filter(record=>record.appRelativePath===undefined).map(record=>record.destination));
1070
+ const applicationFiles=new Set(browserRecords.filter(function applicationRecord(record){
983
1071
  return record.appRelativePath!==undefined;
984
1072
  }).map(function applicationDestination(record){return record.destination;}));
985
1073
  for(const file of files){
@@ -1103,6 +1191,17 @@ async function packageWithContext(context,options={}){
1103
1191
  }
1104
1192
  files.sort(compareText);
1105
1193
  }
1194
+ // Native resources bypass browser rewriting, module projection and PWA generation.
1195
+ // The app's browser selection remains authoritative wherever both select a file.
1196
+ const finalDestinations=new Set(files.map(pathKey));
1197
+ for(const record of nativeRecords){
1198
+ if(finalDestinations.has(pathKey(record.destination))){
1199
+ fail(`Native resource overlaps generated package content: ${record.destination}.`);
1200
+ }
1201
+ }
1202
+ await copyRecords(nativeRecords,stagingRoot,{signal,onEvent});
1203
+ files.push(...nativeRecords.map(record=>record.destination));
1204
+ files.sort(compareText);
1106
1205
  const manifest=releaseManifest(context,files,pwaArtifacts,rootDocument);
1107
1206
  await writeFile(
1108
1207
  path.join(stagingRoot,RELEASE_MANIFEST_NAME),
package/src/process.mjs CHANGED
@@ -33,8 +33,8 @@ function platformArguments(command,args,platform=process.platform){
33
33
  return [cliPath,...args];
34
34
  }
35
35
 
36
- function appendOutput(current,chunk){
37
- return current+chunk.toString('utf8');
36
+ function appendOutput(current,text){
37
+ return current+text;
38
38
  }
39
39
 
40
40
  async function emitLines(events,type,text){
@@ -45,9 +45,9 @@ async function emitLines(events,type,text){
45
45
  }
46
46
  }
47
47
 
48
- function deliverChunk(stream,events,type,chunk){
48
+ function deliverChunk(stream,events,type,text){
49
49
  stream.pause();
50
- return emitLines(events,type,chunk.toString('utf8')).catch(()=>{
50
+ return emitLines(events,type,text).catch(()=>{
51
51
  // The queue owns and propagates the first callback failure.
52
52
  }).finally(()=>{
53
53
  if(!stream.destroyed){
@@ -263,6 +263,10 @@ export async function runProcess(command,args=[],{
263
263
  return;
264
264
  }
265
265
 
266
+ // Each readable retains split UTF-8 sequences and flushes at EOF.
267
+ // Capture and events consume the same decoded text.
268
+ child.stdout.setEncoding('utf8');
269
+ child.stderr.setEncoding('utf8');
266
270
  child.stdout.on('data',chunk=>{
267
271
  stdout=appendOutput(stdout,chunk);
268
272
  ownDelivery(child.stdout,'process.stdout',chunk);
@@ -21,7 +21,7 @@ const DEFINITIONS=[
21
21
  id:'portable',displayName:'Portable native application',status:'pairing-required',
22
22
  platforms:['windows','linux'],architectures:['x64','arm64'],formats:['portable'],
23
23
  signingModes:['unsigned-local-test'],
24
- reason:'Portable output is available when paired to an Arcane OS native provider.'
24
+ reason:'Portable app payloads use the SDK package provider by default; explicit provider pairing remains available.'
25
25
  },
26
26
  {
27
27
  id:'windows-x64',displayName:'Windows x64 executable',status:'pairing-required',
@@ -226,6 +226,7 @@ export function createNativeTargetAdapter({targetId,nativeBuilder}={}){
226
226
  appReleaseRoot,
227
227
  release,
228
228
  appDescriptor,
229
+ selectedSdk,
229
230
  dependencyReleases,
230
231
  minimumCoreVersion,
231
232
  protectedRoots,
@@ -242,6 +243,7 @@ export function createNativeTargetAdapter({targetId,nativeBuilder}={}){
242
243
  appReleaseRoot,
243
244
  release,
244
245
  appDescriptor,
246
+ selectedSdk,
245
247
  dependencyReleases,
246
248
  minimumCoreVersion,
247
249
  protectedRoots,
package/src/toolchain.mjs CHANGED
@@ -836,8 +836,11 @@ function sameCanonicalPath(left,right){
836
836
  return normalize(left)===normalize(right);
837
837
  }
838
838
 
839
- export function assertIntegratedNativeToolchain({workspaceMode,workspaceRoot,toolchainRoot,target}={}){
839
+ export function assertIntegratedNativeToolchain({
840
+ workspaceMode,workspaceRoot,toolchainRoot,target,providerSource='arcane-checkout'
841
+ }={}){
840
842
  if(workspaceMode!=='integrated')return;
843
+ if(providerSource==='sdk-package'&&target==='portable')return;
841
844
  if(!is.string(toolchainRoot)||!sameCanonicalPath(workspaceRoot,toolchainRoot)){
842
845
  throw new ArcaneError(
843
846
  ERROR_CODES.policyDenied,
@@ -992,12 +995,15 @@ function pathsOverlap(left,right){
992
995
 
993
996
  async function packageNativeRelease(prepared,app,{
994
997
  workspaceOperationLease,
998
+ selectedSdk,
999
+ target,
995
1000
  signal,
996
1001
  onEvent
997
1002
  }={}){
998
1003
  const release=await packageApp({
999
1004
  workspaceRoot:prepared.workspaceRoot,
1000
1005
  appId:app.appId,
1006
+ target,
1001
1007
  browserPwa:false,
1002
1008
  workspaceOperationLease,
1003
1009
  signal,
@@ -1010,6 +1016,7 @@ async function packageNativeRelease(prepared,app,{
1010
1016
  release,
1011
1017
  nativeInput:{
1012
1018
  appDescriptor:app.descriptor,
1019
+ ...(selectedSdk===undefined?{}:{selectedSdk}),
1013
1020
  appReleaseRoot:release.outputRoot??path.resolve(prepared.workspaceRoot,release.output),
1014
1021
  release:{manifest:release.manifest,files:[...release.files]}
1015
1022
  }
@@ -1027,7 +1034,8 @@ async function executePairedNativeBuild(options,adapter,{
1027
1034
  target,
1028
1035
  workspaceMode:initialPrepared.workspaceMode,
1029
1036
  workspaceRoot:initialPrepared.workspaceRoot,
1030
- toolchainRoot:options.toolchainRoot
1037
+ toolchainRoot:options.toolchainRoot,
1038
+ providerSource:options.providerSource
1031
1039
  });
1032
1040
  if(options.dryRun){
1033
1041
  throw new ArcaneError(
@@ -1051,10 +1059,15 @@ async function executePairedNativeBuild(options,adapter,{
1051
1059
  }
1052
1060
  const dependencyApps=await nativeDependencyClosure(initialPrepared,{target,signal,onEvent});
1053
1061
  const prepared=initialPrepared;
1062
+ const installedSdk=await readInstalledSdkLayout(prepared.workspaceRoot,prepared.validation.config);
1063
+ const selectedSdk=installedSdk===null?undefined:{
1064
+ packageRoot:installedSdk.packageRoot,
1065
+ packageSource:installedSdk.packageSource
1066
+ };
1054
1067
  const packageOutputs=[];
1055
1068
  for(const app of [prepared.validation.app,...dependencyApps]){
1056
1069
  const releaseRoot=resolvePackageOutputRoot(
1057
- prepared.workspaceRoot,prepared.validation.config,app.manifest
1070
+ prepared.workspaceRoot,prepared.validation.config,app.manifest,{target}
1058
1071
  );
1059
1072
  if(pathsOverlap(releaseRoot,outputRoot)
1060
1073
  ||packageOutputs.some(existing=>pathsOverlap(existing,releaseRoot))){
@@ -1073,13 +1086,13 @@ async function executePairedNativeBuild(options,adapter,{
1073
1086
  descriptorSource:prepared.validation.app.descriptorSource,
1074
1087
  descriptorPath:prepared.validation.app.descriptorPath,
1075
1088
  validation:prepared.validation
1076
- },{workspaceOperationLease,signal,onEvent});
1089
+ },{workspaceOperationLease,selectedSdk,target,signal,onEvent});
1077
1090
  const dependencyReleases=[];
1078
1091
  for(const dependency of dependencyApps){
1079
1092
  dependencyReleases.push(await packageNativeRelease(
1080
1093
  prepared,
1081
1094
  dependency,
1082
- {workspaceOperationLease,signal,onEvent}
1095
+ {workspaceOperationLease,selectedSdk,target,signal,onEvent}
1083
1096
  ));
1084
1097
  }
1085
1098
  const toolchain=await adapter.prepare({
@@ -1094,7 +1107,11 @@ async function executePairedNativeBuild(options,adapter,{
1094
1107
  toolchainRoot:options.toolchainRoot,
1095
1108
  toolchain,
1096
1109
  ...selectedRelease.nativeInput,
1097
- dependencyReleases:dependencyReleases.map(item=>item.nativeInput),
1110
+ dependencyReleases:dependencyReleases.map(item=>({
1111
+ appId:item.appId,
1112
+ releaseRoot:item.nativeInput.appReleaseRoot,
1113
+ release:item.nativeInput.release
1114
+ })),
1098
1115
  minimumCoreVersion:ARCANE_MACHINE_BUNDLE_VERSION,
1099
1116
  protectedRoots,
1100
1117
  outputRoot,