/* * revdep - reverse dependency checker for rawnix * lang.c - detect packages with stale Python versioned paths * * Copyright (c) 2025 zorz@gmx.com * Licensed under the BSD 2-Clause License. See revdep.h. * * On a Python major version bump (e.g. 3.13 → 3.14), packages that * installed files into /usr/lib/python3.13/ become silently broken: * their .py and .so files are unreachable at the new path. ELF * inspection cannot detect this because Python extension modules are * dlopen()'d at runtime using constructed path strings, not DT_NEEDED. * * Strategy: * 1. Find the installed "python3" package in the db and parse its * major.minor version string (e.g. "3.14" from "3.14.2"). * 2. For every file owned by every other package, check whether the * path contains /usr/lib/pythonX.Y/ where X.Y ≠ installed version. * 3. Report those packages as broken. */ #define _POSIX_C_SOURCE 200809L #include "revdep.h" #include #include #include /* prefix we scan for */ #define PY_PREFIX "/usr/lib/python" #define PY_PREFIX_LEN (sizeof(PY_PREFIX) - 1) /* * py_installed_version() * * Walk pkgs looking for a package named "python3". * Return a newly allocated "X.Y" string, or NULL if not found. */ char * py_installed_version(const pkgvec_t *pkgs) { size_t i; const char *ver; char *up, *dot2; char buf[32]; size_t len; if (!pkgs) return NULL; for (i = 0; i < pkgs->count; i++) { if (strcmp(pkgs->items[i]->name, "python3") != 0) continue; /* the db has "3.14.2-1": version-release */ up = pkg_upstream_version(pkgs->items[i]->version); if (!up) return NULL; ver = up; /* copy up to the second dot: "3.14.2" → "3.14" */ dot2 = strchr(ver, '.'); if (!dot2) return up; /* no dot: use as-is */ dot2 = strchr(dot2 + 1, '.'); if (dot2) len = (size_t)(dot2 - ver); else len = strlen(ver); if (len >= sizeof(buf)) len = sizeof(buf) - 1; memcpy(buf, ver, len); buf[len] = '\0'; free(up); return xstrdup(buf); } return NULL; /* python3 not installed */ } /* * py_check_file() * * Return true if `path` is under /usr/lib/pythonX.Y/ and X.Y * does NOT match `installed`. * * Examples: * path=/usr/lib/python3.13/site-packages/xcbgen/__init__.py * installed="3.14" → stale, return true * * path=/usr/lib/python3.14/site-packages/foo.py * installed="3.14" → current, return false */ static bool py_check_file(const char *path, const char *installed) { const char *p, *slash; size_t verlen; char pathver[32]; if (!path || !installed) return false; /* fast prefix check */ if (strncmp(path, PY_PREFIX, PY_PREFIX_LEN) != 0) return false; /* p now points to the version string in the path */ p = path + PY_PREFIX_LEN; /* find the next slash — that delimits the version component */ slash = strchr(p, '/'); if (!slash) return false; verlen = (size_t)(slash - p); if (verlen == 0 || verlen >= sizeof(pathver)) return false; memcpy(pathver, p, verlen); pathver[verlen] = '\0'; /* stale if version differs from installed */ return strcmp(pathver, installed) != 0; } /* * py_check_package() * * Returns true if the package has at least one file under a stale * /usr/lib/pythonX.Y/ directory. Prints details at appropriate * verbosity levels (matching the ELF checker style). */ bool py_check_package(const pkg_t *pkg, const char *installed) { size_t i; bool broken = false; if (!pkg || !installed || pkg->ignore) return false; for (i = 0; i < pkg->files.count; i++) { const char *path = pkg->files.items[i]; if (!py_check_file(path, installed)) continue; if (!broken) { msg(V_FILES, " %s: stale python path\n", pkg->name); broken = true; } msg(V_DETAIL, " %s: %s (expects python%s)\n", pkg->name, path, installed); } return broken; } /* -------------------------------------------------------------------- */ /* Perl versioned path checker */ /* */ /* Same problem as Python: on a Perl major version bump, packages */ /* with files under /usr/lib/perl5/site_perl/5.X.Y/ become stale. */ /* -------------------------------------------------------------------- */ #define PERL_PREFIX "/usr/lib/perl5/site_perl/" #define PERL_PREFIX_LEN (sizeof(PERL_PREFIX) - 1) /* * perl_installed_version() * * Find the "perl" package in the db and return its version string * (e.g. "5.40.0"). The caller should compare the full X.Y.Z since * Perl ABI paths include all three components. */ char * perl_installed_version(const pkgvec_t *pkgs) { size_t i; if (!pkgs) return NULL; for (i = 0; i < pkgs->count; i++) { if (strcmp(pkgs->items[i]->name, "perl") != 0) continue; /* the db has "5.40.1-1"; site_perl uses "5.40.1" */ return pkg_upstream_version(pkgs->items[i]->version); } return NULL; } /* * perl_check_file() * * Return true if path is under /usr/lib/perl5/site_perl/X.Y.Z/ and * X.Y.Z does NOT match installed. */ static bool perl_check_file(const char *path, const char *installed) { const char *p, *slash; size_t verlen; char pathver[32]; if (!path || !installed) return false; if (strncmp(path, PERL_PREFIX, PERL_PREFIX_LEN) != 0) return false; p = path + PERL_PREFIX_LEN; slash = strchr(p, '/'); if (!slash) return false; verlen = (size_t)(slash - p); if (verlen == 0 || verlen >= sizeof(pathver)) return false; memcpy(pathver, p, verlen); pathver[verlen] = '\0'; return strcmp(pathver, installed) != 0; } /* * perl_check_package() * * Returns true if the package has at least one file under a stale * /usr/lib/perl5/site_perl/X.Y.Z/ directory. */ bool perl_check_package(const pkg_t *pkg, const char *installed) { size_t i; bool broken = false; if (!pkg || !installed || pkg->ignore) return false; for (i = 0; i < pkg->files.count; i++) { const char *path = pkg->files.items[i]; if (!perl_check_file(path, installed)) continue; if (!broken) { msg(V_FILES, " %s: stale perl path\n", pkg->name); broken = true; } msg(V_DETAIL, " %s: %s (expects perl%s)\n", pkg->name, path, installed); } return broken; }