blob: ca0868dc83fbb13429e9175bbb0a6602d8a34d3b [file] [log] [blame]
Matthew Barthccb7f852016-11-23 17:43:02 -06001#!/usr/bin/env python
2
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
Matthew Barthccb7f852016-11-23 17:43:02 -060011from urlparse import urljoin
Andrew Jefferya4e31c62018-03-08 13:45:28 +103012from subprocess import check_call, call, CalledProcessError
Matthew Barthccb7f852016-11-23 17:43:02 -060013import os
14import sys
Matthew Barth33df8792016-12-19 14:30:17 -060015import argparse
William A. Kennington IIIa2156732018-06-30 18:38:09 -070016import multiprocessing
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050017import re
William A. Kennington III9a32d5e2018-12-06 17:38:53 -080018import sets
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -080019import subprocess
William A. Kennington III3f1d1202018-12-06 18:02:07 -080020import shutil
William A. Kennington III4e1d0a12018-07-16 12:04:03 -070021import platform
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050022
23
24class DepTree():
25 """
26 Represents package dependency tree, where each node is a DepTree with a
27 name and DepTree children.
28 """
29
30 def __init__(self, name):
31 """
32 Create new DepTree.
33
34 Parameter descriptions:
35 name Name of new tree node.
36 """
37 self.name = name
38 self.children = list()
39
40 def AddChild(self, name):
41 """
42 Add new child node to current node.
43
44 Parameter descriptions:
45 name Name of new child
46 """
47 new_child = DepTree(name)
48 self.children.append(new_child)
49 return new_child
50
51 def AddChildNode(self, node):
52 """
53 Add existing child node to current node.
54
55 Parameter descriptions:
56 node Tree node to add
57 """
58 self.children.append(node)
59
60 def RemoveChild(self, name):
61 """
62 Remove child node.
63
64 Parameter descriptions:
65 name Name of child to remove
66 """
67 for child in self.children:
68 if child.name == name:
69 self.children.remove(child)
70 return
71
72 def GetNode(self, name):
73 """
74 Return node with matching name. Return None if not found.
75
76 Parameter descriptions:
77 name Name of node to return
78 """
79 if self.name == name:
80 return self
81 for child in self.children:
82 node = child.GetNode(name)
83 if node:
84 return node
85 return None
86
87 def GetParentNode(self, name, parent_node=None):
88 """
89 Return parent of node with matching name. Return none if not found.
90
91 Parameter descriptions:
92 name Name of node to get parent of
93 parent_node Parent of current node
94 """
95 if self.name == name:
96 return parent_node
97 for child in self.children:
98 found_node = child.GetParentNode(name, self)
99 if found_node:
100 return found_node
101 return None
102
103 def GetPath(self, name, path=None):
104 """
105 Return list of node names from head to matching name.
106 Return None if not found.
107
108 Parameter descriptions:
109 name Name of node
110 path List of node names from head to current node
111 """
112 if not path:
113 path = []
114 if self.name == name:
115 path.append(self.name)
116 return path
117 for child in self.children:
118 match = child.GetPath(name, path + [self.name])
119 if match:
120 return match
121 return None
122
123 def GetPathRegex(self, name, regex_str, path=None):
124 """
125 Return list of node paths that end in name, or match regex_str.
126 Return empty list if not found.
127
128 Parameter descriptions:
129 name Name of node to search for
130 regex_str Regex string to match node names
131 path Path of node names from head to current node
132 """
133 new_paths = []
134 if not path:
135 path = []
136 match = re.match(regex_str, self.name)
137 if (self.name == name) or (match):
138 new_paths.append(path + [self.name])
139 for child in self.children:
140 return_paths = None
141 full_path = path + [self.name]
142 return_paths = child.GetPathRegex(name, regex_str, full_path)
143 for i in return_paths:
144 new_paths.append(i)
145 return new_paths
146
147 def MoveNode(self, from_name, to_name):
148 """
149 Mode existing from_name node to become child of to_name node.
150
151 Parameter descriptions:
152 from_name Name of node to make a child of to_name
153 to_name Name of node to make parent of from_name
154 """
155 parent_from_node = self.GetParentNode(from_name)
156 from_node = self.GetNode(from_name)
157 parent_from_node.RemoveChild(from_name)
158 to_node = self.GetNode(to_name)
159 to_node.AddChildNode(from_node)
160
161 def ReorderDeps(self, name, regex_str):
162 """
163 Reorder dependency tree. If tree contains nodes with names that
164 match 'name' and 'regex_str', move 'regex_str' nodes that are
165 to the right of 'name' node, so that they become children of the
166 'name' node.
167
168 Parameter descriptions:
169 name Name of node to look for
170 regex_str Regex string to match names to
171 """
172 name_path = self.GetPath(name)
173 if not name_path:
174 return
175 paths = self.GetPathRegex(name, regex_str)
176 is_name_in_paths = False
177 name_index = 0
178 for i in range(len(paths)):
179 path = paths[i]
180 if path[-1] == name:
181 is_name_in_paths = True
182 name_index = i
183 break
184 if not is_name_in_paths:
185 return
186 for i in range(name_index + 1, len(paths)):
187 path = paths[i]
188 if name in path:
189 continue
190 from_name = path[-1]
191 self.MoveNode(from_name, name)
192
193 def GetInstallList(self):
194 """
195 Return post-order list of node names.
196
197 Parameter descriptions:
198 """
199 install_list = []
200 for child in self.children:
201 child_install_list = child.GetInstallList()
202 install_list.extend(child_install_list)
203 install_list.append(self.name)
204 return install_list
205
206 def PrintTree(self, level=0):
207 """
208 Print pre-order node names with indentation denoting node depth level.
209
210 Parameter descriptions:
211 level Current depth level
212 """
213 INDENT_PER_LEVEL = 4
214 print ' ' * (level * INDENT_PER_LEVEL) + self.name
215 for child in self.children:
216 child.PrintTree(level + 1)
Matthew Barth33df8792016-12-19 14:30:17 -0600217
218
William A. Kennington III1fddb972019-02-06 18:03:53 -0800219def check_call_cmd(*cmd):
Matthew Barth33df8792016-12-19 14:30:17 -0600220 """
221 Verbose prints the directory location the given command is called from and
222 the command, then executes the command using check_call.
223
224 Parameter descriptions:
225 dir Directory location command is to be called from
226 cmd List of parameters constructing the complete command
227 """
William A. Kennington III1fddb972019-02-06 18:03:53 -0800228 printline(os.getcwd(), ">", " ".join(cmd))
Matthew Barth33df8792016-12-19 14:30:17 -0600229 check_call(cmd)
Matthew Barthccb7f852016-11-23 17:43:02 -0600230
231
Andrew Geisslera61acb52019-01-03 16:32:44 -0600232def clone_pkg(pkg, branch):
Matthew Barth33df8792016-12-19 14:30:17 -0600233 """
234 Clone the given openbmc package's git repository from gerrit into
235 the WORKSPACE location
236
237 Parameter descriptions:
238 pkg Name of the package to clone
Andrew Geisslera61acb52019-01-03 16:32:44 -0600239 branch Branch to clone from pkg
Matthew Barth33df8792016-12-19 14:30:17 -0600240 """
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030241 pkg_dir = os.path.join(WORKSPACE, pkg)
242 if os.path.exists(os.path.join(pkg_dir, '.git')):
243 return pkg_dir
Matthew Barthccb7f852016-11-23 17:43:02 -0600244 pkg_repo = urljoin('https://gerrit.openbmc-project.xyz/openbmc/', pkg)
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030245 os.mkdir(pkg_dir)
Andrew Geisslera61acb52019-01-03 16:32:44 -0600246 printline(pkg_dir, "> git clone", pkg_repo, branch, "./")
247 try:
248 # first try the branch
249 repo_inst = Repo.clone_from(pkg_repo, pkg_dir,
250 branch=branch).working_dir
251 except:
252 printline("Input branch not found, default to master")
253 repo_inst = Repo.clone_from(pkg_repo, pkg_dir,
254 branch="master").working_dir
255 return repo_inst
Matthew Barth33df8792016-12-19 14:30:17 -0600256
257
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030258def make_target_exists(target):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800259 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030260 Runs a check against the makefile in the current directory to determine
261 if the target exists so that it can be built.
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800262
263 Parameter descriptions:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030264 target The make target we are checking
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800265 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030266 try:
267 cmd = [ 'make', '-n', target ]
268 with open(os.devnull, 'w') as devnull:
269 check_call(cmd, stdout=devnull, stderr=devnull)
270 return True
271 except CalledProcessError:
272 return False
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800273
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800274
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700275make_parallel = [
276 'make',
277 # Run enough jobs to saturate all the cpus
278 '-j', str(multiprocessing.cpu_count()),
279 # Don't start more jobs if the load avg is too high
280 '-l', str(multiprocessing.cpu_count()),
281 # Synchronize the output so logs aren't intermixed in stdout / stderr
282 '-O',
283]
284
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800285
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030286def build_and_install(name, build_for_testing=False):
William A. Kennington III780ec092018-12-06 14:46:50 -0800287 """
288 Builds and installs the package in the environment. Optionally
289 builds the examples and test cases for package.
290
291 Parameter description:
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030292 name The name of the package we are building
William A. Kennington IIIa0454912018-12-06 14:47:16 -0800293 build_for_testing Enable options related to testing on the package?
William A. Kennington III780ec092018-12-06 14:46:50 -0800294 """
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030295 os.chdir(os.path.join(WORKSPACE, name))
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800296
297 # Refresh dynamic linker run time bindings for dependencies
William A. Kennington III1fddb972019-02-06 18:03:53 -0800298 check_call_cmd('sudo', '-n', '--', 'ldconfig')
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800299
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030300 pkg = Package()
301 if build_for_testing:
302 pkg.test()
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800303 else:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030304 pkg.install()
305
William A. Kennington III780ec092018-12-06 14:46:50 -0800306
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030307def build_dep_tree(name, pkgdir, dep_added, head, branch, dep_tree=None):
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500308 """
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030309 For each package (name), starting with the package to be unit tested,
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030310 extract its dependencies. For each package dependency defined, recursively
311 apply the same strategy
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500312
313 Parameter descriptions:
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030314 name Name of the package
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500315 pkgdir Directory where package source is located
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800316 dep_added Current dict of dependencies and added status
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500317 head Head node of the dependency tree
Andrew Geisslera61acb52019-01-03 16:32:44 -0600318 branch Branch to clone from pkg
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500319 dep_tree Current dependency tree node
320 """
321 if not dep_tree:
322 dep_tree = head
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800323
William A. Kennington IIIbe6aab22018-12-06 15:01:54 -0800324 with open("/tmp/depcache", "r") as depcache:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800325 cache = depcache.readline()
326
327 # Read out pkg dependencies
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030328 pkg = Package(name, pkgdir)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800329
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030330 for dep in sets.Set(pkg.build_system().dependencies()):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800331 if dep in cache:
332 continue
333 # Dependency package not already known
334 if dep_added.get(dep) is None:
335 # Dependency package not added
336 new_child = dep_tree.AddChild(dep)
337 dep_added[dep] = False
Andrew Geisslera61acb52019-01-03 16:32:44 -0600338 dep_pkgdir = clone_pkg(dep,branch)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800339 # Determine this dependency package's
340 # dependencies and add them before
341 # returning to add this package
342 dep_added = build_dep_tree(dep,
343 dep_pkgdir,
344 dep_added,
345 head,
Andrew Geisslera61acb52019-01-03 16:32:44 -0600346 branch,
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800347 new_child)
348 else:
349 # Dependency package known and added
350 if dep_added[dep]:
Andrew Jeffery2cb0c7a2018-03-08 13:19:08 +1030351 continue
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500352 else:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800353 # Cyclic dependency failure
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030354 raise Exception("Cyclic dependencies found in "+name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500355
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030356 if not dep_added[name]:
357 dep_added[name] = True
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500358
359 return dep_added
Matthew Barthccb7f852016-11-23 17:43:02 -0600360
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700361
William A. Kennington III90b106a2019-02-06 18:08:24 -0800362def run_cppcheck():
Brad Bishop48424d42020-01-07 13:01:31 -0500363 match_re = re.compile('((?!\.mako\.).)*\.[ch](?:pp)?$', re.I)
364 cppcheck_files = []
365 stdout = subprocess.check_output(['git', 'ls-files'])
Patrick Venturead4354e2018-10-12 16:59:54 -0700366
Brad Bishop48424d42020-01-07 13:01:31 -0500367 for f in stdout.decode('utf-8').split():
368 if match_re.match(f):
369 cppcheck_files.append(f)
370
371 if not cppcheck_files:
372 # skip cppcheck if there arent' any c or cpp sources.
373 print("no files")
374 return None
375
376 # http://cppcheck.sourceforge.net/manual.pdf
377 params = ['cppcheck', '-j', str(multiprocessing.cpu_count()),
378 '--enable=all', '--file-list=-']
379
380 cppcheck_process = subprocess.Popen(
381 params,
382 stdout=subprocess.PIPE,
383 stderr=subprocess.PIPE,
384 stdin=subprocess.PIPE)
385 (stdout, stderr) = cppcheck_process.communicate(
386 input='\n'.join(cppcheck_files))
387
388 if cppcheck_process.wait():
Patrick Venturead4354e2018-10-12 16:59:54 -0700389 raise Exception('Cppcheck failed')
Brad Bishop48424d42020-01-07 13:01:31 -0500390 print(stdout)
391 print(stderr)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700392
William A. Kennington III37a89a22018-12-13 14:32:02 -0800393def is_valgrind_safe():
394 """
395 Returns whether it is safe to run valgrind on our platform
396 """
William A. Kennington III0326ded2019-02-07 00:33:28 -0800397 src = 'unit-test-vg.c'
398 exe = './unit-test-vg'
399 with open(src, 'w') as h:
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700400 h.write('#include <errno.h>\n')
401 h.write('#include <stdio.h>\n')
William A. Kennington III0326ded2019-02-07 00:33:28 -0800402 h.write('#include <stdlib.h>\n')
403 h.write('#include <string.h>\n')
404 h.write('int main() {\n')
405 h.write('char *heap_str = malloc(16);\n')
406 h.write('strcpy(heap_str, "RandString");\n')
407 h.write('int res = strcmp("RandString", heap_str);\n')
408 h.write('free(heap_str);\n')
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700409 h.write('char errstr[64];\n')
410 h.write('strerror_r(EINVAL, errstr, sizeof(errstr));\n')
411 h.write('printf("%s\\n", errstr);\n')
William A. Kennington III0326ded2019-02-07 00:33:28 -0800412 h.write('return res;\n')
413 h.write('}\n')
414 try:
415 with open(os.devnull, 'w') as devnull:
416 check_call(['gcc', '-O2', '-o', exe, src],
417 stdout=devnull, stderr=devnull)
418 check_call(['valgrind', '--error-exitcode=99', exe],
419 stdout=devnull, stderr=devnull)
420 return True
421 except:
422 sys.stderr.write("###### Platform is not valgrind safe ######\n")
423 return False
424 finally:
425 os.remove(src)
426 os.remove(exe)
William A. Kennington III37a89a22018-12-13 14:32:02 -0800427
William A. Kennington III282e3302019-02-04 16:55:05 -0800428def is_sanitize_safe():
429 """
430 Returns whether it is safe to run sanitizers on our platform
431 """
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800432 src = 'unit-test-sanitize.c'
433 exe = './unit-test-sanitize'
434 with open(src, 'w') as h:
435 h.write('int main() { return 0; }\n')
436 try:
437 with open(os.devnull, 'w') as devnull:
438 check_call(['gcc', '-O2', '-fsanitize=address',
439 '-fsanitize=undefined', '-o', exe, src],
440 stdout=devnull, stderr=devnull)
441 check_call([exe], stdout=devnull, stderr=devnull)
442 return True
443 except:
444 sys.stderr.write("###### Platform is not sanitize safe ######\n")
445 return False
446 finally:
447 os.remove(src)
448 os.remove(exe)
William A. Kennington III282e3302019-02-04 16:55:05 -0800449
William A. Kennington III49d4e592019-02-06 17:59:27 -0800450
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800451def maybe_make_valgrind():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700452 """
453 Potentially runs the unit tests through valgrind for the package
454 via `make check-valgrind`. If the package does not have valgrind testing
455 then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700456 """
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700457 # Valgrind testing is currently broken by an aggressive strcmp optimization
458 # that is inlined into optimized code for POWER by gcc 7+. Until we find
459 # a workaround, just don't run valgrind tests on POWER.
460 # https://github.com/openbmc/openbmc/issues/3315
William A. Kennington III37a89a22018-12-13 14:32:02 -0800461 if not is_valgrind_safe():
William A. Kennington III75130192019-02-07 00:34:14 -0800462 sys.stderr.write("###### Skipping valgrind ######\n")
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700463 return
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700464 if not make_target_exists('check-valgrind'):
465 return
466
467 try:
468 cmd = make_parallel + [ 'check-valgrind' ]
William A. Kennington III1fddb972019-02-06 18:03:53 -0800469 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700470 except CalledProcessError:
William A. Kennington III90b106a2019-02-06 18:08:24 -0800471 for root, _, files in os.walk(os.getcwd()):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700472 for f in files:
473 if re.search('test-suite-[a-z]+.log', f) is None:
474 continue
William A. Kennington III1fddb972019-02-06 18:03:53 -0800475 check_call_cmd('cat', os.path.join(root, f))
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700476 raise Exception('Valgrind tests failed')
477
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800478def maybe_make_coverage():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700479 """
480 Potentially runs the unit tests through code coverage for the package
481 via `make check-code-coverage`. If the package does not have code coverage
482 testing then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700483 """
484 if not make_target_exists('check-code-coverage'):
485 return
486
487 # Actually run code coverage
488 try:
489 cmd = make_parallel + [ 'check-code-coverage' ]
William A. Kennington III1fddb972019-02-06 18:03:53 -0800490 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700491 except CalledProcessError:
492 raise Exception('Code coverage failed')
Matthew Barthccb7f852016-11-23 17:43:02 -0600493
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030494
495class BuildSystem(object):
496 """
497 Build systems generally provide the means to configure, build, install and
498 test software. The BuildSystem class defines a set of interfaces on top of
499 which Autotools, Meson, CMake and possibly other build system drivers can
500 be implemented, separating out the phases to control whether a package
501 should merely be installed or also tested and analyzed.
502 """
503 def __init__(self, package, path):
504 """Initialise the driver with properties independent of the build system
505
506 Keyword arguments:
507 package: The name of the package. Derived from the path if None
508 path: The path to the package. Set to the working directory if None
509 """
510 self.path = "." if not path else path
511 self.package = package if package else os.path.basename(os.path.realpath(self.path))
512 self.build_for_testing=False
513
514 def probe(self):
515 """Test if the build system driver can be applied to the package
516
517 Return True if the driver can drive the package's build system,
518 otherwise False.
519
520 Generally probe() is implemented by testing for the presence of the
521 build system's configuration file(s).
522 """
523 raise NotImplemented
524
525 def dependencies(self):
526 """Provide the package's dependencies
527
528 Returns a list of dependencies. If no dependencies are required then an
529 empty list must be returned.
530
531 Generally dependencies() is implemented by analysing and extracting the
532 data from the build system configuration.
533 """
534 raise NotImplemented
535
536 def configure(self, build_for_testing):
537 """Configure the source ready for building
538
539 Should raise an exception if configuration failed.
540
541 Keyword arguments:
542 build_for_testing: Mark the package as being built for testing rather
543 than for installation as a dependency for the
544 package under test. Setting to True generally
545 implies that the package will be configured to build
546 with debug information, at a low level of
547 optimisation and possibly with sanitizers enabled.
548
549 Generally configure() is implemented by invoking the build system
550 tooling to generate Makefiles or equivalent.
551 """
552 raise NotImplemented
553
554 def build(self):
555 """Build the software ready for installation and/or testing
556
557 Should raise an exception if the build fails
558
559 Generally build() is implemented by invoking `make` or `ninja`.
560 """
561 raise NotImplemented
562
563 def install(self):
564 """Install the software ready for use
565
566 Should raise an exception if installation fails
567
568 Like build(), install() is generally implemented by invoking `make` or
569 `ninja`.
570 """
571 raise NotImplemented
572
573 def test(self):
574 """Build and run the test suite associated with the package
575
576 Should raise an exception if the build or testing fails.
577
578 Like install(), test() is generally implemented by invoking `make` or
579 `ninja`.
580 """
581 raise NotImplemented
582
583 def analyze(self):
584 """Run any supported analysis tools over the codebase
585
586 Should raise an exception if analysis fails.
587
588 Some analysis tools such as scan-build need injection into the build
589 system. analyze() provides the necessary hook to implement such
590 behaviour. Analyzers independent of the build system can also be
591 specified here but at the cost of possible duplication of code between
592 the build system driver implementations.
593 """
594 raise NotImplemented
595
596
597class Autotools(BuildSystem):
598 def __init__(self, package=None, path=None):
599 super(Autotools, self).__init__(package, path)
600
601 def probe(self):
602 return os.path.isfile(os.path.join(self.path, 'configure.ac'))
603
604 def dependencies(self):
605 configure_ac = os.path.join(self.path, 'configure.ac')
606
607 configure_ac_contents = ''
608 # Prepend some special function overrides so we can parse out dependencies
609 for macro in DEPENDENCIES.iterkeys():
610 configure_ac_contents += ('m4_define([' + macro + '], [' +
611 macro + '_START$' + str(DEPENDENCIES_OFFSET[macro] + 1) +
612 macro + '_END])\n')
613 with open(configure_ac, "rt") as f:
614 configure_ac_contents += f.read()
615
616 autoconf_process = subprocess.Popen(['autoconf', '-Wno-undefined', '-'],
617 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
618 stderr=subprocess.PIPE)
619 (stdout, stderr) = autoconf_process.communicate(input=configure_ac_contents)
620 if not stdout:
621 print(stderr)
622 raise Exception("Failed to run autoconf for parsing dependencies")
623
624 # Parse out all of the dependency text
625 matches = []
626 for macro in DEPENDENCIES.iterkeys():
627 pattern = '(' + macro + ')_START(.*?)' + macro + '_END'
628 for match in re.compile(pattern).finditer(stdout):
629 matches.append((match.group(1), match.group(2)))
630
631 # Look up dependencies from the text
632 found_deps = []
633 for macro, deptext in matches:
634 for potential_dep in deptext.split(' '):
635 for known_dep in DEPENDENCIES[macro].iterkeys():
636 if potential_dep.startswith(known_dep):
637 found_deps.append(DEPENDENCIES[macro][known_dep])
638
639 return found_deps
640
641 def _configure_feature(self, flag, enabled):
642 """
643 Returns an configure flag as a string
644
645 Parameters:
646 flag The name of the flag
647 enabled Whether the flag is enabled or disabled
648 """
649 return '--' + ('enable' if enabled else 'disable') + '-' + flag
650
651 def configure(self, build_for_testing):
652 self.build_for_testing = build_for_testing
653 conf_flags = [
654 self._configure_feature('silent-rules', False),
655 self._configure_feature('examples', build_for_testing),
656 self._configure_feature('tests', build_for_testing),
657 ]
658 if not TEST_ONLY:
659 conf_flags.extend([
660 self._configure_feature('code-coverage', build_for_testing),
661 self._configure_feature('valgrind', build_for_testing),
662 ])
663 # Add any necessary configure flags for package
664 if CONFIGURE_FLAGS.get(self.package) is not None:
665 conf_flags.extend(CONFIGURE_FLAGS.get(self.package))
666 for bootstrap in ['bootstrap.sh', 'bootstrap', 'autogen.sh']:
667 if os.path.exists(bootstrap):
668 check_call_cmd('./' + bootstrap)
669 break
670 check_call_cmd('./configure', *conf_flags)
671
672 def build(self):
673 check_call_cmd(*make_parallel)
674
675 def install(self):
676 check_call_cmd('sudo', '-n', '--', *(make_parallel + [ 'install' ]))
677
678 def test(self):
679 try:
680 cmd = make_parallel + [ 'check' ]
681 for i in range(0, args.repeat):
682 check_call_cmd(*cmd)
683 except CalledProcessError:
684 for root, _, files in os.walk(os.getcwd()):
685 if 'test-suite.log' not in files:
686 continue
687 check_call_cmd('cat', os.path.join(root, 'test-suite.log'))
688 raise Exception('Unit tests failed')
689
690 def analyze(self):
691 maybe_make_valgrind()
692 maybe_make_coverage()
693 run_cppcheck()
694
695
696class CMake(BuildSystem):
697 def __init__(self, package=None, path=None):
698 super(CMake, self).__init__(package, path)
699
700 def probe(self):
701 return os.path.isfile(os.path.join(self.path, 'CMakeLists.txt'))
702
703 def dependencies(self):
704 return []
705
706 def configure(self, build_for_testing):
707 self.build_for_testing = build_for_testing
708 check_call_cmd('cmake', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON', '.')
709
710 def build(self):
711 check_call_cmd('cmake', '--build', '.', '--', '-j',
712 str(multiprocessing.cpu_count()))
713
714 def install(self):
715 pass
716
717 def test(self):
718 if make_target_exists('test'):
719 check_call_cmd('ctest', '.')
720
721 def analyze(self):
722 if os.path.isfile('.clang-tidy'):
723 check_call_cmd('run-clang-tidy-8.py', '-p', '.')
724 maybe_make_valgrind()
725 maybe_make_coverage()
726 run_cppcheck()
727
728
729class Meson(BuildSystem):
730 def __init__(self, package=None, path=None):
731 super(Meson, self).__init__(package, path)
732
733 def probe(self):
734 return os.path.isfile(os.path.join(self.path, 'meson.build'))
735
736 def dependencies(self):
737 meson_build = os.path.join(self.path, 'meson.build')
738 if not os.path.exists(meson_build):
739 return []
740
741 found_deps = []
742 for root, dirs, files in os.walk(self.path):
743 if 'meson.build' not in files:
744 continue
745 with open(os.path.join(root, 'meson.build'), 'rt') as f:
746 build_contents = f.read()
747 for match in re.finditer(r"dependency\('([^']*)'.*?\)\n", build_contents):
748 maybe_dep = DEPENDENCIES['PKG_CHECK_MODULES'].get(match.group(1))
749 if maybe_dep is not None:
750 found_deps.append(maybe_dep)
751
752 return found_deps
753
754 def _parse_options(self, options_file):
755 """
756 Returns a set of options defined in the provides meson_options.txt file
757
758 Parameters:
759 options_file The file containing options
760 """
761 options_contents = ''
762 with open(options_file, "rt") as f:
763 options_contents += f.read()
764 options = sets.Set()
765 pattern = 'option\\(\\s*\'([^\']*)\''
766 for match in re.compile(pattern).finditer(options_contents):
767 options.add(match.group(1))
768 return options
769
770 def _configure_feature(self, val):
771 """
772 Returns the meson flag which signifies the value
773
774 True is enabled which requires the feature.
775 False is disabled which disables the feature.
776 None is auto which autodetects the feature.
777
778 Parameters:
779 val The value being converted
780 """
781 if val is True:
782 return "enabled"
783 elif val is False:
784 return "disabled"
785 elif val is None:
786 return "auto"
787 else:
788 raise Exception("Bad meson feature value")
789
790 def configure(self, build_for_testing):
791 self.build_for_testing = build_for_testing
792 meson_options = sets.Set()
793 if os.path.exists("meson_options.txt"):
794 meson_options = self._parse_options("meson_options.txt")
795 meson_flags = [
796 '-Db_colorout=never',
797 '-Dwerror=true',
798 '-Dwarning_level=3',
799 ]
800 if build_for_testing:
801 meson_flags.append('--buildtype=debug')
802 else:
803 meson_flags.append('--buildtype=debugoptimized')
804 if 'tests' in meson_options:
805 meson_flags.append('-Dtests=' + self._configure_feature(build_for_testing))
806 if 'examples' in meson_options:
807 meson_flags.append('-Dexamples=' + str(build_for_testing).lower())
808 if MESON_FLAGS.get(self.package) is not None:
809 meson_flags.extend(MESON_FLAGS.get(self.package))
810 try:
811 check_call_cmd('meson', 'setup', '--reconfigure', 'build', *meson_flags)
812 except:
813 shutil.rmtree('build')
814 check_call_cmd('meson', 'setup', 'build', *meson_flags)
815
816 def build(self):
817 check_call_cmd('ninja', '-C', 'build')
818
819 def install(self):
820 check_call_cmd('sudo', '-n', '--', 'ninja', '-C', 'build', 'install')
821
822 def test(self):
823 try:
824 check_call_cmd('meson', 'test', '-C', 'build')
825 except CalledProcessError:
826 for root, _, files in os.walk(os.getcwd()):
827 if 'testlog.txt' not in files:
828 continue
829 check_call_cmd('cat', os.path.join(root, 'testlog.txt'))
830 raise Exception('Unit tests failed')
831
832 def _setup_exists(self, setup):
833 """
834 Returns whether the meson build supports the named test setup.
835
836 Parameter descriptions:
837 setup The setup target to check
838 """
839 try:
840 with open(os.devnull, 'w') as devnull:
841 output = subprocess.check_output(
842 ['meson', 'test', '-C', 'build',
843 '--setup', setup, '-t', '0'],
844 stderr=subprocess.STDOUT)
845 except CalledProcessError as e:
846 output = e.output
847 return not re.search('Test setup .* not found from project', output)
848
849 def _maybe_valgrind(self):
850 """
851 Potentially runs the unit tests through valgrind for the package
852 via `meson test`. The package can specify custom valgrind configurations
853 by utilizing add_test_setup() in a meson.build
854 """
855 if not is_valgrind_safe():
856 sys.stderr.write("###### Skipping valgrind ######\n")
857 return
858 try:
859 if self._setup_exists('valgrind'):
860 check_call_cmd('meson', 'test', '-C', 'build',
861 '--setup', 'valgrind')
862 else:
863 check_call_cmd('meson', 'test', '-C', 'build',
864 '--wrapper', 'valgrind')
865 except CalledProcessError:
866 for root, _, files in os.walk(os.getcwd()):
867 if 'testlog-valgrind.txt' not in files:
868 continue
869 check_call_cmd('cat', os.path.join(root, 'testlog-valgrind.txt'))
870 raise Exception('Valgrind tests failed')
871
872 def analyze(self):
873 self._maybe_valgrind()
874
875 # Run clang-tidy only if the project has a configuration
876 if os.path.isfile('.clang-tidy'):
877 check_call_cmd('run-clang-tidy-8.py', '-p',
878 'build')
879 # Run the basic clang static analyzer otherwise
880 else:
881 check_call_cmd('ninja', '-C', 'build',
882 'scan-build')
883
884 # Run tests through sanitizers
885 # b_lundef is needed if clang++ is CXX since it resolves the
886 # asan symbols at runtime only. We don't want to set it earlier
887 # in the build process to ensure we don't have undefined
888 # runtime code.
889 if is_sanitize_safe():
890 check_call_cmd('meson', 'configure', 'build',
891 '-Db_sanitize=address,undefined',
892 '-Db_lundef=false')
893 check_call_cmd('meson', 'test', '-C', 'build',
894 '--logbase', 'testlog-ubasan')
895 # TODO: Fix memory sanitizer
896 #check_call_cmd('meson', 'configure', 'build',
897 # '-Db_sanitize=memory')
898 #check_call_cmd('meson', 'test', '-C', 'build'
899 # '--logbase', 'testlog-msan')
900 check_call_cmd('meson', 'configure', 'build',
901 '-Db_sanitize=none', '-Db_lundef=true')
902 else:
903 sys.stderr.write("###### Skipping sanitizers ######\n")
904
905 # Run coverage checks
906 check_call_cmd('meson', 'configure', 'build',
907 '-Db_coverage=true')
908 self.test()
909 # Only build coverage HTML if coverage files were produced
910 for root, dirs, files in os.walk('build'):
911 if any([f.endswith('.gcda') for f in files]):
912 check_call_cmd('ninja', '-C', 'build',
913 'coverage-html')
914 break
915 check_call_cmd('meson', 'configure', 'build',
916 '-Db_coverage=false')
917 run_cppcheck()
918
919
920class Package(object):
921 def __init__(self, name=None, path=None):
922 self.supported = [ Autotools, Meson, CMake ]
923 self.name = name
924 self.path = path
925 self.test_only = False
926
927 def build_systems(self):
928 instances = ( system(self.name, self.path) for system in self.supported )
929 return ( instance for instance in instances if instance.probe() )
930
931 def build_system(self, preferred=None):
932 systems = self.build_systems()
933
934 if preferred:
935 return { type(system): system for system in systems }[preferred]
936
937 return next(iter(systems))
938
939 def install(self, system=None):
940 if not system:
941 system = self.build_system()
942
943 system.configure(False)
944 system.build()
945 system.install()
946
Andrew Jeffery19d75672020-03-13 10:42:08 +1030947 def _test_one(self, system):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030948 system.configure(True)
949 system.build()
950 system.install()
951 system.test()
952 system.analyze()
953
Andrew Jeffery19d75672020-03-13 10:42:08 +1030954 def test(self):
955 for system in self.build_systems():
956 self._test_one(system)
957
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030958
Matt Spinler9bfaaad2019-10-25 09:51:50 -0500959def find_file(filename, basedir):
960 """
961 Finds all occurrences of a file in the base directory
962 and passes them back with their relative paths.
963
964 Parameter descriptions:
965 filename The name of the file to find
966 basedir The base directory search in
967 """
968
969 filepaths = []
970 for root, dirs, files in os.walk(basedir):
971 if filename in files:
972 filepaths.append(os.path.join(root, filename))
973 return filepaths
974
Matthew Barthccb7f852016-11-23 17:43:02 -0600975if __name__ == '__main__':
976 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
977 CONFIGURE_FLAGS = {
Matthew Barth1d1c6732017-03-24 10:00:28 -0500978 'sdbusplus': ['--enable-transaction'],
979 'phosphor-logging':
Matt Spinler0744bb82020-01-16 08:23:35 -0600980 ['--enable-metadata-processing', '--enable-openpower-pel-extension',
Deepak Kodihalli3a4e1b42017-06-08 09:52:35 -0500981 'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml']
Matthew Barthccb7f852016-11-23 17:43:02 -0600982 }
983
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800984 # MESON_FLAGS = [GIT REPO]:[MESON FLAGS]
985 MESON_FLAGS = {
986 }
987
Matthew Barthccb7f852016-11-23 17:43:02 -0600988 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
989 DEPENDENCIES = {
990 'AC_CHECK_LIB': {'mapper': 'phosphor-objmgr'},
Matthew Barth710f3f02017-01-18 15:20:19 -0600991 'AC_CHECK_HEADER': {
992 'host-ipmid': 'phosphor-host-ipmid',
Patrick Ventureb41a4462018-10-03 17:27:38 -0700993 'blobs-ipmid': 'phosphor-ipmi-blobs',
Matthew Barth710f3f02017-01-18 15:20:19 -0600994 'sdbusplus': 'sdbusplus',
William A. Kennington IIIb4f730a2018-09-12 11:21:20 -0700995 'sdeventplus': 'sdeventplus',
William A. Kennington III23705242019-01-15 18:17:25 -0800996 'stdplus': 'stdplus',
Patrick Venture22329962018-09-14 10:23:04 -0700997 'gpioplus': 'gpioplus',
Saqib Khan66145052017-02-14 12:02:07 -0600998 'phosphor-logging/log.hpp': 'phosphor-logging',
Patrick Williamseab8a372017-01-30 11:21:32 -0600999 },
Brad Bishopebb49112017-02-13 11:07:26 -05001000 'AC_PATH_PROG': {'sdbus++': 'sdbusplus'},
Patrick Williamseab8a372017-01-30 11:21:32 -06001001 'PKG_CHECK_MODULES': {
Matthew Barth19e261e2017-02-01 12:55:22 -06001002 'phosphor-dbus-interfaces': 'phosphor-dbus-interfaces',
Patrick Williamsf128b402017-03-29 06:45:59 -05001003 'openpower-dbus-interfaces': 'openpower-dbus-interfaces',
Matt Spinler7be19032018-04-13 09:43:14 -05001004 'ibm-dbus-interfaces': 'ibm-dbus-interfaces',
William A. Kennington III993ab332019-02-07 02:12:31 -08001005 'libipmid': 'phosphor-host-ipmid',
1006 'libipmid-host': 'phosphor-host-ipmid',
Brad Bishopebb49112017-02-13 11:07:26 -05001007 'sdbusplus': 'sdbusplus',
William A. Kennington IIIb4f730a2018-09-12 11:21:20 -07001008 'sdeventplus': 'sdeventplus',
William A. Kennington III23705242019-01-15 18:17:25 -08001009 'stdplus': 'stdplus',
Patrick Venture22329962018-09-14 10:23:04 -07001010 'gpioplus': 'gpioplus',
Brad Bishopebb49112017-02-13 11:07:26 -05001011 'phosphor-logging': 'phosphor-logging',
Marri Devender Raoa3eee8a2018-08-13 05:34:27 -05001012 'phosphor-snmp': 'phosphor-snmp',
Patrick Venturee584c3b2019-03-07 09:44:16 -08001013 'ipmiblob': 'ipmi-blob-tool',
Brad Bishopebb49112017-02-13 11:07:26 -05001014 },
Matthew Barthccb7f852016-11-23 17:43:02 -06001015 }
1016
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001017 # Offset into array of macro parameters MACRO(0, 1, ...N)
1018 DEPENDENCIES_OFFSET = {
1019 'AC_CHECK_LIB': 0,
1020 'AC_CHECK_HEADER': 0,
1021 'AC_PATH_PROG': 1,
1022 'PKG_CHECK_MODULES': 1,
1023 }
1024
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001025 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
1026 DEPENDENCIES_REGEX = {
Patrick Ventured37b8052018-10-16 16:03:03 -07001027 'phosphor-logging': r'\S+-dbus-interfaces$'
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001028 }
1029
Matthew Barth33df8792016-12-19 14:30:17 -06001030 # Set command line arguments
1031 parser = argparse.ArgumentParser()
1032 parser.add_argument("-w", "--workspace", dest="WORKSPACE", required=True,
1033 help="Workspace directory location(i.e. /home)")
1034 parser.add_argument("-p", "--package", dest="PACKAGE", required=True,
1035 help="OpenBMC package to be unit tested")
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001036 parser.add_argument("-t", "--test-only", dest="TEST_ONLY",
1037 action="store_true", required=False, default=False,
1038 help="Only run test cases, no other validation")
Matthew Barth33df8792016-12-19 14:30:17 -06001039 parser.add_argument("-v", "--verbose", action="store_true",
1040 help="Print additional package status messages")
Andrew Jeffery468309d2018-03-08 13:46:33 +10301041 parser.add_argument("-r", "--repeat", help="Repeat tests N times",
1042 type=int, default=1)
Andrew Geisslera61acb52019-01-03 16:32:44 -06001043 parser.add_argument("-b", "--branch", dest="BRANCH", required=False,
1044 help="Branch to target for dependent repositories",
1045 default="master")
Lei YU7ef93302019-11-06 13:53:21 +08001046 parser.add_argument("-n", "--noformat", dest="FORMAT",
1047 action="store_false", required=False,
1048 help="Whether or not to run format code")
Matthew Barth33df8792016-12-19 14:30:17 -06001049 args = parser.parse_args(sys.argv[1:])
1050 WORKSPACE = args.WORKSPACE
1051 UNIT_TEST_PKG = args.PACKAGE
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001052 TEST_ONLY = args.TEST_ONLY
Andrew Geisslera61acb52019-01-03 16:32:44 -06001053 BRANCH = args.BRANCH
Lei YU7ef93302019-11-06 13:53:21 +08001054 FORMAT_CODE = args.FORMAT
Matthew Barth33df8792016-12-19 14:30:17 -06001055 if args.verbose:
1056 def printline(*line):
1057 for arg in line:
1058 print arg,
1059 print
1060 else:
1061 printline = lambda *l: None
Matthew Barthccb7f852016-11-23 17:43:02 -06001062
Lei YU7ef93302019-11-06 13:53:21 +08001063 CODE_SCAN_DIR = WORKSPACE + "/" + UNIT_TEST_PKG
1064
James Feist878df5c2018-07-26 14:54:28 -07001065 # First validate code formatting if repo has style formatting files.
Adriana Kobylakbcee22b2018-01-10 16:58:27 -06001066 # The format-code.sh checks for these files.
Lei YU7ef93302019-11-06 13:53:21 +08001067 if FORMAT_CODE:
1068 check_call_cmd("./format-code.sh", CODE_SCAN_DIR)
Andrew Geisslera28286d2018-01-10 11:00:00 -08001069
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301070 prev_umask = os.umask(000)
James Feist878df5c2018-07-26 14:54:28 -07001071
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301072 # Determine dependencies and add them
1073 dep_added = dict()
1074 dep_added[UNIT_TEST_PKG] = False
William A. Kennington III40d5c7c2018-12-13 14:37:59 -08001075
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301076 # Create dependency tree
1077 dep_tree = DepTree(UNIT_TEST_PKG)
1078 build_dep_tree(UNIT_TEST_PKG,
1079 os.path.join(WORKSPACE, UNIT_TEST_PKG),
1080 dep_added,
1081 dep_tree,
1082 BRANCH)
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001083
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301084 # Reorder Dependency Tree
1085 for pkg_name, regex_str in DEPENDENCIES_REGEX.iteritems():
1086 dep_tree.ReorderDeps(pkg_name, regex_str)
1087 if args.verbose:
1088 dep_tree.PrintTree()
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001089
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301090 install_list = dep_tree.GetInstallList()
Andrew Geissler9ced4ed2019-11-18 14:33:58 -06001091
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301092 # We don't want to treat our package as a dependency
1093 install_list.remove(UNIT_TEST_PKG)
James Feist878df5c2018-07-26 14:54:28 -07001094
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301095 # Install reordered dependencies
1096 for dep in install_list:
1097 build_and_install(dep, False)
James Feist878df5c2018-07-26 14:54:28 -07001098
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301099 # Run package unit tests
1100 build_and_install(UNIT_TEST_PKG, True)
James Feist878df5c2018-07-26 14:54:28 -07001101
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301102 os.umask(prev_umask)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001103
1104 # Run any custom CI scripts the repo has, of which there can be
1105 # multiple of and anywhere in the repository.
1106 ci_scripts = find_file('run-ci.sh', os.path.join(WORKSPACE, UNIT_TEST_PKG))
1107 if ci_scripts:
1108 os.chdir(os.path.join(WORKSPACE, UNIT_TEST_PKG))
1109 for ci_script in ci_scripts:
1110 check_call_cmd('sh', ci_script)