Gentoo Archives: gentoo-portage-dev

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

Replies