osi-tools/pacman-choices

243 lines
7.4 KiB
Plaintext
Raw Normal View History

2023-10-25 07:14:54 +00:00
#!/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)
2023-10-25 07:14:54 +00:00
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
2023-10-25 07:14:54 +00:00
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()