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
|
@@ -1,22 +1,57 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import getEnv from '../actions/getEnv.js';
|
|
4
4
|
import loadJson from '../utils/loadJson.js';
|
|
5
5
|
import state from '../state/index.js';
|
|
6
6
|
import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
|
|
7
7
|
import { parseCargoMarkerName } from '../utils/cargoImport.js';
|
|
8
|
+
import { UNBOUND_MESSAGE } from '../assets/js-runtime/adapters/unboundMessage.js';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
// `base` is where the bundler serves the app from, as Vite's `base` names it; webpack's public path wins at run time.
|
|
11
|
+
// Metro asks for `liveExports`: it keeps a header's module as it first transformed it while the app's imports change.
|
|
12
|
+
export default function getCrossbindScript(target, bridgePath, { base = '/', liveExports: isLive = false } = {}) {
|
|
10
13
|
if (!bridgePath) {
|
|
11
14
|
throw new Error('getCrossbindScript needs the bridge file of the imported header');
|
|
12
15
|
}
|
|
13
|
-
|
|
16
|
+
const script = buildScript(target, loadJson(`${bridgePath}.exports.json`), base);
|
|
17
|
+
return isLive ? `${script}${liveExportsScript()}` : script;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A name the module does not export comes off the booted module: undefined before initNative. A name the module the page
|
|
21
|
+
// or app loaded lacks, exported or not, is a function that says why. Serialized into the generated module, so it
|
|
22
|
+
// references nothing outside itself.
|
|
23
|
+
export function liveExports(exportsObject, unbound) {
|
|
24
|
+
return new Proxy(exportsObject, {
|
|
25
|
+
get(target, name, receiver) {
|
|
26
|
+
const value = Reflect.get(target, name, receiver);
|
|
27
|
+
const bound = globalThis.__crossbindModule;
|
|
28
|
+
if (value != null || !bound || typeof name !== 'string' || ['then', 'default', 'toJSON'].includes(name)) return value;
|
|
29
|
+
if (bound[name] !== undefined) return bound[name];
|
|
30
|
+
if (!/^[A-Za-z_]\w*$/.test(name)) return value;
|
|
31
|
+
return function notBound() {
|
|
32
|
+
throw new Error(`crossbind: ${name} ${unbound}`);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function liveExportsScript() {
|
|
39
|
+
return `
|
|
40
|
+
const __crossbindLiveExports = ${liveExports.toString()};
|
|
41
|
+
if (typeof module === 'object' && module) module.exports = __crossbindLiveExports(module.exports, ${JSON.stringify(UNBOUND_MESSAGE)});
|
|
42
|
+
`;
|
|
14
43
|
}
|
|
15
44
|
|
|
16
45
|
// The Rust analog of a .h import: parse the crate surface and emit the same proxy module
|
|
17
46
|
// (per-symbol lets assigned inside this module's initNative). Vectors come from the owning
|
|
18
47
|
// cargo package's config; classes/enums come from the parsed source.
|
|
19
|
-
export function getRustJsScript(target, rsFile) {
|
|
48
|
+
export function getRustJsScript(target, rsFile, { base = '/' } = {}) {
|
|
49
|
+
return buildScript(target, getRustSymbols(rsFile), base);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The names a Rust import exports. An app-local file or a cargo: crate gets its bridge crate here, the way a header
|
|
53
|
+
// gets its bridge from createBridgeFile.
|
|
54
|
+
export function getRustSymbols(rsFile) {
|
|
20
55
|
// Compare real paths: dependency paths go through node_modules symlinks (pnpm workspaces)
|
|
21
56
|
// while bundlers hand the transformer the resolved real file.
|
|
22
57
|
const realCrateDir = (d) => {
|
|
@@ -35,12 +70,13 @@ export function getRustJsScript(target, rsFile) {
|
|
|
35
70
|
modulePath,
|
|
36
71
|
spec: cargoDeps[crateName],
|
|
37
72
|
cacheDir: state.config.paths.cache,
|
|
73
|
+
projectDir: state.config.paths.project,
|
|
38
74
|
dtsMode: state.config.dts,
|
|
39
75
|
log: () => {},
|
|
40
76
|
});
|
|
41
77
|
// A crate registers its public names under per-crate names (two crates may export the
|
|
42
78
|
// same name), so the proxy exports the clean name and reads the registered one off the module.
|
|
43
|
-
return
|
|
79
|
+
return exports;
|
|
44
80
|
}
|
|
45
81
|
|
|
46
82
|
const pkg = state.config.allDependencies.find((d) => d.export?.type === 'cargo'
|
|
@@ -60,7 +96,7 @@ export function getRustJsScript(target, rsFile) {
|
|
|
60
96
|
log: () => {},
|
|
61
97
|
});
|
|
62
98
|
}
|
|
63
|
-
|
|
99
|
+
return [
|
|
64
100
|
...model.classes.map((c) => c.name),
|
|
65
101
|
...(model.streams ?? []).map((s) => s.name),
|
|
66
102
|
...model.enums.map((e) => e.name),
|
|
@@ -69,14 +105,13 @@ export function getRustJsScript(target, rsFile) {
|
|
|
69
105
|
...(model.consts ?? []).map((c) => c.name),
|
|
70
106
|
...vectors.map((v) => v.name),
|
|
71
107
|
];
|
|
72
|
-
return buildScript(target, symbols);
|
|
73
108
|
}
|
|
74
109
|
|
|
75
|
-
function buildScript(target, symbols) {
|
|
110
|
+
function buildScript(target, symbols, base) {
|
|
76
111
|
if (!target) {
|
|
77
112
|
throw new Error('The target is not available!');
|
|
78
113
|
}
|
|
79
|
-
const env = JSON.stringify(
|
|
114
|
+
const env = JSON.stringify(getEnv(target));
|
|
80
115
|
const getPlatformScript = target.platform === 'wasm' ? getWebScript : getReactNativeScript;
|
|
81
116
|
|
|
82
117
|
let symbolExportDefineString = '';
|
|
@@ -90,7 +125,7 @@ function buildScript(target, symbols) {
|
|
|
90
125
|
}
|
|
91
126
|
|
|
92
127
|
return `
|
|
93
|
-
${getPlatformScript(env)}
|
|
128
|
+
${getPlatformScript(env, base)}
|
|
94
129
|
|
|
95
130
|
export let AllSymbols = {};
|
|
96
131
|
${symbolExportDefineString}
|
|
@@ -144,7 +179,7 @@ function getReactNativeScript(env) {
|
|
|
144
179
|
const RNJsiLib = TurboModuleRegistry.get('RNJsiLib');
|
|
145
180
|
|
|
146
181
|
function setEnv() {
|
|
147
|
-
const env =
|
|
182
|
+
const env = ${env};
|
|
148
183
|
const CROSSBIND_DATA_PATH = Module.Crossbind.getEnv('CROSSBIND_DATA_PATH');
|
|
149
184
|
|
|
150
185
|
Object.entries(env).forEach(([key, value]) => {
|
|
@@ -165,8 +200,9 @@ function getReactNativeScript(env) {
|
|
|
165
200
|
`;
|
|
166
201
|
}
|
|
167
202
|
|
|
168
|
-
function getWebScript(env) {
|
|
203
|
+
function getWebScript(env, base) {
|
|
169
204
|
const params = `{
|
|
205
|
+
path: base,
|
|
170
206
|
...config,
|
|
171
207
|
env: {...${env}, ...config.env},
|
|
172
208
|
paths: {
|
|
@@ -179,7 +215,8 @@ function getWebScript(env) {
|
|
|
179
215
|
|
|
180
216
|
return `
|
|
181
217
|
function __crossbindBoot(config) {
|
|
182
|
-
|
|
218
|
+
const base = new URL((typeof __webpack_public_path__ === 'string' ? __webpack_public_path__ : ${JSON.stringify(base)}) || './', document.baseURI).href;
|
|
219
|
+
return import(/* webpackIgnore: true */ /* @vite-ignore */ base + 'crossbind.js')
|
|
183
220
|
.then(n => window.Crossbind.initNative(${params}));
|
|
184
221
|
}
|
|
185
222
|
`;
|
|
@@ -2,6 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import state from '../state/index.js';
|
|
4
4
|
import { parseCargoImport, writeCargoMarker } from '../utils/cargoImport.js';
|
|
5
|
+
import { CONAN_PREFIX, resolveConanHeader } from '../utils/conanImport.js';
|
|
6
|
+
import refreshConanDependencies from '../state/refreshConanDependencies.js';
|
|
5
7
|
|
|
6
8
|
const LAYOUT = { header: 'include', module: 'swig', source: '' };
|
|
7
9
|
|
|
@@ -92,6 +94,13 @@ export default function getDependFilePath(source, target) {
|
|
|
92
94
|
return marker;
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
// `conan:<package>/<header>` names a header of a declared Conan package, staged per target like a
|
|
98
|
+
// port's prebuilt; from there the header takes the same path as any other.
|
|
99
|
+
if (source.startsWith(CONAN_PREFIX)) {
|
|
100
|
+
refreshConanDependencies(state.config);
|
|
101
|
+
return resolveConanHeader(state.config, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
const matches = getMatchingPackages(source);
|
|
96
105
|
if (matches.length === 0) return null;
|
|
97
106
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import upath from 'upath';
|
|
3
|
+
import getDependFilePath from './getDependFilePath.js';
|
|
4
|
+
|
|
5
|
+
// A package that is not a crossbind dependency (the conformance kit) resolves the way the bundler finds it.
|
|
6
|
+
export default function resolveNativeImport(specifier, importer, target) {
|
|
7
|
+
if (specifier.startsWith('.')) return upath.resolve(upath.dirname(importer), specifier);
|
|
8
|
+
if (upath.isAbsolute(specifier)) return specifier;
|
|
9
|
+
try {
|
|
10
|
+
return getDependFilePath(specifier, target) ?? createRequire(importer).resolve(specifier);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/node/dev.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// `node --import crossbind/node/dev app.mjs`: builds this machine's binary when the app's native sources or imports
|
|
2
|
+
// changed since the last build it ran, then serves the app's native imports through the hooks that build wrote.
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import loadConfig from '../state/loadConfig.js';
|
|
5
|
+
import { DEV_BUILD_ENV, ensureDevBuild, projectDirOf } from '../utils/nodeDevBuild.js';
|
|
6
|
+
|
|
7
|
+
if (!process.env[DEV_BUILD_ENV]) {
|
|
8
|
+
// node --watch restarts when a build rewrites the outputs the last start loaded. Its SIGTERM waits for the build
|
|
9
|
+
// and its record, then ends this start: the next one finds the build done instead of starting it over.
|
|
10
|
+
let restarting = false;
|
|
11
|
+
const restart = () => { restarting = true; };
|
|
12
|
+
process.on('SIGTERM', restart);
|
|
13
|
+
const hooks = ensureDevBuild(await loadConfig(projectDirOf(process.argv[1])));
|
|
14
|
+
// A signal that came during the build is handled on the next turn of the event loop.
|
|
15
|
+
await new Promise((resolve) => { setImmediate(resolve); });
|
|
16
|
+
process.off('SIGTERM', restart);
|
|
17
|
+
if (restarting) process.exit(143);
|
|
18
|
+
if (hooks) await import(pathToFileURL(hooks).href);
|
|
19
|
+
}
|
package/src/program.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
deleteAccount as deleteCloudAccount, fetchUsage, login as loginToCloud, logout as logoutOfCloud, usageLines,
|
|
6
|
+
} from './actions/cloudCommands.js';
|
|
7
|
+
import { cloudUrl } from './utils/cloudAccount.js';
|
|
8
|
+
import flattenConfigForTable from './utils/flattenConfigForTable.js';
|
|
9
|
+
import loadSystemConfig, { systemConfigPath } from './utils/systemConfig.js';
|
|
10
|
+
import systemKeys from './utils/systemKeys.js';
|
|
11
|
+
import writeJson from './utils/writeJson.js';
|
|
12
|
+
|
|
13
|
+
const packageJSON = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
|
|
14
|
+
|
|
15
|
+
// Build failures bubble up as exceptions with a `cause` chain (see actions/run.js);
|
|
16
|
+
// show the operator a single line — DEBUG=1 for the full stack — and exit non-zero.
|
|
17
|
+
export const handleFatal = (e) => {
|
|
18
|
+
if (process.env.DEBUG) {
|
|
19
|
+
console.error(e);
|
|
20
|
+
} else {
|
|
21
|
+
let message = e?.message || String(e);
|
|
22
|
+
if (e?.cause?.message) message += `\n caused by: ${e.cause.message.split('\n')[0]}`;
|
|
23
|
+
console.error(message);
|
|
24
|
+
}
|
|
25
|
+
process.exit(1);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const createProgram = () => new Command()
|
|
29
|
+
.name('crossbind')
|
|
30
|
+
.description('Compile C++ files to WebAssembly and native platforms.')
|
|
31
|
+
.version(packageJSON.version)
|
|
32
|
+
.showHelpAfterError();
|
|
33
|
+
|
|
34
|
+
export function addSystemCommands(program) {
|
|
35
|
+
program.command('login')
|
|
36
|
+
.description('sign in to crossbind cloud with your GitHub account')
|
|
37
|
+
.action(async () => {
|
|
38
|
+
const url = cloudUrl(loadSystemConfig());
|
|
39
|
+
const { login } = await loginToCloud({ url });
|
|
40
|
+
console.log(`crossbind: signed in to ${url} as ${login}.`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
program.command('logout')
|
|
44
|
+
.description('sign out of crossbind cloud on this machine')
|
|
45
|
+
.option('--all', 'sign out every machine signed in to this account')
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
const url = cloudUrl(loadSystemConfig());
|
|
48
|
+
const result = await logoutOfCloud({ url, all: Boolean(options.all) });
|
|
49
|
+
if (!result) {
|
|
50
|
+
console.log(`crossbind: not signed in to ${url}.`);
|
|
51
|
+
} else if (result.revoked) {
|
|
52
|
+
console.log(`crossbind: signed ${result.login} out${options.all ? ' on every machine' : ''}.`);
|
|
53
|
+
} else {
|
|
54
|
+
console.warn(`crossbind: signed out here, but crossbind cloud did not revoke the token (${result.reason.replace(/^crossbind: /, '')}). Sign in again and run crossbind logout --all to revoke it.`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
program.command('usage')
|
|
59
|
+
.description('show what you used of crossbind cloud this month and what is left')
|
|
60
|
+
.action(async () => {
|
|
61
|
+
usageLines(await fetchUsage({ url: cloudUrl(loadSystemConfig()) })).forEach((line) => console.log(line));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
program.command('account')
|
|
65
|
+
.description('manage your crossbind cloud account')
|
|
66
|
+
.command('delete')
|
|
67
|
+
.description('delete your crossbind cloud account, with the sign-in of every machine (https://crossbind.dev/privacy/)')
|
|
68
|
+
.option('--yes', 'delete it; without this, nothing is deleted')
|
|
69
|
+
.action(async (options) => {
|
|
70
|
+
const url = cloudUrl(loadSystemConfig());
|
|
71
|
+
if (!options.yes) {
|
|
72
|
+
throw new Error(`crossbind: this deletes your crossbind cloud account at ${url}, with the sign-in of every machine. Run crossbind account delete --yes to go ahead.`);
|
|
73
|
+
}
|
|
74
|
+
const { login } = await deleteCloudAccount({ url });
|
|
75
|
+
console.log(`crossbind: deleted the crossbind cloud account of ${login}.`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const commandConfig = program.command('config')
|
|
79
|
+
.description('manage the crossbind configuration files');
|
|
80
|
+
|
|
81
|
+
commandConfig.command('get')
|
|
82
|
+
.description('get the crossbind system configuration')
|
|
83
|
+
.argument('<key>', 'configuration key to read')
|
|
84
|
+
.action((key) => getSystemConfig(key));
|
|
85
|
+
|
|
86
|
+
commandConfig.command('set')
|
|
87
|
+
.description('set the crossbind system configuration')
|
|
88
|
+
.argument('<key>', 'configuration key to set')
|
|
89
|
+
.argument('<value>', 'value to assign')
|
|
90
|
+
.action((key, value) => setSystemConfig(key, value));
|
|
91
|
+
|
|
92
|
+
commandConfig.command('delete')
|
|
93
|
+
.description('delete the crossbind system configuration')
|
|
94
|
+
.argument('<key>', 'configuration key to remove')
|
|
95
|
+
.action((key) => deleteSystemConfig(key));
|
|
96
|
+
|
|
97
|
+
commandConfig.command('list')
|
|
98
|
+
.description('list the crossbind configurations')
|
|
99
|
+
.addOption(new Option('-t, --type <type>', 'config type').default('system').choices(['all', 'system', 'project']))
|
|
100
|
+
.action((options) => listSystemConfig(options.type));
|
|
101
|
+
|
|
102
|
+
commandConfig.command('keys')
|
|
103
|
+
.description('list all available system configuration keys for crossbind')
|
|
104
|
+
.action(() => listSystemKeys());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function listSystemKeys() {
|
|
108
|
+
console.info('Available configurations:');
|
|
109
|
+
console.table(systemKeys);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function getSystemConfig(systemKey) {
|
|
113
|
+
if (!systemKey) {
|
|
114
|
+
await listSystemConfig('system');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const system = loadSystemConfig();
|
|
118
|
+
const key = systemKey.toUpperCase();
|
|
119
|
+
const output = {};
|
|
120
|
+
if (key in system) {
|
|
121
|
+
output[key] = { value: system[key] || 'undefined', default: systemKeys[key].default };
|
|
122
|
+
}
|
|
123
|
+
Object.keys(systemKeys).filter((k) => k.includes(key)).forEach((k) => {
|
|
124
|
+
if (k in system) {
|
|
125
|
+
output[k] = { value: system[k] || 'undefined', default: systemKeys[k].default };
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
console.table(output);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setSystemConfig(key, value) {
|
|
133
|
+
if (!systemKeys[key]) {
|
|
134
|
+
throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
|
|
135
|
+
}
|
|
136
|
+
if (systemKeys[key].options && !systemKeys[key].options.includes(value)) {
|
|
137
|
+
throw new Error(`Value ${value} is not available. Please choose from the following available values: ${systemKeys[key].options.join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
writeJson(systemConfigPath(), { ...loadSystemConfig(), [key]: value });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deleteSystemConfig(key) {
|
|
144
|
+
if (!systemKeys[key]) {
|
|
145
|
+
throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
const { [key]: deleted, ...system } = loadSystemConfig();
|
|
148
|
+
writeJson(systemConfigPath(), system);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function listSystemConfig(type) {
|
|
152
|
+
// Importing the state loads the project, which fails outside one, so only a listing of the project does.
|
|
153
|
+
const { system: systemConfig, ...projectConfig } = type === 'system'
|
|
154
|
+
? { system: loadSystemConfig() }
|
|
155
|
+
: (await import('./state/index.js')).default.config;
|
|
156
|
+
if (type === 'all' || type === 'system') {
|
|
157
|
+
console.log('System Configuration');
|
|
158
|
+
console.table(systemConfig);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (type === 'all') {
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (type === 'all' || type === 'project') {
|
|
166
|
+
console.log(`Project Configuration (paths relative to ${projectConfig.paths.base})`);
|
|
167
|
+
console.table(flattenConfigForTable(projectConfig, { base: projectConfig.paths.base }));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { sha256Of } from './files.js';
|
|
6
|
+
|
|
7
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
8
|
+
const UPLOAD_ID = /^[\w-]{8,64}$/;
|
|
9
|
+
// A part nobody added to for an hour belongs to an upload that stopped; the disk may be memory (Cloud Run).
|
|
10
|
+
const STALE_PART_MS = 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
export const isHash = (value) => typeof value === 'string' && SHA256.test(value);
|
|
13
|
+
export const isUploadId = (value) => typeof value === 'string' && UPLOAD_ID.test(value);
|
|
14
|
+
|
|
15
|
+
// Content-addressed: a blob is stored under its sha256, and an upload whose bytes hash to something else is dropped.
|
|
16
|
+
export function createBlobStore(dir) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
const root = `${path.resolve(dir)}${path.sep}`;
|
|
19
|
+
|
|
20
|
+
const pathOf = (hash) => {
|
|
21
|
+
if (!isHash(hash)) throw new Error(`crossbind runner: "${hash}" is not a sha256 digest`);
|
|
22
|
+
return path.join(dir, hash);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const has = (hash) => isHash(hash) && fs.existsSync(path.join(dir, hash));
|
|
26
|
+
|
|
27
|
+
async function putStream(hash, stream) {
|
|
28
|
+
const temp = path.join(dir, `.${hash}.${process.pid}.${crypto.randomUUID()}`);
|
|
29
|
+
const digest = crypto.createHash('sha256');
|
|
30
|
+
const output = fs.createWriteStream(temp);
|
|
31
|
+
try {
|
|
32
|
+
await pipeline(stream, async function* hashing(source) {
|
|
33
|
+
for await (const chunk of source) {
|
|
34
|
+
digest.update(chunk);
|
|
35
|
+
yield chunk;
|
|
36
|
+
}
|
|
37
|
+
}, output);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// pipeline settles before a file that is still opening closes; removed earlier, it would come back.
|
|
40
|
+
if (!output.closed) await new Promise((resolve) => { output.once('close', resolve); });
|
|
41
|
+
fs.rmSync(temp, { force: true });
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
if (digest.digest('hex') !== hash) {
|
|
45
|
+
fs.rmSync(temp, { force: true });
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
fs.renameSync(temp, pathOf(hash));
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dropStaleParts() {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
for (const name of fs.readdirSync(dir).filter((file) => file.endsWith('.part'))) {
|
|
55
|
+
const stat = fs.statSync(path.join(dir, name), { throwIfNoEntry: false });
|
|
56
|
+
if (stat && now - stat.mtimeMs > STALE_PART_MS) fs.rmSync(path.join(dir, name), { force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A request names the part file, so its path is checked to stay in the store.
|
|
61
|
+
const partOf = (hash, upload) => {
|
|
62
|
+
const file = path.resolve(root, `.${hash}.${upload}.part`);
|
|
63
|
+
if (!file.startsWith(root)) throw new Error(`crossbind runner: "${hash}" and "${upload}" name no part in the store`);
|
|
64
|
+
return file;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// A blob too large for one request arrives in parts, in order and under the id of its upload; the last part
|
|
68
|
+
// checks the joined file against the hash. A part that breaks off leaves the ones before it, so the client sends
|
|
69
|
+
// that part again, and a part of a blob already joined answers as stored, since the last answer may have been lost.
|
|
70
|
+
async function writePart(hash, upload, { start, end, total }, stream) {
|
|
71
|
+
if (!isUploadId(upload)) throw new Error(`crossbind runner: "${upload}" is not an upload id`);
|
|
72
|
+
if (has(hash)) {
|
|
73
|
+
for await (const chunk of stream) void chunk;
|
|
74
|
+
return { status: 'stored' };
|
|
75
|
+
}
|
|
76
|
+
if (start === 0) dropStaleParts();
|
|
77
|
+
const partial = partOf(hash, upload);
|
|
78
|
+
const received = fs.existsSync(partial) ? fs.statSync(partial).size : 0;
|
|
79
|
+
if (start !== received) return { status: 'gap', received };
|
|
80
|
+
const restore = () => {
|
|
81
|
+
if (start === 0) fs.rmSync(partial, { force: true });
|
|
82
|
+
else if (fs.existsSync(partial)) fs.truncateSync(partial, start);
|
|
83
|
+
};
|
|
84
|
+
let length = 0;
|
|
85
|
+
const output = fs.createWriteStream(partial, { flags: 'a' });
|
|
86
|
+
try {
|
|
87
|
+
await pipeline(stream, async function* counting(source) {
|
|
88
|
+
for await (const chunk of source) {
|
|
89
|
+
length += chunk.length;
|
|
90
|
+
yield chunk;
|
|
91
|
+
}
|
|
92
|
+
}, output);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (!output.closed) await new Promise((resolve) => { output.once('close', resolve); });
|
|
95
|
+
restore();
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
if (length !== end - start + 1) {
|
|
99
|
+
restore();
|
|
100
|
+
return { status: 'short' };
|
|
101
|
+
}
|
|
102
|
+
if (end + 1 < total) return { status: 'partial', received: end + 1 };
|
|
103
|
+
const digest = crypto.createHash('sha256');
|
|
104
|
+
for await (const chunk of fs.createReadStream(partial)) digest.update(chunk);
|
|
105
|
+
const joined = digest.digest('hex');
|
|
106
|
+
if (joined !== hash) {
|
|
107
|
+
fs.rmSync(partial, { force: true });
|
|
108
|
+
return { status: 'mismatch' };
|
|
109
|
+
}
|
|
110
|
+
fs.renameSync(partial, pathOf(joined));
|
|
111
|
+
return { status: 'stored' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The parts of one upload go one at a time: a part whose connection broke off can still be writing when the client
|
|
115
|
+
// sends it again.
|
|
116
|
+
const writing = new Map();
|
|
117
|
+
function putPart(hash, upload, range, stream) {
|
|
118
|
+
const key = `${hash}.${upload}`;
|
|
119
|
+
const current = (writing.get(key) ?? Promise.resolve()).catch(() => {}).then(() => writePart(hash, upload, range, stream));
|
|
120
|
+
writing.set(key, current);
|
|
121
|
+
const forget = () => {
|
|
122
|
+
if (writing.get(key) === current) writing.delete(key);
|
|
123
|
+
};
|
|
124
|
+
current.then(forget, forget);
|
|
125
|
+
return current;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function putBuffer(data) {
|
|
129
|
+
const hash = sha256Of(data);
|
|
130
|
+
if (!has(hash)) fs.writeFileSync(pathOf(hash), data);
|
|
131
|
+
return hash;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { pathOf, has, putStream, putPart, putBuffer };
|
|
135
|
+
}
|