keep track of remote branches locally to be able to push w/o errors
[build.git] / module-tools.py
1 #!/usr/bin/python -u
2
3 import sys, os
4 import re
5 import time
6 from glob import glob
7 from optparse import OptionParser
8
9 # HARDCODED NAME CHANGES
10 #
11 # Moving to git we decided to rename some of the repositories. Here is
12 # a map of name changes applied in git repositories.
13 RENAMED_SVN_MODULES = {
14     "PLEWWW": "plewww",
15     "PLCAPI": "plcapi",
16     "BootManager": "bootmanager",
17     "BootCD": "bootcd",
18     "VserverReference": "vserver-reference",
19     "BootstrapFS": "bootstrapfs",
20     "MyPLC": "myplc",
21     "CoDemux": "codemux",
22     "NodeManager": "nodemanager",
23     "NodeUpdate": "nodeupdate"
24     }
25
26 def svn_to_git_name(module):
27     if RENAMED_SVN_MODULES.has_key(module):
28         return RENAMED_SVN_MODULES[module]
29     return module
30
31 def git_to_svn_name(module):
32     for key in RENAMED_SVN_MODULES:
33         if module == RENAMED_SVN_MODULES[key]:
34             return key
35     return module
36     
37
38 # e.g. other_choices = [ ('d','iff') , ('g','uess') ] - lowercase 
39 def prompt (question,default=True,other_choices=[],allow_outside=False):
40     if not isinstance (other_choices,list):
41         other_choices = [ other_choices ]
42     chars = [ c for (c,rest) in other_choices ]
43
44     choices = []
45     if 'y' not in chars:
46         if default is True: choices.append('[y]')
47         else : choices.append('y')
48     if 'n' not in chars:
49         if default is False: choices.append('[n]')
50         else : choices.append('n')
51
52     for (char,choice) in other_choices:
53         if default == char:
54             choices.append("["+char+"]"+choice)
55         else:
56             choices.append("<"+char+">"+choice)
57     try:
58         answer=raw_input(question + " " + "/".join(choices) + " ? ")
59         if not answer:
60             return default
61         answer=answer[0].lower()
62         if answer == 'y':
63             if 'y' in chars: return 'y'
64             else: return True
65         elif answer == 'n':
66             if 'n' in chars: return 'n'
67             else: return False
68         elif other_choices:
69             for (char,choice) in other_choices:
70                 if answer == char:
71                     return char
72             if allow_outside:
73                 return answer
74         return prompt(question,default,other_choices)
75     except:
76         raise
77
78 def default_editor():
79     try:
80         editor = os.environ['EDITOR']
81     except:
82         editor = "emacs"
83     return editor
84
85 ### fold long lines
86 fold_length=132
87
88 def print_fold (line):
89     while len(line) >= fold_length:
90         print line[:fold_length],'\\'
91         line=line[fold_length:]
92     print line
93
94 class Command:
95     def __init__ (self,command,options):
96         self.command=command
97         self.options=options
98         self.tmp="/tmp/command-%d"%os.getpid()
99
100     def run (self):
101         if self.options.dry_run:
102             print 'dry_run',self.command
103             return 0
104         if self.options.verbose and self.options.mode not in Main.silent_modes:
105             print '+',self.command
106             sys.stdout.flush()
107         return os.system(self.command)
108
109     def run_silent (self):
110         if self.options.dry_run:
111             print 'dry_run',self.command
112             return 0
113         if self.options.verbose:
114             print '+',self.command,' .. ',
115             sys.stdout.flush()
116         retcod=os.system(self.command + " &> " + self.tmp)
117         if retcod != 0:
118             print "FAILED ! -- out+err below (command was %s)"%self.command
119             os.system("cat " + self.tmp)
120             print "FAILED ! -- end of quoted output"
121         elif self.options.verbose:
122             print "OK"
123         os.unlink(self.tmp)
124         return retcod
125
126     def run_fatal(self):
127         if self.run_silent() !=0:
128             raise Exception,"Command %s failed"%self.command
129
130     # returns stdout, like bash's $(mycommand)
131     def output_of (self,with_stderr=False):
132         if self.options.dry_run:
133             print 'dry_run',self.command
134             return 'dry_run output'
135         tmp="/tmp/status-%d"%os.getpid()
136         if self.options.debug:
137             print '+',self.command,' .. ',
138             sys.stdout.flush()
139         command=self.command
140         if with_stderr:
141             command += " &> "
142         else:
143             command += " > "
144         command += tmp
145         os.system(command)
146         result=file(tmp).read()
147         os.unlink(tmp)
148         if self.options.debug:
149             print 'Done',
150         return result
151
152
153 class SvnRepository:
154     type = "svn"
155
156     def __init__(self, path, options):
157         self.path = path
158         self.options = options
159
160     def name(self):
161         return os.path.basename(self.path)
162
163     def url(self):
164         out = Command("svn info %s" % self.path, self.options).output_of()
165         for line in out.split('\n'):
166             if line.startswith("URL:"):
167                 return line.split()[1].strip()
168
169     def repo_root(self):
170         out = Command("svn info %s" % self.path, self.options).output_of()
171         for line in out.split('\n'):
172             if line.startswith("Repository Root:"):
173                 root = line.split()[2].strip()
174                 return "%s/%s" % (root, self.name())
175
176     @classmethod
177     def checkout(cls, remote, local, options, recursive=False):
178         if recursive:
179             svncommand = "svn co %s %s" % (remote, local)
180         else:
181             svncommand = "svn co -N %s %s" % (remote, local)
182         Command("rm -rf %s" % local, options).run_silent()
183         Command(svncommand, options).run_fatal()
184
185         return SvnRepository(local, options)
186
187     @classmethod
188     def remote_exists(cls, remote):
189         return os.system("svn list %s &> /dev/null" % remote) == 0
190
191     def tag_exists(self, tagname):
192         url = "%s/tags/%s" % (self.repo_root(), tagname)
193         return SvnRepository.remote_exists(url)
194
195     def update(self, subdir="", recursive=True, branch=None):
196         path = os.path.join(self.path, subdir)
197         if recursive:
198             svncommand = "svn up %s" % path
199         else:
200             svncommand = "svn up -N %s" % path
201         Command(svncommand, self.options).run_fatal()
202
203     def commit(self, logfile):
204         # add all new files to the repository
205         Command("svn status %s | grep '^\?' | sed -e 's/? *//' | sed -e 's/ /\\ /g' | xargs svn add" %
206                 self.path, self.options).output_of()
207         Command("svn commit -F %s %s" % (logfile, self.path), self.options).run_fatal()
208
209     def to_branch(self, branch):
210         remote = "%s/branches/%s" % (self.repo_root(), branch)
211         SvnRepository.checkout(remote, self.path, self.options, recursive=True)
212
213     def to_tag(self, tag):
214         remote = "%s/tags/%s" % (self.repo_root(), branch)
215         SvnRepository.checkout(remote, self.path, self.options, recursive=True)
216
217     def tag(self, tagname, logfile):
218         tag_url = "%s/tags/%s" % (self.repo_root(), tagname)
219         self_url = self.url()
220         Command("svn copy -F %s %s %s" % (logfile, self_url, tag_url), self.options).run_fatal()
221
222     def diff(self, f=""):
223         if f:
224             f = os.path.join(self.path, f)
225         else:
226             f = self.path
227         return Command("svn diff %s" % f, self.options).output_of(True)
228
229     def diff_with_tag(self, tagname):
230         tag_url = "%s/tags/%s" % (self.repo_root(), tagname)
231         return Command("svn diff %s %s" % (tag_url, self.url()),
232                        self.options).output_of(True)
233
234     def revert(self, f=""):
235         if f:
236             Command("svn revert %s" % os.path.join(self.path, f), self.options).run_fatal()
237         else:
238             # revert all
239             Command("svn revert %s -R" % self.path, self.options).run_fatal()
240             Command("svn status %s | grep '^\?' | sed -e 's/? *//' | sed -e 's/ /\\ /g' | xargs rm -rf " %
241                     self.path, self.options).run_silent()
242
243     def is_clean(self):
244         command="svn status %s" % self.path
245         return len(Command(command,self.options).output_of(True)) == 0
246
247     def is_valid(self):
248         return os.path.exists(os.path.join(self.path, ".svn"))
249     
250
251 class GitRepository:
252     type = "git"
253
254     def __init__(self, path, options):
255         self.path = path
256         self.options = options
257
258     def name(self):
259         return os.path.basename(self.path)
260
261     def url(self):
262         self.repo_root()
263
264     def repo_root(self):
265         c = Command("git remote show origin", self.options)
266         out = self.__run_in_repo(c.output_of)
267         for line in out.split('\n'):
268             if line.strip().startswith("Fetch URL:"):
269                 repo = line.split()[2]
270
271     @classmethod
272     def checkout(cls, remote, local, options, depth=1):
273         Command("rm -rf %s" % local, options).run_silent()
274         Command("git clone --depth %d %s %s" % (depth, remote, local), options).run_fatal()
275         return GitRepository(local, options)
276
277     @classmethod
278     def remote_exists(cls, remote):
279         return os.system("git --no-pager ls-remote %s &> /dev/null" % remote) == 0
280
281     def tag_exists(self, tagname):
282         command = 'git tag -l | grep "^%s$"' % tagname
283         c = Command(command, self.options)
284         out = self.__run_in_repo(c.output_of, with_stderr=True)
285         return len(out) > 0
286
287     def __run_in_repo(self, fun, *args, **kwargs):
288         cwd = os.getcwd()
289         os.chdir(self.path)
290         ret = fun(*args, **kwargs)
291         os.chdir(cwd)
292         return ret
293
294     def __run_command_in_repo(self, command, ignore_errors=False):
295         c = Command(command, self.options)
296         if ignore_errors:
297             return self.__run_in_repo(c.output_of)
298         else:
299             return self.__run_in_repo(c.run_fatal)
300
301     def update(self, subdir=None, recursive=None, branch="master"):
302         if branch == "master":
303             self.__run_command_in_repo("git checkout %s" % branch)
304         else:
305             self.__run_command_in_repo("git checkout origin/%s" % branch)
306         self.__run_command_in_repo("git fetch origin --tags")
307         self.__run_command_in_repo("git fetch origin")
308         self.__run_command_in_repo("git merge --ff origin/%s" % branch)
309
310     def to_branch(self, branch, remote=True):
311         if remote:
312             command = "git branch --track %s origin/%s" % (branch, branch)
313             c = Command(command, self.options)
314             self.__run_in_repo(c.output_of, with_stderr=True)
315         return self.__run_command_in_repo("git checkout %s" % branch)
316
317     def to_tag(self, tag):
318         return self.__run_command_in_repo("git checkout %s" % tag)
319
320     def tag(self, tagname, logfile):
321         self.__run_command_in_repo("git tag %s -F %s" % (tagname, logfile))
322         self.commit(logfile)
323
324     def diff(self, f=""):
325         c = Command("git diff %s" % f, self.options)
326         return self.__run_in_repo(c.output_of, with_stderr=True)
327
328     def diff_with_tag(self, tagname):
329         c = Command("git diff %s" % tagname, self.options)
330         return self.__run_in_repo(c.output_of, with_stderr=True)
331
332     def commit(self, logfile, branch="master"):
333         self.__run_command_in_repo("git add .", ignore_errors=True)
334         self.__run_command_in_repo("git add -u", ignore_errors=True)
335         self.__run_command_in_repo("git commit -F  %s" % logfile, ignore_errors=True)
336         if branch == "master":
337             self.__run_command_in_repo("git push")
338         else:
339             self.__run_command_in_repo("git push %s:%s" % (branch, branch))
340         self.__run_command_in_repo("git push --tags")
341
342     def revert(self, f=""):
343         if f:
344             self.__run_command_in_repo("git checkout %s" % f)
345         else:
346             # revert all
347             self.__run_command_in_repo("git --no-pager reset --hard")
348             self.__run_command_in_repo("git --no-pager clean -f")
349
350     def is_clean(self):
351         def check_commit():
352             command="git status"
353             s="nothing to commit (working directory clean)"
354             return Command(command, self.options).output_of(True).find(s) >= 0
355         return self.__run_in_repo(check_commit)
356
357     def is_valid(self):
358         return os.path.exists(os.path.join(self.path, ".git"))
359     
360
361 class Repository:
362     """ Generic repository """
363     supported_repo_types = [SvnRepository, GitRepository]
364
365     def __init__(self, path, options):
366         self.path = path
367         self.options = options
368         for repo in self.supported_repo_types:
369             self.repo = repo(self.path, self.options)
370             if self.repo.is_valid():
371                 break
372
373     @classmethod
374     def has_moved_to_git(cls, module, config):
375         module = svn_to_git_name(module)
376         # check if the module is already in Git
377 #        return SvnRepository.remote_exists("%s/%s/aaaa-has-moved-to-git" % (config['svnpath'], module))
378         return GitRepository.remote_exists(Module.git_remote_dir(module))
379
380
381     @classmethod
382     def remote_exists(cls, remote):
383         for repo in Repository.supported_repo_types:
384             if repo.remote_exists(remote):
385                 return True
386         return False
387
388     def __getattr__(self, attr):
389         return getattr(self.repo, attr)
390
391
392
393 # support for tagged module is minimal, and is for the Build class only
394 class Module:
395
396     svn_magic_line="--This line, and those below, will be ignored--"
397     setting_tag_format = "Setting tag %s"
398     
399     redirectors=[ # ('module_name_varname','name'),
400                   ('module_version_varname','version'),
401                   ('module_taglevel_varname','taglevel'), ]
402
403     # where to store user's config
404     config_storage="CONFIG"
405     # 
406     config={}
407
408     import commands
409     configKeys=[ ('svnpath',"Enter your toplevel svnpath",
410                   "svn+ssh://%s@svn.planet-lab.org/svn/"%commands.getoutput("id -un")),
411                  ('gitserver', "Enter your git server's hostname", "git.onelab.eu"),
412                  ('gituser', "Enter your user name (login name) on git server", commands.getoutput("id -un")),
413                  ("build", "Enter the name of your build module","build"),
414                  ('username',"Enter your firstname and lastname for changelogs",""),
415                  ("email","Enter your email address for changelogs",""),
416                  ]
417
418     @classmethod
419     def prompt_config_option(cls, key, message, default):
420         cls.config[key]=raw_input("%s [%s] : "%(message,default)).strip() or default
421
422     @classmethod
423     def prompt_config (cls):
424         for (key,message,default) in cls.configKeys:
425             cls.config[key]=""
426             while not cls.config[key]:
427                 cls.prompt_config_option(key, message, default)
428
429
430     # for parsing module spec name:branch
431     matcher_branch_spec=re.compile("\A(?P<name>[\w\.-]+):(?P<branch>[\w\.-]+)\Z")
432     # special form for tagged module - for Build
433     matcher_tag_spec=re.compile("\A(?P<name>[\w-]+)@(?P<tagname>[\w\.-]+)\Z")
434     # parsing specfiles
435     matcher_rpm_define=re.compile("%(define|global)\s+(\S+)\s+(\S*)\s*")
436
437     def __init__ (self,module_spec,options):
438         # parse module spec
439         attempt=Module.matcher_branch_spec.match(module_spec)
440         if attempt:
441             self.name=attempt.group('name')
442             self.branch=attempt.group('branch')
443         else:
444             attempt=Module.matcher_tag_spec.match(module_spec)
445             if attempt:
446                 self.name=attempt.group('name')
447                 self.tagname=attempt.group('tagname')
448             else:
449                 self.name=module_spec
450
451         # when available prefer to use git module name internally
452         self.name = svn_to_git_name(self.name)
453
454         self.options=options
455         self.module_dir="%s/%s"%(options.workdir,self.name)
456         self.repository = None
457         self.build = None
458
459     def run (self,command):
460         return Command(command,self.options).run()
461     def run_fatal (self,command):
462         return Command(command,self.options).run_fatal()
463     def run_prompt (self,message,fun, *args):
464         fun_msg = "%s(%s)" % (fun.func_name, ",".join(args))
465         if not self.options.verbose:
466             while True:
467                 choice=prompt(message,True,('s','how'))
468                 if choice is True:
469                     fun(*args)
470                     return
471                 elif choice is False:
472                     print 'About to run function:', fun_msg
473         else:
474             question=message+" - want to run function: " + fun_msg
475             if prompt(question,True):
476                 fun(*args)
477
478     def friendly_name (self):
479         if hasattr(self,'branch'):
480             return "%s:%s"%(self.name,self.branch)
481         elif hasattr(self,'tagname'):
482             return "%s@%s"%(self.name,self.tagname)
483         else:
484             return self.name
485
486     @classmethod
487     def git_remote_dir (cls, name):
488         return "%s@%s:/git/%s.git" % (cls.config['gituser'], cls.config['gitserver'], name)
489
490     @classmethod
491     def svn_remote_dir (cls, name):
492         name = git_to_svn_name(name)
493         svn = cls.config['svnpath']
494         if svn.endswith('/'):
495             return "%s%s" % (svn, name)
496         return "%s/%s" % (svn, name)
497
498     def svn_selected_remote(self):
499         svn_name = git_to_svn_name(self.name)
500         remote = self.svn_remote_dir(svn_name)
501         if hasattr(self,'branch'):
502             remote = "%s/branches/%s" % (remote, self.branch)
503         elif hasattr(self,'tagname'):
504             remote = "%s/tags/%s" % (remote, self.tagname)
505         else:
506             remote = "%s/trunk" % remote
507         return remote
508
509     ####################
510     @classmethod
511     def init_homedir (cls, options):
512         if options.verbose and options.mode not in Main.silent_modes:
513             print 'Checking for', options.workdir
514         storage="%s/%s"%(options.workdir, cls.config_storage)
515         # sanity check. Either the topdir exists AND we have a config/storage
516         # or topdir does not exist and we create it
517         # to avoid people use their own daily svn repo
518         if os.path.isdir(options.workdir) and not os.path.isfile(storage):
519             print """The directory %s exists and has no CONFIG file
520 If this is your regular working directory, please provide another one as the
521 module-* commands need a fresh working dir. Make sure that you do not use 
522 that for other purposes than tagging""" % options.workdir
523             sys.exit(1)
524
525         def checkout_build():
526             print "Checking out build module..."
527             remote = cls.git_remote_dir(cls.config['build'])
528             local = os.path.join(options.workdir, cls.config['build'])
529             GitRepository.checkout(remote, local, options, depth=1)
530             print "OK"
531
532         def store_config():
533             f=file(storage,"w")
534             for (key,message,default) in Module.configKeys:
535                 f.write("%s=%s\n"%(key,Module.config[key]))
536             f.close()
537             if options.debug:
538                 print 'Stored',storage
539                 Command("cat %s"%storage,options).run()
540
541         def read_config():
542             # read config
543             f=open(storage)
544             for line in f.readlines():
545                 (key,value)=re.compile("^(.+)=(.+)$").match(line).groups()
546                 Module.config[key]=value                
547             f.close()
548
549         if not os.path.isdir (options.workdir):
550             print "Cannot find",options.workdir,"let's create it"
551             Command("mkdir -p %s" % options.workdir, options).run_silent()
552             cls.prompt_config()
553             checkout_build()
554             store_config()
555         else:
556             read_config()
557             # check missing config options
558             old_layout = False
559             for (key,message,default) in cls.configKeys:
560                 if not Module.config.has_key(key):
561                     print "Configuration changed for module-tools"
562                     cls.prompt_config_option(key, message, default)
563                     old_layout = True
564                     
565             if old_layout:
566                 Command("rm -rf %s" % options.workdir, options).run_silent()
567                 Command("mkdir -p %s" % options.workdir, options).run_silent()
568                 checkout_build()
569                 store_config()
570
571             build_dir = os.path.join(options.workdir, cls.config['build'])
572             if not os.path.isdir(build_dir):
573                 checkout_build()
574             else:
575                 build = Repository(build_dir, options)
576                 if not build.is_clean():
577                     print "build module needs a revert"
578                     build.revert()
579                     print "OK"
580                 build.update()
581
582         if options.verbose and options.mode not in Main.silent_modes:
583             print '******** Using config'
584             for (key,message,default) in Module.configKeys:
585                 print '\t',key,'=',Module.config[key]
586
587     def init_module_dir (self):
588         if self.options.verbose:
589             print 'Checking for',self.module_dir
590
591         if not os.path.isdir (self.module_dir):
592             if Repository.has_moved_to_git(self.name, Module.config):
593                 self.repository = GitRepository.checkout(self.git_remote_dir(self.name),
594                                                          self.module_dir,
595                                                          self.options)
596             else:
597                 remote = self.svn_selected_remote()
598                 self.repository = SvnRepository.checkout(remote,
599                                                          self.module_dir,
600                                                          self.options, recursive=False)
601
602         self.repository = Repository(self.module_dir, self.options)
603         if self.repository.type == "svn":
604             # check if module has moved to git    
605             if Repository.has_moved_to_git(self.name, Module.config):
606                 Command("rm -rf %s" % self.module_dir, self.options).run_silent()
607                 self.init_module_dir()
608             # check if we have the required branch/tag
609             if self.repository.url() != self.svn_selected_remote():
610                 Command("rm -rf %s" % self.module_dir, self.options).run_silent()
611                 self.init_module_dir()
612
613         elif self.repository.type == "git":
614             if hasattr(self,'branch'):
615                 self.repository.to_branch(self.branch)
616             elif hasattr(self,'tagname'):
617                 self.repository.to_tag(self.tagname)
618
619         else:
620             raise Exception, 'Cannot find %s - check module name'%self.module_dir
621
622
623     def revert_module_dir (self):
624         if self.options.fast_checks:
625             if self.options.verbose: print 'Skipping revert of %s' % self.module_dir
626             return
627         if self.options.verbose:
628             print 'Checking whether', self.module_dir, 'needs being reverted'
629         
630         if not self.repository.is_clean():
631             self.repository.revert()
632
633     def update_module_dir (self):
634         if self.options.fast_checks:
635             if self.options.verbose: print 'Skipping update of %s' % self.module_dir
636             return
637         if self.options.verbose:
638             print 'Updating', self.module_dir
639
640         if hasattr(self,'branch'):
641             self.repository.update(branch=self.branch)
642         elif hasattr(self,'tagname'):
643             self.repository.update(branch=self.tagname)
644         else:
645             self.repository.update()
646
647     def main_specname (self):
648         attempt="%s/%s.spec"%(self.module_dir,self.name)
649         if os.path.isfile (attempt):
650             return attempt
651         pattern1="%s/*.spec"%self.module_dir
652         level1=glob(pattern1)
653         if level1:
654             return level1[0]
655         pattern2="%s/*/*.spec"%self.module_dir
656         level2=glob(pattern2)
657
658         if level2:
659             return level2[0]
660         raise Exception, 'Cannot guess specfile for module %s -- patterns were %s or %s'%(self.name,pattern1,pattern2)
661
662     def all_specnames (self):
663         level1=glob("%s/*.spec" % self.module_dir)
664         if level1: return level1
665         level2=glob("%s/*/*.spec" % self.module_dir)
666         return level2
667
668     def parse_spec (self, specfile, varnames):
669         if self.options.verbose:
670             print 'Parsing',specfile,
671             for var in varnames:
672                 print "[%s]"%var,
673             print ""
674         result={}
675         f=open(specfile)
676         for line in f.readlines():
677             attempt=Module.matcher_rpm_define.match(line)
678             if attempt:
679                 (define,var,value)=attempt.groups()
680                 if var in varnames:
681                     result[var]=value
682         f.close()
683         if self.options.debug:
684             print 'found',len(result),'keys'
685             for (k,v) in result.iteritems():
686                 print k,'=',v
687         return result
688                 
689     # stores in self.module_name_varname the rpm variable to be used for the module's name
690     # and the list of these names in self.varnames
691     def spec_dict (self):
692         specfile=self.main_specname()
693         redirector_keys = [ varname for (varname,default) in Module.redirectors]
694         redirect_dict = self.parse_spec(specfile,redirector_keys)
695         if self.options.debug:
696             print '1st pass parsing done, redirect_dict=',redirect_dict
697         varnames=[]
698         for (varname,default) in Module.redirectors:
699             if redirect_dict.has_key(varname):
700                 setattr(self,varname,redirect_dict[varname])
701                 varnames += [redirect_dict[varname]]
702             else:
703                 setattr(self,varname,default)
704                 varnames += [ default ] 
705         self.varnames = varnames
706         result = self.parse_spec (specfile,self.varnames)
707         if self.options.debug:
708             print '2st pass parsing done, varnames=',varnames,'result=',result
709         return result
710
711     def patch_spec_var (self, patch_dict,define_missing=False):
712         for specfile in self.all_specnames():
713             # record the keys that were changed
714             changed = dict ( [ (x,False) for x in patch_dict.keys() ] )
715             newspecfile=specfile+".new"
716             if self.options.verbose:
717                 print 'Patching',specfile,'for',patch_dict.keys()
718             spec=open (specfile)
719             new=open(newspecfile,"w")
720
721             for line in spec.readlines():
722                 attempt=Module.matcher_rpm_define.match(line)
723                 if attempt:
724                     (define,var,value)=attempt.groups()
725                     if var in patch_dict.keys():
726                         if self.options.debug:
727                             print 'rewriting %s as %s'%(var,patch_dict[var])
728                         new.write('%%%s %s %s\n'%(define,var,patch_dict[var]))
729                         changed[var]=True
730                         continue
731                 new.write(line)
732             if define_missing:
733                 for (key,was_changed) in changed.iteritems():
734                     if not was_changed:
735                         if self.options.debug:
736                             print 'rewriting missing %s as %s'%(key,patch_dict[key])
737                         new.write('\n%%define %s %s\n'%(key,patch_dict[key]))
738             spec.close()
739             new.close()
740             os.rename(newspecfile,specfile)
741
742     # returns all lines until the magic line
743     def unignored_lines (self, logfile):
744         result=[]
745         white_line_matcher = re.compile("\A\s*\Z")
746         for logline in file(logfile).readlines():
747             if logline.strip() == Module.svn_magic_line:
748                 break
749             elif white_line_matcher.match(logline):
750                 continue
751             else:
752                 result.append(logline.strip()+'\n')
753         return result
754
755     # creates a copy of the input with only the unignored lines
756     def stripped_magic_line_filename (self, filein, fileout ,new_tag_name):
757        f=file(fileout,'w')
758        f.write(self.setting_tag_format%new_tag_name + '\n')
759        for line in self.unignored_lines(filein):
760            f.write(line)
761        f.close()
762
763     def insert_changelog (self, logfile, oldtag, newtag):
764         for specfile in self.all_specnames():
765             newspecfile=specfile+".new"
766             if self.options.verbose:
767                 print 'Inserting changelog from %s into %s'%(logfile,specfile)
768             spec=open (specfile)
769             new=open(newspecfile,"w")
770             for line in spec.readlines():
771                 new.write(line)
772                 if re.compile('%changelog').match(line):
773                     dateformat="* %a %b %d %Y"
774                     datepart=time.strftime(dateformat)
775                     logpart="%s <%s> - %s"%(Module.config['username'],
776                                                  Module.config['email'],
777                                                  newtag)
778                     new.write(datepart+" "+logpart+"\n")
779                     for logline in self.unignored_lines(logfile):
780                         new.write("- " + logline)
781                     new.write("\n")
782             spec.close()
783             new.close()
784             os.rename(newspecfile,specfile)
785             
786     def show_dict (self, spec_dict):
787         if self.options.verbose:
788             for (k,v) in spec_dict.iteritems():
789                 print k,'=',v
790
791     def last_tag (self, spec_dict):
792         try:
793             return "%s-%s" % (spec_dict[self.module_version_varname],
794                               spec_dict[self.module_taglevel_varname])
795         except KeyError,err:
796             raise Exception,'Something is wrong with module %s, cannot determine %s - exiting'%(self.name,err)
797
798     def tag_name (self, spec_dict, old_svn_name=False):
799         base_tag_name = self.name
800         if old_svn_name:
801             base_tag_name = git_to_svn_name(self.name)
802         return "%s-%s" % (base_tag_name, self.last_tag(spec_dict))
803     
804
805 ##############################
806     # using fine_grain means replacing only those instances that currently refer to this tag
807     # otherwise, <module>-{SVNPATH,GITPATH} is replaced unconditionnally
808     def patch_tags_file (self, tagsfile, oldname, newname,fine_grain=True):
809         newtagsfile=tagsfile+".new"
810         tags=open (tagsfile)
811         new=open(newtagsfile,"w")
812
813         matches=0
814         # fine-grain : replace those lines that refer to oldname
815         if fine_grain:
816             if self.options.verbose:
817                 print 'Replacing %s into %s\n\tin %s .. '%(oldname,newname,tagsfile),
818             matcher=re.compile("^(.*)%s(.*)"%oldname)
819             for line in tags.readlines():
820                 if not matcher.match(line):
821                     new.write(line)
822                 else:
823                     (begin,end)=matcher.match(line).groups()
824                     new.write(begin+newname+end+"\n")
825                     matches += 1
826         # brute-force : change uncommented lines that define <module>-SVNPATH
827         else:
828             if self.options.verbose:
829                 print 'Searching for -SVNPATH or -GITPATH lines referring to /%s/\n\tin %s .. '%(self.name,tagsfile),
830             pattern="\A\s*(?P<make_name>[^\s]+)-(SVNPATH|GITPATH)\s*(=|:=)\s*(?P<url_main>[^\s]+)/%s[^\s]+"\
831                                           %(self.name)
832             matcher_module=re.compile(pattern)
833             for line in tags.readlines():
834                 attempt=matcher_module.match(line)
835                 if attempt:
836                     if line.find("-GITPATH") >= 0:
837                         modulepath = "%s-GITPATH"%(attempt.group('make_name'))
838                         replacement = "%-32s:= %s/%s.git@%s\n"%(modulepath,attempt.group('url_main'),self.name,newname)
839                     else:
840                         modulepath = "%s-SVNPATH"%(attempt.group('make_name'))
841                         replacement = "%-32s:= %s/%s/tags/%s\n"%(modulepath,attempt.group('url_main'),self.name,newname)
842                     if self.options.verbose:
843                         print ' ' + modulepath, 
844                     new.write(replacement)
845                     matches += 1
846                 else:
847                     new.write(line)
848         tags.close()
849         new.close()
850         os.rename(newtagsfile,tagsfile)
851         if self.options.verbose: print "%d changes"%matches
852         return matches
853
854     def check_tag(self, tagname, need_it=False, old_svn_tag_name=None):
855         if self.options.verbose:
856             print "Checking %s repository tag: %s - " % (self.repository.type, tagname),
857
858         found_tagname = tagname
859         found = self.repository.tag_exists(tagname)
860         if not found and old_svn_tag_name:
861             if self.options.verbose:
862                 print "KO"
863                 print "Checking %s repository tag: %s - " % (self.repository.type, old_svn_tag_name),
864             found = self.repository.tag_exists(old_svn_tag_name)
865             if found:
866                 found_tagname = old_svn_tag_name
867
868         if (found and need_it) or (not found and not need_it):
869             if self.options.verbose:
870                 print "OK",
871                 if found: print "- found"
872                 else: print "- not found"
873         else:
874             if self.options.verbose:
875                 print "KO"
876             if found:
877                 raise Exception, "tag (%s) is already there" % tagname
878             else:
879                 raise Exception, "can not find required tag (%s)" % tagname
880
881         return found_tagname
882
883
884 ##############################
885     def do_tag (self):
886         self.init_module_dir()
887         self.revert_module_dir()
888         self.update_module_dir()
889         # parse specfile
890         spec_dict = self.spec_dict()
891         self.show_dict(spec_dict)
892         
893         # side effects
894         old_tag_name = self.tag_name(spec_dict)
895         old_svn_tag_name = self.tag_name(spec_dict, old_svn_name=True)
896
897         if (self.options.new_version):
898             # new version set on command line
899             spec_dict[self.module_version_varname] = self.options.new_version
900             spec_dict[self.module_taglevel_varname] = 0
901         else:
902             # increment taglevel
903             new_taglevel = str ( int (spec_dict[self.module_taglevel_varname]) + 1)
904             spec_dict[self.module_taglevel_varname] = new_taglevel
905
906         new_tag_name = self.tag_name(spec_dict)
907
908         # sanity check
909         old_tag_name = self.check_tag(old_tag_name, need_it=True, old_svn_tag_name=old_svn_tag_name)
910         new_tag_name = self.check_tag(new_tag_name, need_it=False)
911
912         # checking for diffs
913         diff_output = self.repository.diff_with_tag(old_tag_name)
914         if len(diff_output) == 0:
915             if not prompt ("No pending difference in module %s, want to tag anyway"%self.name,False):
916                 return
917
918         # side effect in trunk's specfile
919         self.patch_spec_var(spec_dict)
920
921         # prepare changelog file 
922         # we use the standard subversion magic string (see svn_magic_line)
923         # so we can provide useful information, such as version numbers and diff
924         # in the same file
925         changelog="/tmp/%s-%d.edit"%(self.name,os.getpid())
926         changelog_svn="/tmp/%s-%d.svn"%(self.name,os.getpid())
927         setting_tag_line=Module.setting_tag_format%new_tag_name
928         file(changelog,"w").write("""
929 %s
930 %s
931 Please write a changelog for this new tag in the section above
932 """%(Module.svn_magic_line,setting_tag_line))
933
934         if not self.options.verbose or prompt('Want to see diffs while writing changelog',True):
935             file(changelog,"a").write('DIFF=========\n' + diff_output)
936         
937         if self.options.debug:
938             prompt('Proceed ?')
939
940         # edit it        
941         self.run("%s %s"%(self.options.editor,changelog))
942         # strip magic line in second file - looks like svn has changed its magic line with 1.6
943         # so we do the job ourselves
944         self.stripped_magic_line_filename(changelog,changelog_svn,new_tag_name)
945         # insert changelog in spec
946         if self.options.changelog:
947             self.insert_changelog (changelog,old_tag_name,new_tag_name)
948
949         ## update build
950         build_path = os.path.join(self.options.workdir,
951                                   Module.config['build'])
952         build = Repository(build_path, self.options)
953         if self.options.build_branch:
954             build.to_branch(self.options.build_branch)
955         if not build.is_clean():
956             build.revert()
957
958         tagsfiles=glob(build.path+"/*-tags*.mk")
959         tagsdict=dict( [ (x,'todo') for x in tagsfiles ] )
960         default_answer = 'y'
961         tagsfiles.sort()
962         while True:
963             for tagsfile in tagsfiles:
964                 status=tagsdict[tagsfile]
965                 basename=os.path.basename(tagsfile)
966                 print ".................... Dealing with %s"%basename
967                 while tagsdict[tagsfile] == 'todo' :
968                     choice = prompt ("insert %s in %s    "%(new_tag_name,basename),default_answer,
969                                      [ ('y','es'), ('n', 'ext'), ('f','orce'), 
970                                        ('d','iff'), ('r','evert'), ('c', 'at'), ('h','elp') ] ,
971                                      allow_outside=True)
972                     if choice == 'y':
973                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=True)
974                     elif choice == 'n':
975                         print 'Done with %s'%os.path.basename(tagsfile)
976                         tagsdict[tagsfile]='done'
977                     elif choice == 'f':
978                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=False)
979                     elif choice == 'd':
980                         print build.diff(f=tagsfile)
981                     elif choice == 'r':
982                         build.revert(f=tagsfile)
983                     elif choice == 'c':
984                         self.run("cat %s"%tagsfile)
985                     else:
986                         name=self.name
987                         print """y: change %(name)s-{SVNPATH,GITPATH} only if it currently refers to %(old_tag_name)s
988 f: unconditionnally change any line that assigns %(name)s-SVNPATH to using %(new_tag_name)s
989 d: show current diff for this tag file
990 r: revert that tag file
991 c: cat the current tag file
992 n: move to next file"""%locals()
993
994             if prompt("Want to review changes on tags files",False):
995                 tagsdict = dict ( [ (x, 'todo') for x in tagsfiles ] )
996                 default_answer='d'
997             else:
998                 break
999
1000         def diff_all_changes():
1001             print build.diff()
1002             print self.repository.diff()
1003
1004         def commit_all_changes(log):
1005             if hasattr(self,'branch'):
1006                 self.repository.commit(log, branch=self.branch)
1007             else:
1008                 self.repository.commit(log)
1009             build.commit(log)
1010
1011         self.run_prompt("Review module and build", diff_all_changes)
1012         self.run_prompt("Commit module and build", commit_all_changes, changelog_svn)
1013         self.run_prompt("Create tag", self.repository.tag, new_tag_name, changelog_svn)
1014
1015         if self.options.debug:
1016             print 'Preserving',changelog,'and stripped',changelog_svn
1017         else:
1018             os.unlink(changelog)
1019             os.unlink(changelog_svn)
1020
1021
1022 ##############################
1023     def do_version (self):
1024         self.init_module_dir()
1025         self.revert_module_dir()
1026         self.update_module_dir()
1027         spec_dict = self.spec_dict()
1028         if self.options.www:
1029             self.html_store_title('Version for module %s (%s)' % (self.friendly_name(),
1030                                                                   self.last_tag(spec_dict)))
1031         for varname in self.varnames:
1032             if not spec_dict.has_key(varname):
1033                 self.html_print ('Could not find %%define for %s'%varname)
1034                 return
1035             else:
1036                 self.html_print ("%-16s %s"%(varname,spec_dict[varname]))
1037         if self.options.verbose:
1038             self.html_print ("%-16s %s"%('main specfile:',self.main_specname()))
1039             self.html_print ("%-16s %s"%('specfiles:',self.all_specnames()))
1040         self.html_print_end()
1041
1042
1043 ##############################
1044     def do_diff (self):
1045         self.init_module_dir()
1046         self.revert_module_dir()
1047         self.update_module_dir()
1048         spec_dict = self.spec_dict()
1049         self.show_dict(spec_dict)
1050
1051         # side effects
1052         tag_name = self.tag_name(spec_dict)
1053         old_svn_tag_name = self.tag_name(spec_dict, old_svn_name=True)
1054
1055         # sanity check
1056         tag_name = self.check_tag(tag_name, need_it=True, old_svn_tag_name=old_svn_tag_name)
1057
1058         if self.options.verbose:
1059             print 'Getting diff'
1060         diff_output = self.repository.diff_with_tag(tag_name)
1061
1062         if self.options.list:
1063             if diff_output:
1064                 print self.name
1065         else:
1066             thename=self.friendly_name()
1067             do_print=False
1068             if self.options.www and diff_output:
1069                 self.html_store_title("Diffs in module %s (%s) : %d chars"%(\
1070                         thename,self.last_tag(spec_dict),len(diff_output)))
1071
1072                 self.html_store_raw ('<p> &lt; (left) %s </p>' % tag_name)
1073                 self.html_store_raw ('<p> &gt; (right) %s </p>' % thename)
1074                 self.html_store_pre (diff_output)
1075             elif not self.options.www:
1076                 print 'x'*30,'module',thename
1077                 print 'x'*20,'<',tag_name
1078                 print 'x'*20,'>',thename
1079                 print diff_output
1080
1081 ##############################
1082     # store and restitute html fragments
1083     @staticmethod 
1084     def html_href (url,text): return '<a href="%s">%s</a>'%(url,text)
1085
1086     @staticmethod 
1087     def html_anchor (url,text): return '<a name="%s">%s</a>'%(url,text)
1088
1089     @staticmethod
1090     def html_quote (text):
1091         return text.replace('&','&#38;').replace('<','&lt;').replace('>','&gt;')
1092
1093     # only the fake error module has multiple titles
1094     def html_store_title (self, title):
1095         if not hasattr(self,'titles'): self.titles=[]
1096         self.titles.append(title)
1097
1098     def html_store_raw (self, html):
1099         if not hasattr(self,'body'): self.body=''
1100         self.body += html
1101
1102     def html_store_pre (self, text):
1103         if not hasattr(self,'body'): self.body=''
1104         self.body += '<pre>' + self.html_quote(text) + '</pre>'
1105
1106     def html_print (self, txt):
1107         if not self.options.www:
1108             print txt
1109         else:
1110             if not hasattr(self,'in_list') or not self.in_list:
1111                 self.html_store_raw('<ul>')
1112                 self.in_list=True
1113             self.html_store_raw('<li>'+txt+'</li>')
1114
1115     def html_print_end (self):
1116         if self.options.www:
1117             self.html_store_raw ('</ul>')
1118
1119     @staticmethod
1120     def html_dump_header(title):
1121         nowdate=time.strftime("%Y-%m-%d")
1122         nowtime=time.strftime("%H:%M (%Z)")
1123         print """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1124 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
1125 <head>
1126 <title> %s </title>
1127 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1128 <style type="text/css">
1129 body { font-family:georgia, serif; }
1130 h1 {font-size: large; }
1131 p.title {font-size: x-large; }
1132 span.error {text-weight:bold; color: red; }
1133 </style>
1134 </head>
1135 <body>
1136 <p class='title'> %s - status on %s at %s</p>
1137 <ul>
1138 """%(title,title,nowdate,nowtime)
1139
1140     @staticmethod
1141     def html_dump_middle():
1142         print "</ul>"
1143
1144     @staticmethod
1145     def html_dump_footer():
1146         print "</body></html"
1147
1148     def html_dump_toc(self):
1149         if hasattr(self,'titles'):
1150             for title in self.titles:
1151                 print '<li>',self.html_href ('#'+self.friendly_name(),title),'</li>'
1152
1153     def html_dump_body(self):
1154         if hasattr(self,'titles'):
1155             for title in self.titles:
1156                 print '<hr /><h1>',self.html_anchor(self.friendly_name(),title),'</h1>'
1157         if hasattr(self,'body'):
1158             print self.body
1159             print '<p class="top">',self.html_href('#','Back to top'),'</p>'            
1160
1161
1162 ##############################
1163 class Main:
1164
1165     module_usage="""Usage: %prog [options] module_desc [ .. module_desc ]
1166
1167 module-tools : a set of tools to manage subversion tags and specfile
1168   requires the specfile to either
1169   * define *version* and *taglevel*
1170   OR alternatively 
1171   * define redirection variables module_version_varname / module_taglevel_varname
1172 Trunk:
1173   by default, the trunk of modules is taken into account
1174   in this case, just mention the module name as <module_desc>
1175 Branches:
1176   if you wish to work on a branch rather than on the trunk, 
1177   you can use something like e.g. Mom:2.1 as <module_desc>
1178 """
1179     release_usage="""Usage: %prog [options] tag1 .. tagn
1180   Extract release notes from the changes in specfiles between several build tags, latest first
1181   Examples:
1182       release-changelog 4.2-rc25 4.2-rc24 4.2-rc23 4.2-rc22
1183   You can refer to a (build) branch by prepending a colon, like in
1184       release-changelog :4.2 4.2-rc25
1185   You can refer to the build trunk by just mentioning 'trunk', e.g.
1186       release-changelog -t coblitz-tags.mk coblitz-2.01-rc6 trunk
1187 """
1188     common_usage="""More help:
1189   see http://svn.planet-lab.org/wiki/ModuleTools"""
1190
1191     modes={ 
1192         'list' : "displays a list of available tags or branches",
1193         'version' : "check latest specfile and print out details",
1194         'diff' : "show difference between module (trunk or branch) and latest tag",
1195         'tag'  : """increment taglevel in specfile, insert changelog in specfile,
1196                 create new tag and and monitor its adoption in build/*-tags*.mk""",
1197         'branch' : """create a branch for this module, from the latest tag on the trunk, 
1198                   and change trunk's version number to reflect the new branch name;
1199                   you can specify the new branch name by using module:branch""",
1200         'sync' : """create a tag from the module
1201                 this is a last resort option, mostly for repairs""",
1202         'changelog' : """extract changelog between build tags
1203                 expected arguments are a list of tags""",
1204         }
1205
1206     silent_modes = ['list']
1207     release_modes = ['changelog']
1208
1209     @staticmethod
1210     def optparse_list (option, opt, value, parser):
1211         try:
1212             setattr(parser.values,option.dest,getattr(parser.values,option.dest)+value.split())
1213         except:
1214             setattr(parser.values,option.dest,value.split())
1215
1216     def run(self):
1217
1218         mode=None
1219         for function in Main.modes.keys():
1220             if sys.argv[0].find(function) >= 0:
1221                 mode = function
1222                 break
1223         if not mode:
1224             print "Unsupported command",sys.argv[0]
1225             print "Supported commands:" + " ".join(Main.modes.keys())
1226             sys.exit(1)
1227
1228         if mode not in Main.release_modes:
1229             usage = Main.module_usage
1230             usage += Main.common_usage
1231             usage += "\nmodule-%s : %s"%(mode,Main.modes[mode])
1232         else:
1233             usage = Main.release_usage
1234             usage += Main.common_usage
1235
1236         parser=OptionParser(usage=usage)
1237         
1238         if mode == "tag" or mode == 'branch':
1239             parser.add_option("-s","--set-version",action="store",dest="new_version",default=None,
1240                               help="set new version and reset taglevel to 0")
1241         if mode == "tag" :
1242             parser.add_option("-c","--no-changelog", action="store_false", dest="changelog", default=True,
1243                               help="do not update changelog section in specfile when tagging")
1244             parser.add_option("-b","--build-branch", action="store", dest="build_branch", default=None,
1245                               help="specify a build branch; used for locating the *tags*.mk files where adoption is to take place")
1246         if mode == "tag" or mode == "sync" :
1247             parser.add_option("-e","--editor", action="store", dest="editor", default=default_editor(),
1248                               help="specify editor")
1249
1250         if mode in ["diff","version"] :
1251             parser.add_option("-W","--www", action="store", dest="www", default=False,
1252                               help="export diff in html format, e.g. -W trunk")
1253
1254         if mode == "diff" :
1255             parser.add_option("-l","--list", action="store_true", dest="list", default=False,
1256                               help="just list modules that exhibit differences")
1257             
1258         default_modules_list=os.path.dirname(sys.argv[0])+"/modules.list"
1259         parser.add_option("-a","--all",action="store_true",dest="all_modules",default=False,
1260                           help="run on all modules as found in %s"%default_modules_list)
1261         parser.add_option("-f","--file",action="store",dest="modules_list",default=None,
1262                           help="run on all modules found in specified file")
1263         parser.add_option("-n","--dry-run",action="store_true",dest="dry_run",default=False,
1264                           help="dry run - shell commands are only displayed")
1265         parser.add_option("-t","--distrotags",action="callback",callback=Main.optparse_list, dest="distrotags",
1266                           default=[], nargs=1,type="string",
1267                           help="""specify distro-tags files, e.g. onelab-tags-4.2.mk
1268 -- can be set multiple times, or use quotes""")
1269
1270         parser.add_option("-w","--workdir", action="store", dest="workdir", 
1271                           default="%s/%s"%(os.getenv("HOME"),"modules"),
1272                           help="""name for dedicated working dir - defaults to ~/modules
1273 ** THIS MUST NOT ** be your usual working directory""")
1274         parser.add_option("-F","--fast-checks",action="store_true",dest="fast_checks",default=False,
1275                           help="skip safety checks, such as svn updates -- use with care")
1276
1277         # default verbosity depending on function - temp
1278         verbose_modes= ['tag', 'sync', 'branch']
1279         
1280         if mode not in verbose_modes:
1281             parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False, 
1282                               help="run in verbose mode")
1283         else:
1284             parser.add_option("-q","--quiet", action="store_false", dest="verbose", default=True,
1285                               help="run in quiet (non-verbose) mode")
1286         (options, args) = parser.parse_args()
1287         options.mode=mode
1288         if not hasattr(options,'dry_run'):
1289             options.dry_run=False
1290         if not hasattr(options,'www'):
1291             options.www=False
1292         options.debug=False
1293
1294         ########## module-*
1295         if len(args) == 0:
1296             if options.all_modules:
1297                 options.modules_list=default_modules_list
1298             if options.modules_list:
1299                 args=Command("grep -v '#' %s"%options.modules_list,options).output_of().split()
1300             else:
1301                 parser.print_help()
1302                 sys.exit(1)
1303         Module.init_homedir(options)
1304         
1305
1306         modules=[ Module(modname,options) for modname in args ]
1307         # hack: create a dummy Module to store errors/warnings
1308         error_module = Module('__errors__',options)
1309
1310         for module in modules:
1311             if len(args)>1 and mode not in Main.silent_modes:
1312                 if not options.www:
1313                     print '========================================',module.friendly_name()
1314             # call the method called do_<mode>
1315             method=Module.__dict__["do_%s"%mode]
1316             try:
1317                 method(module)
1318             except Exception,e:
1319                 if options.www:
1320                     title='<span class="error"> Skipping module %s - failure: %s </span>'%\
1321                         (module.friendly_name(), str(e))
1322                     error_module.html_store_title(title)
1323                 else:
1324                     import traceback
1325                     traceback.print_exc()
1326                     print 'Skipping module %s: '%modname,e
1327
1328         if options.www:
1329             if mode == "diff":
1330                 modetitle="Changes to tag in %s"%options.www
1331             elif mode == "version":
1332                 modetitle="Latest tags in %s"%options.www
1333             modules.append(error_module)
1334             error_module.html_dump_header(modetitle)
1335             for module in modules:
1336                 module.html_dump_toc()
1337             Module.html_dump_middle()
1338             for module in modules:
1339                 module.html_dump_body()
1340             Module.html_dump_footer()
1341
1342 ####################
1343 if __name__ == "__main__" :
1344     try:
1345         Main().run()
1346     except KeyboardInterrupt:
1347         print '\nBye'