2 # Copyright 1999-2006 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
7 # dispatch-conf -- Integrate modified configs, post-emerge
9 # Jeremy Wohl (http://igmus.org)
17 import atexit, commands, os, re, shutil, stat, string, sys
18 sys.path.insert(0, os.environ.get("PORTAGE_PYM_PATH", "/usr/lib/portage/pym"))
20 import portage, dispatch_conf
21 from portage_exec import find_binary
23 FIND_EXTANT_CONFIGS = "find '%s' %s -iname '._cfg????_%s'"
24 DIFF_CONTENTS = 'diff -Nu %s %s'
25 DIFF_CVS_INTERP = 'diff -Nu %s %s | grep "^[+-][^+-]" | grep -v "# .Header:.*"'
26 DIFF_WSCOMMENTS = 'diff -Nu %s %s | grep "^[+-][^+-]" | grep -v "^[-+]#" | grep -v "^[-+][:space:]*$"'
28 # We need a secure scratch dir and python does silly verbose errors on the use of tempnam
29 oldmask = os.umask(0077)
31 while SCRATCH_DIR is None:
33 mydir = "/tmp/dispatch-conf."
35 if int(random() * 3) == 0:
36 mydir += chr(int(65+random()*26.0))
37 elif int(random() * 2) == 0:
38 mydir += chr(int(97+random()*26.0))
40 mydir += chr(int(48+random()*10.0))
41 if os.path.exists(mydir):
50 # Ensure the scratch dir is deleted
51 def cleanup(mydir=SCRATCH_DIR):
53 atexit.register(cleanup)
55 MANDATORY_OPTS = [ 'archive-dir', 'diff', 'replace-cvs', 'replace-wscomments', 'merge' ]
60 def grind (self, config_paths):
65 self.options = dispatch_conf.read_config(MANDATORY_OPTS)
67 if self.options.has_key("log-file"):
68 if os.path.isfile(self.options["log-file"]):
69 shutil.copy(self.options["log-file"], self.options["log-file"] + '.old')
70 if os.path.isfile(self.options["log-file"]) \
71 or not os.path.exists(self.options["log-file"]):
72 open(self.options["log-file"], 'w').close() # Truncate it
73 os.chmod(self.options["log-file"], 0600)
75 self.options["log-file"] = "/dev/null"
78 # Build list of extant configs
81 for path in config_paths.split ():
82 path = portage.normalize_path(path)
84 mymode = os.lstat(path).st_mode
89 if not stat.S_ISDIR(mymode):
90 path, basename = os.path.split(path)
91 find_opts = "-maxdepth 1"
93 confs += self.massage(os.popen(FIND_EXTANT_CONFIGS % (path, find_opts, basename)).readlines())
95 if self.options['use-rcs'] == 'yes':
96 for rcs_util in ("rcs", "ci", "co", "rcsmerge"):
97 if not find_binary(rcs_util):
98 print >> sys.stderr, \
99 'dispatch-conf: Error finding all RCS utils and " + \
100 "use-rcs=yes in config; fatal'
105 # Remove new configs identical to current
107 # Auto-replace configs a) whose differences are simply CVS interpolations,
108 # or b) whose differences are simply ws or comments,
109 # or c) in paths now unprotected by CONFIG_PROTECT_MASK,
113 mrgconf = re.sub(r'\._cfg', '._mrg', conf['new'])
114 archive = os.path.join(self.options['archive-dir'], conf['current'].lstrip('/'))
115 if self.options['use-rcs'] == 'yes':
116 mrgfail = dispatch_conf.rcs_archive(archive, conf['current'], conf['new'], mrgconf)
118 mrgfail = dispatch_conf.file_archive(archive, conf['current'], conf['new'], mrgconf)
119 if os.path.exists(archive + '.dist'):
120 unmodified = len(commands.getoutput(DIFF_CONTENTS % (conf['current'], archive + '.dist'))) == 0
123 if os.path.exists(mrgconf):
124 if mrgfail or len(commands.getoutput(DIFF_CONTENTS % (conf['new'], mrgconf))) == 0:
126 newconf = conf['new']
130 newconf = conf['new']
132 same_file = len(commands.getoutput (DIFF_CONTENTS % (conf ['current'], newconf))) == 0
133 same_cvs = len(commands.getoutput (DIFF_CVS_INTERP % (conf ['current'], newconf))) == 0
134 same_wsc = len(commands.getoutput (DIFF_WSCOMMENTS % (conf ['current'], newconf))) == 0
137 same_cvs = same_cvs and self.options['replace-cvs'] == 'yes'
138 same_wsc = same_wsc and self.options['replace-wscomments'] == 'yes'
139 unmodified = unmodified and self.options['replace-unmodified'] == 'yes'
142 os.unlink (conf ['new'])
143 self.post_process(conf['current'])
144 if os.path.exists(mrgconf):
147 elif unmodified or same_cvs or same_wsc or conf ['dir'] in portage.settings ['CONFIG_PROTECT_MASK'].split ():
148 self.replace(newconf, conf['current'])
149 self.post_process(conf['current'])
150 if newconf == mrgconf:
151 os.unlink(conf['new'])
152 elif os.path.exists(mrgconf):
158 confs = filter (f, confs)
161 # Interactively process remaining
167 newconf = conf['new']
168 mrgconf = re.sub(r'\._cfg', '._mrg', newconf)
169 if os.path.exists(mrgconf):
175 os.system((self.options['diff']) % (conf['new'], mrgconf))
178 os.system((self.options['diff']) % (conf['current'], newconf))
181 print '>> (%i of %i) -- %s' % (count, len(confs), conf ['current'])
182 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: ',
192 if newconf == mrgconf:
193 newconf = conf['new']
194 elif os.path.exists(mrgconf):
200 merged = SCRATCH_DIR+"/"+os.path.basename(conf['current'])
202 ret = os.system (self.options['merge'] % (merged, conf ['current'], newconf))
204 print "Failure running 'merge' command"
206 shutil.copyfile(merged, mrgconf)
208 mystat = os.lstat(conf['new'])
209 os.chmod(mrgconf, mystat[ST_MODE])
210 os.chown(mrgconf, mystat[ST_UID], mystat[ST_GID])
217 if not os.environ.has_key('EDITOR'):
218 os.environ['EDITOR']='nano'
219 os.system(os.environ['EDITOR'] + ' ' + newconf)
222 os.unlink(conf['new'])
223 if os.path.exists(mrgconf):
227 self.replace(newconf, conf ['current'])
228 self.post_process(conf['current'])
229 if newconf == mrgconf:
230 os.unlink(conf['new'])
231 elif os.path.exists(mrgconf):
238 def replace (self, newconf, curconf):
239 """Replace current config with the new/merged version. Also logs
240 the diff of what changed into the configured log file."""
241 os.system((DIFF_CONTENTS % (curconf, newconf)) + '>>' + self.options["log-file"])
243 shutil.copyfile(newconf, curconf)
245 except (IOError, os.error), why:
246 print >> sys.stderr, 'dispatch-conf: Error renaming %s to %s: %s; fatal' % \
247 (newconf, curconf, str(why))
250 def post_process(self, curconf):
251 archive = os.path.join(self.options['archive-dir'], curconf.lstrip('/'))
252 if self.options['use-rcs'] == 'yes':
253 dispatch_conf.rcs_archive_post_process(archive)
255 dispatch_conf.file_archive_post_process(archive)
258 def massage (self, newconfigs):
259 """Sort, rstrip, remove old versions, break into triad hash.
261 Triad is dictionary of current (/etc/make.conf), new (/etc/._cfg0003_make.conf)
264 We keep ._cfg0002_conf over ._cfg0001_conf and ._cfg0000_conf.
270 for nconf in newconfigs:
271 nconf = nconf.rstrip ()
272 conf = re.sub (r'\._cfg\d+_', '', nconf)
273 dir = re.match (r'^(.+)/', nconf).group (1)
276 mrgconf = re.sub(r'\._cfg', '._mrg', h[conf]['new'])
277 if os.path.exists(mrgconf):
279 os.unlink(h[conf]['new'])
281 h [conf] = { 'current' : conf, 'dir' : dir, 'new' : nconf }
283 configs = h.values ()
284 configs.sort (lambda a, b: cmp(a ['current'], b ['current']))
292 print ' u -- update current config with new config and continue'
293 print ' z -- zap (delete) new config and continue'
294 print ' n -- skip to next config, leave all intact'
295 print ' e -- edit new config'
296 print ' m -- interactively merge current and new configs'
297 print ' l -- look at diff between pre-merged and merged configs'
298 print ' t -- toggle new config between merged and pre-merged state'
299 print ' h -- this screen'
302 print; print 'press any key to return to diff...',
308 # from ASPN - Danny Yoo
310 import sys, tty, termios
312 fd = sys.stdin.fileno()
313 old_settings = termios.tcgetattr(fd)
315 tty.setraw(sys.stdin.fileno())
316 ch = sys.stdin.read(1)
318 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
325 if len(sys.argv) > 1:
327 d.grind (string.join (sys.argv [1:]))
329 d.grind (portage.settings ['CONFIG_PROTECT'])