2 # Copyright 1999-2013 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
6 # dispatch-conf -- Integrate modified configs, post-emerge
8 # Jeremy Wohl (http://igmus.org)
14 from __future__ import print_function
16 from stat import ST_GID, ST_MODE, ST_UID
17 from random import random
18 import atexit, re, shutil, stat, sys
19 from os import path as osp
20 pym_path = osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym")
21 sys.path.insert(0, pym_path)
23 portage._internal_caller = True
24 from portage import os
25 from portage import _unicode_decode
26 from portage.dispatch_conf import diffstatusoutput
27 from portage.process import find_binary, spawn
29 FIND_EXTANT_CONFIGS = "find '%s' %s -name '._cfg????_%s' ! -name '.*~' ! -iname '.*.bak' -print"
30 DIFF_CONTENTS = "diff -Nu '%s' '%s'"
32 # We need a secure scratch dir and python does silly verbose errors on the use of tempnam
33 oldmask = os.umask(0o077)
35 while SCRATCH_DIR is None:
37 mydir = "/tmp/dispatch-conf."
39 if int(random() * 3) == 0:
40 mydir += chr(int(65+random()*26.0))
41 elif int(random() * 2) == 0:
42 mydir += chr(int(97+random()*26.0))
44 mydir += chr(int(48+random()*10.0))
45 if os.path.exists(mydir):
54 # Ensure the scratch dir is deleted
55 def cleanup(mydir=SCRATCH_DIR):
57 atexit.register(cleanup)
59 MANDATORY_OPTS = [ 'archive-dir', 'diff', 'replace-cvs', 'replace-wscomments', 'merge' ]
61 def cmd_var_is_valid(cmd):
63 Return true if the first whitespace-separated token contained
64 in cmd is an executable file, false otherwise.
66 cmd = portage.util.shlex_split(cmd)
70 if os.path.isabs(cmd[0]):
71 return os.access(cmd[0], os.EX_OK)
73 return find_binary(cmd[0]) is not None
78 def grind (self, config_paths):
82 config_root = portage.settings["EPREFIX"] or os.sep
83 self.options = portage.dispatch_conf.read_config(MANDATORY_OPTS)
85 if "log-file" in self.options:
86 if os.path.isfile(self.options["log-file"]):
87 shutil.copy(self.options["log-file"], self.options["log-file"] + '.old')
88 if os.path.isfile(self.options["log-file"]) \
89 or not os.path.exists(self.options["log-file"]):
90 open(self.options["log-file"], 'w').close() # Truncate it
91 os.chmod(self.options["log-file"], 0o600)
93 self.options["log-file"] = "/dev/null"
95 pager = self.options.get("pager")
96 if pager is None or not cmd_var_is_valid(pager):
97 pager = os.environ.get("PAGER")
98 if pager is None or not cmd_var_is_valid(pager):
101 pager_basename = os.path.basename(portage.util.shlex_split(pager)[0])
102 if pager_basename == "less":
103 less_opts = self.options.get("less-opts")
104 if less_opts is not None and less_opts.strip():
105 pager += " " + less_opts
107 if pager_basename == "cat":
110 pager = " | " + pager
113 # Build list of extant configs
116 for path in config_paths:
117 path = portage.normalize_path(
118 os.path.join(config_root, path.lstrip(os.sep)))
120 mymode = os.stat(path).st_mode
124 find_opts = "-name '.*' -type d -prune -o"
125 if not stat.S_ISDIR(mymode):
126 path, basename = os.path.split(path)
127 find_opts = "-maxdepth 1"
129 with os.popen(FIND_EXTANT_CONFIGS %
130 (path, find_opts, basename)) as proc:
131 confs += self.massage(proc.readlines())
133 if self.options['use-rcs'] == 'yes':
134 for rcs_util in ("rcs", "ci", "co", "rcsmerge"):
135 if not find_binary(rcs_util):
136 print('dispatch-conf: Error finding all RCS utils and " + \
137 "use-rcs=yes in config; fatal', file=sys.stderr)
141 # config file freezing support
142 frozen_files = set(self.options.get("frozen-files", "").split())
144 protect_obj = portage.util.ConfigProtect(
145 config_root, config_paths,
146 portage.util.shlex_split(
147 portage.settings.get('CONFIG_PROTECT_MASK', '')))
149 def diff(file1, file2):
150 return diffstatusoutput(DIFF_CONTENTS, file1, file2)
153 # Remove new configs identical to current
155 # Auto-replace configs a) whose differences are simply CVS interpolations,
156 # or b) whose differences are simply ws or comments,
157 # or c) in paths now unprotected by CONFIG_PROTECT_MASK,
161 mrgconf = re.sub(r'\._cfg', '._mrg', conf['new'])
162 archive = os.path.join(self.options['archive-dir'], conf['current'].lstrip('/'))
163 if self.options['use-rcs'] == 'yes':
164 mrgfail = portage.dispatch_conf.rcs_archive(archive, conf['current'], conf['new'], mrgconf)
166 mrgfail = portage.dispatch_conf.file_archive(archive, conf['current'], conf['new'], mrgconf)
167 if os.path.exists(archive + '.dist'):
168 unmodified = len(diff(conf['current'], archive + '.dist')[1]) == 0
171 if os.path.exists(mrgconf):
172 if mrgfail or len(diff(conf['new'], mrgconf)[1]) == 0:
174 newconf = conf['new']
178 newconf = conf['new']
180 if newconf == mrgconf and \
181 self.options.get('ignore-previously-merged') != 'yes' and \
182 os.path.exists(archive+'.dist') and \
183 len(diff(archive+'.dist', conf['new'])[1]) == 0:
184 # The current update is identical to the archived .dist
185 # version that has previously been merged.
187 newconf = conf['new']
189 mystatus, myoutput = diff(conf['current'], newconf)
190 myoutput_len = len(myoutput)
191 same_file = 0 == myoutput_len
192 if mystatus >> 8 == 2:
193 # Binary files differ
197 # Extract all the normal diff lines (ignore the headers).
198 mylines = re.findall('^[+-][^\n+-].*$', myoutput, re.MULTILINE)
200 # Filter out all the cvs headers
201 cvs_header = re.compile('# [$]Header:')
202 cvs_lines = list(filter(cvs_header.search, mylines))
203 same_cvs = len(mylines) == len(cvs_lines)
205 # Filter out comments and whitespace-only changes.
206 # Note: be nice to also ignore lines that only differ in whitespace...
208 for x in ['^[-+]\s*#', '^[-+]\s*$']:
209 wsc_lines += list(filter(re.compile(x).match, mylines))
210 same_wsc = len(mylines) == len(wsc_lines)
213 same_cvs = same_cvs and self.options['replace-cvs'] == 'yes'
214 same_wsc = same_wsc and self.options['replace-wscomments'] == 'yes'
215 unmodified = unmodified and self.options['replace-unmodified'] == 'yes'
218 os.unlink (conf ['new'])
219 self.post_process(conf['current'])
220 if os.path.exists(mrgconf):
223 elif conf['current'] in frozen_files:
224 """Frozen files are automatically zapped. The new config has
225 already been archived with a .new suffix. When zapped, it is
226 left with the .new suffix (post_process is skipped), since it
227 hasn't been merged into the current config."""
228 auto_zapped.append(conf['current'])
229 os.unlink(conf['new'])
235 elif unmodified or same_cvs or same_wsc or \
236 not protect_obj.isprotected(conf['current']):
237 self.replace(newconf, conf['current'])
238 self.post_process(conf['current'])
239 if newconf == mrgconf:
240 os.unlink(conf['new'])
241 elif os.path.exists(mrgconf):
247 confs = [x for x in confs if f(x)]
250 # Interactively process remaining
253 valid_input = "qhtnmlezu"
258 newconf = conf['new']
259 mrgconf = re.sub(r'\._cfg', '._mrg', newconf)
260 if os.path.exists(mrgconf):
267 cmd = self.options['diff'] % (conf['new'], mrgconf)
272 cmd = self.options['diff'] % (conf['current'], newconf)
277 print('>> (%i of %i) -- %s' % (count, len(confs), conf ['current']))
278 print('>> q quit, h help, n next, e edit-new, z zap-new, u use-new\n m merge, t toggle-merge, l look-merge: ', end=' ')
280 # In some cases getch() will return some spurious characters
281 # that do not represent valid input. If we don't validate the
282 # input then the spurious characters can cause us to jump
283 # back into the above "diff" command immediatly after the user
284 # has exited it (which can be quite confusing and gives an
285 # "out of control" feeling).
289 sys.stdout.write('\n')
299 if newconf == mrgconf:
300 newconf = conf['new']
301 elif os.path.exists(mrgconf):
307 merged = SCRATCH_DIR+"/"+os.path.basename(conf['current'])
309 ret = os.system (self.options['merge'] % (merged, conf ['current'], newconf))
310 ret = os.WEXITSTATUS(ret)
314 print("Failure running 'merge' command")
316 shutil.copyfile(merged, mrgconf)
318 mystat = os.lstat(conf['new'])
319 os.chmod(mrgconf, mystat[ST_MODE])
320 os.chown(mrgconf, mystat[ST_UID], mystat[ST_GID])
327 if 'EDITOR' not in os.environ:
328 os.environ['EDITOR']='nano'
329 os.system(os.environ['EDITOR'] + ' ' + newconf)
332 os.unlink(conf['new'])
333 if os.path.exists(mrgconf):
337 self.replace(newconf, conf ['current'])
338 self.post_process(conf['current'])
339 if newconf == mrgconf:
340 os.unlink(conf['new'])
341 elif os.path.exists(mrgconf):
345 raise AssertionError("Invalid Input: %s" % c)
349 print(" One or more updates are frozen and have been automatically zapped:")
351 for frozen in auto_zapped:
352 print(" * '%s'" % frozen)
355 def replace (self, newconf, curconf):
356 """Replace current config with the new/merged version. Also logs
357 the diff of what changed into the configured log file."""
358 os.system((DIFF_CONTENTS % (curconf, newconf)) + '>>' + self.options["log-file"])
360 os.rename(newconf, curconf)
361 except (IOError, os.error) as why:
362 print('dispatch-conf: Error renaming %s to %s: %s; fatal' % \
363 (newconf, curconf, str(why)), file=sys.stderr)
366 def post_process(self, curconf):
367 archive = os.path.join(self.options['archive-dir'], curconf.lstrip('/'))
368 if self.options['use-rcs'] == 'yes':
369 portage.dispatch_conf.rcs_archive_post_process(archive)
371 portage.dispatch_conf.file_archive_post_process(archive)
374 def massage (self, newconfigs):
375 """Sort, rstrip, remove old versions, break into triad hash.
377 Triad is dictionary of current (/etc/make.conf), new (/etc/._cfg0003_make.conf)
380 We keep ._cfg0002_conf over ._cfg0001_conf and ._cfg0000_conf.
386 for nconf in newconfigs:
387 nconf = nconf.rstrip ()
388 conf = re.sub (r'\._cfg\d+_', '', nconf)
389 dirname = os.path.dirname(nconf)
397 mrgconf = re.sub(r'\._cfg', '._mrg', h[conf]['new'])
398 if os.path.exists(mrgconf):
400 os.unlink(h[conf]['new'])
401 h[conf].update(conf_map)
404 configs.append(conf_map)
413 print(' u -- update current config with new config and continue')
414 print(' z -- zap (delete) new config and continue')
415 print(' n -- skip to next config, leave all intact')
416 print(' e -- edit new config')
417 print(' m -- interactively merge current and new configs')
418 print(' l -- look at diff between pre-merged and merged configs')
419 print(' t -- toggle new config between merged and pre-merged state')
420 print(' h -- this screen')
423 print(); print('press any key to return to diff...', end=' ')
429 # from ASPN - Danny Yoo
433 fd = sys.stdin.fileno()
434 old_settings = termios.tcgetattr(fd)
436 tty.setraw(sys.stdin.fileno())
437 ch = sys.stdin.read(1)
439 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
447 sys.stdout.write(_unicode_decode(curses.tigetstr("clear")))
454 os.system("clear 2>/dev/null")
456 shell = os.environ.get("SHELL")
457 if not shell or not os.access(shell, os.EX_OK):
458 shell = find_binary("sh")
460 def spawn_shell(cmd):
462 sys.__stdout__.flush()
463 sys.__stderr__.flush()
464 spawn([shell, "-c", cmd], env=os.environ,
465 fd_pipes = { 0 : portage._get_stdin().fileno(),
466 1 : sys.__stdout__.fileno(),
467 2 : sys.__stderr__.fileno()})
472 print('dispatch-conf: sane configuration file update\n')
473 print('Usage: dispatch-conf [config dirs]\n')
474 print('See the dispatch-conf(1) man page for more details')
478 if x in ('-h', '--help'):
480 elif x in ('--version'):
481 print("Portage", portage.VERSION)
487 if len(sys.argv) > 1:
489 d.grind(sys.argv[1:])
491 d.grind(portage.util.shlex_split(
492 portage.settings.get('CONFIG_PROTECT', '')))