1 |
commit: d705ea3a502d1e92e124a81101501906f8737110 |
2 |
Author: Pavel Kazakov <nullishzero <AT> gentoo <DOT> org> |
3 |
AuthorDate: Sun Aug 3 21:45:14 2014 +0000 |
4 |
Commit: Pavel Kazakov <nullishzero <AT> gentoo <DOT> org> |
5 |
CommitDate: Sun Aug 3 21:45:14 2014 +0000 |
6 |
URL: http://git.overlays.gentoo.org/gitweb/?p=proj/portage.git;a=commit;h=d705ea3a |
7 |
|
8 |
New emaint module: Merges |
9 |
|
10 |
This emaint module scans for failed package merges and will display or |
11 |
fix any failed packages found. This module also saves failed merges |
12 |
found using a tracking file. Subsequent runs of the module will re-load |
13 |
that info for re-display, re-emerge of those packages. Fix typo: |
14 |
-collision-detect ==> -collision-protect <Brian Dolbec> |
15 |
|
16 |
--- |
17 |
pym/portage/emaint/main.py | 6 +- |
18 |
pym/portage/emaint/modules/merges/__init__.py | 30 +++ |
19 |
pym/portage/emaint/modules/merges/merges.py | 290 ++++++++++++++++++++++++++ |
20 |
3 files changed, 324 insertions(+), 2 deletions(-) |
21 |
|
22 |
diff --git a/pym/portage/emaint/main.py b/pym/portage/emaint/main.py |
23 |
index 6a17027..646883d 100644 |
24 |
--- a/pym/portage/emaint/main.py |
25 |
+++ b/pym/portage/emaint/main.py |
26 |
@@ -98,10 +98,11 @@ def module_opts(module_controller, module): |
27 |
class TaskHandler(object): |
28 |
"""Handles the running of the tasks it is given""" |
29 |
|
30 |
- def __init__(self, show_progress_bar=True, verbose=True, callback=None): |
31 |
+ def __init__(self, show_progress_bar=True, verbose=True, callback=None, module_output=None): |
32 |
self.show_progress_bar = show_progress_bar |
33 |
self.verbose = verbose |
34 |
self.callback = callback |
35 |
+ self.module_output = module_output |
36 |
self.isatty = os.environ.get('TERM') != 'dumb' and sys.stdout.isatty() |
37 |
self.progress_bar = ProgressBar(self.isatty, title="Emaint", max_desc_length=27) |
38 |
|
39 |
@@ -124,6 +125,7 @@ class TaskHandler(object): |
40 |
onProgress = None |
41 |
kwargs = { |
42 |
'onProgress': onProgress, |
43 |
+ 'module_output': self.module_output, |
44 |
# pass in a copy of the options so a module can not pollute or change |
45 |
# them for other tasks if there is more to do. |
46 |
'options': options.copy() |
47 |
@@ -219,5 +221,5 @@ def emaint_main(myargv): |
48 |
# need to pass the parser options dict to the modules |
49 |
# so they are available if needed. |
50 |
task_opts = options.__dict__ |
51 |
- taskmaster = TaskHandler(callback=print_results) |
52 |
+ taskmaster = TaskHandler(callback=print_results, module_output=sys.stdout) |
53 |
taskmaster.run_tasks(tasks, func, status, options=task_opts) |
54 |
|
55 |
diff --git a/pym/portage/emaint/modules/merges/__init__.py b/pym/portage/emaint/modules/merges/__init__.py |
56 |
new file mode 100644 |
57 |
index 0000000..96ee71b |
58 |
--- /dev/null |
59 |
+++ b/pym/portage/emaint/modules/merges/__init__.py |
60 |
@@ -0,0 +1,30 @@ |
61 |
+# Copyright 2005-2014 Gentoo Foundation |
62 |
+# Distributed under the terms of the GNU General Public License v2 |
63 |
+ |
64 |
+"""Scan for failed merges and fix them.""" |
65 |
+ |
66 |
+ |
67 |
+module_spec = { |
68 |
+ 'name': 'merges', |
69 |
+ 'description': __doc__, |
70 |
+ 'provides': { |
71 |
+ 'merges': { |
72 |
+ 'name': "merges", |
73 |
+ 'class': "MergesHandler", |
74 |
+ 'description': __doc__, |
75 |
+ 'functions': ['check', 'fix', 'purge'], |
76 |
+ 'func_desc': { |
77 |
+ 'purge': { |
78 |
+ 'short': '-P', 'long': '--purge-tracker', |
79 |
+ 'help': 'Removes the list of previously failed merges.' + |
80 |
+ ' WARNING: Only use this option if you plan on' + |
81 |
+ ' manually fixing them or do not want them' |
82 |
+ ' re-installed.', |
83 |
+ 'status': "Removing %s", |
84 |
+ 'action': 'store_true', |
85 |
+ 'func': 'purge' |
86 |
+ } |
87 |
+ } |
88 |
+ } |
89 |
+ } |
90 |
+} |
91 |
|
92 |
diff --git a/pym/portage/emaint/modules/merges/merges.py b/pym/portage/emaint/modules/merges/merges.py |
93 |
new file mode 100644 |
94 |
index 0000000..085fe2a |
95 |
--- /dev/null |
96 |
+++ b/pym/portage/emaint/modules/merges/merges.py |
97 |
@@ -0,0 +1,290 @@ |
98 |
+# Copyright 2005-2014 Gentoo Foundation |
99 |
+# Distributed under the terms of the GNU General Public License v2 |
100 |
+ |
101 |
+from _emerge.actions import load_emerge_config |
102 |
+ |
103 |
+import portage |
104 |
+from portage import os, _unicode_encode |
105 |
+from portage.const import MERGING_IDENTIFIER, PORTAGE_BIN_PATH, PRIVATE_PATH, \ |
106 |
+ VDB_PATH |
107 |
+from portage.dep import isvalidatom |
108 |
+ |
109 |
+import shutil |
110 |
+import subprocess |
111 |
+import sys |
112 |
+import time |
113 |
+ |
114 |
+class TrackingFile(object): |
115 |
+ """File for keeping track of failed merges.""" |
116 |
+ |
117 |
+ |
118 |
+ def __init__(self, tracking_path): |
119 |
+ """ |
120 |
+ Create a TrackingFile object. |
121 |
+ |
122 |
+ @param tracking_path: file path used to keep track of failed merges |
123 |
+ @type tracking_path: String |
124 |
+ """ |
125 |
+ self._tracking_path = _unicode_encode(tracking_path) |
126 |
+ |
127 |
+ |
128 |
+ def save(self, failed_pkgs): |
129 |
+ """ |
130 |
+ Save the specified packages that failed to merge. |
131 |
+ |
132 |
+ @param failed_pkgs: dictionary of failed packages |
133 |
+ @type failed_pkgs: dict |
134 |
+ """ |
135 |
+ tracking_path = self._tracking_path |
136 |
+ lines = ['%s %s' % (pkg, mtime) for pkg, mtime in failed_pkgs.items()] |
137 |
+ portage.util.write_atomic(tracking_path, '\n'.join(lines)) |
138 |
+ |
139 |
+ |
140 |
+ def load(self): |
141 |
+ """ |
142 |
+ Load previously failed merges. |
143 |
+ |
144 |
+ @rtype: dict |
145 |
+ @return: dictionary of packages that failed to merge |
146 |
+ """ |
147 |
+ tracking_path = self._tracking_path |
148 |
+ if not self.exists(): |
149 |
+ return {} |
150 |
+ failed_pkgs = {} |
151 |
+ with open(tracking_path, 'r') as tracking_file: |
152 |
+ for failed_merge in tracking_file: |
153 |
+ pkg, mtime = failed_merge.strip().split() |
154 |
+ failed_pkgs[pkg] = mtime |
155 |
+ return failed_pkgs |
156 |
+ |
157 |
+ |
158 |
+ def exists(self): |
159 |
+ """ |
160 |
+ Check if tracking file exists. |
161 |
+ |
162 |
+ @rtype: bool |
163 |
+ @return: true if tracking file exists, false otherwise |
164 |
+ """ |
165 |
+ return os.path.exists(self._tracking_path) |
166 |
+ |
167 |
+ |
168 |
+ def purge(self): |
169 |
+ """Delete previously saved tracking file if one exists.""" |
170 |
+ if self.exists(): |
171 |
+ os.remove(self._tracking_path) |
172 |
+ |
173 |
+ |
174 |
+ def __iter__(self): |
175 |
+ """ |
176 |
+ Provide an interator over failed merges. |
177 |
+ |
178 |
+ @return: iterator of packages that failed to merge |
179 |
+ """ |
180 |
+ return self.load().items().__iter__() |
181 |
+ |
182 |
+ |
183 |
+class MergesHandler(object): |
184 |
+ """Handle failed package merges.""" |
185 |
+ |
186 |
+ short_desc = "Remove failed merges" |
187 |
+ |
188 |
+ @staticmethod |
189 |
+ def name(): |
190 |
+ return "merges" |
191 |
+ |
192 |
+ |
193 |
+ def __init__(self): |
194 |
+ """Create MergesHandler object.""" |
195 |
+ eroot = portage.settings['EROOT'] |
196 |
+ tracking_path = os.path.join(eroot, PRIVATE_PATH, 'failed-merges'); |
197 |
+ self._tracking_file = TrackingFile(tracking_path) |
198 |
+ self._vardb_path = os.path.join(eroot, VDB_PATH) |
199 |
+ |
200 |
+ |
201 |
+ def can_progressbar(self, func): |
202 |
+ return func == 'check' |
203 |
+ |
204 |
+ |
205 |
+ def _scan(self, onProgress=None): |
206 |
+ """ |
207 |
+ Scan the file system for failed merges and return any found. |
208 |
+ |
209 |
+ @param onProgress: function to call for updating progress |
210 |
+ @type onProgress: Function |
211 |
+ @rtype: dict |
212 |
+ @return: dictionary of packages that failed to merges |
213 |
+ """ |
214 |
+ failed_pkgs = {} |
215 |
+ for cat in os.listdir(self._vardb_path): |
216 |
+ pkgs_path = os.path.join(self._vardb_path, cat) |
217 |
+ if not os.path.isdir(pkgs_path): |
218 |
+ continue |
219 |
+ pkgs = os.listdir(pkgs_path) |
220 |
+ maxval = len(pkgs) |
221 |
+ for i, pkg in enumerate(pkgs): |
222 |
+ if onProgress: |
223 |
+ onProgress(maxval, i+1) |
224 |
+ if MERGING_IDENTIFIER in pkg: |
225 |
+ mtime = int(os.stat(os.path.join(pkgs_path, pkg)).st_mtime) |
226 |
+ pkg = os.path.join(cat, pkg) |
227 |
+ failed_pkgs[pkg] = mtime |
228 |
+ return failed_pkgs |
229 |
+ |
230 |
+ |
231 |
+ def _failed_pkgs(self, onProgress=None): |
232 |
+ """ |
233 |
+ Return failed packages from both the file system and tracking file. |
234 |
+ |
235 |
+ @rtype: dict |
236 |
+ @return: dictionary of packages that failed to merges |
237 |
+ """ |
238 |
+ failed_pkgs = self._scan(onProgress) |
239 |
+ for pkg, mtime in self._tracking_file: |
240 |
+ if pkg not in failed_pkgs: |
241 |
+ failed_pkgs[pkg] = mtime |
242 |
+ return failed_pkgs |
243 |
+ |
244 |
+ |
245 |
+ def _remove_failed_dirs(self, failed_pkgs): |
246 |
+ """ |
247 |
+ Remove the directories of packages that failed to merge. |
248 |
+ |
249 |
+ @param failed_pkgs: failed packages whose directories to remove |
250 |
+ @type failed_pkg: dict |
251 |
+ """ |
252 |
+ for failed_pkg in failed_pkgs: |
253 |
+ pkg_path = os.path.join(self._vardb_path, failed_pkg) |
254 |
+ # delete failed merge directory if it exists (it might not exist |
255 |
+ # if loaded from tracking file) |
256 |
+ if os.path.exists(pkg_path): |
257 |
+ shutil.rmtree(pkg_path) |
258 |
+ # TODO: try removing package CONTENTS to prevent orphaned |
259 |
+ # files |
260 |
+ |
261 |
+ |
262 |
+ def _get_pkg_atoms(self, failed_pkgs, pkg_atoms, pkg_invalid_entries): |
263 |
+ """ |
264 |
+ Get the package atoms for the specified failed packages. |
265 |
+ |
266 |
+ @param failed_pkgs: failed packages to iterate |
267 |
+ @type failed_pkgs: dict |
268 |
+ @param pkg_atoms: append package atoms to this set |
269 |
+ @type pkg_atoms: set |
270 |
+ @param pkg_invalid_entries: append any packages that are invalid to this set |
271 |
+ @type pkg_invalid_entries: set |
272 |
+ """ |
273 |
+ |
274 |
+ emerge_config = load_emerge_config() |
275 |
+ portdb = emerge_config.target_config.trees['porttree'].dbapi |
276 |
+ for failed_pkg in failed_pkgs: |
277 |
+ # validate pkg name |
278 |
+ pkg_name = '%s' % failed_pkg.replace(MERGING_IDENTIFIER, '') |
279 |
+ pkg_atom = '=%s' % pkg_name |
280 |
+ |
281 |
+ if not isvalidatom(pkg_atom): |
282 |
+ pkg_invalid_entries.append("'%s' is an invalid package atom." |
283 |
+ % pkg_atom) |
284 |
+ if not portdb.cpv_exists(pkg_name): |
285 |
+ pkg_invalid_entries.append( |
286 |
+ "'%s' does not exist in the portage tree." % pkg_name) |
287 |
+ pkg_atoms.add(pkg_atom) |
288 |
+ |
289 |
+ |
290 |
+ def _emerge_pkg_atoms(self, module_output, pkg_atoms): |
291 |
+ """ |
292 |
+ Emerge the specified packages atoms. |
293 |
+ |
294 |
+ @param module_output: output will be written to |
295 |
+ @type module_output: Class |
296 |
+ @param pkg_atoms: packages atoms to emerge |
297 |
+ @type pkg_atoms: set |
298 |
+ @rtype: list |
299 |
+ @return: List of results |
300 |
+ """ |
301 |
+ # TODO: rewrite code to use portage's APIs instead of a subprocess |
302 |
+ env = { |
303 |
+ "FEATURES" : "-collision-protect -protect-owned", |
304 |
+ "PATH" : os.environ["PATH"] |
305 |
+ } |
306 |
+ emerge_cmd = ( |
307 |
+ portage._python_interpreter, |
308 |
+ '-b', |
309 |
+ os.path.join(PORTAGE_BIN_PATH, 'emerge'), |
310 |
+ '--quiet', |
311 |
+ '--oneshot', |
312 |
+ '--complete-graph=y' |
313 |
+ ) |
314 |
+ results = [] |
315 |
+ msg = 'Re-Emerging packages that failed to merge...\n' |
316 |
+ if module_output: |
317 |
+ module_output.write(msg) |
318 |
+ else: |
319 |
+ module_output = subprocess.PIPE |
320 |
+ results.append(msg) |
321 |
+ proc = subprocess.Popen(emerge_cmd + tuple(pkg_atoms), env=env, |
322 |
+ stdout=module_output, stderr=sys.stderr) |
323 |
+ output = proc.communicate()[0] |
324 |
+ if output: |
325 |
+ results.append(output) |
326 |
+ if proc.returncode != os.EX_OK: |
327 |
+ emerge_status = "Failed to emerge '%s'" % (' '.join(pkg_atoms)) |
328 |
+ else: |
329 |
+ emerge_status = "Successfully emerged '%s'" % (' '.join(pkg_atoms)) |
330 |
+ results.append(emerge_status) |
331 |
+ return results |
332 |
+ |
333 |
+ |
334 |
+ def check(self, **kwargs): |
335 |
+ """Check for failed merges.""" |
336 |
+ onProgress = kwargs.get('onProgress', None) |
337 |
+ failed_pkgs = self._failed_pkgs(onProgress) |
338 |
+ errors = [] |
339 |
+ for pkg, mtime in failed_pkgs.items(): |
340 |
+ mtime_str = time.ctime(int(mtime)) |
341 |
+ errors.append("'%s' failed to merge on '%s'" % (pkg, mtime_str)) |
342 |
+ return errors |
343 |
+ |
344 |
+ |
345 |
+ def fix(self, **kwargs): |
346 |
+ """Attempt to fix any failed merges.""" |
347 |
+ module_output = kwargs.get('module_output', None) |
348 |
+ failed_pkgs = self._failed_pkgs() |
349 |
+ if not failed_pkgs: |
350 |
+ return ['No failed merges found.'] |
351 |
+ |
352 |
+ pkg_invalid_entries = set() |
353 |
+ pkg_atoms = set() |
354 |
+ self._get_pkg_atoms(failed_pkgs, pkg_atoms, pkg_invalid_entries) |
355 |
+ if pkg_invalid_entries: |
356 |
+ return pkg_invalid_entries |
357 |
+ |
358 |
+ try: |
359 |
+ self._tracking_file.save(failed_pkgs) |
360 |
+ except IOError as ex: |
361 |
+ errors = ['Unable to save failed merges to tracking file: %s\n' |
362 |
+ % str(ex)] |
363 |
+ errors.append(', '.join(sorted(failed_pkgs))) |
364 |
+ return errors |
365 |
+ self._remove_failed_dirs(failed_pkgs) |
366 |
+ results = self._emerge_pkg_atoms(module_output, pkg_atoms) |
367 |
+ # list any new failed merges |
368 |
+ for pkg in sorted(self._scan()): |
369 |
+ results.append("'%s' still found as a failed merge." % pkg) |
370 |
+ # reload config and remove successful packages from tracking file |
371 |
+ emerge_config = load_emerge_config() |
372 |
+ vardb = emerge_config.target_config.trees['vartree'].dbapi |
373 |
+ still_failed_pkgs = {} |
374 |
+ for pkg, mtime in failed_pkgs.items(): |
375 |
+ pkg_name = '%s' % pkg.replace(MERGING_IDENTIFIER, '') |
376 |
+ if not vardb.cpv_exists(pkg_name): |
377 |
+ still_failed_pkgs[pkg] = mtime |
378 |
+ self._tracking_file.save(still_failed_pkgs) |
379 |
+ return results |
380 |
+ |
381 |
+ |
382 |
+ def purge(self, **kwargs): |
383 |
+ """Attempt to remove previously saved tracking file.""" |
384 |
+ if not self._tracking_file.exists(): |
385 |
+ return ['Tracking file not found.'] |
386 |
+ self._tracking_file.purge() |
387 |
+ return ['Removed tracking file.'] |