blob: 1660042d06d40ffaffc35d44ad34101a0cf5db7f [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
William A. Kennington III3992d102021-05-17 01:41:04 -070023# interpreter is not used directly but this resolves dependency ordering
24# that would be broken if we didn't include it.
Patrick Williamse08ffba2022-12-05 10:33:46 -060025from mesonbuild import interpreter # noqa: F401
William A. Kennington IIIfcd70772020-06-04 00:50:23 -070026from mesonbuild import coredata, optinterpreter
Patrick Williamse95626d2021-04-13 11:13:53 -050027from mesonbuild.mesonlib import OptionKey
Patrick Williams95095f12021-04-14 14:42:35 -050028from mesonbuild.mesonlib import version_compare as meson_version_compare
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050029
30
Patrick Williamse08ffba2022-12-05 10:33:46 -060031class DepTree:
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050032 """
33 Represents package dependency tree, where each node is a DepTree with a
34 name and DepTree children.
35 """
36
37 def __init__(self, name):
38 """
39 Create new DepTree.
40
41 Parameter descriptions:
42 name Name of new tree node.
43 """
44 self.name = name
45 self.children = list()
46
47 def AddChild(self, name):
48 """
49 Add new child node to current node.
50
51 Parameter descriptions:
52 name Name of new child
53 """
54 new_child = DepTree(name)
55 self.children.append(new_child)
56 return new_child
57
58 def AddChildNode(self, node):
59 """
60 Add existing child node to current node.
61
62 Parameter descriptions:
63 node Tree node to add
64 """
65 self.children.append(node)
66
67 def RemoveChild(self, name):
68 """
69 Remove child node.
70
71 Parameter descriptions:
72 name Name of child to remove
73 """
74 for child in self.children:
75 if child.name == name:
76 self.children.remove(child)
77 return
78
79 def GetNode(self, name):
80 """
81 Return node with matching name. Return None if not found.
82
83 Parameter descriptions:
84 name Name of node to return
85 """
86 if self.name == name:
87 return self
88 for child in self.children:
89 node = child.GetNode(name)
90 if node:
91 return node
92 return None
93
94 def GetParentNode(self, name, parent_node=None):
95 """
96 Return parent of node with matching name. Return none if not found.
97
98 Parameter descriptions:
99 name Name of node to get parent of
100 parent_node Parent of current node
101 """
102 if self.name == name:
103 return parent_node
104 for child in self.children:
105 found_node = child.GetParentNode(name, self)
106 if found_node:
107 return found_node
108 return None
109
110 def GetPath(self, name, path=None):
111 """
112 Return list of node names from head to matching name.
113 Return None if not found.
114
115 Parameter descriptions:
116 name Name of node
117 path List of node names from head to current node
118 """
119 if not path:
120 path = []
121 if self.name == name:
122 path.append(self.name)
123 return path
124 for child in self.children:
125 match = child.GetPath(name, path + [self.name])
126 if match:
127 return match
128 return None
129
130 def GetPathRegex(self, name, regex_str, path=None):
131 """
132 Return list of node paths that end in name, or match regex_str.
133 Return empty list if not found.
134
135 Parameter descriptions:
136 name Name of node to search for
137 regex_str Regex string to match node names
138 path Path of node names from head to current node
139 """
140 new_paths = []
141 if not path:
142 path = []
143 match = re.match(regex_str, self.name)
144 if (self.name == name) or (match):
145 new_paths.append(path + [self.name])
146 for child in self.children:
147 return_paths = None
148 full_path = path + [self.name]
149 return_paths = child.GetPathRegex(name, regex_str, full_path)
150 for i in return_paths:
151 new_paths.append(i)
152 return new_paths
153
154 def MoveNode(self, from_name, to_name):
155 """
156 Mode existing from_name node to become child of to_name node.
157
158 Parameter descriptions:
159 from_name Name of node to make a child of to_name
160 to_name Name of node to make parent of from_name
161 """
162 parent_from_node = self.GetParentNode(from_name)
163 from_node = self.GetNode(from_name)
164 parent_from_node.RemoveChild(from_name)
165 to_node = self.GetNode(to_name)
166 to_node.AddChildNode(from_node)
167
168 def ReorderDeps(self, name, regex_str):
169 """
170 Reorder dependency tree. If tree contains nodes with names that
171 match 'name' and 'regex_str', move 'regex_str' nodes that are
172 to the right of 'name' node, so that they become children of the
173 'name' node.
174
175 Parameter descriptions:
176 name Name of node to look for
177 regex_str Regex string to match names to
178 """
179 name_path = self.GetPath(name)
180 if not name_path:
181 return
182 paths = self.GetPathRegex(name, regex_str)
183 is_name_in_paths = False
184 name_index = 0
185 for i in range(len(paths)):
186 path = paths[i]
187 if path[-1] == name:
188 is_name_in_paths = True
189 name_index = i
190 break
191 if not is_name_in_paths:
192 return
193 for i in range(name_index + 1, len(paths)):
194 path = paths[i]
195 if name in path:
196 continue
197 from_name = path[-1]
198 self.MoveNode(from_name, name)
199
200 def GetInstallList(self):
201 """
202 Return post-order list of node names.
203
204 Parameter descriptions:
205 """
206 install_list = []
207 for child in self.children:
208 child_install_list = child.GetInstallList()
209 install_list.extend(child_install_list)
210 install_list.append(self.name)
211 return install_list
212
213 def PrintTree(self, level=0):
214 """
215 Print pre-order node names with indentation denoting node depth level.
216
217 Parameter descriptions:
218 level Current depth level
219 """
220 INDENT_PER_LEVEL = 4
Patrick Williamse08ffba2022-12-05 10:33:46 -0600221 print(" " * (level * INDENT_PER_LEVEL) + self.name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500222 for child in self.children:
223 child.PrintTree(level + 1)
Matthew Barth33df8792016-12-19 14:30:17 -0600224
225
William A. Kennington III1fddb972019-02-06 18:03:53 -0800226def check_call_cmd(*cmd):
Matthew Barth33df8792016-12-19 14:30:17 -0600227 """
228 Verbose prints the directory location the given command is called from and
229 the command, then executes the command using check_call.
230
231 Parameter descriptions:
232 dir Directory location command is to be called from
233 cmd List of parameters constructing the complete command
234 """
William A. Kennington III1fddb972019-02-06 18:03:53 -0800235 printline(os.getcwd(), ">", " ".join(cmd))
Matthew Barth33df8792016-12-19 14:30:17 -0600236 check_call(cmd)
Matthew Barthccb7f852016-11-23 17:43:02 -0600237
238
Andrew Geisslera61acb52019-01-03 16:32:44 -0600239def clone_pkg(pkg, branch):
Matthew Barth33df8792016-12-19 14:30:17 -0600240 """
241 Clone the given openbmc package's git repository from gerrit into
242 the WORKSPACE location
243
244 Parameter descriptions:
245 pkg Name of the package to clone
Andrew Geisslera61acb52019-01-03 16:32:44 -0600246 branch Branch to clone from pkg
Matthew Barth33df8792016-12-19 14:30:17 -0600247 """
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030248 pkg_dir = os.path.join(WORKSPACE, pkg)
Patrick Williamse08ffba2022-12-05 10:33:46 -0600249 if os.path.exists(os.path.join(pkg_dir, ".git")):
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030250 return pkg_dir
Patrick Williamse08ffba2022-12-05 10:33:46 -0600251 pkg_repo = urljoin("https://gerrit.openbmc.org/openbmc/", pkg)
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030252 os.mkdir(pkg_dir)
Andrew Geisslera61acb52019-01-03 16:32:44 -0600253 printline(pkg_dir, "> git clone", pkg_repo, branch, "./")
254 try:
255 # first try the branch
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030256 clone = Repo.clone_from(pkg_repo, pkg_dir, branch=branch)
257 repo_inst = clone.working_dir
Patrick Williamse08ffba2022-12-05 10:33:46 -0600258 except Exception:
Andrew Geisslera61acb52019-01-03 16:32:44 -0600259 printline("Input branch not found, default to master")
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030260 clone = Repo.clone_from(pkg_repo, pkg_dir, branch="master")
261 repo_inst = clone.working_dir
Andrew Geisslera61acb52019-01-03 16:32:44 -0600262 return repo_inst
Matthew Barth33df8792016-12-19 14:30:17 -0600263
264
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030265def make_target_exists(target):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800266 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030267 Runs a check against the makefile in the current directory to determine
268 if the target exists so that it can be built.
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800269
270 Parameter descriptions:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030271 target The make target we are checking
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800272 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030273 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600274 cmd = ["make", "-n", target]
275 with open(os.devnull, "w") as devnull:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030276 check_call(cmd, stdout=devnull, stderr=devnull)
277 return True
278 except CalledProcessError:
279 return False
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800280
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800281
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700282make_parallel = [
Patrick Williamse08ffba2022-12-05 10:33:46 -0600283 "make",
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700284 # Run enough jobs to saturate all the cpus
Patrick Williamse08ffba2022-12-05 10:33:46 -0600285 "-j",
286 str(multiprocessing.cpu_count()),
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700287 # Don't start more jobs if the load avg is too high
Patrick Williamse08ffba2022-12-05 10:33:46 -0600288 "-l",
289 str(multiprocessing.cpu_count()),
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700290 # Synchronize the output so logs aren't intermixed in stdout / stderr
Patrick Williamse08ffba2022-12-05 10:33:46 -0600291 "-O",
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700292]
293
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800294
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030295def build_and_install(name, build_for_testing=False):
William A. Kennington III780ec092018-12-06 14:46:50 -0800296 """
297 Builds and installs the package in the environment. Optionally
298 builds the examples and test cases for package.
299
300 Parameter description:
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030301 name The name of the package we are building
William A. Kennington IIIa0454912018-12-06 14:47:16 -0800302 build_for_testing Enable options related to testing on the package?
William A. Kennington III780ec092018-12-06 14:46:50 -0800303 """
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030304 os.chdir(os.path.join(WORKSPACE, name))
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800305
306 # Refresh dynamic linker run time bindings for dependencies
Patrick Williamse08ffba2022-12-05 10:33:46 -0600307 check_call_cmd("sudo", "-n", "--", "ldconfig")
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800308
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030309 pkg = Package()
310 if build_for_testing:
311 pkg.test()
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800312 else:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030313 pkg.install()
314
William A. Kennington III780ec092018-12-06 14:46:50 -0800315
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030316def build_dep_tree(name, pkgdir, dep_added, head, branch, dep_tree=None):
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500317 """
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030318 For each package (name), starting with the package to be unit tested,
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030319 extract its dependencies. For each package dependency defined, recursively
320 apply the same strategy
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500321
322 Parameter descriptions:
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030323 name Name of the package
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500324 pkgdir Directory where package source is located
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800325 dep_added Current dict of dependencies and added status
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500326 head Head node of the dependency tree
Andrew Geisslera61acb52019-01-03 16:32:44 -0600327 branch Branch to clone from pkg
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500328 dep_tree Current dependency tree node
329 """
330 if not dep_tree:
331 dep_tree = head
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800332
William A. Kennington IIIbe6aab22018-12-06 15:01:54 -0800333 with open("/tmp/depcache", "r") as depcache:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800334 cache = depcache.readline()
335
336 # Read out pkg dependencies
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030337 pkg = Package(name, pkgdir)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800338
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600339 build = pkg.build_system()
Patrick Williamse08ffba2022-12-05 10:33:46 -0600340 if not build:
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600341 raise Exception(f"Unable to find build system for {name}.")
342
343 for dep in set(build.dependencies()):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800344 if dep in cache:
345 continue
346 # Dependency package not already known
347 if dep_added.get(dep) is None:
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600348 print(f"Adding {dep} dependency to {name}.")
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800349 # Dependency package not added
350 new_child = dep_tree.AddChild(dep)
351 dep_added[dep] = False
Andrew Jeffery3b92fdd2020-03-13 11:49:18 +1030352 dep_pkgdir = clone_pkg(dep, branch)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800353 # Determine this dependency package's
354 # dependencies and add them before
355 # returning to add this package
Patrick Williamse08ffba2022-12-05 10:33:46 -0600356 dep_added = build_dep_tree(
357 dep, dep_pkgdir, dep_added, head, branch, new_child
358 )
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800359 else:
360 # Dependency package known and added
361 if dep_added[dep]:
Andrew Jeffery2cb0c7a2018-03-08 13:19:08 +1030362 continue
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500363 else:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800364 # Cyclic dependency failure
Patrick Williamse08ffba2022-12-05 10:33:46 -0600365 raise Exception("Cyclic dependencies found in " + name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500366
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030367 if not dep_added[name]:
368 dep_added[name] = True
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500369
370 return dep_added
Matthew Barthccb7f852016-11-23 17:43:02 -0600371
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700372
William A. Kennington III90b106a2019-02-06 18:08:24 -0800373def run_cppcheck():
Ed Tanousc7198552022-07-01 08:15:50 -0700374 if not os.path.exists(os.path.join("build", "compile_commands.json")):
Brad Bishop48424d42020-01-07 13:01:31 -0500375 return None
376
Patrick Williams485a0922022-08-12 11:44:55 -0500377 with TemporaryDirectory() as cpp_dir:
Patrick Williams485a0922022-08-12 11:44:55 -0500378 # http://cppcheck.sourceforge.net/manual.pdf
379 try:
380 check_call_cmd(
Patrick Williamse08ffba2022-12-05 10:33:46 -0600381 "cppcheck",
382 "-j",
383 str(multiprocessing.cpu_count()),
384 "--enable=style,performance,portability,missingInclude",
385 "--suppress=useStlAlgorithm",
386 "--suppress=unusedStructMember",
387 "--suppress=postfixOperator",
388 "--suppress=unreadVariable",
389 "--suppress=knownConditionTrueFalse",
390 "--library=googletest",
391 "--project=build/compile_commands.json",
392 f"--cppcheck-build-dir={cpp_dir}",
Patrick Williams485a0922022-08-12 11:44:55 -0500393 )
394 except subprocess.CalledProcessError:
395 print("cppcheck found errors")
Lei YUdbd7cd62022-07-19 19:24:01 +0800396
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030397
William A. Kennington III37a89a22018-12-13 14:32:02 -0800398def is_valgrind_safe():
399 """
400 Returns whether it is safe to run valgrind on our platform
401 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600402 src = "unit-test-vg.c"
403 exe = "./unit-test-vg"
404 with open(src, "w") as h:
405 h.write("#include <errno.h>\n")
406 h.write("#include <stdio.h>\n")
407 h.write("#include <stdlib.h>\n")
408 h.write("#include <string.h>\n")
409 h.write("int main() {\n")
410 h.write("char *heap_str = malloc(16);\n")
William A. Kennington III0326ded2019-02-07 00:33:28 -0800411 h.write('strcpy(heap_str, "RandString");\n')
412 h.write('int res = strcmp("RandString", heap_str);\n')
Patrick Williamse08ffba2022-12-05 10:33:46 -0600413 h.write("free(heap_str);\n")
414 h.write("char errstr[64];\n")
415 h.write("strerror_r(EINVAL, errstr, sizeof(errstr));\n")
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700416 h.write('printf("%s\\n", errstr);\n')
Patrick Williamse08ffba2022-12-05 10:33:46 -0600417 h.write("return res;\n")
418 h.write("}\n")
William A. Kennington III0326ded2019-02-07 00:33:28 -0800419 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600420 with open(os.devnull, "w") as devnull:
421 check_call(
422 ["gcc", "-O2", "-o", exe, src], stdout=devnull, stderr=devnull
423 )
424 check_call(
425 ["valgrind", "--error-exitcode=99", exe],
426 stdout=devnull,
427 stderr=devnull,
428 )
William A. Kennington III0326ded2019-02-07 00:33:28 -0800429 return True
Patrick Williamse08ffba2022-12-05 10:33:46 -0600430 except Exception:
William A. Kennington III0326ded2019-02-07 00:33:28 -0800431 sys.stderr.write("###### Platform is not valgrind safe ######\n")
432 return False
433 finally:
434 os.remove(src)
435 os.remove(exe)
William A. Kennington III37a89a22018-12-13 14:32:02 -0800436
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030437
William A. Kennington III282e3302019-02-04 16:55:05 -0800438def is_sanitize_safe():
439 """
440 Returns whether it is safe to run sanitizers on our platform
441 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600442 src = "unit-test-sanitize.c"
443 exe = "./unit-test-sanitize"
444 with open(src, "w") as h:
445 h.write("int main() { return 0; }\n")
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800446 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600447 with open(os.devnull, "w") as devnull:
448 check_call(
449 [
450 "gcc",
451 "-O2",
452 "-fsanitize=address",
453 "-fsanitize=undefined",
454 "-o",
455 exe,
456 src,
457 ],
458 stdout=devnull,
459 stderr=devnull,
460 )
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800461 check_call([exe], stdout=devnull, stderr=devnull)
Andrew Geisslercd9578b2021-05-04 10:18:01 -0500462
463 # TODO - Sanitizer not working on ppc64le
464 # https://github.com/openbmc/openbmc-build-scripts/issues/31
Patrick Williamse08ffba2022-12-05 10:33:46 -0600465 if platform.processor() == "ppc64le":
Andrew Geisslercd9578b2021-05-04 10:18:01 -0500466 sys.stderr.write("###### ppc64le is not sanitize safe ######\n")
467 return False
468 else:
469 return True
Patrick Williamse08ffba2022-12-05 10:33:46 -0600470 except Exception:
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800471 sys.stderr.write("###### Platform is not sanitize safe ######\n")
472 return False
473 finally:
474 os.remove(src)
475 os.remove(exe)
William A. Kennington III282e3302019-02-04 16:55:05 -0800476
William A. Kennington III49d4e592019-02-06 17:59:27 -0800477
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800478def maybe_make_valgrind():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700479 """
480 Potentially runs the unit tests through valgrind for the package
481 via `make check-valgrind`. If the package does not have valgrind testing
482 then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700483 """
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700484 # Valgrind testing is currently broken by an aggressive strcmp optimization
485 # that is inlined into optimized code for POWER by gcc 7+. Until we find
486 # a workaround, just don't run valgrind tests on POWER.
487 # https://github.com/openbmc/openbmc/issues/3315
William A. Kennington III37a89a22018-12-13 14:32:02 -0800488 if not is_valgrind_safe():
William A. Kennington III75130192019-02-07 00:34:14 -0800489 sys.stderr.write("###### Skipping valgrind ######\n")
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700490 return
Patrick Williamse08ffba2022-12-05 10:33:46 -0600491 if not make_target_exists("check-valgrind"):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700492 return
493
494 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600495 cmd = make_parallel + ["check-valgrind"]
William A. Kennington III1fddb972019-02-06 18:03:53 -0800496 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700497 except CalledProcessError:
William A. Kennington III90b106a2019-02-06 18:08:24 -0800498 for root, _, files in os.walk(os.getcwd()):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700499 for f in files:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600500 if re.search("test-suite-[a-z]+.log", f) is None:
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700501 continue
Patrick Williamse08ffba2022-12-05 10:33:46 -0600502 check_call_cmd("cat", os.path.join(root, f))
503 raise Exception("Valgrind tests failed")
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700504
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030505
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800506def maybe_make_coverage():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700507 """
508 Potentially runs the unit tests through code coverage for the package
509 via `make check-code-coverage`. If the package does not have code coverage
510 testing then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700511 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600512 if not make_target_exists("check-code-coverage"):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700513 return
514
515 # Actually run code coverage
516 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600517 cmd = make_parallel + ["check-code-coverage"]
William A. Kennington III1fddb972019-02-06 18:03:53 -0800518 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700519 except CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600520 raise Exception("Code coverage failed")
Matthew Barthccb7f852016-11-23 17:43:02 -0600521
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030522
523class BuildSystem(object):
524 """
525 Build systems generally provide the means to configure, build, install and
526 test software. The BuildSystem class defines a set of interfaces on top of
527 which Autotools, Meson, CMake and possibly other build system drivers can
528 be implemented, separating out the phases to control whether a package
529 should merely be installed or also tested and analyzed.
530 """
Lei YU08d2b922022-04-25 11:21:36 +0800531
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030532 def __init__(self, package, path):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600533 """Initialise the driver with properties independent of the build
534 system
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030535
536 Keyword arguments:
537 package: The name of the package. Derived from the path if None
538 path: The path to the package. Set to the working directory if None
539 """
540 self.path = "." if not path else path
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030541 realpath = os.path.realpath(self.path)
542 self.package = package if package else os.path.basename(realpath)
Andrew Jeffery3f8e5a72020-03-13 11:47:56 +1030543 self.build_for_testing = False
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030544
545 def probe(self):
546 """Test if the build system driver can be applied to the package
547
548 Return True if the driver can drive the package's build system,
549 otherwise False.
550
551 Generally probe() is implemented by testing for the presence of the
552 build system's configuration file(s).
553 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600554 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030555
556 def dependencies(self):
557 """Provide the package's dependencies
558
559 Returns a list of dependencies. If no dependencies are required then an
560 empty list must be returned.
561
562 Generally dependencies() is implemented by analysing and extracting the
563 data from the build system configuration.
564 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600565 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030566
567 def configure(self, build_for_testing):
568 """Configure the source ready for building
569
570 Should raise an exception if configuration failed.
571
572 Keyword arguments:
573 build_for_testing: Mark the package as being built for testing rather
574 than for installation as a dependency for the
575 package under test. Setting to True generally
576 implies that the package will be configured to build
577 with debug information, at a low level of
578 optimisation and possibly with sanitizers enabled.
579
580 Generally configure() is implemented by invoking the build system
581 tooling to generate Makefiles or equivalent.
582 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600583 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030584
585 def build(self):
586 """Build the software ready for installation and/or testing
587
588 Should raise an exception if the build fails
589
590 Generally build() is implemented by invoking `make` or `ninja`.
591 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600592 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030593
594 def install(self):
595 """Install the software ready for use
596
597 Should raise an exception if installation fails
598
599 Like build(), install() is generally implemented by invoking `make` or
600 `ninja`.
601 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600602 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030603
604 def test(self):
605 """Build and run the test suite associated with the package
606
607 Should raise an exception if the build or testing fails.
608
609 Like install(), test() is generally implemented by invoking `make` or
610 `ninja`.
611 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600612 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030613
614 def analyze(self):
615 """Run any supported analysis tools over the codebase
616
617 Should raise an exception if analysis fails.
618
619 Some analysis tools such as scan-build need injection into the build
620 system. analyze() provides the necessary hook to implement such
621 behaviour. Analyzers independent of the build system can also be
622 specified here but at the cost of possible duplication of code between
623 the build system driver implementations.
624 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600625 raise NotImplementedError
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030626
627
628class Autotools(BuildSystem):
629 def __init__(self, package=None, path=None):
630 super(Autotools, self).__init__(package, path)
631
632 def probe(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600633 return os.path.isfile(os.path.join(self.path, "configure.ac"))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030634
635 def dependencies(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600636 configure_ac = os.path.join(self.path, "configure.ac")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030637
Patrick Williamse08ffba2022-12-05 10:33:46 -0600638 contents = ""
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030639 # Prepend some special function overrides so we can parse out
640 # dependencies
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030641 for macro in DEPENDENCIES.keys():
Patrick Williamse08ffba2022-12-05 10:33:46 -0600642 contents += (
643 "m4_define(["
644 + macro
645 + "], ["
646 + macro
647 + "_START$"
648 + str(DEPENDENCIES_OFFSET[macro] + 1)
649 + macro
650 + "_END])\n"
651 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030652 with open(configure_ac, "rt") as f:
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030653 contents += f.read()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030654
Patrick Williamse08ffba2022-12-05 10:33:46 -0600655 autoconf_cmdline = ["autoconf", "-Wno-undefined", "-"]
656 autoconf_process = subprocess.Popen(
657 autoconf_cmdline,
658 stdin=subprocess.PIPE,
659 stdout=subprocess.PIPE,
660 stderr=subprocess.PIPE,
661 )
662 document = contents.encode("utf-8")
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030663 (stdout, stderr) = autoconf_process.communicate(input=document)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030664 if not stdout:
665 print(stderr)
666 raise Exception("Failed to run autoconf for parsing dependencies")
667
668 # Parse out all of the dependency text
669 matches = []
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030670 for macro in DEPENDENCIES.keys():
Patrick Williamse08ffba2022-12-05 10:33:46 -0600671 pattern = "(" + macro + ")_START(.*?)" + macro + "_END"
672 for match in re.compile(pattern).finditer(stdout.decode("utf-8")):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030673 matches.append((match.group(1), match.group(2)))
674
675 # Look up dependencies from the text
676 found_deps = []
677 for macro, deptext in matches:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600678 for potential_dep in deptext.split(" "):
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030679 for known_dep in DEPENDENCIES[macro].keys():
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030680 if potential_dep.startswith(known_dep):
681 found_deps.append(DEPENDENCIES[macro][known_dep])
682
683 return found_deps
684
685 def _configure_feature(self, flag, enabled):
686 """
687 Returns an configure flag as a string
688
689 Parameters:
690 flag The name of the flag
691 enabled Whether the flag is enabled or disabled
692 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600693 return "--" + ("enable" if enabled else "disable") + "-" + flag
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030694
695 def configure(self, build_for_testing):
696 self.build_for_testing = build_for_testing
697 conf_flags = [
Patrick Williamse08ffba2022-12-05 10:33:46 -0600698 self._configure_feature("silent-rules", False),
699 self._configure_feature("examples", build_for_testing),
700 self._configure_feature("tests", build_for_testing),
701 self._configure_feature("itests", INTEGRATION_TEST),
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030702 ]
Patrick Williamse08ffba2022-12-05 10:33:46 -0600703 conf_flags.extend(
704 [
705 self._configure_feature("code-coverage", build_for_testing),
706 self._configure_feature("valgrind", build_for_testing),
707 ]
708 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030709 # Add any necessary configure flags for package
710 if CONFIGURE_FLAGS.get(self.package) is not None:
711 conf_flags.extend(CONFIGURE_FLAGS.get(self.package))
Patrick Williamse08ffba2022-12-05 10:33:46 -0600712 for bootstrap in ["bootstrap.sh", "bootstrap", "autogen.sh"]:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030713 if os.path.exists(bootstrap):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600714 check_call_cmd("./" + bootstrap)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030715 break
Patrick Williamse08ffba2022-12-05 10:33:46 -0600716 check_call_cmd("./configure", *conf_flags)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030717
718 def build(self):
719 check_call_cmd(*make_parallel)
720
721 def install(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600722 check_call_cmd("sudo", "-n", "--", *(make_parallel + ["install"]))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030723
724 def test(self):
725 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600726 cmd = make_parallel + ["check"]
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030727 for i in range(0, args.repeat):
728 check_call_cmd(*cmd)
Andrew Jefferyd0809692021-05-14 16:23:57 +0930729
730 maybe_make_valgrind()
731 maybe_make_coverage()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030732 except CalledProcessError:
733 for root, _, files in os.walk(os.getcwd()):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600734 if "test-suite.log" not in files:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030735 continue
Patrick Williamse08ffba2022-12-05 10:33:46 -0600736 check_call_cmd("cat", os.path.join(root, "test-suite.log"))
737 raise Exception("Unit tests failed")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030738
739 def analyze(self):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030740 run_cppcheck()
741
742
743class CMake(BuildSystem):
744 def __init__(self, package=None, path=None):
745 super(CMake, self).__init__(package, path)
746
747 def probe(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600748 return os.path.isfile(os.path.join(self.path, "CMakeLists.txt"))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030749
750 def dependencies(self):
751 return []
752
753 def configure(self, build_for_testing):
754 self.build_for_testing = build_for_testing
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000755 if INTEGRATION_TEST:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600756 check_call_cmd(
757 "cmake",
758 "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
759 "-DITESTS=ON",
760 ".",
761 )
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000762 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600763 check_call_cmd("cmake", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", ".")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030764
765 def build(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600766 check_call_cmd(
767 "cmake",
768 "--build",
769 ".",
770 "--",
771 "-j",
772 str(multiprocessing.cpu_count()),
773 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030774
775 def install(self):
776 pass
777
778 def test(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600779 if make_target_exists("test"):
780 check_call_cmd("ctest", ".")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030781
782 def analyze(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600783 if os.path.isfile(".clang-tidy"):
784 with TemporaryDirectory(prefix="build", dir=".") as build_dir:
Nan Zhou82aaba02022-09-16 00:21:07 +0000785 # clang-tidy needs to run on a clang-specific build
Patrick Williamse08ffba2022-12-05 10:33:46 -0600786 check_call_cmd(
787 "cmake",
788 "-DCMAKE_C_COMPILER=clang",
789 "-DCMAKE_CXX_COMPILER=clang++",
790 "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
791 "-H.",
792 "-B" + build_dir,
793 )
Nan Zhou524ebee2022-08-27 23:26:13 +0000794
Patrick Williamse08ffba2022-12-05 10:33:46 -0600795 check_call_cmd(
796 "run-clang-tidy", "-header-filter=.*", "-p", build_dir
797 )
Ed Tanous662890f2020-09-28 17:35:20 -0700798
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030799 maybe_make_valgrind()
800 maybe_make_coverage()
801 run_cppcheck()
802
803
804class Meson(BuildSystem):
805 def __init__(self, package=None, path=None):
806 super(Meson, self).__init__(package, path)
807
808 def probe(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600809 return os.path.isfile(os.path.join(self.path, "meson.build"))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030810
811 def dependencies(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600812 meson_build = os.path.join(self.path, "meson.build")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030813 if not os.path.exists(meson_build):
814 return []
815
816 found_deps = []
817 for root, dirs, files in os.walk(self.path):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600818 if "meson.build" not in files:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030819 continue
Patrick Williamse08ffba2022-12-05 10:33:46 -0600820 with open(os.path.join(root, "meson.build"), "rt") as f:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030821 build_contents = f.read()
Nan Zhouef13d532020-07-07 09:52:02 -0700822 pattern = r"dependency\('([^']*)'.*?\),?\n"
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030823 for match in re.finditer(pattern, build_contents):
824 group = match.group(1)
Patrick Williamse08ffba2022-12-05 10:33:46 -0600825 maybe_dep = DEPENDENCIES["PKG_CHECK_MODULES"].get(group)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030826 if maybe_dep is not None:
827 found_deps.append(maybe_dep)
828
829 return found_deps
830
831 def _parse_options(self, options_file):
832 """
833 Returns a set of options defined in the provides meson_options.txt file
834
835 Parameters:
836 options_file The file containing options
837 """
Patrick Williamse08ffba2022-12-05 10:33:46 -0600838 oi = optinterpreter.OptionInterpreter("")
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700839 oi.process(options_file)
840 return oi.options
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030841
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700842 def _configure_boolean(self, val):
843 """
844 Returns the meson flag which signifies the value
845
846 True is true which requires the boolean.
847 False is false which disables the boolean.
848
849 Parameters:
850 val The value being converted
851 """
852 if val is True:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600853 return "true"
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700854 elif val is False:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600855 return "false"
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700856 else:
857 raise Exception("Bad meson boolean value")
858
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030859 def _configure_feature(self, val):
860 """
861 Returns the meson flag which signifies the value
862
863 True is enabled which requires the feature.
864 False is disabled which disables the feature.
865 None is auto which autodetects the feature.
866
867 Parameters:
868 val The value being converted
869 """
870 if val is True:
871 return "enabled"
872 elif val is False:
873 return "disabled"
874 elif val is None:
875 return "auto"
876 else:
877 raise Exception("Bad meson feature value")
878
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700879 def _configure_option(self, opts, key, val):
880 """
881 Returns the meson flag which signifies the value
882 based on the type of the opt
883
884 Parameters:
885 opt The meson option which we are setting
886 val The value being converted
887 """
888 if isinstance(opts[key], coredata.UserBooleanOption):
889 str_val = self._configure_boolean(val)
890 elif isinstance(opts[key], coredata.UserFeatureOption):
891 str_val = self._configure_feature(val)
892 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600893 raise Exception("Unknown meson option type")
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700894 return "-D{}={}".format(key, str_val)
895
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030896 def configure(self, build_for_testing):
897 self.build_for_testing = build_for_testing
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700898 meson_options = {}
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030899 if os.path.exists("meson_options.txt"):
900 meson_options = self._parse_options("meson_options.txt")
901 meson_flags = [
Patrick Williamse08ffba2022-12-05 10:33:46 -0600902 "-Db_colorout=never",
903 "-Dwerror=true",
904 "-Dwarning_level=3",
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030905 ]
906 if build_for_testing:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600907 meson_flags.append("--buildtype=debug")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030908 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600909 meson_flags.append("--buildtype=debugoptimized")
910 if OptionKey("tests") in meson_options:
911 meson_flags.append(
912 self._configure_option(
913 meson_options, OptionKey("tests"), build_for_testing
914 )
915 )
916 if OptionKey("examples") in meson_options:
917 meson_flags.append(
918 self._configure_option(
919 meson_options, OptionKey("examples"), build_for_testing
920 )
921 )
922 if OptionKey("itests") in meson_options:
923 meson_flags.append(
924 self._configure_option(
925 meson_options, OptionKey("itests"), INTEGRATION_TEST
926 )
927 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030928 if MESON_FLAGS.get(self.package) is not None:
929 meson_flags.extend(MESON_FLAGS.get(self.package))
930 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600931 check_call_cmd(
932 "meson", "setup", "--reconfigure", "build", *meson_flags
933 )
934 except Exception:
935 shutil.rmtree("build", ignore_errors=True)
936 check_call_cmd("meson", "setup", "build", *meson_flags)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030937
938 def build(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600939 check_call_cmd("ninja", "-C", "build")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030940
941 def install(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -0600942 check_call_cmd("sudo", "-n", "--", "ninja", "-C", "build", "install")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030943
944 def test(self):
Patrick Williams95095f12021-04-14 14:42:35 -0500945 # It is useful to check various settings of the meson.build file
946 # for compatibility, such as meson_version checks. We shouldn't
947 # do this in the configure path though because it affects subprojects
948 # and dependencies as well, but we only want this applied to the
949 # project-under-test (otherwise an upstream dependency could fail
950 # this check without our control).
951 self._extra_meson_checks()
952
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030953 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600954 test_args = ("--repeat", str(args.repeat), "-C", "build")
955 check_call_cmd("meson", "test", "--print-errorlogs", *test_args)
Brad Bishop7b8cef22020-08-26 15:58:09 -0400956
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030957 except CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600958 raise Exception("Unit tests failed")
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030959
960 def _setup_exists(self, setup):
961 """
962 Returns whether the meson build supports the named test setup.
963
964 Parameter descriptions:
965 setup The setup target to check
966 """
967 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600968 with open(os.devnull, "w"):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030969 output = subprocess.check_output(
Patrick Williamse08ffba2022-12-05 10:33:46 -0600970 [
971 "meson",
972 "test",
973 "-C",
974 "build",
975 "--setup",
976 setup,
977 "-t",
978 "0",
979 ],
980 stderr=subprocess.STDOUT,
981 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030982 except CalledProcessError as e:
983 output = e.output
Patrick Williamse08ffba2022-12-05 10:33:46 -0600984 output = output.decode("utf-8")
985 return not re.search("Test setup .* not found from project", output)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030986
987 def _maybe_valgrind(self):
988 """
989 Potentially runs the unit tests through valgrind for the package
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030990 via `meson test`. The package can specify custom valgrind
991 configurations by utilizing add_test_setup() in a meson.build
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030992 """
993 if not is_valgrind_safe():
994 sys.stderr.write("###### Skipping valgrind ######\n")
995 return
996 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -0600997 if self._setup_exists("valgrind"):
998 check_call_cmd(
999 "meson",
1000 "test",
1001 "-t",
1002 "10",
1003 "-C",
1004 "build",
1005 "--print-errorlogs",
1006 "--setup",
1007 "valgrind",
1008 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301009 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001010 check_call_cmd(
1011 "meson",
1012 "test",
1013 "-t",
1014 "10",
1015 "-C",
1016 "build",
1017 "--print-errorlogs",
1018 "--wrapper",
1019 "valgrind",
1020 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301021 except CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001022 raise Exception("Valgrind tests failed")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301023
1024 def analyze(self):
1025 self._maybe_valgrind()
1026
1027 # Run clang-tidy only if the project has a configuration
Patrick Williamse08ffba2022-12-05 10:33:46 -06001028 if os.path.isfile(".clang-tidy"):
Manojkiran Eda1aa91992020-10-02 14:11:53 +05301029 os.environ["CXX"] = "clang++"
Patrick Williamse08ffba2022-12-05 10:33:46 -06001030 with TemporaryDirectory(prefix="build", dir=".") as build_dir:
1031 check_call_cmd("meson", "setup", build_dir)
Nan Zhou82aaba02022-09-16 00:21:07 +00001032 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001033 check_call_cmd(
1034 "run-clang-tidy", "-fix", "-format", "-p", build_dir
1035 )
Nan Zhou82aaba02022-09-16 00:21:07 +00001036 except subprocess.CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001037 check_call_cmd(
1038 "git", "-C", CODE_SCAN_DIR, "--no-pager", "diff"
1039 )
Nan Zhou82aaba02022-09-16 00:21:07 +00001040 raise
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301041 # Run the basic clang static analyzer otherwise
1042 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001043 check_call_cmd("ninja", "-C", "build", "scan-build")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301044
1045 # Run tests through sanitizers
1046 # b_lundef is needed if clang++ is CXX since it resolves the
1047 # asan symbols at runtime only. We don't want to set it earlier
1048 # in the build process to ensure we don't have undefined
1049 # runtime code.
1050 if is_sanitize_safe():
Patrick Williamse08ffba2022-12-05 10:33:46 -06001051 check_call_cmd(
1052 "meson",
1053 "configure",
1054 "build",
1055 "-Db_sanitize=address,undefined",
1056 "-Db_lundef=false",
1057 )
1058 check_call_cmd(
1059 "meson",
1060 "test",
1061 "-C",
1062 "build",
1063 "--print-errorlogs",
1064 "--logbase",
1065 "testlog-ubasan",
1066 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301067 # TODO: Fix memory sanitizer
Andrew Jeffery6015ca12020-03-13 11:50:10 +10301068 # check_call_cmd('meson', 'configure', 'build',
1069 # '-Db_sanitize=memory')
1070 # check_call_cmd('meson', 'test', '-C', 'build'
1071 # '--logbase', 'testlog-msan')
Patrick Williamse08ffba2022-12-05 10:33:46 -06001072 check_call_cmd("meson", "configure", "build", "-Db_sanitize=none")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301073 else:
1074 sys.stderr.write("###### Skipping sanitizers ######\n")
1075
1076 # Run coverage checks
Patrick Williamse08ffba2022-12-05 10:33:46 -06001077 check_call_cmd("meson", "configure", "build", "-Db_coverage=true")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301078 self.test()
1079 # Only build coverage HTML if coverage files were produced
Patrick Williamse08ffba2022-12-05 10:33:46 -06001080 for root, dirs, files in os.walk("build"):
1081 if any([f.endswith(".gcda") for f in files]):
1082 check_call_cmd("ninja", "-C", "build", "coverage-html")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301083 break
Patrick Williamse08ffba2022-12-05 10:33:46 -06001084 check_call_cmd("meson", "configure", "build", "-Db_coverage=false")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301085 run_cppcheck()
1086
Patrick Williams95095f12021-04-14 14:42:35 -05001087 def _extra_meson_checks(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001088 with open(os.path.join(self.path, "meson.build"), "rt") as f:
Patrick Williams95095f12021-04-14 14:42:35 -05001089 build_contents = f.read()
1090
1091 # Find project's specified meson_version.
1092 meson_version = None
1093 pattern = r"meson_version:[^']*'([^']*)'"
1094 for match in re.finditer(pattern, build_contents):
1095 group = match.group(1)
1096 meson_version = group
1097
1098 # C++20 requires at least Meson 0.57 but Meson itself doesn't
1099 # identify this. Add to our unit-test checks so that we don't
1100 # get a meson.build missing this.
1101 pattern = r"'cpp_std=c\+\+20'"
1102 for match in re.finditer(pattern, build_contents):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001103 if not meson_version or not meson_version_compare(
1104 meson_version, ">=0.57"
1105 ):
Patrick Williams95095f12021-04-14 14:42:35 -05001106 raise Exception(
1107 "C++20 support requires specifying in meson.build: "
1108 + "meson_version: '>=0.57'"
1109 )
1110
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301111
1112class Package(object):
1113 def __init__(self, name=None, path=None):
Manojkiran Eda29d28252020-06-15 18:58:09 +05301114 self.supported = [Meson, Autotools, CMake]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301115 self.name = name
1116 self.path = path
1117 self.test_only = False
1118
1119 def build_systems(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +10301120 instances = (system(self.name, self.path) for system in self.supported)
1121 return (instance for instance in instances if instance.probe())
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301122
1123 def build_system(self, preferred=None):
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001124 systems = list(self.build_systems())
1125
1126 if not systems:
1127 return None
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301128
1129 if preferred:
Andrew Jeffery881041f2020-03-13 11:46:30 +10301130 return {type(system): system for system in systems}[preferred]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301131
1132 return next(iter(systems))
1133
1134 def install(self, system=None):
1135 if not system:
1136 system = self.build_system()
1137
1138 system.configure(False)
1139 system.build()
1140 system.install()
1141
Andrew Jeffery19d75672020-03-13 10:42:08 +10301142 def _test_one(self, system):
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301143 system.configure(True)
1144 system.build()
1145 system.install()
1146 system.test()
Andrew Jefferyd0809692021-05-14 16:23:57 +09301147 if not TEST_ONLY:
1148 system.analyze()
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301149
Andrew Jeffery19d75672020-03-13 10:42:08 +10301150 def test(self):
1151 for system in self.build_systems():
1152 self._test_one(system)
1153
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301154
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001155def find_file(filename, basedir):
1156 """
Patrick Williams55448ad2020-12-14 14:28:28 -06001157 Finds all occurrences of a file (or list of files) in the base
1158 directory and passes them back with their relative paths.
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001159
1160 Parameter descriptions:
Patrick Williams55448ad2020-12-14 14:28:28 -06001161 filename The name of the file (or list of files) to
1162 find
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001163 basedir The base directory search in
1164 """
1165
Patrick Williams55448ad2020-12-14 14:28:28 -06001166 if not isinstance(filename, list):
Lei YU08d2b922022-04-25 11:21:36 +08001167 filename = [filename]
Patrick Williams55448ad2020-12-14 14:28:28 -06001168
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001169 filepaths = []
1170 for root, dirs, files in os.walk(basedir):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001171 if os.path.split(root)[-1] == "subprojects":
Brad Bishopeb667262021-08-06 13:54:58 -04001172 for f in files:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001173 subproject = ".".join(f.split(".")[0:-1])
1174 if f.endswith(".wrap") and subproject in dirs:
Brad Bishopeb667262021-08-06 13:54:58 -04001175 # don't find files in meson subprojects with wraps
1176 dirs.remove(subproject)
Patrick Williams55448ad2020-12-14 14:28:28 -06001177 for f in filename:
1178 if f in files:
1179 filepaths.append(os.path.join(root, f))
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001180 return filepaths
1181
Andrew Jeffery9cc97d82020-03-13 11:57:44 +10301182
Patrick Williamse08ffba2022-12-05 10:33:46 -06001183if __name__ == "__main__":
Matthew Barthccb7f852016-11-23 17:43:02 -06001184 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
1185 CONFIGURE_FLAGS = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001186 "phosphor-logging": [
1187 "--enable-metadata-processing",
1188 "--enable-openpower-pel-extension",
1189 "YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml",
1190 ]
Matthew Barthccb7f852016-11-23 17:43:02 -06001191 }
1192
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001193 # MESON_FLAGS = [GIT REPO]:[MESON FLAGS]
1194 MESON_FLAGS = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001195 "phosphor-dbus-interfaces": [
1196 "-Ddata_com_ibm=true",
1197 "-Ddata_org_open_power=true",
1198 ],
1199 "phosphor-logging": ["-Dopenpower-pel-extension=enabled"],
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001200 }
1201
Matthew Barthccb7f852016-11-23 17:43:02 -06001202 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
1203 DEPENDENCIES = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001204 "AC_CHECK_LIB": {"mapper": "phosphor-objmgr"},
1205 "AC_CHECK_HEADER": {
1206 "host-ipmid": "phosphor-host-ipmid",
1207 "blobs-ipmid": "phosphor-ipmi-blobs",
1208 "sdbusplus": "sdbusplus",
1209 "sdeventplus": "sdeventplus",
1210 "stdplus": "stdplus",
1211 "gpioplus": "gpioplus",
1212 "phosphor-logging/log.hpp": "phosphor-logging",
Patrick Williamseab8a372017-01-30 11:21:32 -06001213 },
Patrick Williamse08ffba2022-12-05 10:33:46 -06001214 "AC_PATH_PROG": {"sdbus++": "sdbusplus"},
1215 "PKG_CHECK_MODULES": {
1216 "phosphor-dbus-interfaces": "phosphor-dbus-interfaces",
1217 "libipmid": "phosphor-host-ipmid",
1218 "libipmid-host": "phosphor-host-ipmid",
1219 "sdbusplus": "sdbusplus",
1220 "sdeventplus": "sdeventplus",
1221 "stdplus": "stdplus",
1222 "gpioplus": "gpioplus",
1223 "phosphor-logging": "phosphor-logging",
1224 "phosphor-snmp": "phosphor-snmp",
1225 "ipmiblob": "ipmi-blob-tool",
1226 "hei": "openpower-libhei",
1227 "phosphor-ipmi-blobs": "phosphor-ipmi-blobs",
1228 "libcr51sign": "google-misc",
Brad Bishopebb49112017-02-13 11:07:26 -05001229 },
Matthew Barthccb7f852016-11-23 17:43:02 -06001230 }
1231
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001232 # Offset into array of macro parameters MACRO(0, 1, ...N)
1233 DEPENDENCIES_OFFSET = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001234 "AC_CHECK_LIB": 0,
1235 "AC_CHECK_HEADER": 0,
1236 "AC_PATH_PROG": 1,
1237 "PKG_CHECK_MODULES": 1,
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001238 }
1239
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001240 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
Patrick Williamse08ffba2022-12-05 10:33:46 -06001241 DEPENDENCIES_REGEX = {"phosphor-logging": r"\S+-dbus-interfaces$"}
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001242
Matthew Barth33df8792016-12-19 14:30:17 -06001243 # Set command line arguments
1244 parser = argparse.ArgumentParser()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001245 parser.add_argument(
1246 "-w",
1247 "--workspace",
1248 dest="WORKSPACE",
1249 required=True,
1250 help="Workspace directory location(i.e. /home)",
1251 )
1252 parser.add_argument(
1253 "-p",
1254 "--package",
1255 dest="PACKAGE",
1256 required=True,
1257 help="OpenBMC package to be unit tested",
1258 )
1259 parser.add_argument(
1260 "-t",
1261 "--test-only",
1262 dest="TEST_ONLY",
1263 action="store_true",
1264 required=False,
1265 default=False,
1266 help="Only run test cases, no other validation",
1267 )
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001268 arg_inttests = parser.add_mutually_exclusive_group()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001269 arg_inttests.add_argument(
1270 "--integration-tests",
1271 dest="INTEGRATION_TEST",
1272 action="store_true",
1273 required=False,
1274 default=True,
1275 help="Enable integration tests [default].",
1276 )
1277 arg_inttests.add_argument(
1278 "--no-integration-tests",
1279 dest="INTEGRATION_TEST",
1280 action="store_false",
1281 required=False,
1282 help="Disable integration tests.",
1283 )
1284 parser.add_argument(
1285 "-v",
1286 "--verbose",
1287 action="store_true",
1288 help="Print additional package status messages",
1289 )
1290 parser.add_argument(
1291 "-r", "--repeat", help="Repeat tests N times", type=int, default=1
1292 )
1293 parser.add_argument(
1294 "-b",
1295 "--branch",
1296 dest="BRANCH",
1297 required=False,
1298 help="Branch to target for dependent repositories",
1299 default="master",
1300 )
1301 parser.add_argument(
1302 "-n",
1303 "--noformat",
1304 dest="FORMAT",
1305 action="store_false",
1306 required=False,
1307 help="Whether or not to run format code",
1308 )
Matthew Barth33df8792016-12-19 14:30:17 -06001309 args = parser.parse_args(sys.argv[1:])
1310 WORKSPACE = args.WORKSPACE
1311 UNIT_TEST_PKG = args.PACKAGE
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001312 TEST_ONLY = args.TEST_ONLY
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001313 INTEGRATION_TEST = args.INTEGRATION_TEST
Andrew Geisslera61acb52019-01-03 16:32:44 -06001314 BRANCH = args.BRANCH
Lei YU7ef93302019-11-06 13:53:21 +08001315 FORMAT_CODE = args.FORMAT
Matthew Barth33df8792016-12-19 14:30:17 -06001316 if args.verbose:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001317
Matthew Barth33df8792016-12-19 14:30:17 -06001318 def printline(*line):
1319 for arg in line:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001320 print(arg, end=" ")
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301321 print()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001322
Matthew Barth33df8792016-12-19 14:30:17 -06001323 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001324
Andrew Jefferyfce557f2020-03-13 12:12:19 +10301325 def printline(*line):
1326 pass
Matthew Barthccb7f852016-11-23 17:43:02 -06001327
Patrick Williamsb6535952020-12-15 06:40:10 -06001328 CODE_SCAN_DIR = os.path.join(WORKSPACE, UNIT_TEST_PKG)
Lei YU7ef93302019-11-06 13:53:21 +08001329
Patrick Williams330b0772022-11-28 06:14:06 -06001330 # Run format-code.sh, which will in turn call any repo-level formatters.
Lei YU7ef93302019-11-06 13:53:21 +08001331 if FORMAT_CODE:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001332 check_call_cmd(
1333 os.path.join(
1334 WORKSPACE, "openbmc-build-scripts", "scripts", "format-code.sh"
1335 ),
1336 CODE_SCAN_DIR,
1337 )
Andrew Geisslera28286d2018-01-10 11:00:00 -08001338
Ed Tanous32768b82022-01-05 14:14:06 -08001339 # Check to see if any files changed
Patrick Williamse08ffba2022-12-05 10:33:46 -06001340 check_call_cmd(
1341 "git", "-C", CODE_SCAN_DIR, "--no-pager", "diff", "--exit-code"
1342 )
Ed Tanous32768b82022-01-05 14:14:06 -08001343
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001344 # Check if this repo has a supported make infrastructure
Patrick Williamsb6535952020-12-15 06:40:10 -06001345 pkg = Package(UNIT_TEST_PKG, CODE_SCAN_DIR)
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001346 if not pkg.build_system():
1347 print("No valid build system, exit")
1348 sys.exit(0)
1349
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301350 prev_umask = os.umask(000)
James Feist878df5c2018-07-26 14:54:28 -07001351
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301352 # Determine dependencies and add them
1353 dep_added = dict()
1354 dep_added[UNIT_TEST_PKG] = False
William A. Kennington III40d5c7c2018-12-13 14:37:59 -08001355
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301356 # Create dependency tree
1357 dep_tree = DepTree(UNIT_TEST_PKG)
Patrick Williamsb6535952020-12-15 06:40:10 -06001358 build_dep_tree(UNIT_TEST_PKG, CODE_SCAN_DIR, dep_added, dep_tree, BRANCH)
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001359
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301360 # Reorder Dependency Tree
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301361 for pkg_name, regex_str in DEPENDENCIES_REGEX.items():
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301362 dep_tree.ReorderDeps(pkg_name, regex_str)
1363 if args.verbose:
1364 dep_tree.PrintTree()
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001365
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301366 install_list = dep_tree.GetInstallList()
Andrew Geissler9ced4ed2019-11-18 14:33:58 -06001367
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301368 # We don't want to treat our package as a dependency
1369 install_list.remove(UNIT_TEST_PKG)
James Feist878df5c2018-07-26 14:54:28 -07001370
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301371 # Install reordered dependencies
1372 for dep in install_list:
1373 build_and_install(dep, False)
James Feist878df5c2018-07-26 14:54:28 -07001374
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301375 # Run package unit tests
1376 build_and_install(UNIT_TEST_PKG, True)
James Feist878df5c2018-07-26 14:54:28 -07001377
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301378 os.umask(prev_umask)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001379
1380 # Run any custom CI scripts the repo has, of which there can be
1381 # multiple of and anywhere in the repository.
Patrick Williamse08ffba2022-12-05 10:33:46 -06001382 ci_scripts = find_file(["run-ci.sh", "run-ci"], CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001383 if ci_scripts:
Patrick Williamsb6535952020-12-15 06:40:10 -06001384 os.chdir(CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001385 for ci_script in ci_scripts:
Patrick Williams55448ad2020-12-14 14:28:28 -06001386 check_call_cmd(ci_script)