_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file backends/pkgbuild.c
/* PKGBUILD to holy-recipe(5) conversion; see man/holy-recipe.5 and man/holypkg.8.
the file is read as text. no part of it is executed by the converter. */
#define _XOPEN_SOURCE 700
#define _DEFAULT_SOURCE 1
#include "pkgbuild.h"
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <openssl/evp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
struct kb_value {
char *name;
char *text;
size_t line;
};
struct kb_list {
char *name;
struct kb_value *items;
size_t count;
size_t line;
};
struct kb_block {
char *name;
char *body;
size_t length;
size_t first;
size_t last;
};
struct pkgbuild {
char *text;
size_t length;
char *directory;
struct kb_list *lists;
size_t list_count;
struct kb_value *scalars;
size_t scalar_count;
struct kb_block *blocks;
size_t block_count;
};
static void free_list(struct kb_list *list)
{
size_t i;
for (i = 0; i < list->count; ++i) { free(list->items[i].name); free(list->items[i].text); }
free(list->items);
free(list->name);
}
static void pkgbuild_free(struct pkgbuild *pkg)
{
size_t i;
for (i = 0; i < pkg->list_count; ++i) free_list(&pkg->lists[i]);
for (i = 0; i < pkg->scalar_count; ++i) {
free(pkg->scalars[i].name);
free(pkg->scalars[i].text);
}
for (i = 0; i < pkg->block_count; ++i) {
free(pkg->blocks[i].name);
free(pkg->blocks[i].body);
}
free(pkg->lists);
free(pkg->scalars);
free(pkg->blocks);
free(pkg->directory);
free(pkg->text);
memset(pkg, 0, sizeof *pkg);
}
static char *copy_range(const char *start, size_t length)
{
char *value = malloc(length + 1);
if (!value) return NULL;
memcpy(value, start, length);
value[length] = 0;
return value;
}
/* drops one layer of shell quoting; an unquoted value is used as written. */
static char *unquote(const char *start, size_t length)
{
char *value = copy_range(start, length), *out;
size_t used = 0, i;
if (!value) return NULL;
if (length < 2 || (start[0] != '\'' && start[0] != '"') ||
start[length - 1] != start[0]) return value;
out = malloc(length + 1);
if (!out) { free(value); return NULL; }
for (i = 1; i + 1 < length; ++i) {
char c = start[i];
if (c == '\\' && i + 2 < length &&
(start[i + 1] == '\'' || start[i + 1] == '"' || start[i + 1] == '\\')) {
out[used++] = start[++i];
continue;
}
out[used++] = c;
}
out[used] = 0;
free(value);
return out;
}
static int name_char(char c, int first)
{
if (isalpha((unsigned char)c) || c == '_') return 1;
return !first && isdigit((unsigned char)c);
}
static struct kb_list *list_for(struct pkgbuild *pkg, const char *name, size_t line, int create)
{
size_t i;
struct kb_list *grown;
for (i = 0; i < pkg->list_count; ++i)
if (!strcmp(pkg->lists[i].name, name)) return &pkg->lists[i];
if (!create) return NULL;
grown = realloc(pkg->lists, (pkg->list_count + 1) * sizeof *grown);
if (!grown) return NULL;
pkg->lists = grown;
memset(&grown[pkg->list_count], 0, sizeof grown[0]);
grown[pkg->list_count].name = strdup(name);
if (!grown[pkg->list_count].name) return NULL;
grown[pkg->list_count].line = line;
++pkg->list_count;
return &pkg->lists[pkg->list_count - 1];
}
static const struct kb_list *find_list(const struct pkgbuild *pkg, const char *name)
{
size_t i;
for (i = 0; i < pkg->list_count; ++i)
if (!strcmp(pkg->lists[i].name, name)) return &pkg->lists[i];
return NULL;
}
static const char *find_scalar(const struct pkgbuild *pkg, const char *name)
{
size_t i;
for (i = 0; i < pkg->scalar_count; ++i)
if (!strcmp(pkg->scalars[i].name, name)) return pkg->scalars[i].text;
return NULL;
}
static size_t scalar_line(const struct pkgbuild *pkg, const char *name)
{
size_t i;
for (i = 0; i < pkg->scalar_count; ++i)
if (!strcmp(pkg->scalars[i].name, name)) return pkg->scalars[i].line;
return 0;
}
static int list_push(struct kb_list *list, char *text, size_t line)
{
struct kb_value *grown = realloc(list->items, (list->count + 1) * sizeof *grown);
if (!grown) return 0;
list->items = grown;
memset(&grown[list->count], 0, sizeof grown[0]);
grown[list->count].text = text;
grown[list->count].line = line;
++list->count;
return 1;
}
/* makepkg accepts both source=('a') and a single source=a; several keys are scalars. */
static struct kb_list *list_or_scalar(struct pkgbuild *pkg, const char *key)
{
struct kb_list *list = list_for(pkg, key, 0, 0);
const char *value = find_scalar(pkg, key);
char *copy;
if (list || !value) return list;
list = list_for(pkg, key, scalar_line(pkg, key), 1);
if (!list || !(copy = strdup(value))) return list;
if (!list_push(list, copy, scalar_line(pkg, key))) { free(copy); return NULL; }
return list;
}
static const struct kb_block *find_block(const struct pkgbuild *pkg, const char *name)
{
size_t i;
for (i = 0; i < pkg->block_count; ++i)
if (!strcmp(pkg->blocks[i].name, name)) return &pkg->blocks[i];
return NULL;
}
static int is_sha256(const char *value)
{
size_t i;
for (i = 0; i < 64; ++i)
if (!isxdigit((unsigned char)value[i])) return 0;
return !value[64];
}
/* makepkg expands these names inside source entries; the values are known here. */
static char *expand_names(const char *value, const char *pkgbase, const char *name,
const char *version, const char *release)
{
static const char *const tokens[] = { "$pkgbase", "$pkgname", "$pkgver", "$pkgrel", NULL };
const char *names[4];
char *out = copy_range(value, strlen(value));
size_t index;
if (!out) return NULL;
names[0] = pkgbase;
names[1] = name;
names[2] = version;
names[3] = release;
for (index = 0; tokens[index]; ++index) {
size_t size = strlen(tokens[index]), used = 0, at = 0;
size_t length = strlen(out);
char *next;
if (!strstr(out, tokens[index])) continue;
next = malloc(length + 1);
if (!next) { free(out); return NULL; }
while (at < length) {
if (!strncmp(out + at, tokens[index], size)) {
used += (size_t)sprintf(next + used, "%s", names[index]);
at += size;
continue;
}
next[used++] = out[at++];
}
next[used] = 0;
free(out);
out = next;
}
return out;
}
/* quote aware scan for the end of a list body; returns NULL when unterminated. */
static const char *list_end(const char *cursor, const char *stop)
{
char quote = 0;
for (; cursor < stop; ++cursor) {
char c = *cursor;
if (quote) {
if (c == quote) quote = 0;
else if (c == '\\' && quote == '"' && cursor + 1 < stop) ++cursor;
continue;
}
if (c == '\'' || c == '"') quote = c;
else if (c == ')' || c == '\n') return cursor;
}
return NULL;
}
/* splits the body of a parenthesized list into its elements. */
static int parse_list(struct pkgbuild *pkg, const char *name, const char *body, size_t length,
size_t line)
{
struct kb_list *list = list_for(pkg, name, line, 1);
size_t i = 0;
if (!list) return 0;
/* a shell array separates elements on whitespace, so a comma stays in the name */
while (i < length) {
char quote = 0;
size_t start;
while (i < length && isspace((unsigned char)body[i])) ++i;
if (i >= length) break;
start = i;
while (i < length) {
char c = body[i];
if (quote) {
if (c == quote) quote = 0;
else if (c == '\\' && quote == '"' && i + 1 < length) ++i;
} else if (c == '\'' || c == '"') {
quote = c;
} else if (isspace((unsigned char)c)) {
break;
}
++i;
}
if (quote) return 0;
{
char *text = unquote(body + start, i - start);
if (!text || !list_push(list, text, line)) { free(text); return 0; }
}
}
return 1;
}
/* finds the closing brace of a function body, ignoring quoted and substituted text. */
static const char *block_end(const char *body, const char *stop)
{
size_t depth = 0, i;
char quote = 0;
for (i = 0; body + i < stop; ++i) {
char c = body[i];
if (quote) {
if (c == quote) quote = 0;
else if (c == '\\' && quote == '"') ++i;
continue;
}
if (c == '\'' || c == '"' || c == '`') { quote = c; continue; }
if ((c == '$' || c == '\\') && body + i + 1 < stop &&
(body[i + 1] == '(' || body[i + 1] == '{')) {
char open = body[i + 1];
char close = open == '(' ? ')' : '}';
size_t inner = 0;
for (++i; body + i < stop; ++i) {
if (body[i] == open) ++inner;
else if (body[i] == close) {
if (!inner) break;
--inner;
}
}
continue;
}
if (c == '{') ++depth;
else if (c == '}') {
if (!depth) return body + i;
--depth;
}
}
return NULL;
}
/* reads name=value records, name=( ... ) lists and function bodies. */
static int parse_pkgbuild(struct pkgbuild *pkg)
{
size_t offset = 0, line = 0;
while (offset < pkg->length) {
char *start = pkg->text + offset;
char *newline = memchr(start, '\n', pkg->length - offset);
size_t size = newline ? (size_t)(newline - start) : pkg->length - offset;
size_t name_length = 0, i;
char *cursor;
++line;
offset += size + (newline ? 1 : 0);
while (size && (*start == ' ' || *start == '\t' || start[size - 1] == '\r')) {
++start;
--size;
}
if (!size || *start == '#') continue;
cursor = start;
while (name_length < size && name_char(cursor[name_length], !name_length)) ++name_length;
if (!name_length) {
fprintf(stderr, "holypkg: PKGBUILD:%zu: unsupported top level statement\n", line);
return 2;
}
if (name_length + 2 < size && cursor[name_length] == '(' && cursor[name_length + 1] == ')' &&
isspace((unsigned char)cursor[name_length + 2])) {
const char *brace = memchr(cursor, '{', size);
const char *body = brace ? brace + 1 : start + size;
const char *close = block_end(body, pkg->text + pkg->length);
size_t body_line = line;
struct kb_block block;
if (!close) {
fprintf(stderr, "holypkg: PKGBUILD:%zu: unterminated function\n", line);
return 2;
}
for (i = 0; body + i < close; ++i) if (body[i] == '\n') ++line;
offset = (size_t)(close - pkg->text) + 1;
memset(&block, 0, sizeof block);
block.name = copy_range(start, name_length);
block.body = copy_range(body, (size_t)(close - body));
block.length = (size_t)(close - body);
block.first = body_line;
block.last = line;
if (!block.name || !block.body) {
free(block.name);
free(block.body);
return 2;
}
{
struct kb_block *grown = realloc(pkg->blocks,
(pkg->block_count + 1) * sizeof *grown);
if (!grown) { free(block.name); free(block.body); return 2; }
pkg->blocks = grown;
pkg->blocks[pkg->block_count++] = block;
}
continue;
}
if (name_length >= size || cursor[name_length] != '=') {
fprintf(stderr, "holypkg: PKGBUILD:%zu: unsupported statement\n", line);
return 2;
}
{
char *name = copy_range(start, name_length);
char *value = cursor + name_length + 1;
size_t value_size = size - name_length - 1;
while (value_size && (*value == ' ' || *value == '\t')) { ++value; --value_size; }
if (!name) return 2;
if (value_size && *value == '(') {
const char *body = value + 1;
const char *stop = pkg->text + pkg->length;
size_t body_line = line;
const char *close = list_end(body, value + value_size);
int ok;
while (!close && offset < pkg->length) {
char *row = pkg->text + offset;
char *row_end = memchr(row, '\n', pkg->length - offset);
size_t row_size = row_end ? (size_t)(row_end - row) : pkg->length - offset;
++line;
offset += row_size + (row_end ? 1 : 0);
close = list_end(row, row + row_size);
if (close) break;
}
if (!close) close = stop;
ok = close > body && parse_list(pkg, name, body, (size_t)(close - body), body_line);
free(name);
if (!ok) {
fprintf(stderr, "holypkg: PKGBUILD: unterminated list\n");
return 2;
}
continue;
}
{
char *text = unquote(value, value_size);
struct kb_value *grown = realloc(pkg->scalars,
(pkg->scalar_count + 1) * sizeof *grown);
if (!text || !grown) { free(text); free(name); return 2; }
pkg->scalars = grown;
pkg->scalars[pkg->scalar_count].name = name;
pkg->scalars[pkg->scalar_count].text = text;
pkg->scalars[pkg->scalar_count].line = line;
++pkg->scalar_count;
}
}
}
return 0;
}
struct note {
char **lines;
size_t count;
size_t carried, preserved, helper, unknown, changes;
};
static int note_add(struct note *note, const char *kind, const char *format, ...)
{
char body[1024], *line;
va_list arguments;
char **grown;
va_start(arguments, format);
if (vsnprintf(body, sizeof body, format, arguments) < 0) { va_end(arguments); return 0; }
va_end(arguments);
line = malloc(strlen(kind) + strlen(body) + 2);
if (!line) return 0;
sprintf(line, "%s %s", kind, body);
grown = realloc(note->lines, (note->count + 1) * sizeof *grown);
if (!grown) { free(line); return 0; }
note->lines = grown;
note->lines[note->count++] = line;
if (!strcmp(kind, "carried")) ++note->carried;
else if (!strcmp(kind, "preserved")) ++note->preserved;
else if (!strcmp(kind, "helper")) ++note->helper;
else if (!strcmp(kind, "unknown")) ++note->unknown;
else if (!strcmp(kind, "semantic-change")) ++note->changes;
return 1;
}
static void note_free(struct note *note)
{
size_t i;
for (i = 0; i < note->count; ++i) free(note->lines[i]);
free(note->lines);
memset(note, 0, sizeof *note);
}
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);
}
/* the manager names relations itself, so the upstream operator is translated. */
static const char *relation_name(const char *operator)
{
if (!strcmp(operator, ">=")) return "ge";
if (!strcmp(operator, "<=")) return "le";
if (!strcmp(operator, ">")) return "gt";
if (!strcmp(operator, "<")) return "lt";
if (!strcmp(operator, "=")) return "eq";
return "any";
}
/* writes one dependency keeping the upstream name, its relation and its version. */
static void emit_dependency(FILE *out, const char *raw, const char *kind)
{
static const char *const operators[] = { ">=", "<=", "=", ">", "<", NULL };
char name[256];
size_t used = 0;
const char *cursor = raw;
const char *relation = NULL;
while (*cursor == ' ' || *cursor == '\t') ++cursor;
if (!*cursor) return;
if (!strcmp(kind, "build-depend") && !strncmp(cursor, "cmd:", 4)) {
fputs("build-depend cmd:", out);
token(out, cursor + 4);
fputc('\n', out);
return;
}
/* the name ends at whitespace, at a description colon or at a relation operator */
while (*cursor && !isspace((unsigned char)*cursor) && *cursor != ':' &&
!strchr("<>=~", *cursor) && used + 1 < sizeof name)
name[used++] = *cursor++;
name[used] = 0;
if (!used) return;
while (isspace((unsigned char)*cursor)) ++cursor;
{
size_t index;
for (index = 0; operators[index]; ++index)
if (!strncmp(cursor, operators[index], strlen(operators[index]))) {
relation = operators[index];
cursor += strlen(operators[index]);
break;
}
}
while (*cursor == ' ' || *cursor == '\t') ++cursor;
fputs(kind, out);
fputc(' ', out);
token(out, name);
if (relation && *cursor) {
fputc(' ', out);
token(out, relation_name(relation));
fputc(' ', out);
token(out, cursor);
} else {
fputs(" \"any\" \"-\"", out);
}
fputc('\n', out);
}
struct substitution {
const char *from;
const char *to;
};
static const struct substitution common_variables[] = {
{ "$srcdir", "$HOLY_SRC" },
{ "$startdir", "$HOLY_BUILD" },
{ "$builddir", "$HOLY_BUILD" },
{ "$srcdest", "$HOLY_WORK/sources" },
{ "$pkgdest", "$HOLY_OUT" }
};
/* rewrites the makepkg working variables the manager already exports.
a reference inside a double quoted word needs no quoting of its own. */
static char *substitute(const char *body, size_t length, const char *pkgdir, size_t *out_length)
{
struct substitution variables[6];
size_t capacity = length * 2 + 256, used = 0, i = 0, count = 0, index;
char quote = 0;
char *out = malloc(capacity);
if (!out) return NULL;
variables[count].from = "$pkgdir";
variables[count++].to = pkgdir;
for (index = 0; index < sizeof common_variables / sizeof *common_variables; ++index)
variables[count++] = common_variables[index];
while (i < length) {
size_t rule;
int matched = 0;
for (rule = 0; quote != '\'' && rule < count; ++rule) {
size_t size = strlen(variables[rule].from);
size_t size_to = strlen(variables[rule].to);
size_t end = i + size;
if (strncmp(body + i, variables[rule].from, size)) continue;
if (body[end] == '{') {
const char *close = memchr(body + end, '}', length - end);
if (!close) continue;
end = (size_t)(close - body) + 1;
}
if (used + size_to + 2 > capacity) {
char *grown = realloc(out, capacity * 2);
if (!grown) { free(out); return NULL; }
capacity *= 2;
out = grown;
}
/* inside quotes the variable expands in place, outside it is one word */
if (quote != '"') out[used++] = '"';
memcpy(out + used, variables[rule].to, size_to);
used += size_to;
if (quote != '"') out[used++] = '"';
i = end;
matched = 1;
break;
}
if (matched) continue;
if (quote) {
if (body[i] == quote) quote = 0;
else if (body[i] == '\\' && quote == '"' && i + 1 < length) out[used++] = body[i++];
} else if (body[i] == '\'' || body[i] == '"') {
quote = body[i];
}
out[used++] = body[i++];
}
out[used] = 0;
if (out_length) *out_length = used;
return out;
}
/* the makepkg variables a phase body may read, rebuilt from the exported paths. */
static char *prologue(const char *name, const char *version, const char *release,
const char *arch, int split)
{
char *text = NULL;
size_t used = 0;
FILE *out = open_memstream(&text, &used);
if (!out) return NULL;
fputs("# makepkg variables translated from PKGBUILD\n", out);
fprintf(out, "pkgname=%s\npkgver=%s\npkgrel=%s\narch=%s\n", name, version, release, arch);
fputs("srcdir=\"$HOLY_SRC\"\nstartdir=\"$HOLY_BUILD\"\nbuilddir=\"$HOLY_BUILD\"\n", out);
fprintf(out, "pkgdir=%s\n", split ? "\"$HOLY_SPLIT_DEST\"" : "\"$HOLY_DEST\"");
fputs("srcdest=\"$HOLY_WORK/sources\"\npkgdest=\"$HOLY_OUT\"\n", out);
fputs("CFLAGS=${CFLAGS:--O2 -pipe}\nCXXFLAGS=${CXXFLAGS:-$CFLAGS}\n", out);
fputs("CPPFLAGS=${CPPFLAGS:-}\nLDFLAGS=${LDFLAGS:--Wl,-z,relro -Wl,--as-needed}\n", out);
fputs("MAKEFLAGS=${MAKEFLAGS:--j$HOLY_JOBS}\n", out);
fclose(out);
return text;
}
static int emit_block(FILE *out, const char *phase, const char *tag, const char *prefix,
const char *pkgdir, const struct kb_block *block)
{
size_t length = 0;
char *body = substitute(block->body, block->length, pkgdir, &length);
int result = 1;
if (!body) return 0;
if (fprintf(out, "step %s /bin/bash <<%s\n", phase, tag) < 0 ||
fputs(prefix, out) < 0 || fwrite(body, 1, length, out) != length ||
fprintf(out, "\n%s\n", tag) < 0) result = 0;
free(body);
return result;
}
/* a package_NAME function fills the staging tree of one named output. */
static int emit_split_block(FILE *out, const char *output, const char *prefix,
const struct kb_block *block)
{
size_t length = 0;
char *body = substitute(block->body, block->length, "$HOLY_SPLIT_DEST", &length);
int result = 1;
if (!body) return 0;
if (fputs("split-step ", out) < 0 || fputs(output, out) < 0 ||
fputs(" split /bin/bash <<SPLIT\n", out) < 0 ||
fputs(prefix, out) < 0 || fwrite(body, 1, length, out) != length ||
fprintf(out, "\nSPLIT\n") < 0) result = 0;
free(body);
return result;
}
static int write_digest(const char *target, const char *input, char hash[65])
{
unsigned char buffer[65536], bytes[32];
unsigned size = 0;
size_t got, i;
EVP_MD_CTX *context = EVP_MD_CTX_new();
FILE *in = fopen(input, "rb"), *out = fopen(target, "wb");
int result = 0;
if (!in || !out || !context || EVP_DigestInit_ex(context, EVP_sha256(), NULL) != 1) goto done;
while ((got = fread(buffer, 1, sizeof buffer, in)) > 0)
if (EVP_DigestUpdate(context, buffer, got) != 1 ||
fwrite(buffer, 1, got, out) != got) goto done;
if (ferror(in) || EVP_DigestFinal_ex(context, bytes, &size) != 1 || size != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(hash + i * 2, 3, "%02x", bytes[i]);
hash[64] = 0;
result = !fflush(out);
done:
if (out) { if (fclose(out) && !result) result = 0; }
if (in) fclose(in);
EVP_MD_CTX_free(context);
if (result) chmod(target, 0600);
else unlink(target);
return result;
}
int holy_convert_pkgbuild(const char *input, const char *source, const char *output)
{
struct pkgbuild pkg = {0};
struct note note = {0};
struct { char name[512]; const struct kb_block *block; } splits[16];
struct { char base[256]; char installed[600]; } hook_paths[8];
FILE *out = NULL;
char hash[65] = {0};
char recipe_path[4096], report_path[4096], target[4096];
char name[512] = {0}, arch[64] = "x86_64", version[512] = {0}, release[64] = {0};
const char *license = NULL;
size_t splits_used = 0, hooks = 0, k;
int result = 1, review = 0, i, wrote = 1;
if (!input || !output || !*output) {
fputs("usage: holypkg convert PKGBUILD --source NAME --output NEW_DIRECTORY\n", stderr);
return 2;
}
if (!source || !*source || !strcmp(source, "local")) {
fputs("holypkg: a converted recipe needs the source name it came from\n", stderr);
return 2;
}
for (i = 0; source[i]; ++i)
if ((unsigned char)source[i] <= 32 || source[i] == ':' || source[i] == '/' ||
source[i] == '@') {
fputs("holypkg: invalid source name for conversion\n", stderr);
return 2;
}
{
struct stat st;
FILE *file;
if (stat(input, &st) || !S_ISREG(st.st_mode) || st.st_size > 4 * 1024 * 1024) {
fprintf(stderr, "holypkg: PKGBUILD unavailable: %s\n", input);
return 6;
}
pkg.length = (size_t)st.st_size;
pkg.text = malloc(pkg.length + 1);
file = pkg.text ? fopen(input, "rb") : NULL;
if (!file || fread(pkg.text, 1, pkg.length, file) != pkg.length) {
if (file) fclose(file);
pkgbuild_free(&pkg);
fputs("holypkg: PKGBUILD could not be read\n", stderr);
return 6;
}
fclose(file);
pkg.text[pkg.length] = 0;
}
pkg.directory = strdup(input);
if (!pkg.directory) { pkgbuild_free(&pkg); return 1; }
{
char *slash = strrchr(pkg.directory, '/');
if (slash) *slash = 0;
else strcpy(pkg.directory, ".");
}
result = parse_pkgbuild(&pkg);
if (result) goto done;
{
const char *pkgname = find_scalar(&pkg, "pkgname");
const char *pkgbase = find_scalar(&pkg, "pkgbase");
const char *declared = find_scalar(&pkg, "pkgver");
const char *packager = find_scalar(&pkg, "pkgrel");
const struct kb_list *arches = find_list(&pkg, "arch");
const struct kb_list *licenses = find_list(&pkg, "license");
if (!pkgbase) pkgbase = pkgname;
if (!pkgbase || !declared || !packager) {
fputs("holypkg: PKGBUILD: pkgbase, pkgver and pkgrel are required\n", stderr);
result = 2;
goto done;
}
if (strpbrk(declared, "$`") || strpbrk(packager, "$`") ||
strlen(pkgbase) > 480 || strlen(declared) > 480 || strlen(packager) > 60) {
fputs("holypkg: PKGBUILD: identity must be literal and short\n", stderr);
result = 2;
goto done;
}
snprintf(name, sizeof name, "%s", pkgbase);
snprintf(version, sizeof version, "%s", declared);
snprintf(release, sizeof release, "%s", packager);
if (licenses && licenses->count) {
char joined[512];
size_t used = 0;
for (k = 0; k < licenses->count; ++k)
used += (size_t)snprintf(joined + used, sizeof joined - used, "%s%s",
used ? " " : "", licenses->items[k].text);
license = strdup(joined);
} else {
license = strdup(find_scalar(&pkg, "license") ? find_scalar(&pkg, "license") : "");
}
if (!license) { result = 1; goto done; }
if (arches) {
int x86_64 = 0, i686 = 0, noarch = 0, any = 0;
for (k = 0; k < arches->count; ++k) {
if (!strcmp(arches->items[k].text, "x86_64")) x86_64 = 1;
else if (!strcmp(arches->items[k].text, "x86")) i686 = 1;
else if (!strcmp(arches->items[k].text, "noarch")) noarch = 1;
else if (!strcmp(arches->items[k].text, "any")) any = 1;
}
if (noarch && arches->count == 1) snprintf(arch, sizeof arch, "noarch");
else if (i686 && !x86_64) snprintf(arch, sizeof arch, "i686");
else if (x86_64) snprintf(arch, sizeof arch, "x86_64");
else if (any && arches->count == 1) snprintf(arch, sizeof arch, "any");
else {
snprintf(arch, sizeof arch, "%s", arches->items[0].text);
review = 1;
note_add(¬e, "unknown", "arch list PKGBUILD:%zu", arches->line);
}
}
}
note_add(¬e, "carried", "name PKGBUILD:%zu", scalar_line(&pkg, "pkgbase") ?
scalar_line(&pkg, "pkgbase") : scalar_line(&pkg, "pkgname"));
note_add(¬e, "carried", "version PKGBUILD:%zu", scalar_line(&pkg, "pkgver"));
note_add(¬e, "carried", "release PKGBUILD:%zu", scalar_line(&pkg, "pkgrel"));
for (k = 0; k < pkg.block_count; ++k) {
const char *block_name = pkg.blocks[k].name;
if (!strcmp(block_name, "prepare") || !strcmp(block_name, "build") ||
!strcmp(block_name, "check") || !strcmp(block_name, "package")) continue;
if (strncmp(block_name, "package_", 8) || !block_name[8] ||
splits_used >= sizeof splits / sizeof *splits ||
snprintf(splits[splits_used].name, sizeof splits[splits_used].name, "%s-%s",
name, block_name + 8) >= (int)sizeof splits[splits_used].name) {
review = 1;
note_add(¬e, "unknown", "function %s PKGBUILD:%zu-%zu",
block_name, pkg.blocks[k].first, pkg.blocks[k].last);
continue;
}
splits[splits_used].block = &pkg.blocks[k];
++splits_used;
}
{
static const char *const unmapped[] = {
"noextract", "validpgpkeys", "options", "b2sums", "md5sums",
"source_x86_64", "source_i686", "source_aarch64", "source_any", NULL
};
size_t index;
for (index = 0; unmapped[index]; ++index) {
const struct kb_list *list = find_list(&pkg, unmapped[index]);
if (!list) continue;
review = 1;
note_add(¬e, "unknown", "%s PKGBUILD:%zu", unmapped[index], list->line);
}
}
if (mkdir(output, 0700) && errno != EEXIST) {
fprintf(stderr, "holypkg: conversion directory unavailable: %s\n", output);
result = 1;
goto done;
}
snprintf(target, sizeof target, "%s/PKGBUILD", output);
if (!write_digest(target, input, hash)) {
fprintf(stderr, "holypkg: PKGBUILD copy failed\n");
result = 1;
goto done;
}
snprintf(recipe_path, sizeof recipe_path, "%s/%s.recipe", output, name);
snprintf(report_path, sizeof report_path, "%s/conversion", output);
out = fopen(recipe_path, "wb");
if (!out) {
fprintf(stderr, "holypkg: recipe unavailable: %s\n", recipe_path);
result = 1;
goto done;
}
fputs("format holy-recipe-1\nname ", out); token(out, name);
fputs("\nversion ", out); token(out, version);
fputs("\nrelease ", out); token(out, release);
fputs("\narch ", out); token(out, arch);
fputs("\nlibc any\n", out);
{
const char *summary = find_scalar(&pkg, "pkgdesc");
const char *homepage = find_scalar(&pkg, "url");
if (summary) { fputs("summary ", out); token(out, summary); fputc('\n', out); }
if (homepage) { fputs("homepage ", out); token(out, homepage); fputc('\n', out); }
if (*license) { fputs("license ", out); token(out, license); fputc('\n', out); }
}
fputs("x-source-family pacman\nx-converter pkgbuild-1\n", out);
{
const char *epoch = find_scalar(&pkg, "epoch");
const char *groups = find_scalar(&pkg, "groups");
const char *data = find_scalar(&pkg, "xdata");
if (epoch) { fputs("x-epoch ", out); token(out, epoch); fputc('\n', out); }
if (groups) { fputs("x-groups ", out); token(out, groups); fputc('\n', out); }
if (data) { fputs("x-data ", out); token(out, data); fputc('\n', out); }
}
{
const struct kb_list *sources = find_list(&pkg, "source");
const struct kb_list *sums = find_list(&pkg, "sha256sums");
const char *pkgname = find_scalar(&pkg, "pkgname");
for (k = 0; sources && k < sources->count; ++k) {
char *url = expand_names(sources->items[k].text, name,
pkgname ? pkgname : name, version, release);
const char *base = url ? strrchr(url, '/') : NULL;
const char *digest = sums && k < sums->count && is_sha256(sums->items[k].text) ?
sums->items[k].text : NULL;
char copied[4096], original[4096], ignored[65];
struct stat st;
int ok = url && *url;
base = base ? base + 1 : url;
if (ok && (!base || !*base || strlen(base) > 400 || strpbrk(base, " \t$`'\"") ||
!strcmp(base, ".") || !strcmp(base, ".."))) ok = 0;
/* a local source travels with the recipe, so it is copied beside it */
if (ok && !strpbrk(url, ":/")) {
snprintf(original, sizeof original, "%s/%s", pkg.directory, url);
snprintf(copied, sizeof copied, "%s/%s", output, base);
if (stat(original, &st) || !S_ISREG(st.st_mode) ||
!write_digest(copied, original, ignored)) {
review = 1;
note_add(¬e, "unknown", "local source %s is not next to the PKGBUILD "
"PKGBUILD:%zu", url, sources->items[k].line);
free(url);
continue;
}
note_add(¬e, "semantic-change", "local source %s copied next to the recipe",
url);
}
if (!ok) {
review = 1;
note_add(¬e, "unknown", "source %s PKGBUILD:%zu",
sources->items[k].text, sources->items[k].line);
free(url);
continue;
}
fputs("source ", out);
token(out, base);
fputc(' ', out);
token(out, url);
fputc('\n', out);
if (digest) {
fputs("source-sha256 ", out);
token(out, base);
fputc(' ', out);
token(out, digest);
fputc('\n', out);
} else if (strpbrk(url, ":/")) {
review = 1;
note_add(¬e, "unknown", "source %s has no sha256sum PKGBUILD:%zu",
url, sources->items[k].line);
}
note_add(¬e, "carried", "source %s PKGBUILD:%zu", url, sources->items[k].line);
free(url);
}
}
{
static const char *const build_lists[] = { "makedepends", "checkdepends", NULL };
size_t index;
for (index = 0; build_lists[index]; ++index) {
const struct kb_list *list = find_list(&pkg, build_lists[index]);
for (k = 0; list && k < list->count; ++k) {
emit_dependency(out, list->items[k].text, "build-depend");
note_add(¬e, "carried", "%s %s PKGBUILD:%zu",
build_lists[index], list->items[k].text, list->items[k].line);
}
}
}
{
const struct kb_list *depends = find_list(&pkg, "depends");
for (k = 0; depends && k < depends->count; ++k) {
emit_dependency(out, depends->items[k].text, "depend");
note_add(¬e, "carried", "depends %s PKGBUILD:%zu",
depends->items[k].text, depends->items[k].line);
}
}
{
static const char *const kept[] = { "provides", "conflicts", "replaces", NULL };
size_t index;
for (index = 0; kept[index]; ++index) {
const struct kb_list *list = find_list(&pkg, kept[index]);
for (k = 0; list && k < list->count; ++k) {
fputs("x-", out);
fputs(kept[index], out);
fputc(' ', out);
token(out, list->items[k].text);
fputc('\n', out);
review = 1;
note_add(¬e, "carried", "%s %s PKGBUILD:%zu",
kept[index], list->items[k].text, list->items[k].line);
}
}
}
{
const struct kb_list *optional = find_list(&pkg, "optdepends");
for (k = 0; optional && k < optional->count; ++k) {
const char *text = optional->items[k].text;
const char *colon = strchr(text, ':');
const char *hint = colon ? colon + 1 : NULL;
char item[512];
while (hint && isspace((unsigned char)*hint)) ++hint;
snprintf(item, sizeof item, "%.*s", colon ? (int)(colon - text) : (int)strlen(text),
text);
fputs("x-optdepend ", out);
token(out, item);
if (hint && *hint) { fputc(' ', out); token(out, hint); }
fputc('\n', out);
note_add(¬e, "carried", "optdepends %s PKGBUILD:%zu", text,
optional->items[k].line);
}
}
{
const struct kb_list *backup = find_list(&pkg, "backup");
for (k = 0; backup && k < backup->count; ++k) {
const char *path = backup->items[k].text;
if (strncmp(path, "etc/", 4) || strpbrk(path, "$` \t")) {
review = 1;
note_add(¬e, "unknown", "backup %s PKGBUILD:%zu", path, backup->items[k].line);
continue;
}
fputs("config ", out);
token(out, path);
fputc('\n', out);
note_add(¬e, "carried", "backup %s PKGBUILD:%zu", path, backup->items[k].line);
}
}
{
const struct kb_list *installs = list_or_scalar(&pkg, "install");
for (k = 0; installs && k < installs->count; ++k) {
const char *file = installs->items[k].text;
const char *base = strrchr(file, '/');
char installed[600], copied[4096], original[4096], ignored[65];
struct stat st;
size_t size;
base = base ? base + 1 : file;
if (strpbrk(file, "$` \t") || !base[0] || strlen(base) > 200 ||
!strcmp(base, ".") || !strcmp(base, "..")) {
review = 1;
note_add(¬e, "unknown", "install %s PKGBUILD:%zu", file, installs->items[k].line);
continue;
}
size = strlen(base);
if (size > 8 && !strcmp(base + size - 8, ".install"))
snprintf(installed, sizeof installed, "usr/share/holy/%s/%s", name, base);
else
snprintf(installed, sizeof installed, "usr/share/holy/%s/%s.install", name, base);
snprintf(original, sizeof original, "%s/%s", pkg.directory, file);
snprintf(copied, sizeof copied, "%s/%s", output, base);
if (stat(original, &st) || !S_ISREG(st.st_mode) ||
!write_digest(copied, original, ignored)) {
review = 1;
note_add(¬e, "unknown", "install %s is not next to the PKGBUILD PKGBUILD:%zu",
file, installs->items[k].line);
continue;
}
fputs("source ", out);
token(out, base);
fputc(' ', out);
token(out, base);
fputc('\n', out);
fputs("hook-install /bin/bash ", out);
token(out, installed);
fputc('\n', out);
if (hooks >= sizeof hook_paths / sizeof *hook_paths) continue;
snprintf(hook_paths[hooks].base, sizeof hook_paths[hooks].base, "%s", base);
snprintf(hook_paths[hooks].installed, sizeof hook_paths[hooks].installed, "%s", installed);
++hooks;
note_add(¬e, "preserved", "hook %s PKGBUILD:%zu", file, installs->items[k].line);
note_add(¬e, "semantic-change",
"%s installed as %s; pre_install and post_install become one postinstall",
file, installed);
}
}
{
const struct kb_list *sources = find_list(&pkg, "source");
if (sources && sources->count == 1) {
const char *pkgname = find_scalar(&pkg, "pkgname");
char *url = expand_names(sources->items[0].text, name,
pkgname ? pkgname : name, version, release);
const char *base = url ? strrchr(url, '/') : NULL;
base = base ? base + 1 : url;
if (url && *base && !strpbrk(base, " \t$`'\"")) {
/* makepkg points srcdir at the directory inside the archive, not at
the archive name, so the extracted content is lifted one level. */
fputs("step unpack /bin/sh <<UNPACK\n", out);
fprintf(out, "for entry in \"$HOLY_SRC/%s\"/* \"$HOLY_SRC/%s\"/.[!.]* "
"\"$HOLY_SRC/%s\"/..?*; do\n"
" [ -e \"$entry\" ] || continue\n"
" mv \"$entry\" \"$HOLY_SRC/\"\n"
"done\n"
"rm -rf \"$HOLY_SRC/%s\"\n"
"UNPACK\n", base, base, base, base);
note_add(¬e, "semantic-change",
"single source lifted so HOLY_SRC matches the makepkg srcdir");
}
free(url);
}
}
{
const struct kb_block *package = find_block(&pkg, "package");
const struct kb_block *prepare = find_block(&pkg, "prepare");
const struct kb_block *build = find_block(&pkg, "build");
const struct kb_block *check = find_block(&pkg, "check");
char *prefix = prologue(name, version, release, arch, 0);
if (!prefix) { result = 1; goto done; }
fputs("output ", out);
token(out, name);
fputs(package ? " runtime\n" : " metapackage\n", out);
for (k = 0; k < splits_used; ++k) {
fputs("output ", out);
token(out, splits[k].name);
fputs(" runtime\n", out);
}
if (package) {
if (prepare && !emit_block(out, "prepare", "PREPARE", prefix, "$HOLY_DEST", prepare))
wrote = 0;
if (build && !emit_block(out, "build", "BUILD", prefix, "$HOLY_DEST", build)) wrote = 0;
if (check && !emit_block(out, "check", "CHECK", prefix, "$HOLY_DEST", check)) wrote = 0;
if (!emit_block(out, "package", "PACKAGE", prefix, "$HOLY_DEST", package)) wrote = 0;
}
free(prefix);
for (k = 0; k < hooks; ++k) {
char directory[600];
char *slash;
snprintf(directory, sizeof directory, "%s", hook_paths[k].installed);
slash = strrchr(directory, '/');
if (!slash) { wrote = 0; break; }
*slash = 0;
if (fprintf(out, "step package /bin/sh <<HOOKS\n"
"mkdir -p \"$HOLY_DEST/%s\"\n"
"cp \"$HOLY_SRC/%s\" \"$HOLY_DEST/%s\"\n"
"HOOKS\n", directory, hook_paths[k].base,
hook_paths[k].installed) < 0) wrote = 0;
}
for (k = 0; k < splits_used && wrote; ++k) {
char *split_prefix = prologue(name, version, release, arch, 1);
if (!split_prefix || !emit_split_block(out, splits[k].name, split_prefix,
splits[k].block)) wrote = 0;
free(split_prefix);
}
if (package) {
note_add(¬e, "preserved", "package PKGBUILD:%zu-%zu",
package->first, package->last);
if (prepare) note_add(¬e, "preserved", "prepare PKGBUILD:%zu-%zu",
prepare->first, prepare->last);
if (build) note_add(¬e, "preserved", "build PKGBUILD:%zu-%zu",
build->first, build->last);
if (check) note_add(¬e, "preserved", "check PKGBUILD:%zu-%zu",
check->first, check->last);
}
for (k = 0; k < splits_used; ++k)
note_add(¬e, "preserved", "split %s PKGBUILD:%zu-%zu", splits[k].name,
splits[k].block->first, splits[k].block->last);
}
if (find_block(&pkg, "prepare"))
note_add(¬e, "helper", "makepkg prepare environment not carried");
if (find_block(&pkg, "build"))
note_add(¬e, "helper", "makepkg.conf CFLAGS and LDFLAGS profile not carried");
if (find_block(&pkg, "check"))
note_add(¬e, "helper", "makepkg check environment not carried");
if (fflush(out) || fclose(out)) { out = NULL; result = 1; goto done; }
out = NULL;
if (!wrote) {
unlink(recipe_path);
fputs("holypkg: the converted recipe could not be written\n", stderr);
result = 1;
goto done;
}
out = fopen(report_path, "wb");
if (!out) {
fprintf(stderr, "holypkg: report unavailable: %s\n", report_path);
result = 1;
goto done;
}
fputs("format holy-recipe-conversion-1\nconverter pkgbuild-1\n", out);
fputs("source-name ", out); token(out, source); fputc('\n', out);
fputs("source-file PKGBUILD\n", out);
fprintf(out, "source-sha256 %s\n", hash);
fputs("pkgbase ", out); token(out, name); fputc('\n', out);
fputs("version ", out); token(out, version); fputc('\n', out);
fputs("release ", out); token(out, release); fputc('\n', out);
fputs("arch ", out); token(out, arch); fputc('\n', out);
fputs("recipe ", out); token(out, name); fputs(".recipe\n", out);
fprintf(out, "status %s\n", review ? "review-required" : "native");
for (k = 0; k < note.count; ++k) fprintf(out, "%s\n", note.lines[k]);
fprintf(out, "summary carried %zu preserved %zu helper %zu unknown %zu changes %zu\n",
note.carried, note.preserved, note.helper, note.unknown, note.changes);
if (fflush(out) || fclose(out)) { out = NULL; result = 1; goto done; }
out = NULL;
printf("converted %s status %s\n", name, review ? "review-required" : "native");
printf("recipe %s\nreport %s\n", recipe_path, report_path);
if (review) {
fputs("holypkg: the converted recipe needs review before its first build\n", stderr);
result = 3;
} else {
result = 0;
}
done:
if (out) fclose(out);
if (result > 1 && result != 3)
fprintf(stderr, "holypkg: PKGBUILD conversion failed (status %d)\n", result);
note_free(¬e);
free((char *)license);
pkgbuild_free(&pkg);
return result;
}