blob: 2e7c44f2271eda92085947304028dc03cff2ed04 [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
4from linaropy.git.worktree import Worktree
5
6
7class LLVMSubproject(object):
8 """
9 Class that keeps track of everything related to an LLVM subproject (clang,
10 lld, compiler-rt etc): repo URL, location preferred by CMake,
11 CMake variable for adding or removing it from the build etc.
12 """
13
14 def __init__(self, cmake_path, cmake_var):
15 """Create an LLVMSubproject with the provided info.
16
17 Parameters
18 ----------
19 cmake_path
20 Path relative to the LLVM source root where this subproject should
21 live so that CMake can automatically pick it up during the build.
22 cmake_var
23 The name of the CMake variable that can be used to enable/disable
24 building this subproject.
25 """
26 self.cmake_path = cmake_path
27 self.cmake_var = cmake_var
28
29 def get_cmake_path(self, llvm_source_directory):
30 """
31 Get the path where this subproject should live in the given LLVM tree so
32 that CMake can pick it up by default.
33 """
34 return os.path.join(llvm_source_directory, self.cmake_path)
35
36 @classmethod
37 def get_all_subprojects(cls):
38 """
39 Return a dictionary of all the LLVM subprojects.
40 At the moment, llvm itself is not part of the subprojects, because it
41 always needs to be there and everything is relative to it.
42 """
43 return {
44 "clang":
45 cls(os.path.join("tools", "clang"), "LLVM_TOOL_CLANG_BUILD"),
46 "compiler-rt":
47 cls(os.path.join("projects", "compiler-rt"),
48 "LLVM_TOOL_COMPILER_RT_BUILD"),
49 "libcxx":
50 cls(os.path.join("projects", "libcxx"),
51 "LLVM_TOOL_LIBCXX_BUILD"),
52 "libcxxabi":
53 cls(os.path.join("projects", "libcxxabi"),
54 "LLVM_TOOL_LIBCXXABI_BUILD"),
55 "libunwind":
56 cls(os.path.join("projects", "libunwind"),
57 "LLVM_TOOL_LIBUNWIND_BUILD"),
58 "lld":
59 cls(os.path.join("tools", "lld"), "LLVM_TOOL_LLD_BUILD"),
60 "lldb":
61 cls(os.path.join("tools", "lldb"), "LLVM_TOOL_LLDB_BUILD"),
62 "test-suite":
63 cls(os.path.join("projects", "test-suite"), None),
64 }
65
66
67class LLVMSourceConfig(object):
68 """Class for managing an LLVM source tree.
69
70 It keeps track of which subprojects are enabled in a given tree and provides
71 functionality for adding / removing them.
72 """
73
74 def __init__(self, proj, sourcePath,
75 subprojs=LLVMSubproject.get_all_subprojects()):
76 """ Create a source configuration.
77
78 Parameters
79 ----------
80 proj : Proj
81 Temporary project directory (used mostly for logging).
82 sourcePath
83 Must point to a valid LLVM source tree.
84 subprojs : dictionary
85 Dictionary containing a number of LLVMSubproject objects.
86 By default, this contains all the LLVM subprojects as returned by
87 LLVMSubproject.get_all_subprojects(), but any subset would work just
88 as well.
89 The keys will be used to identify the subprojects in any of this
90 class's methods. It is an error to invoke them with a subproj that
91 does not exist in this dictionary.
92 """
93 sourcePath = str(sourcePath)
94 if not os.path.isdir(sourcePath):
95 raise EnvironmentError("Invalid path to LLVM source tree")
96
97 self.proj = proj
98 self.llvmSourceTree = Worktree(proj, sourcePath)
99 self.subprojs = subprojs
100
101 def get_enabled_subprojects(self):
102 """Get a list of the subprojects enabled in this configuration."""
103 enabled = []
104 for subproj in self.subprojs:
105 if self.__is_enabled(subproj):
106 enabled.append(subproj)
107 return enabled
108
109 def update(self, add={}, remove=[]):
110 """Update the configuration by adding/removing subprojects.
111
112 Parameters
113 ----------
114 add : dictionary
115 A map of (subproject name, repo) to add to the config. The order in
116 which the subprojects are added is unspecified. Subprojects that are
117 already enabled in the config are ignored.
118 remove: list
119 A list of subproject names to remove from the config. The order in
120 which the subprojects are removed is unspecified. Duplicates and
121 subprojects that aren't enabled in the config are ignored. A
122 subproject may be removed even if it is in an invalid state.
123
124 For both add and remove, the subproject name must exist in the
125 dictionary that the config was initialized with and the repo must be a
126 valid GitRepo object.
127
128 TODO: If adding/removing a project fails, this function should try to
129 recover.
130 """
131
132 # Make the inputs friendly
133 if add is None:
134 add = {}
135
136 if remove is None:
137 remove = []
138
Diana Picusb4307602017-04-05 19:48:39 +0200139 for subproj in list(add.keys()):
Diana Picus3b2ef822016-10-13 16:53:18 +0300140 if subproj in remove:
141 raise ValueError("Can't add and remove {} at the same time"
142 .format(subproj))
143
Diana Picusb4307602017-04-05 19:48:39 +0200144 for (subproj, repo) in list(add.items()):
Diana Picus3b2ef822016-10-13 16:53:18 +0300145 self.__add_subproject(subproj, repo)
146
147 for subproj in remove:
148 self.__remove_subproject(subproj)
149
Diana Picusefc7bda2017-06-09 19:14:08 +0200150 def for_each_enabled(self, action):
151 """Perform the given action for each enabled subproject including LLVM.
152
153 The action must be a callable object receiving the path to the
154 subproject's directory as its only parameter.
155
156 If the action throws an exception, it will be rethrown as a
157 RuntimeError. Note that this does not have transactional behaviour.
158 """
159 for subproj in self.get_enabled_subprojects():
160 try:
161 action(self.__get_subproj_cmake_path(subproj))
162 except Exception as exc:
163 raise RuntimeError("Error while processing {}".format(subproj)) from exc
164
165 # Visit LLVM last, in case getting the enabled subprojects errors out.
166 action(self.llvmSourceTree.repodir)
167
Diana Picus3b2ef822016-10-13 16:53:18 +0300168 def __get_subproj_object(self, subprojName):
169 """Get the LLVMSubproject object corresponding to subprojName."""
Diana Picusb4307602017-04-05 19:48:39 +0200170 if not subprojName in list(self.subprojs.keys()):
Diana Picus3b2ef822016-10-13 16:53:18 +0300171 raise ValueError("Unknown llvm subproject {0}".format(subprojName))
172 return self.subprojs[subprojName]
173
174 def __get_subproj_cmake_path(self, subprojName):
175 """Get the full path to subprojName in this source tree."""
176 subproj = self.__get_subproj_object(subprojName)
177 return subproj.get_cmake_path(self.llvmSourceTree.repodir)
178
179 def __is_enabled(self, subprojName):
180 """
181 Check if subproj is enabled in this configuration. A subproj is
Diana Picusefc7bda2017-06-09 19:14:08 +0200182 considered to be enabled if it is a worktree on the correct branch. If
183 a directory for the subproject exists but does not satisfy those
184 conditions, an EnvironmentError is thrown.
Diana Picus3b2ef822016-10-13 16:53:18 +0300185 """
Diana Picusefc7bda2017-06-09 19:14:08 +0200186 subprojPath = self.__get_subproj_cmake_path(subprojName)
187
188 if not os.path.isdir(subprojPath):
Diana Picus3b2ef822016-10-13 16:53:18 +0300189 return False
190
Diana Picusefc7bda2017-06-09 19:14:08 +0200191 existing = Worktree(self.proj, subprojPath)
192
193 if existing.getbranch() != self.llvmSourceTree.getbranch():
194 raise EnvironmentError("{} is on branch {}, but should be on {}".format(
195 subprojName, existing.getbranch(), self.llvmSourceTree.getbranch()))
196
197 return True
Diana Picus3b2ef822016-10-13 16:53:18 +0300198
199 # TODO: add_subproject, remove_subproject and is_enabled should live in
200 # another object (AddRemoveStrategy?) that would know what we want to add
201 # (worktrees, links, whatever)
202 def __add_subproject(self, subprojName, subprojRepo):
203 """Add a given subproject to this configuration.
204
205 This will make sure the subproject's sources are visible in the proper
206 place in the LLVM source tree that the configuration was created with.
207
208 We currently achieve this by creating a worktree for the subproject, but
209 if more flexibility is needed we can add a config option. The branch
210 that the worktree will be created with is the same branch that the
211 existing LLVM source tree is on, and it will be tracking a branch
212 corresponding to the one that the LLVM branch was forked from.
213 """
214 path = self.__get_subproj_cmake_path(subprojName)
215
216 if self.__is_enabled(subprojName):
217 # Subproject has already been added, nothing to do.
218 return
219
220 if os.path.exists(path):
221 raise EnvironmentError(
222 "{} already exists but is not a valid subproject directory."
223 .format(path) +
224 "Please make sure it is a worktree on the same branch as LLVM.")
225
226 # Create a new worktree
227 branch = self.llvmSourceTree.getbranch()
Diana Picusb4307602017-04-05 19:48:39 +0200228 if subprojRepo.branch_exists(branch):
Diana Picus3b2ef822016-10-13 16:53:18 +0300229 Worktree.create(self.proj, subprojRepo, path, branch)
230 else:
231 trackedBranch = "master" # TODO: track proper branch
232 Worktree.create(
233 self.proj,
234 subprojRepo,
235 path,
236 branch,
237 trackedBranch)
238
239 def __remove_subproject(self, subprojName):
240 """Remove a given subproject from this build configuration."""
241 path = self.__get_subproj_cmake_path(subprojName)
242
243 if not os.path.isdir(path):
244 return
245
246 worktree = Worktree(self.proj, path)
247 worktree.clean(False)
Diana Picusefc7bda2017-06-09 19:14:08 +0200248
249
250# FIXME: repo.pushToBranch doesn't work, because it doesn't handle remote
251# branches properly. Furthermore, there's no support for getting the remote URL,
252# so we need to resort to raw git commands. We may also consider moving the
253# functionality for parsing info out of the remote URL into the repo object.
254from sh import git
255from linaropy.cd import cd
256
257
258def get_user_from_remote(url):
259 """Get the username used as part of the remote URL, or None.
260
261 The remote URLs that we expect to see look like $protocol://$user@$location.
262 If they look any different, we won't be able to parse them.
263 """
264 pattern = re.compile("(.*://)?(?P<user>.*)@(.*)\n?")
265 match = pattern.match(str(url))
266 if match is None:
267 return None
268 return match.group('user')
269
270
271def push_current_branch(proj, pathToRepo):
272 """Push the current branch to origin.
273
274 It will be pushed into linaro-local/$user/$branch, where $branch is the
275 current branch and $user is the username used as part of the remote URL.
276 """
277 repo = Worktree(proj, pathToRepo)
278
279 with cd(repo.repodir):
280 remote = git("remote", "get-url", "--push", "origin").strip()
281 user = get_user_from_remote(remote)
282 if not user:
283 raise EnvironmentError("Couldn't parse user from {}.".format(remote))
284
285 local_branch = repo.getbranch()
286 remote_branch = "linaro-local/{}/{}".format(user, local_branch)
287 if not repo.is_valid_branch_name(remote_branch):
288 raise EnvironmentError(
289 "{} is not a valid branch name.".format(remote_branch))
290
291 with cd(repo.repodir):
292 git("push", "-u", "origin", "+{}:{}".format(local_branch, remote_branch))