arcane-os 0.55.0 → 0.57.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.
- package/CHANGELOG.md +51 -0
- package/README.md +5 -3
- package/browser-runtime/ai/core-local.mjs +306 -0
- package/browser-runtime/core/client.mjs +75 -14
- package/browser-runtime/core/contracts.mjs +2 -1
- package/docs/consumer-updates.md +82 -0
- package/docs/publishing.md +41 -18
- package/docs/reference/README.md +31 -1
- package/docs/reference/availability-and-normalization.md +11 -3
- package/docs/reference/cli.md +9 -1
- package/docs/reference/core-client.md +45 -2
- package/docs/reference/core-native-packaging.md +195 -11
- package/docs/reference/core-runtime.md +10 -0
- package/docs/reference/inventory/package-api.json +160 -20
- package/docs/reference/local-ai.md +197 -0
- package/docs/reference/protocols.md +91 -14
- package/docs/reference/pwa.md +17 -0
- package/docs/reference/runtime-modules.md +6 -5
- package/docs/reference/sdk-api.md +165 -26
- package/package.json +5 -1
- package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
- package/schemas/arcane-app.schema.json +64 -0
- package/schemas/arcane-package.schema.json +54 -0
- package/src/app-descriptor.mjs +35 -2
- package/src/app-layout.mjs +6 -2
- package/src/cli/main.mjs +8 -2
- package/src/core/host.mjs +23 -0
- package/src/core/hosts/arcane-core-loader.cjs +2 -0
- package/src/core/hosts/linux/arcane_core_process.c +568 -0
- package/src/core/hosts/linux/arcane_core_process.h +127 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
- package/src/core/hosts/sea-launcher.cjs +17 -0
- package/src/core/hosts/windows/ArcaneCoreProcess.cs +416 -0
- package/src/core/hosts/windows/ArcaneHost.cs +695 -0
- package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
- package/src/core/runtime.mjs +15 -3
- package/src/core/services/local-ai.mjs +543 -0
- package/src/core-classic-source.mjs +2 -2
- package/src/dev-server.mjs +7 -0
- package/src/import-map.mjs +1 -0
- package/src/local-ai/archive.mjs +52 -0
- package/src/local-ai/config.mjs +62 -0
- package/src/local-ai/dev.mjs +390 -0
- package/src/local-ai/discover.mjs +71 -0
- package/src/local-ai/http.mjs +106 -0
- package/src/local-ai/install.mjs +344 -0
- package/src/local-ai/server.mjs +262 -0
- package/src/native/portable-layout.mjs +29 -4
- package/src/native/portable-provider.mjs +39 -8
- package/src/native/windows-provider.mjs +262 -0
- package/src/native-provider-loader.mjs +8 -2
- package/src/packager/core.mjs +89 -15
- package/src/process.mjs +74 -12
- package/src/toolchain.mjs +38 -8
|
@@ -61,7 +61,7 @@ function serviceSelection(services, release) {
|
|
|
61
61
|
if (!is.string(service?.module) || !release.files.includes(service.module)) {
|
|
62
62
|
throw new ArcaneError(ERROR_CODES.prerequisiteMissing, `The selected release does not contain service module ${String(service?.module)}.`);
|
|
63
63
|
}
|
|
64
|
-
return {
|
|
64
|
+
return {...service, options: service.options === undefined ? {} : service.options};
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -81,7 +81,7 @@ function eventOwnerPath(release) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/** Package-owned assembly; never imports an OS checkout or application service. */
|
|
84
|
-
export function createPortableNativeProvider({services
|
|
84
|
+
export function createPortableNativeProvider({services} = {}) {
|
|
85
85
|
return {
|
|
86
86
|
protocol: PROTOCOL,
|
|
87
87
|
async describe() {
|
|
@@ -120,7 +120,8 @@ export function createPortableNativeProvider({services = []} = {}) {
|
|
|
120
120
|
const {appDescriptor, release, signal, onEvent} = request;
|
|
121
121
|
throwIfAborted(signal);
|
|
122
122
|
const target = portableTarget(request.targetRequest);
|
|
123
|
-
const
|
|
123
|
+
const localAI = appDescriptor.native?.localAI;
|
|
124
|
+
const selectedServices = serviceSelection(services ?? appDescriptor.native?.services ?? [], release);
|
|
124
125
|
const eventOwnerModuleURL = eventOwnerPath(release);
|
|
125
126
|
const output = await outputDirectory(request);
|
|
126
127
|
throwIfAborted(signal);
|
|
@@ -130,7 +131,33 @@ export function createPortableNativeProvider({services = []} = {}) {
|
|
|
130
131
|
const files = [];
|
|
131
132
|
try {
|
|
132
133
|
await emit(onEvent, {type: 'native.payload.started', appId: appDescriptor.id, outputRoot: root});
|
|
133
|
-
|
|
134
|
+
async function bundleSelectedRuntimes() {
|
|
135
|
+
if (localAI === undefined) return {runtimes: [], files: []};
|
|
136
|
+
const {bundleLocalAIRuntimes} = await import('../local-ai/install.mjs');
|
|
137
|
+
return bundleLocalAIRuntimes({
|
|
138
|
+
runtimes: localAI.runtimes,
|
|
139
|
+
directory: path.join(output, 'local-ai-runtimes'),
|
|
140
|
+
outputRoot: root,
|
|
141
|
+
platform: target.platform === 'windows' ? 'win32' : target.platform,
|
|
142
|
+
architecture: target.architecture,
|
|
143
|
+
signal,
|
|
144
|
+
onEvent
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// These outputs have separate directories. Join both owners
|
|
148
|
+
// before cleanup so neither can keep writing a removed assembly.
|
|
149
|
+
const preparation = await Promise.allSettled([
|
|
150
|
+
copyRelease({...request, releaseRoot: request.appReleaseRoot}, 'app', root, files, signal),
|
|
151
|
+
bundleSelectedRuntimes()
|
|
152
|
+
]);
|
|
153
|
+
const failures = preparation.filter(function failedPreparation(result) { return result.status === 'rejected'; });
|
|
154
|
+
if (failures.length === 1) throw failures[0].reason;
|
|
155
|
+
if (failures.length > 1) {
|
|
156
|
+
throw new AggregateError(failures.map(function preparationError(result) { return result.reason; }),
|
|
157
|
+
'Portable application and local runtime preparation failed.');
|
|
158
|
+
}
|
|
159
|
+
const bundled = preparation[1].value;
|
|
160
|
+
files.push(...bundled.files);
|
|
134
161
|
const dependencies = [];
|
|
135
162
|
for (const [index, dependency] of (request.dependencies ?? []).entries()) {
|
|
136
163
|
const directory = `dependencies/${index}`;
|
|
@@ -155,8 +182,9 @@ export function createPortableNativeProvider({services = []} = {}) {
|
|
|
155
182
|
appSdk = {name: metadata.name, version: metadata.version, root: destination};
|
|
156
183
|
}
|
|
157
184
|
await writeOutput(root, 'package.json', '{"type":"module"}\n', files, signal);
|
|
158
|
-
await writeOutput(root, 'runtime/arcane-core.mjs', coreEntrySource(appDescriptor, sdk.version, selectedServices
|
|
159
|
-
|
|
185
|
+
await writeOutput(root, 'runtime/arcane-core.mjs', coreEntrySource(appDescriptor, sdk.version, selectedServices,
|
|
186
|
+
{localAI, runtimes: bundled.runtimes}), files, signal);
|
|
187
|
+
await writeOutput(root, 'runtime/arcane-api.js', await createCoreClassicSource({eventOwnerModuleURL, replayRuntimeState: true}), files, signal);
|
|
160
188
|
const manifest = {
|
|
161
189
|
schemaVersion: 1,
|
|
162
190
|
kind: 'arcane-portable-native',
|
|
@@ -166,8 +194,11 @@ export function createPortableNativeProvider({services = []} = {}) {
|
|
|
166
194
|
target,
|
|
167
195
|
webRoot: 'app',
|
|
168
196
|
start: release.manifest?.app?.start ?? './index.html',
|
|
169
|
-
core: {
|
|
170
|
-
|
|
197
|
+
core: {
|
|
198
|
+
entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices,
|
|
199
|
+
...(localAI === undefined ? {} : {localAIRuntimes: bundled.runtimes})
|
|
200
|
+
},
|
|
201
|
+
client: {source: 'runtime/arcane-api.js', injection: 'document-created', eventOwnerModuleURL, replayRuntimeState: true},
|
|
171
202
|
dependencies,
|
|
172
203
|
minimumCoreVersion: request.minimumCoreVersion ?? null,
|
|
173
204
|
files: [...files, MANIFEST]
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {createWriteStream} from 'node:fs';
|
|
2
|
+
import {mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {Readable} from 'node:stream';
|
|
5
|
+
import {pipeline} from 'node:stream/promises';
|
|
6
|
+
import Is from 'strong-type';
|
|
7
|
+
import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
|
|
8
|
+
import {runProcess} from '../process.mjs';
|
|
9
|
+
import {createPortableNativeProvider} from './portable-provider.mjs';
|
|
10
|
+
import {writeOutput} from './portable-layout.mjs';
|
|
11
|
+
|
|
12
|
+
const is = new Is(false);
|
|
13
|
+
const PROTOCOL = 'arcane-native-builder/1';
|
|
14
|
+
const MANIFEST = 'arcane-native.json';
|
|
15
|
+
const HOST_ASSET = 'arcane-native-windows-x64.tar.gz';
|
|
16
|
+
const HOST_FILES = [
|
|
17
|
+
'Arcane.exe',
|
|
18
|
+
'Arcane.exe.config',
|
|
19
|
+
'Microsoft.Web.WebView2.Core.dll',
|
|
20
|
+
'Microsoft.Web.WebView2.WinForms.dll',
|
|
21
|
+
'WebView2Loader.dll',
|
|
22
|
+
'runtime/ArcaneCore.exe',
|
|
23
|
+
'runtime/arcane-core-loader.cjs',
|
|
24
|
+
'runtime/NODE-LICENSE',
|
|
25
|
+
'WEBVIEW2-LICENSE',
|
|
26
|
+
'LICENSE',
|
|
27
|
+
'COMMERCIAL-LICENSE.md',
|
|
28
|
+
'NOTICE'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function windowsTarget(value) {
|
|
32
|
+
if (value?.target !== 'windows-x64' || value.platform !== 'windows'
|
|
33
|
+
|| value.architecture !== 'x64' || value.format !== 'exe') {
|
|
34
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable, 'The Windows provider assembles windows-x64 executables.');
|
|
35
|
+
}
|
|
36
|
+
return {...value};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function portableRequest(target) {
|
|
40
|
+
return {...target, target: 'portable', format: 'portable'};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function emit(onEvent, event) {
|
|
44
|
+
if (is.function(onEvent)) await onEvent(event);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function hostFilesMissing(directory) {
|
|
48
|
+
const missing = [];
|
|
49
|
+
for (const relative of HOST_FILES) {
|
|
50
|
+
try {
|
|
51
|
+
if (!(await stat(path.join(directory, relative))).isFile()) missing.push(relative);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code !== 'ENOENT') throw error;
|
|
54
|
+
missing.push(relative);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return missing;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function requireHostDirectory(directory) {
|
|
61
|
+
const resolved = await realpath(directory);
|
|
62
|
+
const missing = await hostFilesMissing(resolved);
|
|
63
|
+
if (missing.length) {
|
|
64
|
+
throw new ArcaneError(ERROR_CODES.prerequisiteMissing, 'The selected Windows host assets are incomplete.', {
|
|
65
|
+
details: {hostDirectory: resolved, missing}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return resolved;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function removeOwnedDirectory(directory, error) {
|
|
72
|
+
try { await rm(directory, {recursive: true, force: true}); }
|
|
73
|
+
catch (cleanupError) {
|
|
74
|
+
throw new AggregateError([error, cleanupError], `Windows preparation failed; incomplete output remains at ${directory}.`);
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function releasedHostDirectory(outputRoot, version, {signal, onEvent}) {
|
|
80
|
+
const cache = path.join(outputRoot, '.arcane-native-hosts', version);
|
|
81
|
+
const destination = path.join(cache, 'windows-x64');
|
|
82
|
+
try { return await requireHostDirectory(destination); }
|
|
83
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
84
|
+
throwIfAborted(signal);
|
|
85
|
+
await mkdir(cache, {recursive: true});
|
|
86
|
+
const temporary = await mkdtemp(path.join(cache, 'preparing-windows-x64-'));
|
|
87
|
+
const archive = path.join(temporary, HOST_ASSET);
|
|
88
|
+
const extracted = path.join(temporary, 'host');
|
|
89
|
+
const url = `https://github.com/TheWizardNexus/arcane-os-sdk/releases/download/${version}/${HOST_ASSET}`;
|
|
90
|
+
try {
|
|
91
|
+
await emit(onEvent, {type: 'native.host.download.started', target: 'windows-x64', version, url});
|
|
92
|
+
throwIfAborted(signal);
|
|
93
|
+
const response = await fetch(url, {signal});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new ArcaneError(ERROR_CODES.prerequisiteMissing, `The SDK Windows host download returned HTTP ${response.status}.`, {
|
|
96
|
+
details: {url, status: response.status, response: await response.text()}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!response.body) throw new ArcaneError(ERROR_CODES.operationFailed, 'The SDK Windows host download returned no content.', {details: {url}});
|
|
100
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(archive, {flags: 'wx'}), {signal});
|
|
101
|
+
throwIfAborted(signal);
|
|
102
|
+
await mkdir(extracted);
|
|
103
|
+
await runProcess('tar', ['-xzf', archive, '-C', extracted], {cwd: temporary, signal, onEvent});
|
|
104
|
+
await requireHostDirectory(extracted);
|
|
105
|
+
throwIfAborted(signal);
|
|
106
|
+
try { await rename(extracted, destination); }
|
|
107
|
+
catch (error) {
|
|
108
|
+
// Another preparation may have finished this exact version while
|
|
109
|
+
// this download was in flight. Preserve its complete asset tree.
|
|
110
|
+
if (!['EEXIST', 'ENOTEMPTY', 'EPERM'].includes(error.code)) throw error;
|
|
111
|
+
try { await requireHostDirectory(destination); }
|
|
112
|
+
catch { throw error; }
|
|
113
|
+
}
|
|
114
|
+
await rm(temporary, {recursive: true, force: true});
|
|
115
|
+
await emit(onEvent, {type: 'native.host.download.completed', target: 'windows-x64', version, hostDirectory: destination});
|
|
116
|
+
throwIfAborted(signal);
|
|
117
|
+
return destination;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return removeOwnedDirectory(temporary, error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function contains(root, candidate) {
|
|
124
|
+
const relative = path.relative(root, candidate);
|
|
125
|
+
return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function copyHostAssets(source, root, files, signal, relative = '', ancestors = new Set()) {
|
|
129
|
+
throwIfAborted(signal);
|
|
130
|
+
const canonical = await realpath(source);
|
|
131
|
+
const output = await realpath(root);
|
|
132
|
+
if (contains(canonical, output) || contains(output, canonical) || ancestors.has(canonical)) {
|
|
133
|
+
throw new ArcaneError(ERROR_CODES.usage, 'Windows host assets overlap their output or contain a directory cycle.', {
|
|
134
|
+
details: {source, outputRoot: root}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const parents = new Set([...ancestors, canonical]);
|
|
138
|
+
for (const entry of await readdir(source, {withFileTypes: true})) {
|
|
139
|
+
throwIfAborted(signal);
|
|
140
|
+
const selected = path.join(source, entry.name);
|
|
141
|
+
const destination = relative ? `${relative}/${entry.name}` : entry.name;
|
|
142
|
+
const information = await stat(selected);
|
|
143
|
+
if (information.isDirectory()) {
|
|
144
|
+
await mkdir(path.join(root, destination), {recursive: true});
|
|
145
|
+
await copyHostAssets(selected, root, files, signal, destination, parents);
|
|
146
|
+
} else if (information.isFile()) {
|
|
147
|
+
await writeOutput(root, destination, await readFile(selected, {signal}), files, signal);
|
|
148
|
+
} else {
|
|
149
|
+
throw new ArcaneError(ERROR_CODES.operationFailed, `Cannot copy Windows host asset ${selected}.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Compose app-owned services with the SDK's precompiled Windows host. */
|
|
155
|
+
export function createWindowsNativeProvider({services, hostDirectory} = {}) {
|
|
156
|
+
const portable = createPortableNativeProvider(services === undefined ? {} : {services});
|
|
157
|
+
return {
|
|
158
|
+
protocol: PROTOCOL,
|
|
159
|
+
async describe() {
|
|
160
|
+
return {
|
|
161
|
+
protocol: PROTOCOL,
|
|
162
|
+
id: 'arcane-sdk-windows-x64',
|
|
163
|
+
targets: ['windows-x64'],
|
|
164
|
+
executable: true,
|
|
165
|
+
serviceConfiguration: 'explicit release module factories'
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
async doctor({targetRequest, signal, onEvent} = {}) {
|
|
169
|
+
throwIfAborted(signal);
|
|
170
|
+
const target = windowsTarget(targetRequest);
|
|
171
|
+
const core = await portable.doctor({targetRequest: portableRequest(target), signal});
|
|
172
|
+
const missing = hostDirectory === undefined ? [] : await hostFilesMissing(path.resolve(hostDirectory));
|
|
173
|
+
const result = {
|
|
174
|
+
target: 'windows-x64', executable: true, ready: core.ready && missing.length === 0,
|
|
175
|
+
sdkRoot: core.sdkRoot, missing: [...core.missing, ...missing],
|
|
176
|
+
hostDirectory: hostDirectory === undefined ? null : path.resolve(hostDirectory),
|
|
177
|
+
hostSource: hostDirectory === undefined ? 'sdk-release' : 'selected-directory',
|
|
178
|
+
downloadRequired: hostDirectory === undefined,
|
|
179
|
+
runtimePrerequisite: 'Microsoft Edge WebView2 Runtime',
|
|
180
|
+
buildPrerequisite: hostDirectory === undefined ? 'tar' : null
|
|
181
|
+
};
|
|
182
|
+
await emit(onEvent, {type: 'native.doctor.completed', ...result});
|
|
183
|
+
throwIfAborted(signal);
|
|
184
|
+
return result;
|
|
185
|
+
},
|
|
186
|
+
async prepare({targetRequest, signal, onEvent} = {}) {
|
|
187
|
+
throwIfAborted(signal);
|
|
188
|
+
const target = windowsTarget(targetRequest);
|
|
189
|
+
const selection = await portable.prepare({targetRequest: portableRequest(target), signal});
|
|
190
|
+
const selected = hostDirectory === undefined ? undefined : await requireHostDirectory(hostDirectory);
|
|
191
|
+
// Downloads belong to build(), which has the caller's selected
|
|
192
|
+
// outputRoot. Preparation never invents a machine-wide cache.
|
|
193
|
+
const result = {...selection, target, executable: true, ...(selected ? {hostDirectory: selected} : {})};
|
|
194
|
+
await emit(onEvent, {type: 'native.prepared', target: 'windows-x64', sdkVersion: selection.sdkVersion});
|
|
195
|
+
throwIfAborted(signal);
|
|
196
|
+
return result;
|
|
197
|
+
},
|
|
198
|
+
async build(request = {}) {
|
|
199
|
+
const {signal, onEvent} = request;
|
|
200
|
+
throwIfAborted(signal);
|
|
201
|
+
const target = windowsTarget(request.targetRequest);
|
|
202
|
+
const selected = hostDirectory ?? request.toolchain?.hostDirectory;
|
|
203
|
+
const selectedHost = selected === undefined ? null : await requireHostDirectory(selected);
|
|
204
|
+
const artifact = await portable.build({
|
|
205
|
+
...request,
|
|
206
|
+
targetRequest: portableRequest(target),
|
|
207
|
+
async onEvent(event) {
|
|
208
|
+
if (event.type !== 'native.payload.completed') await emit(onEvent, {...event, target: 'windows-x64'});
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
const root = artifact.target.rootDir;
|
|
212
|
+
try {
|
|
213
|
+
const host = selectedHost ?? await releasedHostDirectory(path.resolve(request.outputRoot), artifact.manifest.sdk.version, {signal, onEvent});
|
|
214
|
+
const files = [...artifact.manifest.files];
|
|
215
|
+
await copyHostAssets(host, root, files, signal);
|
|
216
|
+
const manifest = {
|
|
217
|
+
...artifact.manifest,
|
|
218
|
+
kind: 'arcane-windows-native',
|
|
219
|
+
target,
|
|
220
|
+
host: {
|
|
221
|
+
executable: 'Arcane.exe',
|
|
222
|
+
platform: 'windows', architecture: 'x64', runtime: 'webview2',
|
|
223
|
+
coreExecutable: 'runtime/ArcaneCore.exe',
|
|
224
|
+
coreLoader: 'runtime/arcane-core-loader.cjs'
|
|
225
|
+
},
|
|
226
|
+
files
|
|
227
|
+
};
|
|
228
|
+
await writeFile(path.join(root, MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`, {signal});
|
|
229
|
+
throwIfAborted(signal);
|
|
230
|
+
await emit(onEvent, {type: 'native.payload.completed', target: 'windows-x64', appId: request.appDescriptor.id, outputRoot: root});
|
|
231
|
+
throwIfAborted(signal);
|
|
232
|
+
return {...artifact, target: {...target, rootDir: root}, manifest};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return removeOwnedDirectory(root, error);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
async verify({artifact, targetRequest, signal, onEvent} = {}) {
|
|
238
|
+
throwIfAborted(signal);
|
|
239
|
+
const target = windowsTarget(targetRequest);
|
|
240
|
+
const result = await portable.verify({artifact, targetRequest: portableRequest(target), signal});
|
|
241
|
+
await requireHostDirectory(result.outputRoot);
|
|
242
|
+
const verified = {...result, target: 'windows-x64', executable: true};
|
|
243
|
+
await emit(onEvent, {type: 'native.verified', target: 'windows-x64', outputRoot: result.outputRoot});
|
|
244
|
+
throwIfAborted(signal);
|
|
245
|
+
return verified;
|
|
246
|
+
},
|
|
247
|
+
async run({artifact, targetRequest, signal, onEvent} = {}) {
|
|
248
|
+
throwIfAborted(signal);
|
|
249
|
+
windowsTarget(targetRequest);
|
|
250
|
+
if (process.platform !== 'win32' || process.arch !== 'x64') {
|
|
251
|
+
throw new ArcaneError(ERROR_CODES.nativeRunUnsupported, 'Running this Windows x64 artifact requires a Windows x64 host.');
|
|
252
|
+
}
|
|
253
|
+
const root = await requireHostDirectory(artifact?.target?.rootDir);
|
|
254
|
+
return runProcess(path.join(root, 'Arcane.exe'), ['--close-on-stdin-eof'], {
|
|
255
|
+
cwd: root, signal, onEvent, cancellationMode: 'close-input'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const arcaneNativeBuilderProvider = createWindowsNativeProvider();
|
|
262
|
+
export default arcaneNativeBuilderProvider;
|
|
@@ -22,6 +22,11 @@ const FIXED_NATIVE_PROVIDER_PATHS={
|
|
|
22
22
|
'android-arm64':providerPath('android-native-provider.mjs')
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
const SDK_NATIVE_PROVIDER_PATHS={
|
|
26
|
+
portable:'./native/portable-provider.mjs',
|
|
27
|
+
'windows-x64':'./native/windows-provider.mjs'
|
|
28
|
+
};
|
|
29
|
+
|
|
25
30
|
export const ARCANE_NATIVE_PROVIDER_PATHS={
|
|
26
31
|
portable:[...FIXED_NATIVE_PROVIDER_PATHS.portable],
|
|
27
32
|
'windows-x64':[...FIXED_NATIVE_PROVIDER_PATHS['windows-x64']],
|
|
@@ -133,10 +138,11 @@ export async function loadArcaneNativeProvider(options={}){
|
|
|
133
138
|
target,
|
|
134
139
|
message:`Loading the Arcane ${String(target)} provider.`
|
|
135
140
|
});
|
|
136
|
-
const
|
|
141
|
+
const sdkProvider=SDK_NATIVE_PROVIDER_PATHS[target];
|
|
142
|
+
const location=arcaneRoot===undefined&&sdkProvider
|
|
137
143
|
?{
|
|
138
144
|
canonicalRoot:path.resolve(fileURLToPath(new URL('../',import.meta.url))),
|
|
139
|
-
providerPath:fileURLToPath(new URL(
|
|
145
|
+
providerPath:fileURLToPath(new URL(sdkProvider,import.meta.url)),
|
|
140
146
|
providerSource:'sdk-package'
|
|
141
147
|
}
|
|
142
148
|
:await resolveProviderLocation({arcaneRoot,target,inspect,canonicalize});
|
package/src/packager/core.mjs
CHANGED
|
@@ -13,6 +13,8 @@ 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';
|
|
17
|
+
import {normalizeLocalAIConfig} from '../local-ai/config.mjs';
|
|
16
18
|
import {readInstalledSdkLayout} from '../sdk-runtime-layout.mjs';
|
|
17
19
|
import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
|
|
18
20
|
import {materializeNativeModules} from './native-modules.mjs';
|
|
@@ -320,7 +322,7 @@ function normalizeOptionalRecord(value,label){
|
|
|
320
322
|
export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.join(appRelativeRoot(rootConfig,appId),APP_CONFIG_NAME)){
|
|
321
323
|
assertOnlyKeys(value,new Set([
|
|
322
324
|
'schemaVersion','id','displayName','version','entry','strategy','security',
|
|
323
|
-
'localAIModelPolicy','include','exclude','shared','adapter','pwa','outputDirectory','documents'
|
|
325
|
+
'localAIModelPolicy','localAI','include','exclude','shared','adapter','pwa','outputDirectory','documents','nativeResources'
|
|
324
326
|
]),`${appId}/${APP_CONFIG_NAME}`);
|
|
325
327
|
if(value.schemaVersion!==1)fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
|
|
326
328
|
if(!is.string(value.id)||value.id!==appId||!APP_ID_PATTERN.test(value.id)){
|
|
@@ -335,6 +337,15 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
|
|
|
335
337
|
);
|
|
336
338
|
const include=validatePathList(value.include,`${appId}/${APP_CONFIG_NAME}.include`,{required:true});
|
|
337
339
|
const exclude=validatePathList(value.exclude??[],`${appId}/${APP_CONFIG_NAME}.exclude`);
|
|
340
|
+
let nativeResources;
|
|
341
|
+
if(value.nativeResources!==undefined){
|
|
342
|
+
const label=`${appId}/${APP_CONFIG_NAME}.nativeResources`;
|
|
343
|
+
assertOnlyKeys(value.nativeResources,new Set(['include','exclude']),label);
|
|
344
|
+
nativeResources={
|
|
345
|
+
include:validatePathList(value.nativeResources.include,`${label}.include`),
|
|
346
|
+
exclude:validatePathList(value.nativeResources.exclude??[],`${label}.exclude`)
|
|
347
|
+
};
|
|
348
|
+
}
|
|
338
349
|
if(include.some(allowed=>sameOrDescendant(APP_CONFIG_NAME,allowed))){
|
|
339
350
|
fail(`${appId}/${APP_CONFIG_NAME}.include must not expose the authored package configuration.`);
|
|
340
351
|
}
|
|
@@ -380,8 +391,10 @@ export function validateAppConfig(value,appId,rootConfig,configPath=path.posix.j
|
|
|
380
391
|
value.localAIModelPolicy,
|
|
381
392
|
`${appId}/${APP_CONFIG_NAME}.localAIModelPolicy`
|
|
382
393
|
)}),
|
|
394
|
+
...(value.localAI===undefined?{}:{localAI:normalizeLocalAIConfig(value.localAI)}),
|
|
383
395
|
include,
|
|
384
396
|
exclude,
|
|
397
|
+
...(nativeResources===undefined?{}:{nativeResources}),
|
|
385
398
|
shared:[...value.shared],
|
|
386
399
|
...(adapter===undefined?{}:{adapter}),
|
|
387
400
|
configPath
|
|
@@ -423,7 +436,8 @@ async function assertContainedRealPath(root,candidate,label){
|
|
|
423
436
|
}
|
|
424
437
|
}
|
|
425
438
|
|
|
426
|
-
async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory}={}){
|
|
439
|
+
async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory,target='browser'}={}){
|
|
440
|
+
if(!TARGET_IDS.includes(target))fail(`Unsupported package target: ${String(target)}.`);
|
|
427
441
|
const workspaceRoot=await realDirectory(normalizeWorkspaceRoot(requestedWorkspaceRoot),'Workspace root');
|
|
428
442
|
const rootConfigPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
|
|
429
443
|
const rootConfig=validateRootConfig(await readJson(rootConfigPath,ROOT_CONFIG_NAME),rootConfigPath);
|
|
@@ -436,14 +450,19 @@ async function loadContext(requestedWorkspaceRoot,appId,{outputDirectory}={}){
|
|
|
436
450
|
if(outputDirectory!==undefined){
|
|
437
451
|
config.outputDirectory=normalizeRelativePath(outputDirectory,'package outputDirectory');
|
|
438
452
|
}
|
|
453
|
+
const outputRoot=resolvePackageOutputRoot(workspaceRoot,rootConfig,config,{target,outputDirectory});
|
|
454
|
+
if(target!=='browser'&&config.nativeResources?.include.length>0){
|
|
455
|
+
config.outputDirectory=path.relative(workspaceRoot,outputRoot).split(path.sep).join('/');
|
|
456
|
+
}
|
|
439
457
|
return {
|
|
440
458
|
workspaceRoot,
|
|
441
459
|
rootConfig,
|
|
442
460
|
appsRoot,
|
|
443
461
|
appRoot,
|
|
444
462
|
appId,
|
|
463
|
+
target,
|
|
445
464
|
config,
|
|
446
|
-
outputRoot
|
|
465
|
+
outputRoot
|
|
447
466
|
};
|
|
448
467
|
}
|
|
449
468
|
|
|
@@ -494,6 +513,11 @@ async function assertPackageOutputLocation(context){
|
|
|
494
513
|
for(const selected of context.config.include){
|
|
495
514
|
assertSelectedInput(context.appRoot,selected,context.config.exclude,context.appId);
|
|
496
515
|
}
|
|
516
|
+
if(context.target!=='browser'&&context.config.nativeResources){
|
|
517
|
+
for(const selected of context.config.nativeResources.include){
|
|
518
|
+
assertSelectedInput(context.appRoot,selected,context.config.nativeResources.exclude,`${context.appId} native`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
497
521
|
for(const sharedId of context.config.shared){
|
|
498
522
|
for(const route of context.rootConfig.sharedPayloads[sharedId]){
|
|
499
523
|
const sourceRoot=path.resolve(context.workspaceRoot,route.source);
|
|
@@ -526,6 +550,7 @@ async function collectSelectedPath({
|
|
|
526
550
|
reject,
|
|
527
551
|
records,
|
|
528
552
|
destinations,
|
|
553
|
+
coalescedRecords,
|
|
529
554
|
signal,
|
|
530
555
|
label,
|
|
531
556
|
allowRoot=false
|
|
@@ -555,6 +580,7 @@ async function collectSelectedPath({
|
|
|
555
580
|
reject,
|
|
556
581
|
records,
|
|
557
582
|
destinations,
|
|
583
|
+
coalescedRecords,
|
|
558
584
|
signal,
|
|
559
585
|
label
|
|
560
586
|
});
|
|
@@ -567,9 +593,15 @@ async function collectSelectedPath({
|
|
|
567
593
|
fail(`${label} overlaps the generated release manifest.`);
|
|
568
594
|
}
|
|
569
595
|
const key=pathKey(normalizedDestination);
|
|
570
|
-
if(destinations.has(key))
|
|
596
|
+
if(destinations.has(key)){
|
|
597
|
+
const existing=coalescedRecords?.get(key);
|
|
598
|
+
if(existing?.source===absolute&&existing.destination===normalizedDestination)return;
|
|
599
|
+
fail(`Package destination collision: ${normalizedDestination}.`);
|
|
600
|
+
}
|
|
571
601
|
destinations.add(key);
|
|
572
|
-
|
|
602
|
+
const record={source:absolute,destination:normalizedDestination};
|
|
603
|
+
records.push(record);
|
|
604
|
+
coalescedRecords?.set(key,record);
|
|
573
605
|
}
|
|
574
606
|
|
|
575
607
|
async function collectPackageRecords(context,{signal}={}){
|
|
@@ -613,6 +645,29 @@ async function collectPackageRecords(context,{signal}={}){
|
|
|
613
645
|
}
|
|
614
646
|
}
|
|
615
647
|
}
|
|
648
|
+
if(context.target!=='browser'&&context.config.nativeResources){
|
|
649
|
+
const nativeRecords=[];
|
|
650
|
+
const coalescedRecords=new Map(records.map(record=>[pathKey(record.destination),record]));
|
|
651
|
+
for(const selected of context.config.nativeResources.include){
|
|
652
|
+
await collectSelectedPath({
|
|
653
|
+
sourceRoot:context.appRoot,
|
|
654
|
+
selected,
|
|
655
|
+
destination:appPackagePath(context,selected),
|
|
656
|
+
excludes:context.config.nativeResources.exclude,
|
|
657
|
+
reject:isAppSourceForbidden,
|
|
658
|
+
records:nativeRecords,
|
|
659
|
+
destinations,
|
|
660
|
+
coalescedRecords,
|
|
661
|
+
signal,
|
|
662
|
+
label:`${context.appId} nativeResources`
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
for(const record of nativeRecords){
|
|
666
|
+
record.appRelativePath=path.relative(context.appRoot,record.source).split(path.sep).join('/');
|
|
667
|
+
record.nativeOnly=true;
|
|
668
|
+
records.push(record);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
616
671
|
records.sort((left,right)=>compareText(left.destination,right.destination));
|
|
617
672
|
if(!records.some(record=>pathKey(record.destination)===pathKey(appPackagePath(context, context.config.entry)))){
|
|
618
673
|
fail(`Package entry is missing from the selected files: ${context.config.entry}.`);
|
|
@@ -671,7 +726,7 @@ async function optionalDescriptor(context){
|
|
|
671
726
|
|
|
672
727
|
async function inspectContext(context,{signal,records}={}){
|
|
673
728
|
const selectedRecords=records??await collectPackageRecords(context,{signal});
|
|
674
|
-
const documents=await browserDocuments(selectedRecords,context.config.entry,context.config.documents);
|
|
729
|
+
const documents=await browserDocuments(selectedRecords.filter(record=>!record.nativeOnly),context.config.entry,context.config.documents);
|
|
675
730
|
return {
|
|
676
731
|
appId:context.appId,
|
|
677
732
|
displayName:context.config.displayName,
|
|
@@ -683,11 +738,13 @@ async function inspectContext(context,{signal,records}={}){
|
|
|
683
738
|
...(context.config.pwa===undefined?{}:{pwa:copyJson(context.config.pwa)}),
|
|
684
739
|
include:[...context.config.include],
|
|
685
740
|
exclude:[...context.config.exclude],
|
|
741
|
+
...(context.config.nativeResources===undefined?{}:{nativeResources:copyJson(context.config.nativeResources)}),
|
|
686
742
|
shared:[...context.config.shared],
|
|
687
743
|
...(context.config.security===undefined?{}:{security:copyJson(context.config.security)}),
|
|
688
744
|
...(context.config.localAIModelPolicy===undefined?{}:{
|
|
689
745
|
localAIModelPolicy:copyJson(context.config.localAIModelPolicy)
|
|
690
746
|
}),
|
|
747
|
+
...(context.config.localAI===undefined?{}:{localAI:copyJson(context.config.localAI)}),
|
|
691
748
|
...(context.config.adapter===undefined?{}:{adapter:context.config.adapter}),
|
|
692
749
|
descriptor:await optionalDescriptor(context),
|
|
693
750
|
browserDocuments:documents,
|
|
@@ -724,9 +781,9 @@ export async function discoverApps({workspaceRoot:requestedWorkspaceRoot}={}){
|
|
|
724
781
|
return apps;
|
|
725
782
|
}
|
|
726
783
|
|
|
727
|
-
export async function inspectApp({workspaceRoot,appId,signal}={}){
|
|
784
|
+
export async function inspectApp({workspaceRoot,appId,signal,target,outputDirectory}={}){
|
|
728
785
|
throwIfAborted(signal);
|
|
729
|
-
const context=await loadContext(workspaceRoot,appId);
|
|
786
|
+
const context=await loadContext(workspaceRoot,appId,{target,outputDirectory});
|
|
730
787
|
return inspectContext(context,{signal});
|
|
731
788
|
}
|
|
732
789
|
|
|
@@ -817,7 +874,8 @@ function releaseManifest(context,files,pwaArtifacts,rootDocument){
|
|
|
817
874
|
...(context.config.security===undefined?{}:{security:copyJson(context.config.security)}),
|
|
818
875
|
...(context.config.localAIModelPolicy===undefined?{}:{
|
|
819
876
|
localAIModelPolicy:copyJson(context.config.localAIModelPolicy)
|
|
820
|
-
})
|
|
877
|
+
}),
|
|
878
|
+
...(context.config.localAI===undefined?{}:{localAI:copyJson(context.config.localAI)})
|
|
821
879
|
},
|
|
822
880
|
files:[...files]
|
|
823
881
|
};
|
|
@@ -846,7 +904,8 @@ async function replaceDirectory(stagingRoot,outputRoot){
|
|
|
846
904
|
}
|
|
847
905
|
|
|
848
906
|
async function packageWithContext(context,options={}){
|
|
849
|
-
const {signal,onEvent,
|
|
907
|
+
const {signal,onEvent,moduleFormat='import-map',browserPwa:requestedBrowserPwa=true}=options;
|
|
908
|
+
const browserPwa=context.target==='browser'&&requestedBrowserPwa;
|
|
850
909
|
if(!['import-map','native'].includes(moduleFormat)){
|
|
851
910
|
throw new TypeError('packageApp moduleFormat must be "import-map" or "native".');
|
|
852
911
|
}
|
|
@@ -854,7 +913,9 @@ async function packageWithContext(context,options={}){
|
|
|
854
913
|
const appPath=appRelativeRoot(context.rootConfig,context.appId);
|
|
855
914
|
const entryPath=appPackagePath(context,context.config.entry);
|
|
856
915
|
const records=await collectPackageRecords(context,{signal});
|
|
857
|
-
const
|
|
916
|
+
const browserRecords=records.filter(record=>!record.nativeOnly);
|
|
917
|
+
const nativeRecords=records.filter(record=>record.nativeOnly);
|
|
918
|
+
const rootDocument = browserRecords.some(
|
|
858
919
|
function selectedRootIndex(record) {
|
|
859
920
|
return record.destination === 'index.html';
|
|
860
921
|
}
|
|
@@ -901,7 +962,7 @@ async function packageWithContext(context,options={}){
|
|
|
901
962
|
let promoted=false;
|
|
902
963
|
try{
|
|
903
964
|
async function copyBase() {
|
|
904
|
-
await copyRecords(
|
|
965
|
+
await copyRecords(browserRecords,stagingRoot,{signal,onEvent});
|
|
905
966
|
if (!rootDocument) {
|
|
906
967
|
await writePackageLauncher(context, stagingRoot);
|
|
907
968
|
}
|
|
@@ -913,6 +974,8 @@ async function packageWithContext(context,options={}){
|
|
|
913
974
|
workspaceRoot:context.workspaceRoot,
|
|
914
975
|
appRoot:context.appRoot,
|
|
915
976
|
outputRoot:stagingRoot,
|
|
977
|
+
target:context.target,
|
|
978
|
+
browserPwa,
|
|
916
979
|
copyBase,
|
|
917
980
|
signal,
|
|
918
981
|
onEvent
|
|
@@ -971,7 +1034,7 @@ async function packageWithContext(context,options={}){
|
|
|
971
1034
|
stagingRoot,
|
|
972
1035
|
files,
|
|
973
1036
|
documents:inspected.browserDocuments.map(function documentPath(document){return document.packagePath;}),
|
|
974
|
-
sharedFiles:
|
|
1037
|
+
sharedFiles:browserRecords.filter(function sharedRecord(record){return record.appRelativePath===undefined;})
|
|
975
1038
|
.map(function sharedPath(record){return record.destination;}),
|
|
976
1039
|
signal,
|
|
977
1040
|
onEvent
|
|
@@ -1007,8 +1070,8 @@ async function packageWithContext(context,options={}){
|
|
|
1007
1070
|
const documentUrl=entryDocument?.bases[0]?.href
|
|
1008
1071
|
?new URL(entryDocument.bases[0].href,entryUrl):entryUrl;
|
|
1009
1072
|
const pending=[{file:entryPath,documentUrl}];
|
|
1010
|
-
const sharedFiles=new Set(
|
|
1011
|
-
const applicationFiles=new Set(
|
|
1073
|
+
const sharedFiles=new Set(browserRecords.filter(record=>record.appRelativePath===undefined).map(record=>record.destination));
|
|
1074
|
+
const applicationFiles=new Set(browserRecords.filter(function applicationRecord(record){
|
|
1012
1075
|
return record.appRelativePath!==undefined;
|
|
1013
1076
|
}).map(function applicationDestination(record){return record.destination;}));
|
|
1014
1077
|
for(const file of files){
|
|
@@ -1132,6 +1195,17 @@ async function packageWithContext(context,options={}){
|
|
|
1132
1195
|
}
|
|
1133
1196
|
files.sort(compareText);
|
|
1134
1197
|
}
|
|
1198
|
+
// Native resources bypass browser rewriting, module projection and PWA generation.
|
|
1199
|
+
// The app's browser selection remains authoritative wherever both select a file.
|
|
1200
|
+
const finalDestinations=new Set(files.map(pathKey));
|
|
1201
|
+
for(const record of nativeRecords){
|
|
1202
|
+
if(finalDestinations.has(pathKey(record.destination))){
|
|
1203
|
+
fail(`Native resource overlaps generated package content: ${record.destination}.`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
await copyRecords(nativeRecords,stagingRoot,{signal,onEvent});
|
|
1207
|
+
files.push(...nativeRecords.map(record=>record.destination));
|
|
1208
|
+
files.sort(compareText);
|
|
1135
1209
|
const manifest=releaseManifest(context,files,pwaArtifacts,rootDocument);
|
|
1136
1210
|
await writeFile(
|
|
1137
1211
|
path.join(stagingRoot,RELEASE_MANIFEST_NAME),
|