Gentoo Archives: gentoo-commits

From: Pavel Kazakov <nullishzero@g.o>
To: gentoo-commits@l.g.o
Subject: [gentoo-commits] proj/portage:master commit in: pym/portage/emaint/modules/merges/, pym/portage/emaint/
Date: Sun, 03 Aug 2014 21:53:25
Message-Id: 1407102314.d705ea3a502d1e92e124a81101501906f8737110.nullishzero@gentoo
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.']