/* * meta.c - package metadata (groups, services, permissions, capabilities) * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ /* chroot() is XSI / BSD; expose it under our std=c99 + _POSIX_C_SOURCE build */ #define _DEFAULT_SOURCE #define _BSD_SOURCE #include "meta.h" #include "archive.h" #include #include #include #include #include #include #include #include #include #include #include #include /* ── Internal helpers ────────────────────────────────────────────────── */ /* * Run a command, optionally inside `root` via chroot, and return its * exit status (127 = exec failed, -1 = fork/wait failed). * * The chroot matters: groupadd, useradd and install must act on the * TARGET filesystem, not the builder's. Without it `addpkg -r /mnt` * silently edits the host's /etc/group and creates service home * directories on the host, while the image being built gets neither. */ static int run_cmd_root(const char *root, const char *path, const char *const argv[]) { pid_t pid = fork(); if (pid == 0) { int fd; if (root && root[0] && strcmp(root, "/") != 0) { if (chroot(root) != 0) _exit(126); if (chdir("/") != 0) _exit(126); } fd = open("/dev/null", O_WRONLY); if (fd >= 0) { dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); close(fd); } execvp(path, (char *const *)argv); _exit(127); } if (pid > 0) { int status; if (waitpid(pid, &status, 0) < 0) return -1; if (WIFEXITED(status)) return WEXITSTATUS(status); return -1; } return -1; } /* * Base-system groups owned by the filesystem skeleton (setup-filesystem), * not by any package. They carry fixed GIDs that are baked into installed * file ownership, backups and disk images, and human accounts are members * of several of them — so they must never be created dynamically (which * would renumber them) nor removed when the last declaring package goes * away (which would strip both file ownership and user membership). * * A port may still declare a reserved group in order to attach a system * USER to it — `groups=(sshd:user:sshd:/var/empty:750)` — in which case * the user half is created and removed normally and only the group half * is left alone. * * Keep in sync with the /etc/group heredoc in setup-filesystem. * Every name here is below GID 100 in the skeleton except users (100) * and rfkill (101), which are pinned by convention; nothing in this * list may ever be allocated by groupadd -r. */ static const char *const reserved_groups[] = { "root", "daemon", "bin", "sys", "adm", "tty", "disk", "lp", "mail", "uucp", "wheel", "kmem", "cron", "dialout", "ftp", "sshd", "cdrom", "floppy", "tape", "input", "kvm", "audio", "netdev", "render", "usb", "sgx", "utmp", "video", "plugdev", "users", "rfkill", "nogroup", NULL }; /* * Homes that are assertions, not directories. * * /nonexistent is a convention meaning "this account has no home". * Creating it makes the assertion false, and worse, gives it an owner: * whichever service account was installed last. /var/empty is a chroot * target and must stay root-owned or the chroot is pointless -- sshd * refuses to start otherwise. Both are shared by several ports, so the * last writer would silently take ownership from the others. * * The grammar has no way to say "record this path, do not create it", * and adding one would put the decision in port data. Compiled in here * instead, for the same reason the reserved group table is. */ static const char *const nocreate_homes[] = { "/nonexistent", "/var/empty", "/dev/null", "/", NULL }; static int home_is_nocreate(const char *path) { size_t i; for (i = 0; nocreate_homes[i]; i++) if (strcmp(nocreate_homes[i], path) == 0) return 1; return 0; } static int group_is_reserved(const char *name) { size_t i; for (i = 0; reserved_groups[i]; i++) if (strcmp(reserved_groups[i], name) == 0) return 1; return 0; } /* * Extract the group name from an entry. * Simple: "seatd" → "seatd" * Extended: "dhcpcd:user:dhcpcd:/var/lib/dhcpcd:700" → "dhcpcd" * Writes into caller-supplied buf[bufsz]. */ static void group_name(const char *entry, char *buf, size_t bufsz) { const char *colon = strchr(entry, ':'); size_t len; if (!colon) { snprintf(buf, bufsz, "%s", entry); return; } len = (size_t)(colon - entry); if (len >= bufsz) len = bufsz - 1; memcpy(buf, entry, len); buf[len] = '\0'; } /* * Parse extended group entry: * * grp:user:username:/homedir:mode[:uid:gid] * * The trailing uid and gid are optional. When absent the account is * allocated dynamically by groupadd -r / useradd -r, which is fine on a * single machine but produces different numbers on every install: the * two allocators descend independently, so a service ends up with a uid * and gid that neither match each other nor reproduce on a second host. * Any state directory carried between machines then has stale numeric * ownership and the service cannot read its own files. * * Pinning both makes a service account reproducible across installs. * * Returns 1 if extended (a user is attached), 0 if group-only. * uid/gid receive the empty string when unpinned. */ static int group_has_user(const char *entry, char *grp, size_t grpsz, char *user, size_t usrsz, char *home, size_t homsz, char *mode, size_t modsz, char *uid, size_t uidsz, char *gid, size_t gidsz) { char tmp[512]; char *f[7]; int n = 0; char *p; snprintf(tmp, sizeof(tmp), "%s", entry); f[n++] = tmp; for (p = tmp; *p && n < 7; p++) { if (*p == ':') { *p = '\0'; f[n++] = p + 1; } } if (n < 5 || strcmp(f[1], "user") != 0) return 0; snprintf(grp, grpsz, "%s", f[0]); snprintf(user, usrsz, "%s", f[2]); snprintf(home, homsz, "%s", f[3]); snprintf(mode, modsz, "%s", f[4]); /* uid and gid are pinned only when BOTH are present */ if (n == 7 && f[5][0] && f[6][0]) { snprintf(uid, uidsz, "%s", f[5]); snprintf(gid, gidsz, "%s", f[6]); } else { uid[0] = '\0'; gid[0] = '\0'; } return 1; } /* * Parse a simple group entry with a pinned GID: "seatd:990" * * Distinguished from the extended form by field count and content: * the extended form is grp:user:... with the literal "user" second, * so a two-field entry whose second field is all digits is a pinned * group with no account attached. * * Returns 1 if a GID was pinned, 0 for a bare "seatd". */ static int group_simple_gid(const char *entry, char *grp, size_t grpsz, char *gid, size_t gidsz) { const char *colon = strchr(entry, ':'); const char *p; size_t len; gid[0] = '\0'; if (!colon || !colon[1]) return 0; /* second field must be entirely digits, and be the last field */ for (p = colon + 1; *p; p++) if (*p < '0' || *p > '9') return 0; len = (size_t)(colon - entry); if (len >= grpsz) len = grpsz - 1; memcpy(grp, entry, len); grp[len] = '\0'; snprintf(gid, gidsz, "%s", colon + 1); return 1; } /* ── meta_read / meta_extract / meta_free ────────────────────────────── */ pkg_meta_t *meta_read(const char *path) { pkg_meta_t *m; FILE *fp; char line[256]; m = xmalloc(sizeof(pkg_meta_t)); m->groups = strarray_new(); m->services = strarray_new(); m->permissions = strarray_new(); m->capabilities = strarray_new(); fp = fopen(path, "r"); if (!fp) return m; /* missing meta file is normal */ while (fgets(line, sizeof(line), fp)) { line[strcspn(line, "\n")] = '\0'; if (strncmp(line, "group ", 6) == 0 && line[6]) strarray_add(m->groups, line + 6); else if (strncmp(line, "service ", 8) == 0 && line[8]) strarray_add(m->services, line + 8); else if (strncmp(line, "permission ", 11) == 0 && line[11]) strarray_add(m->permissions, line + 11); else if (strncmp(line, "capability ", 11) == 0 && line[11]) strarray_add(m->capabilities, line + 11); } fclose(fp); return m; } pkg_meta_t *meta_extract(const char *archive, const char *pkgname) { char tmpdir[PATH_MAX_LEN]; char metarel[PATH_MAX_LEN]; char tmppath[PATH_MAX_LEN]; const char *tmp; pkg_meta_t *m; tmp = getenv("TMPDIR"); if (!tmp || !tmp[0]) tmp = "/tmp"; snprintf(tmpdir, sizeof(tmpdir), "%s/pkgmeta.XXXXXX", tmp); if (!mkdtemp(tmpdir)) { /* return empty meta on failure */ m = xmalloc(sizeof(pkg_meta_t)); m->groups = strarray_new(); m->services = strarray_new(); m->permissions = strarray_new(); m->capabilities = strarray_new(); return m; } /* relative path of meta file inside the archive */ snprintf(metarel, sizeof(metarel), "%s/%s", PKG_METADIR, pkgname); arc_extract_single(archive, metarel, tmpdir); snprintf(tmppath, sizeof(tmppath), "%s/%s", tmpdir, metarel); m = meta_read(tmppath); /* clean up temp tree */ rmrf(tmpdir); return m; } void meta_free(pkg_meta_t *m) { if (!m) return; strarray_free(m->groups); strarray_free(m->services); strarray_free(m->permissions); strarray_free(m->capabilities); free(m); } /* ── Groups ──────────────────────────────────────────────────────────── */ /* * Return 1 if `name` names an existing group inside `root`. * Uses getent so the lookup happens against the target's /etc/group. * If getent is unavailable (exit 127, early bootstrap) we cannot tell, * so report "present" rather than blocking the install. */ static int group_exists(const char *root, const char *name) { const char *argv[] = { "getent", "group", name, NULL }; int rc = run_cmd_root(root, "getent", argv); if (rc == 127 || rc < 0) return 1; /* cannot check — assume ok */ return rc == 0; } void meta_add_groups(pkg_meta_t *m, const char *root) { size_t i; if (!m || !m->groups) return; for (i = 0; i < m->groups->count; i++) { const char *entry = m->groups->items[i]; char grp[256], user[256], home[PATH_MAX_LEN], mode[8]; char uid[32], gid[32]; int extended; extended = group_has_user(entry, grp, sizeof(grp), user, sizeof(user), home, sizeof(home), mode, sizeof(mode), uid, sizeof(uid), gid, sizeof(gid)); if (!extended) { uid[0] = '\0'; if (!group_simple_gid(entry, grp, sizeof(grp), gid, sizeof(gid))) group_name(entry, grp, sizeof(grp)); } if (group_is_reserved(grp)) { /* * Skeleton group: never create it. If it is missing the * target's bootstrap is broken, and silently allocating a * fresh GID would be worse than saying so. */ if (!group_exists(root, grp)) warnx("reserved group %s is missing from %s — " "broken filesystem skeleton, not creating it", grp, (root && root[0]) ? root : "/"); } else if (gid[0]) { const char *ga[] = { "groupadd", "-r", "-g", gid, grp, NULL }; int rc = run_cmd_root(root, "groupadd", ga); /* rc==9: group or gid already taken — normal on reinstall */ if (rc != 0 && rc != 9 && !group_exists(root, grp)) warnx("groupadd -g %s %s failed (exit %d)", gid, grp, rc); } else { const char *ga[] = { "groupadd", "-r", grp, NULL }; int rc = run_cmd_root(root, "groupadd", ga); /* rc==9: group already exists — normal on reinstall */ if (rc != 0 && rc != 9 && !group_exists(root, grp)) warnx("groupadd %s failed (exit %d)", grp, rc); } if (extended) { const char *ua_pinned[] = { "useradd", "-r", "-s", "/bin/false", "-u", uid, "-d", home, "-g", grp, user, NULL }; const char *ua_dyn[] = { "useradd", "-r", "-s", "/bin/false", "-d", home, "-g", grp, user, NULL }; int rc = run_cmd_root(root, "useradd", uid[0] ? ua_pinned : ua_dyn); /* rc==9: user or uid already exists — normal on reinstall */ if (rc != 0 && rc != 9) warnx("useradd %s failed (exit %d)", user, rc); if (!home_is_nocreate(home)) { const char *id[] = { "install", "-d", "-m", mode, "-o", user, "-g", grp, home, NULL }; rc = run_cmd_root(root, "install", id); if (rc != 0) warnx("install -d %s failed (exit %d)", home, rc); } } } } void meta_del_groups(pkg_meta_t *m, const char *root, const char *pkgname) { size_t i; DIR *d; struct dirent *ent; char *metadir; if (!m || !m->groups || m->groups->count == 0) return; metadir = path_join(root, PKG_METADIR); for (i = 0; i < m->groups->count; i++) { const char *entry = m->groups->items[i]; char mygrp[256]; int still_needed = 0; group_name(entry, mygrp, sizeof(mygrp)); d = opendir(metadir); if (d) { while ((ent = readdir(d)) != NULL) { char *other; pkg_meta_t *om; size_t j; char og[256]; if (ent->d_name[0] == '.') continue; if (strcmp(ent->d_name, pkgname) == 0) continue; other = path_join(metadir, ent->d_name); om = meta_read(other); free(other); if (om && om->groups) { for (j = 0; j < om->groups->count; j++) { group_name(om->groups->items[j], og, sizeof(og)); if (strcmp(og, mygrp) == 0) { still_needed = 1; break; } } } meta_free(om); if (still_needed) break; } closedir(d); } if (!still_needed) { char user[256], home[PATH_MAX_LEN], mode[8], g[256]; char uid[32], gid[32]; int extended; extended = group_has_user(entry, g, sizeof(g), user, sizeof(user), home, sizeof(home), mode, sizeof(mode), uid, sizeof(uid), gid, sizeof(gid)); /* the user half is package-owned even for a reserved group */ if (extended) { const char *ud[] = { "userdel", user, NULL }; run_cmd_root(root, "userdel", ud); } /* * Never remove a skeleton group. Its GID is baked into * installed file ownership and human accounts are members * of it; the refcount above only counts packages, so it * cannot see either. */ if (!group_is_reserved(mygrp)) { const char *gd[] = { "groupdel", mygrp, NULL }; run_cmd_root(root, "groupdel", gd); } } } free(metadir); } /* ── Services ────────────────────────────────────────────────────────── */ void meta_add_services(pkg_meta_t *m, const char *root) { size_t i; if (!m || !m->services) return; for (i = 0; i < m->services->count; i++) { const char *name = m->services->items[i]; char *svcdir, *svclink, *target; struct stat st; target = path_join("/etc/sv", name); svcdir = path_join(root, "service"); mkdirp(svcdir, 0755); svclink = path_join(svcdir, name); free(svcdir); if (lstat(svclink, &st) != 0) (void)!symlink(target, svclink); /* best-effort */ free(svclink); free(target); } } static void run_trigger_in_root(const char *root, const char *cmd, const char *const argv[]); static int service_running(const char *root, const char *name); void meta_del_services(pkg_meta_t *m, const char *root) { size_t i; if (!m || !m->services) return; for (i = 0; i < m->services->count; i++) { const char *name = m->services->items[i]; char *svcdir, *svclink; struct stat st; svcdir = path_join(root, "service"); svclink = path_join(svcdir, name); free(svcdir); /* * Unlink first, then stop. `sv exit` terminates runsv, and * runsvdir would start a fresh one within its poll interval if * the symlink were still there. With the symlink gone, sv * still reaches supervise/control by absolute path. */ if (lstat(svclink, &st) == 0 && S_ISLNK(st.st_mode)) unlink(svclink); free(svclink); /* * Stop it deterministically rather than leaving runsvdir to * notice. Without this, remove_pkg_files deletes the run * script out from under a live runsv, which then spins on an * exec that cannot succeed. */ if (service_running(root, name)) { char svpath[PATH_MAX_LEN]; const char *argv[4]; snprintf(svpath, sizeof(svpath), "/etc/sv/%s", name); argv[0] = "sv"; argv[1] = "exit"; argv[2] = svpath; argv[3] = NULL; run_trigger_in_root(root, "sv", argv); } } } /* * Remove the service directory once its files are gone. * * supervise/ is created by runsv at runtime, so it is not in the * payload and delpkg will not touch it — which leaves /etc/sv/ * behind as an empty orphan. rmdir only, so a directory still holding * anything the operator put there survives untouched. */ void meta_prune_service_dirs(pkg_meta_t *m, const char *root) { size_t i; if (!m || !m->services) return; for (i = 0; i < m->services->count; i++) { char *svdir, *sup; svdir = path_join(root, "etc/sv"); sup = path_join(svdir, m->services->items[i]); free(svdir); svdir = path_join(sup, "supervise"); rmdir(svdir); /* empty only; ignore failure */ free(svdir); if (rmdir(sup) == 0) info("removed service directory %s", sup); free(sup); } } /* ── Permissions & capabilities ──────────────────────────────────────── */ /* * Resolve a user or group name the way the installed system will: from * /etc/passwd or /etc/group. getpwnam()/getgrnam() read * the host's files, which under addpkg -r is the wrong system -- the * groups were just created inside root, by groupadd in a chroot. * A numeric name is taken as the id itself. Returns 0 on success. */ static int root_lookup_id(const char *root, const char *dbfile, const char *name, unsigned long *id) { char path[PATH_MAX_LEN]; char line[1024]; FILE *fp; char *end; size_t nlen = strlen(name); *id = strtoul(name, &end, 10); if (*name && !*end) return 0; snprintf(path, sizeof(path), "%s/%s", (root && root[0] && strcmp(root, "/") != 0) ? root : "", dbfile); if (!(fp = fopen(path, "r"))) return -1; while (fgets(line, sizeof(line), fp)) { char *p; /* name:passwd:id:... */ if (strncmp(line, name, nlen) != 0 || line[nlen] != ':') continue; p = strchr(line + nlen + 1, ':'); if (!p) break; *id = strtoul(p + 1, &end, 10); if (end == p + 1 || *end != ':') break; fclose(fp); return 0; } fclose(fp); return -1; } void meta_apply_permissions(pkg_meta_t *m, const char *root) { size_t i; if (!m || !m->permissions) return; for (i = 0; i < m->permissions->count; i++) { const char *entry = m->permissions->items[i]; char tmp[512]; char *f[4]; int n = 0; char *p, *fullpath; unsigned int mode; unsigned long uid, gid; snprintf(tmp, sizeof(tmp), "%s", entry); f[n++] = tmp; for (p = tmp; *p && n < 4; p++) { if (*p == ':') { *p = '\0'; f[n++] = p + 1; } } if (n < 4) { warnx("bad permission entry: %s", entry); continue; } fullpath = path_join(root, f[0]); if (sscanf(f[3], "%o", &mode) != 1) { warnx("bad mode in permission: %s", f[3]); free(fullpath); continue; } if (root_lookup_id(root, "etc/passwd", f[1], &uid) != 0) { warnx("unknown user: %s", f[1]); free(fullpath); continue; } if (root_lookup_id(root, "etc/group", f[2], &gid) != 0) { warnx("unknown group: %s", f[2]); free(fullpath); continue; } /* chown before chmod — chown clears setuid/setgid bits */ if (lchown(fullpath, (uid_t)uid, (gid_t)gid) != 0) warn("chown %s:%s %s", f[1], f[2], fullpath); /* * chmod() follows symlinks and Linux has no AT_SYMLINK_NOFOLLOW * for it, so applying a mode to a symlink would silently rewrite * the permissions of whatever it points at — possibly a file * owned by another package. Symlink modes are meaningless * anyway, so skip. */ { struct stat lst; if (lstat(fullpath, &lst) == 0 && S_ISLNK(lst.st_mode)) { free(fullpath); continue; } } if (chmod(fullpath, (mode_t)mode) != 0) warn("chmod %04o %s", mode, fullpath); free(fullpath); } } void meta_apply_capabilities(pkg_meta_t *m, const char *root) { size_t i; if (!m || !m->capabilities) return; for (i = 0; i < m->capabilities->count; i++) { const char *entry = m->capabilities->items[i]; char tmp[512]; char *colon, *fullpath; snprintf(tmp, sizeof(tmp), "%s", entry); colon = strchr(tmp, ':'); if (!colon) { warnx("bad capability entry: %s (need path:caps)", entry); continue; } *colon = '\0'; fullpath = path_join(root, tmp); { const char *argv[] = { "setcap", colon + 1, fullpath, NULL }; pid_t pid = fork(); if (pid == 0) { execvp("setcap", (char *const *)argv); _exit(1); } if (pid > 0) { int status; waitpid(pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) warnx("setcap %s %s failed", colon + 1, fullpath); } } free(fullpath); } } /* ── Triggers ────────────────────────────────────────────────────────── */ /* * A trigger is a system-side reflex, not port data: keyed on a path * prefix appearing in the package payload, idempotent, taking no * arguments from the MAKEPKG. Table order is execution order. * * `capture` appends the first path component after the prefix to argv * — "lib/modules/6.18.49/kernel/..." yields "6.18.49" — because bare * `depmod -a` operates on uname -r, which is wrong in exactly the case * that matters: installing a kernel you are not currently running. */ struct trigger { const char *prefix; const char *cmd; const char *argv[6]; int capture; /* * Fields below are appended, so existing rows that omit them are * zero-initialised by C99 aggregate rules and keep their behaviour. */ int upgrade_only; /* skip unless addpkg -u */ int need_running; /* skip unless runsv reports "run" */ int arg_abs; /* pass /, not bare */ int need_dir; /* skip unless / is a dir */ }; /* * Is the service supervised and up? * * runsv keeps supervise/stat as a plain text file holding "run", "down" * or "finish". Reading it beats invoking `sv status`, which exits 0 * whether the service is up or down, so its exit code decides nothing. * A missing file means nothing is supervising the service — not enabled, * or we are inside a chroot — and the trigger skips. */ static int service_running(const char *root, const char *name) { char path[PATH_MAX_LEN]; char buf[16]; FILE *fp; int up; snprintf(path, sizeof(path), "%s/etc/sv/%s/supervise/stat", (root && root[0] && strcmp(root, "/") != 0) ? root : "", name); if (!(fp = fopen(path, "r"))) return 0; up = fgets(buf, sizeof(buf), fp) && strncmp(buf, "run", 3) == 0; fclose(fp); return up; } static const struct trigger triggers[] = { /* * need_dir: triggers fire on remove as well as add, and by then the * module tree is gone -- `depmod -a 6.18.50` then fails on a * directory delpkg just deleted. Nothing depends on that run, but * it emits an error and a trigger warning on every kernel removal, * which teaches you to ignore trigger warnings. Guarding on the * directory also covers the upgrade case where a kernel package * changes version and the old tree disappears mid-operation. */ { "lib/modules/", "depmod", { "depmod", "-a", NULL }, 1, 0, 0, 0, 1 }, /* * Reconcile the ESP after anything lands in /boot. Must come after * depmod: the generated menu annotates a kernel with no module tree, * and that check is only meaningful once depmod has run. No package * writes to /boot/efi — this is the only thing that does. */ { "boot/", "limine-sync", { "limine-sync", NULL }, 0, 0, 0, 0, 0 }, /* * limine-sync mirrors /usr/share/limine/BOOTX64.EFI onto the ESP, * so an upgrade of limine itself must reconcile too. The limine * package ships no boot/ path — the ESP copy is not package-owned — * so the boot/ trigger above never matches it. Same command, so * run_triggers' dedup keeps it to one invocation when a payload * happens to match both prefixes. */ { "usr/share/limine/", "limine-sync", { "limine-sync", NULL }, 0, 0, 0, 0, 0 }, { "usr/lib/udev/hwdb.d/", "systemd-hwdb", { "systemd-hwdb", "update", NULL }, 0, 0, 0, 0, 0 }, { "etc/udev/hwdb.d/", "systemd-hwdb", { "systemd-hwdb", "update", NULL }, 0, 0, 0, 0, 0 }, { "usr/share/fonts/", "fc-cache", { "fc-cache", "-s", NULL }, 0, 0, 0, 0, 0 }, { "usr/share/glib-2.0/schemas/", "glib-compile-schemas", { "glib-compile-schemas", "/usr/share/glib-2.0/schemas/", NULL }, 0, 0, 0, 0, 0 }, { "usr/lib/gdk-pixbuf-2.0/", "gdk-pixbuf-query-loaders", { "gdk-pixbuf-query-loaders", "--update-cache", NULL }, 0, 0, 0, 0, 0 }, { "usr/share/mime/", "update-mime-database", { "update-mime-database", "/usr/share/mime", NULL }, 0, 0, 0, 0, 0 }, { "usr/share/icons/", "gtk-update-icon-cache", { "gtk-update-icon-cache", "-q", "/usr/share/icons/hicolor", NULL }, 0, 0, 0, 0, 0 }, { "usr/share/applications/", "update-desktop-database", { "update-desktop-database", "/usr/share/applications", NULL }, 0, 0, 0, 0, 0 }, /* * Restart a service whose binary or run script we just replaced. * Upgrade only: a fresh install ships etc/sv/ with no runsvdir * symlink, so there is nothing to start and enabling it is the * admin's decision. Running only: `sv restart` on a stopped service * starts it, which would undo a deliberate `sv down`. That check * also covers the down file for free — a service with one that is * not running will not be restarted. */ { "etc/sv/", "sv", { "sv", "restart", NULL }, 1, 1, 1, 1, 0 }, { NULL, NULL, { NULL }, 0, 0, 0, 0, 0 } }; static int has_prefix(const char *s, const char *pfx) { return strncmp(s, pfx, strlen(pfx)) == 0; } /* * Fork a trigger command, optionally inside `root` via chroot. * For root="/" we exec normally. For an alternate root we chroot * into it first — this matches how the trigger expects absolute * paths like /usr/share/icons to resolve, and avoids touching the * host system when installing into a chroot. * * If the trigger binary doesn't exist inside the chroot, we silently * skip — partial installs (early bootstrap) shouldn't fail the install. */ static void run_trigger_in_root(const char *root, const char *cmd, const char *const argv[]) { pid_t pid; size_t i; /* * Announce before forking. A silent trigger is indistinguishable * from one that never fired, which is the whole reason a missing * binary skipping quietly is otherwise hard to notice. */ fprintf(stderr, "trigger:"); for (i = 0; argv[i]; i++) fprintf(stderr, " %s", argv[i]); fputc('\n', stderr); pid = fork(); if (pid == 0) { if (root && root[0] && strcmp(root, "/") != 0) { if (chroot(root) != 0) _exit(0); /* skip silently */ if (chdir("/") != 0) _exit(0); } /* * Output is deliberately NOT redirected to /dev/null. Every * trigger announces itself from the parent, so silencing the * child only hides what it actually did — and limine-sync's * warnings ("no usable kernel in series 6.12") are precisely * the ones that must not disappear. Triggers whose normal * output is noise are quietened in their own arg lists * instead: fc-cache -s, gtk-update-icon-cache -q. */ execvp(cmd, (char *const *)argv); _exit(0); /* missing trigger inside the (ch)root: not fatal */ } if (pid > 0) { int status; waitpid(pid, &status, 0); if (WIFEXITED(status) && WEXITSTATUS(status) != 0) warning("trigger %s exited %d", cmd, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) warning("trigger %s killed by signal %d", cmd, WTERMSIG(status)); } } /* * Copy a trigger's argv and append one extra argument. * `out` must have room for the original argv plus two entries. */ static void argv_with_arg(const char *const src[], const char *extra, const char *out[], size_t outsz) { size_t n = 0; while (src[n] && n + 2 < outsz) { out[n] = src[n]; n++; } out[n++] = extra; out[n] = NULL; } void run_triggers(const char *root, strarray_t *files, int upgrade) { const struct trigger *t; size_t i; for (t = triggers; t->prefix; t++) { if (t->upgrade_only && !upgrade) continue; if (!t->capture) { /* * Fire once if any payload path matches. Two prefixes may * share a command (both hwdb directories); running it twice * is harmless but pointless, so skip if an earlier entry * already ran the same command for this payload. */ int matched = 0, already = 0; const struct trigger *e; for (i = 0; i < files->count && !matched; i++) if (has_prefix(files->items[i], t->prefix)) matched = 1; if (!matched) continue; for (e = triggers; e != t && !already; e++) { if (strcmp(e->cmd, t->cmd) != 0) continue; for (i = 0; i < files->count; i++) if (has_prefix(files->items[i], e->prefix)) { already = 1; break; } } if (already) continue; run_trigger_in_root(root, t->cmd, t->argv); continue; } /* * Capturing trigger: fire once per distinct component following * the prefix, so installing a package carrying two kernel * versions runs depmod for each of them. */ { strarray_t *seen = strarray_new(); size_t pfxlen = strlen(t->prefix); for (i = 0; i < files->count; i++) { const char *f = files->items[i]; const char *rest, *slash; char comp[256]; size_t len; if (!has_prefix(f, t->prefix)) continue; rest = f + pfxlen; if (!*rest) continue; slash = strchr(rest, '/'); len = slash ? (size_t)(slash - rest) : strlen(rest); if (len == 0 || len >= sizeof(comp)) continue; memcpy(comp, rest, len); comp[len] = '\0'; if (strarray_contains(seen, comp)) continue; strarray_add(seen, comp); if (t->need_dir) { char dir[PATH_MAX_LEN]; struct stat st; snprintf(dir, sizeof(dir), "%s/%s%s", (root && root[0] && strcmp(root, "/") != 0) ? root : "", t->prefix, comp); if (lstat(dir, &st) != 0 || !S_ISDIR(st.st_mode)) continue; } if (t->need_running && !service_running(root, comp)) continue; { const char *argv[8]; char abs[512]; const char *arg = comp; if (t->arg_abs) { snprintf(abs, sizeof(abs), "/%s%s", t->prefix, comp); arg = abs; } argv_with_arg(t->argv, arg, argv, sizeof(argv) / sizeof(argv[0])); run_trigger_in_root(root, t->cmd, argv); } } strarray_free(seen); } } } void run_ldconfig(const char *root) { char *ldconf; pid_t pid; int status; /* * Probe for the binary, not the config file. setup-filesystem * ships /etc/ld.so.conf on every rawnix install, but musl has no * ldconfig at all — testing the config meant forking a doomed * process on every single addpkg and delpkg. */ ldconf = path_join(root, "/sbin/ldconfig"); if (!file_exists(ldconf)) { free(ldconf); return; } free(ldconf); pid = fork(); if (pid == 0) { execl("/sbin/ldconfig", "ldconfig", "-r", root, (char *)NULL); _exit(1); } if (pid > 0) waitpid(pid, &status, 0); } /* ── remove_pkg_files ────────────────────────────────────────────────── */ /* * Recursively delete every "_"-prefixed subdirectory under `dir`. * * Python's convention is that any directory whose name starts with an * underscore is internal/private/cache: __pycache__ for bytecode * (created on every import) and single-underscore names like * _scripts, _compat, _in_process for content the package writes at * runtime (subprocess wrappers, compatibility shims, etc.) that * addpkg never tracked because the package itself wrote them after * install. Either way, the convention is "regeneratable, not user * data" — so we wipe them automatically. * * Non-underscore directories are walked recursively (in case the * residue is one level down) and then rmdir-attempted, so they * collapse if they only held _-dirs. Anything that doesn't fit * either pattern survives — and the caller emits a notice. */ static void purge_underscore_dirs(const char *dir) { DIR *dp = opendir(dir); struct dirent *de; if (!dp) return; while ((de = readdir(dp)) != NULL) { char path[PATH_MAX_LEN]; struct stat st; if (strcmp(de->d_name, ".") == 0) continue; if (strcmp(de->d_name, "..") == 0) continue; snprintf(path, sizeof(path), "%s/%s", dir, de->d_name); if (lstat(path, &st) != 0) continue; if (!S_ISDIR(st.st_mode)) continue; if (de->d_name[0] == '_') { rmrf(path); } else { purge_underscore_dirs(path); (void)rmdir(path); /* succeeds only if emptied */ } } closedir(dp); } void remove_pkg_files(const char *root, strarray_t *files, pkgdb_t *db, const char *skip_pkg, strarray_t *keep) { pkg_t *p; size_t i; char *path; /* sort all package file lists once before the removal loop */ for (p = db->head; p; p = p->next) strarray_sort(p->files); strarray_sort(files); /* remove deepest entries first (reverse order) */ for (i = files->count; i > 0; i--) { const char *f = files->items[i - 1]; int refs = 0; if (keep && strarray_contains(keep, f)) continue; for (p = db->head; p && !refs; p = p->next) { if (skip_pkg && strcmp(p->name, skip_pkg) == 0) continue; if (strarray_contains(p->files, f)) refs = 1; } if (refs) continue; path = path_join(root, f); if (remove(path) != 0 && errno == ENOTEMPTY) { /* * rmdir failed because the directory still has content. * * Most common cause on rawnix.org: Python writes _-prefixed * subdirs (__pycache__, _scripts, _compat, _in_process, * ...) at import or first-run that addpkg never tracked. * Wipe those by convention, then retry. Anything left is * non-conventional residue — keep it and tell the user * so they can decide whether to clean by hand. * * Scope: ONLY paths under .../site-packages/... We don't * apply this heuristic system-wide because outside Python * the underscore-prefix-is-private convention isn't a * guarantee — a stray `/etc/foo/_secret/data` from some * unrelated package shouldn't disappear during cleanup. */ if (strstr(f, "/site-packages/") != NULL) { purge_underscore_dirs(path); (void)remove(path); } if (access(path, F_OK) == 0) { fprintf(stderr, "delpkg: notice: keeping non-empty: %s\n", f); } } free(path); } }