]> code.delx.au - monosys/blob - bin/pacorphan
notes: fix raspi install notes, also @home -> @username
[monosys] / bin / pacorphan
1 #!/usr/bin/env python3
2
3 import codecs
4 import subprocess
5 import os
6 import sys
7
8 def main():
9 name = os.path.basename(sys.argv[0])
10 if name == "pacorphan":
11 pacorphan_main()
12 elif name == "aptorphan":
13 aptorphan_main()
14 else:
15 print("This script must be named pacorphan or aptorphan!", file=sys.stderr)
16 sys.exit(1)
17
18 def pacorphan_main():
19 keep_pkg_list = load_keep_pkg_list("pacorphan")
20 unneeded_pkg_list = list(run(["pacman", "-Qttq"]))
21 installed_pkg_list = list(run(["pacman", "-Qq"]))
22 explicit_pkg_list = list(run(["pacman", "-Qeq"]))
23 mark_explicit_list = []
24 need_install_list = []
25
26 for pkg in keep_pkg_list:
27 if pkg in unneeded_pkg_list:
28 unneeded_pkg_list.remove(pkg)
29
30 if pkg in explicit_pkg_list:
31 explicit_pkg_list.remove(pkg)
32 else:
33 if pkg in installed_pkg_list:
34 mark_explicit_list.append(pkg)
35 else:
36 need_install_list.append(pkg)
37
38 if unneeded_pkg_list:
39 print("# Found packages to remove")
40 print("sudo pacman -R " + " ".join(unneeded_pkg_list))
41 print()
42
43 if explicit_pkg_list:
44 print("# Found explicitly installed packages to keep or remove")
45 print("echo " + " ".join(explicit_pkg_list) + " | tr ' ' '\\n' >> ~/.pacorphan/keep")
46 print("sudo pacman -D --asdeps " + " ".join(explicit_pkg_list))
47 print()
48
49 if mark_explicit_list:
50 print("# Found packages which should be marked as explicitly installed")
51 print("sudo pacman -D --asexplicit " + " ".join(mark_explicit_list))
52 print()
53
54 if need_install_list:
55 print("# Found packages which should be installed")
56 print("sudo pacman -S " + " ".join(need_install_list))
57 print()
58
59 def aptorphan_main():
60 ensure_apt_config_is_sane()
61
62 keep_pkg_list = load_keep_pkg_list("aptorphan")
63 mark_explicit_list = []
64 need_install_list = []
65 installed_pkg_list = list(run(["aptitude", "search", "?or(~i!~aremove,~ainstall)", "-F", "%p"]))
66 explicit_pkg_list = list(run(["aptitude", "search", "?or(~i!~M!~aremove,~ainstall!~M)", "-F", "%p"]))
67
68 for pkg in keep_pkg_list:
69 if pkg in explicit_pkg_list:
70 explicit_pkg_list.remove(pkg)
71 else:
72 if pkg in installed_pkg_list:
73 mark_explicit_list.append(pkg)
74 else:
75 need_install_list.append(pkg)
76
77
78 if mark_explicit_list:
79 print("# Found packages which should be marked as explicitly installed")
80 print("sudo aptitude --schedule-only install " + " ".join([("'"+x+"&m'") for x in mark_explicit_list]))
81 print()
82
83 if need_install_list:
84 print("# Found packages which should be installed")
85 print("sudo aptitude --schedule-only install " + " ".join(need_install_list))
86 print()
87
88 if explicit_pkg_list:
89 print("# Found explicitly installed packages to keep or remove")
90 print("echo " + " ".join(explicit_pkg_list) + " | tr ' ' '\\n' >> ~/.aptorphan/keep")
91 print("sudo aptitude --schedule-only install " + " ".join([(x+"+M") for x in explicit_pkg_list]))
92 print()
93
94 def ensure_apt_config_is_sane():
95 required_config = """
96 APT::Install-Recommends "false";
97 APT::Install-Suggests "false";
98 APT::AutoRemove::RecommendsImportant "false";
99 APT::AutoRemove::SuggestsImportant "false";
100 """.strip().split("\n")
101
102 actual_config = run(["apt-config", "dump"])
103
104 missing_lines = []
105 for required_line in required_config:
106 for line in actual_config:
107 if line == required_line:
108 break
109 else:
110 missing_lines.append(required_line)
111 if missing_lines:
112 print("Missing apt-config, add these lines to /etc/apt/apt.conf.d/99recommends-disable")
113 print("\n".join(missing_lines))
114 sys.exit(1)
115
116
117 def load_keep_pkg_list(name):
118 config_path = find_config_path(name)
119 if not os.path.isdir(config_path):
120 print("# WARNING, you should create a directory at %s" % config_path)
121 return []
122
123 result = []
124
125 for filename in sorted(os.listdir(config_path)):
126 if filename.startswith("."):
127 continue
128 full_filename = os.path.join(config_path, filename)
129 for pkg in codecs.open(full_filename, "r", "utf-8"):
130 pkg = strip_comment(pkg).strip()
131 if not pkg:
132 continue
133 if filename[0] != "~" and pkg[0] != "~":
134 if pkg in result:
135 print("# Duplicate entry:", pkg, "in file", filename)
136 continue
137 result.append(pkg)
138 else:
139 pkg = pkg.strip("~")
140 if pkg not in result:
141 print("# Redundant removal:", pkg, "in file", filename)
142 continue
143 result.remove(pkg)
144
145 return result
146
147 def find_config_path(name):
148 homedir_path = os.path.expanduser("~/.%s" % name)
149 if os.path.isdir(homedir_path):
150 return homedir_path
151
152 if "XDG_CONFIG_HOME" not in os.environ:
153 return os.path.expanduser("~/.config/%s" % name)
154 else:
155 return os.path.join(os.environ["XDG_CONFIG_HOME"], name)
156
157 def strip_comment(line):
158 pos = line.find("#")
159 if pos >= 0:
160 line = line[:pos]
161 return line.strip()
162
163 def run(cmd):
164 for line in subprocess.check_output(cmd).decode("utf-8").split("\n"):
165 line = line.strip()
166 if line:
167 yield line
168
169 if __name__ == "__main__":
170 main()