_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file backends/pacstall.c
/* Pacstall pacscript to holy-recipe(5) conversion; see man/holy-recipe.5 and
man/holypkg.8. the pacscript is read as text and never run. a pacscript is
bash with a metadata header written as assignments, a small set of phase
functions and a package manager environment that exports DEB variables, so
each phase body keeps its own shell behind a prologue that rebuilds the
variables Pacstall exports onto the Holy paths, and every helper that
environment supplies is reported rather than invented. */
#define _XOPEN_SOURCE 700
#define _DEFAULT_SOURCE 1
#include "pacstall.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>
/* the phase functions, in the order Pacstall runs them */
static const struct {
const char *pacscript;
const char *phase;
} phases[] = {
{ "prepare", "prepare" },
{ "build", "build" },
{ "check", "check" },
{ "package", "package" }
};
/* the install time functions, which run beside the phases. Pacstall picks
pre_install or pre_upgrade and post_install or post_upgrade for itself, and a
Holy hook runs with ACTION unset, so one group becomes one hook. */
static const struct {
const char *pacscript;
const char *group;
} hooks[] = {
{ "pre_install", "install" },
{ "pre_upgrade", "install" },
{ "post_install", "install" },
{ "post_upgrade", "install" },
{ "pre_remove", "remove" },
{ "post_remove", "remove" }
};
/* the helper functions Pacstall exports. a body that calls one cannot run without
the Pacstall environment, so each call is reported by name. */
static const char *const helpers[] = {
"fancy_message", "ask", "select_options", "is_function", "makedeb", "makepkg",
"makepkg_env", "parse_options", "dependency_check", "check_deps", "log_error",
"error_log", "checks", "pre_checks", "gettext", "hashcheck_down",
"genextr_down", "net_down", "git_down", "add_pacscript", NULL
};
/* the variables a body may read that a recipe cannot rebuild: they name a running
installation, a host kernel or a maintainer decision */
static const struct {
const char *name;
const char *why;
} environment[] = {
{ "KVER", "the running kernel a DKMS body builds for" },
{ "STAGEDIR", "the staging root Pacstall lays out itself" },
{ "homedir", "the home directory of the installing user" },
{ "DISTRO", "the distribution Pacstall was started for" },
{ "DIR", "the directory Pacstall cached the pacscript in" },
{ "full_version", "the string Pacstall builds a package file name from" },
{ "pacstall_root", "the Pacstall installation prefix" }
};
/* the dependency lists, in the order Pacstall evaluates them */
static const struct {
const char *key;
const char *kind;
} dependencies[] = {
{ "depends", "depend" },
{ "makedepends", "build-depend" },
{ "checkdepends", "build-depend" },
{ NULL, NULL }
};
/* pacdeps are packages of the same pacstall repository rather than of the
distribution, and a Holy resolver has to find them somewhere, so they become
requirements and the fact is reported */
#define PACDEPS "pacdeps"
/* the relations, none of which names a requirement, so they are preserved as x-
records */
static const struct {
const char *key;
const char *record;
} relations[] = {
{ "provides", "x-provides" },
{ "conflicts", "x-conflicts" },
{ "breaks", "x-conflicts" },
{ "makeconflicts", "x-makeconflicts" },
{ "checkconflicts", "x-checkconflicts" },
{ "replaces", "x-replaces" },
{ "enhances", "x-recommends" },
{ "recommends", "x-recommends" },
{ "suggests", "x-recommends" },
{ NULL, NULL }
};
/* the digest lists, none of which pins the SHA-256 a Holy source needs */
static const char *const digests[] = {
"md5sums", "sha1sums", "sha224sums", "sha384sums", "sha512sums", "b2sums",
"blake2sums", NULL
};
/* the settings that steer a Pacstall run and that a Holy recipe has no place for */
static const char *const settings[] = {
"priority", "external_connection", "incompatible", "compatible", "mask",
"noextract", "custom_fields", "ppa", NULL
};
/* the lists that exist per machine or per distribution under a suffixed name */
static const char *const lists[] = {
"source", "sha256sums", "depends", "makedepends", "checkdepends", PACDEPS,
"optdepends", "provides", "conflicts", "breaks", "replaces", "enhances",
"recommends", "suggests", NULL
};
/* the metadata a recipe records as an x- value, in the order a pacscript writes
it */
static const struct {
const char *key;
const char *record;
} carried[] = {
{ "license", "x-license" },
{ "maintainer", "x-maintainer" },
{ "repology", "x-repology" },
{ "bugs", "x-bugs" },
{ NULL, NULL }
};
/* the keys a pacscript may set that the converter reads itself */
static const char *const known[] = {
"pkgname", "pkgbase", "pkgver", "pkgrel", "epoch", "pkgdesc", "url", "arch",
"license", "maintainer", "repology", "bugs", "gives", "source", "sha256sums",
PACDEPS, "optdepends", "backup", "depends", "makedepends", "checkdepends",
"provides", "conflicts", "breaks", "replaces", "enhances", "recommends",
"suggests", "makeconflicts", "checkconflicts", "md5sums", "sha1sums",
"sha224sums", "sha384sums", "sha512sums", "b2sums", "blake2sums", "priority",
"external_connection", "incompatible", "compatible", "mask", "noextract",
"custom_fields", "ppa", NULL
};
struct pac_value {
char *text;
size_t line;
};
struct pac_list {
struct pac_value *items;
size_t count;
};
/* the identity a pacscript declares in its own assignments */
struct pacscript_name {
char name[256];
char base[256];
char version[256];
char release[64];
char epoch[16];
char gives[256];
char arch[64];
};
static int is_label(const char *value)
{
size_t at;
if (!value || !*value) return 0;
for (at = 0; value[at]; ++at)
if (!isalnum((unsigned char)value[at]) &&
!(value[at] == '.' || value[at] == '_' || value[at] == '+' || value[at] == '-'))
return 0;
return 1;
}
/* a computed value cannot be written into a manifest, so it is refused */
static int literal(const char *value, size_t size)
{
return value && *value && strlen(value) < size && !strpbrk(value, "$`\"'\\ \t");
}
/* copies one list out of the parsed script, because the parser hands out a
snapshot that its own next call replaces */
static int read_list(const struct shell_script *script, const char *key, struct pac_list *list)
{
const struct shell_value *entries;
size_t count = 0, index;
memset(list, 0, sizeof *list);
entries = holy_shell_entries(script, key, &count);
if (!count) return 1;
list->items = malloc(count * sizeof *list->items);
if (!list->items) return 0;
list->count = count;
for (index = 0; index < count; ++index) {
list->items[index].text = strdup(entries[index].text);
list->items[index].line = entries[index].line;
if (!list->items[index].text) {
for (size_t before = 0; before < index; ++before) free(list->items[before].text);
free(list->items);
memset(list, 0, sizeof *list);
return 0;
}
}
return 1;
}
static void free_list(struct pac_list *list)
{
size_t index;
for (index = 0; index < list->count; ++index) free(list->items[index].text);
free(list->items);
memset(list, 0, sizeof *list);
}
/* a list joined with single spaces, which is what one x- record holds */
static char *join_list(const struct pac_list *list)
{
char *joined;
size_t used = 0, index;
if (!list->count) return NULL;
for (index = 0; index < list->count; ++index) used += strlen(list->items[index].text) + 1;
joined = malloc(used + 1);
if (!joined) return NULL;
joined[0] = 0;
for (index = 0; index < list->count; ++index) {
if (index) strcat(joined, " ");
strcat(joined, list->items[index].text);
}
return joined;
}
/* the directory that holds the pacscript, which also holds the files it names */
static char *pacscript_directory(const char *input)
{
char *copy, *slash;
size_t used;
if (!strchr(input, '/')) return strdup(".");
copy = strdup(input);
if (!copy) return NULL;
used = strlen(copy);
while (used && copy[used - 1] == '/') copy[--used] = 0;
slash = strrchr(copy, '/');
if (slash) *slash = 0;
if (!*copy) {
free(copy);
return strdup("/");
}
return copy;
}
/* 1 when a body calls a helper, which is a name in a command position that is not
inside a comment and is not part of a longer name */
static int calls_helper(const char *body, size_t length, const char *name)
{
size_t used = strlen(name);
size_t index;
for (index = 0; index + used <= length; ++index) {
const char *at = body + index;
const char *before = index ? at - 1 : NULL;
const char *after = at + used;
size_t line;
if (memcmp(at, name, used)) continue;
if (before && (isalnum((unsigned char)*before) || *before == '_' || *before == '-' ||
*before == '.' || *before == '/'))
continue;
if (after < body + length && (isalnum((unsigned char)*after) || *after == '_' ||
*after == '-' || *after == '.'))
continue;
if (after < body + length && *after != ' ' && *after != '\t' && *after != '"' &&
*after != '\'' && *after != '\n' && *after != ')' && *after != ';' &&
*after != '|' && *after != '&')
continue;
for (line = index; line; --line)
if (at[-(ptrdiff_t)(index - line)] == '\n') break;
for (line = line ? line : 0; line < index; ++line)
if (at[-(ptrdiff_t)(index - line)] == '#') break;
if (line < index) continue;
return 1;
}
return 0;
}
/* 1 when a body reads a variable, which is a name a dollar sign introduces */
static int reads_variable(const char *body, size_t length, const char *name)
{
size_t used = strlen(name);
size_t index;
for (index = 0; index + 1 < length; ++index) {
size_t at;
if (body[index] != '$') continue;
if (body[index + 1] == '{') {
at = index + 2;
if (at + used >= length || memcmp(body + at, name, used)) continue;
} else {
at = index + 1;
if (at + used > length || memcmp(body + at, name, used)) continue;
}
if (at + used < length && (isalnum((unsigned char)body[at + used]) ||
body[at + used] == '_'))
continue;
return 1;
}
return 0;
}
/* 1 when the ${...} body names the variable and nothing else */
static int variable(const char *body, size_t used, const char *name)
{
return used == strlen(name) && !memcmp(body, name, used);
}
/* the identity a variable names, or NULL when it names something else */
static const char *identity_value(const struct pacscript_name *name, const char *variable_name,
size_t used)
{
if (variable(variable_name, used, "pkgname")) return name->name;
if (variable(variable_name, used, "pkgbase")) return name->base;
if (variable(variable_name, used, "pkgver")) return name->version;
if (variable(variable_name, used, "pkgrel")) return name->release;
if (variable(variable_name, used, "gives")) return name->gives;
if (variable(variable_name, used, "epoch")) return name->epoch;
return NULL;
}
/* a pacscript names its own identity in these variables, so a value written with
only them can be written out; a variable the file states in an assignment of its
own is read from there, and one that needs the Pacstall environment, or a
parameter expansion modifier, is refused rather than half expanded */
static char *expand(const char *value, const struct shell_script *script,
const struct pacscript_name *name)
{
const char *cursor = value;
char *out = NULL;
size_t used = 0;
for (;;) {
const char *open = strchr(cursor, '$');
const char *body;
size_t at, length, inner_length, braces;
char *stated = NULL;
const char *text = NULL;
if (!open) break;
if (open[1] != '{' && open[1] != '(' && !isalpha((unsigned char)open[1]) &&
open[1] != '_') {
cursor = open + 1;
continue;
}
braces = open[1] == '{' || open[1] == '(';
if (braces) {
char stop = open[1] == '{' ? '}' : ')';
size_t depth = 0, scan;
for (at = 2; open[at]; ++at) {
if (open[at] == open[1]) ++depth;
else if (open[at] == stop) {
if (!depth) break;
--depth;
}
}
if (!open[at]) {
free(out);
return NULL;
}
for (scan = 2; scan < at; ++scan)
if (open[scan] == '/' || open[scan] == ':' || open[scan] == '#' ||
open[scan] == '%' || open[scan] == '?' || open[scan] == '-' ||
open[scan] == '+') {
free(out);
return NULL;
}
body = open + 2;
inner_length = at - 2;
length = (size_t)(open - cursor);
at += 1;
} else {
/* the shell reads $name as well as ${name}, and a name runs to the
first character that is not part of it */
for (at = 1; open[at] && (isalnum((unsigned char)open[at]) || open[at] == '_');
++at)
;
body = open + 1;
inner_length = at - 1;
length = (size_t)(open - cursor);
}
text = identity_value(name, body, inner_length);
if (!text) {
/* a private assignment of the same file, which the shell would read
before this value, and only when it holds a literal of its own */
char key[256];
if (inner_length >= sizeof key) {
free(out);
return NULL;
}
memcpy(key, body, inner_length);
key[inner_length] = 0;
stated = holy_shell_join(script, key);
if (stated && (!*stated || strpbrk(stated, "$`"))) {
free(stated);
free(out);
return NULL;
}
if (stated) text = stated;
}
if (!text) {
/* an unknown variable in the unbraced form stays as it was written, and
a braced one is a value this converter cannot reach */
if (!braces) {
cursor = open + 1;
continue;
}
free(out);
return NULL;
}
{
size_t add = length + strlen(text) + at + 1;
char *grown = realloc(out, used + add + 1);
if (!grown) {
free(stated);
free(out);
return NULL;
}
out = grown;
memcpy(out + used, cursor, length);
used += length;
memcpy(out + used, text, strlen(text));
used += strlen(text);
out[used] = 0;
}
free(stated);
/* the braced form stops after its closing brace, the plain one after the
name, and at names the first character of either */
cursor = open + at;
}
{
size_t add = strlen(cursor);
char *grown = realloc(out, used + add + 1);
if (!grown) {
free(out);
return NULL;
}
out = grown;
memcpy(out + used, cursor, add + 1);
}
return out;
}
/* the same expansion over a whole list, where one element that cannot be written
out is a refusal rather than a silent drop */
static int expand_list(struct pac_list *list, const struct shell_script *script,
const struct pacscript_name *name)
{
size_t index;
for (index = 0; index < list->count; ++index) {
char *written = expand(list->items[index].text, script, name);
if (!written) return 0;
free(list->items[index].text);
list->items[index].text = written;
}
return 1;
}
/* the arch list mapped onto the machines Holy carries. a foreign machine is
reported and written as it stands, since a manifest records what the upstream
recipe said. */
static void map_arch(const struct pac_list *list, char *arch, size_t size,
struct recipe_note *note, int *review)
{
size_t index;
snprintf(arch, size, "any");
for (index = 0; index < list->count; ++index) {
const char *value = list->items[index].text;
if (!strcmp(value, "any") || !strcmp(value, "all")) {
snprintf(arch, size, "any");
} else if (!strcmp(value, "amd64") || !strcmp(value, "x86_64")) {
if (list->count > 1) {
*review = 1;
holy_note_add(note, "unknown", "arch %s is one machine of a list, so the recipe is "
"built for the machine the payload needs", value);
}
snprintf(arch, size, "x86_64");
} else if (!strcmp(value, "i386") || !strcmp(value, "i686") ||
!strcmp(value, "x86")) {
if (list->count > 1) {
*review = 1;
holy_note_add(note, "unknown", "arch %s is one machine of a list, so the recipe is "
"built for the machine the payload needs", value);
}
snprintf(arch, size, "i686");
} else {
*review = 1;
holy_note_add(note, "unknown", "arch %s names a machine Holy does not carry, so it is "
"written as it stands", value);
snprintf(arch, size, "%s", value);
}
}
}
/* a dependency entry is a name with an optional comparison, and a group offers
alternatives with a pipe. a Holy record holds one name and one comparison, so
the first alternative is carried and the rest are reported. */
static int emit_dependency(FILE *out, const char *raw, const char *kind, size_t line,
const char *input, struct recipe_note *note, int *review)
{
const char *bar = strchr(raw, '|');
if (bar) {
char *first = holy_shell_copy(raw, (size_t)(bar - raw));
int ok = 1;
*review = 1;
holy_note_add(note, "unknown", "the group %s at %s:%zu offers alternatives with a pipe, "
"and a Holy record cannot choose one, so the first is carried", raw, input,
line);
if (first) {
ok = emit_dependency(out, first, kind, line, input, note, review);
free(first);
}
return ok;
}
if (!*raw) {
*review = 1;
holy_note_add(note, "unknown", "an empty dependency at %s:%zu is dropped", input, line);
return 1;
}
if (strpbrk(raw, "$`")) {
*review = 1;
holy_note_add(note, "unknown", "the dependency %s at %s:%zu is written with a variable, "
"which this converter does not expand", raw, input, line);
return 1;
}
holy_emit_dependency(out, raw, kind);
return 1;
}
static int emit_dependencies(FILE *out, const struct pac_list *list, const char *kind,
const char *key, const char *input, struct recipe_note *note,
int *review)
{
size_t index;
for (index = 0; index < list->count; ++index)
if (!emit_dependency(out, list->items[index].text, kind, list->items[index].line, input,
note, review))
return 0;
if (list->count)
holy_note_add(note, "carried", "%s %s:%zu", key, input, list->items[0].line);
return 1;
}
/* the prologue every phase body runs behind, rebuilt from the exported paths. dest
is the tree the body installs into, which is the split tree in a split step. */
static int emit_prologue(FILE *out, const struct pacscript_name *name, const char *child,
const char *dest)
{
fputs("pkgdir=\"", out); fputs(dest, out); fputs("\"\n", out);
fputs("pacdir=\"$HOLY_WORK\"\nsrcdir=\"$HOLY_SRC\"\n", out);
fputs("startdir=\"$HOLY_BUILD\"\nbuilddir=\"$HOLY_SRC\"\nTARCH=\"$HOLY_ARCH\"\n", out);
fputs("NCPU=\"$HOLY_JOBS\"\npkgname=", out);
holy_token(out, child ? child : name->name);
fputs("\npkgbase=", out); holy_token(out, name->name);
fputs("\npkgver=", out); holy_token(out, name->version);
fputs("\npkgrel=", out); holy_token(out, name->release);
fputs("\npacname=", out); holy_token(out, child ? child : name->name);
fputs("\ngives=", out); holy_token(out, name->gives);
fputc('\n', out);
if (*name->epoch) {
fputs("epoch=", out);
holy_token(out, name->epoch);
fputc('\n', out);
}
return fputs("cd \"$HOLY_SRC\" || exit 1\n", out) >= 0;
}
/* one source record. an entry is NAME::URL, ?NAME::URL or a plain URL, and a
digest list runs parallel to the source list. a local file is copied next to the
recipe; a remote source with no sha256 is reported rather than fetched. */
static int emit_sources(FILE *out, const struct pac_list *sources, const struct pac_list *sums,
const char *directory, const char *output,
const struct shell_script *script, const struct pacscript_name *name,
const char *input, struct recipe_note *note, int *review)
{
size_t index;
for (index = 0; index < sources->count; ++index) {
const char *raw = sources->items[index].text;
size_t line = sources->items[index].line;
char *entry = expand(raw, script, name);
char *url = NULL, *written = NULL;
char *digest = NULL;
int ok = 1;
if (!entry) {
*review = 1;
holy_note_add(note, "unknown", "the source %s at %s:%zu is written with a variable "
"this converter does not expand, so it is reported", raw, input, line);
continue;
}
if (!strncmp(entry, "git+", 4)) {
*review = 1;
holy_note_add(note, "unknown", "the source %s at %s:%zu is a git address, and a Holy "
"recipe fetches archives, so it is reported", raw, input, line);
free(entry);
continue;
}
{
char *marker = strstr(entry, "::");
if (marker) {
*marker = 0;
url = strdup(marker + 2);
/* a leading ? says the recorded name is only a suggestion */
if (*entry == '?') ++entry;
} else {
url = strdup(entry);
}
}
free(entry);
if (!url) return 0;
if (!*url) {
*review = 1;
holy_note_add(note, "unknown", "an empty source at %s:%zu is dropped", input, line);
free(url);
continue;
}
if (strpbrk(url, "$`")) {
*review = 1;
holy_note_add(note, "unknown", "the source %s at %s:%zu still holds a variable after "
"expansion, so it is reported rather than fetched", url, input, line);
free(url);
continue;
}
if (index < sums->count) {
char *value = expand(sums->items[index].text, script, name);
if (value && *value && strcmp(value, "SKIP")) digest = value;
else free(value);
}
if (strstr(url, "://")) {
/* the fetched object is named after the file in the address, because the
engine extracts into a directory that carries that name */
const char *leaf = strrchr(url, '/');
leaf = leaf ? leaf + 1 : url;
if (strchr(leaf, '?')) {
const char *stop = strchr(leaf, '?');
written = holy_shell_copy(leaf, (size_t)(stop - leaf));
} else {
written = strdup(leaf);
}
if (!written) {
free(url);
return 0;
}
if (!*written) {
*review = 1;
holy_note_add(note, "unknown", "the source %s at %s:%zu names no file, so this "
"converter cannot give the fetched object a name", url, input, line);
free(written);
free(url);
continue;
}
if (!digest) {
*review = 1;
holy_note_add(note, "unknown", "the source %s at %s:%zu has no sha256sums entry, "
"and a Holy source needs one, so it is reported rather than "
"fetched", url, input, line);
} else if (!strncmp(url, "https://", 8)) {
fputs("source ", out);
holy_token(out, written);
fputc(' ', out);
holy_token(out, url);
fputc('\n', out);
fputs("source-sha256 ", out);
holy_token(out, written);
fputc(' ', out);
holy_token(out, digest);
fputc('\n', out);
holy_note_add(note, "carried", "the remote source %s", written);
} else {
/* a Holy source is fetched over https, so a plain http address is one
the recipe cannot carry */
*review = 1;
holy_note_add(note, "unknown", "the source %s at %s:%zu is a plain http address, "
"and a Holy source is fetched over https, so it is reported", url,
input, line);
}
} else {
char original[4096], copied[4096], hash[65];
const char *file = strrchr(url, '/');
struct stat st;
file = file ? file + 1 : url;
if (strpbrk(file, " \t") || !*file || !strcmp(file, ".") || !strcmp(file, "..")) {
*review = 1;
holy_note_add(note, "unknown", "the source %s at %s:%zu names a path this "
"converter cannot read", url, input, line);
free(url);
continue;
}
snprintf(original, sizeof original, "%s/%s", directory, file);
snprintf(copied, sizeof copied, "%s/%s", output, file);
if (stat(original, &st) || !S_ISREG(st.st_mode)) {
*review = 1;
holy_note_add(note, "unknown", "the source %s at %s:%zu is not beside the "
"pacscript, so this converter cannot hash it", file, input, line);
} else if (!holy_copy_and_hash(original, copied, hash)) {
ok = 0;
} else {
fputs("source ", out);
holy_token(out, file);
fputc(' ', out);
holy_token(out, file);
fputc('\n', out);
fputs("source-sha256 ", out);
holy_token(out, file);
fputc(' ', out);
holy_token(out, hash);
fputc('\n', out);
holy_note_add(note, "carried", "the local source %s", file);
holy_note_add(note, "semantic-change", "the local source %s is copied next to the "
"recipe and hashed as sha256", file);
}
}
free(url);
free(written);
free(digest);
if (!ok) return 0;
}
return 1;
}
/* writes one hook script beside the recipe from the functions of one group, and
returns the path it is installed at */
static char *emit_hook(FILE *out, const char *group, const struct pacscript_name *name,
const char *input, const char *output, const struct shell_script *script,
struct recipe_note *note, int *review)
{
char base[600], installed[900], copied[4096];
FILE *hook;
size_t index, i;
snprintf(base, sizeof base, "%s.%s", name->name, group);
snprintf(installed, sizeof installed, "usr/share/holy/%s/%s", name->name, base);
snprintf(copied, sizeof copied, "%s/%s", output, base);
hook = fopen(copied, "wb");
if (!hook) return NULL;
fputs("# a Holy hook runs with ACTION unset, so the pre and post bodies of the\n"
"# pacscript arrive in the order Pacstall calls them.\n", hook);
fprintf(hook, "pkgname=%s\n", name->name);
fprintf(hook, "pkgbase=%s\n", name->name);
fprintf(hook, "pkgver=%s\n", name->version);
fprintf(hook, "pkgrel=%s\n", name->release);
fprintf(hook, "gives=%s\n", name->gives);
for (index = 0; index < sizeof hooks / sizeof *hooks; ++index) {
const struct shell_function *function;
if (strcmp(hooks[index].group, group)) continue;
function = holy_shell_function(script, hooks[index].pacscript);
if (!function) continue;
fprintf(hook, "# %s\n", hooks[index].pacscript);
if (function->length &&
fwrite(function->body, 1, function->length, hook) != function->length) {
fclose(hook);
return NULL;
}
if (function->length && function->body[function->length - 1] != '\n') fputc('\n', hook);
holy_note_add(note, "preserved", "the %s body %s:%zu", hooks[index].pacscript, input,
function->first);
if (function->conditional) {
*review = 1;
holy_note_add(note, "unknown", "the %s function is written inside a conditional, which "
"this converter does not choose between", hooks[index].pacscript);
}
}
if (fclose(hook)) return NULL;
fputs("source ", out);
holy_token(out, base);
fputc(' ', out);
holy_token(out, base);
fputc('\n', out);
fputs(strcmp(group, "remove") ? "hook-install " : "hook-remove ", out);
fputs("/bin/bash ", out);
holy_token(out, installed);
fputc('\n', out);
holy_note_add(note, "semantic-change", "the %s functions become one %s hook at %s", group,
group, installed);
for (index = 0; index < script->function_count; ++index) {
const struct shell_function *function = &script->functions[index];
int same = 0;
for (i = 0; i < sizeof hooks / sizeof *hooks; ++i)
if (!strcmp(function->name, hooks[i].pacscript) &&
!strcmp(hooks[i].group, group))
same = 1;
if (!same) continue;
for (i = 0; helpers[i]; ++i)
if (calls_helper(function->body, function->length, helpers[i])) {
*review = 1;
holy_note_add(note, "unknown", "the %s body calls the %s helper, which the "
"Pacstall environment supplies and this converter does not",
function->name, helpers[i]);
}
for (i = 0; i < sizeof environment / sizeof *environment; ++i)
if (reads_variable(function->body, function->length, environment[i].name)) {
*review = 1;
holy_note_add(note, "helper", "the %s body reads %s, which is %s", function->name,
environment[i].name, environment[i].why);
}
}
return strdup(installed);
}
int holy_convert_pacstall(const char *input, const char *source, const char *output)
{
struct shell_script script = {0};
struct recipe_note note = {0};
struct pacscript_name name;
struct pac_list names = {0}, arches = {0}, sources = {0}, sums = {0};
char *directory = NULL, *summary = NULL, *homepage = NULL, *joined = NULL;
char recipe_path[4096], report_path[4096], digest[65];
char *hook_paths[2] = {NULL, NULL};
size_t index, i;
FILE *out = NULL;
int result = 1, review = 0, read_status, split = 0;
memset(&name, 0, sizeof name);
snprintf(name.release, sizeof name.release, "1");
snprintf(name.arch, sizeof name.arch, "any");
if (!input || !output || !*output) {
fputs("usage: holypkg convert NAME.pacscript --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;
}
directory = pacscript_directory(input);
if (!directory) return 1;
read_status = holy_shell_read(input, &script);
if (read_status == 6) { result = 6; goto done; }
if (read_status == 2) { result = 2; goto done; }
if (read_status) { result = 1; goto done; }
if (!read_list(&script, "pkgname", &names) ||
!read_list(&script, "arch", &arches) || !read_list(&script, "source", &sources) ||
!read_list(&script, "sha256sums", &sums)) {
result = 1;
goto done;
}
if (!names.count) {
fputs("holypkg: a pacscript needs a pkgname\n", stderr);
result = 2;
goto done;
}
/* gives is read first, because a pacscript may write its own name with it, and
a body installs under it */
joined = holy_shell_join(&script, "gives");
if (joined) {
char *given = expand(joined, &script, &name);
if (given && literal(given, sizeof name.gives) && is_label(given))
snprintf(name.gives, sizeof name.gives, "%s", given);
free(given);
free(joined);
joined = NULL;
}
/* a name written with the identity the file states for itself, which the shell
reads before the value, is written out; one that needs the Pacstall
environment is a computed identity and is refused */
if (!expand_list(&names, &script, &name)) {
fputs("holypkg: a pacscript needs a literal pkgname\n", stderr);
result = 2;
goto done;
}
/* a list of names is a split pkgbase, and the parent is named by pkgbase */
if (names.count > 1) {
char *base = holy_shell_join(&script, "pkgbase");
char *written = base ? expand(base, &script, &name) : NULL;
split = 1;
if (!written || !literal(written, sizeof name.base) || !is_label(written)) {
free(written);
free(base);
fputs("holypkg: a pacscript with a list of names needs a literal pkgbase\n", stderr);
result = 2;
goto done;
}
snprintf(name.name, sizeof name.name, "%s", written);
snprintf(name.base, sizeof name.base, "%s", written);
free(written);
free(base);
for (index = 0; index < names.count; ++index)
if (!literal(names.items[index].text, 256) ||
!is_label(names.items[index].text)) {
fputs("holypkg: a pacscript needs literal names\n", stderr);
result = 2;
goto done;
}
} else if (!literal(names.items[0].text, sizeof name.name) ||
!is_label(names.items[0].text)) {
fputs("holypkg: a pacscript needs a literal pkgname\n", stderr);
result = 2;
goto done;
} else {
snprintf(name.name, sizeof name.name, "%s", names.items[0].text);
snprintf(name.base, sizeof name.base, "%s", names.items[0].text);
}
{
char *version = holy_shell_join(&script, "pkgver");
char *packager = holy_shell_join(&script, "pkgrel");
char *epoch = holy_shell_join(&script, "epoch");
char *written = version ? expand(version, &script, &name) : NULL;
/* the engine records a version as a label, so a distribution version that
carries a tilde is not one a recipe can hold */
if (!written || !literal(written, sizeof name.version) || !is_label(written)) {
free(written);
free(version);
fputs("holypkg: a pacscript needs a literal pkgver\n", stderr);
result = 2;
goto done;
}
snprintf(name.version, sizeof name.version, "%s", written);
free(written);
free(version);
if (packager) {
written = expand(packager, &script, &name);
if (!literal(written, sizeof name.release) || !is_label(written)) {
free(written);
free(packager);
fputs("holypkg: a pacscript needs a literal pkgrel\n", stderr);
result = 2;
goto done;
}
snprintf(name.release, sizeof name.release, "%s", written);
free(written);
free(packager);
}
/* an epoch orders a packaging revision rather than naming a version, so
it is preserved and dropped */
if (epoch) {
snprintf(name.epoch, sizeof name.epoch, "%s", epoch);
free(epoch);
}
}
/* a gives that names the package now that the name is known */
if (!name.gives[0]) {
joined = holy_shell_join(&script, "gives");
if (joined) {
char *given = expand(joined, &script, &name);
if (given && literal(given, sizeof name.gives) && is_label(given))
snprintf(name.gives, sizeof name.gives, "%s", given);
free(given);
free(joined);
joined = NULL;
}
}
if (!name.gives[0]) snprintf(name.gives, sizeof name.gives, "%s", name.name);
if (mkdir(output, 0700) && errno != EEXIST) {
fprintf(stderr, "holypkg: conversion directory unavailable: %s\n", output);
result = 1;
goto done;
}
snprintf(recipe_path, sizeof recipe_path, "%s/%s.recipe", output, name.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;
}
summary = holy_shell_join(&script, "pkgdesc");
homepage = holy_shell_join(&script, "url");
fputs("format holy-recipe-1\nname ", out); holy_token(out, name.name);
fputs("\nversion ", out); holy_token(out, name.version);
fputs("\nrelease ", out); holy_token(out, name.release);
if (arches.count) map_arch(&arches, name.arch, sizeof name.arch, ¬e, &review);
else {
review = 1;
holy_note_add(¬e, "unknown", "the pacscript names no arch, so the recipe is built for "
"the machine that runs the build");
}
fputs("\narch ", out); holy_token(out, name.arch);
fputs("\nlibc any\n", out);
if (summary && *summary) {
char *stop = strchr(summary, '\n');
if (stop) *stop = 0;
if (*summary) { fputs("summary ", out); holy_token(out, summary); fputc('\n', out); }
}
if (homepage && *homepage && *homepage != '[') {
char *written = expand(homepage, &script, &name);
if (written && *written && !strpbrk(written, "$`") && strstr(written, "://")) {
fputs("homepage ", out); holy_token(out, written); fputc('\n', out);
} else {
review = 1;
holy_note_add(¬e, "unknown", "url %s is not a single address, so it is reported",
homepage);
}
free(written);
}
fputs("x-source-family pacstall\nx-converter pacstall-1\n", out);
if (*name.epoch) {
fputs("x-epoch ", out); holy_token(out, name.epoch); fputc('\n', out);
holy_note_add(¬e, "semantic-change", "the epoch %s is preserved and dropped, since it "
"orders a packaging revision and is not a version", name.epoch);
}
if (split) {
fputs("x-pkgbase ", out); holy_token(out, name.base); fputc('\n', out);
holy_note_add(¬e, "semantic-change", "the list of names becomes one output each, and "
"pkgbase %s names the build they come from", name.base);
}
for (index = 0; carried[index].key; ++index) {
struct pac_list list = {0};
if (!read_list(&script, carried[index].key, &list)) { result = 1; goto done; }
joined = join_list(&list);
if (joined && *joined) {
fputs(carried[index].record, out);
fputc(' ', out);
holy_token(out, joined);
fputc('\n', out);
holy_note_add(¬e, "carried", "%s %s", carried[index].key, joined);
}
free(joined);
joined = NULL;
free_list(&list);
}
{
/* the machine specific lists a pacscript may declare, and the keys that
steer a Pacstall run */
for (index = 0; index < script.value_count; ++index) {
const char *key = script.values[index].name;
int seen = 0;
for (i = 0; known[i]; ++i)
if (!strcmp(key, known[i])) seen = 1;
if (seen) continue;
for (i = 0; lists[i]; ++i) {
size_t used = strlen(lists[i]);
if (strncmp(key, lists[i], used) || key[used] != '_') continue;
review = 1;
holy_note_add(¬e, "unknown", "the %s list names a machine or a distribution, "
"which a Holy recipe holds no list for", key);
seen = 1;
break;
}
if (seen) continue;
review = 1;
holy_note_add(¬e, "unknown", "the %s key is a Pacstall setting this converter "
"reports", key);
}
for (index = 0; settings[index]; ++index) {
if (!holy_shell_present(&script, settings[index])) continue;
review = 1;
holy_note_add(¬e, "unknown", "%s steers a Pacstall run and a Holy recipe carries "
"it neither", settings[index]);
}
for (index = 0; digests[index]; ++index) {
if (!holy_shell_present(&script, digests[index])) continue;
review = 1;
holy_note_add(¬e, "unknown", "the %s list pins a digest a Holy source cannot use",
digests[index]);
}
}
/* the outputs: one per name a list declares, or one for the package itself */
if (split) {
for (index = 0; index < names.count; ++index) {
fputs("output ", out);
holy_token(out, names.items[index].text);
fputs(" runtime\n", out);
}
} else {
fputs("output ", out); holy_token(out, name.name);
fputs(" runtime\n", out);
}
/* the engine fetches and unpacks the recorded sources, so the extracted tree
takes the place of the pacstall srcdir */
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 engine fetches and unpacks the recorded sources, "
"so the extracted tree takes the place of srcdir");
if (sources.count) {
if (!emit_sources(out, &sources, &sums, directory, output, &script, &name, input, ¬e,
&review)) {
result = 1;
goto done;
}
if (sums.count > sources.count) {
review = 1;
holy_note_add(¬e, "unknown", "the sha256sums list holds %zu entries for %zu "
"sources, and this converter reads them in the order the sources stand",
sums.count, sources.count);
}
} else {
review = 1;
holy_note_add(¬e, "unknown", "the pacscript names no source, so the recipe builds "
"nothing to package");
}
for (index = 0; dependencies[index].key; ++index) {
struct pac_list list = {0};
if (!read_list(&script, dependencies[index].key, &list)) { result = 1; goto done; }
if (list.count) {
if (!emit_dependencies(out, &list, dependencies[index].kind,
dependencies[index].key, input, ¬e, &review)) {
free_list(&list);
result = 1;
goto done;
}
}
free_list(&list);
}
{
/* a pacdep is a package of the same pacstall repository, so a Holy
resolver has to find it in a source that has it */
struct pac_list list = {0};
if (!read_list(&script, PACDEPS, &list)) { result = 1; goto done; }
for (index = 0; index < list.count; ++index) {
if (!emit_dependency(out, list.items[index].text, "depend", list.items[index].line,
input, ¬e, &review)) {
free_list(&list);
result = 1;
goto done;
}
review = 1;
holy_note_add(¬e, "unknown", "the pacdep %s names a package of the same pacstall "
"repository, so a Holy resolver has to find it in a source that has it",
list.items[index].text);
}
if (list.count)
holy_note_add(¬e, "carried", "%s %s:%zu", PACDEPS, input, list.items[0].line);
free_list(&list);
}
for (index = 0; relations[index].key; ++index) {
struct pac_list list = {0};
if (!read_list(&script, relations[index].key, &list)) { result = 1; goto done; }
for (i = 0; i < list.count; ++i) {
fputs(relations[index].record, out);
fputc(' ', out);
holy_token(out, list.items[i].text);
fputc('\n', out);
}
if (list.count)
holy_note_add(¬e, "carried", "%s %s:%zu", relations[index].key, input,
list.items[0].line);
free_list(&list);
}
{
/* an optional dependency is a name with a description, and a Holy recipe
holds no soft requirement, so it is preserved as an x- record */
struct pac_list list = {0};
if (!read_list(&script, "optdepends", &list)) { result = 1; goto done; }
for (i = 0; i < list.count; ++i) {
const char *text = list.items[i].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);
holy_token(out, item);
if (hint && *hint) { fputc(' ', out); holy_token(out, hint); }
fputc('\n', out);
holy_note_add(¬e, "carried", "optdepends %s:%zu", text, list.items[i].line);
}
free_list(&list);
}
{
/* a backup entry is a configuration file apt keeps across an upgrade, and
an r: prefix asks for the old file to be removed */
struct pac_list list = {0};
if (!read_list(&script, "backup", &list)) { result = 1; goto done; }
for (i = 0; i < list.count; ++i) {
const char *path = list.items[i].text;
if (strpbrk(path, "$` \t") || !*path || strstr(path, "..")) {
review = 1;
holy_note_add(¬e, "unknown", "the backup entry %s at %s:%zu is not a plain "
"path, so it is reported", path, input, list.items[i].line);
continue;
}
if (!strncmp(path, "r:", 2)) {
const char *kept = path + 2;
fputs("config ", out);
holy_token(out, *kept == '/' ? kept + 1 : kept);
fputs(" mutable\n", out);
review = 1;
holy_note_add(¬e, "unknown", "the backup entry %s asks for the old file to be "
"removed on an upgrade, and a config record only marks a file as "
"mutable", path);
continue;
}
fputs("config ", out);
holy_token(out, *path == '/' ? path + 1 : path);
fputc('\n', out);
holy_note_add(¬e, "carried", "backup %s:%zu", path, list.items[i].line);
}
free_list(&list);
}
/* the phase functions, each behind the prologue that rebuilds the Pacstall
variables the body reads */
for (index = 0; index < sizeof phases / sizeof *phases; ++index) {
const struct shell_function *function =
holy_shell_function(&script, phases[index].pacscript);
if (!function) continue;
if (split && !strcmp(phases[index].pacscript, "package")) {
review = 1;
holy_note_add(¬e, "unknown", "the pacscript defines package beside the "
"package_CHILD functions of a split pkgbase, and a Holy recipe gives each "
"output one split step");
continue;
}
if (function->conditional) {
review = 1;
holy_note_add(¬e, "unknown", "the %s function is written inside a conditional, which "
"this converter does not choose between", phases[index].pacscript);
}
fprintf(out, "step %s /bin/sh <<STEP\n", phases[index].phase);
fputs("# the Pacstall variables this body reads, rebuilt from the exported Holy paths\n",
out);
if (!emit_prologue(out, &name, NULL, "$HOLY_DEST")) { result = 1; goto done; }
if (function->length &&
fwrite(function->body, 1, function->length, out) != function->length) {
result = 1;
goto done;
}
if (function->length && function->body[function->length - 1] != '\n') fputc('\n', out);
fputs("STEP\n", out);
holy_note_add(¬e, "preserved", "the %s body %s:%zu", phases[index].pacscript, input,
function->first);
for (i = 0; helpers[i]; ++i)
if (calls_helper(function->body, function->length, helpers[i])) {
review = 1;
holy_note_add(¬e, "unknown", "the %s body calls the %s helper, which the "
"Pacstall environment supplies and this converter does not",
phases[index].pacscript, helpers[i]);
}
for (i = 0; i < sizeof environment / sizeof *environment; ++i)
if (reads_variable(function->body, function->length, environment[i].name)) {
review = 1;
holy_note_add(¬e, "helper", "the %s body reads %s, which is %s",
phases[index].pacscript, environment[i].name, environment[i].why);
}
}
if (split) {
/* each package_CHILD function is a file list of its own, and a Holy recipe
gives it a staging tree */
for (index = 0; index < names.count; ++index) {
char function_name[300];
const struct shell_function *function;
snprintf(function_name, sizeof function_name, "package_%s",
names.items[index].text);
function = holy_shell_function(&script, function_name);
if (!function) {
review = 1;
holy_note_add(¬e, "unknown", "the split output %s has no %s function, and a "
"Holy recipe gives each output one split step",
names.items[index].text, function_name);
continue;
}
fprintf(out, "split-step \"%s\" split /bin/sh <<STEP\n", names.items[index].text);
fputs("# the Pacstall variables this body reads, rebuilt from the exported Holy paths\n",
out);
if (!emit_prologue(out, &name, names.items[index].text, "$HOLY_SPLIT_DEST")) {
result = 1;
goto done;
}
if (function->length &&
fwrite(function->body, 1, function->length, out) != function->length) {
result = 1;
goto done;
}
if (function->length && function->body[function->length - 1] != '\n') fputc('\n', out);
fputs("STEP\n", out);
holy_note_add(¬e, "preserved", "the %s body %s:%zu", function_name, input,
function->first);
for (i = 0; helpers[i]; ++i)
if (calls_helper(function->body, function->length, helpers[i])) {
review = 1;
holy_note_add(¬e, "unknown", "the %s body calls the %s helper, which the "
"Pacstall environment supplies and this converter does not",
function_name, helpers[i]);
}
for (i = 0; i < sizeof environment / sizeof *environment; ++i)
if (reads_variable(function->body, function->length, environment[i].name)) {
review = 1;
holy_note_add(¬e, "helper", "the %s body reads %s, which is %s", function_name,
environment[i].name, environment[i].why);
}
}
}
/* the install time functions become one hook per group, and the script travels
with the recipe so that the payload carries it */
for (i = 0; i < sizeof hooks / sizeof *hooks; ++i) {
size_t group = !strcmp(hooks[i].group, "install") ? 0 : 1;
if (hook_paths[group]) continue;
if (!holy_shell_function(&script, hooks[i].pacscript)) continue;
hook_paths[group] = emit_hook(out, hooks[i].group, &name, input, output, &script, ¬e,
&review);
if (!hook_paths[group]) { result = 1; goto done; }
}
for (i = 0; i < 2; ++i) {
char *slash;
if (!hook_paths[i]) continue;
slash = strrchr(hook_paths[i], '/');
if (!slash) { result = 1; goto done; }
*slash = 0;
fprintf(out, "step package /bin/sh <<HOOKS\n"
"mkdir -p \"$HOLY_DEST/%s\"\n"
"cp \"$HOLY_SRC/%s.%s\" \"$HOLY_DEST/%s\"\n"
"HOOKS\n", hook_paths[i], name.name, i ? "remove" : "install",
hook_paths[i]);
}
/* the conditional blocks a pacscript uses to choose between builds, and the
statements that no reader of a pacscript can interpret */
for (index = 0; index < script.condition_count; ++index) {
const char *text = script.conditions[index].text;
const char *newline = strchr(text, '\n');
char *line = holy_shell_copy(text, newline ? (size_t)(newline - text) : strlen(text));
review = 1;
if (script.conditions[index].unreadable)
holy_note_add(¬e, "unknown", "unreadable statement %s:%zu %s", input,
script.conditions[index].line, line ? line : "");
else
holy_note_add(¬e, "unknown", "conditional block %s:%zu %s", input,
script.conditions[index].line, line ? line : "");
free(line);
}
for (index = 0; index < script.value_count; ++index) {
if (!script.values[index].conditional) continue;
review = 1;
holy_note_add(¬e, "unknown", "conditional assignment %s %s:%zu",
script.values[index].name, input, script.values[index].line);
}
if (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 pacstall-1\n", out);
fputs("source-name ", out); holy_token(out, source); fputc('\n', out);
{
const char *at = strrchr(input, '/');
fputs("source-file ", out); holy_token(out, at ? at + 1 : input); fputc('\n', out);
}
if (holy_hash_file(input, digest)) {
fputs("source-sha256 ", out); holy_token(out, digest); fputc('\n', out);
}
fputs("pkgbase ", out); holy_token(out, name.name); fputc('\n', out);
fputs("version ", out); holy_token(out, name.version); fputc('\n', out);
fputs("release ", out); holy_token(out, name.release); fputc('\n', out);
fputs("arch ", out); holy_token(out, name.arch); fputc('\n', out);
{
/* one record holds one value, so the recipe file name is one token */
char named[300];
snprintf(named, sizeof named, "%s.recipe", name.name);
fputs("recipe ", out);
holy_token(out, named);
fputc('\n', out);
}
fprintf(out, "status %s\n", review ? "review-required" : "native");
for (index = 0; index < note.count; ++index) fprintf(out, "%s\n", note.lines[index]);
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 (fclose(out)) { out = NULL; result = 1; goto done; }
out = NULL;
result = review ? 3 : 0;
printf("converted %s status %s\n", name.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);
done:
if (out) fclose(out);
for (i = 0; i < 2; ++i) free(hook_paths[i]);
free_list(&names);
free_list(&arches);
free_list(&sources);
free_list(&sums);
free(summary);
free(homepage);
free(directory);
holy_shell_free(&script);
holy_note_free(¬e);
return result;
}