_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file backends/aports.c
/* APKBUILD 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.
the abuild phase bodies keep their original shell; a step prologue rebuilds the
abuild variables from the exported Holy paths. */
#define _XOPEN_SOURCE 700
#define _DEFAULT_SOURCE 1
#include "aports.h"
#include "shrecipe.h"
#include <ctype.h>
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
/* a byte search over a template body; the bodies are short and few */
static const char *find_bytes(const char *body, size_t length, const char *needle)
{
size_t size = strlen(needle), index;
if (!size || size > length) return NULL;
for (index = 0; index + size <= length; ++index)
if (!memcmp(body + index, needle, size)) return body + index;
return NULL;
}
/* the abuild split functions that move files without one being written */
static const struct {
const char *name;
const char *files;
} default_splits[] = {
{ "dev", "headers, static archives, pkg-config files and unversioned .so files" },
{ "doc", "usr/share/{doc,man,info,html,sgml,licenses,gtk-doc,ri,help}" },
{ "static", "static archives from lib and usr/lib" },
{ "openrc", "etc/conf.d and etc/init.d" },
{ "libs", "shared objects without their unversioned symlink" }
};
static const char *default_split_help(const char *name)
{
size_t index;
for (index = 0; index < sizeof default_splits / sizeof *default_splits; ++index)
if (!strcmp(default_splits[index].name, name)) return default_splits[index].files;
return NULL;
}
struct apk_source {
char name[512];
char url[1024];
char declared[512];
size_t line;
};
/* the helpers and variables a body uses that this converter cannot supply. a split
step does carry $subpkgdir, so that name is only unresolved in the main package. */
static void report_unknowns(const char *body, size_t length, struct recipe_note *note, int *review,
const char *where, int split)
{
static const struct {
const char *token;
const char *text;
} unresolved[] = {
{ "default_dev", "helper default_dev is not carried" },
{ "default_doc", "helper default_doc is not carried" },
{ "default_static", "helper default_static is not carried" },
{ "default_openrc", "helper default_openrc is not carried" },
{ "default_unpack", "helper default_unpack is not carried" },
{ "default_prepare", "helper default_prepare is not carried" },
{ "default_check", "helper default_check is not carried" },
{ "default_python", "helper default_python is not carried" },
{ "default_luarocks", "helper default_luarocks is not carried" },
{ "default_cargo", "helper default_cargo is not carried" },
{ "default_pie", "helper default_pie is not carried" },
{ "default_perl", "helper default_perl is not carried" },
{ "default_ruby", "helper default_ruby is not carried" },
{ "default_meson", "helper default_meson is not carried" },
{ "lang6to4", "helper lang6to4 is not carried" },
{ "fetch", "helper fetch is not carried" },
{ "verify", "helper verify is not carried" },
{ "unpack", "helper unpack is not carried" },
{ "mkusers", "helper mkusers is not carried" },
{ "snapshot", "helper snapshot is not carried" },
{ "update_abuild_depends", "helper update_abuild_depends is not carried" },
{ "abuild_depends", "helper abuild_depends is not carried" },
{ "run_rc_service", "helper run_rc_service is not carried" },
{ "scanelf", "helper scanelf is not carried" },
{ "abuild-checksums", "helper abuild-checksums is not carried" },
{ "abuild-tar", "helper abuild-tar is not carried" },
{ "$CBUILD", "build architecture is not carried" },
{ "$CHOST", "host architecture is not carried" },
{ "$CTARGET", "target architecture is not carried" },
{ "$CARCH", "architecture is not carried" },
{ "$CROSS_COMPILE", "cross compilation is not carried" },
{ "$CBUILD_ROOT", "cross build root is not carried" },
{ "$CTARGET_ROOT", "cross target root is not carried" },
{ "$CHOST_ROOT", "cross host root is not carried" },
{ "$repo", "abuild repository scope is not carried" },
{ "$GIT_COMMIT", "abuild git state is not carried" },
{ "$abuild_basename", "abuild repository scope is not carried" },
{ "$REPODEST", "abuild repository scope is not carried" },
{ "$srcdest", "abuild distfile cache is not carried" },
{ "$tmpdir", "abuild working directory is not carried" },
{ "$pkgbasedir", "abuild working directory is not carried" },
{ "$subpkgdir", "the subpackage staging tree is not carried inside the main package" },
{ "$startdir", "abuild source directory is not carried" }
};
size_t index;
for (index = 0; index < sizeof unresolved / sizeof *unresolved; ++index) {
if (split && !strcmp(unresolved[index].token, "$subpkgdir")) continue;
if (find_bytes(body, length, unresolved[index].token)) {
holy_note_add(note, "unknown", "%s in %s", unresolved[index].text, where);
*review = 1;
}
}
}
/* the abuild variables a phase body may read, rebuilt from the exported paths */
static char *prologue(const struct shell_script *pkg, const char *name, const char *version,
const char *release, const char *builddir, const char *pkgdir)
{
static const char *const carried[] = {
"patch_args", "conflicts", "replaces", "provides", "install_if", "triggers",
"pkgusers", "pkggroups", "provider_priority", "replaces_priority", "options",
"makedepends", "makedepends_build", "makedepends_host", "checkdepends", NULL
};
static const struct { const char *key; const char *text; } abuild[] = {
{ "CFLAGS", "\"${CFLAGS:--Os -fomit-frame-pointer}\"" },
{ "CXXFLAGS", "\"${CXXFLAGS:-$CFLAGS}\"" },
{ "CPPFLAGS", "\"${CPPFLAGS:-}\"" },
{ "LDFLAGS", "\"${LDFLAGS:--Wl,-z,relro}\"" },
{ "JOBS", "\"$HOLY_JOBS\"" },
{ "MAKEFLAGS", "\"-j$HOLY_JOBS\"" },
{ "SAMUFLAGS", "\"-j$HOLY_JOBS\"" },
{ "NINJAFLAGS", "\"-j$HOLY_JOBS\"" },
{ "CARGO_BUILD_JOBS", "\"$HOLY_JOBS\"" },
{ "CMAKE_BUILD_PARALLEL_LEVEL", "\"$HOLY_JOBS\"" },
{ "CTEST_PARALLEL_LEVEL", "\"$HOLY_JOBS\"" },
{ "DISTDIR", "\"$HOLY_WORK\"" },
{ "PKGDIR", "\"$HOLY_DEST\"" },
{ "GOTMPDIR", "\"$HOLY_BUILD/gotmp\"" },
{ "HOME", "\"$HOLY_WORK/home\"" },
{ "TMPDIR", "\"$HOLY_WORK/tmp\"" }
};
char *text = NULL;
size_t used = 0, index;
FILE *out = open_memstream(&text, &used);
if (!out) return NULL;
fputs("# abuild variables rebuilt from the exported Holy paths\n", out);
fprintf(out, "pkgname=%s\npkgver=%s\npkgrel=%s\npkgver_real=\"%s-r%s\"\n", name, version,
release, version, release);
fprintf(out, "srcdir=\"$HOLY_SRC\"\nstartdir=\"$HOLY_WORK\"\nbuilddir=%s\n", builddir);
/* DESTDIR is the tree being filled, so a split function fills $HOLY_SPLIT_DEST
while $pkgdir keeps naming the main tree it moves files out of */
fprintf(out, "DESTDIR=%s\n", pkgdir);
fprintf(out, "PKGDESTDIR=%s\n", pkgdir);
fputs("pkgdir=\"$HOLY_DEST\"\nsubpkgdir=\"$HOLY_SPLIT_DEST\"\n", out);
for (index = 0; index < sizeof abuild / sizeof *abuild; ++index)
fprintf(out, "%s=%s\n", abuild[index].key, abuild[index].text);
for (index = 0; carried[index]; ++index) {
char *value;
if (!holy_shell_present(pkg, carried[index])) continue;
value = holy_shell_join(pkg, carried[index]);
if (!value || !*value) { free(value); continue; }
fprintf(out, "%s=\"%s\"\n", carried[index], value);
free(value);
}
fclose(out);
return text;
}
/* the identity variables an entry may name; the rest stay unresolved. a source entry
expands $pkgver to the bare upstream version, a dependency to version-rrelease. */
static const char *identity_value(const char *word, const char *name, const char *version,
const char *release, int apk_version, char *out, size_t size)
{
if (!strcmp(word, "pkgname")) snprintf(out, size, "%s", name);
else if (!strcmp(word, "pkgver"))
snprintf(out, size, apk_version ? "%s-r%s" : "%s", version, release);
else if (!strcmp(word, "pkgrel")) snprintf(out, size, "%s", release);
else return NULL;
return out;
}
static int append_text(char **out, size_t *used, size_t *capacity, const char *text, size_t length)
{
while (*used + length + 1 > *capacity) {
char *grown = realloc(*out, *capacity * 2);
if (!grown) return 0;
*capacity *= 2;
*out = grown;
}
memcpy(*out + *used, text, length);
*used += length;
(*out)[*used] = 0;
return 1;
}
static int is_word_char(char c)
{
return isalnum((unsigned char)c) || c == '_';
}
/* expands pkgname, pkgver and pkgrel in one entry and flags every other reference */
static char *expand_names(const char *value, const char *name, const char *version,
const char *release, int apk_version, int *unknown)
{
size_t used = 0, capacity = strlen(value) + 256, i = 0;
char *out = malloc(capacity);
if (!out) return NULL;
out[0] = 0;
while (value[i]) {
char word[128], replacement[1024];
size_t length = 0, start;
const char *resolved;
if (value[i] != '$' || i + 1 >= strlen(value)) {
if (!append_text(&out, &used, &capacity, value + i, 1)) goto failed;
++i;
continue;
}
if (value[i + 1] == '{') {
start = i + 2;
while (value[start] && value[start] != '}' && length + 1 < sizeof word)
word[length++] = value[start++];
if (value[start] != '}') { if (unknown) *unknown = 1; goto kept; }
word[length] = 0;
resolved = identity_value(word, name, version, release, apk_version, replacement,
sizeof replacement);
if (!resolved) { if (unknown) *unknown = 1; goto kept; }
if (!append_text(&out, &used, &capacity, resolved, strlen(resolved))) goto failed;
i = start + 1;
continue;
}
if (!is_word_char(value[i + 1])) {
if (!append_text(&out, &used, &capacity, value + i, 1)) goto failed;
++i;
continue;
}
start = i + 1;
while (value[start] && is_word_char(value[start]) && length + 1 < sizeof word)
word[length++] = value[start++];
word[length] = 0;
resolved = identity_value(word, name, version, release, apk_version, replacement,
sizeof replacement);
if (!resolved) { if (unknown) *unknown = 1; goto kept; }
if (!append_text(&out, &used, &capacity, resolved, strlen(resolved))) goto failed;
i = start;
continue;
kept:
/* a reference this converter does not know keeps its text and needs review */
if (!append_text(&out, &used, &capacity, value + i, strlen(value + i))) goto failed;
return out;
}
return out;
failed:
free(out);
return NULL;
}
static int is_digest(const char *value)
{
size_t index;
for (index = 0; index < 64; ++index)
if (!isxdigit((unsigned char)value[index])) return 0;
return !value[64];
}
/* the declared sha256 for one source name, or NULL when the file has none */
static char *declared_digest(const struct shell_script *pkg, const char *name)
{
char *raw = holy_shell_all(pkg, "sha256sums");
size_t count = 0, index;
char **items = raw ? holy_shell_words(raw, &count) : NULL;
char *found = NULL;
for (index = 0; items && index + 1 < count; index += 2)
if (!strcmp(items[index + 1], name) && is_digest(items[index])) {
found = strdup(items[index]);
break;
}
holy_shell_words_free(items);
free(raw);
return found;
}
/* one source record: a local file travels with the recipe, a remote one is reported */
static int emit_source(FILE *out, const struct shell_script *pkg, const struct apk_source *source,
const char *directory, const char *output, struct recipe_note *note,
int *review)
{
char original[4096], copied[4096], local_hash[65];
struct stat st;
char *declared = declared_digest(pkg, source->name);
int local = !strstr(source->url, "://");
if (local) {
snprintf(original, sizeof original, "%s/%s", directory, source->url);
snprintf(copied, sizeof copied, "%s/%s", output, source->name);
if (stat(original, &st) || !S_ISREG(st.st_mode)) {
*review = 1;
holy_note_add(note, "unknown", "source %s is not next to the APKBUILD APKBUILD:%zu",
source->name, source->line);
free(declared);
return 1;
}
if (!holy_copy_and_hash(original, copied, local_hash)) { free(declared); return 0; }
if (declared && strcmp(declared, local_hash)) {
*review = 1;
holy_note_add(note, "unknown", "source %s does not match its declared sha256",
source->name);
free(declared);
return 0;
}
fputs("source ", out);
holy_token(out, source->name);
fputc(' ', out);
holy_token(out, source->name);
fputc('\n', out);
fputs("source-sha256 ", out);
holy_token(out, source->name);
fputc(' ', out);
holy_token(out, local_hash);
fputc('\n', out);
holy_note_add(note, "carried", "source %s APKBUILD:%zu", source->name, source->line);
holy_note_add(note, "semantic-change",
"local source %s copied next to the recipe and hashed as sha256",
source->name);
free(declared);
return 1;
}
*review = 1;
holy_note_add(note, "unknown", "source %s APKBUILD:%zu", source->name, source->line);
if (!declared) {
holy_note_add(note, "unknown", "source %s has no sha256 digest", source->name);
return 1;
}
fputs("source ", out);
holy_token(out, source->name);
fputc(' ', out);
holy_token(out, source->url);
fputc('\n', out);
fputs("source-sha256 ", out);
holy_token(out, source->name);
fputc(' ', out);
holy_token(out, declared);
fputc('\n', out);
holy_note_add(note, "carried", "source %s APKBUILD:%zu", source->name, source->line);
free(declared);
return 1;
}
/* one source entry: an optional filename:: prefix, then the URL */
static int read_source(const char *entry, const char *name, const char *version,
const char *release, struct apk_source *source, int *unknown)
{
const char *url = entry;
const char *split = strstr(entry, "::");
const char *base;
size_t size;
memset(source, 0, sizeof *source);
if (split) {
url = split + 2;
if ((size_t)(split - entry) >= sizeof source->name) return 0;
memcpy(source->name, entry, (size_t)(split - entry));
source->name[split - entry] = 0;
if (strlen(url) >= sizeof source->url) return 0;
snprintf(source->url, sizeof source->url, "%s", url);
return 1;
}
{
char *expanded = expand_names(url, name, version, release, 0, unknown);
if (!expanded) return 0;
if (strlen(expanded) >= sizeof source->url) { free(expanded); return 0; }
snprintf(source->url, sizeof source->url, "%s", expanded);
free(expanded);
}
base = strrchr(source->url, '/');
base = base ? base + 1 : source->url;
size = strlen(base);
if (!size || size >= sizeof source->name || !strcmp(base, ".") || !strcmp(base, ".."))
return 0;
memcpy(source->name, base, size + 1);
return 1;
}
/* the arch field names a machine; anything else needs review */
static void map_arch(const char *value, char *arch, size_t size, struct recipe_note *note,
int *review)
{
size_t count = 0, index;
char **items = holy_shell_words(value, &count);
int x86_64 = 0, i686 = 0, noarch = 0, other = 0, negated = 0;
snprintf(arch, size, "noarch");
for (index = 0; items && index < count; ++index) {
const char *item = items[index];
if (item[0] == '!') { negated = 1; continue; }
if (!strcmp(item, "all") || !strcmp(item, "noarch")) noarch = 1;
else if (!strcmp(item, "x86_64")) x86_64 = 1;
else if (!strcmp(item, "x86")) i686 = 1;
else other = 1;
}
holy_shell_words_free(items);
if (negated) {
*review = 1;
holy_note_add(note, "preserved", "arch %s", value);
}
if (other) {
*review = 1;
holy_note_add(note, "unknown", "arch %s names a machine Holy does not carry", value);
}
if (x86_64) snprintf(arch, size, "x86_64");
else if (i686 && !noarch && !other) snprintf(arch, size, "i686");
else if (noarch) snprintf(arch, size, "noarch");
}
/* writes one depend record; a conflict or a provided capability is not a requirement */
static int emit_one_dependency(FILE *out, const char *raw, const char *kind,
struct recipe_note *note, int *review, const char *where)
{
if (raw[0] == '!') {
fputs("x-conflicts ", out);
holy_token(out, raw + 1);
fputc('\n', out);
*review = 1;
holy_note_add(note, "preserved", "conflict %s %s", raw + 1, where);
return 1;
}
if (!strncmp(raw, "so:", 3) || !strncmp(raw, "cmd:", 4)) {
*review = 1;
holy_note_add(note, "unknown", "dependency %s uses a resolver-specific prefix", raw);
return 0;
}
holy_emit_dependency(out, raw, kind);
return 1;
}
struct apk_split {
char name[512];
char function[256];
struct shell_function body;
int has_body;
};
/* the split body is copied so the script keeps ownership of its own text */
static int copy_split_body(const struct shell_function *source, struct shell_function *target)
{
target->name = strdup(source->name);
target->body = holy_shell_copy(source->body, source->length);
target->length = source->length;
target->first = source->first;
target->last = source->last;
target->conditional = source->conditional;
if (!target->name || !target->body) {
holy_shell_function_free(target);
return 0;
}
return 1;
}
static void split_free(struct apk_split *split)
{
if (split->has_body) holy_shell_function_free(&split->body);
split->has_body = 0;
}
static int write_body(FILE *out, const char *body)
{
size_t length = strlen(body);
if (!length) return 1;
if (fwrite(body, 1, length, out) != length) return 0;
if (body[length - 1] != '\n' && fputc('\n', out) == EOF) return 0;
return 1;
}
static int emit_phase_step(FILE *out, const char *phase, const char *prefix, const char *body)
{
if (fprintf(out, "step %s /bin/sh <<STEP\n", phase) < 0) return 0;
if (fputs(prefix, out) < 0) return 0;
/* abuild runs a phase in builddir, so the step has to move there first */
if (fputs("cd \"$builddir\"\n", out) < 0) return 0;
if (!write_body(out, body)) return 0;
return fprintf(out, "STEP\n") >= 0;
}
static int emit_split_step(FILE *out, const struct apk_split *split, const char *prefix)
{
if (fprintf(out, "split-step %s split /bin/sh <<SPLIT\n", split->name) < 0) return 0;
if (fputs(prefix, out) < 0) return 0;
if (fputs("cd \"$builddir\"\n", out) < 0) return 0;
if (split->has_body && !write_body(out, split->body.body)) return 0;
return fprintf(out, "SPLIT\n") >= 0;
}
int holy_convert_aports(const char *input, const char *source, const char *output)
{
struct shell_script pkg;
struct recipe_note note;
struct apk_source *sources = NULL;
struct apk_split splits[24];
char name[512] = {0}, version[512] = {0}, release[64] = {0}, arch[64] = "noarch";
char *summary = NULL, *homepage = NULL, *license = NULL, *maintainer = NULL;
char *builddir = NULL;
char recipe_path[4096], report_path[4096], target[4096];
char hash[65] = {0};
FILE *out = NULL;
size_t source_count = 0, split_count = 0, index, k;
int result = 1, review = 0, i, wrote = 1;
memset(&pkg, 0, sizeof pkg);
memset(¬e, 0, sizeof note);
memset(splits, 0, sizeof splits);
if (!input || !output || !*output) {
fputs("usage: holypkg convert APKBUILD --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;
}
result = holy_shell_read(input, &pkg);
if (result) return result;
{
char *declared = holy_shell_join(&pkg, "pkgname");
char *given_version = holy_shell_join(&pkg, "pkgver");
char *given_release = holy_shell_join(&pkg, "pkgrel");
if (!declared || !given_version || !given_release) {
fputs("holypkg: APKBUILD: pkgname, pkgver and pkgrel are required\n", stderr);
result = 2;
goto done;
}
if (strpbrk(declared, "$`") || strpbrk(given_version, "$`") ||
strpbrk(given_release, "$`") || strlen(declared) > 480 ||
strlen(given_version) > 480 || strlen(given_release) > 60) {
fputs("holypkg: APKBUILD: identity must be literal and short\n", stderr);
result = 2;
goto done;
}
snprintf(name, sizeof name, "%s", declared);
snprintf(version, sizeof version, "%s", given_version);
snprintf(release, sizeof release, "%s", given_release);
free(declared);
free(given_version);
free(given_release);
}
summary = holy_shell_join(&pkg, "pkgdesc");
homepage = holy_shell_join(&pkg, "url");
license = holy_shell_all(&pkg, "license");
maintainer = holy_shell_all(&pkg, "maintainer");
{
char *arches = holy_shell_join(&pkg, "arch");
if (arches) map_arch(arches, arch, sizeof arch, ¬e, &review);
else {
review = 1;
holy_note_add(¬e, "unknown", "arch is unset, the payload decides");
}
free(arches);
}
if (pkg.condition_count) {
for (k = 0; k < pkg.condition_count; ++k) {
review = 1;
if (pkg.conditions[k].unreadable)
holy_note_add(¬e, "unknown", "unreadable statement APKBUILD:%zu %s",
pkg.conditions[k].line, pkg.conditions[k].text);
else
holy_note_add(¬e, "unknown", "conditional block APKBUILD:%zu %s",
pkg.conditions[k].line, pkg.conditions[k].text);
}
}
for (k = 0; k < pkg.value_count; ++k) {
if (!pkg.values[k].conditional) continue;
review = 1;
holy_note_add(¬e, "unknown", "conditional assignment %s APKBUILD:%zu",
pkg.values[k].name, pkg.values[k].line);
}
holy_note_add(¬e, "carried", "name APKBUILD:%zu", holy_shell_line(&pkg, "pkgname"));
holy_note_add(¬e, "carried", "version APKBUILD:%zu", holy_shell_line(&pkg, "pkgver"));
holy_note_add(¬e, "carried", "release APKBUILD:%zu", holy_shell_line(&pkg, "pkgrel"));
if (summary && *summary)
holy_note_add(¬e, "carried", "pkgdesc APKBUILD:%zu", holy_shell_line(&pkg, "pkgdesc"));
if (homepage && *homepage)
holy_note_add(¬e, "carried", "url APKBUILD:%zu", holy_shell_line(&pkg, "url"));
if (license && *license)
holy_note_add(¬e, "carried", "license APKBUILD:%zu", holy_shell_line(&pkg, "license"));
if (maintainer && *maintainer)
holy_note_add(¬e, "carried", "maintainer APKBUILD:%zu",
holy_shell_line(&pkg, "maintainer"));
/* the source list carries patches, install scripts and conf files as well */
{
char *entries = holy_shell_all(&pkg, "source");
size_t count = 0, item;
char **items = entries ? holy_shell_words(entries, &count) : NULL;
size_t entry_count = 0;
for (item = 0; items && item < count; ++item) ++entry_count;
if (entry_count) {
sources = calloc(entry_count, sizeof *sources);
if (!sources) { wrote = 0; item = count; }
for (item = 0; sources && item < count; ++item) {
int unknown = 0;
if (!read_source(items[item], name, version, release, &sources[source_count],
&unknown)) {
review = 1;
holy_note_add(¬e, "unknown", "source %s", items[item]);
continue;
}
sources[source_count].line = holy_shell_line(&pkg, "source");
if (unknown) {
review = 1;
holy_note_add(¬e, "unknown", "source %s uses an expansion this converter "
"does not know", items[item]);
continue;
}
++source_count;
}
}
holy_shell_words_free(items);
free(entries);
}
if (mkdir(output, 0700) && errno != EEXIST) {
fprintf(stderr, "holypkg: conversion directory unavailable: %s\n", output);
result = 1;
goto done;
}
snprintf(target, sizeof target, "%s/APKBUILD", output);
if (!holy_copy_and_hash(input, target, hash)) {
fputs("holypkg: APKBUILD copy failed\n", stderr);
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); holy_token(out, name);
fputs("\nversion ", out); holy_token(out, version);
fputs("\nrelease ", out); holy_token(out, release);
fputs("\narch ", out); holy_token(out, arch);
fputs("\nlibc any\n", out);
if (summary && *summary) { fputs("summary ", out); holy_token(out, summary); fputc('\n', out); }
if (homepage && *homepage) { fputs("homepage ", out); holy_token(out, homepage); fputc('\n', out); }
if (license && *license) { fputs("license ", out); holy_token(out, license); fputc('\n', out); }
fputs("x-source-family apk\nx-converter aports-1\n", out);
if (maintainer && *maintainer) {
fputs("x-maintainer ", out); holy_token(out, maintainer); fputc('\n', out);
}
{
static const char *const kept[] = {
"options", "provider_priority", "replaces_priority", "triggers", "install_if",
"pkggroups", "pkgusers", "giturl", "pcprefix", "sonameprefix",
"langdir", "soname", "provides", "replaces", NULL
};
for (index = 0; kept[index]; ++index) {
char *value = holy_shell_all(&pkg, kept[index]);
if (!value || !*value) { free(value); continue; }
fputs("x-", out);
fputs(kept[index], out);
fputc(' ', out);
holy_token(out, value);
fputc('\n', out);
review = 1;
holy_note_add(¬e, "preserved", "%s APKBUILD:%zu", kept[index],
holy_shell_line(&pkg, kept[index]));
free(value);
}
}
/* the local source entries travel with the recipe so the build needs no network */
for (k = 0; k < source_count; ++k) {
if (!emit_source(out, &pkg, &sources[k], pkg.directory, output, ¬e, &review)) {
wrote = 0;
break;
}
}
if (!wrote) { result = 1; goto done; }
{
char *sha512 = holy_shell_all(&pkg, "sha512sums");
if (sha512 && *sha512) {
review = 1;
holy_note_add(¬e, "unknown", "sha512sums pins a digest a Holy source cannot use, "
"the sha256 of each file is taken from a local copy instead");
}
free(sha512);
}
/* dependency lists; the per subpackage ones belong to an output Holy cannot split */
{
static const char *const build_lists[] = {
"makedepends_build", "makedepends_host", "makedepends", "checkdepends", NULL
};
static const char *const split_depends[] = {
"depends_dev", "depends_doc", "depends_openrc", "depends_libs", "depends_static",
"depends_systemd", "depends_udev", NULL
};
char *raw = holy_shell_all(&pkg, "depends");
size_t count = 0, item;
char **items = raw ? holy_shell_words(raw, &count) : NULL;
size_t list_index;
for (item = 0; items && item < count; ++item) {
char *fixed = expand_names(items[item], name, version, release, 1, NULL);
if (fixed && *fixed && emit_one_dependency(out, fixed, "depend", ¬e, &review,
"depends"))
holy_note_add(¬e, "carried", "depends %s APKBUILD:%zu", fixed,
holy_shell_line(&pkg, "depends"));
free(fixed);
}
holy_shell_words_free(items);
free(raw);
for (list_index = 0; build_lists[list_index]; ++list_index) {
char *list = holy_shell_all(&pkg, build_lists[list_index]);
size_t list_size = 0;
char **entries = list ? holy_shell_words(list, &list_size) : NULL;
for (item = 0; entries && item < list_size; ++item) {
char *fixed = expand_names(entries[item], name, version, release, 1, NULL);
if (fixed && *fixed)
emit_one_dependency(out, fixed, "build-depend", ¬e, &review,
build_lists[list_index]);
free(fixed);
}
holy_shell_words_free(entries);
free(list);
}
for (list_index = 0; split_depends[list_index]; ++list_index) {
if (!holy_shell_present(&pkg, split_depends[list_index])) continue;
review = 1;
holy_note_add(¬e, "preserved", "%s APKBUILD:%zu", split_depends[list_index],
holy_shell_line(&pkg, split_depends[list_index]));
}
}
/* an install action script beside the APKBUILD becomes one hook */
{
static const struct { const char *action; const char *key; const char *text; } actions[] = {
{ "post-install", "hook-install", "postinstall" },
{ "pre-install", "hook-install", "preinstall" },
{ "pre-upgrade", NULL, NULL },
{ "post-upgrade", NULL, NULL },
{ "pre-deinstall", "hook-remove", "preremove" },
{ "post-deinstall", NULL, NULL }
};
char *entries = holy_shell_all(&pkg, "install");
size_t count = 0, item;
char **items = entries ? holy_shell_words(entries, &count) : NULL;
char *hooks[8];
size_t hook_count = 0;
for (item = 0; items && item < count; ++item) {
char *expanded = expand_names(items[item], name, version, release, 0, NULL);
const char *entry = expanded ? expanded : items[item];
const char *action = strchr(entry, '.') ? strchr(entry, '.') + 1 : NULL;
char original[4096], copied[4096], installed[600];
struct stat st;
char local_hash[65];
const char *base;
int known = 0;
if (!action) { free(expanded); continue; }
for (index = 0; index < sizeof actions / sizeof *actions; ++index)
if (!strcmp(action, actions[index].action)) known = 1;
if (!known) {
review = 1;
holy_note_add(¬e, "unknown", "install action %s", entry);
free(expanded);
continue;
}
base = strrchr(entry, '/');
base = base ? base + 1 : entry;
snprintf(original, sizeof original, "%s/%s", pkg.directory, base);
if (stat(original, &st) || !S_ISREG(st.st_mode)) {
review = 1;
holy_note_add(¬e, "unknown", "install script %s is not next to the APKBUILD "
"APKBUILD:%zu", entry, holy_shell_line(&pkg, "install"));
free(expanded);
continue;
}
for (index = 0; index < sizeof actions / sizeof *actions; ++index) {
if (strcmp(action, actions[index].action)) continue;
if (actions[index].key) {
snprintf(installed, sizeof installed, "usr/share/holy/%s/%s", name, base);
snprintf(copied, sizeof copied, "%s/%s", output, base);
if (!holy_copy_and_hash(original, copied, local_hash)) { wrote = 0; break; }
fputs("source ", out);
holy_token(out, base);
fputc(' ', out);
holy_token(out, base);
fputc('\n', out);
fputs(actions[index].key, out);
fputc(' ', out);
fputs("/bin/sh ", out);
holy_token(out, installed);
fputc('\n', out);
if (hook_count < sizeof hooks / sizeof *hooks)
hooks[hook_count++] = strdup(base);
holy_note_add(¬e, "preserved", "hook %s (%s)", base, actions[index].text);
holy_note_add(¬e, "semantic-change", "%s runs with ACTION unset, its pre "
"and post branches are not reproduced", base);
} else {
review = 1;
holy_note_add(¬e, "unknown", "install action %s has no Holy hook stage",
entry);
}
}
free(expanded);
}
holy_shell_words_free(items);
free(entries);
if (!wrote) { result = 1; goto done; }
/* the hook scripts live in the payload so the installer can read them */
if (hook_count) {
fprintf(out, "step package /bin/sh <<HOOKS\n"
"mkdir -p \"$HOLY_DEST/usr/share/holy/%s\"\n", name);
for (item = 0; item < hook_count; ++item)
fprintf(out, "cp \"$HOLY_SRC/%s\" \"$HOLY_DEST/usr/share/holy/%s/%s\"\n",
hooks[item], name, hooks[item]);
fputs("HOOKS\n", out);
}
for (item = 0; item < hook_count; ++item) free(hooks[item]);
}
if (!wrote) { result = 1; goto done; }
/* every declared subpackage becomes one output */
{
char *entries = holy_shell_all(&pkg, "subpackages");
size_t count = 0, item;
char **items = entries ? holy_shell_words(entries, &count) : NULL;
for (item = 0; items && item < count; ++item) {
const char *entry = items[item];
const char *first = strchr(entry, ':');
size_t entry_size = first ? (size_t)(first - entry) : strlen(entry);
char *entry_text = holy_shell_copy(entry, entry_size);
char *expanded = entry_text ?
expand_names(entry_text, name, version, release, 1, NULL) : NULL;
const char *split_name = first ? first + 1 : NULL;
struct apk_split *split;
free(entry_text);
if (!expanded) { wrote = 0; break; }
if (split_name) {
const char *second = strchr(split_name, ':');
if (second) {
review = 1;
holy_note_add(¬e, "preserved", "subpackage %s declares its own arch %s",
expanded, second + 1);
}
}
if (split_count >= sizeof splits / sizeof *splits) {
review = 1;
holy_note_add(¬e, "unknown", "more subpackages than outputs are carried %s",
expanded);
free(expanded);
continue;
}
split = &splits[split_count];
if (!*expanded || strlen(expanded) >= sizeof split->name) {
review = 1;
holy_note_add(¬e, "unknown", "subpackage %s has no usable name", entry);
free(expanded);
continue;
}
snprintf(split->name, sizeof split->name, "%s", expanded);
/* abuild takes the split function from the entry, else from the last suffix */
if (split_name) {
const char *second = strchr(split_name, ':');
size_t size = second ? (size_t)(second - split_name) : strlen(split_name);
if (!size) {
/* an empty function means the name suffix picks one */
const char *dash = strrchr(expanded, '-');
split_name = dash ? dash + 1 : expanded;
} else {
snprintf(split->function, sizeof split->function, "%.*s",
(int)(size < sizeof split->function - 1 ? size
: sizeof split->function - 1),
split_name);
}
}
if (!*split->function) {
static const struct { const char *suffix; const char *function; } completions[] = {
{ "-bash-completion", "bashcomp" },
{ "-zsh-completion", "zshcomp" },
{ "-fish-completion", "fishcomp" }
};
const char *function = NULL;
size_t used;
for (used = 0; used < sizeof completions / sizeof *completions; ++used) {
size_t at = strlen(completions[used].suffix);
if (strlen(expanded) > at &&
!strcmp(expanded + strlen(expanded) - at, completions[used].suffix)) {
function = completions[used].function;
break;
}
}
if (!function) {
const char *dash = strrchr(expanded, '-');
function = dash ? dash + 1 : expanded;
}
snprintf(split->function, sizeof split->function, "%s", function);
}
{
const struct shell_function *body =
*split->function ? holy_shell_function(&pkg, split->function) : NULL;
if (body) {
split->has_body = copy_split_body(body, &split->body);
if (!split->has_body) { wrote = 0; break; }
holy_note_add(¬e, "preserved", "subpackage %s from %s APKBUILD:%zu-%zu",
split->name, split->function, body->first, body->last);
} else {
const char *help = default_split_help(split->function);
review = 1;
if (help)
holy_note_add(¬e, "unknown", "subpackage %s needs the abuild default_%s "
"helper, which moves %s", split->name, split->function,
help);
else
holy_note_add(¬e, "unknown", "subpackage %s has no split function %s "
"APKBUILD:%zu", split->name, split->function,
holy_shell_line(&pkg, "subpackages"));
}
}
++split_count;
free(expanded);
}
holy_shell_words_free(items);
free(entries);
if (!wrote) { result = 1; goto done; }
}
fputs("output ", out); holy_token(out, name);
fputs(" runtime\n", out);
for (k = 0; k < split_count; ++k) {
if (!splits[k].has_body) continue;
fputs("output ", out); holy_token(out, splits[k].name);
fputs(" runtime\n", out);
}
{
/* the engine extracts each source under its own name, so one lift turns that
into the single $srcdir tree an abuild phase expects */
fputs("step unpack /bin/sh <<UNPACK\n"
"for entry in \"$HOLY_SRC\"/*; do\n"
" [ -d \"$entry\" ] || continue\n"
" for child in \"$entry\"/* \"$entry\"/.[!.]* \"$entry\"/..?*; do\n"
" [ -e \"$child\" ] || continue\n"
" mv \"$child\" \"$HOLY_SRC/\"\n"
" done\n"
" rmdir \"$entry\" 2>/dev/null || true\n"
"done\n"
"UNPACK\n", out);
holy_note_add(¬e, "semantic-change", "the extracted tree takes the place of the "
"abuild $srcdir directory, and the source lift replaces default_unpack");
}
/* abuild runs a phase in $builddir, which defaults to $srcdir/$pkgname-$pkgver */
{
char *declared = holy_shell_join(&pkg, "builddir");
char *text = NULL;
size_t length;
int unknown = 0;
if (declared && *declared)
text = expand_names(declared, name, version, release, 1, &unknown);
if (!text || unknown || strncmp(text, "$srcdir", 7)) {
if (declared && *declared) {
review = 1;
holy_note_add(¬e, "unknown", "builddir %s names a path this converter cannot "
"rebase on the source tree", declared);
}
length = strlen(name) + strlen(version) + 32;
builddir = malloc(length);
if (builddir) snprintf(builddir, length, "\"$HOLY_SRC/%s-%s\"", name, version);
} else {
length = strlen(text) + 32;
builddir = malloc(length);
if (builddir) snprintf(builddir, length, "\"$HOLY_SRC%s\"", text + 7);
}
free(text);
if (declared && *declared) {
review = 1;
holy_note_add(¬e, "semantic-change", "builddir %s becomes %s", declared,
builddir ? builddir : "\"$HOLY_SRC\"");
} else {
holy_note_add(¬e, "helper", "abuild supplies the default builddir %s-%s under "
"the source tree", name, version);
}
free(declared);
if (!builddir) { result = 1; goto done; }
}
{
static const struct { const char *phase; const char *function; } phases[] = {
{ "prepare", "prepare" }, { "build", "build" }, { "check", "check" },
{ "package", "package" }, { NULL, NULL }
};
for (index = 0; phases[index].phase; ++index) {
const struct shell_function *function =
holy_shell_function(&pkg, phases[index].function);
char *prefix;
if (!function) {
if (strcmp(phases[index].phase, "prepare")) {
holy_note_add(¬e, "helper", "abuild runs the built-in %s phase, which "
"this converter does not reproduce", phases[index].phase);
continue;
}
holy_note_add(¬e, "helper", "abuild runs default_prepare, which applies the "
"patches listed in source");
review = 1;
continue;
}
report_unknowns(function->body, function->length, ¬e, &review,
phases[index].function, 0);
holy_note_add(¬e, "preserved", "%s APKBUILD:%zu-%zu", phases[index].function,
function->first, function->last);
prefix = prologue(&pkg, name, version, release, builddir, "\"$HOLY_DEST\"");
if (!prefix || !emit_phase_step(out, phases[index].phase, prefix, function->body)) {
free(prefix);
wrote = 0;
break;
}
free(prefix);
}
if (!wrote) { result = 1; goto done; }
}
for (k = 0; k < split_count; ++k) {
char *prefix;
if (!splits[k].has_body) continue;
report_unknowns(splits[k].body.body, splits[k].body.length, ¬e, &review,
splits[k].name, 1);
prefix = prologue(&pkg, name, version, release, builddir, "\"$HOLY_SPLIT_DEST\"");
if (!prefix || !emit_split_step(out, &splits[k], prefix)) {
free(prefix);
wrote = 0;
break;
}
free(prefix);
}
if (!wrote) { result = 1; goto done; }
if (fflush(out) || fclose(out)) { out = NULL; result = 1; goto done; }
out = NULL;
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 aports-1\n", out);
fputs("source-name ", out); holy_token(out, source); fputc('\n', out);
fputs("source-file APKBUILD\n", out);
fprintf(out, "source-sha256 %s\n", hash);
fputs("pkgbase ", out); holy_token(out, name); fputc('\n', out);
fputs("version ", out); holy_token(out, version); fputc('\n', out);
fputs("release ", out); holy_token(out, release); fputc('\n', out);
fputs("arch ", out); holy_token(out, arch); fputc('\n', out);
fputs("recipe ", out); holy_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: APKBUILD conversion failed (status %d)\n", result);
for (k = 0; k < split_count; ++k) split_free(&splits[k]);
free(sources);
free(builddir);
holy_note_free(¬e);
free(summary); free(homepage); free(license); free(maintainer);
holy_shell_free(&pkg);
return result;
}