-#!/usr/bin/python -O
-# Copyright 1999-2006 Gentoo Foundation
+#!/usr/bin/python -bbO
+# Copyright 1999-2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
-# $Id$
#
# dispatch-conf -- Integrate modified configs, post-emerge
# dialog menus
#
-if not hasattr(__builtins__, "set"):
- from sets import Set as set
+from __future__ import print_function
-from stat import *
-from random import *
-import atexit, commands, os, re, shutil, stat, sys
-try:
- import portage
-except ImportError:
- sys.path.insert(0, "/usr/lib/portage/pym")
- import portage
+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
-import dispatch_conf
-from portage.process import find_binary
-
-FIND_EXTANT_CONFIGS = "find '%s' %s -iname '._cfg????_%s' ! -iname '.*~' ! -iname '.*.bak'"
-DIFF_CONTENTS = 'diff -Nu %s %s'
-DIFF_CVS_INTERP = 'diff -Nu %s %s | grep "^[+-][^+-]" | grep -v "# .Header:.*"'
-DIFF_WSCOMMENTS = 'diff -Nu %s %s | grep "^[+-][^+-]" | grep -v "^[-+]#" | grep -v "^[-+][:space:]*$"'
+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(0077)
+oldmask = os.umask(0o077)
SCRATCH_DIR = None
while SCRATCH_DIR is None:
try:
continue
os.mkdir(mydir)
SCRATCH_DIR = mydir
- except OSError, e:
+ except OSError as e:
if e.errno != 17:
raise
os.umask(oldmask)
shutil.rmtree(mydir)
atexit.register(cleanup)
-MANDATORY_OPTS = [ 'archive-dir', 'diff', 'replace-cvs', 'replace-wscomments', 'merge' ]
+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 = {}
confs = []
count = 0
-
+ config_root = portage.settings["EPREFIX"] or os.sep
self.options = portage.dispatch_conf.read_config(MANDATORY_OPTS)
- if self.options.has_key("log-file"):
+ 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"], 0600)
+ 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.split ():
- path = portage.normalize_path(path)
+ 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 = ""
+ find_opts = "-name '.*' -type d -prune -o"
if not stat.S_ISDIR(mymode):
path, basename = os.path.split(path)
find_opts = "-maxdepth 1"
- confs += self.massage(os.popen(FIND_EXTANT_CONFIGS % (path, find_opts, basename)).readlines())
+ 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 >> sys.stderr, \
- 'dispatch-conf: Error finding all RCS utils and " + \
- "use-rcs=yes in config; fatal'
+ 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
else:
mrgfail = portage.dispatch_conf.file_archive(archive, conf['current'], conf['new'], mrgconf)
if os.path.exists(archive + '.dist'):
- unmodified = len(commands.getoutput(DIFF_CONTENTS % (conf['current'], archive + '.dist'))) == 0
+ unmodified = len(diff(conf['current'], archive + '.dist')[1]) == 0
else:
unmodified = 0
if os.path.exists(mrgconf):
- if mrgfail or len(commands.getoutput(DIFF_CONTENTS % (conf['new'], mrgconf))) == 0:
+ if mrgfail or len(diff(conf['new'], mrgconf)[1]) == 0:
os.unlink(mrgconf)
newconf = conf['new']
else:
else:
newconf = conf['new']
- mystatus, myoutput = commands.getstatusoutput(
- DIFF_CONTENTS % (conf ['current'], newconf))
- same_file = 0 == len(myoutput)
+ 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:
- same_cvs = 0 == len(commands.getoutput(
- DIFF_CVS_INTERP % (conf ['current'], newconf)))
- same_wsc = 0 == len(commands.getoutput(
- DIFF_WSCOMMENTS % (conf ['current'], newconf)))
+ # 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'
except OSError:
pass
return False
- elif unmodified or same_cvs or same_wsc or conf ['dir'] in portage.settings ['CONFIG_PROTECT_MASK'].split ():
+ 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:
else:
return True
- confs = filter (f, confs)
+ confs = [x for x in confs if f(x)]
#
# Interactively process remaining
#
+ valid_input = "qhtnmlezu"
+
for conf in confs:
count = count + 1
show_new_diff = 0
while 1:
+ clear_screen()
if show_new_diff:
- os.system((self.options['diff']) % (conf['new'], mrgconf))
+ cmd = self.options['diff'] % (conf['new'], mrgconf)
+ cmd += pager
+ spawn_shell(cmd)
show_new_diff = 0
else:
- os.system((self.options['diff']) % (conf['current'], newconf))
-
- 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: ',
-
- c = getch ()
+ 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)
break
elif c == 'm':
merged = SCRATCH_DIR+"/"+os.path.basename(conf['current'])
- print
+ 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"
+ print("Failure running 'merge' command")
continue
shutil.copyfile(merged, mrgconf)
os.remove(merged)
show_new_diff = 1
continue
elif c == 'e':
- if not os.environ.has_key('EDITOR'):
+ if 'EDITOR' not in os.environ:
os.environ['EDITOR']='nano'
os.system(os.environ['EDITOR'] + ' ' + newconf)
continue
os.unlink(mrgconf)
break
else:
- continue
+ raise AssertionError("Invalid Input: %s" % c)
if auto_zapped:
- print
- print " One or more updates are frozen and have been automatically zapped:"
- print
+ print()
+ print(" One or more updates are frozen and have been automatically zapped:")
+ print()
for frozen in auto_zapped:
- print " * '%s'" % frozen
- print
+ print(" * '%s'" % frozen)
+ print()
def replace (self, newconf, curconf):
"""Replace current config with the new/merged version. Also logs
os.system((DIFF_CONTENTS % (curconf, newconf)) + '>>' + self.options["log-file"])
try:
os.rename(newconf, curconf)
- except (IOError, os.error), why:
- print >> sys.stderr, 'dispatch-conf: Error renaming %s to %s: %s; fatal' % \
- (newconf, curconf, str(why))
+ 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):
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)
- dir = re.match (r'^(.+)/', nconf).group (1)
-
- if h.has_key (conf):
+ 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] = { 'current' : conf, 'dir' : dir, 'new' : nconf }
-
- configs = h.values ()
- configs.sort (lambda a, b: cmp(a ['current'], b ['current']))
+ h[conf].update(conf_map)
+ else:
+ h[conf] = conf_map
+ configs.append(conf_map)
return configs
def do_help (self):
- print; print
+ 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(' 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...',
+ print(); print('press any key to return to diff...', end=' ')
getch ()
def getch ():
# from ASPN - Danny Yoo
#
- import sys, tty, termios
+ import tty, termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
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 (" ".join(sys.argv[1:]))
+ d.grind(sys.argv[1:])
else:
- d.grind (portage.settings ['CONFIG_PROTECT'])
+ d.grind(portage.util.shlex_split(
+ portage.settings.get('CONFIG_PROTECT', '')))