-
Notifications
You must be signed in to change notification settings - Fork 2
/
decpac.py
179 lines (160 loc) · 6.03 KB
/
decpac.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env python
import argparse
import shlex
import subprocess
import re
import luxem
import os.path
from tempfile import NamedTemporaryFile
def main():
parser = argparse.ArgumentParser(
description="Arch Linux declarative package management"
)
parser.add_argument("--conf", help="Configuration path", default="/etc/decpac.conf")
parser.add_argument(
"--noconfirm",
help="Don't ask for confirmation; use default responses",
action="store_true",
)
subparsers = parser.add_subparsers(title="Command", dest="_command")
com_gen = subparsers.add_parser(
"generate",
description="Generate configuration from current explicitly installed packages", # noqa
)
com_gen.add_argument(
"-c", "--command", help="Install command",
)
com_gen.add_argument(
"-ac", "--aurcommand", help="AUR install command",
)
com_gen.add_argument(
"-f", "--force", help="Overwrite existing configuration", action="store_true",
)
subparsers.add_parser(
"sync", description="Install, remove, and upgrade packages. Default command.",
)
args = parser.parse_args()
def itercurrent():
res = subprocess.run(
["pacman", "-Qe"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
if res.returncode != 0 and res.stderr:
res.check_returncode()
for line in res.stdout.splitlines():
yield re.search("([^ ]+)", line).group(1)
def iterlocal():
res = subprocess.run(
["pacman", "-Qm"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
if res.returncode != 0 and res.stderr:
res.check_returncode()
for line in res.stdout.splitlines():
yield re.search("([^ ]+)", line).group(1)
command = args._command
if not command:
command = "sync"
if command == "generate":
local = set(iterlocal())
conf = dict(
installed=[
luxem.Typed("aur", package) if package in local else package
for package in itercurrent()
],
)
if args.command:
conf["install_main"] = shlex.split(args.command)
if args.aurcommand:
conf["install_aur"] = shlex.split(args.aurcommand)
if not args.force and os.path.exists(args.conf):
print("decpac.conf already exists. Delete it or run with `-f`.")
return
with NamedTemporaryFile() as tmp:
tmp.write(luxem.dumps(conf, pretty=True))
tmp.flush()
subprocess.check_call(["sudo", "cp", tmp.name, args.conf])
subprocess.check_call(["sudo", "chmod", "644", args.conf])
elif command == "sync":
def pname(package):
if isinstance(package, luxem.Typed):
return package.value
return package
with open(args.conf, "r") as conff:
conf = luxem.load(conff)[0]
print("Scanning current package state...")
state = dict(installed=set(itercurrent()),)
add_main = set()
add_aur = set()
conf_names = set()
# Group packages by type (normal, aur, etc)
for package in conf["installed"]:
if isinstance(package, luxem.Typed):
conf_names.add(package.value)
if package.value not in state["installed"]:
if package.name == "main":
add_main.add(package.value)
elif package.name == "aur":
add_aur.add(package.value)
else:
raise RuntimeError(
"Unknown package type {} for {}".format(
package.name, package.value
)
)
else:
conf_names.add(package)
if package not in state["installed"]:
add_main.add(package)
add = add_main | add_aur
print("Installing {}".format(add))
remove = state["installed"] - conf_names
print("Removing {}".format(remove))
if not args.noconfirm and not input("Okay? (y/N) ") == "y":
print("Aborting.")
return
print()
# Add new packages or re-mark mis-marked packages
if add:
mark = []
for command, command_default, type_add in [
["install_main", ["sudo", "pacman", "--noconfirm", "-S"], add_main],
["install_aur", None, add_aur],
]:
add_args = conf.get(command) or command_default
_type_add, type_add = type_add, []
for package in _type_add:
if subprocess.call(["pacman", "-Q", package]) != 0:
type_add.append(package)
else:
mark.append(package)
if type_add:
if not add_args:
raise RuntimeError(
"Field {} missing from decpac.conf".format(command)
)
subprocess.check_call(add_args + type_add)
# Hack for trizen which splits up packages which leads to
# command line packages that are deps of others to be marked
# as non-explicit.
if mark:
subprocess.call(
["sudo", "pacman", "--noconfirm", "-D", "--asexplicit"] + mark
)
else:
print("No packages to install.")
# Remove no longer listed packages
if remove:
subprocess.check_call(
["sudo", "pacman", "-D", "--asdeps", "--noconfirm"] + list(remove)
)
subprocess.check_call(
["sudo", "pacman", "-Rsu", "--noconfirm"] + list(remove)
)
else:
print("No packages to remove.")
print("Done")