/* Gentoo ebuild to holy-recipe(5) conversion; see man/holy-recipe.5 and man/holypkg.8. the ebuild is read as text and never run. an ebuild is bash with a metadata header, a set of standard phase functions and a package manager environment, so each phase body keeps its own shell behind a prologue that rebuilds the variables Portage 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 "gentoo.h" #include "shrecipe.h" #include #include #include #include #include #include #include #include #include /* the standard phase functions, in the order Portage runs them */ static const struct { const char *ebuild; const char *phase; } phases[] = { { "src_unpack", "unpack" }, { "src_prepare", "prepare" }, { "src_configure", "configure" }, { "src_compile", "build" }, { "src_test", "check" }, { "src_install", "package" } }; /* the functions a maintainer writes to run beside the standard phases. Portage runs them at install time, so they are not build phases. */ static const char *const maintainer[] = { "pkg_preinst", "pkg_postinst", "pkg_prerm", "pkg_postrm", NULL }; /* the helper functions ebuild.sh exports. a body that calls one cannot run without the Portage environment, so each call is reported by name. */ static const char *const helpers[] = { "econf", "econfargs", "emake", "einstall", "einstalldocs", "einfo", "ewarn", "eerror", "elog", "die", "assert", "use", "usex", "useq", "usen", "has", "hasq", "hasv", "hasd", "hask", "ver_test", "ver_cut", "ver_cmp", "diropts", "into", "insinto", "doins", "dolib", "dobin", "doexe", "doman", "dosbin", "dosrc", "dosed", "dosym", "newbin", "newman", "newdoc", "newrdoc", "newhtmldoc", "newreadme", "dohtml", "doinfo", "dofiledoc", "doicon", "docinto", "default", "strip-bin", "strip-libs", "append-cxxflags", "append-cflags", "append-fflags", "append-cppflags", "append-ldflags", "append-lfs", "filter-flags", "filter-ldflags", "tc-getCC", "tc-getCXX", "tc-getAS", "tc-getNM", "tc-getAR", "tc-getRANLIB", "tc-getSTRIP", "tc-getPKG_TOOL", "tc-getPKG_CONFIG", "tc-getBUILD_CC", "tc-getFEATURES", "tc-getCHOST", "tc-getBUILD_CHOST", "tc-getBUILD_CXX", "tc-export_PKG_CONFIG_ALLOW_SYSTEM_CLIBS", "tc-export_PKG_CONFIG_ALLOW_SYSTEM_LIBS", "user_vacant", "nonfatal", "get_libdir", "get_libdir_arch", NULL }; /* the metadata that steers a build but that a Holy recipe has no place for */ static const char *const metadata[] = { "IUSE", "REQUIRED_USE", "RESTRICT", "PROPERTIES", NULL }; /* the dependency lists, in the order Portage evaluates them */ static const struct { const char *key; const char *kind; } dependencies[] = { { "RDEPEND", "depend" }, { "PDEPEND", "depend" }, { "DEPEND", "build-depend" }, { "BDEPEND", "build-depend" }, { NULL, NULL } }; /* an ebuild states its identity in the file name, which is PN-PV-rPR.ebuild */ struct ebuild_name { char name[256]; char version[256]; char release[64]; char epoch[16]; }; static int is_label_char(char c, int first) { if (isalnum((unsigned char)c)) return 1; if (first) return 0; return c == '.' || c == '_' || c == '+' || c == '-'; } static int is_label(const char *value) { size_t at; if (!value || !*value) return 0; for (at = 0; value[at]; ++at) if (!is_label_char(value[at], at == 0)) return 0; return 1; } /* the package name, version and revision out of a PN-PV-rPR.ebuild file name. the package name may carry dashes, so the split is taken from the right: a trailing -rN is the revision and the last dash a digit follows is the one that starts the version. */ static int split_file_name(const char *file, struct ebuild_name *out) { char base[512]; const char *leaf; size_t length; char *at, *revision = NULL, *split = NULL, *version, *epoch; memset(out, 0, sizeof *out); length = strlen(file); if (length >= sizeof base) return 0; memcpy(base, file, length + 1); /* the identity is in the leaf, so the directory in front of it is dropped */ leaf = strrchr(base, '/'); if (leaf) { ++leaf; memmove(base, leaf, strlen(leaf) + 1); } else { leaf = base; } length = strlen(base); if (length < 9 || strcmp(base + length - 7, ".ebuild")) return 0; base[length - 7] = 0; for (at = base; (at = strchr(at, '-')) != NULL; ++at) { if (at[1] != 'r' || !isdigit((unsigned char)at[2])) continue; { const char *scan = at + 2; size_t digits = 0; for (; isdigit((unsigned char)*scan); ++scan) ++digits; if (!*scan && digits) revision = at; } } if (revision) { size_t used = 0; for (at = revision + 2; *at; ++at) if (used + 1 < sizeof out->release) out->release[used++] = *at; out->release[used] = 0; if (used > 1 && out->release[0] == '0') return 0; *revision = 0; } else { snprintf(out->release, sizeof out->release, "1"); } for (at = base; (at = strchr(at, '-')) != NULL; ++at) if (isdigit((unsigned char)at[1])) split = at; if (!split || split == base) return 0; version = split + 1; /* an epoch names a packaging revision order and is not part of the version */ epoch = strchr(version, ':'); if (epoch) { if (epoch - version >= (ptrdiff_t)sizeof out->epoch) return 0; memcpy(out->epoch, version, (size_t)(epoch - version)); out->epoch[epoch - version] = 0; memmove(version, epoch + 1, strlen(epoch + 1) + 1); } if (!is_label(version) || strlen(version) >= sizeof out->version) return 0; memcpy(out->version, version, strlen(version) + 1); *split = 0; if (!is_label(base) || strlen(base) >= sizeof out->name) return 0; memcpy(out->name, base, strlen(base) + 1); return 1; } /* the directory that holds the ebuild, which also holds its files directory */ static char *ebuild_directory(const char *input) { const char *at = strrchr(input, '/'); size_t used; char *copy; if (!at) return strdup("."); copy = strdup(input); if (!copy) return NULL; used = strlen(copy); while (used && copy[used - 1] == '/') copy[--used] = 0; { char *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 != ')') continue; for (line = index; line; --line) if (at[-(ptrdiff_t)(index - line)] == '\n') break; for (line = line ? line : 0; line < index; ++line) if (body[line] == '#') break; if (line < index) continue; return 1; } return 0; } /* the eclasses an ebuild inherits, in the order Portage loads them */ static char **inherited(const struct shell_script *script, size_t *count) { char **list = NULL; size_t used = 0, index; *count = 0; for (index = 0; index < script->condition_count; ++index) { const char *text = script->conditions[index].text; const char *newline = strchr(text, '\n'); char *line; char **words; size_t word_count = 0, word; if (strncmp(text, "inherit", 7) || (text[7] != ' ' && text[7] != '\t')) continue; /* an unreadable statement carries the rest of the file, so only the line that named the eclasses is read */ line = holy_shell_copy(text, newline ? (size_t)(newline - text) : strlen(text)); if (!line) break; words = holy_shell_words(line + 7, &word_count); for (word = 0; words && word < word_count; ++word) { char **grown = realloc(list, (used + 2) * sizeof *grown); if (!grown) break; list = grown; list[used++] = words[word]; words[word] = NULL; } if (words) holy_shell_words_free(words); free(line); } if (list) list[used] = NULL; *count = used; return list; } /* a Portage version starts at the last dash a digit follows, since a package name may carry dashes of its own */ static const char *atom_split(const char *atom) { const char *last = strrchr(atom, '/'); const char *dash = NULL, *at; last = last ? last + 1 : atom; for (at = last; (at = strchr(at, '-')) != NULL; ++at) if (isdigit((unsigned char)at[1])) dash = at; return dash; } /* the package name of an atom, which is what a Holy record names */ static void atom_name(const char *atom, char *out, size_t size) { const char *last = strrchr(atom, '/'); const char *dash; size_t used; last = last ? last + 1 : atom; dash = atom_split(atom); used = dash ? (size_t)(dash - last) : strlen(last); if (used >= size) used = size - 1; memcpy(out, last, used); out[used] = 0; } /* the version of an atom, which keeps no revision, because a Holy record carries the upstream version */ static int atom_version(const char *atom, char *out, size_t size) { const char *dash = atom_split(atom); size_t used, at; if (!dash) { snprintf(out, size, "-"); return 1; } ++dash; used = strlen(dash); if (!used || used >= size) return 0; for (at = 0; at < used; ++at) if (!is_label_char(dash[at], at == 0)) return 0; memcpy(out, dash, used + 1); return 1; } /* a Gentoo atom is [!!|!][~][OP]CATEGORY/PACKAGE[-PV][:SLOT][use-deps]. a Holy depend record names one package with one comparison, so a blocker becomes a conflict, a version keeps only its upstream part, and a use dependency, a slot and a version range are reported. */ static int emit_atom(FILE *out, const char *raw, const char *kind, struct recipe_note *note, int *review, const char *where) { const char *cursor = raw; char requirement[512], package[256], relation[16] = "any", wanted[256]; size_t used = 0; int blocker = 0, strong = 0, weak = 0; while (*cursor == ' ' || *cursor == '\t') ++cursor; if (*cursor == '!') { ++cursor; blocker = 1; if (*cursor == '!') { ++cursor; strong = 1; } } if (*cursor == '~') { ++cursor; weak = 1; } if (*cursor == '<' || *cursor == '>' || *cursor == '=') { char operator[4] = {0}; size_t operator_length = 0; while (*cursor == '<' || *cursor == '>' || *cursor == '=') { if (operator_length + 1 < sizeof operator) operator[operator_length++] = *cursor; ++cursor; } operator[operator_length] = 0; snprintf(relation, sizeof relation, "%s", holy_relation_name(operator)); } while (used < sizeof requirement && cursor[used] && cursor[used] != ' ' && cursor[used] != '\t' && cursor[used] != ':' && cursor[used] != '[') ++used; if (!used) return 1; memcpy(requirement, cursor, used); requirement[used] = 0; cursor += used; if (*cursor == ':') { *review = 1; holy_note_add(note, "unknown", "the slot of %s in %s is dropped, because a Holy recipe " "holds one version of a package", raw, where); } if (!strchr(requirement, '/')) { *review = 1; holy_note_add(note, "unknown", "%s in %s is not a category/package atom", raw, where); return 1; } if (!strncmp(requirement, "virtual/", 8)) { /* a virtual names an interface rather than a package, so a record that holds the name alone cannot say which package is meant */ *review = 1; holy_note_add(note, "unknown", "the virtual %s in %s names an interface, not a " "package, so a Holy record cannot require it", raw, where); } if (!atom_version(requirement, wanted, sizeof wanted)) { *review = 1; holy_note_add(note, "unknown", "the version of %s in %s is a range or a wildcard, " "which a Holy record cannot hold", raw, where); return 1; } atom_name(requirement, package, sizeof package); if (!*package) return 1; /* an atom with no comparator but a version still names one version */ if (strcmp(wanted, "-") && !strcmp(relation, "any")) snprintf(relation, sizeof relation, "eq"); if (blocker) { fputs("x-conflicts ", out); holy_token(out, package); fputc('\n', out); if (strong) { *review = 1; holy_note_add(note, "unknown", "the strong blocker %s in %s names a blocker with " "no expiry, which a conflict record cannot hold", raw, where); } if (strcmp(wanted, "-")) { *review = 1; holy_note_add(note, "unknown", "the blocker %s in %s names a version, which a " "conflict record cannot hold", raw, where); } holy_note_add(note, "carried", "%s %s becomes a conflict", where, raw); return 1; } if (weak) { *review = 1; holy_note_add(note, "unknown", "the ~ atom %s in %s is a version range, which a Holy " "depend record cannot hold, so only the package is carried", raw, where); snprintf(wanted, sizeof wanted, "-"); snprintf(relation, sizeof relation, "any"); } fputs(kind, out); fputc(' ', out); holy_token(out, package); if (strcmp(wanted, "-")) { fputc(' ', out); holy_token(out, relation); fputc(' ', out); holy_token(out, wanted); } fputc('\n', out); holy_note_add(note, "carried", "%s %s becomes a requirement", where, raw); return 1; } /* 1 when a ${...} 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); } /* an ebuild names its own identity in these variables, so a value written with only them can be written out; one that needs the Portage environment, or a parameter expansion modifier, is refused rather than half expanded */ static char *expand_identity(const char *value, const struct ebuild_name *name, const char *eapi) { const char *cursor = value; char *out = NULL; size_t used = 0; char joined[600]; for (;;) { const char *open = strchr(cursor, '$'); size_t at, length, inner_length; const char *text = NULL; if (!open) break; if (open[1] != '{' && open[1] != '(') { cursor = open + 1; continue; } { 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] == '%') { free(out); return NULL; } } length = (size_t)(open - cursor); inner_length = at - 2; if (variable(open + 2, inner_length, "PN")) text = name->name; else if (variable(open + 2, inner_length, "PV")) text = name->version; else if (variable(open + 2, inner_length, "PR")) text = name->release; else if (variable(open + 2, inner_length, "PF")) text = name->name; else if (variable(open + 2, inner_length, "EAPI")) text = eapi; else if (variable(open + 2, inner_length, "PVR")) { if (strcmp(name->release, "1")) snprintf(joined, sizeof joined, "%s-r%s", name->version, name->release); else snprintf(joined, sizeof joined, "%s", name->version); text = joined; } else if (variable(open + 2, inner_length, "P")) { if (strcmp(name->release, "1")) snprintf(joined, sizeof joined, "%s-%s-r%s", name->name, name->version, name->release); else snprintf(joined, sizeof joined, "%s-%s", name->name, name->version); text = joined; } /* any other variable needs the Portage environment, so it is refused */ if (!text) { free(out); return NULL; } { size_t add = length + strlen(text) + at + 1; char *grown = realloc(out, used + add + 1); if (!grown) { 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; } cursor = open + at + 1; } { 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; } /* a PATCHES entry names a file in the files directory beside the ebuild, so the ${FILESDIR} prefix is the one path this converter can write out, and the rest of the entry is expanded afterwards */ static char *files_directory_path(const char *entry, const char *directory, const struct ebuild_name *name, const char *eapi) { const char *marker = strstr(entry, "${FILESDIR}"); char *joined; char *out; if (!marker) return expand_identity(entry, name, eapi); /* a ${FILESDIR:-path} default needs a shell to pick, so it is refused */ if (marker[11] == ':' || marker[11] == '-' || marker[11] == '+' || marker[11] == '=' || marker[11] == '#' || marker[11] == '%' || marker[11] == '?') return NULL; { size_t head = (size_t)(marker - entry); size_t tail = strlen(entry + head + 11); joined = malloc(head + strlen(directory) + 7 + tail + 1); if (!joined) return NULL; memcpy(joined, entry, head); sprintf(joined + head, "%s/files/%s", directory, entry + head + 11); } out = expand_identity(joined, name, eapi); free(joined); return out; } /* the name of the USE flag a mark? stands for, which runs back over the name and an optional leading bang. returns the start of the name. */ static const char *flag_start(const char *begin, const char *mark) { const char *at = mark; while (at > begin && (at[-1] == '!' || at[-1] == '*' || at[-1] == '@')) --at; while (at > begin && (isalnum((unsigned char)at[-1]) || at[-1] == '_' || at[-1] == '-' || at[-1] == '+' || at[-1] == '.')) --at; return at; } /* a dependency string is a list of atoms, USE conditionals and any-of groups. a Holy recipe has no USE flags, so a conditional group is reported and the atoms inside it are carried anyway, since dropping them would lose a dependency the build needs when the flag is on. */ static int emit_dependencies(FILE *out, const char *value, const char *kind, struct recipe_note *note, int *review, const char *where, int depth) { const char *cursor = value; if (depth > 6) { *review = 1; holy_note_add(note, "unknown", "the groups in %s nest too deeply for this converter", where); return 1; } while (*cursor) { const char *start; char *atom; size_t used; while (*cursor == ' ' || *cursor == '\t' || *cursor == '\n') ++cursor; if (!*cursor) break; /* an any-of group is a set of alternatives of which one is enough */ if ((cursor[0] == '|' && cursor[1] == '|') || (cursor[0] == '^' && cursor[1] == '^')) { const char *open = strchr(cursor, '('); const char *close = open ? memchr(open, ')', strlen(open)) : NULL; *review = 1; holy_note_add(note, "unknown", "the any-of group in %s is reported as a whole, " "since a Holy recipe cannot choose one of its members", where); if (open && open - cursor < 4 && close) { char *inner = holy_shell_copy(open + 1, (size_t)(close - open - 1)); if (inner) { if (!emit_dependencies(out, inner, kind, note, review, where, depth + 1)) { free(inner); return 0; } free(inner); } cursor = close + 1; continue; } /* a group with no parentheses runs to the end of the list, so every remaining atom is one of its members */ { char *rest = strdup(cursor + 2); if (rest && !emit_dependencies(out, rest, kind, note, review, where, depth + 1)) { free(rest); return 0; } free(rest); } break; } /* a USE conditional is flag? ( list ), which this converter cannot switch */ { const char *mark = strchr(cursor, '?'); const char *open = strchr(cursor, '('); int header = 1; const char *scan; for (scan = cursor; mark && scan < mark; ++scan) if (!isalnum((unsigned char)*scan) && !strchr("_-.+!*@", *scan)) header = 0; if (mark && open && header && open > mark && open[-1] != '^' && open[-1] != '|') { const char *close = memchr(open, ')', strlen(open)); const char *from = flag_start(cursor, mark); size_t between = (size_t)(mark - from); if (close && between < 128) { char flag[128]; int negated = mark > from && mark[-1] == '!'; memcpy(flag, from, between); flag[between] = 0; *review = 1; holy_note_add(note, "unknown", "the USE flag %s in %s gates its atoms, and " "a Holy recipe cannot switch a flag, so the group is reported", negated ? flag + 1 : flag, where); { char *inner = holy_shell_copy(open + 1, (size_t)(close - open - 1)); if (inner) { if (!emit_dependencies(out, inner, kind, note, review, where, depth + 1)) { free(inner); return 0; } free(inner); } } cursor = close + 1; continue; } } } start = cursor; while (*cursor && *cursor != ' ' && *cursor != '\t' && *cursor != '\n') ++cursor; used = (size_t)(cursor - start); atom = holy_shell_copy(start, used); if (!atom) return 0; { /* a use dependency is bracketed and a group is parenthesized, and a Holy record holds neither, so both are cut and the first is named */ char *mark = strchr(atom, '['); if (mark) { *review = 1; holy_note_add(note, "unknown", "the use dependency of %s in %s is dropped, " "because a Holy recipe has no USE flags", atom, where); *mark = 0; } else { mark = strchr(atom, '('); if (mark) *mark = 0; } } if (strncmp(atom, "${", 2) && strncmp(atom, "$(", 2)) { if (!emit_atom(out, atom, kind, note, review, where)) { free(atom); return 0; } } else { *review = 1; holy_note_add(note, "unknown", "the list in %s includes %s, which this converter " "does not expand", where, atom); } free(atom); } return 1; } /* the prologue every phase body runs behind, rebuilt from the exported paths */ static int emit_prologue(FILE *out, const struct ebuild_name *name, const char *eapi) { fprintf(out, "EPREFIX=\"\"\nED=\"$HOLY_DEST\"\nD=\"$HOLY_DEST\"\nDESTDIR=\"$HOLY_DEST\"\n"); fprintf(out, "WORKDIR=\"$HOLY_SRC\"\nDISTDIR=\"$HOLY_SRC\"\nT=\"$HOLY_BUILD\"\n"); fprintf(out, "PN=%s\nPV=%s\nPR=%s\nPF=%s\nEAPI=%s\n", name->name, name->version, name->release, name->name, eapi); fprintf(out, "P=%s-%s", name->name, name->version); if (strcmp(name->release, "1")) fprintf(out, "-r%s", name->release); fputc('\n', out); fprintf(out, "PVR=%s", name->version); if (strcmp(name->release, "1")) fprintf(out, "-r%s", name->release); fputc('\n', out); return fputs("cd \"$HOLY_SRC\" || exit 1\n", out) >= 0; } /* one source record. a remote archive has no digest here, because a Gentoo Manifest pins a BLAKE2B and a SHA-512 rather than the SHA-256 a Holy source needs, so it is reported; a file from the files directory travels with the recipe. */ static int emit_source(FILE *out, const char *entry, const char *directory, const char *output, const struct ebuild_name *name, const char *eapi, struct recipe_note *note, int *review) { char *copy = NULL; char *written = NULL; char *target_name = NULL; char *arrow; int ok = 1; copy = strdup(entry); if (!copy) return 0; arrow = strstr(copy, "->"); if (arrow) { char *stop = arrow + 2; *arrow = 0; while (*stop == ' ') ++stop; target_name = strdup(stop); } else { target_name = strdup(entry); } if (!target_name) { free(copy); return 0; } /* the file name Portage uses is the target of a rename, and it is the name an archive keeps in the distfiles directory */ written = expand_identity(target_name, name, eapi); free(target_name); if (!written) { free(copy); return 0; } target_name = written; if (!strncmp(target_name, "mirror://", 9)) { *review = 1; holy_note_add(note, "unknown", "the source %s is a mirror URI, which names no single " "address, so it is reported rather than fetched", target_name); } else if (strstr(target_name, "://")) { *review = 1; holy_note_add(note, "unknown", "the source %s has no sha256, and a Gentoo Manifest " "pins a BLAKE2B rather than one, so it is reported rather than fetched", target_name); } else if (strchr(target_name, '/')) { *review = 1; holy_note_add(note, "unknown", "the source %s names a path this converter cannot read", target_name); } else { char original[4096], target[4096], digest[65]; struct stat st; snprintf(original, sizeof original, "%s/files/%s", directory, target_name); snprintf(target, sizeof target, "%s/%s", output, target_name); if (stat(original, &st) || !S_ISREG(st.st_mode)) { *review = 1; holy_note_add(note, "unknown", "the source %s is not in the files directory, so " "this converter cannot hash it", target_name); } else if (!holy_copy_and_hash(original, target, digest)) { ok = 0; } else { fputs("source ", out); holy_token(out, target_name); fputc(' ', out); holy_token(out, target_name); fputc('\n', out); fputs("source-sha256 ", out); holy_token(out, target_name); fputc(' ', out); holy_token(out, digest); fputc('\n', out); holy_note_add(note, "carried", "the local source %s", target_name); holy_note_add(note, "semantic-change", "the local source %s is copied next to the " "recipe and hashed as sha256", target_name); } } free(copy); free(target_name); return ok; } /* SRC_URI is a list of entries, and an entry behind a USE flag is written flag? ( url ), which names a file a Holy recipe cannot reach */ static int emit_sources(FILE *out, const char *uri, const char *directory, const char *output, const struct ebuild_name *name, const char *eapi, struct recipe_note *note, int *review) { const char *cursor = uri; while (*cursor) { const char *start, *after; char *entry; size_t used; while (*cursor == ' ' || *cursor == '\t' || *cursor == '\n') ++cursor; if (!*cursor) break; start = cursor; while (*cursor && *cursor != ' ' && *cursor != '\t' && *cursor != '\n') ++cursor; used = (size_t)(cursor - start); after = cursor; entry = holy_shell_copy(start, used); if (!entry) return 0; /* the closing paren of the group follows the entries it holds */ if (used && entry[used - 1] == '?') { const char *open = strchr(after, '('); const char *close = open ? memchr(open, ')', strlen(open)) : NULL; const char *from = flag_start(start, start + used - 1); char flag[128]; size_t between = (size_t)((start + used - 1) - from); if (close && between < sizeof flag) { memcpy(flag, from, between); flag[between] = 0; *review = 1; holy_note_add(note, "unknown", "the USE flag %s gates the sources inside it, " "and a Holy recipe cannot switch a flag", flag); if (open > after + 1) { char *inner = holy_shell_copy(after + 1, (size_t)(open - after - 1)); if (inner) { if (!emit_sources(out, inner, directory, output, name, eapi, note, review)) { free(inner); free(entry); return 0; } free(inner); } } free(entry); cursor = close + 1; continue; } } if (!emit_source(out, entry, directory, output, name, eapi, note, review)) { free(entry); return 0; } free(entry); } return 1; } int holy_convert_gentoo(const char *input, const char *source, const char *output) { struct shell_script script = {0}; struct recipe_note note = {0}; struct ebuild_name name; char *directory = NULL, *summary = NULL, *homepage = NULL, *eapi = NULL; char *license = NULL, *slot = NULL; char recipe_path[4096], report_path[4096], digest[65]; char **eclasses = NULL; size_t eclass_count = 0, index, i; FILE *out = NULL; int result = 1, review = 0, read_status; if (!input || !output || !*output) { fputs("usage: holypkg convert NAME.ebuild --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; } if (!split_file_name(input, &name)) { fputs("holypkg: an ebuild is named PN-PV-rPR.ebuild\n", stderr); return 2; } directory = ebuild_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; } eapi = holy_shell_join(&script, "EAPI"); summary = holy_shell_join(&script, "DESCRIPTION"); homepage = holy_shell_join(&script, "HOMEPAGE"); license = holy_shell_all(&script, "LICENSE"); slot = holy_shell_join(&script, "SLOT"); if (!eapi || !*eapi) { free(eapi); eapi = strdup("0"); if (!eapi) { result = 1; goto done; } } 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; } 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); /* a keyword names a machine an ebuild is tested on rather than the machine that builds it, so the recipe is built for the machine that runs the build */ fputs("\narch \"any\"\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 && !strchr(homepage, '$')) { fputs("homepage ", out); holy_token(out, homepage); fputc('\n', out); } fputs("x-source-family gentoo\nx-converter gentoo-1\n", out); if (strcmp(eapi, "0")) { fputs("x-eapi ", out); holy_token(out, eapi); fputc('\n', out); holy_note_add(¬e, "carried", "EAPI %s", eapi); } else { review = 1; holy_note_add(¬e, "unknown", "the ebuild names no EAPI, so the phase functions it " "defines are the only ones a recipe can see"); } if (license && *license && !strchr(license, '$')) { fputs("x-license ", out); holy_token(out, license); fputc('\n', out); holy_note_add(¬e, "carried", "LICENSE %s", license); } if (slot && *slot && !strchr(slot, '$')) { fputs("x-slot ", out); holy_token(out, slot); fputc('\n', out); holy_note_add(¬e, "carried", "SLOT %s", slot); } fputs("output ", out); holy_token(out, name.name); fputs(" runtime\n", out); /* the eclasses an ebuild inherits, each of which is a helper environment */ eclasses = inherited(&script, &eclass_count); for (index = 0; index < eclass_count; ++index) { review = 1; holy_note_add(¬e, "helper", "the %s eclass, which this converter does not run", eclasses[index]); } /* the metadata that steers a build and that a Holy recipe has no place for */ for (index = 0; metadata[index]; ++index) { char *value = holy_shell_all(&script, metadata[index]); if (!value) continue; if (*value && strcmp(value, "(")) { review = 1; holy_note_add(¬e, "unknown", "%s %s is a package manager setting, and a Holy " "recipe carries neither", metadata[index], value); } free(value); } /* the revision is a packaging revision of its own, and a Holy release names it */ if (strcmp(name.release, "1")) { holy_note_add(¬e, "semantic-change", "the revision r%s becomes the Holy release", name.release); } if (*name.epoch) { holy_note_add(¬e, "semantic-change", "the epoch %s: of the file name is preserved " "and dropped, since it orders a packaging revision and is not a version", name.epoch); } { /* the engine unpacks each declared source under its own name, so one lift turns that into the single WORKDIR tree an ebuild phase expects */ fputs("step unpack /bin/sh </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 WORKDIR"); } { char *value = holy_shell_all(&script, "SRC_URI"); if (value) { if (!emit_sources(out, value, directory, output, &name, eapi, ¬e, &review)) { free(value); result = 1; goto done; } free(value); } } /* a patch the ebuild names lives in the files directory, so it travels with the recipe for the src_prepare default to apply */ { char *value = holy_shell_all(&script, "PATCHES"); const char *cursor = value; if (value) { while (*cursor) { const char *start; char *raw, *quoted, *file, *expanded; size_t used; while (*cursor == ' ' || *cursor == '\t' || *cursor == '\n' || *cursor == '(' || *cursor == ')') ++cursor; if (!*cursor) break; start = cursor; while (*cursor && *cursor != ' ' && *cursor != '\t' && *cursor != '\n' && *cursor != ')' && *cursor != '(') ++cursor; used = (size_t)(cursor - start); raw = holy_shell_copy(start, used); if (!raw) break; quoted = *raw == '"' ? holy_shell_unquote(raw, used) : strdup(raw); free(raw); if (!quoted) break; if (!quoted) break; { /* a list entry may be two adjacent quoted words, and a file name in a patch list never holds a quote of its own */ char *read = quoted, *write = quoted; while (*read) { if (*read != '"') *write++ = *read; ++read; } *write = 0; } expanded = files_directory_path(quoted, directory, &name, eapi); free(quoted); if (!expanded) { review = 1; holy_note_add(¬e, "unknown", "a PATCHES entry is written with a variable " "this converter does not expand, so the patch does not travel " "with the recipe"); continue; } file = strrchr(expanded, '/'); file = file ? file + 1 : expanded; { char target[4096], digest[65]; struct stat st; snprintf(target, sizeof target, "%s/%s", output, file); if (stat(expanded, &st) || !S_ISREG(st.st_mode)) { review = 1; holy_note_add(¬e, "unknown", "the patch %s is not in the files " "directory, so this converter cannot hash it", file); } else if (!holy_copy_and_hash(expanded, target, digest)) { result = 1; free(expanded); break; } 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, digest); fputc('\n', out); holy_note_add(¬e, "carried", "the local patch %s", file); holy_note_add(¬e, "semantic-change", "the patch %s is copied next to " "the recipe and hashed as sha256", file); } } free(expanded); } free(value); } } for (index = 0; dependencies[index].key; ++index) { char *value = holy_shell_all(&script, dependencies[index].key); if (!value) continue; if (*value) { if (!emit_dependencies(out, value, dependencies[index].kind, ¬e, &review, dependencies[index].key, 0)) { free(value); result = 1; goto done; } holy_note_add(¬e, "carried", "%s %s:%zu", dependencies[index].key, input, holy_shell_line(&script, dependencies[index].key)); } free(value); } /* each standard phase function becomes the matching phase, and a phase the ebuild leaves out is the one the eclasses supply */ for (index = 0; index < sizeof phases / sizeof *phases; ++index) { const struct shell_function *function = holy_shell_function(&script, phases[index].ebuild); if (!function) { review = 1; holy_note_add(¬e, "helper", "the %s phase, which the inherited eclasses supply " "and this converter does not run", phases[index].phase); 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].ebuild); } fprintf(out, "step %s /bin/sh <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].ebuild, 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 " "Portage environment supplies and this converter does not", phases[index].ebuild, helpers[i]); } } /* the maintainer scripts run beside the phases rather than inside them */ for (i = 0; maintainer[i]; ++i) { if (!holy_shell_function(&script, maintainer[i])) continue; review = 1; holy_note_add(¬e, "unknown", "the %s function runs at install time through Portage, " "and a Holy hook carries a script rather than a function", maintainer[i]); } 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 gentoo-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 \"any\"\nrecipe ", out); holy_token(out, name.name); fprintf(out, ".recipe\n"); 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 = 3; 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); if (eclasses) holy_shell_words_free(eclasses); free(eapi); free(summary); free(homepage); free(license); free(slot); free(directory); holy_shell_free(&script); holy_note_free(¬e); return result; }