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 |