/* Debian source package to holy-recipe(5) conversion; see man/holy-recipe.5 and man/holypkg.8. the debian directory is read as text and never run. a source package is a control file, a changelog, a rules file and a set of file lists, so it needs a parser of its own. debhelper is a macro framework rather than a build script, so the rules body is carried and every dh call in it is reported as a helper the converter cannot run. */ #define _XOPEN_SOURCE 700 #define _DEFAULT_SOURCE 1 #include "debsrc.h" #include "shrecipe.h" #include #include #include #include #include #include #include #include #include #define MAX_BINARIES 32 #define MAX_FIELDS 96 struct deb_field { char name[64]; char value[2048]; size_t line; int binary; }; struct deb_binary { char name[256]; char summary[1024]; char architecture[64]; size_t line; int has_list; char list[2048]; size_t list_line; int main; }; /* a growing text buffer */ struct text { char *data; size_t used; size_t capacity; }; static int text_add(struct text *out, const char *data, size_t length) { while (out->used + length + 1 > out->capacity) { size_t capacity = out->capacity ? out->capacity * 2 : 256; char *grown = realloc(out->data, capacity); if (!grown) return 0; out->data = grown; out->capacity = capacity; } memcpy(out->data + out->used, data, length); out->used += length; out->data[out->used] = 0; return 1; } static int text_puts(struct text *out, const char *data) { return text_add(out, data, strlen(data)); } static char *join_path(const char *directory, const char *name) { char path[4096]; if (!name || !*name || strlen(name) > 400 || strchr(name, '/') || !strcmp(name, ".") || !strcmp(name, "..")) return NULL; if (snprintf(path, sizeof path, "%s/%s", directory, name) >= (int)sizeof path) return NULL; return strdup(path); } /* the text of a line without its newline and trailing blanks */ static char *clean_line(const char *start, size_t length) { char *value; while (length && isspace((unsigned char)start[length - 1])) --length; while (length && isspace((unsigned char)*start)) { ++start; --length; } value = holy_shell_copy(start, length); return value; } /* reads a whole file into memory, or NULL when it is not there */ static char *read_file(const char *path, size_t *length) { struct stat st; FILE *in = stat(path, &st) || !S_ISREG(st.st_mode) || st.st_size > 4 * 1024 * 1024 ? NULL : fopen(path, "rb"); char *data; size_t size; if (!in) return NULL; size = (size_t)st.st_size; data = malloc(size + 1); if (!data || fread(data, 1, size, in) != size) { free(data); fclose(in); return NULL; } fclose(in); data[size] = 0; if (length) *length = size; return data; } /* the debian substitution variables dpkg fills in, which carry no meaning here */ static int is_substitution(const char *value) { return value[0] == '$' && value[1] == '{'; } /* one debian requirement, split into its name, relation and version. a Debian version is epoch:upstream-revision, so the epoch is reported and dropped. */ static int split_requirement(const char *raw, char *name, size_t size, char *relation, size_t relation_size, char *version, size_t version_size, int *epoch, size_t *at) { const char *cursor = raw; const char *stop; while (*cursor == ' ' || *cursor == '\t') ++cursor; /* an alternative and an architecture qualifier are not one package name */ if (strchr(cursor, '|') || strchr(cursor, '[')) { *at = 1; return 0; } stop = cursor; while (*stop && *stop != ' ' && *stop != '\t' && *stop != '(') ++stop; if (stop == cursor) return 0; if ((size_t)(stop - cursor) >= size) return 0; memcpy(name, cursor, (size_t)(stop - cursor)); name[stop - cursor] = 0; while (*stop == ' ' || *stop == '\t') ++stop; if (*stop != '(') { snprintf(relation, relation_size, "any"); snprintf(version, version_size, "-"); return 1; } ++stop; /* the comparison takes the same name a Holy depend record reads */ { char operator[8]; size_t operator_length = 0; while (*stop && *stop != ' ' && operator_length + 1 < sizeof operator) { if (strchr("<>=", *stop)) operator[operator_length++] = *stop; ++stop; } operator[operator_length] = 0; if (!operator_length) snprintf(relation, relation_size, "any"); else if (!strcmp(operator, "<<")) snprintf(relation, relation_size, "lt"); else if (!strcmp(operator, ">>")) snprintf(relation, relation_size, "gt"); else snprintf(relation, relation_size, "%s", holy_relation_name(operator)); } while (*stop == ' ' || *stop == '\t') ++stop; snprintf(version, version_size, "%s", stop); if (!relation[0]) snprintf(relation, relation_size, "any"); /* a closing paren and a build profile may follow the version */ { char *close = strchr(version, ')'); char *profile = strchr(version, '<'); if (close) *close = 0; if (profile) *profile = 0; if (profile) *at = 2; } /* the epoch names a packaging revision order and is not part of the version */ { char *colon = strchr(version, ':'); if (colon) { *epoch = 1; memmove(version, colon + 1, strlen(colon + 1) + 1); } } /* the revision follows the last dash, and it is the Debian release */ { char *dash = strrchr(version, '-'); if (dash) *dash = 0; } if (!version[0]) snprintf(version, version_size, "-"); return 1; } /* a debian relationship field is comma separated groups, and a group may offer alternatives with a pipe. a Holy depend record names one package, so the first alternative of each group is carried and the rest are reported. */ static char **relationship_groups(const char *value, size_t *count) { char **list = NULL; size_t used = 0; const char *cursor = value; *count = 0; while (cursor && *cursor) { const char *comma = strchr(cursor, ','); size_t length = comma ? (size_t)(comma - cursor) : strlen(cursor); char *group = clean_line(cursor, length); char **grown; if (!group) break; cursor = comma ? comma + 1 : NULL; if (!*group) { free(group); continue; } grown = realloc(list, (used + 2) * sizeof *grown); if (!grown) { free(group); break; } list = grown; list[used++] = group; list[used] = NULL; } *count = used; return list; } static void list_free(char **list); /* writes one depend record */ static void emit_dependency(FILE *out, const char *raw, const char *kind, struct recipe_note *note, int *review, const char *where) { char name[512], relation[16], version[512]; int epoch = 0; size_t at = 0; if (is_substitution(raw)) { *review = 1; holy_note_add(note, "unknown", "the substitution %s in %s is filled in by dpkg", raw, where); return; } if (!split_requirement(raw, name, sizeof name, relation, sizeof relation, version, sizeof version, &epoch, &at)) return; if (at == 1) { *review = 1; holy_note_add(note, "unknown", "requirement %s uses an alternative or a qualifier in " "%s", raw, where); return; } if (at == 2) holy_note_add(note, "preserved", "the build profile of %s in %s", name, where); if (epoch) holy_note_add(note, "preserved", "the epoch of %s in %s", name, where); fputs(kind, out); fputc(' ', out); holy_token(out, name); fputc(' ', out); holy_token(out, relation); fputc(' ', out); holy_token(out, version); fputc('\n', out); } /* one record per line of a debian list, where a blank line continues the record before it, the way dpkg reads a wrapped field */ static char **list_lines(const char *text, size_t *count) { char **list = NULL; size_t used = 0; const char *cursor = text; *count = 0; while (cursor && *cursor) { const char *newline = strchr(cursor, '\n'); size_t length = newline ? (size_t)(newline - cursor) : strlen(cursor); char *entry = clean_line(cursor, length); cursor = newline ? newline + 1 : NULL; if (!entry) break; if (!*entry) { /* an empty line wraps the record before it, so the next line joins it */ if (used && cursor && *cursor && *cursor != '\n') continue; free(entry); continue; } if (*entry == '#') { free(entry); continue; } if (used && entry[0] == ' ') { /* a wrapped continuation is indented, and joins the record before it */ char *joined = malloc(strlen(list[used - 1]) + strlen(entry) + 1); if (joined) { sprintf(joined, "%s%s", list[used - 1], entry); free(list[used - 1]); list[used - 1] = joined; } free(entry); continue; } { char **grown = realloc(list, (used + 2) * sizeof *grown); if (!grown) { free(entry); break; } list = grown; list[used++] = entry; list[used] = NULL; } } *count = used; return list; } static void list_free(char **list) { size_t index; if (!list) return; for (index = 0; list[index]; ++index) free(list[index]); free(list); } int holy_convert_debsrc(const char *input, const char *source, const char *output) { struct deb_field fields[MAX_FIELDS]; struct deb_binary binaries[MAX_BINARIES]; struct recipe_note note; char *control = NULL, *changelog = NULL, *rules = NULL; char *directory = NULL; char name[512] = {0}, version[512] = {0}, release[64] = "1"; char summary[1024] = {0}, homepage[1024] = {0}, arch[64] = "any"; char recipe_path[4096], report_path[4096], control_hash[65] = {0}; FILE *out = NULL; size_t field_count = 0, binary_count = 0, control_length = 0; size_t offset = 0, line = 0, index; size_t mains = 0; int current = -1, last_field = -1, review = 0, result = 1, i, wrote = 1; memset(fields, 0, sizeof fields); memset(binaries, 0, sizeof binaries); memset(¬e, 0, sizeof note); if (!input || !output || !*output) { fputs("usage: holypkg convert debian --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; { size_t used = strlen(directory); while (used && directory[used - 1] == '/') directory[--used] = 0; } { char *path = join_path(directory, "control"); if (!path) { result = 1; goto done; } control = read_file(path, &control_length); if (!control) { fprintf(stderr, "holypkg: debian/control unavailable: %s\n", path); free(path); result = 6; goto done; } free(path); } if (mkdir(output, 0700) && errno != EEXIST) { fprintf(stderr, "holypkg: conversion directory unavailable: %s\n", output); result = 1; goto done; } /* the control file travels with the recipe, and its digest is the provenance */ { char *path = join_path(directory, "control"); char *copy = join_path(output, "control"); if (path && copy) { if (!holy_copy_and_hash(path, copy, control_hash)) wrote = 0; else holy_note_add(¬e, "carried", "debian/control"); } free(path); free(copy); } /* one pass over the control file: a source stanza then one per binary */ while (offset < control_length) { const char *row = control + offset; const char *newline = memchr(row, '\n', control_length - offset); size_t size = newline ? (size_t)(newline - row) : control_length - offset; /* a leading blank marks a continuation, so it is read before the line is trimmed, since the trimmed text no longer says so */ int continuation = size && (row[0] == ' ' || row[0] == '\t'); char *value = clean_line(row, size); ++line; offset += size + (newline ? 1 : 0); if (!value) { result = 1; goto done; } if (!*value) { /* a blank line ends a stanza, so the fields before it belong to one package */ current = -1; last_field = -1; free(value); continue; } if (continuation) { /* a continuation line belongs to the field it follows, so the field before it is tracked rather than searched for, since a stanza repeats names */ if (last_field >= 0 && (size_t)last_field < field_count) { size_t used = strlen(fields[last_field].value); if (used + size + 2 < sizeof fields[0].value) { fields[last_field].value[used++] = ' '; memcpy(fields[last_field].value + used, value, size + 1); } else { review = 1; holy_note_add(¬e, "unknown", "%s runs past what a record holds", fields[last_field].name); } } free(value); continue; } { char *colon = strchr(value, ':'); char *key; char *field; if (!colon || colon == value) { free(value); continue; } *colon = 0; key = clean_line(value, strlen(value)); field = clean_line(colon + 1, strlen(colon + 1)); if (key && field) { int binary = current < 0 ? 0 : current + 1; if (!strcmp(key, "Package")) { if (binary_count < MAX_BINARIES) { snprintf(binaries[binary_count].name, sizeof binaries[0].name, "%s", field); binaries[binary_count].line = line; current = (int)binary_count; ++binary_count; holy_note_add(¬e, "carried", "Package %s %zu", field, line); } else { review = 1; holy_note_add(¬e, "unknown", "more binary packages than outputs are " "carried %s", field); } } if (field_count < MAX_FIELDS) { snprintf(fields[field_count].name, sizeof fields[0].name, "%s", key); snprintf(fields[field_count].value, sizeof fields[0].value, "%s", field); fields[field_count].line = line; fields[field_count].binary = binary; last_field = (int)field_count; ++field_count; if (!binary) { if (!strcmp(key, "Source")) { const char *mark = strchr(field, '('); if (mark) { size_t used = (size_t)(mark - field); while (used && isspace((unsigned char)field[used - 1])) --used; if (used >= sizeof name) used = sizeof name - 1; memcpy(name, field, used); name[used] = 0; } else { snprintf(name, sizeof name, "%s", field); } } else if (!strcmp(key, "Homepage") && !*homepage) { snprintf(homepage, sizeof homepage, "%s", field); } else if (!strcmp(key, "Standards-Version")) { holy_note_add(¬e, "preserved", "Standards-Version %s", field); } else if (!strcmp(key, "Vcs-Browser") && !*homepage) { snprintf(homepage, sizeof homepage, "%s", field); } } else { struct deb_binary *package = &binaries[binary - 1]; if (!strcmp(key, "Architecture")) { snprintf(package->architecture, sizeof package->architecture, "%s", field); } else if (!strcmp(key, "Description")) { snprintf(package->summary, sizeof package->summary, "%s", field); } } } } free(key); free(field); } free(value); } if (!*name) { fputs("holypkg: debian/control: the source stanza needs a Source or Package\n", stderr); result = 2; goto done; } /* the changelog first record gives the version the source was built at */ { char *path = join_path(directory, "changelog"); if (path) { changelog = read_file(path, NULL); free(path); } } if (!changelog || !*changelog) { review = 1; holy_note_add(¬e, "unknown", "debian/changelog is not beside the control file, so " "the version is unknown"); snprintf(version, sizeof version, "0"); } else { const char *open = strchr(changelog, '('); const char *close = open ? strchr(open, ')') : NULL; if (!close) { fputs("holypkg: debian/changelog: the first record is malformed\n", stderr); result = 2; goto done; } { char *value = clean_line(open + 1, (size_t)(close - open - 1)); char *epoch; if (!value) { result = 1; goto done; } epoch = strchr(value, ':'); if (epoch) { review = 1; holy_note_add(¬e, "preserved", "the epoch %.*s of the changelog version", (int)(epoch - value), value); memmove(value, epoch + 1, strlen(epoch + 1) + 1); } snprintf(version, sizeof version, "%s", value); free(value); } /* a Debian version is upstream-revision, and a native package has no revision */ { char *dash = strrchr(version, '-'); if (dash) { size_t at; for (at = 1; dash[at]; ++at) if (!isdigit((unsigned char)dash[at])) break; if (!dash[at] && (size_t)(dash - version + 1) < sizeof release) { memcpy(release, dash + 1, strlen(dash + 1) + 1); *dash = 0; holy_note_add(¬e, "semantic-change", "the Debian revision %s becomes the " "Holy release", release); } else { snprintf(release, sizeof release, "1"); } } else { snprintf(release, sizeof release, "1"); } } holy_note_add(¬e, "carried", "changelog version %s", version); } /* the changelog is copied so the record travels with the recipe */ if (changelog) { char *path = join_path(directory, "changelog"); char *copy = join_path(output, "changelog"); char digest[65]; if (path && copy && !holy_copy_and_hash(path, copy, digest)) wrote = 0; free(path); free(copy); } if (strpbrk(name, "$`/% \t") || !*version || strlen(name) > 480 || strlen(version) > 480) { fputs("holypkg: debian/control: the identity must be literal and short\n", stderr); result = 2; goto done; } /* a binary that lists its own files becomes a split output */ for (index = 0; index < binary_count; ++index) { static const char *const suffixes[] = { "install", "docs", "manpages", "links", NULL }; size_t used; for (used = 0; suffixes[used]; ++used) { char file[512]; char *path; /* a debian file name is NAME.SUFFIX, and a long NAME would overrun */ snprintf(file, sizeof file, "%.*s.%s", (int)(strlen(binaries[index].name) < sizeof file - 32 ? strlen(binaries[index].name) : sizeof file - 32), binaries[index].name, suffixes[used]); path = join_path(directory, file); if (!path) continue; { struct stat st; if (!stat(path, &st) && S_ISREG(st.st_mode)) { binaries[index].has_list = 1; snprintf(binaries[index].list, sizeof binaries[index].list, "%s", file); binaries[index].list_line = 0; } } free(path); } } { for (index = 0; index < binary_count; ++index) if (!binaries[index].has_list) ++mains; if (mains > 1) { review = 1; holy_note_add(¬e, "unknown", "%zu binary packages name no file list, so one of " "them cannot be told apart", mains); } for (index = 0; index < binary_count; ++index) binaries[index].main = !binaries[index].has_list && mains == 1; } if (!binary_count) { /* a source package with no binary stanza still builds one output */ { size_t used = strlen(name); if (used >= sizeof binaries[0].name) used = sizeof binaries[0].name - 1; memcpy(binaries[0].name, name, used); binaries[0].name[used] = 0; } binaries[0].main = 1; binary_count = 1; } snprintf(recipe_path, sizeof recipe_path, "%s/%s.recipe", output, name); snprintf(report_path, sizeof report_path, "%s/conversion", output); out = fopen(recipe_path, "wb"); if (!out) { fprintf(stderr, "holypkg: recipe unavailable: %s\n", recipe_path); result = 1; goto done; } fputs("format holy-recipe-1\nname ", out); holy_token(out, name); fputs("\nversion ", out); holy_token(out, version); fputs("\nrelease ", out); holy_token(out, release); fputs("\narch ", out); holy_token(out, arch); fputs("\nlibc any\n", out); { /* the short description is the first line, the rest is the long one */ size_t at = 0; const char *main_summary = summary; for (at = 0; at < binary_count; ++at) if (binaries[at].main && *binaries[at].summary) main_summary = binaries[at].summary; if (*main_summary) { fputs("summary ", out); holy_token(out, main_summary); fputc('\n', out); } } if (*homepage) { fputs("homepage ", out); holy_token(out, homepage); fputc('\n', out); } fputs("x-source-family deb\nx-converter debsrc-1\n", out); for (index = 0; index < binary_count; ++index) if (binaries[index].has_list) { fputs("output ", out); holy_token(out, binaries[index].name); fputs(" runtime\n", out); } if (mains <= 1) { /* the binary that names no file list is the one the whole tree belongs to */ fputs("output ", out); holy_token(out, name); fputs(" runtime\n", out); } /* the requirement lists of the main package become dependency records */ for (index = 0; index < field_count; ++index) { static const struct { const char *key; const char *kind; const char *where; } lists[] = { { "Build-Depends", "build-depend", "Build-Depends" }, { "Build-Depends-Indep", "build-depend", "Build-Depends-Indep" }, { "Depends", "depend", "Depends" }, { "Pre-Depends", "depend", "Pre-Depends" }, { "Recommends", NULL, NULL }, { "Suggests", NULL, NULL }, { "Enhances", NULL, NULL }, { "Breaks", NULL, NULL }, { "Conflicts", NULL, NULL }, { "Replaces", NULL, NULL }, { "Provides", NULL, NULL }, { NULL, NULL, NULL } }; size_t list_index; char **items; size_t count = 0, item; for (list_index = 0; lists[list_index].key; ++list_index) if (!strcmp(fields[index].name, lists[list_index].key)) break; if (!lists[list_index].key) continue; /* only the main package and the source stanza reach the recipe */ if (fields[index].binary && fields[index].binary > (int)binary_count) continue; if (fields[index].binary) { int at; for (at = 0; at < (int)binary_count; ++at) if (at + 1 == fields[index].binary && !binaries[at].main) break; if (at < (int)binary_count) continue; } if (!lists[list_index].kind) { /* a capability, a conflict and a replacement are none of them a requirement, and each carries its own version, so only the name is recorded */ int provides = !strcmp(fields[index].name, "Provides"); int suggests = !strcmp(fields[index].name, "Recommends") || !strcmp(fields[index].name, "Suggests") || !strcmp(fields[index].name, "Enhances"); char **entries; size_t entries_used = 0, entry; entries = relationship_groups(fields[index].value, &entries_used); for (entry = 0; entries && entry < entries_used; ++entry) { char requirement[512], relation[16], wanted[512]; int epoch = 0; size_t at = 0; if (!split_requirement(entries[entry], requirement, sizeof requirement, relation, sizeof relation, wanted, sizeof wanted, &epoch, &at)) { review = 1; holy_note_add(¬e, "unknown", "%s %s carries an entry this converter " "cannot name", fields[index].name, entries[entry]); continue; } fputs(provides ? "x-provides " : suggests ? "x-suggests " : "x-conflicts ", out); holy_token(out, requirement); fputc('\n', out); if (strcmp(relation, "any")) holy_note_add(¬e, "preserved", "%s %s %s", fields[index].name, relation, wanted); } list_free(entries); continue; } items = relationship_groups(fields[index].value, &count); for (item = 0; items && item < count; ++item) { char *pipe = strchr(items[item], '|'); if (pipe) { size_t used = (size_t)(pipe - items[item]); review = 1; while (used && items[item][used - 1] == ' ') --used; items[item][used] = 0; holy_note_add(¬e, "unknown", "%s offers alternatives for %s, only the first " "is carried", lists[list_index].where, items[item]); } emit_dependency(out, items[item], lists[list_index].kind, ¬e, &review, lists[list_index].where); } list_free(items); } /* the rules file is the build, and dh is a macro framework, not a script */ { char *path = join_path(directory, "rules"); if (path) { rules = read_file(path, NULL); free(path); } } { /* the lintian and helper files beside control are carried as records */ static const char *const carried[] = { "source/format", "copyright", "watch", NULL }; size_t used; for (used = 0; carried[used]; ++used) { char file[512]; struct stat st; snprintf(file, sizeof file, "%s", carried[used]); { char *path = join_path(directory, file); if (path && !stat(path, &st) && S_ISREG(st.st_mode)) { char digest[65]; char *copy = join_path(output, file); if (copy && !holy_copy_and_hash(path, copy, digest)) wrote = 0; holy_note_add(¬e, "preserved", "%s", file); free(copy); } free(path); } } } for (index = 0; index < binary_count; ++index) { struct deb_binary *package = &binaries[index]; static const char *const hooks[] = { "postinst", "preinst", "prerm", "postrm", NULL }; size_t used; for (used = 0; hooks[used]; ++used) { char file[512]; char *path; struct stat st; snprintf(file, sizeof file, "%.*s.%s", (int)(strlen(package->name) < sizeof file - 32 ? strlen(package->name) : sizeof file - 32), package->name, hooks[used]); path = join_path(directory, file); if (path && !stat(path, &st) && S_ISREG(st.st_mode)) { char digest[65]; char *copy = join_path(output, file); if (!copy || !holy_copy_and_hash(path, copy, digest)) { wrote = 0; } free(copy); holy_note_add(¬e, "preserved", "maintainer script %s", file); free(path); continue; } free(path); } if (!package->has_list) continue; } if (!wrote) { result = 1; goto done; } /* the file list of a binary names the paths its own package carries */ for (index = 0; index < binary_count; ++index) { struct deb_binary *package = &binaries[index]; char *path; char *list; char **items; size_t count = 0, item; if (!package->has_list) continue; path = join_path(directory, package->list); list = path ? read_file(path, NULL) : NULL; if (!list) { review = 1; holy_note_add(¬e, "unknown", "the file list %s of %s could not be read", package->list, package->name); free(path); continue; } /* the destinations the list names, so a list with none is reported rather than written as a split step that would fill no tree */ struct text paths = {NULL, 0, 0}; items = list_lines(list, &count); if (!text_add(&paths, "", 0)) { wrote = 0; } for (item = 0; items && item < count && wrote; ++item) { /* a line is SOURCE DESTINATION, or a source with no destination */ char *cursor = items[item]; char *space; char *destination; while (*cursor == ' ') ++cursor; if (*cursor == '.') continue; space = strchr(cursor, ' '); if (space) { *space = 0; destination = space + 1; while (*destination == ' ') ++destination; } else { destination = cursor; } { /* a path ending in a slash names the directory itself */ size_t used = strlen(destination); if (used && destination[used - 1] == '/') destination[used - 1] = 0; } if (!*destination) continue; if (!text_puts(&paths, destination) || !text_puts(&paths, " ")) wrote = 0; } if (!paths.data || !*paths.data) { review = 1; holy_note_add(¬e, "unknown", "the file list %s of %s names no path this " "converter can carry", package->list, package->name); free(paths.data); list_free(items); free(list); free(path); continue; } fprintf(out, "split-step %s split /bin/sh <name); fputs("# the debian file list names the paths this binary package carries\n", out); fputs("set -f\n", out); fputs("for entry in", out); { char *cursor = paths.data; while (*cursor) { char *stop = strchr(cursor, ' '); size_t used = stop ? (size_t)(stop - cursor) : strlen(cursor); char line[1024]; if (used >= sizeof line) used = sizeof line - 1; memcpy(line, cursor, used); line[used] = 0; if (*line) { fputc(' ', out); holy_token(out, line); } cursor += used + (stop ? 1 : 0); } } fputs("; do\n", out); fputs(" [ -e \"$HOLY_DEST/$entry\" ] || continue\n", out); fputs(" mkdir -p \"$HOLY_SPLIT_DEST/$(dirname \"$entry\")\"\n", out); fputs(" cp -a \"$HOLY_DEST/$entry\" \"$HOLY_SPLIT_DEST/$entry\"\n", out); fputs("done\n", out); free(paths.data); list_free(items); free(list); free(path); fputs("SPLIT\n", out); holy_note_add(¬e, "preserved", "subpackage %s from its file list", package->name); holy_note_add(¬e, "semantic-change", "the file list of %s becomes a copy out of " "the main tree, so a debian path cannot grow a file the build did not " "place", package->name); } /* the rules body keeps its own shell behind a prologue */ if (rules) { char *copy = join_path(output, "rules"); { char *from = join_path(directory, "rules"); char digest[65]; if (!from || !copy || !holy_copy_and_hash(from, copy, digest)) wrote = 0; free(from); } free(copy); } { static const struct { const char *token; const char *text; } helpers[] = { { "dh ", "debhelper calls the recipe make, which this converter cannot run" }, { "dh_auto", "debhelper autodetects the build system, which this converter cannot" }, { "dh_install", "debhelper installs the file lists, which this converter cannot" }, { "dpkg-buildpackage", "the dpkg build driver is not carried" }, { "debian/rules", "the rules file is included, which a Holy step cannot" }, { "override_dh_", "a debhelper override, which debhelper runs and this cannot" } }; size_t used; for (used = 0; used < sizeof helpers / sizeof *helpers; ++used) if (rules && strstr(rules, helpers[used].token)) { review = 1; holy_note_add(¬e, "unknown", "%s", helpers[used].text); } } if (rules && *rules) { fputs("step build /bin/sh < 1 && result != 3) fprintf(stderr, "holypkg: Debian conversion failed (status %d)\n", result); free(control); free(changelog); free(rules); free(directory); holy_note_free(¬e); return result; }