553282b8076c2ae904b1f2d9b21b6928376a184d
[mass-build.git] / mass_build.py
1 #!/usr/bin/python3
2 # mass-build - Easily Build Software Involving a Large Amount of Source Repositories
3 # Copyright (C) 2012-2013 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
20 import argparse, os, sys, subprocess
21 from collections import OrderedDict
22
23 # helper functions
24 def load_module(name, path, write_bytecode = False):
25     old_val = sys.dont_write_bytecode
26     sys.dont_write_bytecode = not write_bytecode
27     module = None
28     try:
29         from importlib.machinery import SourceFileLoader
30         module = SourceFileLoader(name, path).load_module()
31     except ImportError:
32         import imp
33         module = imp.load_source(name, path)
34     finally:
35         sys.dont_write_bytecode = old_val
36     return module
37
38 # an entire Project
39 class Project:
40     def __init__(self, folder, config):
41         self.folder = folder
42         self.name = config['name']
43         # VCS
44         vcsName = config['vcs']
45         if vcsName == 'git':
46             self.vcs = vcs.Git(self.sourceFolder(), config)
47         elif vcsName == 'svn':
48             self.vcs = vcs.SVN(self.sourceFolder(), config['url'])
49         else:
50             raise Exception("Unknown VCS type "+vcsName)
51         # build system
52         if config.get('buildDeb', False):
53             self.buildSystem = build_system.AutoDebuild(self.sourceFolder(), self.buildFolder(), config, self.vcs)
54         else:
55             buildSystemName = config['buildSystem']
56             if buildSystemName == 'cmake':
57                 self.buildSystem = build_system.CMake(self.sourceFolder(), self.buildFolder(), config)
58             else:
59                 raise Exception("Unknown build system type "+buildSystemName)
60     
61     def sourceFolder(self):
62         return os.path.join(self.folder, self.name)
63     
64     def buildFolder(self):
65         return os.path.join(config['buildDir'], self.sourceFolder())
66
67 # read command-line arguments
68 parser = argparse.ArgumentParser(description='Update and build a bunch of stuff')
69 parser.add_argument("-c", "--config",
70                     dest="config", default="mass-build.conf",
71                     help="mass-build config file")
72 parser.add_argument("--reconfigure",
73                     action="store_true", dest="reconfigure",
74                     help="Force configuration to be run")
75 parser.add_argument("-w", "--wait-after-config",
76                     action="store_true", dest="wait_after_config",
77                     help="Wait for user confirmation after configuration is finished")
78 parser.add_argument("--reset-source",
79                     action="store_true", dest="reset_source",
80                     help="Reset sourcecode to the given version (removes local changes!)")
81 parser.add_argument("--no-update",
82                     action="store_false", dest="update",
83                     help="Do not update projects before compilation")
84 parser.add_argument("--resume-from", metavar='PROJECT',
85                     dest="resume_from",
86                     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)")
87 parser.add_argument("--check-versions",
88                     action="store_true", dest="version_check",
89                     help="Check the repositories for newer tags, if possible (does not perform any building steps)")
90 parser.add_argument("projects",  metavar='PROJECT', nargs='*',
91                     help="Manually specify projects or folders to be built (project names take precedence)")
92 args = parser.parse_args()
93 if args.reset_source and not args.update:
94     raise Exception("Can not reset sources without doing an update")
95
96 # load config as dictionary
97 config = vars(load_module('config', args.config))
98
99 # initialise variables holding the configuration
100 allProjects = OrderedDict() # all projects
101 allFolders = {} # all folders
102 workProjects = [] # projects we work on
103
104 # copy all items which don't exist below, except for those in the exclude list
105 def inherit(subConfig, superConfig, exclude = ('name', 'projects')):
106     for name in superConfig.keys():
107         if (not name in subConfig) and (not name in exclude):
108             subConfig[name] = superConfig[name]
109
110 # populate list of projects, return list of projects in that folder
111 def loadProjects(config, folder=''):
112     folderProjects = []
113     for projectConfig in config['projects']:
114         assert 'name' in projectConfig # everything must have a name
115         inherit(projectConfig, config)
116         if 'projects' in projectConfig: # a subpath
117             folderProjects += loadProjects(projectConfig, os.path.join(folder, projectConfig['name']))
118         else: # a proper project
119             if projectConfig['name'] in allProjects:
120                 raise Exception("Duplicate project name "+projectConfig['name'])
121             project = Project(folder, projectConfig)
122             allProjects[projectConfig['name']] = project
123             folderProjects.append(project)
124     # store projects of this folder
125     if folder in allFolders:
126         raise Exception("Duplicate folder name "+folder)
127     allFolders[folder] = folderProjects
128     return folderProjects
129
130 # load available projects
131 loadProjects(config)
132 # get base set og projects to process
133 if args.projects:
134     for name in args.projects:
135         if name in allProjects:
136             workProjects.append(allProjects[name])
137         elif name in allFolders:
138             workProjects += allFolders[name]
139         else:
140             raise Exception("Project or folder %s does not exist" % name)
141 else:
142     workProjects = list(allProjects.values()) # all the projects
143 # apply the "resume from"
144 if args.resume_from is not None:
145     # find project index
146     startIndex = 0
147     while startIndex < len(workProjects):
148         if workProjects[startIndex].name == args.resume_from:
149             break # we found it
150         else:
151             startIndex += 1
152     if startIndex >= len(workProjects): # project not found
153         raise Exception("%s not found in list of projects to work on" % args.resume_from)
154     # start here
155     workProjects = workProjects[startIndex:]
156
157 # and do it!
158 for project in workProjects:
159     try:
160         if args.version_check:
161             print("Checking project",project.sourceFolder())
162             project.vcs.checkVersions()
163         else:
164             if args.update:
165                 print("Updating project",project.sourceFolder())
166                 project.vcs.update(mode = vcs.MODE_RESET if args.reset_source else vcs.MODE_REBASE)
167             print("Building project",project.sourceFolder())
168             project.buildSystem.build(reconfigure=args.reconfigure, waitAfterConfig=args.wait_after_config)
169         print()
170     except (subprocess.CalledProcessError, KeyboardInterrupt) as e: # for some exceptions, a stackrace is usually pointless
171         print(file=sys.stderr)
172         print(file=sys.stderr)
173         if isinstance(e, KeyboardInterrupt): # str(e) would be the empty string
174             print("Interruped by user while processing %s" % (project.name), file=sys.stderr)
175         else:
176             print("Error while processing %s: %s" % (project.name, str(e)), file=sys.stderr)
177         print(file=sys.stderr)
178         sys.exit(1)
179 print("All operations successfully completed")