blob: 94e2d6cac4f4ebbecbe21ef00bf53df936629c98 [file] [log] [blame]
#!/usr/bin/python -O
# Copyright 1999-2006 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Id$
#
# dispatch-conf -- Integrate modified configs, post-emerge
#
# Jeremy Wohl (http://igmus.org)
#
# TODO
# dialog menus
#
if not hasattr(__builtins__, "set"):
from sets import Set as set
from stat import *
from random import *
import atexit, commands, os, re, shutil, stat, sys
try:
import portage
except ImportError:
from os import path as osp
sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
import portage
import dispatch_conf
from portage_exec 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:]*$'"
# We need a secure scratch dir and python does silly verbose errors on the use of tempnam
oldmask = os.umask(0077)
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, 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' ]
class dispatch:
options = {}
def grind (self, config_paths):
confs = []
count = 0
self.options = dispatch_conf.read_config(MANDATORY_OPTS)
if self.options.has_key("log-file"):
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)
else:
self.options["log-file"] = "/dev/null"
#
# Build list of extant configs
#
for path in config_paths.split ():
path = portage.normalize_path(path)
try:
mymode = os.stat(path).st_mode
except OSError:
continue
basename = "*"
find_opts = ""
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())
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'
return False
# config file freezing support
frozen_files = set(self.options.get("frozen-files", "").split())
auto_zapped = []
#
# 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 = dispatch_conf.rcs_archive(archive, conf['current'], conf['new'], mrgconf)
else:
mrgfail = 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
else:
unmodified = 0
if os.path.exists(mrgconf):
if mrgfail or len(commands.getoutput(DIFF_CONTENTS % (conf['new'], mrgconf))) == 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(commands.getoutput(DIFF_CONTENTS % (archive+'.dist', conf['new']))) == 0:
# The current update is identical to the archived .dist
# version that has previously been merged.
os.unlink(mrgconf)
newconf = conf['new']
mystatus, myoutput = commands.getstatusoutput(
DIFF_CONTENTS % (conf ['current'], newconf))
same_file = 0 == len(myoutput)
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)))
# 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 conf ['dir'] in portage.settings ['CONFIG_PROTECT_MASK'].split ():
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 = filter (f, confs)
#
# 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:
os.system((self.options['diff']) % (conf['new'], mrgconf))
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: ',
# 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:
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))
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 not os.environ.has_key('EDITOR'):
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), why:
print >> sys.stderr, 'dispatch-conf: Error renaming %s to %s: %s; fatal' % \
(newconf, curconf, str(why))
def post_process(self, curconf):
archive = os.path.join(self.options['archive-dir'], curconf.lstrip('/'))
if self.options['use-rcs'] == 'yes':
dispatch_conf.rcs_archive_post_process(archive)
else:
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 = {}
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):
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']))
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...',
getch ()
def getch ():
# from ASPN - Danny Yoo
#
import sys, 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(curses.tigetstr("clear"))
sys.stdout.flush()
return
except curses.error:
pass
except ImportError:
pass
os.system("clear 2>/dev/null")
# run
d = dispatch ()
if len(sys.argv) > 1:
# for testing
d.grind (" ".join(sys.argv[1:]))
else:
d.grind (portage.settings ['CONFIG_PROTECT'])