/* * revdep - reverse dependency checker for rawnix * main.c - entry point, argument parsing, check logic * * Copyright (c) 2025 zorz@gmx.com * Licensed under the BSD 2-Clause License. See revdep.h. * * Concept based on revdep by James Buren for CRUX Linux. */ #define _POSIX_C_SOURCE 200809L #define _DEFAULT_SOURCE #include "revdep.h" #include #include #include #include #include #include /* installed Python version (major.minor), NULL if not found */ static char *py_version; /* installed Perl version (X.Y.Z), NULL if not found */ static char *perl_version; /* * The system library path the way musl's dynamic linker builds it: * from /etc/ld-musl-x86_64.path, entries separated by ':' or newlines, * if that file exists; otherwise musl's default /lib:/usr/local/lib: * /usr/lib. An existing but empty file means no system path, as for * musl. */ static void load_sys_path(strvec_t *dirs) { FILE *fp; char line[4096]; size_t len; fp = fopen(MUSL_PATH_FILE, "r"); if (!fp) { strsplit(MUSL_SYS_PATH, ':', dirs); return; } while (fgets(line, sizeof(line), fp)) { len = strlen(line); if (len > 0 && line[len - 1] == '\n') line[--len] = '\0'; strsplit(line, ':', dirs); } fclose(fp); } static void usage(void) { printf("revdep %s - reverse dependency checker for rawnix\n\n", VERSION); printf("usage: revdep [-r] [-i pkg,...] [-v] [package ...]\n"); printf(" revdep -h | -V\n\n"); printf("modes:\n"); printf(" (default) check packages for broken libraries\n"); printf(" -r reverse: show what depends on a package\n\n"); printf("options:\n"); printf(" -i pkg,... ignore packages (comma-separated)\n"); printf(" -v increase verbosity (up to -vvvv)\n"); printf(" -h show this help\n"); printf(" -V show the version\n\n"); printf("verbosity:\n"); printf(" (none) broken package names only\n"); printf(" -v status for each package checked\n"); printf(" -vv include broken file paths\n"); printf(" -vvv include missing library names\n"); printf(" -vvvv debug trace\n\n"); printf("files:\n"); printf(" /var/lib/pkg/db package database\n"); printf(" /etc/revdep.d/ per-package library directories\n"); printf(" %s system library path\n", MUSL_PATH_FILE); } /* -------------------------------------------------------------------- */ /* Forward check: are this package's ELF files satisfied? */ /* -------------------------------------------------------------------- */ static bool check_file(const pkg_t *pkg, const char *path, const strvec_t *sys_dirs) { elfinfo_t *elf; size_t i; bool ok = true; msg(V_DEBUG, " %s:%s: checking\n", pkg->name, path); if (!is_regfile(path)) return true; elf = cache_lookup(path); if (!elf) return true; /* not an ELF, skip */ for (i = 0; i < elf->needed.count; i++) { const char *lib = elf->needed.items[i]; if (!lib_find(elf, pkg, lib, sys_dirs)) { msg(V_DETAIL, " %s: %s requires %s (MISSING)\n", pkg->name, path, lib); ok = false; } } if (!ok) msg(V_FILES, " %s: %s: broken\n", pkg->name, path); return ok; } static bool check_package(const pkg_t *pkg, const strvec_t *sys_dirs) { size_t i; bool ok = true; if (pkg->ignore) return true; for (i = 0; i < pkg->files.count; i++) { if (!check_file(pkg, pkg->files.items[i], sys_dirs)) ok = false; } /* Python versioned path check */ if (py_version && py_check_package(pkg, py_version)) ok = false; /* Perl versioned path check */ if (perl_version && perl_check_package(pkg, perl_version)) ok = false; return ok; } static int run_check(pkgvec_t *pkgs, const strvec_t *sys_dirs, int argc, char **argv, int first) { size_t i; int ret = 0; bool ok; if (first >= argc) { /* check all packages */ msg(V_NORMAL, ">> checking %zu packages\n", pkgs->count); for (i = 0; i < pkgs->count; i++) { ok = check_package(pkgs->items[i], sys_dirs); if (!ok) { if (verbosity >= V_NORMAL) msg(V_NORMAL, "%s: BROKEN\n", pkgs->items[i]->name); else msg(V_QUIET, "%s\n", pkgs->items[i]->name); ret = 1; } else { msg(V_NORMAL, "%s: ok\n", pkgs->items[i]->name); } } } else { /* check specific packages */ int count = argc - first; msg(V_NORMAL, ">> checking %d package%s\n", count, count > 1 ? "s" : ""); for (i = first; i < (size_t)argc; i++) { pkg_t *pkg = pkgvec_find(pkgs, argv[i]); if (!pkg) { fprintf(stderr, "revdep: %s: not found in db\n", argv[i]); ret = 1; continue; } ok = check_package(pkg, sys_dirs); if (!ok) { if (verbosity >= V_NORMAL) msg(V_NORMAL, "%s: BROKEN\n", pkg->name); else msg(V_QUIET, "%s\n", pkg->name); ret = 1; } else { msg(V_NORMAL, "%s: ok\n", pkg->name); } } } return ret; } /* -------------------------------------------------------------------- */ /* Reverse mode: what packages depend on 's shared libraries? */ /* */ /* 1. Collect all .so files owned by the target package(s). */ /* 2. Walk every other package's ELF files. */ /* 3. If any DT_NEEDED matches a target .so filename, report it. */ /* -------------------------------------------------------------------- */ static bool is_soname(const char *base) { const char *p = base; while ((p = strstr(p, ".so")) != NULL) { if (p != base && (p[3] == '\0' || p[3] == '.')) return true; p += 3; } return false; } static void collect_sonames(const pkg_t *pkg, strvec_t *sonames) { size_t i; const char *path, *base; for (i = 0; i < pkg->files.count; i++) { path = pkg->files.items[i]; /* extract basename */ base = strrchr(path, '/'); base = base ? base + 1 : path; /* only shared objects: name.so or name.so.N... */ if (!is_soname(base)) continue; /* avoid duplicates */ if (!strvec_contains(sonames, base)) strvec_push(sonames, base); } } static int run_reverse(pkgvec_t *pkgs, const strvec_t *sys_dirs, int argc, char **argv, int first) { strvec_t sonames; size_t i, j, k; int ret = 0; bool found_any = false; (void)sys_dirs; if (first >= argc) { fprintf(stderr, "revdep: -r requires at least one package\n"); return 1; } strvec_init(&sonames); /* collect .so basenames from all target packages */ for (i = first; i < (size_t)argc; i++) { pkg_t *pkg = pkgvec_find(pkgs, argv[i]); if (!pkg) { fprintf(stderr, "revdep: %s: not found in db\n", argv[i]); ret = 1; continue; } collect_sonames(pkg, &sonames); } if (sonames.count == 0) { msg(V_NORMAL, ">> no shared libraries found in target%s\n", (argc - first) > 1 ? "s" : ""); strvec_free(&sonames); return ret; } msg(V_NORMAL, ">> %zu shared librar%s provided by target%s\n", sonames.count, sonames.count == 1 ? "y" : "ies", (argc - first) > 1 ? "s" : ""); for (i = 0; i < sonames.count; i++) msg(V_FILES, " %s\n", sonames.items[i]); msg(V_NORMAL, ">> scanning %zu packages for dependants\n", pkgs->count); /* scan every package */ for (i = 0; i < pkgs->count; i++) { pkg_t *pkg = pkgs->items[i]; bool depends = false; /* skip if this is one of the target packages */ bool is_target = false; for (j = first; j < (size_t)argc; j++) { if (strcmp(pkg->name, argv[j]) == 0) { is_target = true; break; } } if (is_target || pkg->ignore) continue; /* check each file */ for (j = 0; j < pkg->files.count && !depends; j++) { const char *path = pkg->files.items[j]; elfinfo_t *elf; if (!is_regfile(path)) continue; elf = cache_lookup(path); if (!elf) continue; for (k = 0; k < elf->needed.count; k++) { if (strvec_contains(&sonames, elf->needed.items[k])) { depends = true; msg(V_FILES, " %s: %s needs %s\n", pkg->name, path, elf->needed.items[k]); /* keep scanning files for -vv */ if (verbosity < V_FILES) break; } } } if (depends) { if (verbosity >= V_NORMAL) msg(V_NORMAL, "%s: depends\n", pkg->name); else msg(V_QUIET, "%s\n", pkg->name); found_any = true; } } if (!found_any) msg(V_NORMAL, ">> no dependants found\n"); strvec_free(&sonames); return ret; } /* -------------------------------------------------------------------- */ /* main */ /* -------------------------------------------------------------------- */ int main(int argc, char **argv) { int opt, ret; bool reverse = false; strvec_t ignores; strvec_t sys_dirs; pkgvec_t pkgs; size_t i; strvec_init(&ignores); strvec_init(&sys_dirs); pkgvec_init(&pkgs); /* libelf init */ if (elf_version(EV_CURRENT) == EV_NONE) { fprintf(stderr, "revdep: libelf init failed\n"); return 1; } while ((opt = getopt(argc, argv, "ri:vhV")) != -1) { switch (opt) { case 'r': reverse = true; break; case 'i': strsplit(optarg, ',', &ignores); break; case 'v': if (verbosity < V_DEBUG) verbosity++; break; case 'h': usage(); return 0; case 'V': printf("revdep %s\n", VERSION); return 0; default: fprintf(stderr, "usage: revdep [-r] [-i pkg,...] [-v] " "[package ...]\n"); return 1; } } /* read package database */ if (!db_read(PKG_DB_PATH, &pkgs)) { fprintf(stderr, "revdep: cannot read %s\n", PKG_DB_PATH); ret = 1; goto out; } msg(V_NORMAL, ">> %zu packages in database\n", pkgs.count); /* detect installed Python version for path checks */ py_version = py_installed_version(&pkgs); if (py_version) msg(V_DEBUG, ">> python version: %s\n", py_version); /* detect installed Perl version for path checks */ perl_version = perl_installed_version(&pkgs); if (perl_version) msg(V_DEBUG, ">> perl version: %s\n", perl_version); /* read per-package dirs */ db_read_dirs(REVDEP_DIR, &pkgs); /* mark ignored packages */ for (i = 0; i < ignores.count; i++) { pkg_t *pkg = pkgvec_find(&pkgs, ignores.items[i]); if (pkg) pkg->ignore = true; } /* system library directories — musl's own */ load_sys_path(&sys_dirs); for (i = 0; i < sys_dirs.count; i++) msg(V_DEBUG, ">> system library dir: %s\n", sys_dirs.items[i]); /* dispatch */ if (reverse) ret = run_reverse(&pkgs, &sys_dirs, argc, argv, optind); else ret = run_check(&pkgs, &sys_dirs, argc, argv, optind); out: cache_clear(); free(py_version); free(perl_version); strvec_free(&sys_dirs); strvec_free(&ignores); pkgvec_free(&pkgs); return ret; }