git mirror - github.com/owenewans/holy - branch master
clone: https://src.holypkg.eu/holy/

file src/recipe.c

/* recipe manifest parsing and the build runner; see man/holy-recipe.5 */
#define _XOPEN_SOURCE 700
#include "recipe.h"
#include "config.h"
#include "elf.h"
#include "fetch.h"
#include "pack.h"
#include "version.h"

#include <archive.h>
#include <archive_entry.h>
#include <openssl/evp.h>
#include <ctype.h>
#include <stdarg.h>
#include <stddef.h>
#include <elf.h>
#define _DEFAULT_SOURCE 1
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

struct recipe_source {
    char *name, *url, *hash;
};

struct recipe_depend {
    char *kind, *name, *relation, *version;
};

struct recipe_output {
    char *name, *kind;
};

struct recipe_split {
    char *output, *pattern;
};

struct recipe_hook {
    char *interpreter, *path;
};

struct recipe_config {
    char *path;
    int mutable;
};

struct recipe_step {
    char *phase, *tag, *body, *file, *output;
    char **argv;
    size_t argc;
    size_t length;
    size_t line;
};

struct recipe {
    char *format, *name, *version, *release, *arch, *libc, *summary;
    char *homepage, *license, *build_target, *extra;
    struct recipe_source *sources;
    size_t source_count;
    struct recipe_depend *depends;
    size_t depend_count;
    struct recipe_depend *build_depends;
    size_t build_depend_count;
    struct recipe_output *outputs;
    size_t output_count;
    struct recipe_split *splits;
    size_t split_count;
    struct recipe_hook *hook_install;
    size_t hook_install_count;
    struct recipe_hook *hook_remove;
    size_t hook_remove_count;
    struct recipe_config *configs;
    size_t config_count;
    struct recipe_step *steps;
    size_t step_count;
    char *text;
    size_t text_length;
};

static const char *const phases[] = {
    "fetch", "unpack", "prepare", "configure", "build", "check", "package", "split"
};

static int remove_tree(const char *path);

static void recipe_free(struct recipe *r)
{
    size_t i, j;
    free(r->format); free(r->name); free(r->version); free(r->release);
    free(r->arch); free(r->libc); free(r->summary); free(r->homepage);
    free(r->license); free(r->build_target); free(r->extra); free(r->text);
    for (i = 0; i < r->source_count; ++i) {
        free(r->sources[i].name); free(r->sources[i].url); free(r->sources[i].hash);
    }
    for (i = 0; i < r->depend_count; ++i) {
        free(r->depends[i].kind); free(r->depends[i].name);
        free(r->depends[i].relation); free(r->depends[i].version);
    }
    for (i = 0; i < r->build_depend_count; ++i) {
        free(r->build_depends[i].kind); free(r->build_depends[i].name);
        free(r->build_depends[i].relation); free(r->build_depends[i].version);
    }
    for (i = 0; i < r->output_count; ++i) {
        free(r->outputs[i].name); free(r->outputs[i].kind);
    }
    for (i = 0; i < r->split_count; ++i) {
        free(r->splits[i].output); free(r->splits[i].pattern);
    }
    for (i = 0; i < r->hook_install_count; ++i) {
        free(r->hook_install[i].interpreter); free(r->hook_install[i].path);
    }
    for (i = 0; i < r->hook_remove_count; ++i) {
        free(r->hook_remove[i].interpreter); free(r->hook_remove[i].path);
    }
    for (i = 0; i < r->config_count; ++i) free(r->configs[i].path);
    free(r->configs);
    for (i = 0; i < r->step_count; ++i) {
        free(r->steps[i].phase); free(r->steps[i].tag); free(r->steps[i].body);
        free(r->steps[i].file); free(r->steps[i].output);
        for (j = 0; j < r->steps[i].argc; ++j) free(r->steps[i].argv[j]);
        free(r->steps[i].argv);
    }
    free(r->sources); free(r->depends); free(r->build_depends);
    free(r->outputs); free(r->splits); free(r->hook_install);
    free(r->hook_remove); free(r->steps);
    memset(r, 0, sizeof *r);
}

static int label(const char *value, size_t limit)
{
    const unsigned char *p = (const unsigned char *)value;
    size_t n = value ? strlen(value) : 0;
    if (!n || n >= limit || !isalnum(*p)) return 0;
    for (; *p; ++p)
        if (!isalnum(*p) && *p != '.' && *p != '_' && *p != '+' && *p != '-') return 0;
    return 1;
}

static int digest(const char *value)
{
    return value && strlen(value) == 64 && strspn(value, "0123456789abcdef") == 64;
}

static int safe_path(const char *value)
{
    const char *p = value;
    if (!value || !*value || *value == '/') return 0;
    while (*p) {
        size_t length = strcspn(p, "/");
        if (!length || (length == 1 && *p == '.') || (length == 2 && p[0] == '.' && p[1] == '.'))
            return 0;
        p += length;
        if (*p == '/') ++p;
    }
    return 1;
}

static int problem(const char *file, size_t line, const char *fmt, ...)
{
    char message[512];
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(message, sizeof message, fmt, ap);
    va_end(ap);
    fprintf(stderr, "holypkg: %s:%zu: %s\n", file, line, message);
    return 2;
}

static char *joined(const char *directory, const char *name)
{
    size_t a = strlen(directory), b = strlen(name);
    char *out;
    if (a > SIZE_MAX - b - 2) return NULL;
    out = malloc(a + b + 2);
    if (!out) return NULL;
    memcpy(out, directory, a);
    out[a] = '/';
    memcpy(out + a + 1, name, b + 1);
    return out;
}

static int phase_index(const char *name)
{
    size_t i;
    for (i = 0; i < sizeof phases / sizeof *phases; ++i)
        if (!strcmp(phases[i], name)) return (int)i;
    return -1;
}

static int append_depend(struct recipe_depend **list, size_t *count,
                         char **tokens, size_t tokens_count)
{
    struct recipe_depend *grown = realloc(*list, (*count + 1) * sizeof **list);
    if (!grown) return 0;
    *list = grown;
    memset(&grown[*count], 0, sizeof grown[*count]);
    if (tokens_count == 2 && !strncmp(tokens[0], "cmd:", 4)) {
        grown[*count].kind = strdup("command");
        grown[*count].name = strdup(tokens[0] + 4);
    } else {
        grown[*count].kind = strdup("package");
        grown[*count].name = strdup(tokens[0]);
    }
    if (!grown[*count].kind || !grown[*count].name) return 0;
    if (tokens_count >= 3) {
        grown[*count].relation = strdup(tokens[1]);
        grown[*count].version = strdup(tokens[2]);
        if (!grown[*count].relation || !grown[*count].version) return 0;
    } else {
        grown[*count].relation = strdup("any");
        grown[*count].version = strdup("-");
        if (!grown[*count].relation || !grown[*count].version) return 0;
    }
    ++*count;
    return 1;
}

static int parse_step(struct recipe *recipe, const char *header, size_t length,
                      const char *body, size_t body_length, const char *file,
                      size_t line)
{
    char **tokens = NULL;
    size_t count = 0, i, block = 0, base = 1;
    char *error = NULL;
    struct recipe_step *step, *grown;
    int ok = 0;
    if (!holy_lex(header, length, &tokens, &count, file, line, &error)) {
        fprintf(stderr, "holypkg: %s\n", error ? error : "invalid step header");
        free(error);
        return 2;
    }
    if (count < 4 || (strcmp(tokens[0], "step") && strcmp(tokens[0], "step-file") &&
                      strcmp(tokens[0], "split-step"))) {
        holy_tokens_free(tokens, count);
        return problem(file, line, "step needs a phase, an interpreter and <<TAG or a script path");
    }
    /* a split step names its output before the phase */
    if (!strcmp(tokens[0], "split-step")) base = 2;
    if (phase_index(tokens[base]) < 0) {
        holy_tokens_free(tokens, count);
        return problem(file, line, "unknown phase %s", tokens[base]);
    }
    if (!strcmp(tokens[0], "step") || !strcmp(tokens[0], "split-step")) {
        size_t tag_length = strlen(tokens[count - 1]);
        if (tag_length < 3 || strncmp(tokens[count - 1], "<<", 2)) {
            holy_tokens_free(tokens, count);
            return problem(file, line, "step block needs a <<TAG terminator");
        }
        block = count - 1;
    }
    if (recipe->step_count >= 256) {
        holy_tokens_free(tokens, count);
        return problem(file, line, "too many steps");
    }
    grown = realloc(recipe->steps, (recipe->step_count + 1) * sizeof *recipe->steps);
    if (!grown) { holy_tokens_free(tokens, count); return 1; }
    recipe->steps = grown;
    step = &recipe->steps[recipe->step_count];
    memset(step, 0, sizeof *step);
    step->line = line;
    step->phase = strdup(tokens[base]);
    step->argc = block ? block - (base + 1) : count - (base + 2);
    if (base == 2) step->output = strdup(tokens[1]);
    if (block) {
        step->tag = strdup(tokens[count - 1] + 2);
        step->body = malloc(body_length + 1);
        if (step->body) {
            memcpy(step->body, body, body_length);
            step->body[body_length] = 0;
            step->length = body_length;
        }
    } else {
        step->file = strdup(tokens[count - 1]);
        if (!step->argc) step->argc = 1;
    }
    if (!step->phase || (base == 2 ? !step->output : 0) ||
        (block ? !step->tag || !step->body : !step->file)) goto done;
    if (!step->argc) goto done;
    step->argv = calloc(step->argc + 1, sizeof *step->argv);
    if (!step->argv) goto done;
    for (i = 0; i < step->argc; ++i) {
        if (block) step->argv[i] = strdup(tokens[i + base + 1]);
        else step->argv[i] = strdup(i + base + 1 < count - 1 ? tokens[i + base + 1] : tokens[count - 1]);
        if (!step->argv[i]) goto done;
    }
    ok = 1;
done:
    if (!ok) {
        free(step->phase); free(step->tag); free(step->body); free(step->file);
        free(step->output);
        for (i = 0; i < step->argc; ++i) free(step->argv[i]);
        free(step->argv);
        memset(step, 0, sizeof *step);
    }
    holy_tokens_free(tokens, count);
    if (!ok) return problem(file, line, "invalid step");
    ++recipe->step_count;
    return 0;
}

static int parse_recipe(const char *path, struct recipe *recipe)
{
    char *text = NULL;
    size_t length = 0, offset = 0, line = 0;
    FILE *file = fopen(path, "rb");
    int result = 2;
    if (!file) {
        fprintf(stderr, "holypkg: recipe unavailable: %s\n", path);
        return 6;
    }
    if (fseek(file, 0, SEEK_END) || (length = (size_t)ftell(file)) > 4 * 1024 * 1024 ||
        fseek(file, 0, SEEK_SET)) goto done;
    text = malloc(length + 1);
    if (!text) { result = 1; goto done; }
    if (fread(text, 1, length, file) != length) goto done;
    text[length] = 0;
    while (offset < length) {
        char *start = text + offset, *end = memchr(start, '\n', length - offset);
        size_t size;
        char **tokens = NULL;
        size_t count = 0;
        char *error = NULL;
        if (!end) { result = problem(path, line + 1, "unterminated line"); goto done; }
        size = (size_t)(end - start);
        offset += size + 1;
        ++line;
        if (!size || start[0] == '#') continue;
        if (!strncmp(start, "step ", 5) || !strncmp(start, "step-file ", 10) ||
            !strncmp(start, "split-step ", 11)) {
            char *cursor = end + 1, *stop = NULL;
            size_t remaining = length - offset, body = 0;
            char *tag = NULL;
            if (strncmp(start, "step ", 5) && strncmp(start, "split-step ", 11)) {
                result = parse_step(recipe, start, size, NULL, 0, path, line);
                if (result) goto done;
                continue;
            }
            if (!holy_lex(start, size, &tokens, &count, path, line, &error)) {
                fprintf(stderr, "holypkg: %s\n", error ? error : "invalid step header");
                free(error); goto done;
            }
            if (count < 4 || strncmp(tokens[count - 1], "<<", 2)) {
                holy_tokens_free(tokens, count);
                result = problem(path, line, "step needs a phase, an interpreter and <<TAG");
                goto done;
            }
            tag = tokens[count - 1] + 2;
            while (body < remaining) {
                char *row = cursor + body;
                char *row_end = memchr(row, '\n', remaining - body);
                size_t row_size = row_end ? (size_t)(row_end - row) : remaining - body;
                if (row_size == strlen(tag) && !memcmp(row, tag, row_size)) {
                    stop = cursor + body;
                    body += row_size + (row_end ? 1 : 0);
                    break;
                }
                body += row_size + (row_end ? 1 : 0);
            }
            if (!stop) {
                holy_tokens_free(tokens, count);
                result = problem(path, line, "step block is not terminated by %s", tag);
                goto done;
            }
            result = parse_step(recipe, start, size, cursor,
                                (size_t)(stop - cursor), path, line);
            holy_tokens_free(tokens, count);
            if (result) goto done;
            offset = (size_t)((cursor + body) - text);
            line = 0;
            for (size_t i = 0; i < offset; ++i) if (text[i] == '\n') ++line;
            continue;
        }
        if (!holy_lex(start, size, &tokens, &count, path, line, &error)) {
            fprintf(stderr, "holypkg: %s\n", error ? error : "invalid manifest line");
            free(error);
            goto done;
        }
        if (!count) { holy_tokens_free(tokens, count); continue; }
        {
            static const struct { const char *key; size_t offset; } free_text[] = {
                { "summary", offsetof(struct recipe, summary) },
                { "homepage", offsetof(struct recipe, homepage) },
                { "license", offsetof(struct recipe, license) }
            };
            size_t index, k;
            for (index = 0; index < sizeof free_text / sizeof *free_text; ++index) {
                char **slot, *joined_text;
                size_t used = 1;
                if (strcmp(tokens[0], free_text[index].key)) continue;
                if (count < 2) {
                    result = problem(path, line, "%s needs text", tokens[0]);
                    break;
                }
                slot = (char **)((char *)recipe + free_text[index].offset);
                for (k = 1; k < count; ++k) used += strlen(tokens[k]) + 1;
                joined_text = malloc(used);
                if (!joined_text) { result = 1; break; }
                strcpy(joined_text, tokens[1]);
                for (k = 2; k < count; ++k) {
                    strcat(joined_text, " ");
                    strcat(joined_text, tokens[k]);
                }
                if (*slot) {
                    result = problem(path, line, "duplicate %s", tokens[0]);
                    free(joined_text);
                } else {
                    *slot = joined_text;
                    result = 0;
                }
                break;
            }
            if (index < sizeof free_text / sizeof *free_text) goto next_line;
        }
        {
            static const struct { const char *key; size_t offset; } scalars[] = {
                { "format", offsetof(struct recipe, format) },
                { "name", offsetof(struct recipe, name) },
                { "version", offsetof(struct recipe, version) },
                { "release", offsetof(struct recipe, release) },
                { "build-target", offsetof(struct recipe, build_target) },
                { "arch", offsetof(struct recipe, arch) },
                { "libc", offsetof(struct recipe, libc) }
            };
            size_t index;
            for (index = 0; index < sizeof scalars / sizeof *scalars; ++index) {
                char **slot;
                if (strcmp(tokens[0], scalars[index].key)) continue;
                if (count != 2) {
                    result = problem(path, line, "%s needs one value", tokens[0]);
                    break;
                }
                slot = (char **)((char *)recipe + scalars[index].offset);
                if (*slot) result = problem(path, line, "duplicate %s", tokens[0]);
                else if (!(*slot = strdup(tokens[1]))) result = 1;
                else result = 0;
                break;
            }
            if (index < sizeof scalars / sizeof *scalars) goto next_line;
        }
        if (!strcmp(tokens[0], "source") && count == 3) {
            struct recipe_source *grown = realloc(recipe->sources,
                                       (recipe->source_count + 1) * sizeof *recipe->sources);
            if (!grown) { holy_tokens_free(tokens, count); result = 1; goto done; }
            recipe->sources = grown;
            memset(&grown[recipe->source_count], 0, sizeof grown[0]);
            grown[recipe->source_count].name = strdup(tokens[1]);
            grown[recipe->source_count].url = strdup(tokens[2]);
            if (!grown[recipe->source_count].name || !grown[recipe->source_count].url) {
                holy_tokens_free(tokens, count); result = 1; goto done;
            }
            ++recipe->source_count;
        } else if (!strcmp(tokens[0], "source-sha256") && count == 3) {
            size_t i;
            for (i = 0; i < recipe->source_count; ++i)
                if (!strcmp(recipe->sources[i].name, tokens[1])) break;
            if (i == recipe->source_count)
                result = problem(path, line, "source-sha256 names an undeclared source %s", tokens[1]);
            else if (!digest(tokens[2]))
                result = problem(path, line, "source-sha256 needs 64 hexadecimal digits");
            else if (recipe->sources[i].hash)
                result = problem(path, line, "duplicate source-sha256 for %s", tokens[1]);
            else if (!(recipe->sources[i].hash = strdup(tokens[2]))) { result = 1; }
            else result = 0;
        } else if (!strcmp(tokens[0], "build-depend") && count >= 2 && count <= 4) {
            result = append_depend(&recipe->build_depends, &recipe->build_depend_count,
                                   tokens + 1, count - 1) ? 0 : 1;
        } else if (!strcmp(tokens[0], "depend") && count >= 2 && count <= 4) {
            result = append_depend(&recipe->depends, &recipe->depend_count,
                                   tokens + 1, count - 1) ? 0 : 1;
        } else if (!strcmp(tokens[0], "output") && count == 3) {
            struct recipe_output *grown = realloc(recipe->outputs,
                                       (recipe->output_count + 1) * sizeof *recipe->outputs);
            size_t i;
            if (!grown) { holy_tokens_free(tokens, count); result = 1; goto done; }
            recipe->outputs = grown;
            memset(&grown[recipe->output_count], 0, sizeof grown[0]);
            grown[recipe->output_count].name = strdup(tokens[1]);
            grown[recipe->output_count].kind = strdup(tokens[2]);
            if (!grown[recipe->output_count].name || !grown[recipe->output_count].kind) {
                holy_tokens_free(tokens, count); result = 1; goto done;
            }
            for (i = 0; i < recipe->output_count; ++i)
                if (!strcmp(recipe->outputs[i].name, tokens[1])) {
                    result = problem(path, line, "duplicate output %s", tokens[1]);
                    break;
                }
            if (!result) ++recipe->output_count;
        } else if (!strcmp(tokens[0], "config") && (count == 2 || count == 3)) {
            struct recipe_config *grown = realloc(recipe->configs,
                                       (recipe->config_count + 1) * sizeof *recipe->configs);
            size_t i;
            if (!grown) { holy_tokens_free(tokens, count); result = 1; goto done; }
            recipe->configs = grown;
            memset(&grown[recipe->config_count], 0, sizeof grown[0]);
            grown[recipe->config_count].path = strdup(tokens[1]);
            grown[recipe->config_count].mutable = count == 3 && !strcmp(tokens[2], "mutable");
            if (!grown[recipe->config_count].path ||
                (count == 3 && !grown[recipe->config_count].mutable)) {
                holy_tokens_free(tokens, count);
                free(grown[recipe->config_count].path);
                result = 2;
                goto done;
            }
            for (i = 0; i < recipe->config_count; ++i)
                if (!strcmp(recipe->configs[i].path, tokens[1])) {
                    result = problem(path, line, "duplicate config %s", tokens[1]);
                    break;
                }
            if (!result) ++recipe->config_count;
        } else if (!strcmp(tokens[0], "split") && count == 3) {
            struct recipe_split *grown = realloc(recipe->splits,
                                       (recipe->split_count + 1) * sizeof *recipe->splits);
            size_t i;
            if (!grown) { holy_tokens_free(tokens, count); result = 1; goto done; }
            recipe->splits = grown;
            memset(&grown[recipe->split_count], 0, sizeof grown[0]);
            grown[recipe->split_count].output = strdup(tokens[1]);
            grown[recipe->split_count].pattern = strdup(tokens[2]);
            if (!grown[recipe->split_count].output || !grown[recipe->split_count].pattern) {
                holy_tokens_free(tokens, count); result = 1; goto done;
            }
            for (i = 0; i < recipe->output_count; ++i)
                if (!strcmp(recipe->outputs[i].name, tokens[1])) break;
            if (i == recipe->output_count) {
                result = problem(path, line, "split names an undeclared output %s", tokens[1]);
            } else {
                ++recipe->split_count;
                result = 0;
            }
        } else if ((!strcmp(tokens[0], "hook-install") || !strcmp(tokens[0], "hook-remove")) &&
                   count == 3) {
            struct recipe_hook **list = !strcmp(tokens[0], "hook-install") ?
                &recipe->hook_install : &recipe->hook_remove;
            size_t *total = !strcmp(tokens[0], "hook-install") ?
                &recipe->hook_install_count : &recipe->hook_remove_count;
            struct recipe_hook *grown = realloc(*list, (*total + 1) * sizeof **list);
            if (!grown) { holy_tokens_free(tokens, count); result = 1; goto done; }
            *list = grown;
            memset(&grown[*total], 0, sizeof grown[0]);
            grown[*total].interpreter = strdup(tokens[1]);
            grown[*total].path = strdup(tokens[2]);
            if (!grown[*total].interpreter || !grown[*total].path) {
                holy_tokens_free(tokens, count); result = 1; goto done;
            }
            ++*total;
            result = 0;
        } else if (!strncmp(tokens[0], "x-", 2) && count >= 2) {
            /* HOLY/meta accepts one key and one value, so the words are rejoined
               and requoted into a single value, one record per line. */
            size_t i, used = recipe->extra ? strlen(recipe->extra) : 0, needed = strlen(tokens[0]) + 3;
            char *record;
            /* a byte outside printable ASCII is written as \xHH, so it needs four */
            for (i = 1; i < count; ++i) needed += strlen(tokens[i]) * 4 + 3;
            record = malloc(needed);
            if (!record) { holy_tokens_free(tokens, count); result = 1; goto done; }
            needed = (size_t)sprintf(record, "%s \"", tokens[0]);
            for (i = 1; i < count; ++i) {
                const unsigned char *p = (const unsigned char *)tokens[i];
                if (i > 1) record[needed++] = ' ';
                for (; *p; ++p) {
                    if (*p == '"' || *p == '\\') record[needed++] = '\\';
                    else if (*p < 32 || *p >= 127) {
                        sprintf(record + needed, "\\x%02x", *p);
                        needed += 4;
                        continue;
                    }
                    record[needed++] = (char)*p;
                }
            }
            record[needed++] = '"';
            record[needed++] = '\n';
            record[needed] = 0;
            {
                char *joined = realloc(recipe->extra, used + needed + 1);
                if (!joined) { free(record); holy_tokens_free(tokens, count); result = 1; goto done; }
                recipe->extra = joined;
                memcpy(recipe->extra + used, record, needed + 1);
            }
            free(record);
            result = 0;
        } else {
            result = problem(path, line, "unknown key %s", tokens[0]);
        }
next_line:
        holy_tokens_free(tokens, count);
        if (result) goto done;
    }
    result = 0;
done:
    fclose(file);
    if (!result) { recipe->text = text; recipe->text_length = length; }
    else free(text);
    return result;
}

static int validate_recipe(struct recipe *recipe, const char *path)
{
    size_t i;
    if (!recipe->format || strcmp(recipe->format, "holy-recipe-1")) {
        fprintf(stderr, "holypkg: %s: format must be holy-recipe-1\n", path);
        return 2;
    }
    if (!label(recipe->name, 256) || !label(recipe->version, 256) ||
        !label(recipe->release, 64)) {
        fprintf(stderr, "holypkg: %s: name, version and release are required labels\n", path);
        return 2;
    }
    if (!recipe->arch || !label(recipe->arch, 64) || !recipe->libc ||
        !label(recipe->libc, 64)) {
        fprintf(stderr, "holypkg: %s: arch and libc are required labels\n", path);
        return 2;
    }
    if (!recipe->output_count) {
        fprintf(stderr, "holypkg: %s: at least one output is required\n", path);
        return 2;
    }
    for (i = 0; i < recipe->source_count; ++i) {
        const char *url = recipe->sources[i].url;
        if (!label(recipe->sources[i].name, 256) || !*url) {
            fprintf(stderr, "holypkg: %s: source needs a name and an input\n", path);
            return 2;
        }
        if (strstr(url, "://") && !recipe->sources[i].hash) {
            fprintf(stderr, "holypkg: %s: network source %s needs source-sha256\n",
                    path, recipe->sources[i].name);
            return 2;
        }
        if (recipe->sources[i].hash && !digest(recipe->sources[i].hash)) {
            fprintf(stderr, "holypkg: %s: source %s has a malformed digest\n",
                    path, recipe->sources[i].name);
            return 2;
        }
        if (strstr(url, "://") && strncmp(url, "https://", 8)) {
            fprintf(stderr, "holypkg: %s: source %s needs https\n", path, recipe->sources[i].name);
            return 2;
        }
    }
    for (i = 0; i < recipe->depend_count; ++i) {
        if (!recipe->depends[i].name[0]) {
            fprintf(stderr, "holypkg: %s: depend needs a name\n", path);
            return 2;
        }
    }
    for (i = 0; i < recipe->step_count; ++i) {
        size_t k;
        if (!recipe->steps[i].output) continue;
        for (k = 0; k < recipe->output_count; ++k)
            if (!strcmp(recipe->outputs[k].name, recipe->steps[i].output)) break;
        if (k == recipe->output_count) {
            fprintf(stderr, "holypkg: %s: split-step names an undeclared output %s\n",
                    path, recipe->steps[i].output);
            return 2;
        }
        if (strcmp(recipe->steps[i].phase, "split")) {
            fprintf(stderr, "holypkg: %s: split-step must run in the split phase\n", path);
            return 2;
        }
    }
    for (i = 0; i < recipe->hook_install_count; ++i) {
        if (recipe->hook_install[i].interpreter[0] != '/' ||
            !safe_path(recipe->hook_install[i].path)) {
            fprintf(stderr, "holypkg: %s: hook-install needs an absolute interpreter and a relative script\n", path);
            return 2;
        }
    }
    for (i = 0; i < recipe->hook_remove_count; ++i) {
        if (recipe->hook_remove[i].interpreter[0] != '/' ||
            !safe_path(recipe->hook_remove[i].path)) {
            fprintf(stderr, "holypkg: %s: hook-remove needs an absolute interpreter and a relative script\n", path);
            return 2;
        }
    }
    for (i = 0; i < recipe->config_count; ++i)
        if (!safe_path(recipe->configs[i].path)) {
            fprintf(stderr, "holypkg: %s: config %s needs a relative payload path\n",
                    path, recipe->configs[i].path);
            return 2;
        }
    return 0;
}

struct run_paths {
    char *work, *src, *build, *dest, *out, *sources;
};

static void run_paths_free(struct run_paths *paths)
{
    free(paths->work); free(paths->src); free(paths->build); free(paths->dest);
    free(paths->out); free(paths->sources);
    memset(paths, 0, sizeof *paths);
}

/* zero on failure, matching the other local helpers in this file. */
/* copies one pinned local object to a named path inside the build root. */
static int stage_local(const char *source, const char *target, const char *expected)
{
    char *directory = strdup(target), *slash;
    unsigned char buffer[65536], digest_bytes[32];
    EVP_MD_CTX *context = EVP_MD_CTX_new();
    unsigned length;
    ssize_t got;
    int in = -1, out = -1, ok = 0;
    if (!directory || !(slash = strrchr(directory, '/'))) goto done;
    *slash = 0;
    {
        size_t length = strlen(directory), index;
        for (index = 1; index <= length; ++index) {
            char saved;
            if (index < length && directory[index] != '/') continue;
            saved = directory[index];
            directory[index] = 0;
            if (mkdir(directory, 0700) && errno != EEXIST) { directory[index] = saved; goto done; }
            directory[index] = saved;
        }
    }
    in = open(source, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (in < 0 || !context || EVP_DigestInit_ex(context, EVP_sha256(), NULL) != 1) goto done;
    out = open(target, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0600);
    if (out < 0) goto done;
    while ((got = read(in, buffer, sizeof buffer)) > 0) {
        if (EVP_DigestUpdate(context, buffer, (size_t)got) != 1 ||
            write(out, buffer, (size_t)got) != got) goto done;
    }
    if (got < 0 || EVP_DigestFinal_ex(context, digest_bytes, &length) != 1 || length != 32) goto done;
    {
        char actual[65];
        size_t i;
        for (i = 0; i < 32; ++i) snprintf(actual + i * 2, 3, "%02x", digest_bytes[i]);
        if (expected && strcmp(actual, expected)) goto done;
    }
    ok = 1;
done:
    if (out >= 0 && close(out) && ok) ok = 0;
    if (in >= 0) close(in);
    EVP_MD_CTX_free(context);
    free(directory);
    return ok;
}

static int populate(const char *work)
{
    static const char *const names[] = { "src", "build", "dest", "out", "sources" };
    char *path = NULL;
    size_t i;
    int result = 0;
    if (mkdir(work, 0700) && errno != EEXIST) {
        fprintf(stderr, "holypkg: build root unavailable: %s\n", work);
        return 1;
    }
    for (i = 0; i < sizeof names / sizeof *names; ++i) {
        free(path);
        path = joined(work, names[i]);
        if (!path || (mkdir(path, 0700) && errno != EEXIST)) {
            fprintf(stderr, "holypkg: build directory unavailable: %s/%s\n", work, names[i]);
            result = 1;
            goto done;
        }
    }
done:
    free(path);
    return result;
}

/* creates every missing parent of a path that lies inside the target. */
static int make_parents(char *path)
{
    char *parent = strdup(path), *slash;
    int result = 0;
    if (!parent) return 0;
    for (slash = strchr(parent, '/'); slash; slash = strchr(slash + 1, '/')) {
        *slash = 0;
        if (*parent && mkdir(parent, 0700) && errno != EEXIST) goto done;
        *slash = '/';
    }
    result = 1;
done:
    free(parent);
    return result;
}

/* extracts one tar archive, compressed or not, with a total size bound. */
static int unpack_archive(const char *archive_path, const char *target)
{
    struct archive *a = archive_read_new();
    struct archive_entry *entry = NULL;
    char buffer[65536];
    la_ssize_t copied;
    unsigned long long total = 0;
    int ok = 0, first = 1;
    if (!a) return 0;
    if (archive_read_support_filter_all(a) != ARCHIVE_OK ||
        archive_read_support_format_tar(a) != ARCHIVE_OK ||
        archive_read_open_filename(a, archive_path, 65536) != ARCHIVE_OK ||
        archive_read_next_header(a, &entry) != ARCHIVE_OK) {
        archive_read_free(a);
        return 0;
    }
    while (first ? 1 : archive_read_next_header(a, &entry) == ARCHIVE_OK) {
        char *path = NULL;
        const char *name;
        int fd;
        first = 0;
        if (!entry) break;
        name = archive_entry_pathname(entry);
        if (!safe_path(name)) { archive_read_free(a); return 0; }
        path = joined(target, name);
        if (!path) { archive_read_free(a); return 1; }
        if (archive_entry_filetype(entry) == AE_IFDIR) {
            if (!make_parents(path) || (mkdir(path, 0700) && errno != EEXIST)) {
                free(path); archive_read_free(a);
                return 0;
            }
        } else if (archive_entry_filetype(entry) == AE_IFLNK) {
            const char *link = archive_entry_symlink(entry);
            if (!link || !safe_path(link) || !make_parents(path) || symlink(link, path)) {
                free(path); archive_read_free(a);
                return 0;
            }
        } else if (archive_entry_filetype(entry) == AE_IFREG) {
            mode_t mode = archive_entry_perm(entry);
            if (!make_parents(path)) { free(path); archive_read_free(a); return 0; }
            fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, mode & 0777);
            if (fd < 0) { free(path); archive_read_free(a); return 0; }
            while ((copied = archive_read_data(a, buffer, sizeof buffer)) > 0) {
                if (total > 1024ULL * 1024 * 1024 - (unsigned long long)copied ||
                    write(fd, buffer, (size_t)copied) != (ssize_t)copied) {
                    close(fd); free(path); archive_read_free(a);
                    return 0;
                }
                total += (unsigned long long)copied;
            }
            if (copied < 0 || close(fd)) { free(path); archive_read_free(a); return 0; }
        }
        free(path);
    }
    if (archive_errno(a) != 0) { archive_read_free(a); return 0; }
    ok = 1;
    archive_read_free(a);
    return ok;
}

static int write_file(const char *path, const char *body, size_t length, unsigned mode)
{
    int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, mode);
    size_t used = 0;
    if (fd < 0) return 0;
    while (used < length) {
        ssize_t written = write(fd, body + used, length - used);
        if (written < 0 && errno == EINTR) continue;
        if (written <= 0) { close(fd); return 0; }
        used += (size_t)written;
    }
    return !close(fd);
}

static int run_step(const struct recipe_step *step, const char *work, const char *recipe_dir,
                    const struct recipe *recipe, const char *environment, unsigned jobs,
                    int approve_all, int noninteractive, int *approve_rest,
                    const char *split_dest)
{
    char *script = NULL, *argv[64] = {0};
    char work_buffer[4096], src_buffer[4096], build_buffer[4096], dest_buffer[4096];
    char out_buffer[4096], jobs_buffer[32], target_buffer[256];
    const char *cwd = work;
    size_t i;
    pid_t child;
    int status = 0, result = 1;
    if (step->body) {
        script = joined(work, "step-script");
        if (!script || !write_file(script, step->body, step->length, 0700)) {
            fprintf(stderr, "holypkg: recipe step %zu script unavailable\n", step->line);
            free(script);
            return 1;
        }
    } else {
        char *candidate = safe_path(step->file) ? joined(recipe_dir, step->file) : NULL;
        struct stat st;
        if (!candidate || stat(candidate, &st) || !S_ISREG(st.st_mode) ||
            st.st_size > 4 * 1024 * 1024) {
            fprintf(stderr, "holypkg: recipe step %zu script unavailable: %s\n",
                    step->line, step->file);
            free(candidate);
            return 1;
        }
        script = candidate;
    }
    for (i = 0; i < step->argc && i + 1 < sizeof argv / sizeof *argv; ++i)
        argv[i] = step->argv[i];
    argv[i] = script;
    argv[i + 1] = NULL;
    if (!strcmp(step->phase, "build") || !strcmp(step->phase, "package") ||
        !strcmp(step->phase, "split") || !strcmp(step->phase, "check")) {
        if (strlen(work) + 8 > sizeof build_buffer) { free(script); return 1; }
        snprintf(build_buffer, sizeof build_buffer, "%s/build", work);
        cwd = build_buffer;
    }
    snprintf(work_buffer, sizeof work_buffer, "%s", work);
    snprintf(src_buffer, sizeof src_buffer, "%s/src", work);
    snprintf(dest_buffer, sizeof dest_buffer, "%s/dest", work);
    snprintf(out_buffer, sizeof out_buffer, "%s/out", work);
    snprintf(jobs_buffer, sizeof jobs_buffer, "%u", jobs ? jobs : 1);
    snprintf(target_buffer, sizeof target_buffer, "%s-linux-%s",
             recipe->arch ? recipe->arch : "any", recipe->libc ? recipe->libc : "nolibc");
    if (!*approve_rest && !approve_all) {
        char answer[16];
        if (noninteractive || !isatty(STDIN_FILENO)) {
            fprintf(stderr, "holypkg: decision-required recipe step %s; pass --yes after review\n",
                    step->phase);
            free(script);
            return 3;
        }
        printf("phase %s interpreter %s\n", step->phase, step->argc ? step->argv[0] : "-");
        printf("argv");
        for (i = 0; i < step->argc; ++i) printf(" %s", step->argv[i]);
        printf(" %s\n", script);
        printf("cwd %s uid %ld\n", cwd, (long)getuid());
        if (step->body) fwrite(step->body, 1, step->length, stdout);
        fflush(stdout);
        if (!fgets(answer, sizeof answer, stdin)) { free(script); return 3; }
        if (answer[0] == 'a' || answer[0] == 'A') *approve_rest = 1;
        else if (answer[0] != 'y' && answer[0] != 'Y') { free(script); return 3; }
    }
    child = fork();
    if (child < 0) { free(script); return 1; }
    if (!child) {
        const char *library = getenv("LD_LIBRARY_PATH");
        if (chdir(cwd)) _exit(126);
        setenv("HOLY_WORK", work_buffer, 1);
        setenv("HOLY_SRC", src_buffer, 1);
        setenv("HOLY_BUILD", build_buffer, 1);
        setenv("HOLY_DEST", dest_buffer, 1);
        setenv("HOLY_OUT", out_buffer, 1);
        setenv("HOLY_ARCH", recipe->arch ? recipe->arch : "any", 1);
        setenv("HOLY_LIBC", recipe->libc ? recipe->libc : "nolibc", 1);
        setenv("HOLY_JOBS", jobs_buffer, 1);
        setenv("HOLY_BUILD_TARGET", target_buffer, 1);
        setenv("HOLY_HOST_TARGET", target_buffer, 1);
        setenv("HOLY_TARGET", target_buffer, 1);
        if (split_dest) setenv("HOLY_SPLIT_DEST", split_dest, 1);
        if (library) setenv("LD_LIBRARY_PATH", library, 1);
        if (!strcmp(environment, "clean")) {
            unsetenv("HOME");
            setenv("PATH", "/usr/bin:/bin", 1);
        }
        execvp(argv[0], argv);
        _exit(127);
    }
    while (waitpid(child, &status, 0) < 0 && errno == EINTR) continue;
    if (WIFEXITED(status) && WEXITSTATUS(status) == 0) result = 0;
    else {
        fprintf(stderr, "holypkg: recipe step %s failed with status %d\n",
                step->phase, WIFEXITED(status) ? WEXITSTATUS(status) : -1);
        result = WIFEXITED(status) && WEXITSTATUS(status) == 127 ? 6 : 1;
    }
    if (script && strncmp(script, work, strlen(work))) unlink(script);
    free(script);
    return result;
}

struct payload_entry {
    char *path;
    char *link;
    char *group;
    char *root;
    char *owner;
    unsigned int mode;
    unsigned long long size;
    int directory;
};

struct payload {
    struct payload_entry *items;
    size_t count;
    char *groups[8];
    size_t group_count;
};

static void payload_free(struct payload *payload)
{
    size_t i;
    for (i = 0; i < payload->count; ++i) {
        free(payload->items[i].path);
        free(payload->items[i].link);
        free(payload->items[i].root);
        free(payload->items[i].owner);
    }
    for (i = 0; i < payload->group_count; ++i) free(payload->groups[i]);
    free(payload->items);
    memset(payload, 0, sizeof *payload);
}

static int payload_add(struct payload *payload, const char *path, const char *link,
                       const char *group, const char *root, const char *owner,
                       unsigned int mode, unsigned long long size, int directory)
{
    struct payload_entry *grown = realloc(payload->items, (payload->count + 1) * sizeof *grown);
    if (!grown) return 0;
    payload->items = grown;
    memset(&grown[payload->count], 0, sizeof grown[0]);
    grown[payload->count].path = strdup(path);
    grown[payload->count].link = link ? strdup(link) : NULL;
    grown[payload->count].group = group ? strdup(group) : NULL;
    grown[payload->count].root = strdup(root);
    grown[payload->count].owner = owner ? strdup(owner) : NULL;
    grown[payload->count].mode = mode;
    grown[payload->count].size = size;
    grown[payload->count].directory = directory;
    if (!grown[payload->count].path || (link && !grown[payload->count].link) ||
        !grown[payload->count].root || (group && !grown[payload->count].group) ||
        (owner && !grown[payload->count].owner)) return 0;
    ++payload->count;
    return 1;
}

static int entry_order(const void *left, const void *right)
{
    return strcmp(((const struct payload_entry *)left)->path,
                  ((const struct payload_entry *)right)->path);
}

static int collect_payload(const char *root, const char *relative, const char *owner,
                           struct payload *payload)
{
    char *path = relative && *relative ? joined(root, relative) : strdup(root);
    DIR *dir;
    struct dirent *item;
    int result = 1;
    if (!path) return 0;
    dir = opendir(path);
    if (!dir) { free(path); return 0; }
    while ((item = readdir(dir))) {
        char *child_relative = NULL, *child_path = NULL;
        struct stat st;
        unsigned long long size = 0;
        int ok = 1;
        if (!strcmp(item->d_name, ".") || !strcmp(item->d_name, "..")) continue;
        if (!safe_path(item->d_name)) goto done;
        child_relative = relative && *relative ?
            joined(relative, item->d_name) : strdup(item->d_name);
        child_path = joined(path, item->d_name);
        if (!child_relative || !child_path) { ok = 0; goto next; }
        if (lstat(child_path, &st)) goto next;
        if (S_ISDIR(st.st_mode)) {
            if (!payload_add(payload, child_relative, NULL, NULL, root, owner,
                             (unsigned int)(st.st_mode & 07777), 0, 1)) ok = 0;
            else ok = collect_payload(root, child_relative, owner, payload);
        } else if (S_ISLNK(st.st_mode)) {
            char target[4096];
            ssize_t length = readlink(child_path, target, sizeof target - 1);
            if (length <= 0) goto next;
            target[length] = 0;
            if (!safe_path(target)) {
                fprintf(stderr, "holypkg: unsafe payload symlink %s -> %s\n", child_relative, target);
                ok = 0;
            } else if (!payload_add(payload, child_relative, target, NULL, root, owner, 0777, 0, 0))
                ok = 0;
        } else if (S_ISREG(st.st_mode)) {
            const char *group = "noarch-nolibc";
            struct holy_elf_info info;
            if (!holy_elf_read(child_path, &info)) {
                const char *machine = holy_elf_machine(&info);
                const char *runtime = holy_elf_runtime(&info);
                char buffer[64];
                if (!strcmp(machine, "unknown") || !strcmp(runtime, "unknown")) {
                    fprintf(stderr, "holypkg: unclassified ELF in build output: %s\n", child_relative);
                    holy_elf_free(&info);
                    ok = 0;
                } else {
                    size_t k;
                    snprintf(buffer, sizeof buffer, "%s-%s", machine, runtime);
                    for (k = 0; k < payload->group_count; ++k)
                        if (!strcmp(payload->groups[k], buffer)) break;
                    if (k == payload->group_count) {
                        if (k >= sizeof payload->groups / sizeof *payload->groups) {
                            fprintf(stderr, "holypkg: too many ABI groups in one build\n");
                            holy_elf_free(&info);
                            ok = 0;
                        } else {
                            payload->groups[k] = strdup(buffer);
                            if (!payload->groups[k]) { holy_elf_free(&info); ok = 0; }
                            else ++payload->group_count;
                        }
                    }
                    if (ok) group = payload->groups[k];
                }
                holy_elf_free(&info);
            }
            if (ok) {
                size = (unsigned long long)st.st_size;
                ok = payload_add(payload, child_relative, NULL, group, root, owner,
                                 (unsigned int)(st.st_mode & 07777), size, 0);
            }
        }
next:
        free(child_relative);
        free(child_path);
        if (!ok) { result = 0; break; }
    }
done:
    closedir(dir);
    free(path);
    return result;
}

static int copy_into(const char *dest_tree, const char *source_root,
                     const struct payload_entry *entry)
{
    char *target = joined(dest_tree, entry->path);
    char *parent = NULL, *slash;
    int result = 1, fd;
    if (!target) return 0;
    parent = strdup(target);
    if (!parent) { free(target); return 0; }
    slash = strrchr(parent, '/');
    if (!slash) { free(target); free(parent); return 0; }
    *slash = 0;
    {
        size_t length = strlen(parent), index;
        for (index = 1; index <= length; ++index) {
            char saved;
            if (index < length && parent[index] != '/') continue;
            saved = parent[index];
            parent[index] = 0;
            if (mkdir(parent, 0755) && errno != EEXIST) {
                fprintf(stderr, "holypkg: build directory unavailable: %s\n", parent);
                parent[index] = saved;
                result = 0;
                break;
            }
            parent[index] = saved;
        }
    }
    if (result && entry->directory) {
        if (mkdir(target, 0700) && errno != EEXIST) result = 0;
    } else if (result && entry->link) {
        if (symlink(entry->link, target)) result = 0;
    } else if (result) {
        char *source = joined(source_root, entry->path);
        unsigned char buffer[65536];
        ssize_t got;
        if (!source) result = 0;
        fd = result ? open(source, O_RDONLY | O_CLOEXEC | O_NOFOLLOW) : -1;
        if (fd < 0) result = 0;
        else {
            int out = open(target, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC,
                           entry->mode & 0777);
            if (out < 0) result = 0;
            while (result && (got = read(fd, buffer, sizeof buffer)) > 0)
                if (write(out, buffer, (size_t)got) != got) result = 0;
            if (got < 0) result = 0;
            if (close(out)) result = 0;
        }
        if (fd >= 0) close(fd);
        free(source);
    }
    free(target);
    free(parent);
    return result;
}

static const char *output_for(const struct recipe *recipe, const char *path)
{
    size_t i;
    for (i = 0; i < recipe->split_count; ++i)
        if (!fnmatch(recipe->splits[i].pattern, path, 0)) return recipe->splits[i].output;
    return recipe->output_count ? recipe->outputs[0].name : NULL;
}

/* a split step owns its whole staging tree, so no pattern may redirect it. */
static const char *entry_output(const struct recipe *recipe, const struct payload_entry *entry)
{
    if (entry->owner) return entry->owner;
    return output_for(recipe, entry->path);
}

static void split_group(const char *group, char arch[64], char libc[64])
{
    char *dash;
    snprintf(arch, 64, "%s", group);
    dash = strchr(arch, '-');
    if (!dash) { snprintf(libc, 64, "nolibc"); return; }
    snprintf(libc, 64, "%s", dash + 1);
    *dash = 0;
}

static void token(FILE *out, const char *value)
{
    const unsigned char *p = (const unsigned char *)value;
    fputc('"', out);
    for (; *p; ++p) {
        if (*p == '"' || *p == '\\') fprintf(out, "\\%c", *p);
        else if (*p < 32 || *p >= 127) fprintf(out, "\\x%02x", *p);
        else fputc(*p, out);
    }
    fputc('"', out);
}

struct requirement {
    char kind[32], name[256], arch[64], libc[64], relation[16], version[128];
    char evidence[256];
    /* a payload requirement belongs to the one output and ABI group that carry it. */
    char scope[512];
    char group[64];
};

static int requirement_line(FILE *out, unsigned index, const char *consumer,
                            const struct requirement *item)
{
    char id[96];
    snprintf(id, sizeof id, "%s-req-%u", consumer, index);
    fputs("require ", out);
    token(out, id); fputc(' ', out);
    token(out, consumer); fputc(' ', out);
    token(out, item->kind); fputc(' ', out);
    token(out, item->name); fputc(' ', out);
    token(out, item->arch); fputc(' ', out);
    token(out, item->libc); fputc(' ', out);
    token(out, item->relation); fputc(' ', out);
    token(out, item->version); fputc(' ', out);
    token(out, item->evidence); fputc(' ', out);
    fputs("\"holy-recipe\"\n", out);
    return ferror(out) ? 0 : 1;
}

static int requirement_applies(const struct requirement *item, const char *output_name,
                               const char *group)
{
    if (item->scope[0] && strcmp(item->scope, output_name)) return 0;
    if (item->group[0] && group && strcmp(item->group, group)) return 0;
    return 1;
}

/* a hook script may live in the main tree or in the staging tree of a split output.
   returns 1 with a digest, 0 when this output does not carry the script, -1 on error. */
static int hook_digest(const struct recipe *recipe, const struct payload *payload,
                       const char *output_name, const char *relative, char hash[65],
                       int *claimed)
{
    char *path = NULL;
    struct stat st;
    int fd = -1, ok = 0;
    unsigned char buffer[65536], digest_bytes[32];
    ssize_t got;
    size_t used = 0, i;
    EVP_MD_CTX *context = NULL;
    unsigned length;
    *claimed = 0;
    for (i = 0; i < payload->count; ++i) {
        const struct payload_entry *entry = &payload->items[i];
        const char *target = entry->directory ? NULL : entry_output(recipe, entry);
        if (!target || strcmp(target, output_name) || strcmp(entry->path, relative)) continue;
        path = joined(entry->root, relative);
        *claimed = 1;
        break;
    }
    if (!path) return 0;
    fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 1024 * 1024) goto done;
    context = EVP_MD_CTX_new();
    if (!context || EVP_DigestInit_ex(context, EVP_sha256(), NULL) != 1) goto done;
    while ((got = read(fd, buffer, sizeof buffer)) > 0)
        if (EVP_DigestUpdate(context, buffer, (size_t)got) != 1) goto done;
    if (got < 0 || EVP_DigestFinal_ex(context, digest_bytes, &length) != 1 || length != 32) goto done;
    for (used = 0; used < 32; ++used) snprintf(hash + used * 2, 3, "%02x", digest_bytes[used]);
    ok = 1;
done:
    EVP_MD_CTX_free(context);
    if (fd >= 0) close(fd);
    free(path);
    if (!ok && *claimed) return -1;
    return ok;
}

/* the packer writes flag none; a declared config path is rewritten in place. */
static int mark_config(const char *manifest, const struct recipe *recipe, const char *output_name)
{
    char *text = NULL, *rewritten = NULL;
    size_t length = 0, offset = 0, used = 0, i;
    FILE *out = NULL;
    FILE *input = fopen(manifest, "rb");
    int ok = 0;
    if (!input) return 0;
    if (fseek(input, 0, SEEK_END) || (length = (size_t)ftell(input)) > 64 * 1024 * 1024 ||
        fseek(input, 0, SEEK_SET)) goto done;
    text = malloc(length + 1);
    if (!text || fread(text, 1, length, input) != length) goto done;
    text[length] = 0;
    out = open_memstream(&rewritten, &used);
    if (!out) goto done;
    while (offset < length) {
        char *start = text + offset, *end = memchr(start, '\n', length - offset);
        char **tokens = NULL;
        size_t count = 0, k;
        char *error = NULL;
        size_t size = end ? (size_t)(end - start) : length - offset;
        offset += size + (end ? 1 : 0);
        if (!holy_lex(start, size, &tokens, &count, "HOLY/files", 1, &error)) {
            free(error);
            holy_tokens_free(tokens, count);
            fclose(out);
            goto done;
        }
        if (!count) { holy_tokens_free(tokens, count); continue; }
        if (count >= 12 && !strcmp(tokens[0], "file")) {
            for (i = 0; i < recipe->config_count; ++i)
                if (!strcmp(recipe->configs[i].path, tokens[1])) {
                    fprintf(out, "file ");
                    token(out, tokens[1]);
                    for (k = 2; k < 9; ++k) fprintf(out, " %s", tokens[k]);
                    fprintf(out, " %s", recipe->configs[i].mutable ? "config,mutable" : "config");
                    for (k = 10; k < count; ++k) fprintf(out, " %s", tokens[k]);
                    fputc('\n', out);
                    break;
                }
            if (i < recipe->config_count) { holy_tokens_free(tokens, count); continue; }
        }
        fwrite(start, 1, size, out);
        if (end) fputc('\n', out);
        holy_tokens_free(tokens, count);
    }
    if (fflush(out) || ferror(out) || fclose(out)) { out = NULL; goto done; }
    out = NULL;
    input = freopen(manifest, "w", input);
    if (!input || fwrite(rewritten, 1, used, input) != used || fflush(input) ||
        fsync(fileno(input)) || fclose(input)) { input = NULL; goto done; }
    input = NULL;
    ok = 1;
done:
    if (out) fclose(out);
    if (input) fclose(input);
    free(text);
    free(rewritten);
    (void)output_name;
    return ok;
}

/* an output is grouped by the ABI facts of the files it actually receives. */
struct output_group {
    char name[64];
    int seen;
};

static int collect_output_groups(const struct recipe *recipe, const char *output_name,
                                 const struct payload *payload, struct output_group *groups,
                                 size_t limit)
{
    size_t i, k, used = 0;
    for (i = 0; i < payload->count; ++i) {
        const struct payload_entry *entry = &payload->items[i];
        const char *target = entry->directory ? NULL : entry_output(recipe, entry);
        if (!entry->group || !target || strcmp(target, output_name)) continue;
        for (k = 0; k < used; ++k)
            if (!strcmp(groups[k].name, entry->group)) break;
        if (k == used) {
            if (used >= limit) return -1;
            snprintf(groups[used].name, sizeof groups[used].name, "%s", entry->group);
            groups[used].seen = 1;
            ++used;
        }
    }
    return (int)used;
}

static int emit_output(struct recipe *recipe, const char *group, const char *output_name,
                       const char *kind, const char *output_directory,
                       unsigned serial, int multi_group, const struct payload *payload,
                       struct requirement *requirements, size_t requirement_count,
                       const char **variants, size_t variant_count)
{
    char *tree = NULL, *holy = NULL, *files_path = NULL, *meta_path = NULL;
    char *deps_path = NULL, *provides_path = NULL, *hooks_path = NULL;
    char *origin_path = NULL, *transform_path = NULL, *artifact = NULL;
    char tree_name[64];
    char arch[64], libc[64];
    unsigned long long size = 0;
    size_t i, included = 0;
    int result = 1;
    if (!output_name) return 0;
    split_group(group, arch, libc);
    snprintf(tree_name, sizeof tree_name, "tree-%u", serial);
    tree = joined(output_directory, tree_name);
    if (!tree || mkdir(tree, 0700)) {
        fprintf(stderr, "holypkg: build output tree unavailable\n");
        goto done;
    }
    holy = joined(tree, "HOLY");
    if (!holy || mkdir(holy, 0700) || !(files_path = joined(holy, "files"))) goto done;
    {
        char *data = joined(tree, "DATA");
        if (!data || mkdir(data, 0700)) { free(data); goto done; }
        free(data);
    }
    {
        char *data = joined(tree, "DATA");
        if (!data) goto done;
        /* a declared directory keeps the mode the build gave it, since a package that
           shares /usr with another one has to declare the same mode for it */
        for (i = 0; i < payload->count; ++i) {
            const struct payload_entry *entry = &payload->items[i];
            char *path;
            if (!entry->directory || !entry_output(recipe, entry) ||
                strcmp(entry_output(recipe, entry), output_name)) continue;
            if (entry->group && strcmp(entry->group, group)) continue;
            path = joined(data, entry->path);
            if (!path) { free(data); goto done; }
            if (!make_parents(path) || (mkdir(path, 0700) && errno != EEXIST) ||
                chmod(path, (mode_t)(entry->mode & 07777))) {
                fprintf(stderr, "holypkg: build directory unavailable: %s\n", path);
                free(path); free(data); goto done;
            }
            free(path);
        }
        for (i = 0; i < payload->count; ++i) {
            const struct payload_entry *entry = &payload->items[i];
            const char *target;
            if (entry->directory) continue;
            target = entry_output(recipe, entry);
            if (!target || strcmp(target, output_name)) continue;
            if (entry->group && strcmp(entry->group, group)) continue;
            if (!entry->group && multi_group) {
                /* a link or data file follows the first group that claims it */
                printf("ambiguous %s assigned to %s--%s\n", entry->path, output_name, group);
                continue;
            }
            if (!copy_into(data, entry->root, entry)) { free(data); goto done; }
            size += entry->size;
            ++included;
        }
        free(data);
    }
    if (!strcmp(kind, "metapackage")) {
        if (payload->count && !included && strcmp(output_name, recipe->outputs[0].name)) goto done;
    } else if (!included && strcmp(output_name, recipe->outputs[0].name)) {
        result = 0;
        goto done;
    }
    {
        /* the manifest generator refuses paths inside the tree it walks */
        char *manifest = joined(output_directory, "build-manifest");
        if (!manifest || !holy_generate_files(tree, manifest) ||
            rename(manifest, files_path)) {
            fprintf(stderr, "holypkg: manifest generation failed for %s\n", output_name);
            free(manifest);
            goto done;
        }
        free(manifest);
    }
    if (recipe->config_count && !mark_config(files_path, recipe, output_name)) {
        fprintf(stderr, "holypkg: config flags not applied for %s\n", output_name);
        goto done;
    }
    meta_path = joined(holy, "meta");
    deps_path = joined(holy, "deps");
    provides_path = joined(holy, "provides");
    hooks_path = joined(holy, "hooks");
    origin_path = joined(holy, "origin");
    transform_path = joined(holy, "transform");
    if (!meta_path || !deps_path || !provides_path || !hooks_path || !origin_path ||
        !transform_path) goto done;
    {
        FILE *out = fopen(meta_path, "w");
        if (!out) goto done;
        fputs("format holy-package-1\nname ", out); token(out, output_name);
        fputs("\nversion ", out); token(out, recipe->version);
        fputs("\nrelease ", out); token(out, recipe->release);
        fputs("\nos linux\narch ", out); token(out, arch);
        fputs("\nlibc ", out); token(out, libc);
        fputs("\nx-version-family holy\nx-build-target ", out);
        token(out, recipe->build_target ? recipe->build_target : "host");
        fputc('\n', out);
        fprintf(out, "installed-size %llu\n", size);
        if (recipe->summary) { fputs("summary ", out); token(out, recipe->summary); fputc('\n', out); }
        if (recipe->homepage) { fputs("homepage ", out); token(out, recipe->homepage); fputc('\n', out); }
        if (recipe->license) { fputs("license ", out); token(out, recipe->license); fputc('\n', out); }
        if (recipe->extra) fputs(recipe->extra, out);
        if (fclose(out)) goto done;
    }
    {
        FILE *out = fopen(deps_path, "w");
        if (!out) goto done;
        for (i = 0; i < requirement_count; ++i) {
            if (!requirement_applies(&requirements[i], output_name, group)) continue;
            if (!requirement_line(out, (unsigned)i, output_name, &requirements[i])) {
                fclose(out); goto done;
            }
        }
        for (i = 0; i < variant_count; ++i) {
            struct requirement metapackage;
            memset(&metapackage, 0, sizeof metapackage);
            snprintf(metapackage.kind, sizeof metapackage.kind, "package");
            snprintf(metapackage.name, sizeof metapackage.name, "%s", variants[i]);
            snprintf(metapackage.arch, sizeof metapackage.arch, "%s", arch);
            snprintf(metapackage.libc, sizeof metapackage.libc, "any");
            snprintf(metapackage.relation, sizeof metapackage.relation, "any");
            snprintf(metapackage.version, sizeof metapackage.version, "-");
            snprintf(metapackage.evidence, sizeof metapackage.evidence, "holy-recipe-output");
            if (!requirement_line(out, (unsigned)(requirement_count + i), output_name, &metapackage)) {
                fclose(out); goto done;
            }
        }
        if (fclose(out)) goto done;
    }
    {
        char *data = joined(tree, "DATA");
        FILE *out = fopen(provides_path, "w");
        if (!data || !out) { free(data); if (out) fclose(out); goto done; }
        fprintf(out, "provide package \"%s\" any any \"-\" holy\n", output_name);
        for (i = 0; i < payload->count; ++i) {
            const struct payload_entry *entry = &payload->items[i];
            const char *target = entry->directory ? NULL : entry_output(recipe, entry);
            struct holy_elf_info info;
            char *path;
            if (entry->directory || entry->link || !target || strcmp(target, output_name)) continue;
            if (entry->group && strcmp(entry->group, group)) continue;
            path = joined(data, entry->path);
            if (!path) { fclose(out); free(data); goto done; }
            if (!holy_elf_read(path, &info) && info.soname && !strchr(info.soname, '/') &&
                info.type == ET_DYN) {
                fprintf(out, "provide soname \"%s\" any any \"-\" holy\n", info.soname);
            }
            holy_elf_free(&info);
            free(path);
        }
        free(data);
        if (fclose(out)) goto done;
    }
    {
        FILE *out = fopen(hooks_path, "w");
        if (!out) goto done;
        /* a hook belongs to the output that carries its script */
        for (i = 0; i < recipe->hook_install_count; ++i) {
            char hash[65];
            int found = hook_digest(recipe, payload, output_name,
                                    recipe->hook_install[i].path, hash, &found);
            if (found < 0) { fclose(out); goto done; }
            if (found && fprintf(out, "hook postinstall \"%s\" \"%s\" sha256 %s\n",
                                 recipe->hook_install[i].interpreter,
                                 recipe->hook_install[i].path, hash) < 0) { fclose(out); goto done; }
        }
        for (i = 0; i < recipe->hook_remove_count; ++i) {
            char hash[65];
            int found = hook_digest(recipe, payload, output_name,
                                    recipe->hook_remove[i].path, hash, &found);
            if (found < 0) { fclose(out); goto done; }
            if (found && fprintf(out, "hook preremove \"%s\" \"%s\" sha256 %s\n",
                                 recipe->hook_remove[i].interpreter,
                                 recipe->hook_remove[i].path, hash) < 0) { fclose(out); goto done; }
        }
        if (fclose(out)) goto done;
    }
    {
        FILE *out = fopen(origin_path, "w");
        if (!out) goto done;
        fputs("format holy-recipe-origin-1\nsource local-recipe\nverification built-locally\n"
              "converter holy-recipe-1\n", out);
        for (i = 0; i < recipe->source_count; ++i) {
            fprintf(out, "recipe-source %s ", recipe->sources[i].name);
            token(out, recipe->sources[i].url);
            if (recipe->sources[i].hash) fprintf(out, " sha256 %s", recipe->sources[i].hash);
            fputc('\n', out);
        }
        if (fclose(out)) goto done;
    }
    {
        FILE *out = fopen(transform_path, "w");
        if (!out) goto done;
        fprintf(out, "format holy-recipe-transform-1\nbuild-environment recorded\n");
        for (i = 0; i < recipe->build_depend_count; ++i) {
            fputs("build-depend ", out);
            token(out, recipe->build_depends[i].name); fputc('\n', out);
        }
        if (fclose(out)) goto done;
    }
    {
        char filename[512];
        snprintf(filename, sizeof filename, "%s--%s--%s.holy", output_name, arch, libc);
        artifact = joined(output_directory, filename);
        if (!artifact || !holy_pack(tree, artifact)) goto done;
    }
    printf("built %s--%s--%s %llu bytes %zu files\n", output_name, arch, libc, size, included);
    result = 0;
done:
    if (result) fprintf(stderr, "holypkg: build output %s failed\n", output_name);
    if (tree) remove_tree(tree);
    free(tree);
    free(holy); free(files_path); free(meta_path); free(deps_path);
    free(provides_path); free(hooks_path); free(origin_path); free(transform_path);
    free(artifact);
    return result;
}

static int remove_tree(const char *path)
{
    DIR *dir = opendir(path);
    struct dirent *item;
    int result = 0;
    if (!dir) return unlink(path) && errno == ENOENT;
    while ((item = readdir(dir))) {
        char *child;
        if (!strcmp(item->d_name, ".") || !strcmp(item->d_name, "..")) continue;
        child = joined(path, item->d_name);
        if (!child) { result = 1; break; }
        {
            struct stat st;
            if (!lstat(child, &st) && S_ISDIR(st.st_mode)) remove_tree(child);
            else unlink(child);
        }
        free(child);
    }
    closedir(dir);
    return result || rmdir(path);
}

int holy_recipe_build(const char *path, const char *environment, const char *work,
                      const char *output, unsigned jobs, int approve_all,
                      int noninteractive, int keep)
{
    struct recipe recipe = {0};
    struct run_paths paths = {0};
    struct payload payload = {0};
    struct requirement *requirements = NULL;
    size_t requirement_count = 0, requirement_capacity = 0, i, k;
    char *recipe_dir = NULL, *slash = NULL, *own_work = NULL;
    int result = 1, approve_rest = 0;
    time_t started = time(NULL);
    if (!path || !environment || !output || !*output ||
        (strcmp(environment, "host") && strcmp(environment, "clean"))) {
        if (path && environment && !strcmp(environment, "vm")) {
            fprintf(stderr, "holypkg: vm builds need a booted Holy image and qemu-system; "
                    "no such environment is available\n");
            return 6;
        }
        fputs("usage: holypkg build RECIPE --output NEW_DIRECTORY "
              "[--environment host|clean|vm] [--work NEW_DIRECTORY] [--jobs N] "
              "[--yes] [--noninteractive] [--keep]\n", stderr);
        return 2;
    }
    if (mkdir(output, 0700) && errno != EEXIST) {
        fprintf(stderr, "holypkg: output directory unavailable: %s\n", output);
        return 1;
    }
    result = parse_recipe(path, &recipe);
    if (!result) result = validate_recipe(&recipe, path);
    if (result) goto done;
    recipe_dir = strdup(path);
    if (!recipe_dir) { result = 1; goto done; }
    slash = strrchr(recipe_dir, '/');
    if (slash) *slash = 0;
    else strcpy(recipe_dir, ".");
    if (work) {
        struct stat st;
        if (stat(work, &st) || !S_ISDIR(st.st_mode)) {
            fprintf(stderr, "holypkg: build root unavailable: %s\n", work);
            result = 6; goto done;
        }
    } else {
        char temporary[] = "/tmp/holy-recipe-XXXXXX";
        if (!mkdtemp(temporary)) {
            fprintf(stderr, "holypkg: build root unavailable\n");
            result = 6; goto done;
        }
        /* the caller owns a copy; the array above is gone after this block */
        own_work = strdup(temporary);
        if (!own_work) { result = 1; goto done; }
        work = own_work;
    }
    if (populate(work)) { result = 1; goto done; }
    paths.work = strdup(work);
    paths.src = joined(work, "src");
    paths.build = joined(work, "build");
    paths.dest = joined(work, "dest");
    paths.out = joined(work, "out");
    paths.sources = joined(work, "sources");
    if (!paths.work || !paths.src || !paths.build || !paths.dest || !paths.out ||
        !paths.sources) { result = 1; goto done; }
    printf("recipe %s %s-%s environment %s work %s\n",
           recipe.name, recipe.version, recipe.release, environment, work);
    for (i = 0; i < recipe.source_count; ++i) {
        char filename[512], *target;
        const char *url = recipe.sources[i].url;
        int fetch_result;
        snprintf(filename, sizeof filename, "%s", recipe.sources[i].name);
        target = joined(paths.sources, filename);
        if (!target) { result = 1; goto done; }
        if (strstr(url, "://")) {
            fetch_result = recipe.sources[i].hash ?
                holy_fetch_https(url, recipe.sources[i].hash, target, NULL, 0) :
                0;
            if (fetch_result) {
                fprintf(stderr, "holypkg: source %s unavailable from %s (status %d)\n",
                        recipe.sources[i].name, url, fetch_result);
                free(target);
                result = fetch_result;
                goto done;
            }
        } else {
            char *local = *url == '/' ? strdup(url) :
                (safe_path(url) ? joined(recipe_dir, url) : NULL);
            if (!local || !*local || !stage_local(local, target, recipe.sources[i].hash)) {
                fprintf(stderr, "holypkg: source %s unavailable: %s\n",
                        recipe.sources[i].name, url);
                free(local); free(target);
                result = 6; goto done;
            }
            free(local);
        }
        printf("fetched %s sha256 %s\n", recipe.sources[i].name,
               recipe.sources[i].hash ? recipe.sources[i].hash : "unpinned");
        free(target);
    }
    for (i = 0; i < recipe.source_count; ++i) {
        char filename[512];
        struct stat st;
        snprintf(filename, sizeof filename, "%s", recipe.sources[i].name);
        {
            char *archive_path = joined(paths.sources, filename);
            char *target = joined(paths.src, filename);
            if (!archive_path || !target) { free(archive_path); free(target); result = 1; goto done; }
            if (!stat(archive_path, &st) && S_ISREG(st.st_mode)) {
                if (!unpack_archive(archive_path, target) &&
                    !stage_local(archive_path, target, NULL)) {
                    fprintf(stderr, "holypkg: source %s could not enter the source tree\n",
                            recipe.sources[i].name);
                    free(archive_path); free(target);
                    result = 1; goto done;
                }
            }
            free(archive_path);
            free(target);
        }
    }
    for (k = 0; k < sizeof phases / sizeof *phases; ++k) {
        for (i = 0; i < recipe.step_count; ++i) {
            if (strcmp(recipe.steps[i].phase, phases[k])) continue;
            if (recipe.steps[i].output) continue;
            result = run_step(&recipe.steps[i], work, recipe_dir, &recipe, environment, jobs,
                              approve_all, noninteractive, &approve_rest, NULL);
            if (result) goto done;
        }
        if (k == 0 || k == 1) continue;
        if (k == 3 && !recipe.step_count) continue;
    }
    /* a split step fills its own staging tree; the scan then reads that tree */
    for (i = 0; i < recipe.step_count; ++i) {
        char *root, *stage;
        if (!recipe.steps[i].output) continue;
        root = joined(work, "split");
        if (!root) { result = 1; goto done; }
        if (mkdir(root, 0700) && errno != EEXIST) {
            fprintf(stderr, "holypkg: split staging unavailable: %s\n", root);
            free(root);
            result = 1;
            goto done;
        }
        stage = joined(root, recipe.steps[i].output);
        if (!stage || mkdir(stage, 0700)) {
            fprintf(stderr, "holypkg: split staging unavailable for %s\n", recipe.steps[i].output);
            free(root); free(stage);
            result = 1;
            goto done;
        }
        result = run_step(&recipe.steps[i], work, recipe_dir, &recipe, environment, jobs,
                          approve_all, noninteractive, &approve_rest, stage);
        if (!result) result = collect_payload(stage, NULL, recipe.steps[i].output, &payload) ? 0 : 1;
        if (result) {
            fprintf(stderr, "holypkg: split step for %s produced no usable tree\n",
                    recipe.steps[i].output);
            free(root); free(stage);
            goto done;
        }
        printf("split %s staged %s\n", recipe.steps[i].output, stage);
        free(root); free(stage);
    }
    if (!collect_payload(paths.dest, NULL, NULL, &payload)) { result = 1; goto done; }
    qsort(payload.items, payload.count, sizeof *payload.items, entry_order);
    if (!payload.count) {
        fprintf(stderr, "holypkg: package phase installed no files into the destination\n");
        result = 1;
        goto done;
    }
    for (i = 0; i < payload.count; ++i) {
        const struct payload_entry *entry = &payload.items[i];
        struct holy_elf_info info;
        char *absolute;
        if (entry->directory || entry->link || !entry->group) continue;
        absolute = joined(entry->root, entry->path);
        if (!absolute) { result = 1; goto done; }
        if (!holy_elf_read(absolute, &info)) {
            for (k = 0; k < info.needed_count; ++k) {
                struct requirement *grown;
                if (!info.needed[k][0] || strchr(info.needed[k], '/')) continue;
                if (requirement_count == requirement_capacity) {
                    size_t next = requirement_capacity ? requirement_capacity * 2 : 32;
                    grown = realloc(requirements, next * sizeof *grown);
                    if (!grown) { holy_elf_free(&info); free(absolute); result = 1; goto done; }
                    requirements = grown;
                    requirement_capacity = next;
                }
                memset(&requirements[requirement_count], 0, sizeof *grown);
                snprintf(requirements[requirement_count].kind,
                         sizeof requirements[requirement_count].kind, "soname");
                snprintf(requirements[requirement_count].name,
                         sizeof requirements[requirement_count].name, "%s", info.needed[k]);
                snprintf(requirements[requirement_count].arch,
                         sizeof requirements[requirement_count].arch, "%s",
                         recipe.arch ? recipe.arch : "any");
                snprintf(requirements[requirement_count].libc,
                         sizeof requirements[requirement_count].libc, "any");
                snprintf(requirements[requirement_count].relation,
                         sizeof requirements[requirement_count].relation, "any");
                snprintf(requirements[requirement_count].version,
                         sizeof requirements[requirement_count].version, "-");
                snprintf(requirements[requirement_count].evidence,
                         sizeof requirements[requirement_count].evidence, "%s", entry->path);
                snprintf(requirements[requirement_count].scope,
                         sizeof requirements[requirement_count].scope, "%s",
                         entry_output(&recipe, entry));
                snprintf(requirements[requirement_count].group,
                         sizeof requirements[requirement_count].group, "%s", entry->group);
                ++requirement_count;
            }
            holy_elf_free(&info);
        }
        free(absolute);
    }
    for (i = 0; i < recipe.depend_count; ++i) {
        struct requirement *grown = realloc(requirements,
                                     (requirement_count + 1) * sizeof *grown);
        if (!grown) { result = 1; goto done; }
        requirements = grown;
        memset(&grown[requirement_count], 0, sizeof grown[0]);
        snprintf(grown[requirement_count].kind, sizeof grown[0].kind, "%s",
                 recipe.depends[i].kind);
        snprintf(grown[requirement_count].name, sizeof grown[0].name, "%s",
                 recipe.depends[i].name);
        snprintf(grown[requirement_count].arch, sizeof grown[0].arch, "any");
        snprintf(grown[requirement_count].libc, sizeof grown[0].libc, "any");
        snprintf(grown[requirement_count].relation, sizeof grown[0].relation, "%s",
                 recipe.depends[i].relation);
        snprintf(grown[requirement_count].version, sizeof grown[0].version, "%s",
                 recipe.depends[i].version);
        snprintf(grown[requirement_count].evidence, sizeof grown[0].evidence, "recipe-depend");
        ++requirement_count;
    }
    {
        const char *variants[8];
        size_t variant_count = 0;
        unsigned serial = 0;
        for (i = 0; i < payload.group_count; ++i)
            for (k = 0; k < recipe.output_count; ++k)
                if (!strcmp(recipe.outputs[k].kind, "metapackage")) {
                    char arch[64], libc[64], name[512];
                    split_group(payload.groups[i], arch, libc);
                    if (variant_count >= sizeof variants / sizeof *variants) break;
                    snprintf(name, sizeof name, "%s--%s--%s", recipe.outputs[k].name, arch, libc);
                    variants[variant_count] = name;
                    ++variant_count;
                }
        for (i = 0; i < recipe.output_count; ++i) {
            struct output_group groups[8];
            int group_count = collect_output_groups(&recipe, recipe.outputs[i].name,
                                                    &payload, groups,
                                                    sizeof groups / sizeof *groups);
            size_t g;
            if (group_count < 0) {
                fputs("holypkg: too many ABI groups in one output\n", stderr);
                result = 1; goto done;
            }
            if (!strcmp(recipe.outputs[i].kind, "metapackage")) {
                if (emit_output(&recipe, "noarch-nolibc", recipe.outputs[i].name,
                                recipe.outputs[i].kind, output, serial,
                                group_count > 1, &payload, requirements, requirement_count,
                                variants, variant_count)) { result = 1; goto done; }
                ++serial;
                continue;
            }
            if (!group_count) {
                printf("skipped %s no payload assigned\n", recipe.outputs[i].name);
                continue;
            }
            for (g = 0; g < (size_t)group_count; ++g) {
                if (emit_output(&recipe, groups[g].name, recipe.outputs[i].name,
                                recipe.outputs[i].kind, output, serial,
                                group_count > 1, &payload, requirements, requirement_count,
                                NULL, 0)) {
                    result = 1; goto done;
                }
                ++serial;
            }
        }
    }
    printf("build complete %zu files %zu requirements %lds\n",
           payload.count, requirement_count, (long)(time(NULL) - started));
    result = 0;
done:
    if (result && result != 1)
        fprintf(stderr, "holypkg: build failed (status %d)\n", result);
    /* a kept root was not created here when the caller named one, so report it */
    if (work && keep) printf("build root kept %s\n", own_work ? own_work : "(caller)");
    else if (work && !result) remove_tree(work);
    free(requirements);
    payload_free(&payload);
    run_paths_free(&paths);
    free(recipe_dir);
    free(own_work);
    recipe_free(&recipe);
    return result;
}