Path: blob/main/Tools/scripts/port_conflicts_check.lua
16462 views
#!/usr/libexec/flua12--[[3SPDX-License-Identifier: BSD-2-Clause-FreeBSD45Copyright (c) 2022 Stefan Esser <[email protected]>67Generate a list of existing and required CONFLICTS_INSTALL lines8for all ports (limited to ports for which official packages are9provided).1011This script depends on the ports-mgmt/pkg-provides port for the list12of files installed by all pre-built packages for the architecture13the script is run on.1415The script generates a list of ports by running "pkg provides ." and16a mapping from package base name to origin via "pkg rquery '%n %o'".1718The existing CONFLICTS and CONFLICTS_INSTALL definitions are fetched19by "make -C $origin -V CONFLICTS -V CONFLICTS_INSTALL". This list is20only representative for the options configured for each port (i.e.21if non-default options have been selected and registered, these may22lead to a non-default list of conflicts).2324The script detects files used by more than one port, than lists by25origin the existing definition and the list of package base names26that have been detected to cause install conflicts followed by the27list of duplicate files separated by a hash character "#".2829This script uses the "hidden" LUA interpreter in the FreeBSD base30systems and does not need any port except "pkg-provides" to be run.3132The run-time on my system checking the ~32000 packages available33for -CURRENT on amd64 is less than 250 seconds.3435Example output:3637# Port: games/sol38# Files: bin/sol39# < aisleriot gnome-games40# > aisleriot41portedit merge -ie 'CONFLICTS_INSTALL=aisleriot # bin/sol' /usr/ports/games/sol4243The output is per port (for all flavors of the port, if applicable),44gives examples of conflicting files (mostly to understand whether45different versions of a port could co-exist), the current CONFLICTS46and CONFLICTS_INSTALL entries merged, and a suggested new entry.47This information is followed by a portedit command line that should48do the right thing for simple cases, but the result should always49be checked before the resulting Makefile is committed.50--]]5152require "lfs"5354-------------------------------------------------------------------55local file_pattern = "."56local database = "/var/db/pkg/provides/provides.db"57local max_age = 1 * 24 * 3600 -- maximum age of database file in seconds5859-------------------------------------------------------------------60local function table_sorted_keys(t)61local result = {}62for k, _ in pairs(t) do63result[#result + 1] = k64end65table.sort(result)66return result67end6869-------------------------------------------------------------------70local function table_sort_uniq(t)71local result = {}72if t then73local last74table.sort(t)75for _, entry in ipairs(t) do76if entry ~= last then77last = entry78result[#result + 1] = entry79end80end81end82return result83end8485-------------------------------------------------------------------86local function fnmatch(name, pattern)87local function fnsubst(s)88s = string.gsub(s, "%%", "%%%%")89s = string.gsub(s, "%+", "%%+")90s = string.gsub(s, "%-", "%%-")91s = string.gsub(s, "%.", "%%.")92s = string.gsub(s, "%?", ".")93s = string.gsub(s, "%*", ".*")94return s95end96local rexpr = ""97local left, middle, right98while true do99left, middle, right = string.match(pattern, "([^[]*)(%[[^]]+%])(.*)")100if not left then101break102end103rexpr = rexpr .. fnsubst(left) .. middle104pattern = right105end106rexpr = "^" .. rexpr .. fnsubst(pattern) .. "$"107return string.find(name, rexpr)108end109110-------------------------------------------------------------------111local function fetch_pkgs_origins()112local pkgs = {}113local pipe = io.popen("pkg rquery '%n %o'")114for line in pipe:lines() do115local pkgbase, origin = string.match(line, "(%S+) (%S+)")116pkgs[origin] = pkgbase117end118pipe:close()119pipe = io.popen("pkg rquery '%n %o %At %Av'")120for line in pipe:lines() do121local pkgbase, origin, tag, value = string.match(line, "(%S+) (%S+) (%S+) (%S+)")122if tag == "flavor" then123pkgs[origin] = nil124pkgs[origin .. "@" .. value] = pkgbase125end126end127pipe:close()128return pkgs129end130131-------------------------------------------------------------------132local BAD_FILE_PATTERN = {133"^[^/]+$",134"^lib/python[%d%.]+/site%-packages/examples/[^/]+$",135"^lib/python[%d%.]+/site%-packages/samples/[^/]+$",136"^lib/python[%d%.]+/site%-packages/test/[^/]+$",137"^lib/python[%d%.]+/site%-packages/test_app/[^/]+$",138"^lib/python[%d%.]+/site%-packages/tests/[^/]+$",139"^lib/python[%d%.]+/site%-packages/tests/unit/[^/]+$",140}141142local BAD_FILE_PKGS = {}143144local function check_bad_file(pkgbase, file)145for _, pattern in ipairs(BAD_FILE_PATTERN) do146if string.match(file, pattern) then147BAD_FILE_PKGS[pkgbase] = BAD_FILE_PKGS[pkgbase] or {}148table.insert(BAD_FILE_PKGS[pkgbase], file)149break150end151end152end153154-------------------------------------------------------------------155local function read_files(pattern)156local files_table = {}157local now = os.time()158local modification_time = lfs.attributes(database, "modification")159if not modification_time then160print("# Aborting: package file database " .. database .. " does not exist.")161print("# Install the 'pkg-provides' package and add it as a module to 'pkg.conf'.")162print("# Then fetch the database with 'pkg update' or 'pkg provides -u'.")163os.exit(1)164end165if now - modification_time > max_age then166print("# Aborting: package file database " .. database)167print("# is older than " .. max_age .. " seconds.")168print("# Use 'pkg provides -u' to update the database.")169os.exit(2)170end171local pipe = io.popen("locate -d " .. database .. " " .. pattern)172if pipe then173for line in pipe:lines() do174local pkgbase, file = string.match(line, "([^*]+)%*([^*]+)")175if file:sub(1, 11) == "/usr/local/" then176file = file:sub(12)177end178check_bad_file(pkgbase, file)179local t = files_table[file] or {}180t[#t + 1] = pkgbase181files_table[file] = t182end183pipe:close()184end185return files_table186end187188-------------------------------------------------------------------189local DUPLICATE_FILE = {}190191local function fetch_pkg_pairs(pattern)192local pkg_pairs = {}193for file, pkgbases in pairs(read_files(pattern)) do194if #pkgbases >= 2 then195DUPLICATE_FILE[file] = true196for i = 1, #pkgbases -1 do197local pkg_i = pkgbases[i]198for j = i + 1, #pkgbases do199local pkg_j = pkgbases[j]200if pkg_i ~= pkg_j then201local p1 = pkg_pairs[pkg_i] or {}202local p2 = p1[pkg_j] or {}203p2[#p2 + 1] = file204p1[pkg_j] = p2205pkg_pairs[pkg_i] = p1206end207end208end209end210end211return pkg_pairs212end213214-------------------------------------------------------------------215local function conflicts_delta(old, new)216local old_seen = {}217local changed218for i = 1, #new do219local matched220for j = 1, #old do221if new[i] == old[j] or fnmatch(new[i], old[j]) then222new[i] = old[j]223old_seen[j] = true224matched = true225break226end227end228changed = changed or not matched229end230if not changed then231for j = 1, #old do232if not old_seen[j] then233changed = true234break235end236end237end238if changed then239return table_sort_uniq(new)240end241end242243-------------------------------------------------------------------244local function fetch_port_conflicts(origin)245local dir, flavor = origin:match("([^@]+)@?(.*)")246if flavor ~= "" then247flavor = " FLAVOR=" .. flavor248end249local seen = {}250local portdir = "/usr/ports/" .. dir251local pipe = io.popen("make -C " .. portdir .. flavor .. " -V CONFLICTS -V CONFLICTS_INSTALL 2>/dev/null")252for line in pipe:lines() do253for word in line:gmatch("(%S+)%s?") do254seen[word] = true255end256end257pipe:close()258return table_sorted_keys(seen)259end260261-------------------------------------------------------------------262local function conflicting_pkgs(conflicting)263local pkgs = {}264for origin, pkgbase in pairs(fetch_pkgs_origins()) do265if conflicting[pkgbase] then266pkgs[origin] = pkgbase267end268end269return pkgs270end271272-------------------------------------------------------------------273local function collect_conflicts(pkg_pairs)274local pkgs = {}275for pkg_i, p1 in pairs(pkg_pairs) do276for pkg_j, _ in pairs(p1) do277pkgs[pkg_i] = pkgs[pkg_i] or {}278table.insert(pkgs[pkg_i], pkg_j)279pkgs[pkg_j] = pkgs[pkg_j] or {}280table.insert(pkgs[pkg_j], pkg_i)281end282end283return pkgs284end285286-------------------------------------------------------------------287local function split_origins(origin_list)288local port_list = {}289local flavors = {}290local last_port291for _, origin in ipairs(origin_list) do292local port, flavor = string.match(origin, "([^@]+)@?(.*)")293if port ~= last_port then294port_list[#port_list + 1] = port295if flavor ~= "" then296flavors[port] = {flavor}297end298else299table.insert(flavors[port], flavor)300end301last_port = port302end303return port_list, flavors304end305306-------------------------------------------------------------------307local PKG_PAIR_FILES = fetch_pkg_pairs(file_pattern)308local CONFLICT_PKGS = collect_conflicts(PKG_PAIR_FILES)309local PKGBASE = conflicting_pkgs(CONFLICT_PKGS)310local ORIGIN_LIST = table_sorted_keys(PKGBASE)311local PORT_LIST, FLAVORS = split_origins(ORIGIN_LIST)312313local function conflicting_files(pkg_i, pkgs)314local files = {}315local all_files = {}316local f317local p1 = PKG_PAIR_FILES[pkg_i]318if p1 then319for _, pkg_j in ipairs(pkgs) do320f = p1[pkg_j]321if f then322table.sort(f)323files[#files + 1] = f[1]324table.move(f, 1, #f, #all_files + 1, all_files)325end326end327end328for _, pkg_j in ipairs(pkgs) do329p1 = PKG_PAIR_FILES[pkg_j]330f = p1 and p1[pkg_i]331if f then332table.sort(f)333files[#files + 1] = f[1]334table.move(f, 1, #f, #all_files + 1, all_files)335end336end337return table_sort_uniq(files), table_sort_uniq(all_files)338end339340---------------------------------------------------------------------341local version_pattern = {342"^lib/python%d%.%d/",343"^share/py3%d%d%?-",344"^share/%a+/py3%d%d%?-",345"^lib/lua/%d%.%d/",346"^share/lua/%d%.%d/",347"^lib/perl5/[%d%.]+/",348"^lib/perl5/site_perl/mach/[%d%.]+/",349"^lib/ruby/gems/%d%.%d/",350"^lib/ruby/site_ruby/%d%.%d/",351}352353local function generalize_patterns(pkgs, files)354local function match_any(str, pattern_array)355for _, pattern in ipairs(pattern_array) do356if string.match(str, pattern) then357return true358end359end360return false361end362local function unversioned_files()363for i = 1, #files do364if not match_any(files[i], version_pattern) then365return true366end367end368return false369end370local function pkg_wildcards(from, ...)371local to_list = {...}372local result = {}373for i = 1, #pkgs do374local orig_pkg = pkgs[i]375for _, to in ipairs(to_list) do376result[string.gsub(orig_pkg, from, to)] = true377end378end379pkgs = table_sorted_keys(result)380end381local pkg_pfx_php = "php[0-9][0-9]-"382local pkg_sfx_php = "-php[0-9][0-9]"383local pkg_pfx_python2384local pkg_sfx_python2385local pkg_pfx_python3386local pkg_sfx_python3387local pkg_pfx_lua388local pkg_pfx_ruby389pkgs = table_sort_uniq(pkgs)390if unversioned_files() then391pkg_pfx_python2 = "py3[0-9]-" -- e.g. py39-392pkg_sfx_python2 = "-py3[0-9]"393pkg_pfx_python3 = "py3[0-9][0-9]-" -- e.g. py311-394pkg_sfx_python3 = "-py3[0-9][0-9]"395pkg_pfx_lua = "lua[0-9][0-9]-"396pkg_pfx_ruby = "ruby[0-9][0-9]-"397else398pkg_pfx_python2 = "${PYTHON_PKGNAMEPREFIX}"399pkg_sfx_python2 = "${PYTHON_PKGNAMESUFFIX}"400pkg_pfx_python3 = nil401pkg_sfx_python3 = nil402pkg_pfx_lua = "${LUA_PKGNAMEPREFIX}"403pkg_pfx_ruby = "${RUBY_PKGNAMEPREFIX}"404end405pkg_wildcards("^php%d%d%-", pkg_pfx_php)406pkg_wildcards("-php%d%d$", pkg_sfx_php)407pkg_wildcards("^phpunit%d%-", "phpunit[0-9]-")408pkg_wildcards("^py3%d%-", pkg_pfx_python2, pkg_pfx_python3)409pkg_wildcards("-py3%d%$", pkg_sfx_python2, pkg_sfx_python3)410pkg_wildcards("^py3%d%d%-", pkg_pfx_python2, pkg_pfx_python3)411pkg_wildcards("-py3%d%d%$", pkg_sfx_python2, pkg_sfx_python3)412pkg_wildcards("^lua%d%d%-", pkg_pfx_lua)413pkg_wildcards("-emacs_[%a_]*", "-emacs_*")414pkg_wildcards("^ghostscript%d%-", "ghostscript[0-9]-")415pkg_wildcards("^bacula%d%-", "bacula[0-9]-")416pkg_wildcards("^bacula%d%d%-", "bacula[0-9][0-9]-")417pkg_wildcards("^bareos%d%-", "bareos[0-9]-")418pkg_wildcards("^bareos%d%d%-", "bareos[0-9][0-9]-")419pkg_wildcards("^moosefs%d%-", "moosefs[0-9]-")420pkg_wildcards("^ruby%d+-", pkg_pfx_ruby)421return table_sort_uniq(pkgs)422end423424-------------------------------------------------------------------425for _, port in ipairs(PORT_LIST) do426local function merge_table(t1, t2)427table.move(t2, 1, #t2, #t1 + 1, t1)428end429local port_conflicts = {}430local files = {}431local msg_files = {}432local conflict_pkgs = {}433local function merge_data(origin)434local pkgbase = PKGBASE[origin]435if not BAD_FILE_PKGS[pkgbase] then436local pkg_confl_file1, pkg_confl_files = conflicting_files(pkgbase, CONFLICT_PKGS[pkgbase])437merge_table(msg_files, pkg_confl_file1) -- 1 file per flavor438merge_table(files, pkg_confl_files) -- all conflicting files439merge_table(conflict_pkgs, CONFLICT_PKGS[pkgbase])440merge_table(port_conflicts, fetch_port_conflicts(origin))441end442end443local flavors = FLAVORS[port]444if flavors then445for _, flavor in ipairs(flavors) do446merge_data(port .. "@" .. flavor)447end448else449merge_data(port)450end451files = table_sort_uniq(files)452msg_files = table_sort_uniq(msg_files)453conflict_pkgs = generalize_patterns(conflict_pkgs, files)454if #port_conflicts then455port_conflicts = table_sort_uniq(port_conflicts)456conflict_pkgs = conflicts_delta(port_conflicts, conflict_pkgs)457end458if conflict_pkgs then459local conflicts_string_cur = table.concat(port_conflicts, " ")460local conflicts_string_new = table.concat(conflict_pkgs, " ")461local file_list = table.concat(msg_files, " ")462print("# Port: " .. port)463print("# Files: " .. file_list)464if conflicts_string_cur ~= "" then465print("# < " .. conflicts_string_cur)466end467print("# > " .. conflicts_string_new)468print("portedit merge -ie 'CONFLICTS_INSTALL=" .. conflicts_string_new ..469" # " .. file_list .. "' /usr/ports/" .. port)470print()471end472end473474-------------------------------------------------------------------475local BAD_FILES_ORIGINS = {}476477for _, origin in ipairs(ORIGIN_LIST) do478local pkgbase = PKGBASE[origin]479local files = BAD_FILE_PKGS[pkgbase]480if files then481for _, file in ipairs(files) do482if DUPLICATE_FILE[file] then483local port = string.match(origin, "([^@]+)@?")484BAD_FILES_ORIGINS[port] = BAD_FILES_ORIGINS[origin] or {}485table.insert(BAD_FILES_ORIGINS[port], file)486end487end488end489end490491-------------------------------------------------------------------492local bad_origins = table_sorted_keys(BAD_FILES_ORIGINS)493494if #bad_origins > 0 then495print ("# Ports with badly named files:")496print ()497for _, port in ipairs(bad_origins) do498print ("# " .. port)499local files = BAD_FILES_ORIGINS[port]500table.sort(files)501for _, file in ipairs(files) do502print ("#", file)503end504print()505end506end507508509