==> src/build.c <==1/* The program catalog: for each program, its images under <out>/<name>/ beside
2 * its program.toml -- the GPU image <name>.ptx (the sexc ptx package's bitcode
3 * linked by mesh-ptx-link in the cluster float mode, then checked: `.target
4 * <arch>` exactly, the float rules, mesh_main, MESH_RT_ABI), the wasm image
5 * <name>.wasm (checked for its mesh_init, mesh_task, mesh_rt_abi exports), a
6 * native program's binary <triple>/<name> and its source.hash -- then its
7 * variants (programs/<name>/variants/<v>.toml: other names for the same images).
8 * Everything lands by rename (tmp then mv), so a running meshd never reads half
9 * a file. Ported from alpha-rust-mesh scripts/build-programs.sh. */
10#include "build.h"
11
12#include "mesh/catalog_toml.h"
13#include "mesh/ptx_float_rules.h"
14#include "mesh/source_hash.h"
15
16#include <dirent.h>
17#include <errno.h>
18#include <stdarg.h>
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22#include <sys/stat.h>
23#include <sys/utsname.h>
24#include <sys/wait.h>
25#include <unistd.h>
26
27#define MBP_MAX 256
28
29static char *fmt(const char *f, ...) {
30 va_list a, b;
31 va_start(a, f);
32 va_copy(b, a);
33 int n = vsnprintf(NULL, 0, f, a);
34 va_end(a);
35 char *s = malloc((size_t)n + 1);
36 if (!s) abort();
37 vsnprintf(s, (size_t)n + 1, f, b);
38 va_end(b);
39 return s;
40}
41
42static int fail(const char *f, ...) {
43 va_list a;
44 va_start(a, f);
45 fputs("error: ", stderr);
46 vfprintf(stderr, f, a);
47 fputc('\n', stderr);
48 va_end(a);
49 return 1;
50}
51
52static char *slurp(const char *path, size_t *len) {
53 FILE *f = fopen(path, "rb");
54 if (!f) return NULL;
55 size_t cap = 1 << 16, n = 0;
56 char *b = malloc(cap + 1);
57 for (;;) {
58 if (!b) break;
59 n += fread(b + n, 1, cap - n, f);
60 if (n < cap) break;
61 cap *= 2;
62 char *nb = realloc(b, cap + 1);
63 if (!nb) { free(b); b = NULL; break; }
64 b = nb;
65 }
66 bool bad = ferror(f);
67 fclose(f);
68 if (!b || bad) { free(b); return NULL; }
69 b[n] = 0;
70 if (len) *len = n;
71 return b;
72}
73
74static bool is_file(const char *p) {
75 struct stat st;
76 return stat(p, &st) == 0 && S_ISREG(st.st_mode);
77}
78static bool mkdirs(const char *p) {
79 char *c = fmt("%s", p);
80 for (char *s = c + 1; *s; s++)
81 if (*s == '/') {
82 *s = 0;
83 mkdir(c, 0755);
84 *s = '/';
85 }
86 bool ok = mkdir(c, 0755) == 0 || errno == EEXIST;
87 free(c);
88 return ok;
89}
90/* cp src dst.tmp && mv dst.tmp dst, keeping the executable bit. */
91static bool install(const char *src, const char *dst) {
92 size_t n;
93 char *b = slurp(src, &n);
94 if (!b) return false;
95 struct stat st;
96 mode_t mode = stat(src, &st) == 0 ? (st.st_mode & 0777) : 0644;
97 char *tmp = fmt("%s.tmp", dst);
98 FILE *f = fopen(tmp, "wb");
99 bool ok = f && fwrite(b, 1, n, f) == n;
100 if (f) ok = (fclose(f) == 0) && ok;
101 ok = ok && chmod(tmp, mode) == 0 && rename(tmp, dst) == 0;
102 free(tmp);
103 free(b);
104 return ok;
105}
106
107char *mbp_pkg_id(const char *name) {
108 char *s = fmt("%s", name[0] == '@' ? name + 1 : name);
109 for (char *p = s; *p; p++)
110 if (*p == '/') *p = '-';
111 return s;
112}
113
114bool mbp_valid_arch(const char *a) {
115 if (strncmp(a, "sm_", 3) != 0) return false;
116 size_t i = 3, d = 0;
117 while (a[i] >= '0' && a[i] <= '9') i++, d++;
118 if (d < 2 || d > 3) return false;
119 if (a[i] == 'a' || a[i] == 'f') i++;
120 return a[i] == 0;
121}
122
123const char *mbp_triple(void) {
124#if defined(__APPLE__) && defined(__aarch64__)
125 return "aarch64-apple-darwin";
126#elif defined(__APPLE__)
127 return "x86_64-apple-darwin";
128#elif defined(__aarch64__)
129 return "aarch64-unknown-linux-gnu";
130#elif defined(__x86_64__)
131 return "x86_64-unknown-linux-gnu";
132#else
133 return "unknown";
134#endif
135}
136
137char *mbp_json_field(const char *json, const char *key) {
138 char *pat = fmt("\"%s\"", key);
139 const char *p = strstr(json, pat);
140 free(pat);
141 if (!p) return NULL;
142 p += strlen(key) + 2;
143 while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
144 if (*p != ':') return NULL;
145 p++;
146 while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
147 if (*p != '"') return NULL;
148 const char *e = strchr(p + 1, '"');
149 if (!e) return NULL;
150 return fmt("%.*s", (int)(e - p - 1), p + 1);
151}
152
153/* Runs argv (inheriting stdout and stderr) with one extra environment entry; its exit status. */
154static int run(char *const *argv, const char *env) {
155 fflush(stdout);
156 fflush(stderr);
157 pid_t pid = fork();
158 if (pid < 0) return fail("fork: %s", strerror(errno));
159 if (pid == 0) {
160 if (env) putenv((char *)env);
161 execvp(argv[0], argv);
162 fprintf(stderr, "error: %s: %s\n", argv[0], strerror(errno));
163 _exit(127);
164 }
165 int st;
166 while (waitpid(pid, &st, 0) < 0)
167 if (errno != EINTR) return 1;
168 return WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st);
169}
170
171/* ---- the programs under <root>/programs */
172typedef struct {
173 char *name; /* program.toml's name */
174 char *toml_dir; /* programs/<dir> holding the manifest (a -wasm dir only for a wasm-only program) */
175 char *toml; /* its text */
176 char *gpu, *native, *wasm; /* the sexc package of each image */
177 char *native_bin;
178} program;
179
180typedef struct {
181 program v[MBP_MAX];
182 size_t n;
183} programs;
184
185static program *find_prog(programs *ps, const char *name) {
186 for (size_t i = 0; i < ps->n; i++)
187 if (strcmp(ps->v[i].name, name) == 0) return &ps->v[i];
188 if (ps->n == MBP_MAX) return NULL;
189 program *p = &ps->v[ps->n++];
190 memset(p, 0, sizeof *p);
191 p->name = fmt("%s", name);
192 return p;
193}
194
195static int by_name(const void *a, const void *b) { return strcmp(*(char *const *)a, *(char *const *)b); }
196
197static int scan(const mbp_opts *o, programs *ps) {
198 char *dir = fmt("%s/programs", o->root);
199 DIR *d = opendir(dir);
200 if (!d) { int r = fail("no programs under %s", dir); free(dir); return r; }
201 char *names[MBP_MAX];
202 size_t nn = 0;
203 struct dirent *e;
204 while ((e = readdir(d)) && nn < MBP_MAX)
205 if (e->d_name[0] != '.') names[nn++] = fmt("%s", e->d_name);
206 closedir(d);
207 qsort(names, nn, sizeof *names, by_name);
208 int rc = 0;
209 for (size_t i = 0; i < nn && rc == 0; i++) {
210 char *toml_path = fmt("%s/%s/program.toml", dir, names[i]), *json_path = fmt("%s/%s/sexc.json", dir, names[i]);
211 if (is_file(toml_path) && is_file(json_path)) {
212 char *toml = slurp(toml_path, NULL), *json = slurp(json_path, NULL), nm[64];
213 if (!toml || !json || !mesh_catalog_toml_name(toml, nm, sizeof nm)) {
214 rc = fail("programs/%s/program.toml: name must be \"%s\"", names[i], names[i]);
215 } else {
216 size_t dl = strlen(names[i]), nl = strlen(nm);
217 bool own = strcmp(nm, names[i]) == 0, wasm_dir = dl == nl + 5 && strncmp(names[i], nm, nl) == 0 && strcmp(names[i] + nl, "-wasm") == 0;
218 if (!own && !wasm_dir) rc = fail("programs/%s/program.toml: name must be \"%s\"", names[i], names[i]);
219 program *p = rc ? NULL : find_prog(ps, nm);
220 char *pkg = mbp_json_field(json, "name"), *kind = mbp_json_field(json, "kind");
221 if (p && pkg) {
222 if (own || !p->toml_dir) {
223 free(p->toml_dir), free(p->toml);
224 p->toml_dir = fmt("programs/%s", names[i]);
225 p->toml = toml;
226 toml = NULL;
227 }
228 if (kind && strcmp(kind, "ptx") == 0) p->gpu = fmt("%s", pkg);
229 else if (kind && strcmp(kind, "wasm") == 0) p->wasm = fmt("%s", pkg);
230 else if (kind && strcmp(kind, "executable") == 0) {
231 p->native = fmt("%s", pkg);
232 char *bin = mbp_json_field(json, "bin");
233 p->native_bin = bin ? bin : fmt("%s", strchr(pkg, '/') ? strchr(pkg, '/') + 1 : pkg);
234 }
235 }
236 free(pkg), free(kind);
237 }
238 free(toml), free(json);
239 }
240 free(toml_path), free(json_path);
241 }
242 for (size_t i = 0; i < nn; i++) free(names[i]);
243 free(dir);
244 if (rc == 0 && ps->n == 0) rc = fail("no programs under programs/");
245 return rc;
246}
247
248static int sexc_build(const mbp_opts *o, const char *pkg) {
249 char *env = fmt("SEXC_PTX_ARCH=%s", o->arch);
250 char *argv[] = {(char *)o->sexc, (char *)"build", (char *)"-C", (char *)o->root, (char *)"--release", (char *)pkg, NULL};
251 int r = run(argv, env);
252 /* putenv keeps the string in the child only; ours is ours to free. */
253 free(env);
254 return r == 0 ? 0 : fail("sexc build %s failed", pkg);
255}
256
257/* variants NAME: programs/<dir>/variants/<v>.toml run NAME's images under the name v. */
258static int variants(const mbp_opts *o, const program *p) {
259 char *vdir = fmt("%s/%s/variants", o->root, p->toml_dir);
260 DIR *d = opendir(vdir);
261 if (!d) { free(vdir); return 0; }
262 char *names[MBP_MAX];
263 size_t nn = 0;
264 struct dirent *e;
265 while ((e = readdir(d)) && nn < MBP_MAX) {
266 size_t k = strlen(e->d_name);
267 if (k > 5 && strcmp(e->d_name + k - 5, ".toml") == 0) names[nn++] = fmt("%.*s", (int)(k - 5), e->d_name);
268 }
269 closedir(d);
270 qsort(names, nn, sizeof *names, by_name);
271 int rc = 0;
272 for (size_t i = 0; i < nn && rc == 0; i++) {
273 const char *vn = names[i];
274 char *vt = fmt("%s/%s.toml", vdir, vn), *text = slurp(vt, NULL), nm[64];
275 if (!text || !mesh_catalog_toml_name(text, nm, sizeof nm) || strcmp(nm, vn) != 0) rc = fail("%s: name must be \"%s\"", vt, vn);
276 char *dst = fmt("%s/%s", o->out, vn);
277 if (!rc && !mkdirs(dst)) rc = fail("%s: %s", dst, strerror(errno));
278 const char *exts[2] = {"ptx", "wasm"};
279 for (int k = 0; k < 2 && !rc; k++) {
280 char *s = fmt("%s/%s/%s.%s", o->out, p->name, p->name, exts[k]), *t = fmt("%s/%s.%s", dst, vn, exts[k]);
281 if (is_file(s) && !install(s, t)) rc = fail("%s: %s", t, strerror(errno));
282 free(s), free(t);
283 }
284 /* A native program's binary (one per target triple) under the variant's name, and its source identity. */
285 char *src_dir = fmt("%s/%s", o->out, p->name);
286 DIR *sd = rc ? NULL : opendir(src_dir);
287 struct dirent *te;
288 while (sd && (te = readdir(sd))) {
289 if (te->d_name[0] == '.') continue;
290 char *b = fmt("%s/%s/%s", src_dir, te->d_name, p->name);
291 if (is_file(b)) {
292 char *td = fmt("%s/%s", dst, te->d_name), *tb = fmt("%s/%s/%s", dst, te->d_name, vn);
293 if (!mkdirs(td) || !install(b, tb)) rc = fail("%s: %s", tb, strerror(errno));
294 free(td), free(tb);
295 }
296 free(b);
297 }
298 if (sd) closedir(sd);
299 char *sh = fmt("%s/source.hash", src_dir), *th = fmt("%s/source.hash", dst), *tt = fmt("%s/program.toml", dst);
300 if (!rc && is_file(sh) && !install(sh, th)) rc = fail("%s: %s", th, strerror(errno));
301 if (!rc && !install(vt, tt)) rc = fail("%s: %s", tt, strerror(errno));
302 if (!rc) printf("%s: a variant of %s\n", dst, p->name);
303 free(sh), free(th), free(tt), free(src_dir), free(dst), free(vt), free(text);
304 }
305 for (size_t i = 0; i < nn; i++) free(names[i]);
306 free(vdir);
307 return rc;
308}
309
310static int write_toml(const mbp_opts *o, const program *p) {
311 char *src = fmt("%s/%s/program.toml", o->root, p->toml_dir), *dir = fmt("%s/%s", o->out, p->name), *dst = fmt("%s/program.toml", dir);
312 int rc = !mkdirs(dir) || !install(src, dst) ? fail("%s: %s", dst, strerror(errno)) : 0;
313 free(src), free(dir), free(dst);
314 return rc;
315}
316
317/* The wasm image: the sexc wasm package's module, checked for the runtime's exports. */
318static int build_wasm(const mbp_opts *o, const program *p) {
319 if (!p->wasm) return fail("%s/program.toml lists backend wasm, but programs/ has no wasm package for %s", p->toml_dir, p->name);
320 if (sexc_build(o, p->wasm)) return 1;
321 char *id = mbp_pkg_id(p->wasm), *wasm = fmt("%s/.sexc/out/release/wasm/%s.wasm", o->root, id);
322 size_t n;
323 char *b = slurp(wasm, &n);
324 int rc = b ? 0 : fail("%s: %s", wasm, strerror(errno));
325 const char *syms[3] = {"mesh_init", "mesh_task", "mesh_rt_abi"};
326 for (int i = 0; i < 3 && !rc; i++) {
327 size_t k = strlen(syms[i]);
328 bool found = false;
329 for (size_t j = 0; j + k <= n && !found; j++) found = memcmp(b + j, syms[i], k) == 0;
330 if (!found) rc = fail("%s exports no %s (MESH_PROGRAM missing?)", wasm, syms[i]);
331 }
332 char *dir = fmt("%s/%s", o->out, p->name), *dst = fmt("%s/%s.wasm", dir, p->name);
333 if (!rc && (!mkdirs(dir) || !install(wasm, dst))) rc = fail("%s: %s", dst, strerror(errno));
334 if (!rc) printf("%s: %zu bytes\n", dst, n);
335 free(b), free(id), free(wasm), free(dir), free(dst);
336 return rc;
337}
338
339/* The GPU image: link, check, install. */
340static int build_ptx(const mbp_opts *o, const program *p) {
341 if (!p->gpu) return fail("programs/%s has no ptx package (sexc.json \"kind\": \"ptx\")", p->name);
342 if (sexc_build(o, p->gpu)) return 1;
343 char *id = mbp_pkg_id(p->gpu), *bc = fmt("%s/.sexc/out/release/ptx/%s.bc", o->root, id);
344 char *dir = fmt("%s/%s", o->out, p->name), *tmp = fmt("%s/%s.link.ptx", dir, p->name), *dst = fmt("%s/%s.ptx", dir, p->name);
345 int rc = mkdirs(dir) ? 0 : fail("%s: %s", dir, strerror(errno));
346 if (!rc) {
347 char *argv[] = {(char *)o->ptx_link, (char *)"--mcpu", (char *)o->arch, (char *)"-O3", (char *)"-o", tmp, bc, NULL};
348 if (run(argv, NULL) != 0) rc = fail("mesh-ptx-link %s failed", bc);
349 }
350 size_t n = 0;
351 char *text = rc ? NULL : slurp(tmp, &n);
352 if (!rc && !text) rc = fail("%s: %s", tmp, strerror(errno));
353 if (!rc) {
354 /* Exactly `.target <arch>`: any `, debug` (unoptimised JIT) or another arch is refused. */
355 const char *t = strstr(text, "\n.target");
356 t = t ? t + 1 : (strncmp(text, ".target", 7) == 0 ? text : NULL);
357 size_t tl = t ? strcspn(t, "\n") : 0;
358 char *tline = fmt("%.*s", (int)tl, t ? t : "");
359 char *want = fmt(".target %s", o->arch);
360 if (strstr(tline, "debug")) rc = fail("%s: '%s' targets debug (unoptimised JIT)", tmp, tline);
361 else if (strcmp(tline, want) != 0) rc = fail("%s: '%s', want '%s'", tmp, tline, want);
362 free(tline), free(want);
363 }
364 /* The cluster float mode: refused, not rewritten. */
365 if (!rc && mesh_ptx_float_check(tmp, text, n, NULL) != 0) {
366 fprintf(stderr, "error: %s: float op outside the cluster float mode (FTZ, correctly rounded, unfused):\n", tmp);
367 mesh_ptx_float_check(tmp, text, n, stderr);
368 rc = 1;
369 }
370 if (!rc && !strstr(text, "\n.visible .entry mesh_main")) rc = fail("%s has no mesh_main entry (MESH_PROGRAM missing?)", tmp);
371 if (!rc && !strstr(text, "MESH_RT_ABI")) rc = fail("%s has no MESH_RT_ABI global", tmp);
372 if (!rc && rename(tmp, dst) != 0) rc = fail("%s: %s", dst, strerror(errno));
373 if (!rc) {
374 size_t lines = 0;
375 for (size_t i = 0; i < n; i++) lines += text[i] == '\n';
376 rc = write_toml(o, p);
377 if (!rc) printf("%s: %zu lines\n", dst, lines);
378 }
379 /* The linker's intermediates beside the output. */
380 const char *side[3] = {".link.o", ".link.optimized.o", ".link.symbols.txt"};
381 for (int i = 0; i < 3; i++) {
382 char *s = fmt("%s/%s%s", dir, p->name, side[i]);
383 unlink(s);
384 free(s);
385 }
386 unlink(tmp);
387 free(text), free(id), free(bc), free(dir), free(tmp), free(dst);
388 return rc;
389}
390
391/* The folders of a native program's source identity: the program's, the
392 * worker SDK's (packages/mesh-worker*), the contract's (packages/mesh-abi*),
393 * the references' (packages/mesh-ref*), and the manifest's `# hash-also:`. */
394static size_t hash_dirs(const mbp_opts *o, const program *p, const char *also, char **out, size_t cap) {
395 size_t n = 0;
396 out[n++] = fmt("%s", p->toml_dir);
397 char *pk = fmt("%s/packages", o->root);
398 DIR *d = opendir(pk);
399 struct dirent *e;
400 while (d && (e = readdir(d)) && n < cap)
401 if (strncmp(e->d_name, "mesh-worker", 11) == 0 || strncmp(e->d_name, "mesh-abi", 8) == 0 || strncmp(e->d_name, "mesh-ref", 8) == 0)
402 out[n++] = fmt("packages/%s", e->d_name);
403 if (d) closedir(d);
404 free(pk);
405 for (const char *a = also; *a && n < cap;) {
406 while (*a == ' ' || *a == '\n') a++;
407 size_t k = strcspn(a, " \n");
408 if (k) out[n++] = fmt("%.*s", (int)k, a);
409 a += k;
410 }
411 return n;
412}
413
414/* A native worker program for this machine's target. */
415static int build_native(const mbp_opts *o, const program *p) {
416 char *dir = fmt("%s/%s", o->out, p->name);
417 char also[512] = "", os[64] = "";
418 mesh_catalog_toml_comment(p->toml, "hash-also", also, sizeof also);
419 /* `# build-os: Darwin`: built on that OS only (elsewhere the catalog gets the manifest without a binary). */
420 struct utsname u;
421 bool other_os = mesh_catalog_toml_comment(p->toml, "build-os", os, sizeof os) && os[0] && (uname(&u) != 0 || strcmp(os, u.sysname) != 0);
422 int rc = 0;
423 char *bin = NULL;
424 if (!other_os) {
425 if (!p->native) { free(dir); return fail("programs/%s has no executable package", p->name); }
426 if (sexc_build(o, p->native)) { free(dir); return 1; }
427 char *src = fmt("%s/.sexc/out/release/bin/%s", o->root, p->native_bin), *td = fmt("%s/%s", dir, mbp_triple());
428 bin = fmt("%s/%s", td, p->name);
429 if (!mkdirs(td) || !install(src, bin)) rc = fail("%s: %s", bin, strerror(errno));
430 free(src), free(td);
431 }
432 if (!rc) rc = write_toml(o, p);
433 char *dirs[MBP_MAX];
434 size_t nd = rc ? 0 : hash_dirs(o, p, also, dirs, MBP_MAX);
435 char hash[17];
436 if (!rc && !mesh_source_hash(o->root, (const char *const *)dirs, nd, hash)) rc = fail("%s: reading the sources failed", p->toml_dir);
437 for (size_t i = 0; i < nd; i++) free(dirs[i]);
438 if (!rc) {
439 char *sh = fmt("%s/source.hash", dir), *tmp = fmt("%s.tmp", sh);
440 FILE *f = fopen(tmp, "w");
441 bool ok = f && fprintf(f, "%s\n", hash) > 0;
442 if (f) ok = fclose(f) == 0 && ok;
443 if (!ok || rename(tmp, sh) != 0) rc = fail("%s: %s", sh, strerror(errno));
444 free(sh), free(tmp);
445 }
446 if (!rc) {
447 if (other_os) printf("%s: native, %s only: no binary for %s\n", dir, os, mbp_triple());
448 else {
449 struct stat st;
450 printf("%s: native, %lld bytes, source %s\n", bin, stat(bin, &st) == 0 ? (long long)st.st_size : 0LL, hash);
451 }
452 rc = variants(o, p);
453 }
454 free(bin);
455 free(dir);
456 return rc;
457}
458
459int mbp_run(const mbp_opts *o, char *const *names, size_t n) {
460 if (!mbp_valid_arch(o->arch)) return fail("MESH_PTX_ARCH=%s: want sm_NN (e.g. sm_86, sm_121)", o->arch);
461 static programs ps;
462 memset(&ps, 0, sizeof ps);
463 int rc = scan(o, &ps);
464 char *const *want = names;
465 size_t nw = n;
466 char *all[MBP_MAX];
467 if (!rc && nw == 0) {
468 for (size_t i = 0; i < ps.n; i++) all[i] = ps.v[i].name;
469 want = all;
470 nw = ps.n;
471 }
472 for (size_t i = 0; i < nw && rc == 0; i++) {
473 program *p = NULL;
474 for (size_t k = 0; k < ps.n; k++)
475 if (strcmp(ps.v[k].name, want[i]) == 0) p = &ps.v[k];
476 if (!p || !p->toml) { rc = fail("programs/%s needs program.toml and sexc.json", want[i]); break; }
477 if (mesh_catalog_toml_native(p->toml)) {
478 if (!o->wasm_only) rc = build_native(o, p);
479 continue;
480 }
481 if (!o->native_only && !o->skip_wasm && mesh_catalog_toml_backend(p->toml, "wasm")) rc = build_wasm(o, p);
482 if (rc) break;
483 /* A wasm-only program (backends lists neither cuda nor metal) has no PTX. */
484 bool gpu = !(mesh_catalog_toml_has_backends(p->toml) && !mesh_catalog_toml_backend(p->toml, "cuda") && !mesh_catalog_toml_backend(p->toml, "metal"));
485 if (o->wasm_only || !gpu) {
486 rc = write_toml(o, p);
487 if (!rc) rc = variants(o, p);
488 continue;
489 }
490 if (o->native_only) continue;
491 rc = build_ptx(o, p);
492 if (!rc) rc = variants(o, p);
493 }
494 for (size_t i = 0; i < ps.n; i++) {
495 program *p = &ps.v[i];
496 free(p->name), free(p->toml_dir), free(p->toml), free(p->gpu), free(p->native), free(p->wasm), free(p->native_bin);
497 }
498 return rc;
499}