/* * mkpkg.c - package build tool for rawnix.org * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ #include "mkpkg.h" #include "archive.h" #include "db.h" #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); } static config_t *g_config = NULL; static void on_interrupt(int sig) { (void)sig; if (g_config && !g_config->keep_work) remove_work_dir(g_config); _exit(1); } static void init_config(config_t *c, const char *cwd) { /* preserve flags set before init */ int keep_work = c->keep_work; int resume = c->resume; int clean = c->clean; int clean_download = c->clean_download; int clean_footprint = c->clean_footprint; int clean_signature = c->clean_signature; int clean_work = c->clean_work; int download = c->download; int download_only = c->download_only; int extract_only = c->extract_only; int print_footprint = c->print_footprint; int print_checksums = c->print_checksums; int update_footprint = c->update_footprint; int update_checksums = c->update_checksums; int update_signature = c->update_signature; int update_pubkey = c->update_pubkey; int check_signature = c->check_signature; int show_pubkey = c->show_pubkey; int ignore_footprint = c->ignore_footprint; int ignore_checksums = c->ignore_checksums; int ignore_signature = c->ignore_signature; int lockbox = c->lockbox; int no_lockbox = c->no_lockbox; int install = c->install; char rootfs[PATH_MAX_LEN]; char compression[8]; snprintf(rootfs, sizeof(rootfs), "%s", c->rootfs); snprintf(compression, sizeof(compression), "%s", c->compression); memset(c, 0, sizeof(config_t)); snprintf(c->root_dir, sizeof(c->root_dir), "%s", cwd); snprintf(c->source_dir, sizeof(c->source_dir), "%s", cwd); snprintf(c->package_dir, sizeof(c->package_dir), "%s", cwd); snprintf(c->work_dir, sizeof(c->work_dir), "%s/work", cwd); snprintf(c->tmp_dir, sizeof(c->tmp_dir), "/dev/shm"); /* * Recipe selection. * * A locked port keeps the operator's edited recipe in * MAKEPKG.lock. `ports -u` overwrites MAKEPKG with upstream's * but carries --exclude=*.lock, so the lock file survives the * sync and is the recipe to build from whenever it exists. * * Every consumer of the recipe goes through config->pkgfile -- * parse_pkgfile(), the sha256sums writer in checksum.c and the * signify() writer in signature.c -- so this one assignment * covers building, `mkpkg -u` and signing alike. That is * deliberate: a locked port's checksums and signature belong to * the locked recipe, not to upstream's. * * Announced on stderr, never silent. The failure mode this * trades against is editing MAKEPKG, rebuilding, and finding * nothing changed with no clue why. * * The lock pins the whole build description, not just the recipe: * .footprint.lock and .signature.lock go with MAKEPKG.lock. * `ports -u` overwrites .footprint and .signature with upstream's * (its --exclude=*.lock spares all three .lock files), so a locked * build checked against them failed its footprint, and on a machine * with a secret key the automatic post-build signing overwrote the * port's real .signature with one for the lock. Every reader and * writer goes through footprint_file / signature_file. */ { char lockpath[PATH_MAX_LEN]; int locked; snprintf(lockpath, sizeof(lockpath), "%s/MAKEPKG.lock", cwd); locked = access(lockpath, F_OK) == 0; snprintf(c->pkgfile, sizeof(c->pkgfile), "%s", locked ? "MAKEPKG.lock" : "MAKEPKG"); snprintf(c->footprint_file, sizeof(c->footprint_file), "%s", locked ? ".footprint.lock" : ".footprint"); snprintf(c->signature_file, sizeof(c->signature_file), "%s", locked ? ".signature.lock" : ".signature"); if (locked) fprintf(stderr, "mkpkg: port is locked, building MAKEPKG.lock\n"); } snprintf(c->config_file, sizeof(c->config_file), "/etc/mkpkg.conf"); snprintf(c->nostrip_file, sizeof(c->nostrip_file), ".nostrip"); /* restore flags */ c->keep_work = keep_work; c->resume = resume; c->clean = clean; c->clean_download = clean_download; c->clean_footprint = clean_footprint; c->clean_signature = clean_signature; c->clean_work = clean_work; c->download = download; c->download_only = download_only; c->extract_only = extract_only; c->print_footprint = print_footprint; c->print_checksums = print_checksums; c->update_footprint = update_footprint; c->update_checksums = update_checksums; c->update_signature = update_signature; c->update_pubkey = update_pubkey; c->check_signature = check_signature; c->show_pubkey = show_pubkey; c->ignore_footprint = ignore_footprint; c->ignore_checksums = ignore_checksums; c->ignore_signature = ignore_signature; c->lockbox = lockbox; c->no_lockbox = no_lockbox; c->install = install; snprintf(c->rootfs, sizeof(c->rootfs), "%s", rootfs); snprintf(c->compression, sizeof(c->compression), "%s", compression); } int check_directory(const char *path, int write_required) { struct stat st; if (stat(path, &st) != 0) { error("directory '%s' does not exist.", path); return E_DIR_PERM; } if (!S_ISDIR(st.st_mode)) { error("'%s' is not a directory.", path); return E_DIR_PERM; } if (write_required && access(path, W_OK) != 0) { error("directory '%s' not writable.", path); return E_DIR_PERM; } if (access(path, R_OK | X_OK) != 0) { error("directory '%s' not readable.", path); return E_DIR_PERM; } return 0; } void make_work_dir(config_t *c) { char pkg_dir[PATH_MAX_LEN]; char src_dir[PATH_MAX_LEN]; if (c->work_dir[0] == '/' && !str_ends_with(c->work_dir, "/work")) { char actual[PATH_MAX_LEN]; snprintf(actual, sizeof(actual), "%s/mkpkg_%d", c->work_dir, getpid()); info("using tmpfs build directory: %s", actual); snprintf(c->work_dir, sizeof(c->work_dir), "%s", actual); } snprintf(pkg_dir, sizeof(pkg_dir), "%s/pkg", c->work_dir); snprintf(src_dir, sizeof(src_dir), "%s/src", c->work_dir); remove_work_dir(c); if (mkdirp(c->work_dir, 0755) < 0) { error("cannot create work directory: %s", c->work_dir); exit(E_DIR_PERM); } if (mkdir(pkg_dir, 0755) != 0) { error("cannot create pkg directory"); exit(E_DIR_PERM); } if (mkdir(src_dir, 0755) != 0) { error("cannot create src directory"); exit(E_DIR_PERM); } } int remove_work_dir(config_t *c) { if (c->work_dir[0]) return rmrf(c->work_dir); return 0; } int build_needed(pkgfile_t *pkgfile, config_t *config, const char *target) { struct stat pkg_st, src_st; int i; if (access(target, F_OK) != 0) return 1; if (stat(target, &pkg_st) != 0) return 1; for (i = 0; i < pkgfile->source_count; i++) { char local[PATH_MAX_LEN]; const char *url = pkgfile->sources[i].url; if (pkgfile->sources[i].rename && strcmp(pkgfile->sources[i].rename, "SKIP") == 0) continue; 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 (stat(local, &src_st) == 0 && src_st.st_mtime > pkg_st.st_mtime) return 1; } return 0; } /* * Is the full lockbox wanted for this build? -b or lockbox=(yes) turn * it on; -ib turns it off and wins over both. */ static int use_lockbox(const pkgfile_t *pkgfile, const config_t *config) { if (config->no_lockbox) return 0; return config->lockbox || pkgfile->has_lockbox; } static void run_post_shell(pkgfile_t *pkgfile, config_t *config) { if (pkgfile->has_post_shell) run_shell_hook(pkgfile, config, "post", pkgfile->post_shell_cmds, pkgfile->post_shell_count); } /* * shell() [pre], then the build itself, full lockbox or not. * * A failing shell() [pre] stops here: it sets up what build() relies * on (wrappers, links), and building without it only fails later with * a less useful error -- or, worse, succeeds with the wrong tools. */ static int run_build(pkgfile_t *pkgfile, config_t *config, const char *target) { int ret; if (pkgfile->has_pre_shell) { ret = run_shell_hook(pkgfile, config, "pre", pkgfile->pre_shell_cmds, pkgfile->pre_shell_count); if (ret != 0) { /* undo whatever part of the setup did happen */ run_post_shell(pkgfile, config); return ret; /* run_shell_hook() said why */ } } if (config->resume) info("resuming build"); else make_work_dir(config); if (use_lockbox(pkgfile, config)) ret = lockbox_enter(pkgfile, config, target); else ret = build_package(pkgfile, config, target); if (!config->keep_work) remove_work_dir(config); /* * shell() [post] is the cleanup for shell() [pre]; a failed build * must not leave the pre hook's wrappers behind. On success the * caller runs it later, after the footprint check and signing. */ if (ret != 0) run_post_shell(pkgfile, config); return ret; } /* * Make sure the sources are there and correct: when a build is needed * (or -d asks for it) fetch what is missing -- an error without -d -- * then verify sha256sums unless -ic. */ static int prepare_sources(pkgfile_t *pkgfile, config_t *config, const char *target) { int ret, i; if (config->download || build_needed(pkgfile, config, target)) { if (!config->download) { int missing = 0; for (i = 0; i < pkgfile->source_count; i++) { source_t *src = &pkgfile->sources[i]; const char *url = src->url; char dest[PATH_MAX_LEN]; const char *base; if (src->rename && strcmp(src->rename, "SKIP") == 0) continue; if (!strchr(url, '/')) continue; /* local file, not a URL */ if (src->rename && src->rename[0]) snprintf(dest, sizeof(dest), "%s/%s", config->source_dir, src->rename); else { base = strrchr(url, '/'); snprintf(dest, sizeof(dest), "%s/%s", config->source_dir, base ? base + 1 : url); } if (access(dest, F_OK) != 0) missing++; } if (missing) { error("%s %s sources not found (use -d to download)", pkgfile->name, pkgfile->version); return E_DOWNLOAD; } } ret = download_sources(pkgfile, config); if (ret != 0) return ret; } if (!config->ignore_checksums) { ret = verify_checksums(pkgfile, config); if (ret != 0) return ret; } return 0; } /* * Is another version of `name` installed in `root`? Then -r installs * with addpkg -u. The same version is left to addpkg to refuse. */ static int other_version_installed(const char *root, const char *name, const char *verrel) { pkgdb_t *db = db_open(root && root[0] ? root : "/", 0); pkg_t *p = db_find(db, name); int r = p && strcmp(p->version, verrel) != 0; db_close(db); return r; } static void print_help(void) { printf( "usage: mkpkg [dir] [options]\n" " -d download sources\n" " -e extract sources\n" " -c print checksums to stdout\n" " -p show embedded public key\n" " -f print footprint to stdout\n" " -s verify signature\n" " -w / -ww keep work dir / resume build\n" " -b force lockbox\n" " -x [dfsw] remove\n" " -u [cfsp] update\n" " -i [cfsnb] ignore\n" " -o [de] only\n" " -r install to rootfs after build\n" " -g alternate config file\n" " -v / -h version / help\n" ); } static void parse_flags(const char *arg, config_t *c, int *flag_w) { const char *p; char grp = 0; for (p = arg; *p; p++) { switch (*p) { case 'x': grp = 'x'; c->clean = 1; break; case 'u': grp = 'u'; break; case 'i': grp = 'i'; break; case 'o': grp = 'o'; break; default: if (grp == 'x') { if (*p == 'd') c->clean_download = 1; if (*p == 'f') c->clean_footprint = 1; if (*p == 's') c->clean_signature = 1; if (*p == 'w') c->clean_work = 1; } else if (grp == 'u') { if (*p == 'c') c->update_checksums = 1; if (*p == 'f') c->update_footprint = 1; if (*p == 's') c->update_signature = 1; if (*p == 'p') c->update_pubkey = 1; } else if (grp == 'i') { if (*p == 'c') c->ignore_checksums = 1; if (*p == 'f') c->ignore_footprint = 1; if (*p == 's') c->ignore_signature = 1; if (*p == 'b') c->no_lockbox = 1; } else if (grp == 'o') { if (*p == 'd') c->download_only = 1; if (*p == 'e') c->extract_only = 1; } else { switch (*p) { case 'd': c->download = 1; break; case 'e': c->extract_only = 1; break; case 'c': c->print_checksums = 1; break; case 'f': c->print_footprint = 1; break; case 's': c->check_signature = 1; break; case 'p': c->show_pubkey = 1; break; case 'b': c->lockbox = 1; break; case 'w': (*flag_w)++; break; case 'v': printf("mkpkg %s\n", MKPKG_VERSION); exit(0); case 'h': print_help(); exit(0); default: errx(1, "unknown option: -%c", *p); } } } } } int main(int argc, char **argv) { config_t config; pkgfile_t pkgfile; char cwd[PATH_MAX_LEN]; char target[PATH_MAX_LEN]; char sig_file[PATH_MAX_LEN]; char footprint_path[PATH_MAX_LEN]; char pkg_name[256], pkg_verrel[512]; char *opt_rootfs = NULL; char *opt_config = NULL; char *opt_dir = NULL; int flag_w = 0; int ret = 0; int built = 0; int i; memset(&config, 0, sizeof(config)); for (i = 1; i < argc; i++) { char *a = argv[i]; if (a[0] != '-') { if (!opt_dir) { opt_dir = a; continue; } errx(1, "unexpected argument: %s", a); } if (strcmp(a, "--help") == 0) { print_help(); return 0; } if (strcmp(a, "--version") == 0) { printf("mkpkg %s\n", MKPKG_VERSION); return 0; } if (strcmp(a, "-r") == 0) { if (++i >= argc) errx(1, "-r requires an argument"); opt_rootfs = argv[i]; continue; } if (strcmp(a, "-g") == 0) { if (++i >= argc) errx(1, "-g requires an argument"); opt_config = argv[i]; continue; } parse_flags(a + 1, &config, &flag_w); } config.keep_work = flag_w > 0; config.resume = flag_w > 1; if (opt_rootfs) { snprintf(config.rootfs, sizeof(config.rootfs), "%s", opt_rootfs); config.install = 1; } /* resolve -g before chdir(dir): it names a file relative to where * mkpkg was started, and the hooks need an absolute path */ if (opt_config) { static char conf_abs[PATH_MAX]; if (!realpath(opt_config, conf_abs)) err(1, "%s", opt_config); opt_config = conf_abs; } if (opt_dir && chdir(opt_dir) != 0) err(1, "cannot chdir to '%s'", opt_dir); if (getcwd(cwd, sizeof(cwd)) == NULL) err(1, "getcwd"); init_config(&config, cwd); if (opt_config) snprintf(config.config_file, sizeof(config.config_file), "%s", opt_config); parse_config(config.config_file, &config); /* MKPKG_COMPRESSION_MODE default — gzip if not configured */ if (!config.compression[0]) snprintf(config.compression, sizeof(config.compression), "gz"); g_config = &config; signal(SIGINT, on_interrupt); signal(SIGTERM, on_interrupt); signal(SIGPIPE, SIG_IGN); /* ignore broken pipes from child pipelines */ if (access(config.pkgfile, F_OK) != 0) errx(1, "file '%s' not found.", config.pkgfile); /* resolve target */ { pkgfile_t tmp; if (parse_pkgfile(config.pkgfile, &tmp) != 0) return E_PKGFILE; snprintf(target, sizeof(target), "%s/%s#%s-%s.pkg.tar.%s", config.package_dir, tmp.name, tmp.version, tmp.release, config.compression); snprintf(pkg_name, sizeof(pkg_name), "%s", tmp.name); snprintf(pkg_verrel, sizeof(pkg_verrel), "%s-%s", tmp.version, tmp.release); free_pkgfile(&tmp); } snprintf(sig_file, sizeof(sig_file), "%s/%s", config.root_dir, config.signature_file); snprintf(footprint_path, sizeof(footprint_path), "%s/%s", config.root_dir, config.footprint_file); /* -x [dfs]: remove */ if (config.clean) { if (access(target, F_OK) == 0) { info("removing %s", target); unlink(target); } if (config.clean_download) { pkgfile_t tmp; if (parse_pkgfile(config.pkgfile, &tmp) == 0) { for (i = 0; i < tmp.source_count; i++) { const char *url = tmp.sources[i].url; const char *base = strrchr(url, '/'); if (base) { char local[PATH_MAX_LEN]; snprintf(local, sizeof(local), "%s/%s", config.source_dir, base + 1); if (access(local, F_OK) == 0) { info("removing %s", local); unlink(local); } } } free_pkgfile(&tmp); } } if (config.clean_footprint && access(footprint_path, F_OK) == 0) { info("removing %s", footprint_path); unlink(footprint_path); } if (config.clean_signature && access(sig_file, F_OK) == 0) { info("removing %s", sig_file); unlink(sig_file); } if (config.clean_work) { char workdir[PATH_MAX_LEN]; snprintf(workdir, sizeof(workdir), "%s/work", config.root_dir); if (access(workdir, F_OK) == 0) { info("removing %s", workdir); rmrf(workdir); } } return 0; } /* -f: print footprint to stdout */ if (config.print_footprint) { if (access(target, F_OK) != 0) errx(1, "package not found: %s", target); return arc_footprint(target); } /* -c: print checksums to stdout */ if (config.print_checksums) { pkgfile_t pf; if (parse_pkgfile(config.pkgfile, &pf) != 0) return E_PKGFILE; ret = generate_checksums(&pf, &config); free_pkgfile(&pf); return ret; } /* -p: show embedded public key */ if (config.show_pubkey) { pkgfile_t pf; if (parse_pkgfile(config.pkgfile, &pf) != 0) return E_PKGFILE; ret = show_pubkey(&pf); free_pkgfile(&pf); return ret; } /* -s: verify signature */ if (config.check_signature) { pkgfile_t pf; if (parse_pkgfile(config.pkgfile, &pf) != 0) return E_PKGFILE; if (!pf.has_signify) { error("no signify() function found in %s", config.pkgfile); free_pkgfile(&pf); return E_SIGNATURE; } if (access(sig_file, F_OK) != 0) { error("signature not found: %s", sig_file); free_pkgfile(&pf); return E_SIGNATURE; } ret = verify_signature(sig_file, &pf, &config); if (ret == 0) info("signature check passed"); free_pkgfile(&pf); return ret; } /* -od: download only */ if (config.download_only) { pkgfile_t pf; if (parse_pkgfile(config.pkgfile, &pf) != 0) return E_PKGFILE; ret = download_sources(&pf, &config); free_pkgfile(&pf); return ret; } /* -oe / -e: extract only */ if (config.extract_only) { pkgfile_t pf; char src_dir[PATH_MAX_LEN]; if (parse_pkgfile(config.pkgfile, &pf) != 0) return E_PKGFILE; snprintf(src_dir, sizeof(src_dir), "%s/src", config.work_dir); make_work_dir(&config); ret = unpack_sources(&pf, &config, src_dir); free_pkgfile(&pf); return ret; } /* * -u[cfsp]: the letters combine, and run in the order a maintainer * would: p (key into the recipe), c (checksums into the recipe), * then with s the build if needed, f (accept the footprint), and * the signature last, so it covers what p, c and f wrote. */ if (config.update_pubkey) { pkgfile_t pf; if (parse_pkgfile(config.pkgfile, &pf) != 0) return E_PKGFILE; ret = update_pubkey(&pf, &config); free_pkgfile(&pf); if (ret != 0) return ret; } if (config.update_checksums) { pkgfile_t pf; if (parse_pkgfile(config.pkgfile, &pf) != 0) return E_PKGFILE; ret = update_checksums(&pf, &config); free_pkgfile(&pf); if (ret != 0) return ret; } if (config.update_footprint && !config.update_signature) { if (access(target, F_OK) != 0) errx(1, "package not found: %s", target); return update_footprint(target, footprint_path, &config); } if ((config.update_pubkey || config.update_checksums) && !config.update_signature) return 0; /* parse MAKEPKG fully for build / sign */ memset(&pkgfile, 0, sizeof(pkgfile)); if (parse_pkgfile(config.pkgfile, &pkgfile) != 0) return E_PKGFILE; /* -us: update signature */ if (config.update_signature) { /* the signature vouches for the sources: check them first, * whether or not the package has to be rebuilt */ ret = prepare_sources(&pkgfile, &config, target); if (ret != 0) { free_pkgfile(&pkgfile); return ret; } if (build_needed(&pkgfile, &config, target)) { ret = run_build(&pkgfile, &config, target); if (ret != 0) { free_pkgfile(&pkgfile); return ret; } built = 1; } else { info("%s is up to date.", target); } if (config.update_footprint) ret = update_footprint(target, footprint_path, &config); if (ret == 0) { int fp_ret = check_footprint(target, footprint_path, &config); if (fp_ret != 0 && !config.ignore_footprint) { error("footprint mismatch"); error("not signed"); ret = E_FOOTPRINT; } else { int sr = create_signature(&pkgfile, &config); if (sr == -2) { error("no secret key"); error("not signed"); ret = E_SIGNATURE; } else if (sr != 0) { error("signature failed"); ret = E_SIGNATURE; } } } if (built) run_post_shell(&pkgfile, &config); free_pkgfile(&pkgfile); return ret; } /* verify signature before build */ if (!config.ignore_signature) { if (!pkgfile.has_signify) { error("no signify() function found in %s", config.pkgfile); free_pkgfile(&pkgfile); return E_SIGNATURE; } if (access(sig_file, F_OK) != 0) { error("signature not found: %s", sig_file); if (strcmp(config.pkgfile, "MAKEPKG.lock") == 0) fprintf(stderr, "mkpkg: locked port: sign the lock with " "`mkpkg -us`, or build with -is\n"); free_pkgfile(&pkgfile); return E_SIGNATURE; } if (verify_signature(sig_file, &pkgfile, &config) != 0) { error("port signature verification failed"); free_pkgfile(&pkgfile); return E_SIGNATURE; } } /* download, verify checksums */ ret = prepare_sources(&pkgfile, &config, target); if (ret != 0) { free_pkgfile(&pkgfile); return ret; } /* up to date? */ if (!build_needed(&pkgfile, &config, target)) { info("%s is up to date.", target); free_pkgfile(&pkgfile); return 0; } /* shell() [pre], build (full lockbox: Landlock restricts the build * in a child; per-cmd lockbox: lbexec wraps commands in build()) */ ret = run_build(&pkgfile, &config, target); if (ret != 0) { free_pkgfile(&pkgfile); return ret; } /* * Footprint check -- signing only if the footprint is clean. A * mismatch does not fail the build: the package of a long build * (firefox, llvm) is kept, and the maintainer reviews the list, * then accepts it with -uf and signs with -us. */ { int fp_ret = check_footprint(target, footprint_path, &config); if (!config.ignore_signature && pkgfile.has_signify) { if (fp_ret == 0 || config.ignore_footprint) { int sr = create_signature(&pkgfile, &config); if (sr != 0 && sr != -2) warning("failed to create signature"); /* sr == -2: no key configured, skip silently */ } } } /* post-shell hook */ run_post_shell(&pkgfile, &config); /* install to rootfs -- an upgrade if another version is there */ if (config.install) { config.upgrade = other_version_installed(config.rootfs, pkg_name, pkg_verrel); ret = install_package(target, config.upgrade, config.rootfs); } free_pkgfile(&pkgfile); return ret; }