-
Notifications
You must be signed in to change notification settings - Fork 3
/
pacresolve
executable file
·179 lines (168 loc) · 5.28 KB
/
pacresolve
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 python3
#
# This script automatically handles *.pac{new,save} files.
# It requires root privileges to run.
#
# There are three types of actions determined by the ACTIONS config:
#
# * never_upgrade: Keep the current file; delete pacnew.
# * always_upgrade: Always replace current file with pacnew; delete pacsave.
# * Other files are automatically rebased if a pacbase is found.
#
# Normally, you'll want to create a pacbase the first time a config is modified.
#
# To retroactively create a pacbase, you can do something like:
#
# mkdir -p /root/.config/pacbases
# cd /root/.config/pacbases
#
# # You'll need to manually find the right version.
# pkgtar=/var/cache/pacman/pkg/dhcpcd-6.11.2-1-x86_64.pkg.tar.xz
# conf=etc/dhcpcd.conf
#
# tar -xf "$pkgtar" "$conf"
#
# # Check if it looks reasonable
# diff "$conf" "/$conf"
#
import fnmatch, os, re, shlex, subprocess, sys
from typing import Iterable, Optional, Sequence, Tuple, TypeVar
ACTIONS = [
("/etc/ImageMagick-6", "always_upgrade"),
("/etc/cups/*", "never_upgrade"),
("/etc/group", "never_upgrade"),
("/etc/gshadow", "never_upgrade"),
("/etc/hostapd/hostapd.conf", "never_upgrade"),
("/etc/hosts", "never_upgrade"),
("/etc/iptables/iptables.rules", "never_upgrade"),
("/etc/passwd", "never_upgrade"),
("/etc/resolv.conf", "never_upgrade"),
("/etc/shadow", "never_upgrade"),
]
BASE_DIR = "/root/.config/pacbases"
T = TypeVar("T")
def not_none(optional: Optional[T]) -> T:
if optional is None:
raise ValueError("did not expect None")
return optional
def locate(regex: str) -> Sequence[str]:
subprocess.run(["updatedb"], check=True)
p = subprocess.run(
["locate", "-0", "-e", "--r", regex],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
if p.returncode and p.stderr:
raise ValueError(p.stderr)
return p.stdout.split("\0")[:-1]
def find_fnmatch(
patterns: Iterable[Tuple[str, T]],
path: str,
default: T,
) -> T:
for pattern, value in patterns:
if fnmatch.fnmatch(path, pattern):
return value
return default
def patch_command(base: str, a: str, b: str, dry_run: bool = False) -> str:
return (
f"diff -u {shlex.join([base, a])} | "
f"patch -N{' --dry-run' if dry_run else ''} {shlex.quote(b)}"
)
DEV_NULL = "/dev/null"
diffs = []
moves = []
patches = []
removes = []
for pac_path in locate(r"\.pac\(new\|save\)$"):
cur_path, suffix = not_none(re.match(
r"(?s)(.*)\.pac(new|save)\Z",
pac_path,
)).groups()
if not os.path.exists(cur_path):
cur_path = DEV_NULL
action = find_fnmatch(ACTIONS, cur_path, "")
if not action:
base_path = os.path.join(BASE_DIR, cur_path.lstrip("/"))
if os.path.exists(base_path):
if suffix == "new":
patches.append((
base_path,
pac_path,
cur_path,
))
elif suffix == "save":
diffs.append((cur_path, pac_path, f"unresolved pac{suffix}"))
else:
raise ValueError("unrecognized suffix: {suffix!r}")
else:
diffs.append((cur_path, pac_path, f"unresolved pac{suffix}"))
elif action == "never_upgrade":
if suffix == "new":
removes.append(pac_path)
elif suffix == "save":
diffs.append((cur_path, pac_path, f"unresolved pac{suffix}"))
else:
raise ValueError("unrecognized suffix: {suffix!r}")
elif action == "always_upgrade":
if suffix == "new":
if cur_path == DEV_NULL:
removes.append(pac_path)
else:
moves.append((pac_path, cur_path))
elif suffix == "save":
removes.append(pac_path)
else:
raise ValueError("unrecognized suffix: {suffix!r}")
else:
raise ValueError("unrecognized action: {action!r}")
command_groups = []
if removes:
command_groups.append([f"rm -i {shlex.join(sorted(removes))}"])
if moves:
command_groups.append([
f"mv -Ti {shlex.join([src, dst])}"
for src, dst in sorted(moves)
])
for base, a, b in sorted(patches):
dry_run = subprocess.run([
"sh",
"-c",
patch_command(base, a, b, dry_run=True),
], stdout=subprocess.PIPE, universal_newlines=True)
command_group = []
if dry_run.returncode:
command_group.append(
"# Warning: Patch won't apply cleanly. "
"Output from dry run:"
)
command_group.append("#")
command_group.extend(
"# " + line
for line in dry_run.stdout.splitlines()
)
command_group.append("#")
command_group.extend([
f"{patch_command(base, a, b)} &&",
f" mv -T {shlex.join([a, base])}",
])
command_groups.append(command_group)
for a, b, comment in sorted(diffs):
command_groups.append([
f"# {comment}",
f"diff -u --color {shlex.join([a, b])}",
])
if not command_groups:
sys.exit(0)
print("""
# Run these commands as root:
(
set -ex
"""[1:-1])
for commands in command_groups:
print("")
for command in commands:
print(" " + command)
print(")")
sys.exit(1)