blob: 664e7ec41351df539543f7817f83c39b9c175a40 [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
Patrick Williamse08ffba2022-12-05 10:33:46 -060010import argparse
11import multiprocessing
12import os
13import platform
14import re
15import shutil
16import subprocess
17import sys
18from subprocess import CalledProcessError, check_call
19from tempfile import TemporaryDirectory
20from urllib.parse import urljoin
21
Matthew Barthd1810372016-12-19 16:57:21 -060022from git import Repo
Patrick Williamse795dfe2022-12-06 10:07:02 -060023
William A. Kennington III3992d102021-05-17 01:41:04 -070024# interpreter is not used directly but this resolves dependency ordering
25# that would be broken if we didn't include it.
Patrick Williamse08ffba2022-12-05 10:33:46 -060026from mesonbuild import interpreter # noqa: F401
William A. Kennington IIIfcd70772020-06-04 00:50:23 -070027from mesonbuild import coredata, optinterpreter
Patrick Williamse95626d2021-04-13 11:13:53 -050028from mesonbuild.mesonlib import OptionKey
Patrick Williams95095f12021-04-14 14:42:35 -050029from mesonbuild.mesonlib import version_compare as meson_version_compare
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050030
31
Patrick Williamse08ffba2022-12-05 10:33:46 -060032class DepTree:
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050033 """
34 Represents package dependency tree, where each node is a DepTree with a
35 name and DepTree children.
36 """
37
38 def __init__(self, name):
39 """
40 Create new DepTree.
41
42 Parameter descriptions:
43 name Name of new tree node.
44 """
45 self.name = name
46 self.children = list()
47
48 def AddChild(self, name):
49 """
50 Add new child node to current node.
51
52 Parameter descriptions:
53 name Name of new child
54 """
55 new_child = DepTree(name)
56 self.children.append(new_child)
57 return new_child
58
59 def AddChildNode(self, node):
60 """
61 Add existing child node to current node.
62
63 Parameter descriptions:
64 node Tree node to add
65 """
66 self.children.append(node)
67
68 def RemoveChild(self, name):
69 """
70 Remove child node.
71
72 Parameter descriptions:
73 name Name of child to remove
74 """
75 for child in self.children:
76 if child.name == name:
77 self.children.remove(child)
78 return
79
80 def GetNode(self, name):
81 """
82 Return node with matching name. Return None if not found.
83
84 Parameter descriptions:
85 name Name of node to return
86 """
87 if self.name == name:
88 return self
89 for child in self.children:
90 node = child.GetNode(name)
91 if node:
92 return node
93 return None
94
95 def GetParentNode(self, name, parent_node=None):
96 """
97 Return parent of node with matching name. Return none if not found.
98
99 Parameter descriptions:
100 name Name of node to get parent of
101 parent_node Parent of current node
102 """
103 if self.name == name:
104 return parent_node
105 for child in self.children:
106 found_node = child.GetParentNode(name, self)
107 if found_node:
108 return found_node
109 return None
110
111 def GetPath(self, name, path=None):
112 """
113 Return list of node names from head to matching name.
114 Return None if not found.
115
116 Parameter descriptions:
117 name Name of node
118 path List of node names from head to current node
119 """
120 if not path:
121 path = []
122 if self.name == name:
123 path.append(self.name)
124 return path
125 for child in self.children:
126 match = child.GetPath(name, path + [self.name])
127 if match:
128 return match
129 return None
130
131 def GetPathRegex(self, name, regex_str, path=None):
132 """
133 Return list of node paths that end in name, or match regex_str.
134 Return empty list if not found.
135
136 Parameter descriptions:
137 name Name of node to search for
138 regex_str Regex string to match node names
139 path Path of node names from head to current node
140 """
141 new_paths = []
142 if not path:
143 path = []
144 match = re.match(regex_str, self.name)
145 if (self.name == name) or (match):
146 new_paths.append(path + [self.name])
147 for child in self.children:
148 return_paths = None
149 full_path = path + [self.name]
150 return_paths = child.GetPathRegex(name, regex_str, full_path)
151 for i in return_paths:
152 new_paths.append(i)
153 return new_paths
154
155 def MoveNode(self, from_name, to_name):
156 """
157 Mode existing from_name node to become child of to_name node.
158
159 Parameter descriptions:
160 from_name Name of node to make a child of to_name
161 to_name Name of node to make parent of from_name
162 """
163 parent_from_node = self.GetParentNode(from_name)
164 from_node = self.GetNode(from_name)
165 parent_from_node.RemoveChild(from_name)
166 to_node = self.GetNode(to_name)
167 to_node.AddChildNode(from_node)
168
169 def ReorderDeps(self, name, regex_str):
170 """
171 Reorder dependency tree. If tree contains nodes with names that
172 match 'name' and 'regex_str', move 'regex_str' nodes that are
173 to the right of 'name' node, so that they become children of the
174 'name' node.
175
176 Parameter descriptions:
177 name Name of node to look for
178 regex_str Regex string to match names to
179 """
180 name_path = self.GetPath(name)
181 if not name_path:
182 return
183 paths = self.GetPathRegex(name, regex_str)
184 is_name_in_paths = False
185 name_index = 0
186 for i in range(len(paths)):
187 path = paths[i]
188 if path[-1] == name:
189 is_name_in_paths = True
190 name_index = i
191 break
192 if not is_name_in_paths:
193 return
194 for i in range(name_index + 1, len(paths)):
195 path = paths[i]
196 if name in path:
197 continue
198 from_name = path[-1]
199 self.MoveNode(from_name, name)
200
201 def GetInstallList(self):
202 """
203 Return post-order list of node names.
204
205 Parameter descriptions:
206 """
207 install_list = []
208 for child in self.children:
209 child_install_list = child.GetInstallList()
210 install_list.extend(child_install_list)
211 install_list.append(self.name)
212 return install_list
213
214 def PrintTree(self, level=0):
215 """
216 Print pre-order node names with indentation denoting node depth level.
217
218 Parameter descriptions:
219 level Current depth level
220 """
221 INDENT_PER_LEVEL = 4
Patrick Williamse08ffba2022-12-05 10:33:46 -0600222 print(" " * (level * INDENT_PER_LEVEL) + self.name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500223 for child in self.children:
224 child.PrintTree(level + 1)
Matthew Barth33df8792016-12-19 14:30:17 -0600225
226
William A. Kennington III1fddb972019-02-06 18:03:53 -0800227def check_call_cmd(*cmd):
Matthew Barth33df8792016-12-19 14:30:17 -0600228 """
229 Verbose prints the directory location the given command is called from and
230 the command, then executes the command using check_call.
231
232 Parameter descriptions:
233 dir Directory location command is to be called from
234 cmd List of parameters constructing the complete command
235 """
William A. Kennington III1fddb972019-02-06 18:03:53 -0800236 printline(os.getcwd(), ">", " ".join(cmd))
Matthew Barth33df8792016-12-19 14:30:17 -0600237 check_call(cmd)
Matthew Barthccb7f852016-11-23 17:43:02 -0600238
239
Andrew Geisslera61acb52019-01-03 16:32:44 -0600240def clone_pkg(pkg, branch):
Matthew Barth33df8792016-12-19 14:30:17 -0600241 """
242 Clone the given openbmc package's git repository from gerrit into
243 the WORKSPACE location
244
245 Parameter descriptions:
246 pkg Name of the package to clone
Andrew Geisslera61acb52019-01-03 16:32:44 -0600247 branch Branch to clone from pkg
Matthew Barth33df8792016-12-19 14:30:17 -0600248 """
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030249 pkg_dir = os.path.join(WORKSPACE, pkg)
Patrick Williamse08ffba2022-12-05 10:33:46 -0600250 if os.path.exists(os.path.join(pkg_dir, ".git")):
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030251 return pkg_dir
Patrick Williamse08ffba2022-12-05 10:33:46 -0600252 pkg_repo = urljoin("https://gerrit.openbmc.org/openbmc/", pkg)
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030253 os.mkdir(pkg_dir)
Andrew Geisslera61acb52019-01-03 16:32:44 -0600254 printline(pkg_dir, "> git clone", pkg_repo, branch, "./")
255 try:
256 # first try the branch
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030257 clone = Repo.clone_from(pkg_repo, pkg_dir, branch=branch)
258 repo_inst = clone.working_dir
Patrick Williamse08ffba2022-12-05 10:33:46 -0600259 except Exception:
Andrew Geisslera61acb52019-01-03 16:32:44 -0600260 printline("Input branch not found, default to master")
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030261 clone = Repo.clone_from(pkg_repo, pkg_dir, branch="master")
262 repo_inst = clone.working_dir
Andrew Geisslera61acb52019-01-03 16:32:44 -0600263 return repo_inst
Matthew Barth33df8792016-12-19 14:30:17 -0600264
265
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030266def make_target_exists(target):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800267 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030268 Runs a check against the makefile in the current directory to determine
269 if the target exists so that it can be built.
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800270
271 Parameter descriptions:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030272 target The make target we are checking
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800273 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030274 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600275 cmd = ["make", "-n", target]
276 with open(os.devnull, "w") as devnull:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030277 check_call(cmd, stdout=devnull, stderr=devnull)
278 return True
279 except CalledProcessError:
280 return False
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800281
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800282
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700283make_parallel = [
Patrick Williamse08ffba2022-12-05 10:33:46 -0600284 "make",
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700285 # Run enough jobs to saturate all the cpus
Patrick Williamse08ffba2022-12-05 10:33:46 -0600286 "-j",
287 str(multiprocessing.cpu_count()),
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700288 # Don't start more jobs if the load avg is too high
Patrick Williamse08ffba2022-12-05 10:33:46 -0600289 "-l",
290 str(multiprocessing.cpu_count()),
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700291 # Synchronize the output so logs aren't intermixed in stdout / stderr
Patrick Williamse08ffba2022-12-05 10:33:46 -0600292 "-O",
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700293]
294
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800295
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030296def build_and_install(name, build_for_testing=False):
William A. Kennington III780ec092018-12-06 14:46:50 -0800297 """
298 Builds and installs the package in the environment. Optionally
299 builds the examples and test cases for package.
300
301 Parameter description:
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030302 name The name of the package we are building
William A. Kennington IIIa0454912018-12-06 14:47:16 -0800303 build_for_testing Enable options related to testing on the package?
William A. Kennington III780ec092018-12-06 14:46:50 -0800304 """
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030305 os.chdir(os.path.join(WORKSPACE, name))
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800306
307 # Refresh dynamic linker run time bindings for dependencies
Patrick Williamse08ffba2022-12-05 10:33:46 -0600308 check_call_cmd("sudo", "-n", "--", "ldconfig")
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800309
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030310 pkg = Package()
311 if build_for_testing:
312 pkg.test()
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800313 else:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030314 pkg.install()
315
William A. Kennington III780ec092018-12-06 14:46:50 -0800316
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030317def build_dep_tree(name, pkgdir, dep_added, head, branch, dep_tree=None):
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500318 """
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030319 For each package (name), starting with the package to be unit tested,
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030320 extract its dependencies. For each package dependency defined, recursively
321 apply the same strategy
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500322
323 Parameter descriptions:
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030324 name Name of the package
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500325 pkgdir Directory where package source is located
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800326 dep_added Current dict of dependencies and added status
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500327 head Head node of the dependency tree
Andrew Geisslera61acb52019-01-03 16:32:44 -0600328 branch Branch to clone from pkg
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500329 dep_tree Current dependency tree node
330 """
331 if not dep_tree:
332 dep_tree = head
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800333
William A. Kennington IIIbe6aab22018-12-06 15:01:54 -0800334 with open("/tmp/depcache", "r") as depcache:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800335 cache = depcache.readline()
336
337 # Read out pkg dependencies
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030338 pkg = Package(name, pkgdir)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800339
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600340 build = pkg.build_system()
Patrick Williamse08ffba2022-12-05 10:33:46 -0600341 if not build:
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600342 raise Exception(f"Unable to find build system for {name}.")
343
344 for dep in set(build.dependencies()):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800345 if dep in cache:
346 continue
347 # Dependency package not already known
348 if dep_added.get(dep) is None:
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600349 print(f"Adding {dep} dependency to {name}.")
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800350 # Dependency package not added
351 new_child = dep_tree.AddChild(dep)
352 dep_added[dep] = False
Andrew Jeffery3b92fdd2020-03-13 11:49:18 +1030353 dep_pkgdir = clone_pkg(dep, branch)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800354 # Determine this dependency package's
355 # dependencies and add them before
356 # returning to add this package
Patrick Williamse08ffba2022-12-05 10:33:46 -0600357 dep_added = build_dep_tree(
358 dep, dep_pkgdir, dep_added, head, branch, new_child
359 )
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800360 else:
361 # Dependency package known and added
362 if dep_added[dep]:
Andrew Jeffery2cb0c7a2018-03-08 13:19:08 +1030363 continue
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500364 else:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800365 # Cyclic dependency failure
Patrick Williamse08ffba2022-12-05 10:33:46 -0600366 raise Exception("Cyclic dependencies found in " + name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500367
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030368 if not dep_added[name]:
369 dep_added[name] = True
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500370
371 return dep_added
Matthew Barthccb7f852016-11-23 17:43:02 -0600372
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700373
William A. Kennington III90b106a2019-02-06 18:08:24 -0800374def run_cppcheck():
Ed Tanousc7198552022-07-01 08:15:50 -0700375 if not os.path.exists(os.path.join("build", "compile_commands.json")):
Brad Bishop48424d42020-01-07 13:01:31 -0500376 return None
377
Patrick Williams485a0922022-08-12 11:44:55 -0500378 with TemporaryDirectory() as cpp_dir:
Patrick Williams485a0922022-08-12 11:44:55 -0500379 # http://cppcheck.sourceforge.net/manual.pdf
380 try:
381 check_call_cmd(
Patrick Williamse08ffba2022-12-05 10:33:46 -0600382 "cppcheck",
383 "-j",
384 str(multiprocessing.cpu_count()),
385 "--enable=style,performance,portability,missingInclude",
386 "--suppress=useStlAlgorithm",
387 "--suppress=unusedStructMember",
388 "--suppress=postfixOperator",
389 "--suppress=unreadVariable",
390 "--suppress=knownConditionTrueFalse",
391 "--library=googletest",
392 "--project=build/compile_commands.json",
393 f"--cppcheck-build-dir={cpp_dir}",
Patrick Williams485a0922022-08-12 11:44:55 -0500394 )
395 except subprocess.CalledProcessError:
396 print("cppcheck found errors")
Lei YUdbd7cd62022-07-19 19:24:01 +0800397
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030398
William A. Kennington III37a89a22018-12-13 14:32:02 -0800399def is_valgrind_safe():
400 """
401 Returns whether it is safe to run valgrind on our platform
402 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600403 src = "unit-test-vg.c"
404 exe = "./unit-test-vg"
405 with open(src, "w") as h:
406 h.write("#include <errno.h>\n")
407 h.write("#include <stdio.h>\n")
408 h.write("#include <stdlib.h>\n")
409 h.write("#include <string.h>\n")
410 h.write("int main() {\n")
411 h.write("char *heap_str = malloc(16);\n")
William A. Kennington III0326ded2019-02-07 00:33:28 -0800412 h.write('strcpy(heap_str, "RandString");\n')
413 h.write('int res = strcmp("RandString", heap_str);\n')
Patrick Williamse08ffba2022-12-05 10:33:46 -0600414 h.write("free(heap_str);\n")
415 h.write("char errstr[64];\n")
416 h.write("strerror_r(EINVAL, errstr, sizeof(errstr));\n")
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700417 h.write('printf("%s\\n", errstr);\n')
Patrick Williamse08ffba2022-12-05 10:33:46 -0600418 h.write("return res;\n")
419 h.write("}\n")
William A. Kennington III0326ded2019-02-07 00:33:28 -0800420 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600421 with open(os.devnull, "w") as devnull:
422 check_call(
423 ["gcc", "-O2", "-o", exe, src], stdout=devnull, stderr=devnull
424 )
425 check_call(
426 ["valgrind", "--error-exitcode=99", exe],
427 stdout=devnull,
428 stderr=devnull,
429 )
William A. Kennington III0326ded2019-02-07 00:33:28 -0800430 return True
Patrick Williamse08ffba2022-12-05 10:33:46 -0600431 except Exception:
William A. Kennington III0326ded2019-02-07 00:33:28 -0800432 sys.stderr.write("###### Platform is not valgrind safe ######\n")
433 return False
434 finally:
435 os.remove(src)
436 os.remove(exe)
William A. Kennington III37a89a22018-12-13 14:32:02 -0800437
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030438
William A. Kennington III282e3302019-02-04 16:55:05 -0800439def is_sanitize_safe():
440 """
441 Returns whether it is safe to run sanitizers on our platform
442 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600443 src = "unit-test-sanitize.c"
444 exe = "./unit-test-sanitize"
445 with open(src, "w") as h:
446 h.write("int main() { return 0; }\n")
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800447 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600448 with open(os.devnull, "w") as devnull:
449 check_call(
450 [
451 "gcc",
452 "-O2",
453 "-fsanitize=address",
454 "-fsanitize=undefined",
455 "-o",
456 exe,
457 src,
458 ],
459 stdout=devnull,
460 stderr=devnull,
461 )
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800462 check_call([exe], stdout=devnull, stderr=devnull)
Andrew Geisslercd9578b2021-05-04 10:18:01 -0500463
464 # TODO - Sanitizer not working on ppc64le
465 # https://github.com/openbmc/openbmc-build-scripts/issues/31
Patrick Williamse08ffba2022-12-05 10:33:46 -0600466 if platform.processor() == "ppc64le":
Andrew Geisslercd9578b2021-05-04 10:18:01 -0500467 sys.stderr.write("###### ppc64le is not sanitize safe ######\n")
468 return False
469 else:
470 return True
Patrick Williamse08ffba2022-12-05 10:33:46 -0600471 except Exception:
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800472 sys.stderr.write("###### Platform is not sanitize safe ######\n")
473 return False
474 finally:
475 os.remove(src)
476 os.remove(exe)
William A. Kennington III282e3302019-02-04 16:55:05 -0800477
William A. Kennington III49d4e592019-02-06 17:59:27 -0800478
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800479def maybe_make_valgrind():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700480 """
481 Potentially runs the unit tests through valgrind for the package
482 via `make check-valgrind`. If the package does not have valgrind testing
483 then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700484 """
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700485 # Valgrind testing is currently broken by an aggressive strcmp optimization
486 # that is inlined into optimized code for POWER by gcc 7+. Until we find
487 # a workaround, just don't run valgrind tests on POWER.
488 # https://github.com/openbmc/openbmc/issues/3315
William A. Kennington III37a89a22018-12-13 14:32:02 -0800489 if not is_valgrind_safe():
William A. Kennington III75130192019-02-07 00:34:14 -0800490 sys.stderr.write("###### Skipping valgrind ######\n")
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700491 return
Patrick Williamse08ffba2022-12-05 10:33:46 -0600492 if not make_target_exists("check-valgrind"):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700493 return
494
495 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600496 cmd = make_parallel + ["check-valgrind"]
William A. Kennington III1fddb972019-02-06 18:03:53 -0800497 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700498 except CalledProcessError:
William A. Kennington III90b106a2019-02-06 18:08:24 -0800499 for root, _, files in os.walk(os.getcwd()):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700500 for f in files:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600501 if re.search("test-suite-[a-z]+.log", f) is None:
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700502 continue
Patrick Williamse08ffba2022-12-05 10:33:46 -0600503 check_call_cmd("cat", os.path.join(root, f))
504 raise Exception("Valgrind tests failed")
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700505
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030506
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800507def maybe_make_coverage():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700508 """
509 Potentially runs the unit tests through code coverage for the package
510 via `make check-code-coverage`. If the package does not have code coverage
511 testing then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700512 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600513 if not make_target_exists("check-code-coverage"):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700514 return
515
516 # Actually run code coverage
517 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600518 cmd = make_parallel + ["check-code-coverage"]
William A. Kennington III1fddb972019-02-06 18:03:53 -0800519 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700520 except CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600521 raise Exception("Code coverage failed")
Matthew Barthccb7f852016-11-23 17:43:02 -0600522
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030523
524class BuildSystem(object):
525 """
526 Build systems generally provide the means to configure, build, install and
527 test software. The BuildSystem class defines a set of interfaces on top of
528 which Autotools, Meson, CMake and possibly other build system drivers can
529 be implemented, separating out the phases to control whether a package
530 should merely be installed or also tested and analyzed.
531 """
Lei YU08d2b922022-04-25 11:21:36 +0800532
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030533 def __init__(self, package, path):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600534 """Initialise the driver with properties independent of the build
535 system
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030536
537 Keyword arguments:
538 package: The name of the package. Derived from the path if None
539 path: The path to the package. Set to the working directory if None
540 """
541 self.path = "." if not path else path
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030542 realpath = os.path.realpath(self.path)
543 self.package = package if package else os.path.basename(realpath)
Andrew Jeffery3f8e5a72020-03-13 11:47:56 +1030544 self.build_for_testing = False
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030545
546 def probe(self):
547 """Test if the build system driver can be applied to the package
548
549 Return True if the driver can drive the package's build system,
550 otherwise False.
551
552 Generally probe() is implemented by testing for the presence of the
553 build system's configuration file(s).
554 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600555 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030556
557 def dependencies(self):
558 """Provide the package's dependencies
559
560 Returns a list of dependencies. If no dependencies are required then an
561 empty list must be returned.
562
563 Generally dependencies() is implemented by analysing and extracting the
564 data from the build system configuration.
565 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600566 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030567
568 def configure(self, build_for_testing):
569 """Configure the source ready for building
570
571 Should raise an exception if configuration failed.
572
573 Keyword arguments:
574 build_for_testing: Mark the package as being built for testing rather
575 than for installation as a dependency for the
576 package under test. Setting to True generally
577 implies that the package will be configured to build
578 with debug information, at a low level of
579 optimisation and possibly with sanitizers enabled.
580
581 Generally configure() is implemented by invoking the build system
582 tooling to generate Makefiles or equivalent.
583 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600584 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030585
586 def build(self):
587 """Build the software ready for installation and/or testing
588
589 Should raise an exception if the build fails
590
591 Generally build() is implemented by invoking `make` or `ninja`.
592 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600593 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030594
595 def install(self):
596 """Install the software ready for use
597
598 Should raise an exception if installation fails
599
600 Like build(), install() is generally implemented by invoking `make` or
601 `ninja`.
602 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600603 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030604
605 def test(self):
606 """Build and run the test suite associated with the package
607
608 Should raise an exception if the build or testing fails.
609
610 Like install(), test() is generally implemented by invoking `make` or
611 `ninja`.
612 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600613 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030614
615 def analyze(self):
616 """Run any supported analysis tools over the codebase
617
618 Should raise an exception if analysis fails.
619
620 Some analysis tools such as scan-build need injection into the build
621 system. analyze() provides the necessary hook to implement such
622 behaviour. Analyzers independent of the build system can also be
623 specified here but at the cost of possible duplication of code between
624 the build system driver implementations.
625 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600626 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030627
628
629class Autotools(BuildSystem):
630 def __init__(self, package=None, path=None):
631 super(Autotools, self).__init__(package, path)
632
633 def probe(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600634 return os.path.isfile(os.path.join(self.path, "configure.ac"))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030635
636 def dependencies(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600637 configure_ac = os.path.join(self.path, "configure.ac")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030638
Patrick Williamse08ffba2022-12-05 10:33:46 -0600639 contents = ""
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030640 # Prepend some special function overrides so we can parse out
641 # dependencies
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030642 for macro in DEPENDENCIES.keys():
Patrick Williamse08ffba2022-12-05 10:33:46 -0600643 contents += (
644 "m4_define(["
645 + macro
646 + "], ["
647 + macro
648 + "_START$"
649 + str(DEPENDENCIES_OFFSET[macro] + 1)
650 + macro
651 + "_END])\n"
652 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030653 with open(configure_ac, "rt") as f:
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030654 contents += f.read()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030655
Patrick Williamse08ffba2022-12-05 10:33:46 -0600656 autoconf_cmdline = ["autoconf", "-Wno-undefined", "-"]
657 autoconf_process = subprocess.Popen(
658 autoconf_cmdline,
659 stdin=subprocess.PIPE,
660 stdout=subprocess.PIPE,
661 stderr=subprocess.PIPE,
662 )
663 document = contents.encode("utf-8")
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030664 (stdout, stderr) = autoconf_process.communicate(input=document)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030665 if not stdout:
666 print(stderr)
667 raise Exception("Failed to run autoconf for parsing dependencies")
668
669 # Parse out all of the dependency text
670 matches = []
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030671 for macro in DEPENDENCIES.keys():
Patrick Williamse08ffba2022-12-05 10:33:46 -0600672 pattern = "(" + macro + ")_START(.*?)" + macro + "_END"
673 for match in re.compile(pattern).finditer(stdout.decode("utf-8")):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030674 matches.append((match.group(1), match.group(2)))
675
676 # Look up dependencies from the text
677 found_deps = []
678 for macro, deptext in matches:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600679 for potential_dep in deptext.split(" "):
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030680 for known_dep in DEPENDENCIES[macro].keys():
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030681 if potential_dep.startswith(known_dep):
682 found_deps.append(DEPENDENCIES[macro][known_dep])
683
684 return found_deps
685
686 def _configure_feature(self, flag, enabled):
687 """
688 Returns an configure flag as a string
689
690 Parameters:
691 flag The name of the flag
692 enabled Whether the flag is enabled or disabled
693 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600694 return "--" + ("enable" if enabled else "disable") + "-" + flag
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030695
696 def configure(self, build_for_testing):
697 self.build_for_testing = build_for_testing
698 conf_flags = [
Patrick Williamse08ffba2022-12-05 10:33:46 -0600699 self._configure_feature("silent-rules", False),
700 self._configure_feature("examples", build_for_testing),
701 self._configure_feature("tests", build_for_testing),
702 self._configure_feature("itests", INTEGRATION_TEST),
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030703 ]
Patrick Williamse08ffba2022-12-05 10:33:46 -0600704 conf_flags.extend(
705 [
706 self._configure_feature("code-coverage", build_for_testing),
707 self._configure_feature("valgrind", build_for_testing),
708 ]
709 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030710 # Add any necessary configure flags for package
711 if CONFIGURE_FLAGS.get(self.package) is not None:
712 conf_flags.extend(CONFIGURE_FLAGS.get(self.package))
Patrick Williamse08ffba2022-12-05 10:33:46 -0600713 for bootstrap in ["bootstrap.sh", "bootstrap", "autogen.sh"]:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030714 if os.path.exists(bootstrap):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600715 check_call_cmd("./" + bootstrap)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030716 break
Patrick Williamse08ffba2022-12-05 10:33:46 -0600717 check_call_cmd("./configure", *conf_flags)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030718
719 def build(self):
720 check_call_cmd(*make_parallel)
721
722 def install(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600723 check_call_cmd("sudo", "-n", "--", *(make_parallel + ["install"]))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030724
725 def test(self):
726 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600727 cmd = make_parallel + ["check"]
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030728 for i in range(0, args.repeat):
729 check_call_cmd(*cmd)
Andrew Jefferyd0809692021-05-14 16:23:57 +0930730
731 maybe_make_valgrind()
732 maybe_make_coverage()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030733 except CalledProcessError:
734 for root, _, files in os.walk(os.getcwd()):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600735 if "test-suite.log" not in files:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030736 continue
Patrick Williamse08ffba2022-12-05 10:33:46 -0600737 check_call_cmd("cat", os.path.join(root, "test-suite.log"))
738 raise Exception("Unit tests failed")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030739
740 def analyze(self):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030741 run_cppcheck()
742
743
744class CMake(BuildSystem):
745 def __init__(self, package=None, path=None):
746 super(CMake, self).__init__(package, path)
747
748 def probe(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600749 return os.path.isfile(os.path.join(self.path, "CMakeLists.txt"))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030750
751 def dependencies(self):
752 return []
753
754 def configure(self, build_for_testing):
755 self.build_for_testing = build_for_testing
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000756 if INTEGRATION_TEST:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600757 check_call_cmd(
758 "cmake",
759 "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
760 "-DITESTS=ON",
761 ".",
762 )
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000763 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600764 check_call_cmd("cmake", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", ".")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030765
766 def build(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600767 check_call_cmd(
768 "cmake",
769 "--build",
770 ".",
771 "--",
772 "-j",
773 str(multiprocessing.cpu_count()),
774 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030775
776 def install(self):
Michael Shen87ab5702023-01-13 07:41:11 +0000777 check_call_cmd("sudo", "cmake", "--install", ".")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030778
779 def test(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600780 if make_target_exists("test"):
781 check_call_cmd("ctest", ".")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030782
783 def analyze(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600784 if os.path.isfile(".clang-tidy"):
785 with TemporaryDirectory(prefix="build", dir=".") as build_dir:
Nan Zhou82aaba02022-09-16 00:21:07 +0000786 # clang-tidy needs to run on a clang-specific build
Patrick Williamse08ffba2022-12-05 10:33:46 -0600787 check_call_cmd(
788 "cmake",
789 "-DCMAKE_C_COMPILER=clang",
790 "-DCMAKE_CXX_COMPILER=clang++",
791 "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
792 "-H.",
793 "-B" + build_dir,
794 )
Nan Zhou524ebee2022-08-27 23:26:13 +0000795
Patrick Williamse08ffba2022-12-05 10:33:46 -0600796 check_call_cmd(
797 "run-clang-tidy", "-header-filter=.*", "-p", build_dir
798 )
Ed Tanous662890f2020-09-28 17:35:20 -0700799
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030800 maybe_make_valgrind()
801 maybe_make_coverage()
802 run_cppcheck()
803
804
805class Meson(BuildSystem):
806 def __init__(self, package=None, path=None):
807 super(Meson, self).__init__(package, path)
808
809 def probe(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600810 return os.path.isfile(os.path.join(self.path, "meson.build"))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030811
812 def dependencies(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600813 meson_build = os.path.join(self.path, "meson.build")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030814 if not os.path.exists(meson_build):
815 return []
816
817 found_deps = []
818 for root, dirs, files in os.walk(self.path):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600819 if "meson.build" not in files:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030820 continue
Patrick Williamse08ffba2022-12-05 10:33:46 -0600821 with open(os.path.join(root, "meson.build"), "rt") as f:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030822 build_contents = f.read()
Nan Zhouef13d532020-07-07 09:52:02 -0700823 pattern = r"dependency\('([^']*)'.*?\),?\n"
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030824 for match in re.finditer(pattern, build_contents):
825 group = match.group(1)
Patrick Williamse08ffba2022-12-05 10:33:46 -0600826 maybe_dep = DEPENDENCIES["PKG_CHECK_MODULES"].get(group)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030827 if maybe_dep is not None:
828 found_deps.append(maybe_dep)
829
830 return found_deps
831
832 def _parse_options(self, options_file):
833 """
834 Returns a set of options defined in the provides meson_options.txt file
835
836 Parameters:
837 options_file The file containing options
838 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600839 oi = optinterpreter.OptionInterpreter("")
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700840 oi.process(options_file)
841 return oi.options
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030842
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700843 def _configure_boolean(self, val):
844 """
845 Returns the meson flag which signifies the value
846
847 True is true which requires the boolean.
848 False is false which disables the boolean.
849
850 Parameters:
851 val The value being converted
852 """
853 if val is True:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600854 return "true"
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700855 elif val is False:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600856 return "false"
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700857 else:
858 raise Exception("Bad meson boolean value")
859
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030860 def _configure_feature(self, val):
861 """
862 Returns the meson flag which signifies the value
863
864 True is enabled which requires the feature.
865 False is disabled which disables the feature.
866 None is auto which autodetects the feature.
867
868 Parameters:
869 val The value being converted
870 """
871 if val is True:
872 return "enabled"
873 elif val is False:
874 return "disabled"
875 elif val is None:
876 return "auto"
877 else:
878 raise Exception("Bad meson feature value")
879
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700880 def _configure_option(self, opts, key, val):
881 """
882 Returns the meson flag which signifies the value
883 based on the type of the opt
884
885 Parameters:
886 opt The meson option which we are setting
887 val The value being converted
888 """
889 if isinstance(opts[key], coredata.UserBooleanOption):
890 str_val = self._configure_boolean(val)
891 elif isinstance(opts[key], coredata.UserFeatureOption):
892 str_val = self._configure_feature(val)
893 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600894 raise Exception("Unknown meson option type")
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700895 return "-D{}={}".format(key, str_val)
896
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030897 def configure(self, build_for_testing):
898 self.build_for_testing = build_for_testing
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700899 meson_options = {}
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030900 if os.path.exists("meson_options.txt"):
901 meson_options = self._parse_options("meson_options.txt")
902 meson_flags = [
Patrick Williamse08ffba2022-12-05 10:33:46 -0600903 "-Db_colorout=never",
904 "-Dwerror=true",
905 "-Dwarning_level=3",
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030906 ]
907 if build_for_testing:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600908 meson_flags.append("--buildtype=debug")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030909 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600910 meson_flags.append("--buildtype=debugoptimized")
911 if OptionKey("tests") in meson_options:
912 meson_flags.append(
913 self._configure_option(
914 meson_options, OptionKey("tests"), build_for_testing
915 )
916 )
917 if OptionKey("examples") in meson_options:
918 meson_flags.append(
919 self._configure_option(
920 meson_options, OptionKey("examples"), build_for_testing
921 )
922 )
923 if OptionKey("itests") in meson_options:
924 meson_flags.append(
925 self._configure_option(
926 meson_options, OptionKey("itests"), INTEGRATION_TEST
927 )
928 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030929 if MESON_FLAGS.get(self.package) is not None:
930 meson_flags.extend(MESON_FLAGS.get(self.package))
931 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600932 check_call_cmd(
933 "meson", "setup", "--reconfigure", "build", *meson_flags
934 )
935 except Exception:
936 shutil.rmtree("build", ignore_errors=True)
937 check_call_cmd("meson", "setup", "build", *meson_flags)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030938
939 def build(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600940 check_call_cmd("ninja", "-C", "build")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030941
942 def install(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600943 check_call_cmd("sudo", "-n", "--", "ninja", "-C", "build", "install")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030944
945 def test(self):
Patrick Williams95095f12021-04-14 14:42:35 -0500946 # It is useful to check various settings of the meson.build file
947 # for compatibility, such as meson_version checks. We shouldn't
948 # do this in the configure path though because it affects subprojects
949 # and dependencies as well, but we only want this applied to the
950 # project-under-test (otherwise an upstream dependency could fail
951 # this check without our control).
952 self._extra_meson_checks()
953
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030954 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600955 test_args = ("--repeat", str(args.repeat), "-C", "build")
956 check_call_cmd("meson", "test", "--print-errorlogs", *test_args)
Brad Bishop7b8cef22020-08-26 15:58:09 -0400957
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030958 except CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600959 raise Exception("Unit tests failed")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030960
961 def _setup_exists(self, setup):
962 """
963 Returns whether the meson build supports the named test setup.
964
965 Parameter descriptions:
966 setup The setup target to check
967 """
968 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600969 with open(os.devnull, "w"):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030970 output = subprocess.check_output(
Patrick Williamse08ffba2022-12-05 10:33:46 -0600971 [
972 "meson",
973 "test",
974 "-C",
975 "build",
976 "--setup",
977 setup,
978 "-t",
979 "0",
980 ],
981 stderr=subprocess.STDOUT,
982 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030983 except CalledProcessError as e:
984 output = e.output
Patrick Williamse08ffba2022-12-05 10:33:46 -0600985 output = output.decode("utf-8")
986 return not re.search("Test setup .* not found from project", output)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030987
988 def _maybe_valgrind(self):
989 """
990 Potentially runs the unit tests through valgrind for the package
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030991 via `meson test`. The package can specify custom valgrind
992 configurations by utilizing add_test_setup() in a meson.build
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030993 """
994 if not is_valgrind_safe():
995 sys.stderr.write("###### Skipping valgrind ######\n")
996 return
997 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600998 if self._setup_exists("valgrind"):
999 check_call_cmd(
1000 "meson",
1001 "test",
1002 "-t",
1003 "10",
1004 "-C",
1005 "build",
1006 "--print-errorlogs",
1007 "--setup",
1008 "valgrind",
1009 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301010 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001011 check_call_cmd(
1012 "meson",
1013 "test",
1014 "-t",
1015 "10",
1016 "-C",
1017 "build",
1018 "--print-errorlogs",
1019 "--wrapper",
1020 "valgrind",
1021 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301022 except CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001023 raise Exception("Valgrind tests failed")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301024
1025 def analyze(self):
1026 self._maybe_valgrind()
1027
1028 # Run clang-tidy only if the project has a configuration
Patrick Williamse08ffba2022-12-05 10:33:46 -06001029 if os.path.isfile(".clang-tidy"):
Manojkiran Eda1aa91992020-10-02 14:11:53 +05301030 os.environ["CXX"] = "clang++"
Patrick Williamse08ffba2022-12-05 10:33:46 -06001031 with TemporaryDirectory(prefix="build", dir=".") as build_dir:
1032 check_call_cmd("meson", "setup", build_dir)
Ed Tanous29e02312022-12-19 16:20:34 -08001033 if not os.path.isfile(".openbmc-no-clang"):
1034 check_call_cmd("meson", "compile", "-C", build_dir)
Nan Zhou82aaba02022-09-16 00:21:07 +00001035 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001036 check_call_cmd(
1037 "run-clang-tidy", "-fix", "-format", "-p", build_dir
1038 )
Nan Zhou82aaba02022-09-16 00:21:07 +00001039 except subprocess.CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001040 check_call_cmd(
1041 "git", "-C", CODE_SCAN_DIR, "--no-pager", "diff"
1042 )
Nan Zhou82aaba02022-09-16 00:21:07 +00001043 raise
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301044 # Run the basic clang static analyzer otherwise
1045 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001046 check_call_cmd("ninja", "-C", "build", "scan-build")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301047
1048 # Run tests through sanitizers
1049 # b_lundef is needed if clang++ is CXX since it resolves the
1050 # asan symbols at runtime only. We don't want to set it earlier
1051 # in the build process to ensure we don't have undefined
1052 # runtime code.
1053 if is_sanitize_safe():
Patrick Williamse08ffba2022-12-05 10:33:46 -06001054 check_call_cmd(
1055 "meson",
1056 "configure",
1057 "build",
1058 "-Db_sanitize=address,undefined",
1059 "-Db_lundef=false",
1060 )
1061 check_call_cmd(
1062 "meson",
1063 "test",
1064 "-C",
1065 "build",
1066 "--print-errorlogs",
1067 "--logbase",
1068 "testlog-ubasan",
1069 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301070 # TODO: Fix memory sanitizer
Andrew Jeffery6015ca12020-03-13 11:50:10 +10301071 # check_call_cmd('meson', 'configure', 'build',
1072 # '-Db_sanitize=memory')
1073 # check_call_cmd('meson', 'test', '-C', 'build'
1074 # '--logbase', 'testlog-msan')
Patrick Williamse08ffba2022-12-05 10:33:46 -06001075 check_call_cmd("meson", "configure", "build", "-Db_sanitize=none")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301076 else:
1077 sys.stderr.write("###### Skipping sanitizers ######\n")
1078
1079 # Run coverage checks
Patrick Williamse08ffba2022-12-05 10:33:46 -06001080 check_call_cmd("meson", "configure", "build", "-Db_coverage=true")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301081 self.test()
1082 # Only build coverage HTML if coverage files were produced
Patrick Williamse08ffba2022-12-05 10:33:46 -06001083 for root, dirs, files in os.walk("build"):
1084 if any([f.endswith(".gcda") for f in files]):
1085 check_call_cmd("ninja", "-C", "build", "coverage-html")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301086 break
Patrick Williamse08ffba2022-12-05 10:33:46 -06001087 check_call_cmd("meson", "configure", "build", "-Db_coverage=false")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301088 run_cppcheck()
1089
Patrick Williams95095f12021-04-14 14:42:35 -05001090 def _extra_meson_checks(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001091 with open(os.path.join(self.path, "meson.build"), "rt") as f:
Patrick Williams95095f12021-04-14 14:42:35 -05001092 build_contents = f.read()
1093
1094 # Find project's specified meson_version.
1095 meson_version = None
1096 pattern = r"meson_version:[^']*'([^']*)'"
1097 for match in re.finditer(pattern, build_contents):
1098 group = match.group(1)
1099 meson_version = group
1100
1101 # C++20 requires at least Meson 0.57 but Meson itself doesn't
1102 # identify this. Add to our unit-test checks so that we don't
1103 # get a meson.build missing this.
1104 pattern = r"'cpp_std=c\+\+20'"
1105 for match in re.finditer(pattern, build_contents):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001106 if not meson_version or not meson_version_compare(
1107 meson_version, ">=0.57"
1108 ):
Patrick Williams95095f12021-04-14 14:42:35 -05001109 raise Exception(
1110 "C++20 support requires specifying in meson.build: "
1111 + "meson_version: '>=0.57'"
1112 )
1113
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301114
1115class Package(object):
1116 def __init__(self, name=None, path=None):
Manojkiran Eda29d28252020-06-15 18:58:09 +05301117 self.supported = [Meson, Autotools, CMake]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301118 self.name = name
1119 self.path = path
1120 self.test_only = False
1121
1122 def build_systems(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +10301123 instances = (system(self.name, self.path) for system in self.supported)
1124 return (instance for instance in instances if instance.probe())
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301125
1126 def build_system(self, preferred=None):
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001127 systems = list(self.build_systems())
1128
1129 if not systems:
1130 return None
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301131
1132 if preferred:
Andrew Jeffery881041f2020-03-13 11:46:30 +10301133 return {type(system): system for system in systems}[preferred]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301134
1135 return next(iter(systems))
1136
1137 def install(self, system=None):
1138 if not system:
1139 system = self.build_system()
1140
1141 system.configure(False)
1142 system.build()
1143 system.install()
1144
Andrew Jeffery19d75672020-03-13 10:42:08 +10301145 def _test_one(self, system):
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301146 system.configure(True)
1147 system.build()
1148 system.install()
1149 system.test()
Andrew Jefferyd0809692021-05-14 16:23:57 +09301150 if not TEST_ONLY:
1151 system.analyze()
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301152
Andrew Jeffery19d75672020-03-13 10:42:08 +10301153 def test(self):
1154 for system in self.build_systems():
1155 self._test_one(system)
1156
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301157
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001158def find_file(filename, basedir):
1159 """
Patrick Williams55448ad2020-12-14 14:28:28 -06001160 Finds all occurrences of a file (or list of files) in the base
1161 directory and passes them back with their relative paths.
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001162
1163 Parameter descriptions:
Patrick Williams55448ad2020-12-14 14:28:28 -06001164 filename The name of the file (or list of files) to
1165 find
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001166 basedir The base directory search in
1167 """
1168
Patrick Williams55448ad2020-12-14 14:28:28 -06001169 if not isinstance(filename, list):
Lei YU08d2b922022-04-25 11:21:36 +08001170 filename = [filename]
Patrick Williams55448ad2020-12-14 14:28:28 -06001171
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001172 filepaths = []
1173 for root, dirs, files in os.walk(basedir):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001174 if os.path.split(root)[-1] == "subprojects":
Brad Bishopeb667262021-08-06 13:54:58 -04001175 for f in files:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001176 subproject = ".".join(f.split(".")[0:-1])
1177 if f.endswith(".wrap") and subproject in dirs:
Brad Bishopeb667262021-08-06 13:54:58 -04001178 # don't find files in meson subprojects with wraps
1179 dirs.remove(subproject)
Patrick Williams55448ad2020-12-14 14:28:28 -06001180 for f in filename:
1181 if f in files:
1182 filepaths.append(os.path.join(root, f))
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001183 return filepaths
1184
Andrew Jeffery9cc97d82020-03-13 11:57:44 +10301185
Patrick Williamse08ffba2022-12-05 10:33:46 -06001186if __name__ == "__main__":
Matthew Barthccb7f852016-11-23 17:43:02 -06001187 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
1188 CONFIGURE_FLAGS = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001189 "phosphor-logging": [
1190 "--enable-metadata-processing",
1191 "--enable-openpower-pel-extension",
1192 "YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml",
1193 ]
Matthew Barthccb7f852016-11-23 17:43:02 -06001194 }
1195
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001196 # MESON_FLAGS = [GIT REPO]:[MESON FLAGS]
1197 MESON_FLAGS = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001198 "phosphor-dbus-interfaces": [
1199 "-Ddata_com_ibm=true",
1200 "-Ddata_org_open_power=true",
1201 ],
1202 "phosphor-logging": ["-Dopenpower-pel-extension=enabled"],
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001203 }
1204
Matthew Barthccb7f852016-11-23 17:43:02 -06001205 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
1206 DEPENDENCIES = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001207 "AC_CHECK_LIB": {"mapper": "phosphor-objmgr"},
1208 "AC_CHECK_HEADER": {
1209 "host-ipmid": "phosphor-host-ipmid",
1210 "blobs-ipmid": "phosphor-ipmi-blobs",
1211 "sdbusplus": "sdbusplus",
1212 "sdeventplus": "sdeventplus",
1213 "stdplus": "stdplus",
1214 "gpioplus": "gpioplus",
1215 "phosphor-logging/log.hpp": "phosphor-logging",
Patrick Williamseab8a372017-01-30 11:21:32 -06001216 },
Patrick Williamse08ffba2022-12-05 10:33:46 -06001217 "AC_PATH_PROG": {"sdbus++": "sdbusplus"},
1218 "PKG_CHECK_MODULES": {
1219 "phosphor-dbus-interfaces": "phosphor-dbus-interfaces",
1220 "libipmid": "phosphor-host-ipmid",
1221 "libipmid-host": "phosphor-host-ipmid",
1222 "sdbusplus": "sdbusplus",
1223 "sdeventplus": "sdeventplus",
1224 "stdplus": "stdplus",
1225 "gpioplus": "gpioplus",
1226 "phosphor-logging": "phosphor-logging",
1227 "phosphor-snmp": "phosphor-snmp",
1228 "ipmiblob": "ipmi-blob-tool",
1229 "hei": "openpower-libhei",
1230 "phosphor-ipmi-blobs": "phosphor-ipmi-blobs",
1231 "libcr51sign": "google-misc",
Brad Bishopebb49112017-02-13 11:07:26 -05001232 },
Matthew Barthccb7f852016-11-23 17:43:02 -06001233 }
1234
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001235 # Offset into array of macro parameters MACRO(0, 1, ...N)
1236 DEPENDENCIES_OFFSET = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001237 "AC_CHECK_LIB": 0,
1238 "AC_CHECK_HEADER": 0,
1239 "AC_PATH_PROG": 1,
1240 "PKG_CHECK_MODULES": 1,
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001241 }
1242
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001243 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
Patrick Williamse08ffba2022-12-05 10:33:46 -06001244 DEPENDENCIES_REGEX = {"phosphor-logging": r"\S+-dbus-interfaces$"}
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001245
Matthew Barth33df8792016-12-19 14:30:17 -06001246 # Set command line arguments
1247 parser = argparse.ArgumentParser()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001248 parser.add_argument(
1249 "-w",
1250 "--workspace",
1251 dest="WORKSPACE",
1252 required=True,
1253 help="Workspace directory location(i.e. /home)",
1254 )
1255 parser.add_argument(
1256 "-p",
1257 "--package",
1258 dest="PACKAGE",
1259 required=True,
1260 help="OpenBMC package to be unit tested",
1261 )
1262 parser.add_argument(
1263 "-t",
1264 "--test-only",
1265 dest="TEST_ONLY",
1266 action="store_true",
1267 required=False,
1268 default=False,
1269 help="Only run test cases, no other validation",
1270 )
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001271 arg_inttests = parser.add_mutually_exclusive_group()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001272 arg_inttests.add_argument(
1273 "--integration-tests",
1274 dest="INTEGRATION_TEST",
1275 action="store_true",
1276 required=False,
1277 default=True,
1278 help="Enable integration tests [default].",
1279 )
1280 arg_inttests.add_argument(
1281 "--no-integration-tests",
1282 dest="INTEGRATION_TEST",
1283 action="store_false",
1284 required=False,
1285 help="Disable integration tests.",
1286 )
1287 parser.add_argument(
1288 "-v",
1289 "--verbose",
1290 action="store_true",
1291 help="Print additional package status messages",
1292 )
1293 parser.add_argument(
1294 "-r", "--repeat", help="Repeat tests N times", type=int, default=1
1295 )
1296 parser.add_argument(
1297 "-b",
1298 "--branch",
1299 dest="BRANCH",
1300 required=False,
1301 help="Branch to target for dependent repositories",
1302 default="master",
1303 )
1304 parser.add_argument(
1305 "-n",
1306 "--noformat",
1307 dest="FORMAT",
1308 action="store_false",
1309 required=False,
1310 help="Whether or not to run format code",
1311 )
Matthew Barth33df8792016-12-19 14:30:17 -06001312 args = parser.parse_args(sys.argv[1:])
1313 WORKSPACE = args.WORKSPACE
1314 UNIT_TEST_PKG = args.PACKAGE
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001315 TEST_ONLY = args.TEST_ONLY
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001316 INTEGRATION_TEST = args.INTEGRATION_TEST
Andrew Geisslera61acb52019-01-03 16:32:44 -06001317 BRANCH = args.BRANCH
Lei YU7ef93302019-11-06 13:53:21 +08001318 FORMAT_CODE = args.FORMAT
Matthew Barth33df8792016-12-19 14:30:17 -06001319 if args.verbose:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001320
Matthew Barth33df8792016-12-19 14:30:17 -06001321 def printline(*line):
1322 for arg in line:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001323 print(arg, end=" ")
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301324 print()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001325
Matthew Barth33df8792016-12-19 14:30:17 -06001326 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001327
Andrew Jefferyfce557f2020-03-13 12:12:19 +10301328 def printline(*line):
1329 pass
Matthew Barthccb7f852016-11-23 17:43:02 -06001330
Patrick Williamsb6535952020-12-15 06:40:10 -06001331 CODE_SCAN_DIR = os.path.join(WORKSPACE, UNIT_TEST_PKG)
Lei YU7ef93302019-11-06 13:53:21 +08001332
Patrick Williams330b0772022-11-28 06:14:06 -06001333 # Run format-code.sh, which will in turn call any repo-level formatters.
Lei YU7ef93302019-11-06 13:53:21 +08001334 if FORMAT_CODE:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001335 check_call_cmd(
1336 os.path.join(
1337 WORKSPACE, "openbmc-build-scripts", "scripts", "format-code.sh"
1338 ),
1339 CODE_SCAN_DIR,
1340 )
Andrew Geisslera28286d2018-01-10 11:00:00 -08001341
Ed Tanous32768b82022-01-05 14:14:06 -08001342 # Check to see if any files changed
Patrick Williamse08ffba2022-12-05 10:33:46 -06001343 check_call_cmd(
1344 "git", "-C", CODE_SCAN_DIR, "--no-pager", "diff", "--exit-code"
1345 )
Ed Tanous32768b82022-01-05 14:14:06 -08001346
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001347 # Check if this repo has a supported make infrastructure
Patrick Williamsb6535952020-12-15 06:40:10 -06001348 pkg = Package(UNIT_TEST_PKG, CODE_SCAN_DIR)
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001349 if not pkg.build_system():
1350 print("No valid build system, exit")
1351 sys.exit(0)
1352
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301353 prev_umask = os.umask(000)
James Feist878df5c2018-07-26 14:54:28 -07001354
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301355 # Determine dependencies and add them
1356 dep_added = dict()
1357 dep_added[UNIT_TEST_PKG] = False
William A. Kennington III40d5c7c2018-12-13 14:37:59 -08001358
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301359 # Create dependency tree
1360 dep_tree = DepTree(UNIT_TEST_PKG)
Patrick Williamsb6535952020-12-15 06:40:10 -06001361 build_dep_tree(UNIT_TEST_PKG, CODE_SCAN_DIR, dep_added, dep_tree, BRANCH)
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001362
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301363 # Reorder Dependency Tree
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301364 for pkg_name, regex_str in DEPENDENCIES_REGEX.items():
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301365 dep_tree.ReorderDeps(pkg_name, regex_str)
1366 if args.verbose:
1367 dep_tree.PrintTree()
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001368
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301369 install_list = dep_tree.GetInstallList()
Andrew Geissler9ced4ed2019-11-18 14:33:58 -06001370
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301371 # We don't want to treat our package as a dependency
1372 install_list.remove(UNIT_TEST_PKG)
James Feist878df5c2018-07-26 14:54:28 -07001373
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301374 # Install reordered dependencies
1375 for dep in install_list:
1376 build_and_install(dep, False)
James Feist878df5c2018-07-26 14:54:28 -07001377
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301378 # Run package unit tests
1379 build_and_install(UNIT_TEST_PKG, True)
James Feist878df5c2018-07-26 14:54:28 -07001380
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301381 os.umask(prev_umask)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001382
1383 # Run any custom CI scripts the repo has, of which there can be
1384 # multiple of and anywhere in the repository.
Patrick Williamse08ffba2022-12-05 10:33:46 -06001385 ci_scripts = find_file(["run-ci.sh", "run-ci"], CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001386 if ci_scripts:
Patrick Williamsb6535952020-12-15 06:40:10 -06001387 os.chdir(CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001388 for ci_script in ci_scripts:
Patrick Williams55448ad2020-12-14 14:28:28 -06001389 check_call_cmd(ci_script)