skip to content
==> 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}