streamline portage pythonpath handling and add an override analog to how the bash...
[portage.git] / bin / dispatch-conf
1 #!/usr/bin/python -O
2 # Copyright 1999-2006 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4 # $Id$
5
6 #
7 # dispatch-conf -- Integrate modified configs, post-emerge
8 #
9 #  Jeremy Wohl (http://igmus.org)
10 #
11 # TODO
12 #  dialog menus
13 #
14
15 from stat import *
16 from random import *
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"))
19
20 import portage, dispatch_conf
21 from portage_exec import find_binary
22
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:]*$"'
27
28 # We need a secure scratch dir and python does silly verbose errors on the use of tempnam
29 oldmask = os.umask(0077)
30 SCRATCH_DIR = None
31 while SCRATCH_DIR is None:
32     try:
33         mydir = "/tmp/dispatch-conf."
34         for x in range(0,8):
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))
39             else:
40                 mydir += chr(int(48+random()*10.0))
41         if os.path.exists(mydir):
42             continue
43         os.mkdir(mydir)
44         SCRATCH_DIR = mydir
45     except OSError, e:
46         if e.errno != 17:
47             raise
48 os.umask(oldmask)
49
50 # Ensure the scratch dir is deleted
51 def cleanup(mydir=SCRATCH_DIR):
52     shutil.rmtree(mydir)
53 atexit.register(cleanup)
54
55 MANDATORY_OPTS  = [ 'archive-dir', 'diff', 'replace-cvs', 'replace-wscomments', 'merge' ]
56
57 class dispatch:
58     options = {}
59
60     def grind (self, config_paths):
61         confs = []
62         count = 0
63
64
65         self.options = dispatch_conf.read_config(MANDATORY_OPTS)
66
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)
74         else:
75             self.options["log-file"] = "/dev/null"
76
77         #
78         # Build list of extant configs
79         #
80
81         for path in config_paths.split ():
82             path = portage.normalize_path(path)
83             try:
84                 mymode = os.lstat(path).st_mode
85             except OSError:
86                 continue
87             basename = "*"
88             find_opts = ""
89             if not stat.S_ISDIR(mymode):
90                 path, basename = os.path.split(path)
91                 find_opts = "-maxdepth 1"
92
93             confs += self.massage(os.popen(FIND_EXTANT_CONFIGS % (path, find_opts, basename)).readlines())
94
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'
101                     return False
102
103
104         #
105         # Remove new configs identical to current
106         #                  and
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,
110         #
111
112         def f (conf):
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)
117             else:
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
121             else:
122                 unmodified = 0
123             if os.path.exists(mrgconf):
124                 if mrgfail or len(commands.getoutput(DIFF_CONTENTS % (conf['new'], mrgconf))) == 0:
125                     os.unlink(mrgconf)
126                     newconf = conf['new']
127                 else:
128                     newconf = mrgconf
129             else:
130                 newconf = conf['new']
131
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
135
136             # Do options permit?
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'
140
141             if same_file:
142                 os.unlink (conf ['new'])
143                 self.post_process(conf['current'])
144                 if os.path.exists(mrgconf):
145                     os.unlink(mrgconf)
146                 return False
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):
153                     os.unlink(mrgconf)
154                 return False
155             else:
156                 return True
157
158         confs = filter (f, confs)
159
160         #
161         # Interactively process remaining
162         #
163
164         for conf in confs:
165             count = count + 1
166
167             newconf = conf['new']
168             mrgconf = re.sub(r'\._cfg', '._mrg', newconf)
169             if os.path.exists(mrgconf):
170                 newconf = mrgconf
171             show_new_diff = 0
172
173             while 1:
174                 if show_new_diff:
175                     os.system((self.options['diff']) % (conf['new'], mrgconf))
176                     show_new_diff = 0
177                 else:
178                     os.system((self.options['diff']) % (conf['current'], newconf))
179
180                 print
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: ',
183
184                 c = getch ()
185
186                 if c == 'q':
187                     sys.exit (0)
188                 if c == 'h':
189                     self.do_help ()
190                     continue
191                 elif c == 't':
192                     if newconf == mrgconf:
193                         newconf = conf['new']
194                     elif os.path.exists(mrgconf):
195                         newconf = mrgconf
196                     continue
197                 elif c == 'n':
198                     break
199                 elif c == 'm':
200                     merged = SCRATCH_DIR+"/"+os.path.basename(conf['current'])
201                     print
202                     ret = os.system (self.options['merge'] % (merged, conf ['current'], newconf))
203                     if ret:
204                         print "Failure running 'merge' command"
205                         continue
206                     shutil.copyfile(merged, mrgconf)
207                     os.remove(merged)
208                     mystat = os.lstat(conf['new'])
209                     os.chmod(mrgconf, mystat[ST_MODE])
210                     os.chown(mrgconf, mystat[ST_UID], mystat[ST_GID])
211                     newconf = mrgconf
212                     continue
213                 elif c == 'l':
214                     show_new_diff = 1
215                     continue
216                 elif c == 'e':
217                     if not os.environ.has_key('EDITOR'):
218                         os.environ['EDITOR']='nano'
219                     os.system(os.environ['EDITOR'] + ' ' + newconf)
220                     continue
221                 elif c == 'z':
222                     os.unlink(conf['new'])
223                     if os.path.exists(mrgconf):
224                         os.unlink(mrgconf)
225                     break
226                 elif c == 'u':
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):
232                         os.unlink(mrgconf)
233                     break
234                 else:
235                     continue
236
237
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"])
242         try:
243             shutil.copyfile(newconf, curconf)
244             os.remove(newconf)
245         except (IOError, os.error), why:
246             print >> sys.stderr, 'dispatch-conf: Error renaming %s to %s: %s; fatal' % \
247                   (newconf, curconf, str(why))
248
249
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)
254         else:
255             dispatch_conf.file_archive_post_process(archive)
256
257
258     def massage (self, newconfigs):
259         """Sort, rstrip, remove old versions, break into triad hash.
260
261         Triad is dictionary of current (/etc/make.conf), new (/etc/._cfg0003_make.conf)
262         and dir (/etc).
263
264         We keep ._cfg0002_conf over ._cfg0001_conf and ._cfg0000_conf.
265         """
266         h = {}
267
268         newconfigs.sort ()
269
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)
274
275             if h.has_key (conf):
276                 mrgconf = re.sub(r'\._cfg', '._mrg', h[conf]['new'])
277                 if os.path.exists(mrgconf):
278                     os.unlink(mrgconf)
279                 os.unlink(h[conf]['new'])
280
281             h [conf] = { 'current' : conf, 'dir' : dir, 'new' : nconf }
282
283         configs = h.values ()
284         configs.sort (lambda a, b: cmp(a ['current'], b ['current']))
285
286         return configs
287
288
289     def do_help (self):
290         print; print
291
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'
300         print '  q -- quit'
301
302         print; print 'press any key to return to diff...',
303
304         getch ()
305
306
307 def getch ():
308     # from ASPN - Danny Yoo
309     #
310     import sys, tty, termios
311
312     fd = sys.stdin.fileno()
313     old_settings = termios.tcgetattr(fd)
314     try:
315         tty.setraw(sys.stdin.fileno())
316         ch = sys.stdin.read(1)
317     finally:
318         termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
319     return ch
320
321
322 # run
323 d = dispatch ()
324
325 if len(sys.argv) > 1:
326     # for testing
327     d.grind (string.join (sys.argv [1:]))
328 else:
329     d.grind (portage.settings ['CONFIG_PROTECT'])