/* 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 #include #include #include #include #include #include #define _DEFAULT_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 <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 <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; }