| #!/usr/bin/python -bO |
| # Copyright 1999-2014 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| # |
| # dispatch-conf -- Integrate modified configs, post-emerge |
| # |
| # Jeremy Wohl (http://igmus.org) |
| # |
| # TODO |
| # dialog menus |
| # |
| |
| from __future__ import print_function |
| |
| from stat import ST_GID, ST_MODE, ST_UID |
| from random import random |
| import atexit, re, shutil, stat, sys |
| from os import path as osp |
| pym_path = osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym") |
| sys.path.insert(0, pym_path) |
| import portage |
| portage._internal_caller = True |
| from portage import os |
| from portage import _unicode_decode |
| from portage.dispatch_conf import diffstatusoutput |
| from portage.process import find_binary, spawn |
| |
| FIND_EXTANT_CONFIGS = "find '%s' %s -name '._cfg????_%s' ! -name '.*~' ! -iname '.*.bak' -print" |
| DIFF_CONTENTS = "diff -Nu '%s' '%s'" |
| |
| # We need a secure scratch dir and python does silly verbose errors on the use of tempnam |
| oldmask = os.umask(0o077) |
| SCRATCH_DIR = None |
| while SCRATCH_DIR is None: |
| try: |
| mydir = "/tmp/dispatch-conf." |
| for x in range(0,8): |
| if int(random() * 3) == 0: |
| mydir += chr(int(65+random()*26.0)) |
| elif int(random() * 2) == 0: |
| mydir += chr(int(97+random()*26.0)) |
| else: |
| mydir += chr(int(48+random()*10.0)) |
| if os.path.exists(mydir): |
| continue |
| os.mkdir(mydir) |
| SCRATCH_DIR = mydir |
| except OSError as e: |
| if e.errno != 17: |
| raise |
| os.umask(oldmask) |
| |
| # Ensure the scratch dir is deleted |
| def cleanup(mydir=SCRATCH_DIR): |
| shutil.rmtree(mydir) |
| atexit.register(cleanup) |
| |
| MANDATORY_OPTS = [ 'archive-dir', 'diff', 'replace-cvs', 'replace-wscomments', 'merge' ] |
| |
| def cmd_var_is_valid(cmd): |
| """ |
| Return true if the first whitespace-separated token contained |
| in cmd is an executable file, false otherwise. |
| """ |
| cmd = portage.util.shlex_split(cmd) |
| if not cmd: |
| return False |
| |
| if os.path.isabs(cmd[0]): |
| return os.access(cmd[0], os.EX_OK) |
| |
| return find_binary(cmd[0]) is not None |
| |
| class dispatch: |
| options = {} |
| |
| def grind (self, config_paths): |
| confs = [] |
| count = 0 |
| |
| config_root = portage.settings["EPREFIX"] or os.sep |
| self.options = portage.dispatch_conf.read_config(MANDATORY_OPTS) |
| |
| if "log-file" in self.options: |
| if os.path.isfile(self.options["log-file"]): |
| shutil.copy(self.options["log-file"], self.options["log-file"] + '.old') |
| if os.path.isfile(self.options["log-file"]) \ |
| or not os.path.exists(self.options["log-file"]): |
| open(self.options["log-file"], 'w').close() # Truncate it |
| os.chmod(self.options["log-file"], 0o600) |
| else: |
| self.options["log-file"] = "/dev/null" |
| |
| pager = self.options.get("pager") |
| if pager is None or not cmd_var_is_valid(pager): |
| pager = os.environ.get("PAGER") |
| if pager is None or not cmd_var_is_valid(pager): |
| pager = "cat" |
| |
| pager_basename = os.path.basename(portage.util.shlex_split(pager)[0]) |
| if pager_basename == "less": |
| less_opts = self.options.get("less-opts") |
| if less_opts is not None and less_opts.strip(): |
| pager += " " + less_opts |
| |
| if pager_basename == "cat": |
| pager = "" |
| else: |
| pager = " | " + pager |
| |
| # |
| # Build list of extant configs |
| # |
| |
| for path in config_paths: |
| path = portage.normalize_path( |
| os.path.join(config_root, path.lstrip(os.sep))) |
| try: |
| mymode = os.stat(path).st_mode |
| except OSError: |
| continue |
| basename = "*" |
| find_opts = "-name '.*' -type d -prune -o" |
| if not stat.S_ISDIR(mymode): |
| path, basename = os.path.split(path) |
| find_opts = "-maxdepth 1" |
| |
| with os.popen(FIND_EXTANT_CONFIGS % |
| (path, find_opts, basename)) as proc: |
| confs += self.massage(proc.readlines()) |
| |
| if self.options['use-rcs'] == 'yes': |
| for rcs_util in ("rcs", "ci", "co", "rcsmerge"): |
| if not find_binary(rcs_util): |
| print('dispatch-conf: Error finding all RCS utils and " + \ |
| "use-rcs=yes in config; fatal', file=sys.stderr) |
| return False |
| |
| |
| # config file freezing support |
| frozen_files = set(self.options.get("frozen-files", "").split()) |
| auto_zapped = [] |
| protect_obj = portage.util.ConfigProtect( |
| config_root, config_paths, |
| portage.util.shlex_split( |
| portage.settings.get('CONFIG_PROTECT_MASK', ''))) |
| |
| def diff(file1, file2): |
| return diffstatusoutput(DIFF_CONTENTS, file1, file2) |
| |
| # |
| # Remove new configs identical to current |
| # and |
| # Auto-replace configs a) whose differences are simply CVS interpolations, |
| # or b) whose differences are simply ws or comments, |
| # or c) in paths now unprotected by CONFIG_PROTECT_MASK, |
| # |
| |
| def f (conf): |
| mrgconf = re.sub(r'\._cfg', '._mrg', conf['new']) |
| archive = os.path.join(self.options['archive-dir'], conf['current'].lstrip('/')) |
| if self.options['use-rcs'] == 'yes': |
| mrgfail = portage.dispatch_conf.rcs_archive(archive, conf['current'], conf['new'], mrgconf) |
| else: |
| mrgfail = portage.dispatch_conf.file_archive(archive, conf['current'], conf['new'], mrgconf) |
| if os.path.exists(archive + '.dist'): |
| unmodified = len(diff(conf['current'], archive + '.dist')[1]) == 0 |
| else: |
| unmodified = 0 |
| if os.path.exists(mrgconf): |
| if mrgfail or len(diff(conf['new'], mrgconf)[1]) == 0: |
| os.unlink(mrgconf) |
| newconf = conf['new'] |
| else: |
| newconf = mrgconf |
| else: |
| newconf = conf['new'] |
| |
| if newconf == mrgconf and \ |
| self.options.get('ignore-previously-merged') != 'yes' and \ |
| os.path.exists(archive+'.dist') and \ |
| len(diff(archive+'.dist', conf['new'])[1]) == 0: |
| # The current update is identical to the archived .dist |
| # version that has previously been merged. |
| os.unlink(mrgconf) |
| newconf = conf['new'] |
| |
| mystatus, myoutput = diff(conf['current'], newconf) |
| myoutput_len = len(myoutput) |
| same_file = 0 == myoutput_len |
| if mystatus >> 8 == 2: |
| # Binary files differ |
| same_cvs = False |
| same_wsc = False |
| else: |
| # Extract all the normal diff lines (ignore the headers). |
| mylines = re.findall('^[+-][^\n+-].*$', myoutput, re.MULTILINE) |
| |
| # Filter out all the cvs headers |
| cvs_header = re.compile('# [$]Header:') |
| cvs_lines = list(filter(cvs_header.search, mylines)) |
| same_cvs = len(mylines) == len(cvs_lines) |
| |
| # Filter out comments and whitespace-only changes. |
| # Note: be nice to also ignore lines that only differ in whitespace... |
| wsc_lines = [] |
| for x in ['^[-+]\s*#', '^[-+]\s*$']: |
| wsc_lines += list(filter(re.compile(x).match, mylines)) |
| same_wsc = len(mylines) == len(wsc_lines) |
| |
| # Do options permit? |
| same_cvs = same_cvs and self.options['replace-cvs'] == 'yes' |
| same_wsc = same_wsc and self.options['replace-wscomments'] == 'yes' |
| unmodified = unmodified and self.options['replace-unmodified'] == 'yes' |
| |
| if same_file: |
| os.unlink (conf ['new']) |
| self.post_process(conf['current']) |
| if os.path.exists(mrgconf): |
| os.unlink(mrgconf) |
| return False |
| elif conf['current'] in frozen_files: |
| """Frozen files are automatically zapped. The new config has |
| already been archived with a .new suffix. When zapped, it is |
| left with the .new suffix (post_process is skipped), since it |
| hasn't been merged into the current config.""" |
| auto_zapped.append(conf['current']) |
| os.unlink(conf['new']) |
| try: |
| os.unlink(mrgconf) |
| except OSError: |
| pass |
| return False |
| elif unmodified or same_cvs or same_wsc or \ |
| not protect_obj.isprotected(conf['current']): |
| self.replace(newconf, conf['current']) |
| self.post_process(conf['current']) |
| if newconf == mrgconf: |
| os.unlink(conf['new']) |
| elif os.path.exists(mrgconf): |
| os.unlink(mrgconf) |
| return False |
| else: |
| return True |
| |
| confs = [x for x in confs if f(x)] |
| |
| # |
| # Interactively process remaining |
| # |
| |
| valid_input = "qhtnmlezu" |
| |
| for conf in confs: |
| count = count + 1 |
| |
| newconf = conf['new'] |
| mrgconf = re.sub(r'\._cfg', '._mrg', newconf) |
| if os.path.exists(mrgconf): |
| newconf = mrgconf |
| show_new_diff = 0 |
| |
| while 1: |
| clear_screen() |
| if show_new_diff: |
| cmd = self.options['diff'] % (conf['new'], mrgconf) |
| cmd += pager |
| spawn_shell(cmd) |
| show_new_diff = 0 |
| else: |
| cmd = self.options['diff'] % (conf['current'], newconf) |
| cmd += pager |
| spawn_shell(cmd) |
| |
| print() |
| print('>> (%i of %i) -- %s' % (count, len(confs), conf ['current'])) |
| 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=' ') |
| |
| # In some cases getch() will return some spurious characters |
| # that do not represent valid input. If we don't validate the |
| # input then the spurious characters can cause us to jump |
| # back into the above "diff" command immediatly after the user |
| # has exited it (which can be quite confusing and gives an |
| # "out of control" feeling). |
| while True: |
| c = getch() |
| if c in valid_input: |
| sys.stdout.write('\n') |
| sys.stdout.flush() |
| break |
| |
| if c == 'q': |
| sys.exit (0) |
| if c == 'h': |
| self.do_help () |
| continue |
| elif c == 't': |
| if newconf == mrgconf: |
| newconf = conf['new'] |
| elif os.path.exists(mrgconf): |
| newconf = mrgconf |
| continue |
| elif c == 'n': |
| break |
| elif c == 'm': |
| merged = SCRATCH_DIR+"/"+os.path.basename(conf['current']) |
| print() |
| ret = os.system (self.options['merge'] % (merged, conf ['current'], newconf)) |
| ret = os.WEXITSTATUS(ret) |
| if ret < 2: |
| ret = 0 |
| if ret: |
| print("Failure running 'merge' command") |
| continue |
| shutil.copyfile(merged, mrgconf) |
| os.remove(merged) |
| mystat = os.lstat(conf['new']) |
| os.chmod(mrgconf, mystat[ST_MODE]) |
| os.chown(mrgconf, mystat[ST_UID], mystat[ST_GID]) |
| newconf = mrgconf |
| continue |
| elif c == 'l': |
| show_new_diff = 1 |
| continue |
| elif c == 'e': |
| if 'EDITOR' not in os.environ: |
| os.environ['EDITOR']='nano' |
| os.system(os.environ['EDITOR'] + ' ' + newconf) |
| continue |
| elif c == 'z': |
| os.unlink(conf['new']) |
| if os.path.exists(mrgconf): |
| os.unlink(mrgconf) |
| break |
| elif c == 'u': |
| self.replace(newconf, conf ['current']) |
| self.post_process(conf['current']) |
| if newconf == mrgconf: |
| os.unlink(conf['new']) |
| elif os.path.exists(mrgconf): |
| os.unlink(mrgconf) |
| break |
| else: |
| raise AssertionError("Invalid Input: %s" % c) |
| |
| if auto_zapped: |
| print() |
| print(" One or more updates are frozen and have been automatically zapped:") |
| print() |
| for frozen in auto_zapped: |
| print(" * '%s'" % frozen) |
| print() |
| |
| def replace (self, newconf, curconf): |
| """Replace current config with the new/merged version. Also logs |
| the diff of what changed into the configured log file.""" |
| os.system((DIFF_CONTENTS % (curconf, newconf)) + '>>' + self.options["log-file"]) |
| try: |
| os.rename(newconf, curconf) |
| except (IOError, os.error) as why: |
| print('dispatch-conf: Error renaming %s to %s: %s; fatal' % \ |
| (newconf, curconf, str(why)), file=sys.stderr) |
| |
| |
| def post_process(self, curconf): |
| archive = os.path.join(self.options['archive-dir'], curconf.lstrip('/')) |
| if self.options['use-rcs'] == 'yes': |
| portage.dispatch_conf.rcs_archive_post_process(archive) |
| else: |
| portage.dispatch_conf.file_archive_post_process(archive) |
| |
| |
| def massage (self, newconfigs): |
| """Sort, rstrip, remove old versions, break into triad hash. |
| |
| Triad is dictionary of current (/etc/make.conf), new (/etc/._cfg0003_make.conf) |
| and dir (/etc). |
| |
| We keep ._cfg0002_conf over ._cfg0001_conf and ._cfg0000_conf. |
| """ |
| h = {} |
| configs = [] |
| newconfigs.sort () |
| |
| for nconf in newconfigs: |
| nconf = nconf.rstrip () |
| conf = re.sub (r'\._cfg\d+_', '', nconf) |
| dirname = os.path.dirname(nconf) |
| conf_map = { |
| 'current' : conf, |
| 'dir' : dirname, |
| 'new' : nconf, |
| } |
| |
| if conf in h: |
| mrgconf = re.sub(r'\._cfg', '._mrg', h[conf]['new']) |
| if os.path.exists(mrgconf): |
| os.unlink(mrgconf) |
| os.unlink(h[conf]['new']) |
| h[conf].update(conf_map) |
| else: |
| h[conf] = conf_map |
| configs.append(conf_map) |
| |
| return configs |
| |
| |
| def do_help (self): |
| print() |
| print() |
| |
| print(' u -- update current config with new config and continue') |
| print(' z -- zap (delete) new config and continue') |
| print(' n -- skip to next config, leave all intact') |
| print(' e -- edit new config') |
| print(' m -- interactively merge current and new configs') |
| print(' l -- look at diff between pre-merged and merged configs') |
| print(' t -- toggle new config between merged and pre-merged state') |
| print(' h -- this screen') |
| print(' q -- quit') |
| |
| print(); print('press any key to return to diff...', end=' ') |
| |
| getch () |
| |
| |
| def getch (): |
| # from ASPN - Danny Yoo |
| # |
| import tty, termios |
| |
| fd = sys.stdin.fileno() |
| old_settings = termios.tcgetattr(fd) |
| try: |
| tty.setraw(sys.stdin.fileno()) |
| ch = sys.stdin.read(1) |
| finally: |
| termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) |
| return ch |
| |
| def clear_screen(): |
| try: |
| import curses |
| try: |
| curses.setupterm() |
| sys.stdout.write(_unicode_decode(curses.tigetstr("clear"))) |
| sys.stdout.flush() |
| return |
| except curses.error: |
| pass |
| except ImportError: |
| pass |
| os.system("clear 2>/dev/null") |
| |
| shell = os.environ.get("SHELL") |
| if not shell or not os.access(shell, os.EX_OK): |
| shell = find_binary("sh") |
| |
| def spawn_shell(cmd): |
| if shell: |
| sys.__stdout__.flush() |
| sys.__stderr__.flush() |
| spawn([shell, "-c", cmd], env=os.environ, |
| fd_pipes = { 0 : portage._get_stdin().fileno(), |
| 1 : sys.__stdout__.fileno(), |
| 2 : sys.__stderr__.fileno()}) |
| else: |
| os.system(cmd) |
| |
| def usage(argv): |
| print('dispatch-conf: sane configuration file update\n') |
| print('Usage: dispatch-conf [config dirs]\n') |
| print('See the dispatch-conf(1) man page for more details') |
| sys.exit(os.EX_OK) |
| |
| for x in sys.argv: |
| if x in ('-h', '--help'): |
| usage(sys.argv) |
| elif x in ('--version'): |
| print("Portage", portage.VERSION) |
| sys.exit(os.EX_OK) |
| |
| # run |
| d = dispatch () |
| |
| if len(sys.argv) > 1: |
| # for testing |
| d.grind(sys.argv[1:]) |
| else: |
| d.grind(portage.util.shlex_split( |
| portage.settings.get('CONFIG_PROTECT', ''))) |