crossbind 2.0.0-beta.62 → 2.0.0-beta.64
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/AGENTS.md +2 -1
- package/README.md +2 -2
- package/package.json +7 -2
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildDependencies.js +15 -2
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildJs.js +2 -9
- package/src/actions/buildLib.js +25 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +49 -10
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +10 -9
- package/src/actions/buildWasm.js +72 -31
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +71 -0
- package/src/actions/createInterface.js +247 -68
- package/src/actions/createLib.js +70 -42
- package/src/actions/getData.js +6 -2
- package/src/actions/getEnv.js +7 -0
- package/src/actions/getLinkInputs.js +12 -5
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +143 -19
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +63 -66
- package/src/actions/runnerCommands.js +433 -0
- package/src/actions/writeRuntimeEntry.js +80 -0
- package/src/assets/cmake/command/CMakeLists.txt +21 -0
- package/src/assets/cmake/command/command.cpp +1 -0
- package/src/assets/cmake/dist.cmake +2 -1
- package/src/assets/configure/install.sh +22 -0
- package/src/assets/cpp-runtime/node.cpp +18 -1
- package/src/assets/js-runtime/adapters/path-url.js +6 -3
- package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +138 -60
- package/src/assets/js-runtime/core.js +12 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +19 -19
- package/src/assets/wasi-runtime/http/README.md +17 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
- package/src/assets/wasi-runtime/http/handler.cpp +298 -0
- package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
- package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
- package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
- package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
- package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
- package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
- package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +218 -161
- package/src/cli.js +14 -0
- package/src/index.js +3 -0
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +50 -13
- package/src/integration/getDependFilePath.js +9 -0
- package/src/integration/resolveNativeImport.js +14 -0
- package/src/node/dev.mjs +19 -0
- package/src/program.js +169 -0
- package/src/runner/blobs.js +135 -0
- package/src/runner/files.js +193 -0
- package/src/runner/server.js +320 -0
- package/src/state/attachConanDependencies.js +56 -0
- package/src/state/index.js +20 -11
- package/src/state/loadConfig.js +37 -35
- package/src/state/refreshBuiltDependencies.js +20 -0
- package/src/state/refreshConanDependencies.js +17 -0
- package/src/utils/androidToolchain.js +4 -0
- package/src/utils/appRustCrates.js +19 -1
- package/src/utils/appSources.js +97 -6
- package/src/utils/archiveFlags.js +12 -0
- package/src/utils/boundHeaders.js +79 -0
- package/src/utils/bridgeExtras.js +554 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -0
- package/src/utils/cargoLock.js +19 -0
- package/src/utils/cargoTarget.js +21 -3
- package/src/utils/cloudAccount.js +55 -0
- package/src/utils/conanDependencies.js +66 -0
- package/src/utils/conanImport.js +58 -0
- package/src/utils/conanInstall.js +154 -0
- package/src/utils/conanProfile.js +203 -0
- package/src/utils/conanStage.js +292 -0
- package/src/utils/copyToDistSource.js +13 -0
- package/src/utils/cppDts.js +60 -15
- package/src/utils/darwinToolchain.js +11 -0
- package/src/utils/dirLock.js +137 -28
- package/src/utils/downloadAndExtractFile.js +34 -7
- package/src/utils/embindArgumentGuards.js +41 -2
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/embindObjectLifetime.js +35 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +16 -5
- package/src/utils/hostPlatform.js +21 -0
- package/src/utils/iosToolchain.js +6 -0
- package/src/utils/isSourceCmakePackage.js +10 -0
- package/src/utils/licenseReport.js +11 -2
- package/src/utils/linuxToolchain.js +24 -0
- package/src/utils/makeTreeWritable.js +12 -0
- package/src/utils/nativeImports.js +23 -0
- package/src/utils/nativeSourceStamp.js +36 -0
- package/src/utils/nodeDevBuild.js +92 -0
- package/src/utils/packageLicense.js +103 -0
- package/src/utils/pthreadMainScript.js +14 -0
- package/src/utils/pullDockerImage.js +3 -1
- package/src/utils/relocatePrebuilt.js +119 -0
- package/src/utils/remoteClient.js +127 -0
- package/src/utils/remoteOutputs.js +39 -0
- package/src/utils/remoteRunner.js +97 -0
- package/src/utils/remoteUpload.js +134 -0
- package/src/utils/replaceFile.js +8 -0
- package/src/utils/resetCMakeCacheForSource.js +26 -0
- package/src/utils/runCargo.js +48 -51
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +22 -8
- package/src/utils/rustMt.js +9 -0
- package/src/utils/rustSysroot.js +7 -5
- package/src/utils/scopedEmbind.js +22 -0
- package/src/utils/selectRunner.js +81 -0
- package/src/utils/sourceFingerprint.js +4 -3
- package/src/utils/swigInterface.js +70 -4
- package/src/utils/systemConfig.js +13 -0
- package/src/utils/systemKeys.js +35 -3
- package/src/utils/targets.js +60 -4
- package/src/utils/toolchainNotices.js +5 -0
- package/src/utils/wasiHttp.js +50 -0
- package/src/utils/wasiToolchain.js +14 -1
- package/src/utils/windowsToolchain.js +18 -0
- package/vitest.config.js +11 -0
package/src/actions/buildWasm.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
|
|
2
3
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import replace from 'replace';
|
|
4
4
|
import run from './run.js';
|
|
5
5
|
import getLinkInputs from './getLinkInputs.js';
|
|
6
6
|
import getData from './getData.js';
|
|
7
|
+
import getEnv from './getEnv.js';
|
|
7
8
|
import buildJs from './buildJs.js';
|
|
8
9
|
import triggerExtensions from './extensions.js';
|
|
9
10
|
import state from '../state/index.js';
|
|
@@ -12,6 +13,13 @@ import logger from '../utils/logger.js';
|
|
|
12
13
|
import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
|
|
13
14
|
import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
14
15
|
import { guardBigIntArguments, guardEmbindArguments } from '../utils/embindArgumentGuards.js';
|
|
16
|
+
import { separateCallArguments } from '../utils/embindCallArguments.js';
|
|
17
|
+
import { queueJspiCalls } from '../utils/embindJspiQueue.js';
|
|
18
|
+
import { releaseOwnedHandles } from '../utils/embindObjectLifetime.js';
|
|
19
|
+
import { spawnPthreadsFromMainScript } from '../utils/pthreadMainScript.js';
|
|
20
|
+
|
|
21
|
+
const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/embindObjectLifetime.js', '../utils/pthreadMainScript.js']
|
|
22
|
+
.map((file) => fileURLToPath(new URL(file, import.meta.url)));
|
|
15
23
|
|
|
16
24
|
// embind's bigint converter turns any Number into a BigInt, so 2^53+1 silently becomes 2^53.
|
|
17
25
|
// A 64-bit parameter takes a BigInt or a Number that is a safe integer instead; the jsi adapter
|
|
@@ -36,15 +44,52 @@ function guardArgumentConversions(target) {
|
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
// Overlapping calls of one embind function free each other's arguments (utils/embindCallArguments.js);
|
|
48
|
+
// each runtimeEnv links its own glue, so each one calls this after its link.
|
|
49
|
+
function separateEmbindCalls(target) {
|
|
50
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
51
|
+
const { text, missed } = separateCallArguments(fs.readFileSync(gluePath, 'utf8'));
|
|
52
|
+
fs.writeFileSync(gluePath, text);
|
|
53
|
+
if (missed) {
|
|
54
|
+
logger.error('embind per-call argument rewrite missed (emscripten glue format changed?): overlapping calls of one function free each other\'s arguments');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Suspended _JSPI calls that resume out of order overwrite each other's C stack (utils/embindJspiQueue.js);
|
|
59
|
+
// each runtimeEnv links its own glue, so each one calls this after its link.
|
|
60
|
+
function queueJspiEmbindCalls(target) {
|
|
61
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
62
|
+
const { text, missed } = queueJspiCalls(fs.readFileSync(gluePath, 'utf8'));
|
|
63
|
+
fs.writeFileSync(gluePath, text);
|
|
64
|
+
if (missed) {
|
|
65
|
+
logger.error('embind _JSPI queue rewrite missed (emscripten glue format changed?): suspended _JSPI calls can overwrite each other\'s C stack');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Objects JavaScript owns outside a smart pointer were freed only by .delete() (utils/embindObjectLifetime.js);
|
|
70
|
+
// each runtimeEnv links its own glue, so each one calls this after its link.
|
|
71
|
+
function releaseOwnedObjects(target) {
|
|
72
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
73
|
+
const { text, missed } = releaseOwnedHandles(fs.readFileSync(gluePath, 'utf8'));
|
|
74
|
+
fs.writeFileSync(gluePath, text);
|
|
75
|
+
if (missed) {
|
|
76
|
+
logger.error('embind object lifetime rewrite missed (emscripten glue format changed?): returned values and constructed vectors are freed only by .delete()');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// A direct-mode mt init in a browser spawns its pthreads from "/undefined" (utils/pthreadMainScript.js).
|
|
81
|
+
function pointPthreadSpawn(target) {
|
|
82
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
83
|
+
const { text, missed } = spawnPthreadsFromMainScript(fs.readFileSync(gluePath, 'utf8'));
|
|
84
|
+
fs.writeFileSync(gluePath, text);
|
|
85
|
+
if (missed) {
|
|
86
|
+
logger.error('pthread spawn rewrite missed (emscripten glue format changed?): mt builds may fetch /undefined workers');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
39
90
|
export default async function buildWasm(target, options = {}) {
|
|
40
91
|
const isProd = target.buildType === 'release';
|
|
41
92
|
|
|
42
|
-
// Caller can opt out of the final emcc link entirely (e.g. when the
|
|
43
|
-
// package is consumed only as a static library by downstream builds).
|
|
44
|
-
if (state.config.export.bundle === false) {
|
|
45
|
-
logger.info(`[${target.path}] wasm+js skipped (export.bundle = false)`);
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
93
|
// Cargo packages only stage their staticlib; the consuming app links and bundles it.
|
|
49
94
|
if (state.config.export.type === 'cargo') {
|
|
50
95
|
logger.info(`[${target.path}] wasm+js skipped (cargo package - staticlib only)`);
|
|
@@ -131,7 +176,8 @@ export default async function buildWasm(target, options = {}) {
|
|
|
131
176
|
// The emcc arg tail is hardcoded per environment in this file, invisible to the
|
|
132
177
|
// emccFlags entry below - hashing the builder itself makes any inline-arg edit
|
|
133
178
|
// (a new -s flag, an EXPORTED_RUNTIME_METHODS change) a guaranteed cache miss.
|
|
134
|
-
builder
|
|
179
|
+
// The glue rewrites applied after the link count as the builder too.
|
|
180
|
+
builder: getFilesFingerprint([fileURLToPath(import.meta.url), ...GLUE_REWRITES]),
|
|
135
181
|
wholeArchiveAll,
|
|
136
182
|
wholeArchiveNames: [...wholeArchiveNames].sort(),
|
|
137
183
|
emccFlags,
|
|
@@ -140,6 +186,8 @@ export default async function buildWasm(target, options = {}) {
|
|
|
140
186
|
return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
|
|
141
187
|
}),
|
|
142
188
|
data: getData('data', target),
|
|
189
|
+
// The JS the link step writes carries the env.
|
|
190
|
+
env: getEnv(target),
|
|
143
191
|
runtime: getFilesFingerprint(runtimeAssets),
|
|
144
192
|
rustSources: getFilesFingerprint(rustSources),
|
|
145
193
|
}));
|
|
@@ -188,27 +236,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
188
236
|
const t1 = performance.now();
|
|
189
237
|
logger.doneStep(target, 'wasm');
|
|
190
238
|
logger.startStep(target, 'js');
|
|
191
|
-
|
|
192
|
-
// document.currentScript at LOAD time - undefined when the bridge is
|
|
193
|
-
// loaded as a module script, so workers would fetch "/undefined" and
|
|
194
|
-
// direct-mode mt init hangs. Inject Module.crossbindMainScript (set by the
|
|
195
|
-
// browser adapter from paths.worker/paths.js; a crossbind-specific key, as
|
|
196
|
-
// emscripten's own mainScriptUrlOrBlob trips emsdk 6 debug assertions
|
|
197
|
-
// on st targets) at SPAWN time instead. The previous spaced
|
|
198
|
-
// 'var _scriptName = ' rewrite no longer matched the minified glue and
|
|
199
|
-
// silently no-opped; the guard below turns any future format drift
|
|
200
|
-
// into a visible error instead.
|
|
201
|
-
replace({
|
|
202
|
-
regex: 'pthreadMainJs=_scriptName',
|
|
203
|
-
replacement: 'pthreadMainJs=Module["crossbindMainScript"]||_scriptName',
|
|
204
|
-
paths: [`${state.config.paths.build}/${target.rawJsName}`],
|
|
205
|
-
recursive: false,
|
|
206
|
-
silent: true,
|
|
207
|
-
});
|
|
208
|
-
const glue = fs.readFileSync(`${state.config.paths.build}/${target.rawJsName}`, 'utf8');
|
|
209
|
-
if (glue.includes('pthreadMainJs') && !glue.includes('Module["crossbindMainScript"]||_scriptName')) {
|
|
210
|
-
logger.error('pthread spawn rewrite missed (emscripten glue format changed?): mt builds may fetch /undefined workers');
|
|
211
|
-
}
|
|
239
|
+
pointPthreadSpawn(target);
|
|
212
240
|
/* replace({
|
|
213
241
|
regex: 'val === 10',
|
|
214
242
|
replacement: 'false',
|
|
@@ -218,6 +246,9 @@ export default async function buildWasm(target, options = {}) {
|
|
|
218
246
|
}); */
|
|
219
247
|
guardBigIntConversions(target);
|
|
220
248
|
guardArgumentConversions(target);
|
|
249
|
+
separateEmbindCalls(target);
|
|
250
|
+
queueJspiEmbindCalls(target);
|
|
251
|
+
releaseOwnedObjects(target);
|
|
221
252
|
await buildJs(target);
|
|
222
253
|
// fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
|
|
223
254
|
// fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
|
|
@@ -242,12 +273,13 @@ export default async function buildWasm(target, options = {}) {
|
|
|
242
273
|
// it any linked Rust package archive fails with undefined crossbind_* symbols.
|
|
243
274
|
...linkLibs, ...rustSources,
|
|
244
275
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
245
|
-
'-s', 'RESERVED_FUNCTION_POINTERS=200',
|
|
276
|
+
'-s', 'RESERVED_FUNCTION_POINTERS=200',
|
|
246
277
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
247
278
|
// See the GROWABLE_ARRAYBUFFERS note in the browser block (Firefox/WebKit TextDecoder).
|
|
248
279
|
'-s', 'GROWABLE_ARRAYBUFFERS=0',
|
|
280
|
+
'-s', 'FORCE_FILESYSTEM=1',
|
|
249
281
|
'-s', 'ENVIRONMENT=web',
|
|
250
|
-
'-s', 'EXPORTED_RUNTIME_METHODS=["ENV", "getExceptionMessage"]',
|
|
282
|
+
'-s', 'EXPORTED_RUNTIME_METHODS=["FS", "ENV", "getExceptionMessage"]',
|
|
251
283
|
'-fwasm-exceptions',
|
|
252
284
|
'-sEXPORT_EXCEPTION_HANDLING_HELPERS',
|
|
253
285
|
'-o', `${state.config.paths.build}/${target.rawJsName}`,
|
|
@@ -258,6 +290,9 @@ export default async function buildWasm(target, options = {}) {
|
|
|
258
290
|
logger.startStep(target, 'js');
|
|
259
291
|
guardBigIntConversions(target);
|
|
260
292
|
guardArgumentConversions(target);
|
|
293
|
+
separateEmbindCalls(target);
|
|
294
|
+
queueJspiEmbindCalls(target);
|
|
295
|
+
releaseOwnedObjects(target);
|
|
261
296
|
await buildJs(target);
|
|
262
297
|
logger.doneStep(target, 'js');
|
|
263
298
|
}
|
|
@@ -275,6 +310,8 @@ export default async function buildWasm(target, options = {}) {
|
|
|
275
310
|
// '-s', 'FETCH', '-sJSPI', '-pthread', '-sPTHREAD_POOL_SIZE=5',
|
|
276
311
|
'-s', 'FORCE_FILESYSTEM=1',
|
|
277
312
|
...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/node.cpp`,
|
|
313
|
+
// node.cpp answers fsync and fdatasync for host files.
|
|
314
|
+
'-Wl,--wrap=__wasi_fd_sync', '-Wl,--wrap=__syscall_fdatasync',
|
|
278
315
|
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
279
316
|
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'DISABLE_EXCEPTION_CATCHING=0', '-s', 'FORCE_FILESYSTEM=1',
|
|
280
317
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
@@ -292,6 +329,9 @@ export default async function buildWasm(target, options = {}) {
|
|
|
292
329
|
logger.startStep(target, 'js');
|
|
293
330
|
guardBigIntConversions(target);
|
|
294
331
|
guardArgumentConversions(target);
|
|
332
|
+
separateEmbindCalls(target);
|
|
333
|
+
queueJspiEmbindCalls(target);
|
|
334
|
+
releaseOwnedObjects(target);
|
|
295
335
|
await buildJs(target);
|
|
296
336
|
if (emccFlags.includes('FETCH')) {
|
|
297
337
|
fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
|
|
@@ -315,5 +355,6 @@ export default async function buildWasm(target, options = {}) {
|
|
|
315
355
|
}
|
|
316
356
|
|
|
317
357
|
fs.writeFileSync(linkFingerprintFile, linkFingerprint);
|
|
358
|
+
writeNativeSourceStamp(`${state.config.paths.build}/${target.jsName}`, state.config.paths.native);
|
|
318
359
|
return true;
|
|
319
360
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CREDENTIALS_FILE, cloudCredentials, readCredentials, writeCredentials,
|
|
3
|
+
} from '../utils/cloudAccount.js';
|
|
4
|
+
|
|
5
|
+
// `crossbind login`, `logout` and `usage`, against tooling/cloud/worker/account-api.js.
|
|
6
|
+
|
|
7
|
+
const GITHUB_ORIGIN = 'https://github.com';
|
|
8
|
+
const TOKEN_PATTERN = /^cbt_[A-Za-z0-9_-]{43}$/;
|
|
9
|
+
const MAX_NAME_LENGTH = 64;
|
|
10
|
+
// GitHub's defaults for a device code; it asks for no faster polling than this.
|
|
11
|
+
const MIN_INTERVAL_SECONDS = 5;
|
|
12
|
+
const DEFAULT_EXPIRES_SECONDS = 900;
|
|
13
|
+
|
|
14
|
+
const wait = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
15
|
+
// What the cloud sends ends up on the terminal: no escape sequences.
|
|
16
|
+
const printable = (value) => String(value ?? '').replace(/\p{Cc}/gu, '');
|
|
17
|
+
const paceOf = (seconds, fallback) => Math.max(MIN_INTERVAL_SECONDS, Number.isFinite(seconds) ? seconds : fallback);
|
|
18
|
+
|
|
19
|
+
async function call(fetcher, url, route, { method = 'GET', token, body } = {}) {
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
// A redirect would carry the token to an address nobody configured.
|
|
23
|
+
response = await fetcher(`${url}${route}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), ...(body ? { 'content-type': 'application/json' } : {}) },
|
|
26
|
+
body: body && JSON.stringify(body),
|
|
27
|
+
redirect: 'error',
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`crossbind: crossbind cloud at ${url} is unreachable (${error.cause?.code ?? error.message}).`, { cause: error });
|
|
31
|
+
}
|
|
32
|
+
const answer = await response.json().catch(() => null);
|
|
33
|
+
return { status: response.status, ok: response.ok, body: answer !== null && typeof answer === 'object' ? answer : {} };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const refused = ({ status, body }) => new Error(`crossbind: ${printable(body.error ?? `crossbind cloud answered ${status}.`)}`);
|
|
37
|
+
|
|
38
|
+
// { revoked: true }, or { revoked: false, reason }.
|
|
39
|
+
async function revoke(fetcher, url, token, all) {
|
|
40
|
+
try {
|
|
41
|
+
const answer = await call(fetcher, url, all ? '/v1/tokens' : '/v1/token', { method: 'DELETE', token });
|
|
42
|
+
// 401: the cloud no longer knows this token, so there is nothing left to revoke, unless every token was asked for.
|
|
43
|
+
return answer.ok || (!all && answer.status === 401) ? { revoked: true } : { revoked: false, reason: refused(answer).message };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return { revoked: false, reason: error.message };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// GitHub's device flow: the user approves a code on github.com while this polls crossbind cloud, which asks GitHub.
|
|
50
|
+
export async function login({
|
|
51
|
+
url, file = CREDENTIALS_FILE, fetcher = fetch, sleep = wait, now = Date.now, print = console.log,
|
|
52
|
+
}) {
|
|
53
|
+
const device = await call(fetcher, url, '/v1/login/device', { method: 'POST' });
|
|
54
|
+
if (!device.ok) throw refused(device);
|
|
55
|
+
const { deviceCode, userCode, verificationUri } = device.body;
|
|
56
|
+
if (URL.parse(verificationUri)?.origin !== GITHUB_ORIGIN || typeof userCode !== 'string') {
|
|
57
|
+
throw new Error('crossbind: crossbind cloud sent a sign-in page that is not on github.com.');
|
|
58
|
+
}
|
|
59
|
+
print(`crossbind: open ${printable(verificationUri)} and enter the code ${printable(userCode)} to sign in with GitHub.`);
|
|
60
|
+
let pace = paceOf(device.body.interval, MIN_INTERVAL_SECONDS);
|
|
61
|
+
const deadline = now() + (device.body.expiresIn ?? DEFAULT_EXPIRES_SECONDS) * 1000;
|
|
62
|
+
while (now() < deadline) {
|
|
63
|
+
await sleep(pace * 1000);
|
|
64
|
+
const answer = await call(fetcher, url, '/v1/login/token', { method: 'POST', body: { deviceCode } });
|
|
65
|
+
if (answer.status === 202) {
|
|
66
|
+
pace = paceOf(answer.body.interval, pace);
|
|
67
|
+
} else {
|
|
68
|
+
if (!answer.ok) throw refused(answer);
|
|
69
|
+
const { token } = answer.body;
|
|
70
|
+
if (!TOKEN_PATTERN.test(token ?? '')) throw new Error('crossbind: crossbind cloud sent no usable token.');
|
|
71
|
+
const name = printable(answer.body.login).slice(0, MAX_NAME_LENGTH);
|
|
72
|
+
const previous = cloudCredentials(url, file);
|
|
73
|
+
writeCredentials({ ...readCredentials(file), [url]: { token, login: name } }, file);
|
|
74
|
+
if (previous && previous.token !== token) {
|
|
75
|
+
const outcome = await revoke(fetcher, url, previous.token, false);
|
|
76
|
+
if (!outcome.revoked) print(`crossbind: the token this sign-in replaces still works (${outcome.reason.replace(/^crossbind: /, '')}); crossbind logout --all revokes it.`);
|
|
77
|
+
}
|
|
78
|
+
return { login: name };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new Error('crossbind: the code expired before it was entered; run crossbind login again.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Forgets the token here whatever the cloud answers; `revoked` says whether the cloud dropped it too.
|
|
85
|
+
export async function logout({
|
|
86
|
+
url, all = false, file = CREDENTIALS_FILE, fetcher = fetch,
|
|
87
|
+
}) {
|
|
88
|
+
const { [url]: entry, ...rest } = readCredentials(file);
|
|
89
|
+
if (!entry) return null;
|
|
90
|
+
const outcome = await revoke(fetcher, url, entry.token, all);
|
|
91
|
+
writeCredentials(rest, file);
|
|
92
|
+
return { login: entry.login, ...outcome };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The account, with the sign-in of every machine; the sign-in here is forgotten only once the cloud confirms.
|
|
96
|
+
export async function deleteAccount({ url, file = CREDENTIALS_FILE, fetcher = fetch }) {
|
|
97
|
+
const { [url]: entry, ...rest } = readCredentials(file);
|
|
98
|
+
if (!entry) throw new Error(`crossbind: not signed in to ${url}; run crossbind login first.`);
|
|
99
|
+
const answer = await call(fetcher, url, '/v1/account', { method: 'DELETE', token: entry.token });
|
|
100
|
+
if (answer.status === 401) throw new Error('crossbind: this sign-in is no longer valid; run crossbind login, then delete the account.');
|
|
101
|
+
if (!answer.ok) throw refused(answer);
|
|
102
|
+
writeCredentials(rest, file);
|
|
103
|
+
return { login: entry.login };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function fetchUsage({ url, file = CREDENTIALS_FILE, fetcher = fetch }) {
|
|
107
|
+
const entry = cloudCredentials(url, file);
|
|
108
|
+
if (!entry) throw new Error(`crossbind: not signed in to ${url}; run crossbind login.`);
|
|
109
|
+
const answer = await call(fetcher, url, '/v1/usage', { token: entry.token });
|
|
110
|
+
if (answer.status === 401) throw new Error('crossbind: this sign-in is no longer valid; run crossbind login.');
|
|
111
|
+
if (!answer.ok) throw refused(answer);
|
|
112
|
+
return answer.body;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatSeconds(seconds) {
|
|
116
|
+
const whole = Math.max(0, Math.round(seconds));
|
|
117
|
+
const minutes = Math.floor(whole / 60);
|
|
118
|
+
const rest = whole % 60;
|
|
119
|
+
if (minutes === 0) return `${rest}s`;
|
|
120
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function usageLines({ login: name, builds, playground }) {
|
|
124
|
+
const images = Object.entries(builds.byImage).sort(([, a], [, b]) => b - a);
|
|
125
|
+
const column = Math.max(...images.map(([image]) => image.length)) + 3;
|
|
126
|
+
const left = builds.limitSeconds - builds.usedSeconds;
|
|
127
|
+
return [
|
|
128
|
+
`Signed in as ${printable(name)}.`,
|
|
129
|
+
`Cloud builds in ${printable(builds.month)}: ${formatSeconds(builds.usedSeconds)} of ${formatSeconds(builds.limitSeconds)} used, ${formatSeconds(left)} left.`,
|
|
130
|
+
...images.map(([image, seconds]) => ` ${printable(image).padEnd(column)}${formatSeconds(seconds)}`),
|
|
131
|
+
`Playground: ${playground.remaining} of ${playground.limit} compiles left today.`,
|
|
132
|
+
];
|
|
133
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import upath from 'upath';
|
|
3
|
+
import state from '../state/index.js';
|
|
4
|
+
import createBridgeFile from './createInterface.js';
|
|
5
|
+
import getDependFilePath from '../integration/getDependFilePath.js';
|
|
6
|
+
import resolveNativeImport from '../integration/resolveNativeImport.js';
|
|
7
|
+
import { getRustSymbols } from '../integration/getCrossbindScript.js';
|
|
8
|
+
import { findNativeImportsIn, nativeSpecifierTest } from '../utils/nativeImports.js';
|
|
9
|
+
|
|
10
|
+
const SCHEME = /^(cargo|conan):/;
|
|
11
|
+
const isBare = (specifier) => !specifier.startsWith('.') && !upath.isAbsolute(specifier);
|
|
12
|
+
|
|
13
|
+
function realPath(file) {
|
|
14
|
+
try {
|
|
15
|
+
return upath.normalize(fs.realpathSync(file));
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return upath.resolve(file);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The first target whose packages ship the file. A cargo: or conan: import that none resolves fails the build with
|
|
22
|
+
// what to declare or install, as it does in a bundler.
|
|
23
|
+
function resolveImport(specifier, importer, targets) {
|
|
24
|
+
let failure = null;
|
|
25
|
+
for (const target of targets) {
|
|
26
|
+
try {
|
|
27
|
+
const file = SCHEME.test(specifier) ? getDependFilePath(specifier, target) : resolveNativeImport(specifier, importer, target);
|
|
28
|
+
if (file && fs.existsSync(file)) return { target, file };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
failure ??= error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (failure) throw failure;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Whether a binary binds what the app's own sources import. A Node binary does: its module hooks resolve those
|
|
38
|
+
// imports. A browser binary only with --bind-imports: without a bundler a page cannot resolve a header import
|
|
39
|
+
// unless it maps the specifiers itself, as the playground's sandbox does. Nothing on an edge runtime resolves one.
|
|
40
|
+
export function bindsAppImports(target, { bindImports = false } = {}) {
|
|
41
|
+
return target.runtimeEnv === 'node' || (bindImports && target.runtimeEnv === 'browser');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The native files the app's own sources import, by real path, each under the first target whose packages ship it
|
|
45
|
+
// and with the bare specifiers that name it, which Node cannot resolve on its own.
|
|
46
|
+
export function findAppNativeImports(targets) {
|
|
47
|
+
const projectDir = state.config.paths.project;
|
|
48
|
+
const imports = new Map();
|
|
49
|
+
if (!projectDir || !fs.existsSync(projectDir)) return imports;
|
|
50
|
+
for (const { importer, specifier } of findNativeImportsIn(projectDir, nativeSpecifierTest(state.config.ext))) {
|
|
51
|
+
const resolved = resolveImport(specifier, importer, targets);
|
|
52
|
+
if (!resolved) {
|
|
53
|
+
console.warn(`crossbind: ${upath.relative(projectDir, importer)} imports ${specifier}, which no dependency ships for ${targets.map((t) => t.path).join(', ')}.`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const file = realPath(resolved.file);
|
|
57
|
+
const found = imports.get(file) ?? { file, target: resolved.target, specifiers: [] };
|
|
58
|
+
const specifiers = isBare(specifier) && !found.specifiers.includes(specifier) ? [...found.specifiers, specifier] : found.specifiers;
|
|
59
|
+
imports.set(file, { ...found, specifiers });
|
|
60
|
+
}
|
|
61
|
+
return imports;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The imported files no bridge of the build covers yet, bound the way a bundler plugin binds what it transforms. A Rust
|
|
65
|
+
// import has no bridge file: its crate links through buildAppRustCrates or its package, so it brings its names.
|
|
66
|
+
export function createImportedBridges(bound, imports) {
|
|
67
|
+
const boundFiles = new Set(bound.map(({ file }) => realPath(file)));
|
|
68
|
+
return [...imports.values()].filter(({ file }) => !boundFiles.has(file)).map(({ file, target }) => (file.endsWith('.rs')
|
|
69
|
+
? { file, names: getRustSymbols(file) }
|
|
70
|
+
: { file, bridge: createBridgeFile(file, target) }));
|
|
71
|
+
}
|