1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
|
/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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;
}
|