/* Flatpak manifest to holy-recipe(5) conversion; see man/holy-recipe.5 and man/holypkg.8. the manifest is read as JSON text and never built. a manifest names its runtime and its SDK as Flatpak ids, builds every module through a template flatpak-builder supplies and asks for sandbox permissions the installed program may use, so an id becomes a requirement, a buildsystem becomes the shell that runs the same tools and each permission is reported as dropped rather than kept as a promise. */ #define _XOPEN_SOURCE 700 #define _DEFAULT_SOURCE 1 #include "flatpak.h" #include "shrecipe.h" #include #include #include #include #include #include #include #include #include #include /* a manifest states no shell, so a value that asks one is reported rather than expanded */ static int json_literal(const char *value) { return value && *value && !strpbrk(value, "$`\\\"'"); } static int json_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; } /* the last dotted component of a Flatpak id, which is the part a package name may hold; a branch after // names a remote and is left out */ static int flatpak_component(const char *id, char *out, size_t size) { const char *at, *stop; size_t length; if (!id) return 0; at = strstr(id, "//"); stop = at ? at : id + strlen(id); if (stop == id) return 0; for (at = stop; at > id && at[-1] != '.'; --at) continue; length = (size_t)(stop - at); if (!length || length >= size) return 0; memcpy(out, at, length); out[length] = 0; return json_label(out); } /* what one module leaves for the step that builds it */ struct flatpak_build { FILE *out; struct recipe_note *note; const char *input; const char *directory; const char *output; const char *name; char tree[256]; char **patches; size_t patch_count; char **collected; size_t collected_count; int review; }; static int flatpak_note(struct flatpak_build *build, const char *kind, const char *format, ...); /* the lines a source or an option asks the build to run, kept until the step that carries them is open */ static int flatpak_collect(struct flatpak_build *build, const char *line) { char **grown = realloc(build->collected, (build->collected_count + 1) * sizeof *grown); if (!grown) return 0; build->collected = grown; build->collected[build->collected_count] = strdup(line); if (!build->collected[build->collected_count]) return 0; ++build->collected_count; return 1; } static int flatpak_keep_line(struct flatpak_build *build, const char *line) { size_t length = strlen(line); if (!length) return 1; if (fputs(line, build->out) < 0 || fputc('\n', build->out) == EOF) return 0; return 1; } /* a /app path is where the Flatpak template installs and where a Holy build root has nothing, so a path of its own becomes the payload and the rewrite is reported; anything else is left as the manifest wrote it */ static void flatpak_keep_command(struct flatpak_build *build, const char *line) { static const char before[] = "\"'=(:$ "; static const char after[] = "/\"' ):;="; const char *mark = line; const char *at = line; int rewritten = 0; while ((at = strstr(at, "/app"))) { char next = at[4]; if ((at == line || strchr(before, at[-1])) && (!next || strchr(after, next))) { size_t used = (size_t)(at - mark); if (used && fwrite(mark, 1, used, build->out) != used) return; fputs("$DESTDIR", build->out); mark = at + 4; at = mark; rewritten = 1; continue; } at += 4; } if (!rewritten) { flatpak_keep_line(build, line); return; } fputs(mark, build->out); fputc('\n', build->out); flatpak_note(build, "semantic-change", "the build command %s writes under /app, so the " "path becomes $DESTDIR and the install stays in the payload", line); } /* a command list a module runs, kept in the order the manifest writes it */ static void flatpak_keep_commands(struct flatpak_build *build, const struct holy_json_value *list) { size_t i; for (i = 0; i < (list ? list->count : 0); ++i) { const char *line = holy_json_text(list->members[i].value); if (line) flatpak_keep_command(build, line); } } /* one local file the conversion directory carries, with the digest it has there */ static int flatpak_local_record(struct flatpak_build *build, const char *base, const char *digest, const char *kind) { fputs("source ", build->out); holy_token(build->out, base); fputc(' ', build->out); holy_token(build->out, base); fputc('\n', build->out); fputs("source-sha256 ", build->out); holy_token(build->out, base); fputc(' ', build->out); holy_token(build->out, digest); fputc('\n', build->out); flatpak_note(build, "carried", "the %s file %s travels beside the recipe and is verified " "there", kind, base); return 1; } /* copies a file the manifest names beside it into the conversion directory and records it as a local source with its own digest */ static int flatpak_local(struct flatpak_build *build, const char *path, const char *base, char named[4096], char digest[65], const char *kind) { char target[4096]; size_t at; if (!json_literal(base) || !json_label(base) || strstr(base, "/")) return 0; at = snprintf(target, sizeof target, "%s/%s", build->output, base); if (at >= sizeof target) return 0; if (!holy_copy_and_hash(path, target, digest)) { flatpak_note(build, "unknown", "the %s file %s is not beside the manifest, so a Holy " "recipe has no source for it", kind, base); build->review = 1; return 0; } snprintf(named, 4096, "%s", base); return flatpak_local_record(build, base, digest, kind); } static int flatpak_patch(struct flatpak_build *build, const char *file) { char **grown = realloc(build->patches, (build->patch_count + 1) * sizeof *grown); if (!grown) return 0; build->patches = grown; build->patches[build->patch_count] = strdup(file); if (!build->patches[build->patch_count]) return 0; ++build->patch_count; return 1; } /* one source of one module: a recipe record where the format allows one, and a step line or a report where it does not */ static void flatpak_source(struct flatpak_build *build, const char *label, size_t index, const struct holy_json_value *entry) { const char *type = holy_json_text(holy_json_get(entry, "type")); const char *url = holy_json_text(holy_json_get(entry, "url")); const char *sum = holy_json_text(holy_json_get(entry, "sha256")); const char *path = holy_json_text(holy_json_get(entry, "path")); const char *contents = holy_json_text(holy_json_get(entry, "contents")); const char *options = holy_json_text(holy_json_get(entry, "options")); const struct holy_json_value *expressions = holy_json_get(entry, "sed"); char named[4096], named_path[4096], digest[65], record[512]; size_t at; if (!type && url) type = "archive"; if (!type) type = "file"; if (!strcmp(type, "archive") || !strcmp(type, "archive-url")) { if (!url || !json_literal(url)) { flatpak_note(build, "unknown", "the %s source of module %s names no address, which " "a Holy source needs", type, label); build->review = 1; return; } if (!strstr(url, "://")) { /* an archive beside the manifest travels with the recipe, and the engine extracts it where the module builds */ at = snprintf(named_path, sizeof named_path, "%s/%s", build->directory, url); if (at >= sizeof named_path) return; if (!flatpak_local(build, named_path, url, named, digest, "archive")) return; if (sum && strcmp(sum, digest)) { flatpak_note(build, "unknown", "the archive %s states the digest %s and has " "%s beside the manifest", url, sum, digest); build->review = 1; } if (!build->tree[0] && strlen(url) < sizeof build->tree) memcpy(build->tree, url, strlen(url) + 1); } else if (strncmp(url, "https://", 8)) { flatpak_note(build, "unknown", "the %s source of module %s is %s, and a Holy source " "is fetched over https", type, label, strncmp(url, "http://", 7) ? "a non-https address" : "a plain http address"); build->review = 1; return; } else { snprintf(record, sizeof record, "%s-%zu", label, index); if (!json_label(record)) { flatpak_note(build, "unknown", "the module %s has a name a source record cannot " "hold", label); build->review = 1; return; } fputs("source ", build->out); holy_token(build->out, record); fputc(' ', build->out); holy_token(build->out, url); fputc('\n', build->out); flatpak_note(build, "preserved", "the archive %s of module %s", url, label); if (!sum) { flatpak_note(build, "unknown", "the archive %s states no sha256, and a Holy " "network source needs one", url); build->review = 1; return; } fputs("source-sha256 ", build->out); holy_token(build->out, record); fputc(' ', build->out); holy_token(build->out, sum); fputc('\n', build->out); /* the engine extracts an archive into HOLY_SRC/NAME, and that tree is where the module builds */ if (!build->tree[0] && strlen(record) < sizeof build->tree) memcpy(build->tree, record, strlen(record) + 1); } if (holy_json_get(entry, "strip-components")) flatpak_note(build, "unknown", "the archive %s asks for strip-components, which the " "engine does not apply", url); if (holy_json_get(entry, "dest-filename")) flatpak_note(build, "unknown", "the archive %s names dest-filename, and the engine " "names the extracted tree itself", url); if (holy_json_text(holy_json_get(entry, "dest")) && strcmp(holy_json_text(holy_json_get(entry, "dest")), ".")) flatpak_note(build, "unknown", "the archive %s places itself at %s inside the build " "tree, and a Holy source has one extracted directory", url, holy_json_text(holy_json_get(entry, "dest"))); return; } if (!strcmp(type, "file") || !strcmp(type, "patch") || !strcmp(type, "directory")) { if (!path || strchr(path, '/') || !json_label(path)) { flatpak_note(build, "unknown", "the %s source of module %s names no file beside the " "manifest", type, label); build->review = 1; return; } at = snprintf(named_path, sizeof named_path, "%s/%s", build->directory, path); if (at >= sizeof named_path) return; if (!strcmp(type, "directory")) { flatpak_note(build, "unknown", "the directory source %s of module %s is copied into " "the conversion directory as one archive, which this converter does " "not do", path, label); build->review = 1; return; } if (!flatpak_local(build, named_path, path, named, digest, type)) return; if (!strcmp(type, "patch")) { if (!flatpak_patch(build, path)) return; if (options && strcmp(options, "-p1")) { flatpak_note(build, "unknown", "the patch %s asks for the option %s, and the step " "applies it with -p1", path, options); build->review = 1; } else { flatpak_note(build, "preserved", "the patch %s applies with -p1 before module %s " "builds", path, label); } } return; } if (!strcmp(type, "inline")) { char target[4096]; const char *name; FILE *written; if (!contents) { flatpak_note(build, "unknown", "the inline source of module %s states no contents", label); build->review = 1; return; } name = holy_json_text(holy_json_get(entry, "dest-filename")); if (!name || !json_literal(name) || !json_label(name)) name = "inline-source"; at = snprintf(target, sizeof target, "%s/%s", build->output, name); if (at >= sizeof target) return; written = fopen(target, "wb"); if (!written) return; if (fputs(contents, written) == EOF || fputc('\n', written) == EOF || fclose(written)) { flatpak_note(build, "unknown", "the inline source of module %s cannot be written", label); build->review = 1; return; } if (!holy_hash_file(target, digest)) return; if (!flatpak_local_record(build, name, digest, "inline")) return; flatpak_note(build, "preserved", "the inline source of module %s becomes the file %s", label, name); return; } if (!strcmp(type, "sed")) { size_t i; if (!expressions || expressions->kind != HOLY_JSON_ARRAY) { flatpak_note(build, "unknown", "the sed source of module %s names no expression", label); build->review = 1; return; } for (i = 0; i < expressions->count; ++i) { const char *expression = holy_json_text(expressions->members[i].value); if (!expression) continue; flatpak_note(build, "unknown", "the sed source of module %s rewrites a file the " "Flatpak template owns (%s), so the build needs it by hand", label, expression); build->review = 1; } return; } if (!strcmp(type, "shell")) { const struct holy_json_value *commands = holy_json_get(entry, "commands"); size_t i; for (i = 0; i < (commands ? commands->count : 0); ++i) { const char *line = holy_json_text(commands->members[i].value); if (line && !flatpak_collect(build, line)) return; } flatpak_note(build, "preserved", "the shell source of module %s runs before the module " "builds", label); return; } flatpak_note(build, "unknown", "the %s source of module %s is a checkout or a rule that a " "Holy recipe has no record for", type, label); build->review = 1; } /* the shell that runs the tools a buildsystem names, since the flatpak-builder template that normally runs them does not travel with the recipe */ static void flatpak_buildsystem(struct flatpak_build *build, const char *system, const struct holy_json_value *options, const char *prefix) { const struct holy_json_value *flags = holy_json_get(options, "flags"); const struct holy_json_value *directory = holy_json_get(options, "build-dir"); size_t i; int separate = 0; if (holy_json_text(directory) && strcmp(holy_json_text(directory), "false")) separate = 1; if (directory && directory->kind == HOLY_JSON_LITERAL && !strcmp(directory->text, "true")) separate = 1; if (separate && !strcmp(system, "simple")) { flatpak_note(build, "unknown", "the simple buildsystem has no build directory, so the " "build-dir option of module %s does nothing", build->name); build->review = 1; } if (flags && flags->count && (!system || !strcmp(system, "simple"))) { flatpak_note(build, "unknown", "the flags of module %s name no build system, since it is " "simple", build->name); build->review = 1; } if (!system) system = "simple"; if (!strcmp(system, "simple")) return; if (separate) flatpak_note(build, "semantic-change", "the %s template of module %s builds in a " "separate directory, and the step below changes into the source tree", system, build->name); if (!strcmp(system, "make")) { flatpak_note(build, "semantic-change", "the make template of module %s is replaced by the " "make and make install lines below", build->name); flatpak_keep_line(build, "make -j\"$HOLY_JOBS\""); flatpak_keep_line(build, "make DESTDIR=\"$HOLY_DEST\" install"); return; } if (!strcmp(system, "autotools") || !strcmp(system, "autogen")) { flatpak_note(build, "semantic-change", "the %s template of module %s is replaced by the " "configure, make and make install lines below", system, build->name); if (!strcmp(system, "autogen")) { flatpak_keep_line(build, "NOCONFIGURE=1 sh ./autogen.sh"); } fprintf(build->out, "./configure --prefix=\"%s\"", prefix); for (i = 0; i < (flags ? flags->count : 0); ++i) { const char *flag = holy_json_at(flags, i); if (flag) fprintf(build->out, " %s", flag); } fputc('\n', build->out); flatpak_keep_line(build, "make -j\"$HOLY_JOBS\""); flatpak_keep_line(build, "make DESTDIR=\"$HOLY_DEST\" install"); return; } if (!strcmp(system, "cmake")) { flatpak_note(build, "semantic-change", "the cmake template of module %s is replaced by the " "cmake configure, build and install lines below", build->name); flatpak_keep_line(build, "cmake -S . -B build -DCMAKE_INSTALL_PREFIX=\"$PREFIX\"" " -DCMAKE_INSTALL_LIBDIR=lib"); for (i = 0; i < (flags ? flags->count : 0); ++i) { const char *flag = holy_json_at(flags, i); if (flag) fprintf(build->out, " %s\n", flag); } flatpak_keep_line(build, "cmake --build build -j \"$HOLY_JOBS\""); flatpak_keep_line(build, "DESTDIR=\"$HOLY_DEST\" cmake --install build"); return; } if (!strcmp(system, "meson")) { flatpak_note(build, "semantic-change", "the meson template of module %s is replaced by the " "meson setup, compile and install lines below", build->name); flatpak_keep_line(build, "meson setup build --prefix=\"$PREFIX\" --libdir=lib"); for (i = 0; i < (flags ? flags->count : 0); ++i) { const char *flag = holy_json_at(flags, i); if (flag) fprintf(build->out, " %s\n", flag); } flatpak_keep_line(build, "meson compile -C build -j \"$HOLY_JOBS\""); flatpak_keep_line(build, "DESTDIR=\"$HOLY_DEST\" meson install -C build"); return; } if (!strcmp(system, "cargo")) { flatpak_note(build, "unknown", "the cargo template of module %s installs the binaries it " "finds under target/release, and this converter does not choose one", build->name); flatpak_note(build, "semantic-change", "the cargo template of module %s is replaced by " "the cargo build line below", build->name); build->review = 1; flatpak_keep_line(build, "cargo build --release"); return; } flatpak_note(build, "helper", "the %s buildsystem of module %s is a flatpak-builder " "template, which no Holy phase provides", system, build->name); build->review = 1; } /* the environment a module asks the build to run with */ static void flatpak_environment(struct flatpak_build *build, const struct holy_json_value *options) { static const struct { const char *key; const char *variable; } variables[] = { { "cflags", "CFLAGS" }, { "cxxflags", "CXXFLAGS" }, { "ldflags", "LDFLAGS" }, { "cppflags", "CPPFLAGS" }, { NULL, NULL } }; const struct holy_json_value *env = holy_json_get(options, "env"); const struct holy_json_value *append = holy_json_get(options, "append-path"); const char *prefix = holy_json_text(holy_json_get(options, "prefix")); size_t i, j; if (prefix && strcmp(prefix, "/app")) flatpak_note(build, "semantic-change", "the prefix %s of module %s is kept, where a " "Flatpak module installs under /app", prefix, build->name); else if (prefix) flatpak_note(build, "semantic-change", "the prefix /app of module %s becomes /usr, " "because a Holy payload has no /app", build->name); /* env is written as a record or as a list of NAME=VALUE */ for (i = 0; i < (env ? env->count : 0); ++i) { if (env->kind == HOLY_JSON_OBJECT) { const char *key = env->members[i].key; const char *value = holy_json_text(env->members[i].value); if (key && value) fprintf(build->out, "export %s=\"%s\"\n", key, value); continue; } { const char *entry = holy_json_at(env, i); if (entry) fprintf(build->out, "export %s\n", entry); } } for (i = 0; variables[i].key; ++i) { char *value = holy_json_joined(holy_json_get(options, variables[i].key)); if (!value) continue; fprintf(build->out, "export %s=\"%s\"\n", variables[i].variable, value); free(value); } for (i = 0; i < (append ? append->count : 0); ++i) { const char *entry = holy_json_at(append, i); if (!entry) continue; fprintf(build->out, "PATH=\"%s:$PATH\"\n", entry); } if (env && env->kind != HOLY_JSON_OBJECT && (!append || !append->count)) { for (j = 0; j < env->count; ++j) { const char *entry = holy_json_at(env, j); if (entry && !strncmp(entry, "PATH=", 5)) flatpak_note(build, "unknown", "module %s sets PATH in env, where the template " "reads the search path of the SDK", build->name); } } } /* one module: its sources, then the step that builds it */ static void flatpak_module(struct flatpak_build *build, const struct holy_json_value *module, const struct holy_json_value *shared) { const char *name = holy_json_text(holy_json_get(module, "name")); const char *system = holy_json_text(holy_json_get(module, "buildsystem")); const struct holy_json_value *options = holy_json_get(module, "build-options"); const struct holy_json_value *commands = holy_json_get(module, "build-commands"); const struct holy_json_value *sources = holy_json_get(module, "sources"); const struct holy_json_value *pre, *post, *install; const char *prefix = "usr"; size_t i; if (!name) name = "module"; if (!json_label(name)) { flatpak_note(build, "unknown", "the module name %s is not one a source record holds", name); build->review = 1; return; } build->name = name; build->patch_count = 0; build->tree[0] = 0; for (i = 0; i < (sources ? sources->count : 0); ++i) flatpak_source(build, name, i, sources->members[i].value); /* the options of a module replace the ones the manifest states for them all */ if (!options) options = shared; pre = options ? holy_json_get(options, "pre-commands") : NULL; post = options ? holy_json_get(options, "post-commands") : NULL; install = options ? holy_json_get(options, "post-install") : NULL; if (options && holy_json_text(holy_json_get(options, "prefix"))) prefix = holy_json_text(holy_json_get(options, "prefix")); while (*prefix == '/') ++prefix; /* the patches of a module apply to its tree, so they run before it builds */ if (build->patch_count) { fprintf(build->out, "step prepare /bin/sh <tree[0]) { fprintf(build->out, "cd \"$HOLY_SRC/%s\" || exit 1\n", build->tree); } else { flatpak_keep_line(build, "cd \"$HOLY_SRC\" || exit 1"); } for (i = 0; i < build->patch_count; ++i) fprintf(build->out, "patch -p1 -i \"$HOLY_SRC/%s\" < /dev/null || exit 1\n", build->patches[i]); fputs("STEP\n", build->out); flatpak_note(build, "preserved", "the patches of module %s apply before it builds", name); } fprintf(build->out, "step build /bin/sh <out, "export PREFIX=/%s\n", prefix); flatpak_keep_line(build, "export DESTDIR=\"$HOLY_DEST\""); if (options) flatpak_environment(build, options); if (build->tree[0]) { fprintf(build->out, "cd \"$HOLY_SRC/%s\" || exit 1\n", build->tree); } else { flatpak_keep_line(build, "cd \"$HOLY_SRC\" || exit 1"); } flatpak_keep_commands(build, pre); for (i = 0; i < build->collected_count; ++i) flatpak_keep_command(build, build->collected[i]); flatpak_keep_commands(build, commands); flatpak_buildsystem(build, system, options, "$PREFIX"); flatpak_keep_commands(build, install); flatpak_keep_commands(build, post); fputs("STEP\n", build->out); flatpak_note(build, "preserved", "the module %s builds as one step in module order", name); if (holy_json_get(module, "cleanup")) flatpak_note(build, "unknown", "the cleanup of module %s removes or rewrites metadata the " "Flatpak template owns", name); if (holy_json_get(module, "build-extension") || (options && holy_json_get(options, "build-extension"))) flatpak_note(build, "unknown", "module %s names a build extension, which is an SDK " "feature a Holy build root has no place for", name); for (i = 0; i < build->patch_count; ++i) free(build->patches[i]); free(build->patches); build->patches = NULL; for (i = 0; i < build->collected_count; ++i) free(build->collected[i]); free(build->collected); build->collected = NULL; build->collected_count = 0; } static int flatpak_note(struct flatpak_build *build, const char *kind, const char *format, ...) { char body[1024]; va_list arguments; va_start(arguments, format); vsnprintf(body, sizeof body, format, arguments); va_end(arguments); if (!strcmp(kind, "helper") || !strcmp(kind, "unknown")) build->review = 1; return holy_note_add(build->note, kind, "%s", body); } /* whether any module of the manifest pulls an archive the engine has to extract */ static int flatpak_has_archive(const struct holy_json_value *modules) { size_t index, i; for (index = 0; modules && index < modules->count; ++index) { const struct holy_json_value *module = modules->members[index].value; const struct holy_json_value *sources = holy_json_get(module, "sources"); for (i = 0; i < (sources ? sources->count : 0); ++i) { const char *type = holy_json_text(holy_json_get(sources->members[i].value, "type")); if (type && (!strcmp(type, "archive") || !strcmp(type, "archive-url"))) return 1; if (!type && holy_json_get(sources->members[i].value, "url")) return 1; } } return 0; } int holy_convert_flatpak(const char *input, const char *source, const char *output) { struct holy_json_value *manifest = NULL; struct recipe_note note = {0}; struct flatpak_build build; char *directory = NULL, *cut; char name[256], version[256], release[64], arch[64], digest[65]; char recipe_path[4096], report_path[4096], component[256], branch_text[256]; const char *id, *branch, *summary, *homepage, *command, *sdk, *runtime; const struct holy_json_value *modules, *options, *permissions, *cleanup, *extension; const char *at; size_t index, i; int result = 1, read_status = 0; memset(&build, 0, sizeof build); build.note = ¬e; snprintf(release, sizeof release, "1"); snprintf(version, sizeof version, "0"); snprintf(arch, sizeof arch, "any"); if (!input || !output || !*output) { fputs("usage: holypkg convert NAME.json --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 = strdup(input); if (!directory) return 1; cut = strrchr(directory, '/'); if (cut && cut != directory) *cut = 0; else { free(directory); directory = strdup("."); if (!directory) return 1; } manifest = holy_json_read(input, &read_status); if (!manifest) { if (read_status == 6) { fprintf(stderr, "holypkg: manifest unavailable: %s\n", input); } else { fputs("holypkg: a Flatpak manifest is JSON text, and this converter reads no other " "form\n", stderr); } result = read_status; goto done; } if (manifest->kind != HOLY_JSON_OBJECT) { fputs("holypkg: a Flatpak manifest is JSON text, and this converter reads no other form\n", stderr); result = 2; goto done; } id = holy_json_text(holy_json_get(manifest, "id")); if (!id || !json_literal(id)) { fputs("holypkg: a Flatpak manifest needs a literal id\n", stderr); result = 2; goto done; } { /* an id may name its own machine and branch after a slash, so the package name is the last dotted component of the application id before it */ const char *cursor = strchr(id, '/'); const char *stop = cursor ? cursor : id + strlen(id); if (stop == id) { fputs("holypkg: a Flatpak manifest needs an id with a component\n", stderr); result = 2; goto done; } cursor = stop; while (cursor > id && cursor[-1] != '.') --cursor; if ((size_t)(stop - cursor) >= sizeof name) { fputs("holypkg: a Flatpak manifest id is too long to record\n", stderr); result = 2; goto done; } memcpy(name, cursor, (size_t)(stop - cursor)); name[stop - cursor] = 0; if (!json_label(name)) { fputs("holypkg: a Flatpak manifest id is not a package name\n", stderr); result = 2; goto done; } } if (holy_json_text(holy_json_get(manifest, "version"))) { snprintf(version, sizeof version, "%s", holy_json_text(holy_json_get(manifest, "version"))); if (!json_label(version)) { fputs("holypkg: a Flatpak manifest version is not one a recipe can hold\n", stderr); result = 2; goto done; } } branch = holy_json_text(holy_json_get(manifest, "branch")); if (!branch) { /* an id may carry its own machine and branch, so the tail of the id is the branch the manifest did not write on its own */ const char *tail = strchr(id, '/'); if (tail) { if (snprintf(branch_text, sizeof branch_text, "%s", tail + 1) >= (int)sizeof branch_text) { fputs("holypkg: a Flatpak manifest id carries too long a branch\n", stderr); result = 2; goto done; } branch = branch_text; } } if (holy_json_text(holy_json_get(manifest, "arch"))) { const char *machine = holy_json_text(holy_json_get(manifest, "arch")); if (!machine) machine = holy_json_at(holy_json_get(manifest, "arch"), 0); if (machine && !strcmp(machine, "x86_64")) snprintf(arch, sizeof arch, "x86_64"); else if (machine && (!strcmp(machine, "i386") || !strcmp(machine, "i686"))) snprintf(arch, sizeof arch, "i686"); else if (machine && *machine) { snprintf(arch, sizeof arch, "%s", machine); flatpak_note(&build, "unknown", "the machine %s is written as it stands, and the " "payload decides what the artifact is", machine); } } else if (branch) { /* a branch names the machine and the stability of a build */ char machine[64]; size_t used = 0; while (branch[used] && branch[used] != '/' && used + 1 < sizeof machine) { machine[used] = branch[used]; ++used; } machine[used] = 0; if (!strcmp(machine, "x86_64")) snprintf(arch, sizeof arch, "x86_64"); else if (!strcmp(machine, "i386")) snprintf(arch, sizeof arch, "i686"); } if (mkdir(output, 0700) && errno != EEXIST) { fprintf(stderr, "holypkg: conversion directory unavailable: %s\n", output); goto done; } snprintf(recipe_path, sizeof recipe_path, "%s/%s.recipe", output, name); snprintf(report_path, sizeof report_path, "%s/conversion", output); build.out = fopen(recipe_path, "wb"); if (!build.out) { fprintf(stderr, "holypkg: recipe unavailable: %s\n", recipe_path); goto done; } build.note = ¬e; build.input = input; build.directory = directory; build.output = output; build.name = name; fputs("format holy-recipe-1\nname ", build.out); holy_token(build.out, name); fputs("\nversion ", build.out); holy_token(build.out, version); fputs("\nrelease ", build.out); holy_token(build.out, release); fputs("\narch ", build.out); holy_token(build.out, arch); fputs("\nlibc any\n", build.out); summary = holy_json_text(holy_json_get(manifest, "summary")); if (summary && json_literal(summary) && *summary) { fputs("summary ", build.out); holy_token(build.out, summary); fputc('\n', build.out); flatpak_note(&build, "carried", "the summary of the manifest"); } homepage = holy_json_text(holy_json_get(manifest, "url")); if (homepage && json_literal(homepage) && strstr(homepage, "://")) { fputs("homepage ", build.out); holy_token(build.out, homepage); fputc('\n', build.out); flatpak_note(&build, "carried", "the url of the manifest"); } fputs("x-source-family flatpak\nx-converter flatpak-1\n", build.out); fputs("x-app-id ", build.out); holy_token(build.out, id); fputc('\n', build.out); if (strcmp(arch, "any")) flatpak_note(&build, "carried", "the machine %s of the manifest or its branch", arch); else flatpak_note(&build, "unknown", "the manifest names no machine, so the payload decides " "which artifact the recipe builds"); if (strcmp(version, "0")) flatpak_note(&build, "carried", "the version %s of the manifest", version); else flatpak_note(&build, "semantic-change", "the manifest states no version, so the recipe " "records zero and the build report names it"); if (branch) { fputs("x-flatpak-branch ", build.out); holy_token(build.out, branch); fputc('\n', build.out); flatpak_note(&build, "carried", "the branch %s", branch); } command = holy_json_text(holy_json_get(manifest, "command")); if (command) { fputs("x-flatpak-command ", build.out); holy_token(build.out, command); fputc('\n', build.out); flatpak_note(&build, "semantic-change", "the command %s is a path under /app, and a " "Holy payload installs under /usr", command); } sdk = holy_json_text(holy_json_get(manifest, "sdk")); runtime = holy_json_text(holy_json_get(manifest, "runtime")); if (sdk && flatpak_component(sdk, component, sizeof component)) { fputs("build-depend ", build.out); holy_token(build.out, component); fputs(" \"any\" \"-\"\n", build.out); flatpak_note(&build, "carried", "the sdk %s becomes the build requirement %s", sdk, component); } else if (sdk) { flatpak_note(&build, "unknown", "the sdk %s has no component a requirement could name", sdk); build.review = 1; } if (runtime && flatpak_component(runtime, component, sizeof component)) { fputs("depend ", build.out); holy_token(build.out, component); fputs(" \"any\" \"-\"\n", build.out); flatpak_note(&build, "carried", "the runtime %s becomes the requirement %s", runtime, component); } else if (runtime) { flatpak_note(&build, "unknown", "the runtime %s has no component a requirement could " "name", runtime); build.review = 1; } if (sdk && runtime) { const char *version_text = holy_json_text(holy_json_get(manifest, "runtime-version")); flatpak_note(&build, "semantic-change", "the sdk %s builds the modules and the runtime " "%s%s%s is what the program runs on, so they are separate requirements", sdk, runtime, version_text ? "//" : "", version_text ? version_text : ""); } fputs("output ", build.out); holy_token(build.out, name); fputs(" runtime\n", build.out); permissions = holy_json_get(manifest, "finish-args"); if (permissions && permissions->count) { char joined[1024]; size_t used = 0; for (i = 0; i < permissions->count; ++i) { const char *permission = holy_json_at(permissions, i); if (!permission) continue; flatpak_note(&build, "unknown", "the permission %s is a Flatpak sandbox decision, " "and a Holy package runs with the caller's context", permission); used += (size_t)snprintf(joined + used, sizeof joined - used, "%s%s", used ? " " : "", permission); if (used >= sizeof joined) used = sizeof joined - 1; } fputs("x-flatpak-finish-args ", build.out); holy_token(build.out, joined); fputc('\n', build.out); } cleanup = holy_json_get(manifest, "cleanup"); for (i = 0; i < (cleanup ? cleanup->count : 0); ++i) { const char *entry = holy_json_at(cleanup, i); if (entry) flatpak_note(&build, "unknown", "the cleanup step %s runs after the build inside the " "Flatpak sandbox", entry); } extension = holy_json_get(manifest, "build-extension"); if (extension) flatpak_note(&build, "unknown", "the manifest names the build extension %s, which is an " "SDK feature a Holy build root has no place for", holy_json_text(holy_json_get(extension, "name")) ? holy_json_text(holy_json_get(extension, "name")) : "unknown"); modules = holy_json_get(manifest, "modules"); if (!modules || modules->kind != HOLY_JSON_ARRAY || !modules->count) { flatpak_note(&build, "unknown", "the manifest names no module, so the recipe builds " "nothing to package"); build.review = 1; } else { options = holy_json_get(manifest, "build-options"); if (flatpak_has_archive(modules)) { /* the engine extracts an archive into HOLY_SRC/NAME, and a module builds where its sources are, so one top directory inside it is lifted */ fputs("step unpack /bin/sh </dev/null || true\n" " rm -rf \"$HOLY_SRC/.lifted\"\n" "done\n" "UNPACK\n", build.out); flatpak_note(&build, "semantic-change", "the engine extracts every archive into " "HOLY_SRC/NAME, and one top directory inside it is lifted, so a module " "builds where the manifest expects its sources"); } for (index = 0; index < modules->count; ++index) { const struct holy_json_value *module = modules->members[index].value; if (!module || module->kind != HOLY_JSON_OBJECT) { flatpak_note(&build, "unknown", "the module %zu of the manifest is not a " "record", index + 1); build.review = 1; continue; } flatpak_module(&build, module, options); } if (modules->count > 1) flatpak_note(&build, "semantic-change", "a Flatpak build shares one prefix across " "its %zu modules, while each step here installs into the payload, so a " "later module cannot read an earlier install", modules->count); } if (ferror(build.out) || fclose(build.out)) { build.out = NULL; goto done; } build.out = fopen(report_path, "wb"); if (!build.out) { fprintf(stderr, "holypkg: report unavailable: %s\n", report_path); goto done; } fputs("format holy-recipe-conversion-1\nconverter flatpak-1\n", build.out); fputs("source-name ", build.out); holy_token(build.out, source); fputc('\n', build.out); at = strrchr(input, '/'); fputs("source-file ", build.out); holy_token(build.out, at ? at + 1 : input); fputc('\n', build.out); if (holy_hash_file(input, digest)) { fputs("source-sha256 ", build.out); holy_token(build.out, digest); fputc('\n', build.out); } fputs("app-id ", build.out); holy_token(build.out, id); fputc('\n', build.out); fputs("name ", build.out); holy_token(build.out, name); fputc('\n', build.out); fputs("version ", build.out); holy_token(build.out, version); fputc('\n', build.out); fputs("release ", build.out); holy_token(build.out, release); fputc('\n', build.out); fputs("arch ", build.out); holy_token(build.out, arch); fputc('\n', build.out); { char named[300]; snprintf(named, sizeof named, "%s.recipe", name); fputs("recipe ", build.out); holy_token(build.out, named); fputc('\n', build.out); } fprintf(build.out, "status %s\n", build.review ? "review-required" : "native"); for (index = 0; index < note.count; ++index) fprintf(build.out, "%s\n", note.lines[index]); fprintf(build.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(build.out)) { build.out = NULL; goto done; } build.out = NULL; result = build.review ? 3 : 0; printf("converted %s status %s\n", name, build.review ? "review-required" : "native"); printf("recipe %s\nreport %s\n", recipe_path, report_path); if (build.review) fputs("holypkg: the converted recipe needs review before its first build\n", stderr); done: if (build.out) fclose(build.out); for (i = 0; i < build.patch_count; ++i) free(build.patches[i]); free(build.patches); for (i = 0; i < build.collected_count; ++i) free(build.collected[i]); free(build.collected); holy_json_free(manifest); free(directory); holy_note_free(¬e); return result; }