243 lines
7.4 KiB
Python
Executable File
243 lines
7.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (C) 2023 Umorpha Systems
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
from typing import Callable, Literal, NamedTuple, Optional, TypeAlias, cast
|
|
|
|
import pyalpm
|
|
from pycman import config
|
|
|
|
__version__ = "20231024"
|
|
|
|
################################################################################
|
|
|
|
# pyalpm doesn't expose the alpm_find_dbs_satisfier() function, so we
|
|
# replicate it here. Refer to deps.c in pacman.git.
|
|
|
|
alpm_depmod_t: TypeAlias = Literal[None, "<", "<=", "=", ">=", ">"]
|
|
|
|
|
|
class alpm_depend_t(NamedTuple):
|
|
name: str
|
|
version: Optional[str]
|
|
desc: Optional[str]
|
|
mod: alpm_depmod_t
|
|
|
|
def vercmp(self, ver: str) -> bool:
|
|
if self.mod is None:
|
|
return True
|
|
assert self.version
|
|
if self.mod == "<":
|
|
return pyalpm.vercmp(ver, self.version) < 0
|
|
elif self.mod == "<=":
|
|
return pyalpm.vercmp(ver, self.version) <= 0
|
|
elif self.mod == "=":
|
|
return pyalpm.vercmp(ver, self.version) == 0
|
|
elif self.mod == ">=":
|
|
return pyalpm.vercmp(ver, self.version) >= 0
|
|
elif self.mod == ">":
|
|
return pyalpm.vercmp(ver, self.version) > 0
|
|
|
|
def _depcmp_literal(self, pkg: pyalpm.Package) -> bool:
|
|
return pkg.name == self.name and self.vercmp(pkg.version)
|
|
|
|
def _depcmp_provides(self, pkg: pyalpm.Package) -> bool:
|
|
for pvdstring in pkg.provides:
|
|
pvd = alpm_dep_from_string(pvdstring)
|
|
if self.mod is None:
|
|
if pvd.name == self.name:
|
|
return True
|
|
else:
|
|
if (
|
|
pvd.version is not None
|
|
and pvd.name == self.name
|
|
and self.vercmp(pvd.version)
|
|
):
|
|
return True
|
|
return False
|
|
|
|
def depcmp(self, pkg: pyalpm.Package) -> bool:
|
|
return self._depcmp_literal(pkg) or self._depcmp_provides(pkg)
|
|
|
|
|
|
def alpm_dep_from_string(depstring: str) -> alpm_depend_t:
|
|
desc: Optional[str] = None
|
|
if (idx := depstring.find(": ")) >= 0:
|
|
desc = depstring[idx + 2 :]
|
|
depstring = depstring[:idx]
|
|
|
|
mod: alpm_depmod_t = None
|
|
ver: Optional[str] = None
|
|
if (idx := depstring.find("<")) >= 0:
|
|
if depstring[idx + 1 :].startswith("="):
|
|
mod = cast(alpm_depmod_t, depstring[idx : idx + 2])
|
|
ver = depstring[idx + 2 :]
|
|
else:
|
|
mod = cast(alpm_depmod_t, depstring[idx : idx + 1])
|
|
ver = depstring[idx + 1 :]
|
|
depstring = depstring[:idx]
|
|
elif (idx := depstring.find(">")) >= 0:
|
|
if depstring[idx + 1 :].startswith("="):
|
|
mod = cast(alpm_depmod_t, depstring[idx : idx + 2])
|
|
ver = depstring[idx + 2 :]
|
|
else:
|
|
mod = cast(alpm_depmod_t, depstring[idx : idx + 1])
|
|
ver = depstring[idx + 1 :]
|
|
depstring = depstring[:idx]
|
|
elif (idx := depstring.find("=")) >= 0:
|
|
mod = cast(alpm_depmod_t, depstring[idx : idx + 1])
|
|
ver = depstring[idx + 1 :]
|
|
depstring = depstring[:idx]
|
|
|
|
return alpm_depend_t(
|
|
name=depstring,
|
|
version=ver,
|
|
desc=desc,
|
|
mod=mod,
|
|
)
|
|
|
|
|
|
def alpm_find_dbs_satisfier(
|
|
handle: pyalpm.Handle,
|
|
dbs: list[pyalpm.DB],
|
|
depstring: str,
|
|
questioncb: Callable[[list[pyalpm.Package]], Optional[pyalpm.Package]],
|
|
) -> Optional[pyalpm.Package]:
|
|
# BUG(lukeshu): alpm_find_dbs_satisfier doesn't bother to
|
|
# replicate the 'nodepversion' transaction option nor the
|
|
# 'ignorepkgs' and 'ignoregrps' config lists.
|
|
dep = alpm_dep_from_string(depstring)
|
|
|
|
# Literal
|
|
for db in dbs:
|
|
pkg = db.get_pkg(dep.name)
|
|
if pkg and dep._depcmp_literal(pkg):
|
|
return pkg
|
|
|
|
# Provides
|
|
providers: list[pyalpm.Package] = []
|
|
for db in dbs:
|
|
for pkg in db.pkgcache:
|
|
if pkg.name != dep.name and dep._depcmp_provides(pkg):
|
|
providers.append(pkg)
|
|
|
|
if len(providers) == 0:
|
|
return None
|
|
if len(providers) == 1:
|
|
return providers[0]
|
|
return questioncb(providers)
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
def get_all_questions(handle: pyalpm.Handle) -> dict[str, list[pyalpm.Package]]:
|
|
questions: dict[str, list[pyalpm.Package]] = dict()
|
|
by_name: dict[str, list[pyalpm.Package]] = dict()
|
|
|
|
def insert(name: str, pkg: pyalpm.Package) -> None:
|
|
nonlocal by_name
|
|
if name not in by_name:
|
|
by_name[name] = []
|
|
if pkg not in by_name[name]:
|
|
by_name[name].append(pkg)
|
|
|
|
for db in handle.get_syncdbs():
|
|
for pkg in db.pkgcache:
|
|
insert(pkg.name, pkg)
|
|
for pvdstring in pkg.provides:
|
|
pvd = alpm_dep_from_string(pvdstring)
|
|
insert(pvd.name, pkg)
|
|
|
|
for name, pkgs in by_name.items():
|
|
if len(pkgs) < 2 or name.endswith(".so"):
|
|
continue
|
|
if not any(pkg.name == name for pkg in pkgs):
|
|
questions[name] = pkgs
|
|
return questions
|
|
|
|
|
|
def get_questions(
|
|
handle: pyalpm.Handle, targets: list[str]
|
|
) -> dict[str, list[pyalpm.Package]]:
|
|
questions: dict[str, list[pyalpm.Package]] = dict()
|
|
done: set[str] = set()
|
|
for target in targets:
|
|
if target in done:
|
|
continue
|
|
|
|
providers: Optional[list[pyalpm.Package]] = None
|
|
|
|
target_dep = alpm_dep_from_string(target)
|
|
|
|
def questioncb(opts: list[pyalpm.Package]) -> Optional[pyalpm.Package]:
|
|
nonlocal providers
|
|
if target_dep.name.endswith(".so"):
|
|
return opts[0]
|
|
else:
|
|
providers = opts
|
|
return None
|
|
|
|
satisfier = alpm_find_dbs_satisfier(
|
|
handle, handle.get_syncdbs(), target, questioncb
|
|
)
|
|
|
|
if providers:
|
|
questions[target] = providers
|
|
if satisfier:
|
|
targets += satisfier.depends
|
|
done.add(target)
|
|
|
|
return questions
|
|
|
|
|
|
def main() -> None:
|
|
parser = config.make_parser(
|
|
prog="pacman-choices",
|
|
description="""Show package choices that pacman would consider
|
|
when when asked to install `targets`. This does NOT take
|
|
in to account when pacman would automatically decide the
|
|
answer based on other items in the package list or what's
|
|
already installed; it presents these questions even though
|
|
pacman would hide them.""",
|
|
)
|
|
parser.add_argument(
|
|
"-V",
|
|
"--version",
|
|
action="version",
|
|
version=f"%(prog)s (osi-tools) {__version__}",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--all",
|
|
action="store_true",
|
|
help="show all possible questions, not just the ones in the target list",
|
|
)
|
|
parser.add_argument(
|
|
"--oneline",
|
|
action="store_true",
|
|
help="show each question on one line, rather than in a multi-line format",
|
|
)
|
|
parser.add_argument("targets", nargs="*")
|
|
|
|
args = parser.parse_args()
|
|
handle = config.init_with_config_and_options(args)
|
|
|
|
questions: dict[str, list[pyalpm.Package]]
|
|
if args.all:
|
|
questions = get_all_questions(handle)
|
|
else:
|
|
questions = get_questions(handle, args.targets)
|
|
|
|
sep = " " if args.oneline else "\n\t"
|
|
for target, options in sorted(questions.items()):
|
|
text = target + ":"
|
|
for option in options:
|
|
assert option.db
|
|
text += sep + option.db.name + "/" + option.name
|
|
print(text)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|