/* * build.c - package build orchestration * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ #include "mkpkg.h" #include "archive.h" #include #include #include #include #include #include #include #include #include #include #include #include static void error(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fputs("mkpkg: error: ", stderr); vfprintf(stderr, fmt, ap); fputc('\n', stderr); va_end(ap); } /* ── unpack_sources ──────────────────────────────────────────────────── */ int unpack_sources(pkgfile_t *pkgfile, config_t *config, const char *srcdir) { int i; for (i = 0; i < pkgfile->source_count; i++) { source_t *src = &pkgfile->sources[i]; const char *url = src->url; char local[PATH_MAX_LEN]; char dest[PATH_MAX_LEN]; if (src->rename && strcmp(src->rename, "SKIP") == 0) continue; if (src->rename && src->rename[0]) { snprintf(local, sizeof(local), "%s/%s", config->source_dir, src->rename); } else if (strchr(url, '/')) { const char *base = strrchr(url, '/'); snprintf(local, sizeof(local), "%s/%s", config->source_dir, base ? base + 1 : url); } else { snprintf(local, sizeof(local), "%s/%s", config->root_dir, url); } if (arc_is_archive(local)) { if (arc_extract_src(local, srcdir) != 0) { error("failed to extract: %s", local); return E_UNPACK; } } else { /* copy non-archive file into srcdir */ const char *base = strrchr(local, '/'); snprintf(dest, sizeof(dest), "%s/%s", srcdir, base ? base + 1 : local); pid_t pid = fork(); if (pid == 0) { execlp("cp", "cp", local, dest, (char *)NULL); _exit(1); } if (pid < 0) { warn("fork"); return E_UNPACK; } int st; waitpid(pid, &st, 0); if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) { error("failed to copy: %s", local); return E_UNPACK; } } } return 0; } /* ── strip_files ─────────────────────────────────────────────────────── */ int strip_files(const char *pkgdir, const char *nostrip_file) { char cmd[CMD_MAX_LEN]; char filter[PATH_MAX_LEN]; pid_t pid; int status; info("stripping binaries"); if (nostrip_file && access(nostrip_file, R_OK) == 0) snprintf(filter, sizeof(filter), "grep -v -f '%s'", nostrip_file); else snprintf(filter, sizeof(filter), "cat"); snprintf(cmd, sizeof(cmd), "cd '%s' && " "find . -type f -printf '%%P\\n' | %s | " "xargs -r -L10 -P$(nproc) file --no-buffer --separator '>' " "-e apptype -e ascii -e encoding -e tokens -e cdf -e compress -e tar -e text | " "awk 'BEGIN{FS=\">[ ]+\"}" "$0~/ELF.*executable.*not stripped/{print \"--strip-all \\\"\"$1\"\\\"\"}" "$0~/ELF.*shared object.*not stripped/{print \"--strip-unneeded \\\"\"$1\"\\\"\"}" "$2==\"current ar archive\"{print \"--strip-debug \\\"\"$1\"\\\"\"}" "' | xargs -r -L1 -P$(nproc) strip 2>/dev/null", pkgdir, filter); pid = fork(); if (pid == 0) { execlp("sh", "sh", "-c", cmd, (char *)NULL); _exit(1); } if (pid > 0) waitpid(pid, &status, 0); return 0; /* strip failures are non-fatal */ } /* ── compress_manpages ───────────────────────────────────────────────── */ /* * gzip a batch of files with a single gzip invocation. * Returns the child pid (caller reaps), or -1 on fork failure. */ static pid_t gzip_batch(char **files, size_t n) { pid_t pid = fork(); if (pid == 0) { char **argv = malloc((n + 4) * sizeof(char *)); size_t i; if (!argv) _exit(1); argv[0] = "gzip"; argv[1] = "-6"; argv[2] = "-f"; for (i = 0; i < n; i++) argv[i + 3] = files[i]; argv[n + 3] = NULL; int devnull = open("/dev/null", O_WRONLY); if (devnull >= 0) { dup2(devnull, STDERR_FILENO); close(devnull); } execvp("gzip", argv); _exit(1); } return pid; } /* * Compress all collected files using up to nproc parallel gzip * processes, each handed an even chunk of the file list. One * fork+exec per chunk instead of one per file. */ static void gzip_parallel(strarray_t *files) { long njobs; size_t chunk, off; int spawned = 0; if (files->count == 0) return; njobs = sysconf(_SC_NPROCESSORS_ONLN); if (njobs < 1) njobs = 1; if ((size_t)njobs > files->count) njobs = (long)files->count; chunk = (files->count + (size_t)njobs - 1) / (size_t)njobs; for (off = 0; off < files->count; off += chunk) { size_t n = files->count - off; if (n > chunk) n = chunk; if (gzip_batch(files->items + off, n) > 0) spawned++; } while (spawned-- > 0) { int st; wait(&st); } } /* fix a single symlink: if link and target don't end in .gz, rename both */ static void fix_man_symlink(const char *dir, const char *name) { char linkpath[PATH_MAX_LEN]; char target[PATH_MAX_LEN]; char newlink[PATH_MAX_LEN]; char newtarget[PATH_MAX_LEN]; char newtarget_full[PATH_MAX_LEN]; ssize_t n; snprintf(linkpath, sizeof(linkpath), "%s/%s", dir, name); n = readlink(linkpath, target, sizeof(target) - 1); if (n < 0) return; target[n] = '\0'; /* skip if already .gz */ if (str_ends_with(name, ".gz") || str_ends_with(target, ".gz")) return; /* new names */ snprintf(newlink, sizeof(newlink), "%s/%s.gz", dir, name); /* target may include a directory component — only fix the basename */ const char *tbase = strrchr(target, '/'); if (tbase) { char tdir[PATH_MAX_LEN]; snprintf(tdir, sizeof(tdir), "%.*s", (int)(tbase - target), target); snprintf(newtarget, sizeof(newtarget), "%s/%s.gz", tdir, tbase + 1); snprintf(newtarget_full, sizeof(newtarget_full), "%s/%s/%s.gz", dir, tdir, tbase + 1); } else { snprintf(newtarget, sizeof(newtarget), "%s.gz", target); snprintf(newtarget_full, sizeof(newtarget_full), "%s/%s.gz", dir, target); } /* only create the new symlink if the .gz target exists */ if (access(newtarget_full, F_OK) != 0) return; unlink(linkpath); /* symlink may fail if a concurrent process recreated linkpath * between unlink and symlink — accepted as best-effort. */ (void)!symlink(newtarget, newlink); } /* walk one man section directory, collecting uncompressed regular files */ static void collect_man_files(const char *dirpath, strarray_t *out) { DIR *d; struct dirent *ent; struct stat st; char fullpath[PATH_MAX_LEN]; d = opendir(dirpath); if (!d) return; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; snprintf(fullpath, sizeof(fullpath), "%s/%s", dirpath, ent->d_name); if (lstat(fullpath, &st) != 0) continue; if (S_ISREG(st.st_mode) && !str_ends_with(ent->d_name, ".gz")) strarray_add(out, fullpath); } closedir(d); } /* walk one man section directory, fixing dangling symlinks after gzip */ static void fix_man_symlinks_dir(const char *dirpath) { DIR *d; struct dirent *ent; struct stat st; char fullpath[PATH_MAX_LEN]; d = opendir(dirpath); if (!d) return; while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; snprintf(fullpath, sizeof(fullpath), "%s/%s", dirpath, ent->d_name); if (lstat(fullpath, &st) != 0) continue; if (S_ISLNK(st.st_mode)) fix_man_symlink(dirpath, ent->d_name); } closedir(d); } int compress_manpages(const char *pkgdir) { char manbase[PATH_MAX_LEN]; DIR *d; struct dirent *ent; struct stat st; char secpath[PATH_MAX_LEN]; strarray_t *files; info("compressing man pages"); snprintf(manbase, sizeof(manbase), "%s/usr/share/man", pkgdir); d = opendir(manbase); if (!d) return 0; /* no man pages — fine */ /* pass 1: collect every uncompressed file across all sections */ files = strarray_new(); while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; if (strncmp(ent->d_name, "man", 3) != 0) continue; snprintf(secpath, sizeof(secpath), "%s/%s", manbase, ent->d_name); if (lstat(secpath, &st) == 0 && S_ISDIR(st.st_mode)) collect_man_files(secpath, files); } /* pass 2: compress everything with parallel batched gzip */ gzip_parallel(files); strarray_free(files); /* pass 3: fix symlinks now that all .gz targets exist */ rewinddir(d); while ((ent = readdir(d)) != NULL) { if (ent->d_name[0] == '.') continue; if (strncmp(ent->d_name, "man", 3) != 0) continue; snprintf(secpath, sizeof(secpath), "%s/%s", manbase, ent->d_name); if (lstat(secpath, &st) == 0 && S_ISDIR(st.st_mode)) fix_man_symlinks_dir(secpath); } closedir(d); return 0; } /* ── remove_la_files ─────────────────────────────────────────────────── */ static int rm_la_cb(const char *path, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { (void)sb; (void)ftwbuf; if (typeflag == FTW_F && str_ends_with(path, ".la")) unlink(path); return 0; } int remove_la_files(const char *pkgdir) { nftw(pkgdir, rm_la_cb, 8, FTW_PHYS); return 0; } /* ── write_and_run_script ────────────────────────────────────────────── */ /* * Write a build script to script_path and execute it in run_dir. * When source_conf=1, sources /etc/mkpkg.conf after exporting variables * so $SRC/$PKG are available when mkpkg.conf is read. */ /* * Write `value` to fp inside POSIX shell single-quotes, escaping * any embedded apostrophes via the standard '\'' idiom: * foo'bar → 'foo'\''bar' * So `export name='foo'\''bar'` is parsed by /bin/sh as foo'bar. * * NULL and empty strings produce '' (an empty string literal). */ static void shell_quote(FILE *fp, const char *value) { const char *p; fputc('\'', fp); if (value) { for (p = value; *p; p++) { if (*p == '\'') fputs("'\\''", fp); else fputc(*p, fp); } } fputc('\'', fp); } static int write_and_run_script(const char *script_path, const char *run_dir, pkgfile_t *pkgfile, config_t *config, char **cmds, int cmd_count, int source_conf, int with_lbexec) { FILE *fp; char pkg_dir[PATH_MAX_LEN]; char src_dir[PATH_MAX_LEN]; int i; snprintf(pkg_dir, sizeof(pkg_dir), "%s/pkg", config->work_dir); snprintf(src_dir, sizeof(src_dir), "%s/src", config->work_dir); fp = fopen(script_path, "w"); if (!fp) { error("cannot create script: %s", script_path); return E_BUILD; } fprintf(fp, "#!/bin/sh\nset -e\n"); if (getenv("MKPKG_DEBUG")) fprintf(fp, "set -x\n"); fprintf(fp, "\n"); /* export variables first so mkpkg.conf can reference $SRC/$PKG. * All interpolated values go through shell_quote — without it, * a single quote anywhere in a name/version/custom var would * either break the script or, with malicious intent, inject. */ fputs("export PKG=", fp); shell_quote(fp, pkg_dir); fputc('\n', fp); fputs("export SRC=", fp); shell_quote(fp, src_dir); fputc('\n', fp); fputs("export PKGMK_SOURCE_DIR=", fp); shell_quote(fp, config->source_dir); fputc('\n', fp); fputs("export name=", fp); shell_quote(fp, pkgfile->name); fputc('\n', fp); fputs("export version=", fp); shell_quote(fp, pkgfile->version); fputc('\n', fp); fputs("export release=", fp); shell_quote(fp, pkgfile->release); fputc('\n', fp); for (i = 0; i < pkgfile->custom_var_count; i++) { fprintf(fp, "export %s=", pkgfile->custom_vars[i].name); shell_quote(fp, pkgfile->custom_vars[i].value); fputc('\n', fp); } /* the configuration mkpkg itself read: /etc/mkpkg.conf, or -g */ if (source_conf) { fputs("[ -f ", fp); shell_quote(fp, config->config_file); fputs(" ] && . ", fp); shell_quote(fp, config->config_file); fputc('\n', fp); } /* export dep lists for lbexec when lockbox commands are declared */ if (with_lbexec && pkgfile->lockbox_cmd_count > 0) { int has_cargo = 0, k; fputs("export MKPKG_WORK_DIR=", fp); shell_quote(fp, config->work_dir); fputc('\n', fp); /* dep names are constrained (alnum/-/_) by convention so a * simple space-separated concat with surrounding quotes is * safe. shell_quote()ing each token would only matter if a * package name ever contained an apostrophe, which the rest * of mkpkg already would not survive. */ fprintf(fp, "export MKPKG_DEPENDS='"); for (i = 0; i < pkgfile->depend_count; i++) fprintf(fp, "%s%s", i ? " " : "", pkgfile->depends[i]); fprintf(fp, "'\n"); fprintf(fp, "export MKPKG_MAKEDEPENDS='"); for (i = 0; i < pkgfile->makedepend_count; i++) fprintf(fp, "%s%s", i ? " " : "", pkgfile->makedepends[i]); fprintf(fp, "'\n"); for (k = 0; k < pkgfile->lockbox_cmd_count; k++) if (pkgfile->lockbox_cmd_profiles[k] == LOCKBOX_PROFILE_CARGO) has_cargo = 1; if (has_cargo) { /* * Cargo writes temp .rmeta files then renames them to target/. * If TMPDIR and target/ are on different filesystem mounts, * rename() fails with EXDEV (cross-device link, errno 18). * Force TMPDIR inside $SRC so both paths share the same device. */ fprintf(fp, "mkdir -p \"$SRC/tmp\"\n"); fprintf(fp, "export TMPDIR=\"$SRC/tmp\"\n"); } } /* shell hooks run in port dir; build hooks run in $SRC */ if (run_dir == config->source_dir) fprintf(fp, "\ncd \"$PKGMK_SOURCE_DIR\"\n\n"); else fprintf(fp, "\ncd \"$SRC\"\n\n"); for (i = 0; i < cmd_count; i++) { const char *line = cmds[i]; /* prefix lbexec commands when lockbox=(cmd...) is set */ if (with_lbexec && pkgfile->lockbox_cmd_count > 0) { const char *p = line; while (*p == ' ' || *p == '\t') p++; char first[64]; int fi = 0; while (*p && *p != ' ' && *p != '\t' && fi < 63) first[fi++] = *p++; first[fi] = '\0'; int j, wrap = 0; for (j = 0; j < pkgfile->lockbox_cmd_count; j++) { if (fi > 0 && strcmp(first, pkgfile->lockbox_cmds[j]) == 0) { wrap = 1; break; } } if (wrap) { const char *cmd = line; const char *prof; while (*cmd == ' ' || *cmd == '\t') cmd++; prof = (pkgfile->lockbox_cmd_profiles[j] == LOCKBOX_PROFILE_CARGO) ? " -p cargo" : ""; fprintf(fp, "lbexec%s %s\n", prof, cmd); continue; } } fprintf(fp, "%s\n", line); } fclose(fp); chmod(script_path, 0755); /* execute the script */ pid_t pid = fork(); if (pid == 0) { if (chdir(run_dir) != 0) _exit(1); execlp("sh", "sh", script_path, (char *)NULL); _exit(1); } if (pid < 0) { error("fork failed"); return E_BUILD; } int status; waitpid(pid, &status, 0); return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 0 : E_BUILD; } /* ── run_shell_hook ──────────────────────────────────────────────────── */ int run_shell_hook(pkgfile_t *pkgfile, config_t *config, const char *label, char **cmds, int count) { char script[PATH_MAX_LEN]; int ret; info("running shell() [%s]", label); snprintf(script, sizeof(script), "%s/.shell_%s.sh", config->source_dir, label); ret = write_and_run_script(script, config->source_dir, pkgfile, config, cmds, count, 1, 0); unlink(script); if (ret != 0) error("shell() [%s] failed", label); return ret; } /* ── meta file creation ──────────────────────────────────────────────── */ static void write_meta(pkgfile_t *pkgfile, const char *pkgdir) { char meta_dir[PATH_MAX_LEN]; char meta_path[PATH_MAX_LEN]; FILE *mf; int i; if (pkgfile->group_count == 0 && pkgfile->service_count == 0 && pkgfile->permission_count == 0 && pkgfile->capability_count == 0) return; snprintf(meta_dir, sizeof(meta_dir), "%s/var/lib/pkg/meta", pkgdir); if (mkdirp(meta_dir, 0755) < 0) return; snprintf(meta_path, sizeof(meta_path), "%s/%s", meta_dir, pkgfile->name); mf = fopen(meta_path, "w"); if (!mf) { warning("could not create .meta file"); return; } for (i = 0; i < pkgfile->group_count; i++) fprintf(mf, "group %s\n", pkgfile->groups[i]); for (i = 0; i < pkgfile->service_count; i++) fprintf(mf, "service %s\n", pkgfile->services[i]); for (i = 0; i < pkgfile->permission_count; i++) fprintf(mf, "permission %s\n", pkgfile->permissions[i]); for (i = 0; i < pkgfile->capability_count; i++) fprintf(mf, "capability %s\n", pkgfile->capabilities[i]); fclose(mf); info("package metadata: groups=%d services=%d permissions=%d capabilities=%d", pkgfile->group_count, pkgfile->service_count, pkgfile->permission_count, pkgfile->capability_count); } /* ── print_footprint ─────────────────────────────────────────────────── */ static void print_file(const char *path) { FILE *fp; char buf[4096]; size_t n; fp = fopen(path, "r"); if (!fp) return; while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) fwrite(buf, 1, n, stdout); fclose(fp); } /* ── build_package ───────────────────────────────────────────────────── */ /* arc_sink_fn adapter: hash compressed package bytes as written */ static void pkg_hash_sink(const void *buf, size_t len, void *ctx) { sha256_stream_update(ctx, buf, len); } int build_package(pkgfile_t *pkgfile, config_t *config, const char *target) { char pkg_dir[PATH_MAX_LEN]; char src_dir[PATH_MAX_LEN]; char script[PATH_MAX_LEN]; char tmp_fp[PATH_MAX_LEN]; int ret; snprintf(pkg_dir, sizeof(pkg_dir), "%s/pkg", config->work_dir); snprintf(src_dir, sizeof(src_dir), "%s/src", config->work_dir); mkdir(pkg_dir, 0755); if (getuid() != 0) warning("packages should be built as root."); info("building %s", target); if (config->resume) { info("resuming: skipping extract and patch"); } else { /* extract() hook or auto-unpack */ if (pkgfile->has_extract) { info("running extract()"); snprintf(script, sizeof(script), "%s/extract.sh", src_dir); ret = write_and_run_script(script, config->source_dir, pkgfile, config, pkgfile->extract_cmds, pkgfile->extract_count, 0, 0); if (ret != 0) { error("extract function failed"); return E_UNPACK; } } else { if (unpack_sources(pkgfile, config, src_dir) != 0) return E_UNPACK; } /* patch() hook */ if (pkgfile->has_patch) { info("running patch()"); snprintf(script, sizeof(script), "%s/patch.sh", src_dir); ret = write_and_run_script(script, src_dir, pkgfile, config, pkgfile->patch_cmds, pkgfile->patch_count, 0, 0); if (ret != 0) { error("patch function failed"); return E_BUILD; } } } /* build() — always required */ info("running build()"); snprintf(script, sizeof(script), "%s/build.sh", src_dir); ret = write_and_run_script(script, src_dir, pkgfile, config, pkgfile->build_cmds, pkgfile->build_count, 1, !config->no_lockbox); if (ret != 0) { error("build function failed"); return E_BUILD; } /* post_build() hook */ if (pkgfile->has_post_build) { info("running post_build()"); snprintf(script, sizeof(script), "%s/post_build.sh", src_dir); ret = write_and_run_script(script, src_dir, pkgfile, config, pkgfile->post_build_cmds, pkgfile->post_build_count, 1, 0); if (ret != 0) { error("post-build function failed"); return E_BUILD; } } /* post-build processing */ if (!config->no_strip) { char nostrip[PATH_MAX_LEN]; snprintf(nostrip, sizeof(nostrip), "%s/%s", config->root_dir, config->nostrip_file); strip_files(pkg_dir, nostrip); } compress_manpages(pkg_dir); remove_la_files(pkg_dir); /* embed meta file into package tree */ write_meta(pkgfile, pkg_dir); /* fast footprint from pkg directory */ snprintf(tmp_fp, sizeof(tmp_fp), "%s/.footprint.tmp", config->work_dir); if (make_footprint_fast(pkg_dir, tmp_fp) != 0) warning("could not generate footprint from directory"); /* create the package archive, hashing it as it is written so a * later -us doesn't have to re-read a multi-GB tarball */ { void *hctx = sha256_stream_new(); if (arc_create_pkg(target, pkg_dir, config->compression, hctx ? pkg_hash_sink : NULL, hctx) != 0) { char dummy[65]; if (hctx) sha256_stream_final(hctx, dummy, sizeof(dummy)); error("failed to create package"); return E_BUILD; } if (hctx) { char sha[65]; if (sha256_stream_final(hctx, sha, sizeof(sha)) == 0) sha256_cache_store(target, sha); } } /* print package contents */ print_file(tmp_fp); info("build complete: %s", target); return 0; } /* ── install_package ─────────────────────────────────────────────────── */ int install_package(const char *target, int upgrade, const char *rootfs) { pid_t pid; int status; if (rootfs && rootfs[0]) { info("installing %s to rootfs %s", target, rootfs); pid = fork(); if (pid == 0) { if (upgrade) execlp("addpkg", "addpkg", "-u", "-r", rootfs, target, (char *)NULL); else execlp("addpkg", "addpkg", "-r", rootfs, target, (char *)NULL); _exit(1); } } else { info("installing %s", target); pid = fork(); if (pid == 0) { if (upgrade) execlp("addpkg", "addpkg", "-u", target, (char *)NULL); else execlp("addpkg", "addpkg", target, (char *)NULL); _exit(1); } } if (pid < 0) { warn("fork"); return E_INSTALL; } waitpid(pid, &status, 0); if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { info("installed %s", target); return 0; } error("installing '%s' failed.", target); return E_INSTALL; }