blob: 76b317ece25e5e00b607b18f7842024721d398bd [file] [log] [blame]
Diana Picus3b2ef822016-10-13 16:53:18 +03001import os
Diana Picusefc7bda2017-06-09 19:14:08 +02002import re
Diana Picus3b2ef822016-10-13 16:53:18 +03003
Diana Picus052b7d32017-11-24 16:19:41 +01004from functools import partial
5
Diana Picus3b2ef822016-10-13 16:53:18 +03006from linaropy.git.worktree import Worktree
7
8
9class LLVMSubproject(object):
10 """
11 Class that keeps track of everything related to an LLVM subproject (clang,
12 lld, compiler-rt etc): repo URL, location preferred by CMake,
13 CMake variable for adding or removing it from the build etc.
14 """
15
16 def __init__(self, cmake_path, cmake_var):
17 """Create an LLVMSubproject with the provided info.
18
19 Parameters
20 ----------
21 cmake_path
22 Path relative to the LLVM source root where this subproject should
23 live so that CMake can automatically pick it up during the build.
24 cmake_var
25 The name of the CMake variable that can be used to enable/disable
26 building this subproject.
27 """
28 self.cmake_path = cmake_path
29 self.cmake_var = cmake_var
30
31 def get_cmake_path(self, llvm_source_directory):
32 """
33 Get the path where this subproject should live in the given LLVM tree so
34 that CMake can pick it up by default.
35 """
36 return os.path.join(llvm_source_directory, self.cmake_path)
37
38 @classmethod
39 def get_all_subprojects(cls):
40 """
41 Return a dictionary of all the LLVM subprojects.
42 At the moment, llvm itself is not part of the subprojects, because it
43 always needs to be there and everything is relative to it.
44 """
45 return {
46 "clang":
47 cls(os.path.join("tools", "clang"), "LLVM_TOOL_CLANG_BUILD"),
48 "compiler-rt":
49 cls(os.path.join("projects", "compiler-rt"),
50 "LLVM_TOOL_COMPILER_RT_BUILD"),
51 "libcxx":
52 cls(os.path.join("projects", "libcxx"),
53 "LLVM_TOOL_LIBCXX_BUILD"),
54 "libcxxabi":
55 cls(os.path.join("projects", "libcxxabi"),
56 "LLVM_TOOL_LIBCXXABI_BUILD"),
57 "libunwind":
58 cls(os.path.join("projects", "libunwind"),
59 "LLVM_TOOL_LIBUNWIND_BUILD"),
60 "lld":
61 cls(os.path.join("tools", "lld"), "LLVM_TOOL_LLD_BUILD"),
62 "lldb":
63 cls(os.path.join("tools", "lldb"), "LLVM_TOOL_LLDB_BUILD"),
64 "test-suite":
65 cls(os.path.join("projects", "test-suite"), None),
66 }
67
68
69class LLVMSourceConfig(object):
70 """Class for managing an LLVM source tree.
71
72 It keeps track of which subprojects are enabled in a given tree and provides
73 functionality for adding / removing them.
74 """
75
76 def __init__(self, proj, sourcePath,
77 subprojs=LLVMSubproject.get_all_subprojects()):
78 """ Create a source configuration.
79
80 Parameters
81 ----------
82 proj : Proj
83 Temporary project directory (used mostly for logging).
84 sourcePath
85 Must point to a valid LLVM source tree.
86 subprojs : dictionary
87 Dictionary containing a number of LLVMSubproject objects.
88 By default, this contains all the LLVM subprojects as returned by
89 LLVMSubproject.get_all_subprojects(), but any subset would work just
90 as well.
91 The keys will be used to identify the subprojects in any of this
92 class's methods. It is an error to invoke them with a subproj that
93 does not exist in this dictionary.
94 """
95 sourcePath = str(sourcePath)
96 if not os.path.isdir(sourcePath):
97 raise EnvironmentError("Invalid path to LLVM source tree")
98
99 self.proj = proj
100 self.llvmSourceTree = Worktree(proj, sourcePath)
101 self.subprojs = subprojs
102
Diana Picus052b7d32017-11-24 16:19:41 +0100103 def get_path(self):
104 """Get the path corresponding to this source config."""
105 return self.llvmSourceTree.repodir
106
Diana Picus3b2ef822016-10-13 16:53:18 +0300107 def get_enabled_subprojects(self):
108 """Get a list of the subprojects enabled in this configuration."""
109 enabled = []
110 for subproj in self.subprojs:
111 if self.__is_enabled(subproj):
112 enabled.append(subproj)
113 return enabled
114
115 def update(self, add={}, remove=[]):
116 """Update the configuration by adding/removing subprojects.
117
118 Parameters
119 ----------
120 add : dictionary
121 A map of (subproject name, repo) to add to the config. The order in
122 which the subprojects are added is unspecified. Subprojects that are
123 already enabled in the config are ignored.
124 remove: list
125 A list of subproject names to remove from the config. The order in
126 which the subprojects are removed is unspecified. Duplicates and
127 subprojects that aren't enabled in the config are ignored. A
128 subproject may be removed even if it is in an invalid state.
129
130 For both add and remove, the subproject name must exist in the
131 dictionary that the config was initialized with and the repo must be a
132 valid GitRepo object.
133
134 TODO: If adding/removing a project fails, this function should try to
135 recover.
136 """
137
138 # Make the inputs friendly
139 if add is None:
140 add = {}
141
142 if remove is None:
143 remove = []
144
Diana Picusb4307602017-04-05 19:48:39 +0200145 for subproj in list(add.keys()):
Diana Picus3b2ef822016-10-13 16:53:18 +0300146 if subproj in remove:
147 raise ValueError("Can't add and remove {} at the same time"
148 .format(subproj))
149
Diana Picusb4307602017-04-05 19:48:39 +0200150 for (subproj, repo) in list(add.items()):
Diana Picus3b2ef822016-10-13 16:53:18 +0300151 self.__add_subproject(subproj, repo)
152
153 for subproj in remove:
154 self.__remove_subproject(subproj)
155
Diana Picusefc7bda2017-06-09 19:14:08 +0200156 def for_each_enabled(self, action):
157 """Perform the given action for each enabled subproject including LLVM.
158
159 The action must be a callable object receiving the path to the
160 subproject's directory as its only parameter.
161
162 If the action throws an exception, it will be rethrown as a
163 RuntimeError. Note that this does not have transactional behaviour.
164 """
165 for subproj in self.get_enabled_subprojects():
166 try:
167 action(self.__get_subproj_cmake_path(subproj))
168 except Exception as exc:
169 raise RuntimeError("Error while processing {}".format(subproj)) from exc
170
171 # Visit LLVM last, in case getting the enabled subprojects errors out.
172 action(self.llvmSourceTree.repodir)
173
Diana Picus5fec8b72017-11-27 16:09:58 +0100174 def for_each_subproj(self, action):
175 """Perform the given action for each subproject excluding LLVM.
176
177 The action must be a callable object receiving an LLVMSubproject
178 parameter and a boolean representing whether the subproject is enabled
179 in the current configuration or not.
180
181 If the action throws an exception, it will be rethrown as a
182 RuntimeError. Note that this does not have transactional behaviour.
183 """
184 for subprojName, subproj in self.subprojs.items():
185 try:
186 action(subproj, self.__is_enabled(subprojName))
187 except Exception as exc:
188 raise RuntimeError("Error while processing {}".format(subprojName)) from exc
189
Diana Picus3b2ef822016-10-13 16:53:18 +0300190 def __get_subproj_object(self, subprojName):
191 """Get the LLVMSubproject object corresponding to subprojName."""
Diana Picusb4307602017-04-05 19:48:39 +0200192 if not subprojName in list(self.subprojs.keys()):
Diana Picus3b2ef822016-10-13 16:53:18 +0300193 raise ValueError("Unknown llvm subproject {0}".format(subprojName))
194 return self.subprojs[subprojName]
195
196 def __get_subproj_cmake_path(self, subprojName):
197 """Get the full path to subprojName in this source tree."""
198 subproj = self.__get_subproj_object(subprojName)
199 return subproj.get_cmake_path(self.llvmSourceTree.repodir)
200
201 def __is_enabled(self, subprojName):
202 """
203 Check if subproj is enabled in this configuration. A subproj is
Diana Picusefc7bda2017-06-09 19:14:08 +0200204 considered to be enabled if it is a worktree on the correct branch. If
205 a directory for the subproject exists but does not satisfy those
206 conditions, an EnvironmentError is thrown.
Diana Picus3b2ef822016-10-13 16:53:18 +0300207 """
Diana Picusefc7bda2017-06-09 19:14:08 +0200208 subprojPath = self.__get_subproj_cmake_path(subprojName)
209
210 if not os.path.isdir(subprojPath):
Diana Picus3b2ef822016-10-13 16:53:18 +0300211 return False
212
Diana Picusefc7bda2017-06-09 19:14:08 +0200213 existing = Worktree(self.proj, subprojPath)
214
215 if existing.getbranch() != self.llvmSourceTree.getbranch():
216 raise EnvironmentError("{} is on branch {}, but should be on {}".format(
217 subprojName, existing.getbranch(), self.llvmSourceTree.getbranch()))
218
219 return True
Diana Picus3b2ef822016-10-13 16:53:18 +0300220
221 # TODO: add_subproject, remove_subproject and is_enabled should live in
222 # another object (AddRemoveStrategy?) that would know what we want to add
223 # (worktrees, links, whatever)
224 def __add_subproject(self, subprojName, subprojRepo):
225 """Add a given subproject to this configuration.
226
227 This will make sure the subproject's sources are visible in the proper
228 place in the LLVM source tree that the configuration was created with.
229
230 We currently achieve this by creating a worktree for the subproject, but
231 if more flexibility is needed we can add a config option. The branch
232 that the worktree will be created with is the same branch that the
233 existing LLVM source tree is on, and it will be tracking a branch
234 corresponding to the one that the LLVM branch was forked from.
235 """
236 path = self.__get_subproj_cmake_path(subprojName)
237
238 if self.__is_enabled(subprojName):
239 # Subproject has already been added, nothing to do.
240 return
241
242 if os.path.exists(path):
243 raise EnvironmentError(
244 "{} already exists but is not a valid subproject directory."
245 .format(path) +
246 "Please make sure it is a worktree on the same branch as LLVM.")
247
248 # Create a new worktree
249 branch = self.llvmSourceTree.getbranch()
Diana Picusb4307602017-04-05 19:48:39 +0200250 if subprojRepo.branch_exists(branch):
Diana Picus3b2ef822016-10-13 16:53:18 +0300251 Worktree.create(self.proj, subprojRepo, path, branch)
252 else:
253 trackedBranch = "master" # TODO: track proper branch
254 Worktree.create(
255 self.proj,
256 subprojRepo,
257 path,
258 branch,
259 trackedBranch)
260
261 def __remove_subproject(self, subprojName):
262 """Remove a given subproject from this build configuration."""
263 path = self.__get_subproj_cmake_path(subprojName)
264
265 if not os.path.isdir(path):
266 return
267
268 worktree = Worktree(self.proj, path)
269 worktree.clean(False)
Diana Picusefc7bda2017-06-09 19:14:08 +0200270
271
Diana Picus052b7d32017-11-24 16:19:41 +0100272class LLVMBuildConfig(object):
273 """Class for managing an LLVM build directory.
274
275 It should know how to configure a build directory (with CMake) and how to
276 run a build command. The directory must already exist, but it may be empty.
277 """
278
279 def __init__(self, sourceConfig, buildDir=None):
280 """Create an LLVMBuildConfig."""
281 self.sourceConfig = sourceConfig
282 self.buildDir = buildDir
283
284 def cmake(self, commandConsumer, cmakeFlags, generator):
285 """
286 Generate the CMake command needed for configuring the build directory
287 with the given flags and generator, and pass it to the 'commandConsumer'.
288
289 The command will always explicitly enable or disable the build of
290 specific subprojects to mirror the source config. This is important
291 because projects can always be added or removed from the source config,
292 and CMake doesn't by default pick up the new situation (so we might end
293 up trying to build subprojects that were removed, or not build
294 subprojects that were added).
295
296 The 'commandConsumer' should have a 'consume' method taking two
297 parameters: the command to be consumed (in the form of a list) and the
298 directory where the command should be run. Any exceptions that may be
299 raised by that method should be handled by the calling code.
300 """
301 cmakeSubprojFlags = self.__get_subproj_flags()
302 command = ["cmake", "-G", generator] + cmakeSubprojFlags + \
303 cmakeFlags + [self.sourceConfig.get_path()]
304 commandConsumer.consume(command, self.buildDir)
305
306 def __get_subproj_flags(self):
307 """
308 Get the CMake flags needed to explicitly enable or disable the build of
309 each specific subproject, depending on whether it is enabled or disabled
310 in the source config.
311 """
312 def append_cmake_var(cmakeVars, subproj, enabled):
313 if subproj.cmake_var is None:
314 return
315
316 if enabled:
317 status = "ON"
318 else:
319 status = "OFF"
320 cmakeVars.append("-D{}={}".format(subproj.cmake_var, status))
321
322 cmakeSubprojFlags = []
323 self.sourceConfig.for_each_subproj(partial(append_cmake_var,
324 cmakeSubprojFlags))
325
326 return cmakeSubprojFlags