You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
pcmn/pcmn.py

188 lines
5.6 KiB

#!/bin/python3
import json
import logging
import re
import subprocess
import argparse
import sys
from collections.abc import Iterable
from os.path import expanduser, isfile, isdir, dirname
groups = {
"pacman": {
"packages": set(),
"install_cmd": ["sudo", "pacman", "-S", "--asexplicit", "--noconfirm"],
"query_cmd": [["pacman", "-Qenq"], ["pacman", "-Qdtnq"]],
"remove_cmd": [
["sudo", "pacman", "-D", "--asdeps"],
["sudo", "pacman", "-Rsu", "--noconfirm"],
],
},
"aur": {
"packages": set(),
"install_cmd": ["yay", "-S"],
"query_cmd": [["pacman", "-Qemq"], ["pacman", "-Qdtmq"]],
"remove_cmd": [
["sudo", "pacman", "-D", "--asdeps"],
["sudo", "pacman", "-Rsu", "--noconfirm"],
],
},
}
RE_PACKAGE = re.compile(r"(\[(?P<group>[\w-]+)\]\s+)?(?P<package>[\w+.-]+)\s*(#.*)?")
DEFAULT_GROUP = "pacman"
DEFAULT_PKGLIST_PATH = expanduser("~/.config/pcmn/pkglist")
DEFAULT_CONFIG_PATH = expanduser("~/.config/pcmn/config.json")
def parse_packages(pkglist):
with open(pkglist) as pkgl:
for line in pkgl:
m = RE_PACKAGE.match(line)
if m:
grp = m.group("group")
pkg = m.group("package")
if not grp:
grp = DEFAULT_GROUP
if grp not in groups:
logging.warning(f"Invalid group '{grp}'")
else:
groups[grp]["packages"].add(pkg)
def install_packages(gdict, packages):
cmds = []
# Allow 'install_cmd' to be a single list for a command, or a list of lists,
# for multiple commands
if isinstance(gdict["install_cmd"][0], list):
for cmd in gdict["install_cmd"]:
cmds.append(cmd + list(packages))
else:
cmds.append(gdict["install_cmd"] + list(packages))
for cmd in cmds:
subprocess.run(cmd)
def query_packages(gdict):
packages = set()
cmds = []
# Allow 'query_cmd' to be a single list for a command, or a list of lists,
# for multiple commands
if isinstance(gdict["query_cmd"][0], list):
for cmd in gdict["query_cmd"]:
cmds.append(cmd)
else:
cmds.append(gdict["query_cmd"])
for cmd in cmds:
query = subprocess.run(cmd, capture_output=True, text=True)
packages = packages.union(set(p for p in query.stdout.split("\n") if p))
return packages
def remove_packages(gdict, packages):
cmds = []
# Allow 'remove_cmd' to be a single list for a command, or a list of lists,
# for multiple commands
if isinstance(gdict["remove_cmd"][0], list):
for cmd in gdict["remove_cmd"]:
cmds.append(cmd + list(packages))
else:
cmds.append(gdict["remove_cmd"] + list(packages))
for cmd in cmds:
subprocess.run(cmd)
def apply_packages(group):
gdict = groups[group]
installed_packages = query_packages(gdict)
needed_packages = gdict["packages"].difference(installed_packages)
if needed_packages:
print(f"The following packages will be installed: {needed_packages}")
ans = input("Proceed? [Y/n] ")
if not ans or ans.lower() == "y":
install_packages(gdict, needed_packages)
unneeded_packages = installed_packages.difference(gdict["packages"])
if unneeded_packages:
print(f"The following packages will be removed: {unneeded_packages}")
ans = input("Proceed? [Y/n] ")
if not ans or ans.lower() == "y":
remove_packages(gdict, unneeded_packages)
def generate_packages_list(pkglist):
if isfile(pkglist):
logging.error(f"File '{pkglist}' already exists, aborting.")
sys.exit(1)
with open(pkglist, "w+") as f:
for grp in groups:
packages = query_packages(groups[grp])
for pkg in sorted(packages):
if grp is not DEFAULT_GROUP:
f.write(f"[{grp}] {pkg}\n")
else:
f.write(f"{pkg}\n")
def load_config(config_file):
with open(config_file) as f:
groups = json.load(f)
# Setup 'packages' fields to have a set()
for grp in groups:
groups[grp]["packages"] = set()
return groups
def main():
logging.basicConfig(
format="%(levelname)s: %(message)s", encoding="utf-8", level=logging.WARN
)
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title="Command", dest="command", required=True)
parser.add_argument(
"--pkglist", action="store", help="Path to package list file to use"
)
parser.add_argument("--config", action="store", help="Path to config file to use")
cmd_apply = subparsers.add_parser(
"apply", help="Apply package list. Install needed, remove unneeded."
)
cmd_gen = subparsers.add_parser("generate", help="Generate package list")
args = parser.parse_args()
pkglist = DEFAULT_PKGLIST_PATH
if not args.pkglist:
logging.info(f"Using default pkglist path: '{DEFAULT_PKGLIST_PATH}'")
else:
pkglist = args.pkglist
global groups
if isfile(DEFAULT_CONFIG_PATH):
logging.info(
f"Using file present in default config path: '{DEFAULT_PKGLIST_PATH}'"
)
groups = load_config(DEFAULT_CONFIG_PATH)
elif args.config:
groups = load_config(args.config)
if args.command == "apply":
parse_packages(pkglist)
for grp in groups:
apply_packages(grp)
elif args.command == "generate":
if not isdir(dirname(pkglist)):
os.makedirs(pkglist)
generate_packages_list(pkglist)
if __name__ == "__main__":
main()