convex-rust 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +137 -2
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +200 -0
- package/dist/cli/build.d.ts +146 -0
- package/dist/cli/build.js +902 -0
- package/dist/cli/demangle.d.ts +7 -0
- package/dist/cli/demangle.js +328 -0
- package/dist/cli/diagnostics.d.ts +34 -0
- package/dist/cli/diagnostics.js +296 -0
- package/dist/cli/generate.d.ts +29 -0
- package/dist/cli/generate.js +82 -0
- package/dist/cli/manifest.d.ts +24 -0
- package/dist/cli/manifest.js +75 -0
- package/dist/cli/rustApi.d.ts +78 -0
- package/dist/cli/rustApi.js +609 -0
- package/dist/cli/scaffold.d.ts +22 -0
- package/dist/cli/scaffold.js +114 -0
- package/dist/cli/shim.d.ts +30 -0
- package/dist/cli/shim.js +140 -0
- package/dist/cli/size.d.ts +17 -0
- package/dist/cli/size.js +73 -0
- package/dist/cli/split.d.ts +20 -0
- package/dist/cli/split.js +140 -0
- package/dist/cli/symbols.d.ts +30 -0
- package/dist/cli/symbols.js +133 -0
- package/dist/cli/toolchain.d.ts +53 -0
- package/dist/cli/toolchain.js +167 -0
- package/dist/cli/validators.d.ts +6 -0
- package/dist/cli/validators.js +66 -0
- package/dist/cli/wasisdk.d.ts +82 -0
- package/dist/cli/wasisdk.js +220 -0
- package/dist/cli/wasmHints.d.ts +29 -0
- package/dist/cli/wasmHints.js +172 -0
- package/dist/runtime/base64.d.ts +4 -0
- package/dist/runtime/base64.js +77 -0
- package/dist/runtime/crypto.d.ts +7 -0
- package/dist/runtime/crypto.js +421 -0
- package/dist/runtime/fastjson.d.ts +3 -0
- package/dist/runtime/fastjson.js +170 -0
- package/dist/runtime/host.d.ts +73 -0
- package/dist/runtime/host.js +1230 -0
- package/dist/runtime/http.d.ts +40 -0
- package/dist/runtime/http.js +157 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/intl.d.ts +5 -0
- package/dist/runtime/intl.js +69 -0
- package/dist/runtime/tmpfs.d.ts +129 -0
- package/dist/runtime/tmpfs.js +624 -0
- package/dist/runtime/wasi.d.ts +33 -0
- package/dist/runtime/wasi.js +216 -0
- package/package.json +66 -4
- package/rust/convex_fn/Cargo.toml +32 -0
- package/rust/convex_fn/LICENSE +201 -0
- package/rust/convex_fn/build.rs +3 -0
- package/rust/convex_fn/src/args.rs +416 -0
- package/rust/convex_fn/src/crypto.rs +237 -0
- package/rust/convex_fn/src/ctx.rs +1044 -0
- package/rust/convex_fn/src/dns.rs +356 -0
- package/rust/convex_fn/src/error.rs +179 -0
- package/rust/convex_fn/src/executor.rs +577 -0
- package/rust/convex_fn/src/fanout.rs +106 -0
- package/rust/convex_fn/src/fs.rs +19 -0
- package/rust/convex_fn/src/function_ref.rs +219 -0
- package/rust/convex_fn/src/http.rs +441 -0
- package/rust/convex_fn/src/intl.rs +75 -0
- package/rust/convex_fn/src/lib.rs +242 -0
- package/rust/convex_fn/src/logging.rs +222 -0
- package/rust/convex_fn/src/native.rs +360 -0
- package/rust/convex_fn/src/net.rs +122 -0
- package/rust/convex_fn/src/sys.rs +178 -0
- package/rust/convex_fn/src/values.rs +121 -0
- package/rust/convex_fn/src/wire.rs +579 -0
- package/rust/convex_fn_macros/Cargo.toml +13 -0
- package/rust/convex_fn_macros/LICENSE +201 -0
- package/rust/convex_fn_macros/src/derive.rs +632 -0
- package/rust/convex_fn_macros/src/lib.rs +338 -0
- package/rust/convex_reqwest/Cargo.toml +69 -0
- package/rust/convex_reqwest/LICENSE +201 -0
- package/rust/convex_reqwest/src/blocking.rs +287 -0
- package/rust/convex_reqwest/src/body.rs +152 -0
- package/rust/convex_reqwest/src/client.rs +359 -0
- package/rust/convex_reqwest/src/config_types.rs +150 -0
- package/rust/convex_reqwest/src/error.rs +241 -0
- package/rust/convex_reqwest/src/into_url.rs +57 -0
- package/rust/convex_reqwest/src/lib.rs +42 -0
- package/rust/convex_reqwest/src/multipart.rs +140 -0
- package/rust/convex_reqwest/src/request.rs +358 -0
- package/rust/convex_reqwest/src/response.rs +310 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { GENERATED_HEADER, generateFiles } from "./generate.js";
|
|
6
|
+
import { convexModuleName, parseManifest } from "./manifest.js";
|
|
7
|
+
import { RUST_TARGET, checkRustToolchain, offerToolchainFix, wasmOptInstallHint } from "./toolchain.js";
|
|
8
|
+
import { describeWasmSize, formatBytes } from "./size.js";
|
|
9
|
+
import { splitWasmByModule } from "./split.js";
|
|
10
|
+
import { updateApiRs, readRustCache, writeRustCache } from "./rustApi.js";
|
|
11
|
+
import { syncManagedPatch, usesReqwest } from "./shim.js";
|
|
12
|
+
import { cargoTreeArgs, diagnoseCargoFailure, formatFindings } from "./diagnostics.js";
|
|
13
|
+
import { crateNamesWithCBuild, prepareWasiSdk } from "./wasisdk.js";
|
|
14
|
+
import { formatWasmHints, wasmHints } from "./wasmHints.js";
|
|
15
|
+
import { buildSymbolMap, parseFunctionMap, parseNameSection, symbolsDir, writeSymbolMaps } from "./symbols.js";
|
|
16
|
+
export class BuildError extends Error {
|
|
17
|
+
}
|
|
18
|
+
export const GROUP_FEATURE_PREFIX = "m-";
|
|
19
|
+
/** Parses `--group name=modA,modB` values. Throws {@link BuildError} on a malformed or overlapping spec. */
|
|
20
|
+
export function parseGroups(specs) {
|
|
21
|
+
const groups = [];
|
|
22
|
+
const seenModule = new Map();
|
|
23
|
+
for (const spec of specs) {
|
|
24
|
+
const m = /^([A-Za-z0-9_-]+)=(.+)$/.exec(spec.trim());
|
|
25
|
+
if (!m)
|
|
26
|
+
throw new BuildError(`--group expects name=module,module (got "${spec}").`);
|
|
27
|
+
const modules = m[2].split(",").map((x) => x.trim()).filter(Boolean);
|
|
28
|
+
if (modules.length === 0)
|
|
29
|
+
throw new BuildError(`--group ${m[1]} lists no modules.`);
|
|
30
|
+
if (groups.some((g) => g.name === m[1]))
|
|
31
|
+
throw new BuildError(`--group ${m[1]} given twice.`);
|
|
32
|
+
for (const mod of modules) {
|
|
33
|
+
const prev = seenModule.get(mod);
|
|
34
|
+
if (prev)
|
|
35
|
+
throw new BuildError(`module ${mod} is in both group ${prev} and group ${m[1]}.`);
|
|
36
|
+
seenModule.set(mod, m[1]);
|
|
37
|
+
}
|
|
38
|
+
groups.push({ name: m[1], modules });
|
|
39
|
+
}
|
|
40
|
+
return groups;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Convex module names for the crate's `<prefix><module>` features (`m-a-b` -> `a/b`: Rust module names have
|
|
44
|
+
* no dashes, so a dash is a path separator). Sorted; empty when the crate has fewer than two such features
|
|
45
|
+
* (one feature gates nothing worth splitting).
|
|
46
|
+
*/
|
|
47
|
+
export function modulesFromFeatures(features, prefix = GROUP_FEATURE_PREFIX) {
|
|
48
|
+
const mods = features
|
|
49
|
+
.filter((f) => f.startsWith(prefix) && f.length > prefix.length)
|
|
50
|
+
.map((f) => f.slice(prefix.length).replace(/-/g, "/"))
|
|
51
|
+
.sort();
|
|
52
|
+
return mods.length >= 2 ? mods : [];
|
|
53
|
+
}
|
|
54
|
+
/** File-name stem of a module's wasm: the module with `/` as `__`. */
|
|
55
|
+
export function moduleFileName(module) {
|
|
56
|
+
return module.replace(/\//g, "__");
|
|
57
|
+
}
|
|
58
|
+
/** Rejects modules whose wasm file names collide (`a/b` and a real module `a__b` would overwrite each other). */
|
|
59
|
+
export function assertDistinctFileNames(modules) {
|
|
60
|
+
const seen = new Map();
|
|
61
|
+
for (const m of modules) {
|
|
62
|
+
const f = moduleFileName(m);
|
|
63
|
+
const prev = seen.get(f);
|
|
64
|
+
if (prev !== undefined && prev !== m) {
|
|
65
|
+
throw new BuildError(`Rust modules \`${prev}\` and \`${m}\` both map to the wasm file name \`${f}\`; rename one of them.`);
|
|
66
|
+
}
|
|
67
|
+
seen.set(f, m);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** One group per module; the group (file) name is the module with `/` as `__`. */
|
|
71
|
+
export function perModuleGroups(modules) {
|
|
72
|
+
assertDistinctFileNames(modules);
|
|
73
|
+
return modules.map((m) => ({ name: moduleFileName(m), modules: [m] }));
|
|
74
|
+
}
|
|
75
|
+
/** Cargo feature args that build only a group's modules. Module `a/b` maps to feature `<prefix>a-b`. */
|
|
76
|
+
export function groupFeatureArgs(group, prefix = GROUP_FEATURE_PREFIX) {
|
|
77
|
+
return ["--no-default-features", "--features", group.modules.map((m) => prefix + m.replace(/\//g, "-")).join(",")];
|
|
78
|
+
}
|
|
79
|
+
/** Wasm size budget for all per-module wasm together: Convex bundles at most 32 MiB of code per deployment. */
|
|
80
|
+
export const WASM_BUDGET_BYTES = 28 * 1024 * 1024;
|
|
81
|
+
/**
|
|
82
|
+
* Zero-config per-module build: the extra `rustc` arguments (after `cargo rustc ... --`) that make the
|
|
83
|
+
* `#[action]` macro keep only the exports of `fns` (see `crates/convex_fn_macros`). `--check-cfg` keeps
|
|
84
|
+
* the `unexpected_cfgs` lint quiet for crates that do not otherwise declare these cfgs.
|
|
85
|
+
*/
|
|
86
|
+
export function splitRustcArgs(fns) {
|
|
87
|
+
const out = ["--check-cfg", "cfg(convex_split)", "--check-cfg", "cfg(convex_keep, values(any()))", "--cfg", "convex_split"];
|
|
88
|
+
for (const f of [...new Set(fns)].sort())
|
|
89
|
+
out.push("--cfg", `convex_keep="${f}"`);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** The two smallest entries of `sizes` (the next merge candidates when over the size budget), or undefined. */
|
|
93
|
+
export function smallestTwo(sizes) {
|
|
94
|
+
if (sizes.size < 2)
|
|
95
|
+
return undefined;
|
|
96
|
+
const [a, b] = [...sizes].sort((x, y) => x[1] - y[1]);
|
|
97
|
+
return [a[0], b[0]];
|
|
98
|
+
}
|
|
99
|
+
/** Convex functions directory: `functions` in convex.json, default `convex`. */
|
|
100
|
+
export function resolveFunctionsDir(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(cwd, "convex.json"), "utf8"));
|
|
103
|
+
if (typeof cfg.functions === "string" && cfg.functions) {
|
|
104
|
+
return path.resolve(cwd, cfg.functions);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// no convex.json
|
|
109
|
+
}
|
|
110
|
+
return path.resolve(cwd, "convex");
|
|
111
|
+
}
|
|
112
|
+
export function cargoProfileDir(profile) {
|
|
113
|
+
return profile === "dev" ? "convex-dev" : "release";
|
|
114
|
+
}
|
|
115
|
+
/** Cargo args and env for a profile; the dev profile is defined via env. */
|
|
116
|
+
export function cargoBuildConfig(profile) {
|
|
117
|
+
if (profile === "release")
|
|
118
|
+
return { args: ["--release"], env: {} };
|
|
119
|
+
return {
|
|
120
|
+
args: ["--profile", "convex-dev"],
|
|
121
|
+
env: {
|
|
122
|
+
CARGO_PROFILE_CONVEX_DEV_INHERITS: "release",
|
|
123
|
+
CARGO_PROFILE_CONVEX_DEV_OPT_LEVEL: "1",
|
|
124
|
+
CARGO_PROFILE_CONVEX_DEV_INCREMENTAL: "true",
|
|
125
|
+
CARGO_PROFILE_CONVEX_DEV_LTO: "false",
|
|
126
|
+
CARGO_PROFILE_CONVEX_DEV_CODEGEN_UNITS: "16",
|
|
127
|
+
CARGO_PROFILE_CONVEX_DEV_DEBUG: "false",
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Rust flag pair that turns on wasm SIMD. */
|
|
132
|
+
export const SIMD_RUSTFLAG = "target-feature=+simd128";
|
|
133
|
+
/**
|
|
134
|
+
* Extra cargo args / env for the SIMD option. With RUSTFLAGS (or CARGO_ENCODED_RUSTFLAGS) set the flag is
|
|
135
|
+
* appended to it; otherwise it goes in as `--config target.<triple>.rustflags`, which cargo joins with the
|
|
136
|
+
* rustflags of every `.cargo/config.toml` `target.<triple>.rustflags` (a config `build.rustflags` is then
|
|
137
|
+
* shadowed, as for any target-specific rustflags).
|
|
138
|
+
*/
|
|
139
|
+
export function simdCargoConfig(simd, env = process.env) {
|
|
140
|
+
if (!simd)
|
|
141
|
+
return { args: [], env: {} };
|
|
142
|
+
if (env.CARGO_ENCODED_RUSTFLAGS) {
|
|
143
|
+
return { args: [], env: { CARGO_ENCODED_RUSTFLAGS: `${env.CARGO_ENCODED_RUSTFLAGS}\x1f-C\x1f${SIMD_RUSTFLAG}` } };
|
|
144
|
+
}
|
|
145
|
+
if (env.RUSTFLAGS)
|
|
146
|
+
return { args: [], env: { RUSTFLAGS: `${env.RUSTFLAGS} -C ${SIMD_RUSTFLAG}` } };
|
|
147
|
+
return { args: ["--config", `target.${RUST_TARGET}.rustflags=["-C","${SIMD_RUSTFLAG}"]`], env: {} };
|
|
148
|
+
}
|
|
149
|
+
function runCargo(cwd, args, inherit, env = {}) {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const child = spawn("cargo", args, {
|
|
152
|
+
cwd,
|
|
153
|
+
env: { ...process.env, ...env },
|
|
154
|
+
stdio: ["ignore", inherit ? "inherit" : "pipe", "pipe"],
|
|
155
|
+
});
|
|
156
|
+
let stdout = "";
|
|
157
|
+
let stderr = "";
|
|
158
|
+
let error;
|
|
159
|
+
child.stdout?.setEncoding("utf8");
|
|
160
|
+
child.stdout?.on("data", (d) => (stdout += d));
|
|
161
|
+
child.stderr?.setEncoding("utf8");
|
|
162
|
+
child.stderr?.on("data", (d) => {
|
|
163
|
+
process.stderr.write(d);
|
|
164
|
+
if (stderr.length < 8 * 1024 * 1024)
|
|
165
|
+
stderr += d;
|
|
166
|
+
});
|
|
167
|
+
child.on("error", (e) => (error = e));
|
|
168
|
+
child.on("close", (status) => resolve({ status, error, stdout, stderr }));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Cargo target directory and library name of the crate, via `cargo metadata`. */
|
|
172
|
+
export async function findCrateLayout(crateDir) {
|
|
173
|
+
const manifestPath = path.join(crateDir, "Cargo.toml");
|
|
174
|
+
const r = await runCargo(crateDir, ["metadata", "--format-version", "1", "--no-deps", "--manifest-path", manifestPath], false);
|
|
175
|
+
if (r.error || r.status !== 0) {
|
|
176
|
+
throw new BuildError(`\`cargo metadata\` failed for ${manifestPath}.`);
|
|
177
|
+
}
|
|
178
|
+
const meta = JSON.parse(r.stdout);
|
|
179
|
+
const manifestAbs = path.resolve(manifestPath);
|
|
180
|
+
const pkg = meta.packages.find((p) => path.resolve(p.manifest_path) === manifestAbs) ??
|
|
181
|
+
meta.packages[0];
|
|
182
|
+
const lib = pkg.targets.find((t) => t.kind.includes("cdylib")) ??
|
|
183
|
+
pkg.targets.find((t) => t.kind.includes("lib")) ??
|
|
184
|
+
pkg.targets[0];
|
|
185
|
+
return {
|
|
186
|
+
targetDir: meta.target_directory,
|
|
187
|
+
libName: String(lib?.name ?? pkg.name).replace(/-/g, "_"),
|
|
188
|
+
features: Object.keys(pkg.features ?? {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** Crates of the wasm32-wasip1 dependency graph whose build script drives a C compiler (`cc` / `cmake`). */
|
|
192
|
+
export async function cBuildCrates(crateDir) {
|
|
193
|
+
const r = await runCargo(crateDir, ["metadata", "--format-version", "1", "--filter-platform", RUST_TARGET, "--manifest-path", path.join(crateDir, "Cargo.toml")], false);
|
|
194
|
+
if (r.error || r.status !== 0)
|
|
195
|
+
return [];
|
|
196
|
+
return crateNamesWithCBuild(JSON.parse(r.stdout));
|
|
197
|
+
}
|
|
198
|
+
const WASM_OPT_FEATURES = [
|
|
199
|
+
"--enable-bulk-memory",
|
|
200
|
+
"--enable-sign-ext",
|
|
201
|
+
"--enable-mutable-globals",
|
|
202
|
+
"--enable-nontrapping-float-to-int",
|
|
203
|
+
"--enable-multivalue",
|
|
204
|
+
];
|
|
205
|
+
// Speed over size: -O3, no -g (the name section is dropped; panic messages keep their file:line, which
|
|
206
|
+
// comes from the Location data in .rodata). CONVEX_RUST_WASM_OPT_FLAGS (space separated) replaces
|
|
207
|
+
// the optimization flags, e.g. "-Oz -g" for the old behaviour.
|
|
208
|
+
const WASM_OPT_FEATURES_SIMD = ["--enable-simd"];
|
|
209
|
+
export function wasmOptFlags() {
|
|
210
|
+
const o = process.env.CONVEX_RUST_WASM_OPT_FLAGS?.trim();
|
|
211
|
+
const opt = o ? o.split(/\s+/) : ["-O3"];
|
|
212
|
+
return [...opt, ...WASM_OPT_FEATURES, ...WASM_OPT_FEATURES_SIMD];
|
|
213
|
+
}
|
|
214
|
+
function wasmOptVersion() {
|
|
215
|
+
const r = spawnSync("wasm-opt", ["--version"], { encoding: "utf8" });
|
|
216
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* `wasm-opt -O3` when available, with a content-addressed cache in
|
|
220
|
+
* `cacheDir`. Returns the bytes to ship (the input when wasm-opt is missing,
|
|
221
|
+
* fails, does not shrink the module or drops the manifest). `onNames` receives the function names
|
|
222
|
+
* (index in the returned module -> raw name) for the symbol map.
|
|
223
|
+
*/
|
|
224
|
+
export function optimizeWasm(input, cacheDir, log = () => { }, onNames = () => { }) {
|
|
225
|
+
const version = wasmOptVersion();
|
|
226
|
+
if (version === null) {
|
|
227
|
+
log(`wasm-opt not found on PATH; shipping the unoptimized wasm (smaller with binaryen: ${wasmOptInstallHint()})`);
|
|
228
|
+
return input;
|
|
229
|
+
}
|
|
230
|
+
const key = createHash("sha256")
|
|
231
|
+
.update(version + "\0" + wasmOptFlags().join(" "))
|
|
232
|
+
.update(input)
|
|
233
|
+
.digest("hex");
|
|
234
|
+
const cached = path.join(cacheDir, `${key}.wasm`);
|
|
235
|
+
const cachedMap = path.join(cacheDir, `${key}.fnmap`);
|
|
236
|
+
if (fs.existsSync(cached) && fs.existsSync(cachedMap)) {
|
|
237
|
+
log("wasm-opt: cache hit");
|
|
238
|
+
onNames(parseFunctionMap(fs.readFileSync(cachedMap, "utf8")));
|
|
239
|
+
return new Uint8Array(fs.readFileSync(cached));
|
|
240
|
+
}
|
|
241
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
242
|
+
const src = path.join(cacheDir, `${key}.${process.pid}.in`);
|
|
243
|
+
const dst = path.join(cacheDir, `${key}.${process.pid}.tmp`);
|
|
244
|
+
fs.writeFileSync(src, input);
|
|
245
|
+
try {
|
|
246
|
+
// --print-function-map last: it lists the function indexes of the optimized module (wasm-opt renumbers them).
|
|
247
|
+
const r = spawnSync("wasm-opt", [...wasmOptFlags(), "--print-function-map", src, "-o", dst], {
|
|
248
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
249
|
+
encoding: "utf8",
|
|
250
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
251
|
+
});
|
|
252
|
+
if (r.status !== 0) {
|
|
253
|
+
log(`wasm-opt failed (${r.status}); using the unoptimized wasm`);
|
|
254
|
+
return input;
|
|
255
|
+
}
|
|
256
|
+
const after = new Uint8Array(fs.readFileSync(dst));
|
|
257
|
+
if (after.length >= input.length || parseManifest(after).length !== parseManifest(input).length) {
|
|
258
|
+
log("wasm-opt output not smaller or lost the manifest; ignoring");
|
|
259
|
+
return input;
|
|
260
|
+
}
|
|
261
|
+
fs.writeFileSync(cachedMap, r.stdout);
|
|
262
|
+
fs.renameSync(dst, cached);
|
|
263
|
+
onNames(parseFunctionMap(r.stdout));
|
|
264
|
+
log(`wasm-opt: ${input.length} -> ${after.length} bytes`);
|
|
265
|
+
return after;
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
fs.rmSync(src, { force: true });
|
|
269
|
+
fs.rmSync(dst, { force: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function buildFailureHints(crateDir, stderr) {
|
|
273
|
+
try {
|
|
274
|
+
const findings = diagnoseCargoFailure(stderr);
|
|
275
|
+
const trees = {};
|
|
276
|
+
for (const crate of new Set(findings.map((f) => f.crate))) {
|
|
277
|
+
if (!crate || Object.keys(trees).length >= 3)
|
|
278
|
+
continue;
|
|
279
|
+
const r = await runCargo(crateDir, [...cargoTreeArgs(crate)], false);
|
|
280
|
+
if (r.status === 0)
|
|
281
|
+
trees[crate] = r.stdout;
|
|
282
|
+
}
|
|
283
|
+
const text = formatFindings(findings, trees);
|
|
284
|
+
return text === "" ? "" : "\n" + text;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return "";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Resolve Cargo.lock (so a transitive reqwest is seen) and sync the managed reqwest patch. */
|
|
291
|
+
async function prepareCrate(crateDir, log) {
|
|
292
|
+
if (!fs.existsSync(path.join(crateDir, "Cargo.lock"))) {
|
|
293
|
+
await runCargo(crateDir, ["metadata", "--format-version", "1", "--manifest-path", path.join(crateDir, "Cargo.toml")], false);
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
if (syncManagedPatch(crateDir)) {
|
|
297
|
+
log("updated the managed [patch] block in .cargo/config.toml");
|
|
298
|
+
// A Cargo.lock written before the patch pins the registry reqwest; re-resolve so the patch applies.
|
|
299
|
+
if (usesReqwest(crateDir)) {
|
|
300
|
+
await runCargo(crateDir, ["update", "--manifest-path", path.join(crateDir, "Cargo.toml"), "reqwest"], false);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
log(`warning: could not update .cargo/config.toml: ${e?.message ?? e}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** The stock bundler walks every file of the functions directory; cargo output there is a hazard. */
|
|
309
|
+
export function warnIfTargetInsideFunctions(targetDir, functionsDir, log) {
|
|
310
|
+
const rel = path.relative(functionsDir, targetDir);
|
|
311
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
312
|
+
log(`warning: cargo's target directory (${targetDir}) is inside the Convex functions directory, where \`convex dev\` ` +
|
|
313
|
+
`would walk it and may bundle stray .js files. Set \`[build] target-dir\` in ${"`.cargo/config.toml`"} to a path outside it ` +
|
|
314
|
+
"(`convex-rust init` does this for new crates).");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function writeIfChanged(file, data) {
|
|
318
|
+
try {
|
|
319
|
+
const cur = fs.readFileSync(file);
|
|
320
|
+
if (Buffer.compare(cur, Buffer.from(data)) === 0)
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// new file
|
|
325
|
+
}
|
|
326
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
327
|
+
fs.writeFileSync(file, data);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
const SKIP_DIRS = new Set(["node_modules", "target", "src", "_wasm", ".cargo", ".git"]);
|
|
331
|
+
/** Previously generated `.ts` files below `dir` (those starting with the header). */
|
|
332
|
+
function findGenerated(dir, base = dir, out = []) {
|
|
333
|
+
let entries;
|
|
334
|
+
try {
|
|
335
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
for (const e of entries) {
|
|
341
|
+
const p = path.join(dir, e.name);
|
|
342
|
+
if (e.isDirectory()) {
|
|
343
|
+
if (!SKIP_DIRS.has(e.name))
|
|
344
|
+
findGenerated(p, base, out);
|
|
345
|
+
}
|
|
346
|
+
else if (e.name.endsWith(".ts")) {
|
|
347
|
+
try {
|
|
348
|
+
if (fs.readFileSync(p, "utf8").startsWith(GENERATED_HEADER)) {
|
|
349
|
+
out.push(path.relative(base, p).split(path.sep).join("/"));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// unreadable
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
/** Writes the generated files and the wasm; removes stale generated files. */
|
|
360
|
+
export function writeOutput(outDir, wasmFile, wasm, entries, functionsDir, env = [], split, grouped) {
|
|
361
|
+
const rel = path.relative(functionsDir, outDir).split(path.sep).join("/");
|
|
362
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
363
|
+
throw new BuildError(`--out ${outDir} must be inside the Convex functions directory (${functionsDir}).`);
|
|
364
|
+
}
|
|
365
|
+
const wasmHash = createHash("sha256").update(wasm).digest("hex").slice(0, 16);
|
|
366
|
+
const moduleWasm = new Map();
|
|
367
|
+
const wasmFiles = new Map([[wasmFile, wasm]]);
|
|
368
|
+
if (split) {
|
|
369
|
+
const stem = wasmFile.replace(/\.wasm$/, "");
|
|
370
|
+
wasmFiles.clear();
|
|
371
|
+
assertDistinctFileNames(split.keys());
|
|
372
|
+
for (const [module, bytes] of split) {
|
|
373
|
+
const file = `${stem}.${moduleFileName(module)}.wasm`;
|
|
374
|
+
moduleWasm.set(module, { file, hash: createHash("sha256").update(bytes).digest("hex").slice(0, 16) });
|
|
375
|
+
wasmFiles.set(file, bytes);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (grouped) {
|
|
379
|
+
const stem = wasmFile.replace(/\.wasm$/, "");
|
|
380
|
+
wasmFiles.clear();
|
|
381
|
+
const info = new Map();
|
|
382
|
+
for (const [group, bytes] of grouped.wasm) {
|
|
383
|
+
const file = `${stem}.${group}.wasm`;
|
|
384
|
+
info.set(group, { file, hash: createHash("sha256").update(bytes).digest("hex").slice(0, 16) });
|
|
385
|
+
wasmFiles.set(file, bytes);
|
|
386
|
+
}
|
|
387
|
+
for (const [module, group] of grouped.moduleGroup)
|
|
388
|
+
moduleWasm.set(module, info.get(group));
|
|
389
|
+
}
|
|
390
|
+
const files = generateFiles({ entries, wasmFile, outRelToFunctions: rel === "" ? "." : rel, wasmHash, env, moduleWasm });
|
|
391
|
+
const written = [];
|
|
392
|
+
for (const [name, text] of files) {
|
|
393
|
+
if (writeIfChanged(path.join(outDir, name), text))
|
|
394
|
+
written.push(name);
|
|
395
|
+
}
|
|
396
|
+
for (const [file, bytes] of wasmFiles) {
|
|
397
|
+
if (writeIfChanged(path.join(outDir, "_wasm", file), bytes))
|
|
398
|
+
written.push(`_wasm/${file}`);
|
|
399
|
+
}
|
|
400
|
+
const removed = [];
|
|
401
|
+
try {
|
|
402
|
+
// Stale wasm (a previous build, or the other of split / shared): the bundler only ships imported ones, but keep the dir tidy.
|
|
403
|
+
for (const f of fs.readdirSync(path.join(outDir, "_wasm"))) {
|
|
404
|
+
if (f.endsWith(".wasm") && !wasmFiles.has(f)) {
|
|
405
|
+
fs.rmSync(path.join(outDir, "_wasm", f), { force: true });
|
|
406
|
+
removed.push(`_wasm/${f}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// no _wasm dir yet
|
|
412
|
+
}
|
|
413
|
+
for (const old of findGenerated(outDir)) {
|
|
414
|
+
if (!files.has(old)) {
|
|
415
|
+
fs.rmSync(path.join(outDir, old), { force: true });
|
|
416
|
+
removed.push(old);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { written, removed };
|
|
420
|
+
}
|
|
421
|
+
/** Builds the crate and writes the generated files. Throws {@link BuildError}. */
|
|
422
|
+
export async function buildOnce(opts) {
|
|
423
|
+
const log = opts.log ?? ((m) => console.error(m));
|
|
424
|
+
const functionsDir = resolveFunctionsDir(opts.cwd);
|
|
425
|
+
const crateDir = path.resolve(opts.cwd, opts.crate ?? path.join(functionsDir, "rust"));
|
|
426
|
+
const outDir = path.resolve(opts.cwd, opts.out ?? path.join(functionsDir, "rust"));
|
|
427
|
+
const manifestPath = path.join(crateDir, "Cargo.toml");
|
|
428
|
+
if (!fs.existsSync(manifestPath)) {
|
|
429
|
+
throw new BuildError(`No Rust crate found: ${manifestPath} does not exist. Run \`npx convex-rust init\` to create one.`);
|
|
430
|
+
}
|
|
431
|
+
const tc = await offerToolchainFix(crateDir, checkRustToolchain(crateDir), { log });
|
|
432
|
+
if (!tc.ok)
|
|
433
|
+
throw new BuildError(`Rust toolchain check failed.\n${tc.message}`);
|
|
434
|
+
const cfg = cargoBuildConfig(opts.profile);
|
|
435
|
+
const simdCfg = simdCargoConfig(opts.simd ?? true);
|
|
436
|
+
const layout0 = await findCrateLayout(crateDir);
|
|
437
|
+
const cacheFile = path.join(layout0.targetDir, "convex-rust-modules.json");
|
|
438
|
+
warnIfTargetInsideFunctions(layout0.targetDir, functionsDir, log);
|
|
439
|
+
await prepareCrate(crateDir, log);
|
|
440
|
+
try {
|
|
441
|
+
await updateApiRs({
|
|
442
|
+
projectRoot: opts.cwd, functionsDir, crateDir, outDir,
|
|
443
|
+
rustEntries: readRustCache(cacheFile), log,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
log(`warning: could not generate src/_generated/api.rs: ${e?.message ?? e}`);
|
|
448
|
+
}
|
|
449
|
+
const prefix = opts.groupFeaturePrefix ?? GROUP_FEATURE_PREFIX;
|
|
450
|
+
let groups = opts.groups ?? [];
|
|
451
|
+
// Default: one wasm per module when the crate gates its modules with m-<module> features.
|
|
452
|
+
let auto = false;
|
|
453
|
+
if (groups.length === 0 && opts.perModule !== false) {
|
|
454
|
+
const mods = modulesFromFeatures(layout0.features, prefix);
|
|
455
|
+
if (mods.length > 0) {
|
|
456
|
+
groups = perModuleGroups(mods);
|
|
457
|
+
auto = true;
|
|
458
|
+
log(`one wasm per module (${mods.length} ${prefix}<module> features); --single builds one shared wasm`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
let targetDir = layout0.targetDir;
|
|
462
|
+
let libName = layout0.libName;
|
|
463
|
+
const sdkEnv = await prepareWasiSdk({
|
|
464
|
+
mode: opts.wasiSdk ?? "auto",
|
|
465
|
+
cNames: () => cBuildCrates(crateDir),
|
|
466
|
+
log,
|
|
467
|
+
});
|
|
468
|
+
/** One cargo build; `featureArgs` select the cargo features, `rustcArgs` (zero-config split) switch to `cargo rustc`. */
|
|
469
|
+
const compileWasm = async (label, featureArgs, rustcArgs) => {
|
|
470
|
+
log(`Building Rust functions${label} (${manifestPath}, ${opts.profile === "dev" ? "dev profile" : "release"})...`);
|
|
471
|
+
const r = await runCargo(crateDir, [
|
|
472
|
+
rustcArgs ? "rustc" : "build", ...(rustcArgs ? ["--lib"] : []), ...cfg.args, ...simdCfg.args, ...featureArgs,
|
|
473
|
+
"--target", RUST_TARGET, "--manifest-path", manifestPath, ...(rustcArgs ? ["--", ...rustcArgs] : []),
|
|
474
|
+
], true, { ...cfg.env, ...simdCfg.env, ...sdkEnv });
|
|
475
|
+
if (r.error?.code === "ENOENT") {
|
|
476
|
+
throw new BuildError(`\`cargo\` was not found. Install Rust from https://rustup.rs, then run:\n rustup target add ${RUST_TARGET}`);
|
|
477
|
+
}
|
|
478
|
+
if (r.status !== 0) {
|
|
479
|
+
const hints = await buildFailureHints(crateDir, r.stderr);
|
|
480
|
+
throw new BuildError(`Rust build failed (cargo exited with ${r.status}).\nIf the error mentions a missing target, run:\n rustup target add ${RUST_TARGET}${hints}`);
|
|
481
|
+
}
|
|
482
|
+
({ targetDir, libName } = await findCrateLayout(crateDir));
|
|
483
|
+
const built = path.join(targetDir, RUST_TARGET, cargoProfileDir(opts.profile), `${libName}.wasm`);
|
|
484
|
+
if (!fs.existsSync(built)) {
|
|
485
|
+
throw new BuildError(`Build succeeded but ${built} was not found. Make sure Cargo.toml has crate-type = ["cdylib"] in [lib].`);
|
|
486
|
+
}
|
|
487
|
+
return new Uint8Array(fs.readFileSync(built));
|
|
488
|
+
};
|
|
489
|
+
/** wasm-opt (release) and the size report for a compiled wasm. */
|
|
490
|
+
const fnNames = new Map();
|
|
491
|
+
const hintsSeen = new Set();
|
|
492
|
+
const finishWasm = (label, raw) => {
|
|
493
|
+
let bytes = raw;
|
|
494
|
+
let names = new Map();
|
|
495
|
+
try {
|
|
496
|
+
names = parseNameSection(raw);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// best effort
|
|
500
|
+
}
|
|
501
|
+
if (opts.profile === "release") {
|
|
502
|
+
bytes = optimizeWasm(bytes, path.join(targetDir, "convex-rust-wasm-opt-cache"), (m) => log(m), (n) => (names = n));
|
|
503
|
+
}
|
|
504
|
+
fnNames.set(bytes, names);
|
|
505
|
+
try {
|
|
506
|
+
for (const msg of formatWasmHints(wasmHints(bytes), label)) {
|
|
507
|
+
const key = msg.replace(/^warning: \[[^\]]*\]/, "warning:");
|
|
508
|
+
if (!hintsSeen.has(key)) {
|
|
509
|
+
hintsSeen.add(key);
|
|
510
|
+
log(msg);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// hints are best effort
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const { message, warning } = describeWasmSize(bytes);
|
|
519
|
+
log(`${label.trim() ? label.trim() + ": " : ""}${message}`);
|
|
520
|
+
if (warning)
|
|
521
|
+
log(`warning: ${warning}`);
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// size report is best effort
|
|
525
|
+
}
|
|
526
|
+
return bytes;
|
|
527
|
+
};
|
|
528
|
+
const buildWasm = async (label, featureArgs) => finishWasm(label, await compileWasm(label, featureArgs));
|
|
529
|
+
const parse = (bytes) => {
|
|
530
|
+
try {
|
|
531
|
+
return parseManifest(bytes);
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
throw new BuildError(`Rust build failed: ${e?.message ?? e}`);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
let wasm = new Uint8Array();
|
|
538
|
+
let entries = [];
|
|
539
|
+
let grouped;
|
|
540
|
+
let fullRaw;
|
|
541
|
+
if (groups.length === 0 && opts.perModule !== false && (opts.profile === "release" || opts.perModule === true)) {
|
|
542
|
+
// Zero-config split: build everything once to learn the modules, then once per module with only that
|
|
543
|
+
// module's exports kept (cfg from the #[action] macro); the linker drops the rest.
|
|
544
|
+
fullRaw = await compileWasm("", []);
|
|
545
|
+
const all = parse(fullRaw);
|
|
546
|
+
const byModule = new Map();
|
|
547
|
+
for (const e of all) {
|
|
548
|
+
const m = convexModuleName(e.modulePath);
|
|
549
|
+
byModule.set(m, [...(byModule.get(m) ?? []), e]);
|
|
550
|
+
}
|
|
551
|
+
assertDistinctFileNames(byModule.keys());
|
|
552
|
+
if (byModule.size >= 2) {
|
|
553
|
+
const t0 = Date.now();
|
|
554
|
+
log(`one wasm per module (${byModule.size} modules, no cargo features needed); --single builds one shared wasm`);
|
|
555
|
+
grouped = { wasm: new Map(), moduleGroup: new Map() };
|
|
556
|
+
const members = new Map(); // group name -> modules
|
|
557
|
+
const sizes = new Map();
|
|
558
|
+
const build = async (name, mods) => {
|
|
559
|
+
const fns = mods.flatMap((m) => byModule.get(m).map((e) => e.name));
|
|
560
|
+
const bytes = finishWasm(` [${name}]`, await compileWasm(` [${name}]`, [], splitRustcArgs(fns)));
|
|
561
|
+
const got = new Set(parse(bytes).map((e) => `${convexModuleName(e.modulePath)}:${e.name}`));
|
|
562
|
+
for (const m of mods) {
|
|
563
|
+
for (const e of byModule.get(m)) {
|
|
564
|
+
if (!got.has(`${m}:${e.name}`))
|
|
565
|
+
throw new BuildError(`split build [${name}]: ${m}:${e.name} is missing from its wasm.`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
grouped.wasm.set(name, bytes);
|
|
569
|
+
members.set(name, mods);
|
|
570
|
+
sizes.set(name, bytes.length);
|
|
571
|
+
};
|
|
572
|
+
for (const m of [...byModule.keys()].sort())
|
|
573
|
+
await build(moduleFileName(m), [m]);
|
|
574
|
+
let pair;
|
|
575
|
+
let warned = false;
|
|
576
|
+
while ([...sizes.values()].reduce((a, b) => a + b, 0) > (opts.wasmBudget ?? WASM_BUDGET_BYTES) &&
|
|
577
|
+
(pair = smallestTwo(sizes))) {
|
|
578
|
+
if (!warned) {
|
|
579
|
+
log(`warning: the per-module wasm total ${formatBytes([...sizes.values()].reduce((a, b) => a + b, 0))}, over the ${formatBytes(opts.wasmBudget ?? WASM_BUDGET_BYTES)} budget (Convex allows 32 MiB of bundled code); merging the smallest modules`);
|
|
580
|
+
warned = true;
|
|
581
|
+
}
|
|
582
|
+
const [a, b] = pair;
|
|
583
|
+
const mods = [...members.get(a), ...members.get(b)];
|
|
584
|
+
for (const n of [a, b]) {
|
|
585
|
+
grouped.wasm.delete(n);
|
|
586
|
+
members.delete(n);
|
|
587
|
+
sizes.delete(n);
|
|
588
|
+
}
|
|
589
|
+
await build(`${a}_${b}`.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 60), mods);
|
|
590
|
+
}
|
|
591
|
+
for (const [name, mods] of members)
|
|
592
|
+
for (const m of mods)
|
|
593
|
+
grouped.moduleGroup.set(m, name);
|
|
594
|
+
entries = all;
|
|
595
|
+
log(`built ${grouped.wasm.size} wasm in ${((Date.now() - t0) / 1000).toFixed(1)} s`);
|
|
596
|
+
const total = [...grouped.wasm.values()].reduce((a, b) => a + b.length, 0);
|
|
597
|
+
log(`modules: ${[...grouped.wasm].map(([n, b]) => `${n} ${formatBytes(b.length)}`).join(", ")}, ${formatBytes(total)} in total`);
|
|
598
|
+
wasm = grouped.wasm.values().next().value;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (grouped) {
|
|
602
|
+
// zero-config split done
|
|
603
|
+
}
|
|
604
|
+
else if (groups.length > 0) {
|
|
605
|
+
grouped = { wasm: new Map(), moduleGroup: new Map() };
|
|
606
|
+
const t0 = Date.now();
|
|
607
|
+
for (const g of groups) {
|
|
608
|
+
const bytes = await buildWasm(` [${g.name}]`, groupFeatureArgs(g, prefix));
|
|
609
|
+
let ge = parse(bytes);
|
|
610
|
+
const have = new Set(ge.map((e) => convexModuleName(e.modulePath)));
|
|
611
|
+
const missing = g.modules.filter((m) => !have.has(m));
|
|
612
|
+
if (auto) {
|
|
613
|
+
// A feature without functions (a helper) gets no wasm. Modules the features do not gate show up in
|
|
614
|
+
// every build; the first build that has them keeps them.
|
|
615
|
+
if (missing.length === g.modules.length) {
|
|
616
|
+
log(`${g.name}: no functions; skipping its wasm`);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
ge = ge.filter((e) => {
|
|
620
|
+
const m = convexModuleName(e.modulePath);
|
|
621
|
+
return g.modules.includes(m) || !grouped.moduleGroup.has(m);
|
|
622
|
+
});
|
|
623
|
+
for (const e of ge) {
|
|
624
|
+
const m = convexModuleName(e.modulePath);
|
|
625
|
+
if (!grouped.moduleGroup.has(m))
|
|
626
|
+
grouped.moduleGroup.set(m, g.name);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
if (missing.length) {
|
|
631
|
+
throw new BuildError(`group ${g.name}: no functions for module(s) ${missing.join(", ")}; check the crate has features ${missing.map((m) => prefix + m.replace(/\//g, "-")).join(", ")}.`);
|
|
632
|
+
}
|
|
633
|
+
for (const m of have) {
|
|
634
|
+
if (!g.modules.includes(m)) {
|
|
635
|
+
throw new BuildError(`group ${g.name}: its wasm also contains module ${m}, which is not listed in the group (feature ${prefix}${m} should gate it).`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
for (const m of g.modules)
|
|
639
|
+
grouped.moduleGroup.set(m, g.name);
|
|
640
|
+
}
|
|
641
|
+
grouped.wasm.set(g.name, bytes);
|
|
642
|
+
entries.push(...ge);
|
|
643
|
+
}
|
|
644
|
+
log(`built ${grouped.wasm.size} wasm in ${((Date.now() - t0) / 1000).toFixed(1)} s`);
|
|
645
|
+
const total = [...grouped.wasm.values()].reduce((a, b) => a + b.length, 0);
|
|
646
|
+
log(`groups: ${[...grouped.wasm].map(([n, b]) => `${n} ${formatBytes(b.length)}`).join(", ")}, ${formatBytes(total)} in total`);
|
|
647
|
+
wasm = grouped.wasm.values().next().value;
|
|
648
|
+
}
|
|
649
|
+
else if (fullRaw) {
|
|
650
|
+
wasm = finishWasm("", fullRaw);
|
|
651
|
+
entries = parse(wasm);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
wasm = await buildWasm("", []);
|
|
655
|
+
entries = parse(wasm);
|
|
656
|
+
}
|
|
657
|
+
writeRustCache(cacheFile, entries);
|
|
658
|
+
try {
|
|
659
|
+
await updateApiRs({ projectRoot: opts.cwd, functionsDir, crateDir, outDir, rustEntries: entries, log });
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// best effort: the pre-build pass already ran
|
|
663
|
+
}
|
|
664
|
+
const wasmFile = `${libName}.wasm`;
|
|
665
|
+
let split;
|
|
666
|
+
if (opts.split && grouped) {
|
|
667
|
+
log("--split is ignored when building one wasm per module / group");
|
|
668
|
+
}
|
|
669
|
+
else if (opts.split) {
|
|
670
|
+
if (opts.profile !== "release") {
|
|
671
|
+
log("--split only applies to the release profile; using one shared wasm");
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
const modulePaths = [...new Set(entries.map((e) => e.modulePath))];
|
|
675
|
+
const res = splitWasmByModule(wasm, modulePaths, {
|
|
676
|
+
wasmOptArgs: wasmOptFlags(),
|
|
677
|
+
cacheDir: path.join(targetDir, "convex-rust-wasm-opt-cache"),
|
|
678
|
+
log,
|
|
679
|
+
});
|
|
680
|
+
if (res && res.size > 0) {
|
|
681
|
+
split = res;
|
|
682
|
+
const total = [...res.values()].reduce((a, b) => a + b.length, 0);
|
|
683
|
+
log(`split: ${res.size} wasm (${[...res].map(([m, b]) => `${m} ${formatBytes(b.length)}`).join(", ")}), ${formatBytes(total)} in total`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
let result;
|
|
688
|
+
try {
|
|
689
|
+
result = writeOutput(outDir, wasmFile, wasm, entries, functionsDir, opts.env, split, grouped);
|
|
690
|
+
}
|
|
691
|
+
catch (e) {
|
|
692
|
+
if (e instanceof BuildError)
|
|
693
|
+
throw e;
|
|
694
|
+
throw new BuildError(`Could not generate the wrappers: ${e?.message ?? e}`);
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
const stem = wasmFile.replace(/\.wasm$/, "");
|
|
698
|
+
const maps = [];
|
|
699
|
+
if (grouped) {
|
|
700
|
+
for (const [group, bytes] of grouped.wasm) {
|
|
701
|
+
const modules = [...grouped.moduleGroup].filter(([, g]) => g === group).map(([m]) => m);
|
|
702
|
+
const names = fnNames.get(bytes);
|
|
703
|
+
if (names?.size)
|
|
704
|
+
maps.push(buildSymbolMap(names, { wasm: `${stem}.${group}.wasm`, modules }));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
else if (!split) {
|
|
708
|
+
const names = fnNames.get(wasm);
|
|
709
|
+
const modules = [...new Set(entries.map((e) => convexModuleName(e.modulePath)))];
|
|
710
|
+
if (names?.size)
|
|
711
|
+
maps.push(buildSymbolMap(names, { wasm: wasmFile, modules }));
|
|
712
|
+
}
|
|
713
|
+
if (maps.length > 0) {
|
|
714
|
+
writeSymbolMaps(symbolsDir(targetDir), maps);
|
|
715
|
+
log(`function names for \`convex-rust symbolize\` (not deployed): ${symbolsDir(targetDir)}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
catch (e) {
|
|
719
|
+
log(`warning: could not write the symbol map: ${e?.message ?? e}`);
|
|
720
|
+
}
|
|
721
|
+
const functions = entries.map((e) => `${e.modulePath.split("::").slice(1).join("/") || "lib"}:${e.name}`);
|
|
722
|
+
log(`convex-rust: ${functions.length} function(s) -> ${path.relative(opts.cwd, outDir) || "."} ` +
|
|
723
|
+
`(${result.written.length} file(s) written${result.removed.length ? `, ${result.removed.length} removed` : ""})`);
|
|
724
|
+
return { ...result, wasmFile, functions, outDir };
|
|
725
|
+
}
|
|
726
|
+
const WATCH_FILES = new Set(["Cargo.toml", "Cargo.lock", "rust-toolchain", "rust-toolchain.toml", "config.toml"]);
|
|
727
|
+
const IGNORED_PARTS = new Set(["target", "_wasm", "node_modules", ".git", "_generated"]);
|
|
728
|
+
/** Should a change to this crate-relative path trigger a rebuild? */
|
|
729
|
+
export function isWatchedChange(relPath) {
|
|
730
|
+
const parts = relPath.split(/[\\/]/);
|
|
731
|
+
if (parts.some((p) => IGNORED_PARTS.has(p)))
|
|
732
|
+
return false;
|
|
733
|
+
const base = parts[parts.length - 1];
|
|
734
|
+
return base.endsWith(".rs") || WATCH_FILES.has(base);
|
|
735
|
+
}
|
|
736
|
+
/** Should a change to this functions-dir-relative path re-run the api.rs codegen? */
|
|
737
|
+
export function isTsSourceChange(relPath) {
|
|
738
|
+
const parts = relPath.split(/[\\/]/);
|
|
739
|
+
if (parts.some((p) => IGNORED_PARTS.has(p) || p.startsWith(".")))
|
|
740
|
+
return false;
|
|
741
|
+
const base = parts[parts.length - 1];
|
|
742
|
+
return /\.(ts|tsx|js|jsx|mjs|mts)$/.test(base) && !base.endsWith(".d.ts");
|
|
743
|
+
}
|
|
744
|
+
function hashFile(p) {
|
|
745
|
+
try {
|
|
746
|
+
return createHash("sha1").update(fs.readFileSync(p)).digest("hex");
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/** Content hashes of every watched file below `dir` (crate-relative keys). */
|
|
753
|
+
function snapshot(dir, base = dir, out = new Map()) {
|
|
754
|
+
let entries;
|
|
755
|
+
try {
|
|
756
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
for (const e of entries) {
|
|
762
|
+
const p = path.join(dir, e.name);
|
|
763
|
+
if (e.isDirectory()) {
|
|
764
|
+
if (!IGNORED_PARTS.has(e.name))
|
|
765
|
+
snapshot(p, base, out);
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
const rel = path.relative(base, p);
|
|
769
|
+
if (isWatchedChange(rel)) {
|
|
770
|
+
const h = hashFile(p);
|
|
771
|
+
if (h)
|
|
772
|
+
out.set(rel, h);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return out;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Initial build, then rebuild on changes to the crate sources (and re-run the api.rs codegen on
|
|
780
|
+
* TypeScript changes). Events are debounced, and only a real content change versus the state
|
|
781
|
+
* after the last build triggers a rebuild, so the build's own writes (Cargo.lock, the managed
|
|
782
|
+
* config block, api.rs) never loop. Never resolves.
|
|
783
|
+
*/
|
|
784
|
+
export async function watch(opts) {
|
|
785
|
+
const log = opts.log ?? ((m) => console.error(m));
|
|
786
|
+
const functionsDir = resolveFunctionsDir(opts.cwd);
|
|
787
|
+
const crateDir = path.resolve(opts.cwd, opts.crate ?? path.join(functionsDir, "rust"));
|
|
788
|
+
const outDir = path.resolve(opts.cwd, opts.out ?? path.join(functionsDir, "rust"));
|
|
789
|
+
let snap = new Map();
|
|
790
|
+
const build = async () => {
|
|
791
|
+
try {
|
|
792
|
+
await buildOnce(opts);
|
|
793
|
+
}
|
|
794
|
+
catch (e) {
|
|
795
|
+
log(`error: ${e?.message ?? e}`);
|
|
796
|
+
}
|
|
797
|
+
snap = snapshot(crateDir);
|
|
798
|
+
log("Watching for changes...");
|
|
799
|
+
};
|
|
800
|
+
const crateChanged = () => {
|
|
801
|
+
const now = snapshot(crateDir);
|
|
802
|
+
if (now.size !== snap.size)
|
|
803
|
+
return true;
|
|
804
|
+
for (const [k, h] of now)
|
|
805
|
+
if (snap.get(k) !== h)
|
|
806
|
+
return true;
|
|
807
|
+
return false;
|
|
808
|
+
};
|
|
809
|
+
await build();
|
|
810
|
+
let timer = null;
|
|
811
|
+
let running = false;
|
|
812
|
+
let tsDirty = false;
|
|
813
|
+
let pending = false;
|
|
814
|
+
const cacheFile = async () => path.join((await findCrateLayout(crateDir)).targetDir, "convex-rust-modules.json");
|
|
815
|
+
const cycle = async () => {
|
|
816
|
+
if (running) {
|
|
817
|
+
pending = true;
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
running = true;
|
|
821
|
+
try {
|
|
822
|
+
do {
|
|
823
|
+
pending = false;
|
|
824
|
+
let rebuild = crateChanged();
|
|
825
|
+
if (tsDirty) {
|
|
826
|
+
tsDirty = false;
|
|
827
|
+
try {
|
|
828
|
+
const changed = await updateApiRs({
|
|
829
|
+
projectRoot: opts.cwd, functionsDir, crateDir, outDir,
|
|
830
|
+
rustEntries: readRustCache(await cacheFile()), log,
|
|
831
|
+
});
|
|
832
|
+
if (changed) {
|
|
833
|
+
log("src/_generated/api.rs updated from the TypeScript functions");
|
|
834
|
+
rebuild = true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
catch (e) {
|
|
838
|
+
log(`warning: codegen failed: ${e?.message ?? e}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (rebuild) {
|
|
842
|
+
log("Change detected, rebuilding...");
|
|
843
|
+
await build();
|
|
844
|
+
}
|
|
845
|
+
} while (pending);
|
|
846
|
+
}
|
|
847
|
+
finally {
|
|
848
|
+
running = false;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const trigger = () => {
|
|
852
|
+
if (timer)
|
|
853
|
+
clearTimeout(timer);
|
|
854
|
+
timer = setTimeout(() => void cycle(), 300);
|
|
855
|
+
};
|
|
856
|
+
const within = (p, dir) => {
|
|
857
|
+
const r = path.relative(dir, p);
|
|
858
|
+
return !r.startsWith("..") && !path.isAbsolute(r);
|
|
859
|
+
};
|
|
860
|
+
fs.watch(crateDir, { recursive: true }, (_event, name) => {
|
|
861
|
+
if (name && isWatchedChange(name.toString()))
|
|
862
|
+
trigger();
|
|
863
|
+
});
|
|
864
|
+
{
|
|
865
|
+
try {
|
|
866
|
+
fs.watch(functionsDir, { recursive: true }, (_event, name) => {
|
|
867
|
+
if (!name)
|
|
868
|
+
return;
|
|
869
|
+
const abs = path.join(functionsDir, name.toString());
|
|
870
|
+
if (within(abs, crateDir) || within(abs, outDir))
|
|
871
|
+
return;
|
|
872
|
+
if (isTsSourceChange(name.toString())) {
|
|
873
|
+
tsDirty = true;
|
|
874
|
+
trigger();
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
// functions dir watch is best effort
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return await new Promise(() => { });
|
|
883
|
+
}
|
|
884
|
+
/** Regenerates only `src/_generated/api.rs` (no cargo). Needs a previous build for the Rust functions. */
|
|
885
|
+
export async function codegenOnly(opts) {
|
|
886
|
+
const log = opts.log ?? ((m) => console.error(m));
|
|
887
|
+
const functionsDir = resolveFunctionsDir(opts.cwd);
|
|
888
|
+
const crateDir = path.resolve(opts.cwd, opts.crate ?? path.join(functionsDir, "rust"));
|
|
889
|
+
const outDir = path.resolve(opts.cwd, opts.out ?? path.join(functionsDir, "rust"));
|
|
890
|
+
if (!fs.existsSync(path.join(crateDir, "Cargo.toml"))) {
|
|
891
|
+
throw new BuildError(`No Rust crate found: ${path.join(crateDir, "Cargo.toml")} does not exist.`);
|
|
892
|
+
}
|
|
893
|
+
const { targetDir } = await findCrateLayout(crateDir);
|
|
894
|
+
const file = path.join(crateDir, "src", "_generated", "api.rs");
|
|
895
|
+
const before = fs.existsSync(file);
|
|
896
|
+
const changed = await updateApiRs({
|
|
897
|
+
projectRoot: opts.cwd, functionsDir, crateDir, outDir,
|
|
898
|
+
rustEntries: readRustCache(path.join(targetDir, "convex-rust-modules.json")), log,
|
|
899
|
+
});
|
|
900
|
+
log(changed ? "convex-rust: updated src/_generated/api.rs" : before ? "convex-rust: src/_generated/api.rs is up to date" : "convex-rust: nothing to generate (no api.rs and no `api::` use)");
|
|
901
|
+
return changed;
|
|
902
|
+
}
|