Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-ports-kde
Path: blob/main/Tools/scripts/port_conflicts_check.lua
16462 views
1
#!/usr/libexec/flua
2
3
--[[
4
SPDX-License-Identifier: BSD-2-Clause-FreeBSD
5
6
Copyright (c) 2022 Stefan Esser <[email protected]>
7
8
Generate a list of existing and required CONFLICTS_INSTALL lines
9
for all ports (limited to ports for which official packages are
10
provided).
11
12
This script depends on the ports-mgmt/pkg-provides port for the list
13
of files installed by all pre-built packages for the architecture
14
the script is run on.
15
16
The script generates a list of ports by running "pkg provides ." and
17
a mapping from package base name to origin via "pkg rquery '%n %o'".
18
19
The existing CONFLICTS and CONFLICTS_INSTALL definitions are fetched
20
by "make -C $origin -V CONFLICTS -V CONFLICTS_INSTALL". This list is
21
only representative for the options configured for each port (i.e.
22
if non-default options have been selected and registered, these may
23
lead to a non-default list of conflicts).
24
25
The script detects files used by more than one port, than lists by
26
origin the existing definition and the list of package base names
27
that have been detected to cause install conflicts followed by the
28
list of duplicate files separated by a hash character "#".
29
30
This script uses the "hidden" LUA interpreter in the FreeBSD base
31
systems and does not need any port except "pkg-provides" to be run.
32
33
The run-time on my system checking the ~32000 packages available
34
for -CURRENT on amd64 is less than 250 seconds.
35
36
Example output:
37
38
# Port: games/sol
39
# Files: bin/sol
40
# < aisleriot gnome-games
41
# > aisleriot
42
portedit merge -ie 'CONFLICTS_INSTALL=aisleriot # bin/sol' /usr/ports/games/sol
43
44
The output is per port (for all flavors of the port, if applicable),
45
gives examples of conflicting files (mostly to understand whether
46
different versions of a port could co-exist), the current CONFLICTS
47
and CONFLICTS_INSTALL entries merged, and a suggested new entry.
48
This information is followed by a portedit command line that should
49
do the right thing for simple cases, but the result should always
50
be checked before the resulting Makefile is committed.
51
--]]
52
53
require "lfs"
54
55
-------------------------------------------------------------------
56
local file_pattern = "."
57
local database = "/var/db/pkg/provides/provides.db"
58
local max_age = 1 * 24 * 3600 -- maximum age of database file in seconds
59
60
-------------------------------------------------------------------
61
local function table_sorted_keys(t)
62
local result = {}
63
for k, _ in pairs(t) do
64
result[#result + 1] = k
65
end
66
table.sort(result)
67
return result
68
end
69
70
-------------------------------------------------------------------
71
local function table_sort_uniq(t)
72
local result = {}
73
if t then
74
local last
75
table.sort(t)
76
for _, entry in ipairs(t) do
77
if entry ~= last then
78
last = entry
79
result[#result + 1] = entry
80
end
81
end
82
end
83
return result
84
end
85
86
-------------------------------------------------------------------
87
local function fnmatch(name, pattern)
88
local function fnsubst(s)
89
s = string.gsub(s, "%%", "%%%%")
90
s = string.gsub(s, "%+", "%%+")
91
s = string.gsub(s, "%-", "%%-")
92
s = string.gsub(s, "%.", "%%.")
93
s = string.gsub(s, "%?", ".")
94
s = string.gsub(s, "%*", ".*")
95
return s
96
end
97
local rexpr = ""
98
local left, middle, right
99
while true do
100
left, middle, right = string.match(pattern, "([^[]*)(%[[^]]+%])(.*)")
101
if not left then
102
break
103
end
104
rexpr = rexpr .. fnsubst(left) .. middle
105
pattern = right
106
end
107
rexpr = "^" .. rexpr .. fnsubst(pattern) .. "$"
108
return string.find(name, rexpr)
109
end
110
111
-------------------------------------------------------------------
112
local function fetch_pkgs_origins()
113
local pkgs = {}
114
local pipe = io.popen("pkg rquery '%n %o'")
115
for line in pipe:lines() do
116
local pkgbase, origin = string.match(line, "(%S+) (%S+)")
117
pkgs[origin] = pkgbase
118
end
119
pipe:close()
120
pipe = io.popen("pkg rquery '%n %o %At %Av'")
121
for line in pipe:lines() do
122
local pkgbase, origin, tag, value = string.match(line, "(%S+) (%S+) (%S+) (%S+)")
123
if tag == "flavor" then
124
pkgs[origin] = nil
125
pkgs[origin .. "@" .. value] = pkgbase
126
end
127
end
128
pipe:close()
129
return pkgs
130
end
131
132
-------------------------------------------------------------------
133
local BAD_FILE_PATTERN = {
134
"^[^/]+$",
135
"^lib/python[%d%.]+/site%-packages/examples/[^/]+$",
136
"^lib/python[%d%.]+/site%-packages/samples/[^/]+$",
137
"^lib/python[%d%.]+/site%-packages/test/[^/]+$",
138
"^lib/python[%d%.]+/site%-packages/test_app/[^/]+$",
139
"^lib/python[%d%.]+/site%-packages/tests/[^/]+$",
140
"^lib/python[%d%.]+/site%-packages/tests/unit/[^/]+$",
141
}
142
143
local BAD_FILE_PKGS = {}
144
145
local function check_bad_file(pkgbase, file)
146
for _, pattern in ipairs(BAD_FILE_PATTERN) do
147
if string.match(file, pattern) then
148
BAD_FILE_PKGS[pkgbase] = BAD_FILE_PKGS[pkgbase] or {}
149
table.insert(BAD_FILE_PKGS[pkgbase], file)
150
break
151
end
152
end
153
end
154
155
-------------------------------------------------------------------
156
local function read_files(pattern)
157
local files_table = {}
158
local now = os.time()
159
local modification_time = lfs.attributes(database, "modification")
160
if not modification_time then
161
print("# Aborting: package file database " .. database .. " does not exist.")
162
print("# Install the 'pkg-provides' package and add it as a module to 'pkg.conf'.")
163
print("# Then fetch the database with 'pkg update' or 'pkg provides -u'.")
164
os.exit(1)
165
end
166
if now - modification_time > max_age then
167
print("# Aborting: package file database " .. database)
168
print("# is older than " .. max_age .. " seconds.")
169
print("# Use 'pkg provides -u' to update the database.")
170
os.exit(2)
171
end
172
local pipe = io.popen("locate -d " .. database .. " " .. pattern)
173
if pipe then
174
for line in pipe:lines() do
175
local pkgbase, file = string.match(line, "([^*]+)%*([^*]+)")
176
if file:sub(1, 11) == "/usr/local/" then
177
file = file:sub(12)
178
end
179
check_bad_file(pkgbase, file)
180
local t = files_table[file] or {}
181
t[#t + 1] = pkgbase
182
files_table[file] = t
183
end
184
pipe:close()
185
end
186
return files_table
187
end
188
189
-------------------------------------------------------------------
190
local DUPLICATE_FILE = {}
191
192
local function fetch_pkg_pairs(pattern)
193
local pkg_pairs = {}
194
for file, pkgbases in pairs(read_files(pattern)) do
195
if #pkgbases >= 2 then
196
DUPLICATE_FILE[file] = true
197
for i = 1, #pkgbases -1 do
198
local pkg_i = pkgbases[i]
199
for j = i + 1, #pkgbases do
200
local pkg_j = pkgbases[j]
201
if pkg_i ~= pkg_j then
202
local p1 = pkg_pairs[pkg_i] or {}
203
local p2 = p1[pkg_j] or {}
204
p2[#p2 + 1] = file
205
p1[pkg_j] = p2
206
pkg_pairs[pkg_i] = p1
207
end
208
end
209
end
210
end
211
end
212
return pkg_pairs
213
end
214
215
-------------------------------------------------------------------
216
local function conflicts_delta(old, new)
217
local old_seen = {}
218
local changed
219
for i = 1, #new do
220
local matched
221
for j = 1, #old do
222
if new[i] == old[j] or fnmatch(new[i], old[j]) then
223
new[i] = old[j]
224
old_seen[j] = true
225
matched = true
226
break
227
end
228
end
229
changed = changed or not matched
230
end
231
if not changed then
232
for j = 1, #old do
233
if not old_seen[j] then
234
changed = true
235
break
236
end
237
end
238
end
239
if changed then
240
return table_sort_uniq(new)
241
end
242
end
243
244
-------------------------------------------------------------------
245
local function fetch_port_conflicts(origin)
246
local dir, flavor = origin:match("([^@]+)@?(.*)")
247
if flavor ~= "" then
248
flavor = " FLAVOR=" .. flavor
249
end
250
local seen = {}
251
local portdir = "/usr/ports/" .. dir
252
local pipe = io.popen("make -C " .. portdir .. flavor .. " -V CONFLICTS -V CONFLICTS_INSTALL 2>/dev/null")
253
for line in pipe:lines() do
254
for word in line:gmatch("(%S+)%s?") do
255
seen[word] = true
256
end
257
end
258
pipe:close()
259
return table_sorted_keys(seen)
260
end
261
262
-------------------------------------------------------------------
263
local function conflicting_pkgs(conflicting)
264
local pkgs = {}
265
for origin, pkgbase in pairs(fetch_pkgs_origins()) do
266
if conflicting[pkgbase] then
267
pkgs[origin] = pkgbase
268
end
269
end
270
return pkgs
271
end
272
273
-------------------------------------------------------------------
274
local function collect_conflicts(pkg_pairs)
275
local pkgs = {}
276
for pkg_i, p1 in pairs(pkg_pairs) do
277
for pkg_j, _ in pairs(p1) do
278
pkgs[pkg_i] = pkgs[pkg_i] or {}
279
table.insert(pkgs[pkg_i], pkg_j)
280
pkgs[pkg_j] = pkgs[pkg_j] or {}
281
table.insert(pkgs[pkg_j], pkg_i)
282
end
283
end
284
return pkgs
285
end
286
287
-------------------------------------------------------------------
288
local function split_origins(origin_list)
289
local port_list = {}
290
local flavors = {}
291
local last_port
292
for _, origin in ipairs(origin_list) do
293
local port, flavor = string.match(origin, "([^@]+)@?(.*)")
294
if port ~= last_port then
295
port_list[#port_list + 1] = port
296
if flavor ~= "" then
297
flavors[port] = {flavor}
298
end
299
else
300
table.insert(flavors[port], flavor)
301
end
302
last_port = port
303
end
304
return port_list, flavors
305
end
306
307
-------------------------------------------------------------------
308
local PKG_PAIR_FILES = fetch_pkg_pairs(file_pattern)
309
local CONFLICT_PKGS = collect_conflicts(PKG_PAIR_FILES)
310
local PKGBASE = conflicting_pkgs(CONFLICT_PKGS)
311
local ORIGIN_LIST = table_sorted_keys(PKGBASE)
312
local PORT_LIST, FLAVORS = split_origins(ORIGIN_LIST)
313
314
local function conflicting_files(pkg_i, pkgs)
315
local files = {}
316
local all_files = {}
317
local f
318
local p1 = PKG_PAIR_FILES[pkg_i]
319
if p1 then
320
for _, pkg_j in ipairs(pkgs) do
321
f = p1[pkg_j]
322
if f then
323
table.sort(f)
324
files[#files + 1] = f[1]
325
table.move(f, 1, #f, #all_files + 1, all_files)
326
end
327
end
328
end
329
for _, pkg_j in ipairs(pkgs) do
330
p1 = PKG_PAIR_FILES[pkg_j]
331
f = p1 and p1[pkg_i]
332
if f then
333
table.sort(f)
334
files[#files + 1] = f[1]
335
table.move(f, 1, #f, #all_files + 1, all_files)
336
end
337
end
338
return table_sort_uniq(files), table_sort_uniq(all_files)
339
end
340
341
---------------------------------------------------------------------
342
local version_pattern = {
343
"^lib/python%d%.%d/",
344
"^share/py3%d%d%?-",
345
"^share/%a+/py3%d%d%?-",
346
"^lib/lua/%d%.%d/",
347
"^share/lua/%d%.%d/",
348
"^lib/perl5/[%d%.]+/",
349
"^lib/perl5/site_perl/mach/[%d%.]+/",
350
"^lib/ruby/gems/%d%.%d/",
351
"^lib/ruby/site_ruby/%d%.%d/",
352
}
353
354
local function generalize_patterns(pkgs, files)
355
local function match_any(str, pattern_array)
356
for _, pattern in ipairs(pattern_array) do
357
if string.match(str, pattern) then
358
return true
359
end
360
end
361
return false
362
end
363
local function unversioned_files()
364
for i = 1, #files do
365
if not match_any(files[i], version_pattern) then
366
return true
367
end
368
end
369
return false
370
end
371
local function pkg_wildcards(from, ...)
372
local to_list = {...}
373
local result = {}
374
for i = 1, #pkgs do
375
local orig_pkg = pkgs[i]
376
for _, to in ipairs(to_list) do
377
result[string.gsub(orig_pkg, from, to)] = true
378
end
379
end
380
pkgs = table_sorted_keys(result)
381
end
382
local pkg_pfx_php = "php[0-9][0-9]-"
383
local pkg_sfx_php = "-php[0-9][0-9]"
384
local pkg_pfx_python2
385
local pkg_sfx_python2
386
local pkg_pfx_python3
387
local pkg_sfx_python3
388
local pkg_pfx_lua
389
local pkg_pfx_ruby
390
pkgs = table_sort_uniq(pkgs)
391
if unversioned_files() then
392
pkg_pfx_python2 = "py3[0-9]-" -- e.g. py39-
393
pkg_sfx_python2 = "-py3[0-9]"
394
pkg_pfx_python3 = "py3[0-9][0-9]-" -- e.g. py311-
395
pkg_sfx_python3 = "-py3[0-9][0-9]"
396
pkg_pfx_lua = "lua[0-9][0-9]-"
397
pkg_pfx_ruby = "ruby[0-9][0-9]-"
398
else
399
pkg_pfx_python2 = "${PYTHON_PKGNAMEPREFIX}"
400
pkg_sfx_python2 = "${PYTHON_PKGNAMESUFFIX}"
401
pkg_pfx_python3 = nil
402
pkg_sfx_python3 = nil
403
pkg_pfx_lua = "${LUA_PKGNAMEPREFIX}"
404
pkg_pfx_ruby = "${RUBY_PKGNAMEPREFIX}"
405
end
406
pkg_wildcards("^php%d%d%-", pkg_pfx_php)
407
pkg_wildcards("-php%d%d$", pkg_sfx_php)
408
pkg_wildcards("^phpunit%d%-", "phpunit[0-9]-")
409
pkg_wildcards("^py3%d%-", pkg_pfx_python2, pkg_pfx_python3)
410
pkg_wildcards("-py3%d%$", pkg_sfx_python2, pkg_sfx_python3)
411
pkg_wildcards("^py3%d%d%-", pkg_pfx_python2, pkg_pfx_python3)
412
pkg_wildcards("-py3%d%d%$", pkg_sfx_python2, pkg_sfx_python3)
413
pkg_wildcards("^lua%d%d%-", pkg_pfx_lua)
414
pkg_wildcards("-emacs_[%a_]*", "-emacs_*")
415
pkg_wildcards("^ghostscript%d%-", "ghostscript[0-9]-")
416
pkg_wildcards("^bacula%d%-", "bacula[0-9]-")
417
pkg_wildcards("^bacula%d%d%-", "bacula[0-9][0-9]-")
418
pkg_wildcards("^bareos%d%-", "bareos[0-9]-")
419
pkg_wildcards("^bareos%d%d%-", "bareos[0-9][0-9]-")
420
pkg_wildcards("^moosefs%d%-", "moosefs[0-9]-")
421
pkg_wildcards("^ruby%d+-", pkg_pfx_ruby)
422
return table_sort_uniq(pkgs)
423
end
424
425
-------------------------------------------------------------------
426
for _, port in ipairs(PORT_LIST) do
427
local function merge_table(t1, t2)
428
table.move(t2, 1, #t2, #t1 + 1, t1)
429
end
430
local port_conflicts = {}
431
local files = {}
432
local msg_files = {}
433
local conflict_pkgs = {}
434
local function merge_data(origin)
435
local pkgbase = PKGBASE[origin]
436
if not BAD_FILE_PKGS[pkgbase] then
437
local pkg_confl_file1, pkg_confl_files = conflicting_files(pkgbase, CONFLICT_PKGS[pkgbase])
438
merge_table(msg_files, pkg_confl_file1) -- 1 file per flavor
439
merge_table(files, pkg_confl_files) -- all conflicting files
440
merge_table(conflict_pkgs, CONFLICT_PKGS[pkgbase])
441
merge_table(port_conflicts, fetch_port_conflicts(origin))
442
end
443
end
444
local flavors = FLAVORS[port]
445
if flavors then
446
for _, flavor in ipairs(flavors) do
447
merge_data(port .. "@" .. flavor)
448
end
449
else
450
merge_data(port)
451
end
452
files = table_sort_uniq(files)
453
msg_files = table_sort_uniq(msg_files)
454
conflict_pkgs = generalize_patterns(conflict_pkgs, files)
455
if #port_conflicts then
456
port_conflicts = table_sort_uniq(port_conflicts)
457
conflict_pkgs = conflicts_delta(port_conflicts, conflict_pkgs)
458
end
459
if conflict_pkgs then
460
local conflicts_string_cur = table.concat(port_conflicts, " ")
461
local conflicts_string_new = table.concat(conflict_pkgs, " ")
462
local file_list = table.concat(msg_files, " ")
463
print("# Port: " .. port)
464
print("# Files: " .. file_list)
465
if conflicts_string_cur ~= "" then
466
print("# < " .. conflicts_string_cur)
467
end
468
print("# > " .. conflicts_string_new)
469
print("portedit merge -ie 'CONFLICTS_INSTALL=" .. conflicts_string_new ..
470
" # " .. file_list .. "' /usr/ports/" .. port)
471
print()
472
end
473
end
474
475
-------------------------------------------------------------------
476
local BAD_FILES_ORIGINS = {}
477
478
for _, origin in ipairs(ORIGIN_LIST) do
479
local pkgbase = PKGBASE[origin]
480
local files = BAD_FILE_PKGS[pkgbase]
481
if files then
482
for _, file in ipairs(files) do
483
if DUPLICATE_FILE[file] then
484
local port = string.match(origin, "([^@]+)@?")
485
BAD_FILES_ORIGINS[port] = BAD_FILES_ORIGINS[origin] or {}
486
table.insert(BAD_FILES_ORIGINS[port], file)
487
end
488
end
489
end
490
end
491
492
-------------------------------------------------------------------
493
local bad_origins = table_sorted_keys(BAD_FILES_ORIGINS)
494
495
if #bad_origins > 0 then
496
print ("# Ports with badly named files:")
497
print ()
498
for _, port in ipairs(bad_origins) do
499
print ("# " .. port)
500
local files = BAD_FILES_ORIGINS[port]
501
table.sort(files)
502
for _, file in ipairs(files) do
503
print ("#", file)
504
end
505
print()
506
end
507
end
508
509