make mass-build GPLv2+
[mass-build.git] / mass_build.py
1 #!/usr/bin/python
2 # mass-build - Easily Build Software Involving a Large Amount of Source Repositories
3 # Copyright (C) 2012 Ralf Jung <post@ralfj.de>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
19 import vcs, build_system, imp
20 import argparse, os, sys, subprocess
21 from collections import OrderedDict
22
23 # an entire Project
24 class Project:
25         def __init__(self, folder, config):
26                 self.folder = folder
27                 self.name = config['name']
28                 # VCS
29                 vcsName = config['vcs']
30                 if vcsName == 'git':
31                         self.vcs = vcs.Git(self.sourceFolder(), config['url'], config['version'])
32                 elif vcsName == 'svn':
33                         self.vcs = vcs.SVN(self.sourceFolder(), config['url'])
34                 else:
35                         raise Exception("Unknown VCS type "+vcsName)
36                 # build system
37                 if config.get('buildDeb', False):
38                         self.buildSystem = build_system.AutoDebuild(self.sourceFolder(), self.buildFolder(), config, self.vcs)
39                 else:
40                         buildSystemName = config['buildSystem']
41                         if buildSystemName == 'cmake':
42                                 self.buildSystem = build_system.CMake(self.sourceFolder(), self.buildFolder(), config)
43                         else:
44                                 raise Exception("Unknown build system type "+buildSystemName)
45         
46         def sourceFolder(self):
47                 return os.path.join(self.folder, self.name)
48         
49         def buildFolder(self):
50                 return os.path.join(config['buildDir'], self.sourceFolder())
51
52 # read command-line arguments
53 parser = argparse.ArgumentParser(description='Update and build a bunch of stuff')
54 parser.add_argument("-c, --config",
55                     dest="config", default="mass-build.conf",
56                     help="mass-build config file")
57 parser.add_argument("--reconfigure",
58                     action="store_true", dest="reconfigure",
59                     help="Force configuration to be run")
60 parser.add_argument("--wait-after-config",
61                     action="store_true", dest="wait_after_config",
62                     help="Wait for user confirmation after configuration is finished")
63 parser.add_argument("--reset-source",
64                     action="store_true", dest="reset_source",
65                     help="Reset sourcecode to the given version (removes local changes!)")
66 parser.add_argument("--no-update",
67                     action="store_false", dest="update",
68                     help="Do not update projects before compilation")
69 parser.add_argument("--resume-from", metavar='PROJECT',
70                     dest="resume_from",
71                     help="From the projects specified, continue building with this one (i.e., remove all projects before this one from the list - this never adds new projects)")
72 parser.add_argument("projects",  metavar='PROJECT', nargs='*',
73                     help="Manually specify projects or folders to be built (project names take precedence)")
74 args = parser.parse_args()
75 # sanitize
76 if args.reset_source and not args.update:
77         raise Exception("When no update is performed, no reset to the given version can be done either")
78
79 # load config
80 config = imp.load_source('config', args.config).__dict__
81 os.remove(args.config+'c') # remove compiled python file
82 allProjects = OrderedDict() # all projects
83 allFolders = {} # all folders
84 workProjects = [] # projects we work on
85
86 # copy all items which don't exist below, except for those in the exclude list
87 def inherit(subConfig, superConfig, exclude = ('name', 'projects')):
88         for name in superConfig.keys():
89                 if (not name in subConfig) and (not name in exclude):
90                         subConfig[name] = superConfig[name]
91
92 # populate list of projects, return list of projects in that folder
93 def loadProjects(config, folder=''):
94         folderProjects = []
95         for projectConfig in config['projects']:
96                 assert 'name' in projectConfig # everything must have a name
97                 inherit(projectConfig, config)
98                 if 'projects' in projectConfig: # a subpath
99                         folderProjects += loadProjects(projectConfig, os.path.join(folder, projectConfig['name']))
100                 else: # a proper project
101                         if projectConfig['name'] in allProjects:
102                                 raise Exception("Duplicate project name "+projectConfig['name'])
103                         project = Project(folder, projectConfig)
104                         allProjects[projectConfig['name']] = project
105                         folderProjects.append(project)
106         # store projects of this folder
107         if folder in allFolders:
108                 raise Exception("Duplicate folder name "+folder)
109         allFolders[folder] = folderProjects
110         return folderProjects
111
112 # load available projects
113 loadProjects(config)
114 # get base set og projects to process
115 if args.projects:
116         for name in args.projects:
117                 if name in allProjects:
118                         workProjects.append(allProjects[name])
119                 elif name in allFolders:
120                         workProjects += allFolders[name]
121                 else:
122                         raise Exception("Project or folder %s does not exist" % name)
123 else:
124         workProjects = allProjects.values() # all the projects
125 # apply the "resume from"
126 if args.resume_from is not None:
127         # find project index
128         startIndex = 0
129         while startIndex < len(workProjects):
130                 if workProjects[startIndex].name == args.resume_from:
131                         break # we found it
132                 else:
133                         startIndex += 1
134         if startIndex >= len(workProjects): # project not found
135                 raise Exception("%s not found in list of projects to work on" % args.resume_from)
136         # start here
137         workProjects = workProjects[startIndex:]
138
139 # and do it!
140 for project in workProjects:
141         try:
142                 if args.update:
143                         print "Updating project",project.sourceFolder()
144                         project.vcs.update(forceVersion=args.reset_source)
145                 print "Building project",project.sourceFolder()
146                 project.buildSystem.build(reconfigure=args.reconfigure, waitAfterConfig=args.wait_after_config)
147                 print
148         except (subprocess.CalledProcessError, KeyboardInterrupt) as e: # for some exceptions, a stackrace is usually pointless
149                 print >> sys.stderr
150                 print >> sys.stderr
151                 if isinstance(e, KeyboardInterrupt): # str(e) would be the empty string
152                         print >> sys.stderr, "Interruped by user while processing %s" % (project.name)
153                 else:
154                         print >> sys.stderr, "Error while processing %s: %s" % (project.name, str(e))
155                 print >> sys.stderr
156                 sys.exit(1)
157 print "All operations successfully completed"