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 3/3] Add an emaint module that can scan for failed merges and that can fix failed merges.
Date: Thu, 13 Mar 2014 06:13:41
Message-Id: 1394691047-14156-3-git-send-email-nullishzero@gentoo.org
In Reply to: [gentoo-portage-dev] [PATCH 1/3] Move -MERGING- string to a constant variable. 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 | 281 ++++++++++++++++++++++++++
5 3 files changed, 315 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..a99dad4
80 --- /dev/null
81 +++ b/pym/portage/emaint/modules/merges/merges.py
82 @@ -0,0 +1,281 @@
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 time
95 +import shutil
96 +import sys
97 +import subprocess
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 + with open(tracking_path, 'w') as tracking_file:
122 + for pkg, mtime in failed_pkgs.items():
123 + tracking_file.write('%s %s\n' % (pkg, mtime))
124 +
125 +
126 + def load(self):
127 + """
128 + Load previously failed merges.
129 +
130 + @rtype: dict
131 + @return: dictionary of packages that failed to merge
132 + """
133 + tracking_path = self._tracking_path
134 + if not os.path.exists(tracking_path):
135 + return {}
136 + failed_pkgs = {}
137 + with open(tracking_path, 'r') as tracking_file:
138 + for failed_merge in tracking_file:
139 + pkg, mtime = failed_merge.strip().split()
140 + failed_pkgs[pkg] = mtime
141 + return failed_pkgs
142 +
143 +
144 + def exists(self):
145 + """
146 + Check if tracking file exists.
147 +
148 + @rtype: bool
149 + @return: true if tracking file exists, false otherwise
150 + """
151 + return os.path.exists(self._tracking_path)
152 +
153 +
154 + def purge(self):
155 + """Delete previously saved tracking file if one exists."""
156 + if self.exists():
157 + os.remove(self._tracking_path)
158 +
159 +
160 +class MergesHandler(object):
161 + """Handle failed package merges."""
162 +
163 + short_desc = "Remove failed merges"
164 +
165 + @staticmethod
166 + def name():
167 + return "merges"
168 +
169 +
170 + def __init__(self):
171 + """Create MergesHandler object."""
172 + eroot = portage.settings['EROOT']
173 + tracking_path = os.path.join(eroot, PRIVATE_PATH, 'failed-merges');
174 + self._tracking_file = TrackingFile(tracking_path)
175 + self._vardb_path = os.path.join(eroot, VDB_PATH)
176 +
177 +
178 + def can_progressbar(self, func):
179 + return func == 'check'
180 +
181 +
182 + def _scan(self, onProgress=None):
183 + """
184 + Scan the file system for failed merges and return any found.
185 +
186 + @param onProgress: function to call for updating progress
187 + @type onProgress: Function
188 + @rtype: dict
189 + @return: dictionary of packages that failed to merges
190 + """
191 + failed_pkgs = {}
192 + for cat in os.listdir(self._vardb_path):
193 + pkgs_path = os.path.join(self._vardb_path, cat)
194 + if not os.path.isdir(pkgs_path):
195 + continue
196 + pkgs = os.listdir(pkgs_path)
197 + maxval = len(pkgs)
198 + for i, pkg in enumerate(pkgs):
199 + if onProgress:
200 + onProgress(maxval, i+1)
201 + if MERGING_IDENTIFIER in pkg:
202 + mtime = int(os.stat(os.path.join(pkgs_path, pkg)).st_mtime)
203 + pkg = os.path.join(cat, pkg)
204 + failed_pkgs[pkg] = mtime
205 + return failed_pkgs
206 +
207 +
208 + def _failed_pkgs(self, onProgress=None):
209 + """
210 + Return failed packages from both the file system and tracking file.
211 +
212 + @rtype: dict
213 + @return: dictionary of packages that failed to merges
214 + """
215 + failed_pkgs = self._scan(onProgress)
216 + for pkg, mtime in self._tracking_file.load().items():
217 + if pkg not in failed_pkgs:
218 + failed_pkgs[pkg] = mtime
219 + return failed_pkgs
220 +
221 +
222 + def _remove_failed_dirs(self, failed_pkgs):
223 + """
224 + Remove the directories of packages that failed to merge.
225 +
226 + @param failed_pkgs: failed packages whose directories to remove
227 + @type failed_pkg: dict
228 + """
229 + for failed_pkg in failed_pkgs:
230 + pkg_path = os.path.join(self._vardb_path, failed_pkg)
231 + # delete failed merge directory if it exists (it might not exist
232 + # if loaded from tracking file)
233 + if os.path.exists(pkg_path):
234 + shutil.rmtree(pkg_path)
235 + # TODO: try removing package contents to prevent orphaned
236 + # files
237 +
238 +
239 + def _get_pkg_atoms(self, failed_pkgs, pkg_atoms, pkg_dne):
240 + """
241 + Get the package atoms for the specified failed packages.
242 +
243 + @param failed_pkgs: failed packages to iterate
244 + @type failed_pkgs: dict
245 + @param pkg_atoms: append package atoms to this set
246 + @type pkg_atoms: set
247 + @param pkg_dne: append any packages atoms that are invalid to this set
248 + @type pkg_dne: set
249 + """
250 +
251 + emerge_config = load_emerge_config()
252 + portdb = emerge_config.target_config.trees['porttree'].dbapi
253 + for failed_pkg in failed_pkgs:
254 + # validate pkg name
255 + pkg_name = '%s' % failed_pkg.replace(MERGING_IDENTIFIER, '')
256 + pkg_atom = '=%s' % pkg_name
257 +
258 + if not isvalidatom(pkg_atom):
259 + pkg_dne.append( "'%s' is an invalid package atom." % pkg_atom)
260 + if not portdb.cpv_exists(pkg_name):
261 + pkg_dne.append( "'%s' does not exist in the portage tree."
262 + % pkg_name)
263 + pkg_atoms.add(pkg_atom)
264 +
265 +
266 + def _emerge_pkg_atoms(self, module_output, pkg_atoms):
267 + """
268 + Emerge the specified packages atoms.
269 +
270 + @param module_output: output will be written to
271 + @type module_output: Class
272 + @param pkg_atoms: packages atoms to emerge
273 + @type pkg_atoms: set
274 + @rtype: list
275 + @return: List of results
276 + """
277 + # TODO: rewrite code to use portage's APIs instead of a subprocess
278 + env = {
279 + "FEATURES" : "-collision-detect -protect-owned",
280 + "PATH" : os.environ["PATH"]
281 + }
282 + emerge_cmd = (
283 + portage._python_interpreter,
284 + '-b',
285 + os.path.join(PORTAGE_BIN_PATH, 'emerge'),
286 + '--quiet',
287 + '--oneshot',
288 + '--complete-graph=y'
289 + )
290 + results = []
291 + msg = 'Re-Emerging packages that failed to merge...\n'
292 + if module_output:
293 + module_output.write(msg)
294 + else:
295 + module_output = subprocess.PIPE
296 + results.append(msg)
297 + proc = subprocess.Popen(emerge_cmd + tuple(pkg_atoms), env=env,
298 + stdout=module_output, stderr=sys.stderr)
299 + output = proc.communicate()[0]
300 + if output:
301 + results.append(output)
302 + if proc.returncode != os.EX_OK:
303 + emerge_status = "Failed to emerge '%s'" % (' '.join(pkg_atoms))
304 + else:
305 + emerge_status = "Successfully emerged '%s'" % (' '.join(pkg_atoms))
306 + results.append(emerge_status)
307 + return results
308 +
309 +
310 + def check(self, **kwargs):
311 + """Check for failed merges."""
312 + onProgress = kwargs.get('onProgress', None)
313 + failed_pkgs = self._failed_pkgs(onProgress)
314 + errors = []
315 + for pkg, mtime in failed_pkgs.items():
316 + mtime_str = time.ctime(int(mtime))
317 + errors.append("'%s' failed to merge on '%s'" % (pkg, mtime_str))
318 + return errors
319 +
320 +
321 + def fix(self, **kwargs):
322 + """Attempt to fix any failed merges."""
323 + module_output = kwargs.get('module_output', None)
324 + failed_pkgs = self._failed_pkgs()
325 + if not failed_pkgs:
326 + return [ 'No failed merges found. ' ]
327 +
328 + pkg_dne = set()
329 + pkg_atoms = set()
330 + self._get_pkg_atoms(failed_pkgs, pkg_atoms, pkg_dne)
331 + if pkg_dne:
332 + return pkg_dne
333 +
334 + try:
335 + self._tracking_file.save(failed_pkgs)
336 + except IOError as ex:
337 + errors = [ 'Unable to save failed merges to tracking file: %s\n'
338 + % str(ex) ]
339 + errors.append(', '.join(sorted(failed_pkgs)))
340 + return errors
341 + self._remove_failed_dirs(failed_pkgs)
342 + results = self._emerge_pkg_atoms(module_output, pkg_atoms)
343 + # list any new failed merges
344 + for pkg in sorted(self._scan()):
345 + results.append("'%s' still found as a failed merge." % pkg)
346 + # reload config and remove successful packages from tracking file
347 + emerge_config = load_emerge_config()
348 + vardb = emerge_config.target_config.trees['vartree'].dbapi
349 + still_failed_pkgs = {}
350 + for pkg, mtime in failed_pkgs.items():
351 + pkg_name = '%s' % pkg.replace(MERGING_IDENTIFIER, '')
352 + if not vardb.cpv_exists(pkg_name):
353 + still_failed_pkgs[pkg] = mtime
354 + self._tracking_file.save(still_failed_pkgs)
355 + return results
356 +
357 +
358 + def purge(self, **kwargs):
359 + """Attempt to remove previously saved tracking file."""
360 + if not self._tracking_file.exists():
361 + return ['Tracking file not found.']
362 + self._tracking_file.purge()
363 + return ['Removed tracking file.']
364 --
365 1.8.3.2

Replies