blob: 18e47303aa52ad4a2cbd0b0e34679d29cfe80c12 [file] [log] [blame]
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301#!/usr/bin/env python3
Matthew Barthccb7f852016-11-23 17:43:02 -06002
3"""
4This script determines the given package's openbmc dependencies from its
5configure.ac file where it downloads, configures, builds, and installs each of
6these dependencies. Then the given package is configured, built, and installed
7prior to executing its unit tests.
8"""
9
Matthew Barthd1810372016-12-19 16:57:21 -060010from git import Repo
William A. Kennington IIIfcd70772020-06-04 00:50:23 -070011from mesonbuild import coredata, optinterpreter
Patrick Williamse95626d2021-04-13 11:13:53 -050012from mesonbuild.mesonlib import OptionKey
Andrew Jeffery89b64b62020-03-13 12:15:48 +103013from urllib.parse import urljoin
Andrew Jefferya4e31c62018-03-08 13:45:28 +103014from subprocess import check_call, call, CalledProcessError
Matthew Barthccb7f852016-11-23 17:43:02 -060015import os
16import sys
Matthew Barth33df8792016-12-19 14:30:17 -060017import argparse
William A. Kennington IIIa2156732018-06-30 18:38:09 -070018import multiprocessing
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050019import re
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -080020import subprocess
William A. Kennington III3f1d1202018-12-06 18:02:07 -080021import shutil
William A. Kennington III4e1d0a12018-07-16 12:04:03 -070022import platform
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050023
24
25class DepTree():
26 """
27 Represents package dependency tree, where each node is a DepTree with a
28 name and DepTree children.
29 """
30
31 def __init__(self, name):
32 """
33 Create new DepTree.
34
35 Parameter descriptions:
36 name Name of new tree node.
37 """
38 self.name = name
39 self.children = list()
40
41 def AddChild(self, name):
42 """
43 Add new child node to current node.
44
45 Parameter descriptions:
46 name Name of new child
47 """
48 new_child = DepTree(name)
49 self.children.append(new_child)
50 return new_child
51
52 def AddChildNode(self, node):
53 """
54 Add existing child node to current node.
55
56 Parameter descriptions:
57 node Tree node to add
58 """
59 self.children.append(node)
60
61 def RemoveChild(self, name):
62 """
63 Remove child node.
64
65 Parameter descriptions:
66 name Name of child to remove
67 """
68 for child in self.children:
69 if child.name == name:
70 self.children.remove(child)
71 return
72
73 def GetNode(self, name):
74 """
75 Return node with matching name. Return None if not found.
76
77 Parameter descriptions:
78 name Name of node to return
79 """
80 if self.name == name:
81 return self
82 for child in self.children:
83 node = child.GetNode(name)
84 if node:
85 return node
86 return None
87
88 def GetParentNode(self, name, parent_node=None):
89 """
90 Return parent of node with matching name. Return none if not found.
91
92 Parameter descriptions:
93 name Name of node to get parent of
94 parent_node Parent of current node
95 """
96 if self.name == name:
97 return parent_node
98 for child in self.children:
99 found_node = child.GetParentNode(name, self)
100 if found_node:
101 return found_node
102 return None
103
104 def GetPath(self, name, path=None):
105 """
106 Return list of node names from head to matching name.
107 Return None if not found.
108
109 Parameter descriptions:
110 name Name of node
111 path List of node names from head to current node
112 """
113 if not path:
114 path = []
115 if self.name == name:
116 path.append(self.name)
117 return path
118 for child in self.children:
119 match = child.GetPath(name, path + [self.name])
120 if match:
121 return match
122 return None
123
124 def GetPathRegex(self, name, regex_str, path=None):
125 """
126 Return list of node paths that end in name, or match regex_str.
127 Return empty list if not found.
128
129 Parameter descriptions:
130 name Name of node to search for
131 regex_str Regex string to match node names
132 path Path of node names from head to current node
133 """
134 new_paths = []
135 if not path:
136 path = []
137 match = re.match(regex_str, self.name)
138 if (self.name == name) or (match):
139 new_paths.append(path + [self.name])
140 for child in self.children:
141 return_paths = None
142 full_path = path + [self.name]
143 return_paths = child.GetPathRegex(name, regex_str, full_path)
144 for i in return_paths:
145 new_paths.append(i)
146 return new_paths
147
148 def MoveNode(self, from_name, to_name):
149 """
150 Mode existing from_name node to become child of to_name node.
151
152 Parameter descriptions:
153 from_name Name of node to make a child of to_name
154 to_name Name of node to make parent of from_name
155 """
156 parent_from_node = self.GetParentNode(from_name)
157 from_node = self.GetNode(from_name)
158 parent_from_node.RemoveChild(from_name)
159 to_node = self.GetNode(to_name)
160 to_node.AddChildNode(from_node)
161
162 def ReorderDeps(self, name, regex_str):
163 """
164 Reorder dependency tree. If tree contains nodes with names that
165 match 'name' and 'regex_str', move 'regex_str' nodes that are
166 to the right of 'name' node, so that they become children of the
167 'name' node.
168
169 Parameter descriptions:
170 name Name of node to look for
171 regex_str Regex string to match names to
172 """
173 name_path = self.GetPath(name)
174 if not name_path:
175 return
176 paths = self.GetPathRegex(name, regex_str)
177 is_name_in_paths = False
178 name_index = 0
179 for i in range(len(paths)):
180 path = paths[i]
181 if path[-1] == name:
182 is_name_in_paths = True
183 name_index = i
184 break
185 if not is_name_in_paths:
186 return
187 for i in range(name_index + 1, len(paths)):
188 path = paths[i]
189 if name in path:
190 continue
191 from_name = path[-1]
192 self.MoveNode(from_name, name)
193
194 def GetInstallList(self):
195 """
196 Return post-order list of node names.
197
198 Parameter descriptions:
199 """
200 install_list = []
201 for child in self.children:
202 child_install_list = child.GetInstallList()
203 install_list.extend(child_install_list)
204 install_list.append(self.name)
205 return install_list
206
207 def PrintTree(self, level=0):
208 """
209 Print pre-order node names with indentation denoting node depth level.
210
211 Parameter descriptions:
212 level Current depth level
213 """
214 INDENT_PER_LEVEL = 4
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030215 print(' ' * (level * INDENT_PER_LEVEL) + self.name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500216 for child in self.children:
217 child.PrintTree(level + 1)
Matthew Barth33df8792016-12-19 14:30:17 -0600218
219
William A. Kennington III1fddb972019-02-06 18:03:53 -0800220def check_call_cmd(*cmd):
Matthew Barth33df8792016-12-19 14:30:17 -0600221 """
222 Verbose prints the directory location the given command is called from and
223 the command, then executes the command using check_call.
224
225 Parameter descriptions:
226 dir Directory location command is to be called from
227 cmd List of parameters constructing the complete command
228 """
William A. Kennington III1fddb972019-02-06 18:03:53 -0800229 printline(os.getcwd(), ">", " ".join(cmd))
Matthew Barth33df8792016-12-19 14:30:17 -0600230 check_call(cmd)
Matthew Barthccb7f852016-11-23 17:43:02 -0600231
232
Andrew Geisslera61acb52019-01-03 16:32:44 -0600233def clone_pkg(pkg, branch):
Matthew Barth33df8792016-12-19 14:30:17 -0600234 """
235 Clone the given openbmc package's git repository from gerrit into
236 the WORKSPACE location
237
238 Parameter descriptions:
239 pkg Name of the package to clone
Andrew Geisslera61acb52019-01-03 16:32:44 -0600240 branch Branch to clone from pkg
Matthew Barth33df8792016-12-19 14:30:17 -0600241 """
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030242 pkg_dir = os.path.join(WORKSPACE, pkg)
243 if os.path.exists(os.path.join(pkg_dir, '.git')):
244 return pkg_dir
Matthew Barthccb7f852016-11-23 17:43:02 -0600245 pkg_repo = urljoin('https://gerrit.openbmc-project.xyz/openbmc/', pkg)
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030246 os.mkdir(pkg_dir)
Andrew Geisslera61acb52019-01-03 16:32:44 -0600247 printline(pkg_dir, "> git clone", pkg_repo, branch, "./")
248 try:
249 # first try the branch
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030250 clone = Repo.clone_from(pkg_repo, pkg_dir, branch=branch)
251 repo_inst = clone.working_dir
Andrew Geisslera61acb52019-01-03 16:32:44 -0600252 except:
253 printline("Input branch not found, default to master")
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030254 clone = Repo.clone_from(pkg_repo, pkg_dir, branch="master")
255 repo_inst = clone.working_dir
Andrew Geisslera61acb52019-01-03 16:32:44 -0600256 return repo_inst
Matthew Barth33df8792016-12-19 14:30:17 -0600257
258
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030259def make_target_exists(target):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800260 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030261 Runs a check against the makefile in the current directory to determine
262 if the target exists so that it can be built.
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800263
264 Parameter descriptions:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030265 target The make target we are checking
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800266 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030267 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030268 cmd = ['make', '-n', target]
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030269 with open(os.devnull, 'w') as devnull:
270 check_call(cmd, stdout=devnull, stderr=devnull)
271 return True
272 except CalledProcessError:
273 return False
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800274
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800275
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700276make_parallel = [
277 'make',
278 # Run enough jobs to saturate all the cpus
279 '-j', str(multiprocessing.cpu_count()),
280 # Don't start more jobs if the load avg is too high
281 '-l', str(multiprocessing.cpu_count()),
282 # Synchronize the output so logs aren't intermixed in stdout / stderr
283 '-O',
284]
285
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800286
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030287def build_and_install(name, build_for_testing=False):
William A. Kennington III780ec092018-12-06 14:46:50 -0800288 """
289 Builds and installs the package in the environment. Optionally
290 builds the examples and test cases for package.
291
292 Parameter description:
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030293 name The name of the package we are building
William A. Kennington IIIa0454912018-12-06 14:47:16 -0800294 build_for_testing Enable options related to testing on the package?
William A. Kennington III780ec092018-12-06 14:46:50 -0800295 """
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030296 os.chdir(os.path.join(WORKSPACE, name))
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800297
298 # Refresh dynamic linker run time bindings for dependencies
William A. Kennington III1fddb972019-02-06 18:03:53 -0800299 check_call_cmd('sudo', '-n', '--', 'ldconfig')
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800300
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030301 pkg = Package()
302 if build_for_testing:
303 pkg.test()
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800304 else:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030305 pkg.install()
306
William A. Kennington III780ec092018-12-06 14:46:50 -0800307
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030308def build_dep_tree(name, pkgdir, dep_added, head, branch, dep_tree=None):
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500309 """
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030310 For each package (name), starting with the package to be unit tested,
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030311 extract its dependencies. For each package dependency defined, recursively
312 apply the same strategy
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500313
314 Parameter descriptions:
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030315 name Name of the package
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500316 pkgdir Directory where package source is located
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800317 dep_added Current dict of dependencies and added status
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500318 head Head node of the dependency tree
Andrew Geisslera61acb52019-01-03 16:32:44 -0600319 branch Branch to clone from pkg
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500320 dep_tree Current dependency tree node
321 """
322 if not dep_tree:
323 dep_tree = head
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800324
William A. Kennington IIIbe6aab22018-12-06 15:01:54 -0800325 with open("/tmp/depcache", "r") as depcache:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800326 cache = depcache.readline()
327
328 # Read out pkg dependencies
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030329 pkg = Package(name, pkgdir)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800330
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600331 build = pkg.build_system()
332 if build == None:
333 raise Exception(f"Unable to find build system for {name}.")
334
335 for dep in set(build.dependencies()):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800336 if dep in cache:
337 continue
338 # Dependency package not already known
339 if dep_added.get(dep) is None:
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600340 print(f"Adding {dep} dependency to {name}.")
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800341 # Dependency package not added
342 new_child = dep_tree.AddChild(dep)
343 dep_added[dep] = False
Andrew Jeffery3b92fdd2020-03-13 11:49:18 +1030344 dep_pkgdir = clone_pkg(dep, branch)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800345 # Determine this dependency package's
346 # dependencies and add them before
347 # returning to add this package
348 dep_added = build_dep_tree(dep,
349 dep_pkgdir,
350 dep_added,
351 head,
Andrew Geisslera61acb52019-01-03 16:32:44 -0600352 branch,
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800353 new_child)
354 else:
355 # Dependency package known and added
356 if dep_added[dep]:
Andrew Jeffery2cb0c7a2018-03-08 13:19:08 +1030357 continue
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500358 else:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800359 # Cyclic dependency failure
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030360 raise Exception("Cyclic dependencies found in "+name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500361
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030362 if not dep_added[name]:
363 dep_added[name] = True
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500364
365 return dep_added
Matthew Barthccb7f852016-11-23 17:43:02 -0600366
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700367
William A. Kennington III90b106a2019-02-06 18:08:24 -0800368def run_cppcheck():
Andrew Jefferybf735102020-03-13 12:13:21 +1030369 match_re = re.compile(r'((?!\.mako\.).)*\.[ch](?:pp)?$', re.I)
Brad Bishop48424d42020-01-07 13:01:31 -0500370 cppcheck_files = []
371 stdout = subprocess.check_output(['git', 'ls-files'])
Patrick Venturead4354e2018-10-12 16:59:54 -0700372
Brad Bishop48424d42020-01-07 13:01:31 -0500373 for f in stdout.decode('utf-8').split():
374 if match_re.match(f):
375 cppcheck_files.append(f)
376
377 if not cppcheck_files:
378 # skip cppcheck if there arent' any c or cpp sources.
379 print("no files")
380 return None
381
382 # http://cppcheck.sourceforge.net/manual.pdf
383 params = ['cppcheck', '-j', str(multiprocessing.cpu_count()),
Manojkiran Eda89a46cc2020-08-22 17:08:11 +0530384 '--enable=all', '--library=googletest', '--file-list=-']
Brad Bishop48424d42020-01-07 13:01:31 -0500385
386 cppcheck_process = subprocess.Popen(
387 params,
388 stdout=subprocess.PIPE,
389 stderr=subprocess.PIPE,
390 stdin=subprocess.PIPE)
391 (stdout, stderr) = cppcheck_process.communicate(
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030392 input='\n'.join(cppcheck_files).encode('utf-8'))
Brad Bishop48424d42020-01-07 13:01:31 -0500393
394 if cppcheck_process.wait():
Patrick Venturead4354e2018-10-12 16:59:54 -0700395 raise Exception('Cppcheck failed')
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030396 print(stdout.decode('utf-8'))
397 print(stderr.decode('utf-8'))
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700398
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030399
William A. Kennington III37a89a22018-12-13 14:32:02 -0800400def is_valgrind_safe():
401 """
402 Returns whether it is safe to run valgrind on our platform
403 """
William A. Kennington III0326ded2019-02-07 00:33:28 -0800404 src = 'unit-test-vg.c'
405 exe = './unit-test-vg'
406 with open(src, 'w') as h:
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700407 h.write('#include <errno.h>\n')
408 h.write('#include <stdio.h>\n')
William A. Kennington III0326ded2019-02-07 00:33:28 -0800409 h.write('#include <stdlib.h>\n')
410 h.write('#include <string.h>\n')
411 h.write('int main() {\n')
412 h.write('char *heap_str = malloc(16);\n')
413 h.write('strcpy(heap_str, "RandString");\n')
414 h.write('int res = strcmp("RandString", heap_str);\n')
415 h.write('free(heap_str);\n')
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700416 h.write('char errstr[64];\n')
417 h.write('strerror_r(EINVAL, errstr, sizeof(errstr));\n')
418 h.write('printf("%s\\n", errstr);\n')
William A. Kennington III0326ded2019-02-07 00:33:28 -0800419 h.write('return res;\n')
420 h.write('}\n')
421 try:
422 with open(os.devnull, 'w') as devnull:
423 check_call(['gcc', '-O2', '-o', exe, src],
424 stdout=devnull, stderr=devnull)
425 check_call(['valgrind', '--error-exitcode=99', exe],
426 stdout=devnull, stderr=devnull)
427 return True
428 except:
429 sys.stderr.write("###### Platform is not valgrind safe ######\n")
430 return False
431 finally:
432 os.remove(src)
433 os.remove(exe)
William A. Kennington III37a89a22018-12-13 14:32:02 -0800434
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030435
William A. Kennington III282e3302019-02-04 16:55:05 -0800436def is_sanitize_safe():
437 """
438 Returns whether it is safe to run sanitizers on our platform
439 """
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800440 src = 'unit-test-sanitize.c'
441 exe = './unit-test-sanitize'
442 with open(src, 'w') as h:
443 h.write('int main() { return 0; }\n')
444 try:
445 with open(os.devnull, 'w') as devnull:
446 check_call(['gcc', '-O2', '-fsanitize=address',
447 '-fsanitize=undefined', '-o', exe, src],
448 stdout=devnull, stderr=devnull)
449 check_call([exe], stdout=devnull, stderr=devnull)
450 return True
451 except:
452 sys.stderr.write("###### Platform is not sanitize safe ######\n")
453 return False
454 finally:
455 os.remove(src)
456 os.remove(exe)
William A. Kennington III282e3302019-02-04 16:55:05 -0800457
William A. Kennington III49d4e592019-02-06 17:59:27 -0800458
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800459def maybe_make_valgrind():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700460 """
461 Potentially runs the unit tests through valgrind for the package
462 via `make check-valgrind`. If the package does not have valgrind testing
463 then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700464 """
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700465 # Valgrind testing is currently broken by an aggressive strcmp optimization
466 # that is inlined into optimized code for POWER by gcc 7+. Until we find
467 # a workaround, just don't run valgrind tests on POWER.
468 # https://github.com/openbmc/openbmc/issues/3315
William A. Kennington III37a89a22018-12-13 14:32:02 -0800469 if not is_valgrind_safe():
William A. Kennington III75130192019-02-07 00:34:14 -0800470 sys.stderr.write("###### Skipping valgrind ######\n")
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700471 return
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700472 if not make_target_exists('check-valgrind'):
473 return
474
475 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030476 cmd = make_parallel + ['check-valgrind']
William A. Kennington III1fddb972019-02-06 18:03:53 -0800477 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700478 except CalledProcessError:
William A. Kennington III90b106a2019-02-06 18:08:24 -0800479 for root, _, files in os.walk(os.getcwd()):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700480 for f in files:
481 if re.search('test-suite-[a-z]+.log', f) is None:
482 continue
William A. Kennington III1fddb972019-02-06 18:03:53 -0800483 check_call_cmd('cat', os.path.join(root, f))
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700484 raise Exception('Valgrind tests failed')
485
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030486
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800487def maybe_make_coverage():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700488 """
489 Potentially runs the unit tests through code coverage for the package
490 via `make check-code-coverage`. If the package does not have code coverage
491 testing then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700492 """
493 if not make_target_exists('check-code-coverage'):
494 return
495
496 # Actually run code coverage
497 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030498 cmd = make_parallel + ['check-code-coverage']
William A. Kennington III1fddb972019-02-06 18:03:53 -0800499 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700500 except CalledProcessError:
501 raise Exception('Code coverage failed')
Matthew Barthccb7f852016-11-23 17:43:02 -0600502
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030503
504class BuildSystem(object):
505 """
506 Build systems generally provide the means to configure, build, install and
507 test software. The BuildSystem class defines a set of interfaces on top of
508 which Autotools, Meson, CMake and possibly other build system drivers can
509 be implemented, separating out the phases to control whether a package
510 should merely be installed or also tested and analyzed.
511 """
512 def __init__(self, package, path):
513 """Initialise the driver with properties independent of the build system
514
515 Keyword arguments:
516 package: The name of the package. Derived from the path if None
517 path: The path to the package. Set to the working directory if None
518 """
519 self.path = "." if not path else path
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030520 realpath = os.path.realpath(self.path)
521 self.package = package if package else os.path.basename(realpath)
Andrew Jeffery3f8e5a72020-03-13 11:47:56 +1030522 self.build_for_testing = False
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030523
524 def probe(self):
525 """Test if the build system driver can be applied to the package
526
527 Return True if the driver can drive the package's build system,
528 otherwise False.
529
530 Generally probe() is implemented by testing for the presence of the
531 build system's configuration file(s).
532 """
533 raise NotImplemented
534
535 def dependencies(self):
536 """Provide the package's dependencies
537
538 Returns a list of dependencies. If no dependencies are required then an
539 empty list must be returned.
540
541 Generally dependencies() is implemented by analysing and extracting the
542 data from the build system configuration.
543 """
544 raise NotImplemented
545
546 def configure(self, build_for_testing):
547 """Configure the source ready for building
548
549 Should raise an exception if configuration failed.
550
551 Keyword arguments:
552 build_for_testing: Mark the package as being built for testing rather
553 than for installation as a dependency for the
554 package under test. Setting to True generally
555 implies that the package will be configured to build
556 with debug information, at a low level of
557 optimisation and possibly with sanitizers enabled.
558
559 Generally configure() is implemented by invoking the build system
560 tooling to generate Makefiles or equivalent.
561 """
562 raise NotImplemented
563
564 def build(self):
565 """Build the software ready for installation and/or testing
566
567 Should raise an exception if the build fails
568
569 Generally build() is implemented by invoking `make` or `ninja`.
570 """
571 raise NotImplemented
572
573 def install(self):
574 """Install the software ready for use
575
576 Should raise an exception if installation fails
577
578 Like build(), install() is generally implemented by invoking `make` or
579 `ninja`.
580 """
581 raise NotImplemented
582
583 def test(self):
584 """Build and run the test suite associated with the package
585
586 Should raise an exception if the build or testing fails.
587
588 Like install(), test() is generally implemented by invoking `make` or
589 `ninja`.
590 """
591 raise NotImplemented
592
593 def analyze(self):
594 """Run any supported analysis tools over the codebase
595
596 Should raise an exception if analysis fails.
597
598 Some analysis tools such as scan-build need injection into the build
599 system. analyze() provides the necessary hook to implement such
600 behaviour. Analyzers independent of the build system can also be
601 specified here but at the cost of possible duplication of code between
602 the build system driver implementations.
603 """
604 raise NotImplemented
605
606
607class Autotools(BuildSystem):
608 def __init__(self, package=None, path=None):
609 super(Autotools, self).__init__(package, path)
610
611 def probe(self):
612 return os.path.isfile(os.path.join(self.path, 'configure.ac'))
613
614 def dependencies(self):
615 configure_ac = os.path.join(self.path, 'configure.ac')
616
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030617 contents = ''
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030618 # Prepend some special function overrides so we can parse out
619 # dependencies
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030620 for macro in DEPENDENCIES.keys():
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030621 contents += ('m4_define([' + macro + '], [' + macro + '_START$' +
622 str(DEPENDENCIES_OFFSET[macro] + 1) +
623 macro + '_END])\n')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030624 with open(configure_ac, "rt") as f:
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030625 contents += f.read()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030626
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030627 autoconf_cmdline = ['autoconf', '-Wno-undefined', '-']
628 autoconf_process = subprocess.Popen(autoconf_cmdline,
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030629 stdin=subprocess.PIPE,
630 stdout=subprocess.PIPE,
631 stderr=subprocess.PIPE)
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030632 document = contents.encode('utf-8')
633 (stdout, stderr) = autoconf_process.communicate(input=document)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030634 if not stdout:
635 print(stderr)
636 raise Exception("Failed to run autoconf for parsing dependencies")
637
638 # Parse out all of the dependency text
639 matches = []
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030640 for macro in DEPENDENCIES.keys():
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030641 pattern = '(' + macro + ')_START(.*?)' + macro + '_END'
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030642 for match in re.compile(pattern).finditer(stdout.decode('utf-8')):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030643 matches.append((match.group(1), match.group(2)))
644
645 # Look up dependencies from the text
646 found_deps = []
647 for macro, deptext in matches:
648 for potential_dep in deptext.split(' '):
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030649 for known_dep in DEPENDENCIES[macro].keys():
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030650 if potential_dep.startswith(known_dep):
651 found_deps.append(DEPENDENCIES[macro][known_dep])
652
653 return found_deps
654
655 def _configure_feature(self, flag, enabled):
656 """
657 Returns an configure flag as a string
658
659 Parameters:
660 flag The name of the flag
661 enabled Whether the flag is enabled or disabled
662 """
663 return '--' + ('enable' if enabled else 'disable') + '-' + flag
664
665 def configure(self, build_for_testing):
666 self.build_for_testing = build_for_testing
667 conf_flags = [
668 self._configure_feature('silent-rules', False),
669 self._configure_feature('examples', build_for_testing),
670 self._configure_feature('tests', build_for_testing),
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000671 self._configure_feature('itests', INTEGRATION_TEST),
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030672 ]
673 if not TEST_ONLY:
674 conf_flags.extend([
675 self._configure_feature('code-coverage', build_for_testing),
676 self._configure_feature('valgrind', build_for_testing),
677 ])
678 # Add any necessary configure flags for package
679 if CONFIGURE_FLAGS.get(self.package) is not None:
680 conf_flags.extend(CONFIGURE_FLAGS.get(self.package))
681 for bootstrap in ['bootstrap.sh', 'bootstrap', 'autogen.sh']:
682 if os.path.exists(bootstrap):
683 check_call_cmd('./' + bootstrap)
684 break
685 check_call_cmd('./configure', *conf_flags)
686
687 def build(self):
688 check_call_cmd(*make_parallel)
689
690 def install(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +1030691 check_call_cmd('sudo', '-n', '--', *(make_parallel + ['install']))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030692
693 def test(self):
694 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030695 cmd = make_parallel + ['check']
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030696 for i in range(0, args.repeat):
697 check_call_cmd(*cmd)
698 except CalledProcessError:
699 for root, _, files in os.walk(os.getcwd()):
700 if 'test-suite.log' not in files:
701 continue
702 check_call_cmd('cat', os.path.join(root, 'test-suite.log'))
703 raise Exception('Unit tests failed')
704
705 def analyze(self):
706 maybe_make_valgrind()
707 maybe_make_coverage()
708 run_cppcheck()
709
710
711class CMake(BuildSystem):
712 def __init__(self, package=None, path=None):
713 super(CMake, self).__init__(package, path)
714
715 def probe(self):
716 return os.path.isfile(os.path.join(self.path, 'CMakeLists.txt'))
717
718 def dependencies(self):
719 return []
720
721 def configure(self, build_for_testing):
722 self.build_for_testing = build_for_testing
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000723 if INTEGRATION_TEST:
724 check_call_cmd('cmake', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
725 '-DITESTS=ON', '.')
726 else:
727 check_call_cmd('cmake', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON', '.')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030728
729 def build(self):
730 check_call_cmd('cmake', '--build', '.', '--', '-j',
731 str(multiprocessing.cpu_count()))
732
733 def install(self):
734 pass
735
736 def test(self):
737 if make_target_exists('test'):
738 check_call_cmd('ctest', '.')
739
740 def analyze(self):
Andrew Geissler38356642020-03-19 15:12:44 -0500741 if TEST_ONLY:
742 return
743
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030744 if os.path.isfile('.clang-tidy'):
Ed Tanousbc5c1c42020-10-01 17:49:20 -0700745 try:
746 os.mkdir("tidy-build")
747 except FileExistsError as e:
748 pass
749 # clang-tidy needs to run on a clang-specific build
750 check_call_cmd('cmake', '-DCMAKE_C_COMPILER=clang',
751 '-DCMAKE_CXX_COMPILER=clang++',
752 '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
753 '-H.',
754 '-Btidy-build')
755 # we need to cd here because otherwise clang-tidy doesn't find the
756 # .clang-tidy file in the roots of repos. Its arguably a "bug"
757 # with run-clang-tidy at a minimum it's "weird" that it requires
758 # the .clang-tidy to be up a dir
Ed Tanous662890f2020-09-28 17:35:20 -0700759 os.chdir("tidy-build")
760 try:
Ed Tanousbc5c1c42020-10-01 17:49:20 -0700761 check_call_cmd('run-clang-tidy.py', "-header-filter=.*", '-p',
762 '.')
Ed Tanous662890f2020-09-28 17:35:20 -0700763 finally:
764 os.chdir("..")
765
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030766 maybe_make_valgrind()
767 maybe_make_coverage()
768 run_cppcheck()
769
770
771class Meson(BuildSystem):
772 def __init__(self, package=None, path=None):
773 super(Meson, self).__init__(package, path)
774
775 def probe(self):
776 return os.path.isfile(os.path.join(self.path, 'meson.build'))
777
778 def dependencies(self):
779 meson_build = os.path.join(self.path, 'meson.build')
780 if not os.path.exists(meson_build):
781 return []
782
783 found_deps = []
784 for root, dirs, files in os.walk(self.path):
785 if 'meson.build' not in files:
786 continue
787 with open(os.path.join(root, 'meson.build'), 'rt') as f:
788 build_contents = f.read()
Nan Zhouef13d532020-07-07 09:52:02 -0700789 pattern = r"dependency\('([^']*)'.*?\),?\n"
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030790 for match in re.finditer(pattern, build_contents):
791 group = match.group(1)
792 maybe_dep = DEPENDENCIES['PKG_CHECK_MODULES'].get(group)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030793 if maybe_dep is not None:
794 found_deps.append(maybe_dep)
795
796 return found_deps
797
798 def _parse_options(self, options_file):
799 """
800 Returns a set of options defined in the provides meson_options.txt file
801
802 Parameters:
803 options_file The file containing options
804 """
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700805 oi = optinterpreter.OptionInterpreter('')
806 oi.process(options_file)
807 return oi.options
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030808
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700809 def _configure_boolean(self, val):
810 """
811 Returns the meson flag which signifies the value
812
813 True is true which requires the boolean.
814 False is false which disables the boolean.
815
816 Parameters:
817 val The value being converted
818 """
819 if val is True:
820 return 'true'
821 elif val is False:
822 return 'false'
823 else:
824 raise Exception("Bad meson boolean value")
825
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030826 def _configure_feature(self, val):
827 """
828 Returns the meson flag which signifies the value
829
830 True is enabled which requires the feature.
831 False is disabled which disables the feature.
832 None is auto which autodetects the feature.
833
834 Parameters:
835 val The value being converted
836 """
837 if val is True:
838 return "enabled"
839 elif val is False:
840 return "disabled"
841 elif val is None:
842 return "auto"
843 else:
844 raise Exception("Bad meson feature value")
845
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700846 def _configure_option(self, opts, key, val):
847 """
848 Returns the meson flag which signifies the value
849 based on the type of the opt
850
851 Parameters:
852 opt The meson option which we are setting
853 val The value being converted
854 """
855 if isinstance(opts[key], coredata.UserBooleanOption):
856 str_val = self._configure_boolean(val)
857 elif isinstance(opts[key], coredata.UserFeatureOption):
858 str_val = self._configure_feature(val)
859 else:
860 raise Exception('Unknown meson option type')
861 return "-D{}={}".format(key, str_val)
862
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030863 def configure(self, build_for_testing):
864 self.build_for_testing = build_for_testing
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700865 meson_options = {}
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030866 if os.path.exists("meson_options.txt"):
867 meson_options = self._parse_options("meson_options.txt")
868 meson_flags = [
869 '-Db_colorout=never',
870 '-Dwerror=true',
871 '-Dwarning_level=3',
872 ]
873 if build_for_testing:
874 meson_flags.append('--buildtype=debug')
875 else:
876 meson_flags.append('--buildtype=debugoptimized')
Patrick Williamse95626d2021-04-13 11:13:53 -0500877 if OptionKey('tests') in meson_options:
878 meson_flags.append(self._configure_option(meson_options, OptionKey('tests'), build_for_testing))
879 if OptionKey('examples') in meson_options:
880 meson_flags.append(self._configure_option(meson_options, OptionKey('examples'), build_for_testing))
881 if OptionKey('itests') in meson_options:
882 meson_flags.append(self._configure_option(meson_options, OptionKey('itests'), INTEGRATION_TEST))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030883 if MESON_FLAGS.get(self.package) is not None:
884 meson_flags.extend(MESON_FLAGS.get(self.package))
885 try:
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030886 check_call_cmd('meson', 'setup', '--reconfigure', 'build',
887 *meson_flags)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030888 except:
889 shutil.rmtree('build')
890 check_call_cmd('meson', 'setup', 'build', *meson_flags)
891
892 def build(self):
893 check_call_cmd('ninja', '-C', 'build')
894
895 def install(self):
896 check_call_cmd('sudo', '-n', '--', 'ninja', '-C', 'build', 'install')
897
898 def test(self):
899 try:
Brad Bishop7b8cef22020-08-26 15:58:09 -0400900 test_args = ('--repeat', str(args.repeat), '-C', 'build')
901 check_call_cmd('meson', 'test', *test_args)
902
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030903 except CalledProcessError:
904 for root, _, files in os.walk(os.getcwd()):
905 if 'testlog.txt' not in files:
906 continue
907 check_call_cmd('cat', os.path.join(root, 'testlog.txt'))
908 raise Exception('Unit tests failed')
909
910 def _setup_exists(self, setup):
911 """
912 Returns whether the meson build supports the named test setup.
913
914 Parameter descriptions:
915 setup The setup target to check
916 """
917 try:
918 with open(os.devnull, 'w') as devnull:
919 output = subprocess.check_output(
920 ['meson', 'test', '-C', 'build',
921 '--setup', setup, '-t', '0'],
922 stderr=subprocess.STDOUT)
923 except CalledProcessError as e:
924 output = e.output
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030925 output = output.decode('utf-8')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030926 return not re.search('Test setup .* not found from project', output)
927
928 def _maybe_valgrind(self):
929 """
930 Potentially runs the unit tests through valgrind for the package
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030931 via `meson test`. The package can specify custom valgrind
932 configurations by utilizing add_test_setup() in a meson.build
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030933 """
934 if not is_valgrind_safe():
935 sys.stderr.write("###### Skipping valgrind ######\n")
936 return
937 try:
938 if self._setup_exists('valgrind'):
939 check_call_cmd('meson', 'test', '-C', 'build',
940 '--setup', 'valgrind')
941 else:
942 check_call_cmd('meson', 'test', '-C', 'build',
943 '--wrapper', 'valgrind')
944 except CalledProcessError:
945 for root, _, files in os.walk(os.getcwd()):
946 if 'testlog-valgrind.txt' not in files:
947 continue
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030948 cat_args = os.path.join(root, 'testlog-valgrind.txt')
949 check_call_cmd('cat', cat_args)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030950 raise Exception('Valgrind tests failed')
951
952 def analyze(self):
Andrew Geissler38356642020-03-19 15:12:44 -0500953 if TEST_ONLY:
954 return
955
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030956 self._maybe_valgrind()
957
958 # Run clang-tidy only if the project has a configuration
959 if os.path.isfile('.clang-tidy'):
Manojkiran Eda1aa91992020-10-02 14:11:53 +0530960 os.environ["CXX"] = "clang++"
961 check_call_cmd('meson', 'setup', 'build-clang')
Ed Tanousbc5c1c42020-10-01 17:49:20 -0700962 check_call_cmd('run-clang-tidy.py', '-p',
Manojkiran Eda1aa91992020-10-02 14:11:53 +0530963 'build-clang')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030964 # Run the basic clang static analyzer otherwise
965 else:
966 check_call_cmd('ninja', '-C', 'build',
967 'scan-build')
968
969 # Run tests through sanitizers
970 # b_lundef is needed if clang++ is CXX since it resolves the
971 # asan symbols at runtime only. We don't want to set it earlier
972 # in the build process to ensure we don't have undefined
973 # runtime code.
974 if is_sanitize_safe():
975 check_call_cmd('meson', 'configure', 'build',
976 '-Db_sanitize=address,undefined',
977 '-Db_lundef=false')
978 check_call_cmd('meson', 'test', '-C', 'build',
979 '--logbase', 'testlog-ubasan')
980 # TODO: Fix memory sanitizer
Andrew Jeffery6015ca12020-03-13 11:50:10 +1030981 # check_call_cmd('meson', 'configure', 'build',
982 # '-Db_sanitize=memory')
983 # check_call_cmd('meson', 'test', '-C', 'build'
984 # '--logbase', 'testlog-msan')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030985 check_call_cmd('meson', 'configure', 'build',
Lei YUf684ad62020-09-21 17:02:26 +0800986 '-Db_sanitize=none')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030987 else:
988 sys.stderr.write("###### Skipping sanitizers ######\n")
989
990 # Run coverage checks
991 check_call_cmd('meson', 'configure', 'build',
992 '-Db_coverage=true')
993 self.test()
994 # Only build coverage HTML if coverage files were produced
995 for root, dirs, files in os.walk('build'):
996 if any([f.endswith('.gcda') for f in files]):
997 check_call_cmd('ninja', '-C', 'build',
998 'coverage-html')
999 break
1000 check_call_cmd('meson', 'configure', 'build',
1001 '-Db_coverage=false')
1002 run_cppcheck()
1003
1004
1005class Package(object):
1006 def __init__(self, name=None, path=None):
Manojkiran Eda29d28252020-06-15 18:58:09 +05301007 self.supported = [Meson, Autotools, CMake]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301008 self.name = name
1009 self.path = path
1010 self.test_only = False
1011
1012 def build_systems(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +10301013 instances = (system(self.name, self.path) for system in self.supported)
1014 return (instance for instance in instances if instance.probe())
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301015
1016 def build_system(self, preferred=None):
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001017 systems = list(self.build_systems())
1018
1019 if not systems:
1020 return None
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301021
1022 if preferred:
Andrew Jeffery881041f2020-03-13 11:46:30 +10301023 return {type(system): system for system in systems}[preferred]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301024
1025 return next(iter(systems))
1026
1027 def install(self, system=None):
1028 if not system:
1029 system = self.build_system()
1030
1031 system.configure(False)
1032 system.build()
1033 system.install()
1034
Andrew Jeffery19d75672020-03-13 10:42:08 +10301035 def _test_one(self, system):
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301036 system.configure(True)
1037 system.build()
1038 system.install()
1039 system.test()
1040 system.analyze()
1041
Andrew Jeffery19d75672020-03-13 10:42:08 +10301042 def test(self):
1043 for system in self.build_systems():
1044 self._test_one(system)
1045
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301046
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001047def find_file(filename, basedir):
1048 """
Patrick Williams55448ad2020-12-14 14:28:28 -06001049 Finds all occurrences of a file (or list of files) in the base
1050 directory and passes them back with their relative paths.
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001051
1052 Parameter descriptions:
Patrick Williams55448ad2020-12-14 14:28:28 -06001053 filename The name of the file (or list of files) to
1054 find
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001055 basedir The base directory search in
1056 """
1057
Patrick Williams55448ad2020-12-14 14:28:28 -06001058 if not isinstance(filename, list):
1059 filename = [ filename ]
1060
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001061 filepaths = []
1062 for root, dirs, files in os.walk(basedir):
Patrick Williams55448ad2020-12-14 14:28:28 -06001063 for f in filename:
1064 if f in files:
1065 filepaths.append(os.path.join(root, f))
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001066 return filepaths
1067
Andrew Jeffery9cc97d82020-03-13 11:57:44 +10301068
Matthew Barthccb7f852016-11-23 17:43:02 -06001069if __name__ == '__main__':
1070 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
1071 CONFIGURE_FLAGS = {
Matthew Barth1d1c6732017-03-24 10:00:28 -05001072 'phosphor-logging':
Matt Spinler0744bb82020-01-16 08:23:35 -06001073 ['--enable-metadata-processing', '--enable-openpower-pel-extension',
Deepak Kodihalli3a4e1b42017-06-08 09:52:35 -05001074 'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml']
Matthew Barthccb7f852016-11-23 17:43:02 -06001075 }
1076
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001077 # MESON_FLAGS = [GIT REPO]:[MESON FLAGS]
1078 MESON_FLAGS = {
Matt Spinler8448cfd2020-09-10 17:01:48 -05001079 'phosphor-dbus-interfaces':
1080 ['-Ddata_com_ibm=true', '-Ddata_org_open_power=true']
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001081 }
1082
Matthew Barthccb7f852016-11-23 17:43:02 -06001083 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
1084 DEPENDENCIES = {
1085 'AC_CHECK_LIB': {'mapper': 'phosphor-objmgr'},
Matthew Barth710f3f02017-01-18 15:20:19 -06001086 'AC_CHECK_HEADER': {
1087 'host-ipmid': 'phosphor-host-ipmid',
Patrick Ventureb41a4462018-10-03 17:27:38 -07001088 'blobs-ipmid': 'phosphor-ipmi-blobs',
Matthew Barth710f3f02017-01-18 15:20:19 -06001089 'sdbusplus': 'sdbusplus',
William A. Kennington IIIb4f730a2018-09-12 11:21:20 -07001090 'sdeventplus': 'sdeventplus',
William A. Kennington III23705242019-01-15 18:17:25 -08001091 'stdplus': 'stdplus',
Patrick Venture22329962018-09-14 10:23:04 -07001092 'gpioplus': 'gpioplus',
Saqib Khan66145052017-02-14 12:02:07 -06001093 'phosphor-logging/log.hpp': 'phosphor-logging',
Patrick Williamseab8a372017-01-30 11:21:32 -06001094 },
Brad Bishopebb49112017-02-13 11:07:26 -05001095 'AC_PATH_PROG': {'sdbus++': 'sdbusplus'},
Patrick Williamseab8a372017-01-30 11:21:32 -06001096 'PKG_CHECK_MODULES': {
Matthew Barth19e261e2017-02-01 12:55:22 -06001097 'phosphor-dbus-interfaces': 'phosphor-dbus-interfaces',
William A. Kennington III993ab332019-02-07 02:12:31 -08001098 'libipmid': 'phosphor-host-ipmid',
1099 'libipmid-host': 'phosphor-host-ipmid',
Brad Bishopebb49112017-02-13 11:07:26 -05001100 'sdbusplus': 'sdbusplus',
William A. Kennington IIIb4f730a2018-09-12 11:21:20 -07001101 'sdeventplus': 'sdeventplus',
William A. Kennington III23705242019-01-15 18:17:25 -08001102 'stdplus': 'stdplus',
Patrick Venture22329962018-09-14 10:23:04 -07001103 'gpioplus': 'gpioplus',
Brad Bishopebb49112017-02-13 11:07:26 -05001104 'phosphor-logging': 'phosphor-logging',
Marri Devender Raoa3eee8a2018-08-13 05:34:27 -05001105 'phosphor-snmp': 'phosphor-snmp',
Patrick Venturee584c3b2019-03-07 09:44:16 -08001106 'ipmiblob': 'ipmi-blob-tool',
Ben Tyner300ed3c2020-05-14 15:30:37 -05001107 'hei': 'openpower-libhei',
Sui Chen40cf5212020-11-24 12:12:23 -08001108 'phosphor-ipmi-blobs': 'phosphor-ipmi-blobs',
Brad Bishopebb49112017-02-13 11:07:26 -05001109 },
Matthew Barthccb7f852016-11-23 17:43:02 -06001110 }
1111
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001112 # Offset into array of macro parameters MACRO(0, 1, ...N)
1113 DEPENDENCIES_OFFSET = {
1114 'AC_CHECK_LIB': 0,
1115 'AC_CHECK_HEADER': 0,
1116 'AC_PATH_PROG': 1,
1117 'PKG_CHECK_MODULES': 1,
1118 }
1119
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001120 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
1121 DEPENDENCIES_REGEX = {
Patrick Ventured37b8052018-10-16 16:03:03 -07001122 'phosphor-logging': r'\S+-dbus-interfaces$'
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001123 }
1124
Matthew Barth33df8792016-12-19 14:30:17 -06001125 # Set command line arguments
1126 parser = argparse.ArgumentParser()
1127 parser.add_argument("-w", "--workspace", dest="WORKSPACE", required=True,
1128 help="Workspace directory location(i.e. /home)")
1129 parser.add_argument("-p", "--package", dest="PACKAGE", required=True,
1130 help="OpenBMC package to be unit tested")
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001131 parser.add_argument("-t", "--test-only", dest="TEST_ONLY",
1132 action="store_true", required=False, default=False,
1133 help="Only run test cases, no other validation")
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001134 arg_inttests = parser.add_mutually_exclusive_group()
1135 arg_inttests.add_argument("--integration-tests", dest="INTEGRATION_TEST",
1136 action="store_true", required=False, default=True,
1137 help="Enable integration tests [default].")
1138 arg_inttests.add_argument("--no-integration-tests", dest="INTEGRATION_TEST",
1139 action="store_false", required=False,
1140 help="Disable integration tests.")
Matthew Barth33df8792016-12-19 14:30:17 -06001141 parser.add_argument("-v", "--verbose", action="store_true",
1142 help="Print additional package status messages")
Andrew Jeffery468309d2018-03-08 13:46:33 +10301143 parser.add_argument("-r", "--repeat", help="Repeat tests N times",
1144 type=int, default=1)
Andrew Geisslera61acb52019-01-03 16:32:44 -06001145 parser.add_argument("-b", "--branch", dest="BRANCH", required=False,
1146 help="Branch to target for dependent repositories",
1147 default="master")
Lei YU7ef93302019-11-06 13:53:21 +08001148 parser.add_argument("-n", "--noformat", dest="FORMAT",
1149 action="store_false", required=False,
1150 help="Whether or not to run format code")
Matthew Barth33df8792016-12-19 14:30:17 -06001151 args = parser.parse_args(sys.argv[1:])
1152 WORKSPACE = args.WORKSPACE
1153 UNIT_TEST_PKG = args.PACKAGE
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001154 TEST_ONLY = args.TEST_ONLY
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001155 INTEGRATION_TEST = args.INTEGRATION_TEST
Andrew Geisslera61acb52019-01-03 16:32:44 -06001156 BRANCH = args.BRANCH
Lei YU7ef93302019-11-06 13:53:21 +08001157 FORMAT_CODE = args.FORMAT
Matthew Barth33df8792016-12-19 14:30:17 -06001158 if args.verbose:
1159 def printline(*line):
1160 for arg in line:
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301161 print(arg, end=' ')
1162 print()
Matthew Barth33df8792016-12-19 14:30:17 -06001163 else:
Andrew Jefferyfce557f2020-03-13 12:12:19 +10301164 def printline(*line):
1165 pass
Matthew Barthccb7f852016-11-23 17:43:02 -06001166
Patrick Williamsb6535952020-12-15 06:40:10 -06001167 CODE_SCAN_DIR = os.path.join(WORKSPACE, UNIT_TEST_PKG)
Lei YU7ef93302019-11-06 13:53:21 +08001168
James Feist878df5c2018-07-26 14:54:28 -07001169 # First validate code formatting if repo has style formatting files.
Adriana Kobylakbcee22b2018-01-10 16:58:27 -06001170 # The format-code.sh checks for these files.
Lei YU7ef93302019-11-06 13:53:21 +08001171 if FORMAT_CODE:
Patrick Williams5b4a1e82020-12-15 06:46:31 -06001172 format_scripts = find_file(['format-code.sh', 'format-code'],
1173 CODE_SCAN_DIR)
Andrew Geissler31502dd2021-01-07 14:04:02 -06001174
1175 # use default format-code.sh if no other found
1176 if not format_scripts:
1177 format_scripts.append(os.path.join(WORKSPACE, "format-code.sh"))
1178
Patrick Williams5b4a1e82020-12-15 06:46:31 -06001179 for f in format_scripts:
1180 check_call_cmd(f, CODE_SCAN_DIR)
Andrew Geisslera28286d2018-01-10 11:00:00 -08001181
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001182 # Check if this repo has a supported make infrastructure
Patrick Williamsb6535952020-12-15 06:40:10 -06001183 pkg = Package(UNIT_TEST_PKG, CODE_SCAN_DIR)
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001184 if not pkg.build_system():
1185 print("No valid build system, exit")
1186 sys.exit(0)
1187
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301188 prev_umask = os.umask(000)
James Feist878df5c2018-07-26 14:54:28 -07001189
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301190 # Determine dependencies and add them
1191 dep_added = dict()
1192 dep_added[UNIT_TEST_PKG] = False
William A. Kennington III40d5c7c2018-12-13 14:37:59 -08001193
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301194 # Create dependency tree
1195 dep_tree = DepTree(UNIT_TEST_PKG)
Patrick Williamsb6535952020-12-15 06:40:10 -06001196 build_dep_tree(UNIT_TEST_PKG, CODE_SCAN_DIR, dep_added, dep_tree, BRANCH)
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001197
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301198 # Reorder Dependency Tree
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301199 for pkg_name, regex_str in DEPENDENCIES_REGEX.items():
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301200 dep_tree.ReorderDeps(pkg_name, regex_str)
1201 if args.verbose:
1202 dep_tree.PrintTree()
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001203
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301204 install_list = dep_tree.GetInstallList()
Andrew Geissler9ced4ed2019-11-18 14:33:58 -06001205
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301206 # We don't want to treat our package as a dependency
1207 install_list.remove(UNIT_TEST_PKG)
James Feist878df5c2018-07-26 14:54:28 -07001208
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301209 # Install reordered dependencies
1210 for dep in install_list:
1211 build_and_install(dep, False)
James Feist878df5c2018-07-26 14:54:28 -07001212
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301213 # Run package unit tests
1214 build_and_install(UNIT_TEST_PKG, True)
James Feist878df5c2018-07-26 14:54:28 -07001215
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301216 os.umask(prev_umask)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001217
1218 # Run any custom CI scripts the repo has, of which there can be
1219 # multiple of and anywhere in the repository.
Patrick Williamsb6535952020-12-15 06:40:10 -06001220 ci_scripts = find_file(['run-ci.sh', 'run-ci'], CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001221 if ci_scripts:
Patrick Williamsb6535952020-12-15 06:40:10 -06001222 os.chdir(CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001223 for ci_script in ci_scripts:
Patrick Williams55448ad2020-12-14 14:28:28 -06001224 check_call_cmd(ci_script)