blob: aaaf92b76b89bd4acbae979fd899a1631cdfb8c2 [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):
777 pass
778
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)
Nan Zhou82aaba02022-09-16 00:21:07 +00001033 try:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001034 check_call_cmd(
1035 "run-clang-tidy", "-fix", "-format", "-p", build_dir
1036 )
Nan Zhou82aaba02022-09-16 00:21:07 +00001037 except subprocess.CalledProcessError:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001038 check_call_cmd(
1039 "git", "-C", CODE_SCAN_DIR, "--no-pager", "diff"
1040 )
Nan Zhou82aaba02022-09-16 00:21:07 +00001041 raise
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301042 # Run the basic clang static analyzer otherwise
1043 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001044 check_call_cmd("ninja", "-C", "build", "scan-build")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301045
1046 # Run tests through sanitizers
1047 # b_lundef is needed if clang++ is CXX since it resolves the
1048 # asan symbols at runtime only. We don't want to set it earlier
1049 # in the build process to ensure we don't have undefined
1050 # runtime code.
1051 if is_sanitize_safe():
Patrick Williamse08ffba2022-12-05 10:33:46 -06001052 check_call_cmd(
1053 "meson",
1054 "configure",
1055 "build",
1056 "-Db_sanitize=address,undefined",
1057 "-Db_lundef=false",
1058 )
1059 check_call_cmd(
1060 "meson",
1061 "test",
1062 "-C",
1063 "build",
1064 "--print-errorlogs",
1065 "--logbase",
1066 "testlog-ubasan",
1067 )
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301068 # TODO: Fix memory sanitizer
Andrew Jeffery6015ca12020-03-13 11:50:10 +10301069 # check_call_cmd('meson', 'configure', 'build',
1070 # '-Db_sanitize=memory')
1071 # check_call_cmd('meson', 'test', '-C', 'build'
1072 # '--logbase', 'testlog-msan')
Patrick Williamse08ffba2022-12-05 10:33:46 -06001073 check_call_cmd("meson", "configure", "build", "-Db_sanitize=none")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301074 else:
1075 sys.stderr.write("###### Skipping sanitizers ######\n")
1076
1077 # Run coverage checks
Patrick Williamse08ffba2022-12-05 10:33:46 -06001078 check_call_cmd("meson", "configure", "build", "-Db_coverage=true")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301079 self.test()
1080 # Only build coverage HTML if coverage files were produced
Patrick Williamse08ffba2022-12-05 10:33:46 -06001081 for root, dirs, files in os.walk("build"):
1082 if any([f.endswith(".gcda") for f in files]):
1083 check_call_cmd("ninja", "-C", "build", "coverage-html")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301084 break
Patrick Williamse08ffba2022-12-05 10:33:46 -06001085 check_call_cmd("meson", "configure", "build", "-Db_coverage=false")
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301086 run_cppcheck()
1087
Patrick Williams95095f12021-04-14 14:42:35 -05001088 def _extra_meson_checks(self):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001089 with open(os.path.join(self.path, "meson.build"), "rt") as f:
Patrick Williams95095f12021-04-14 14:42:35 -05001090 build_contents = f.read()
1091
1092 # Find project's specified meson_version.
1093 meson_version = None
1094 pattern = r"meson_version:[^']*'([^']*)'"
1095 for match in re.finditer(pattern, build_contents):
1096 group = match.group(1)
1097 meson_version = group
1098
1099 # C++20 requires at least Meson 0.57 but Meson itself doesn't
1100 # identify this. Add to our unit-test checks so that we don't
1101 # get a meson.build missing this.
1102 pattern = r"'cpp_std=c\+\+20'"
1103 for match in re.finditer(pattern, build_contents):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001104 if not meson_version or not meson_version_compare(
1105 meson_version, ">=0.57"
1106 ):
Patrick Williams95095f12021-04-14 14:42:35 -05001107 raise Exception(
1108 "C++20 support requires specifying in meson.build: "
1109 + "meson_version: '>=0.57'"
1110 )
1111
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301112
1113class Package(object):
1114 def __init__(self, name=None, path=None):
Manojkiran Eda29d28252020-06-15 18:58:09 +05301115 self.supported = [Meson, Autotools, CMake]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301116 self.name = name
1117 self.path = path
1118 self.test_only = False
1119
1120 def build_systems(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +10301121 instances = (system(self.name, self.path) for system in self.supported)
1122 return (instance for instance in instances if instance.probe())
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301123
1124 def build_system(self, preferred=None):
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001125 systems = list(self.build_systems())
1126
1127 if not systems:
1128 return None
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301129
1130 if preferred:
Andrew Jeffery881041f2020-03-13 11:46:30 +10301131 return {type(system): system for system in systems}[preferred]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301132
1133 return next(iter(systems))
1134
1135 def install(self, system=None):
1136 if not system:
1137 system = self.build_system()
1138
1139 system.configure(False)
1140 system.build()
1141 system.install()
1142
Andrew Jeffery19d75672020-03-13 10:42:08 +10301143 def _test_one(self, system):
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301144 system.configure(True)
1145 system.build()
1146 system.install()
1147 system.test()
Andrew Jefferyd0809692021-05-14 16:23:57 +09301148 if not TEST_ONLY:
1149 system.analyze()
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301150
Andrew Jeffery19d75672020-03-13 10:42:08 +10301151 def test(self):
1152 for system in self.build_systems():
1153 self._test_one(system)
1154
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301155
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001156def find_file(filename, basedir):
1157 """
Patrick Williams55448ad2020-12-14 14:28:28 -06001158 Finds all occurrences of a file (or list of files) in the base
1159 directory and passes them back with their relative paths.
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001160
1161 Parameter descriptions:
Patrick Williams55448ad2020-12-14 14:28:28 -06001162 filename The name of the file (or list of files) to
1163 find
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001164 basedir The base directory search in
1165 """
1166
Patrick Williams55448ad2020-12-14 14:28:28 -06001167 if not isinstance(filename, list):
Lei YU08d2b922022-04-25 11:21:36 +08001168 filename = [filename]
Patrick Williams55448ad2020-12-14 14:28:28 -06001169
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001170 filepaths = []
1171 for root, dirs, files in os.walk(basedir):
Patrick Williamse08ffba2022-12-05 10:33:46 -06001172 if os.path.split(root)[-1] == "subprojects":
Brad Bishopeb667262021-08-06 13:54:58 -04001173 for f in files:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001174 subproject = ".".join(f.split(".")[0:-1])
1175 if f.endswith(".wrap") and subproject in dirs:
Brad Bishopeb667262021-08-06 13:54:58 -04001176 # don't find files in meson subprojects with wraps
1177 dirs.remove(subproject)
Patrick Williams55448ad2020-12-14 14:28:28 -06001178 for f in filename:
1179 if f in files:
1180 filepaths.append(os.path.join(root, f))
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001181 return filepaths
1182
Andrew Jeffery9cc97d82020-03-13 11:57:44 +10301183
Patrick Williamse08ffba2022-12-05 10:33:46 -06001184if __name__ == "__main__":
Matthew Barthccb7f852016-11-23 17:43:02 -06001185 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
1186 CONFIGURE_FLAGS = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001187 "phosphor-logging": [
1188 "--enable-metadata-processing",
1189 "--enable-openpower-pel-extension",
1190 "YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml",
1191 ]
Matthew Barthccb7f852016-11-23 17:43:02 -06001192 }
1193
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001194 # MESON_FLAGS = [GIT REPO]:[MESON FLAGS]
1195 MESON_FLAGS = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001196 "phosphor-dbus-interfaces": [
1197 "-Ddata_com_ibm=true",
1198 "-Ddata_org_open_power=true",
1199 ],
1200 "phosphor-logging": ["-Dopenpower-pel-extension=enabled"],
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001201 }
1202
Matthew Barthccb7f852016-11-23 17:43:02 -06001203 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
1204 DEPENDENCIES = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001205 "AC_CHECK_LIB": {"mapper": "phosphor-objmgr"},
1206 "AC_CHECK_HEADER": {
1207 "host-ipmid": "phosphor-host-ipmid",
1208 "blobs-ipmid": "phosphor-ipmi-blobs",
1209 "sdbusplus": "sdbusplus",
1210 "sdeventplus": "sdeventplus",
1211 "stdplus": "stdplus",
1212 "gpioplus": "gpioplus",
1213 "phosphor-logging/log.hpp": "phosphor-logging",
Patrick Williamseab8a372017-01-30 11:21:32 -06001214 },
Patrick Williamse08ffba2022-12-05 10:33:46 -06001215 "AC_PATH_PROG": {"sdbus++": "sdbusplus"},
1216 "PKG_CHECK_MODULES": {
1217 "phosphor-dbus-interfaces": "phosphor-dbus-interfaces",
1218 "libipmid": "phosphor-host-ipmid",
1219 "libipmid-host": "phosphor-host-ipmid",
1220 "sdbusplus": "sdbusplus",
1221 "sdeventplus": "sdeventplus",
1222 "stdplus": "stdplus",
1223 "gpioplus": "gpioplus",
1224 "phosphor-logging": "phosphor-logging",
1225 "phosphor-snmp": "phosphor-snmp",
1226 "ipmiblob": "ipmi-blob-tool",
1227 "hei": "openpower-libhei",
1228 "phosphor-ipmi-blobs": "phosphor-ipmi-blobs",
1229 "libcr51sign": "google-misc",
Brad Bishopebb49112017-02-13 11:07:26 -05001230 },
Matthew Barthccb7f852016-11-23 17:43:02 -06001231 }
1232
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001233 # Offset into array of macro parameters MACRO(0, 1, ...N)
1234 DEPENDENCIES_OFFSET = {
Patrick Williamse08ffba2022-12-05 10:33:46 -06001235 "AC_CHECK_LIB": 0,
1236 "AC_CHECK_HEADER": 0,
1237 "AC_PATH_PROG": 1,
1238 "PKG_CHECK_MODULES": 1,
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001239 }
1240
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001241 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
Patrick Williamse08ffba2022-12-05 10:33:46 -06001242 DEPENDENCIES_REGEX = {"phosphor-logging": r"\S+-dbus-interfaces$"}
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001243
Matthew Barth33df8792016-12-19 14:30:17 -06001244 # Set command line arguments
1245 parser = argparse.ArgumentParser()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001246 parser.add_argument(
1247 "-w",
1248 "--workspace",
1249 dest="WORKSPACE",
1250 required=True,
1251 help="Workspace directory location(i.e. /home)",
1252 )
1253 parser.add_argument(
1254 "-p",
1255 "--package",
1256 dest="PACKAGE",
1257 required=True,
1258 help="OpenBMC package to be unit tested",
1259 )
1260 parser.add_argument(
1261 "-t",
1262 "--test-only",
1263 dest="TEST_ONLY",
1264 action="store_true",
1265 required=False,
1266 default=False,
1267 help="Only run test cases, no other validation",
1268 )
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001269 arg_inttests = parser.add_mutually_exclusive_group()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001270 arg_inttests.add_argument(
1271 "--integration-tests",
1272 dest="INTEGRATION_TEST",
1273 action="store_true",
1274 required=False,
1275 default=True,
1276 help="Enable integration tests [default].",
1277 )
1278 arg_inttests.add_argument(
1279 "--no-integration-tests",
1280 dest="INTEGRATION_TEST",
1281 action="store_false",
1282 required=False,
1283 help="Disable integration tests.",
1284 )
1285 parser.add_argument(
1286 "-v",
1287 "--verbose",
1288 action="store_true",
1289 help="Print additional package status messages",
1290 )
1291 parser.add_argument(
1292 "-r", "--repeat", help="Repeat tests N times", type=int, default=1
1293 )
1294 parser.add_argument(
1295 "-b",
1296 "--branch",
1297 dest="BRANCH",
1298 required=False,
1299 help="Branch to target for dependent repositories",
1300 default="master",
1301 )
1302 parser.add_argument(
1303 "-n",
1304 "--noformat",
1305 dest="FORMAT",
1306 action="store_false",
1307 required=False,
1308 help="Whether or not to run format code",
1309 )
Matthew Barth33df8792016-12-19 14:30:17 -06001310 args = parser.parse_args(sys.argv[1:])
1311 WORKSPACE = args.WORKSPACE
1312 UNIT_TEST_PKG = args.PACKAGE
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001313 TEST_ONLY = args.TEST_ONLY
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001314 INTEGRATION_TEST = args.INTEGRATION_TEST
Andrew Geisslera61acb52019-01-03 16:32:44 -06001315 BRANCH = args.BRANCH
Lei YU7ef93302019-11-06 13:53:21 +08001316 FORMAT_CODE = args.FORMAT
Matthew Barth33df8792016-12-19 14:30:17 -06001317 if args.verbose:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001318
Matthew Barth33df8792016-12-19 14:30:17 -06001319 def printline(*line):
1320 for arg in line:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001321 print(arg, end=" ")
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301322 print()
Patrick Williamse08ffba2022-12-05 10:33:46 -06001323
Matthew Barth33df8792016-12-19 14:30:17 -06001324 else:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001325
Andrew Jefferyfce557f2020-03-13 12:12:19 +10301326 def printline(*line):
1327 pass
Matthew Barthccb7f852016-11-23 17:43:02 -06001328
Patrick Williamsb6535952020-12-15 06:40:10 -06001329 CODE_SCAN_DIR = os.path.join(WORKSPACE, UNIT_TEST_PKG)
Lei YU7ef93302019-11-06 13:53:21 +08001330
Patrick Williams330b0772022-11-28 06:14:06 -06001331 # Run format-code.sh, which will in turn call any repo-level formatters.
Lei YU7ef93302019-11-06 13:53:21 +08001332 if FORMAT_CODE:
Patrick Williamse08ffba2022-12-05 10:33:46 -06001333 check_call_cmd(
1334 os.path.join(
1335 WORKSPACE, "openbmc-build-scripts", "scripts", "format-code.sh"
1336 ),
1337 CODE_SCAN_DIR,
1338 )
Andrew Geisslera28286d2018-01-10 11:00:00 -08001339
Ed Tanous32768b82022-01-05 14:14:06 -08001340 # Check to see if any files changed
Patrick Williamse08ffba2022-12-05 10:33:46 -06001341 check_call_cmd(
1342 "git", "-C", CODE_SCAN_DIR, "--no-pager", "diff", "--exit-code"
1343 )
Ed Tanous32768b82022-01-05 14:14:06 -08001344
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001345 # Check if this repo has a supported make infrastructure
Patrick Williamsb6535952020-12-15 06:40:10 -06001346 pkg = Package(UNIT_TEST_PKG, CODE_SCAN_DIR)
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001347 if not pkg.build_system():
1348 print("No valid build system, exit")
1349 sys.exit(0)
1350
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301351 prev_umask = os.umask(000)
James Feist878df5c2018-07-26 14:54:28 -07001352
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301353 # Determine dependencies and add them
1354 dep_added = dict()
1355 dep_added[UNIT_TEST_PKG] = False
William A. Kennington III40d5c7c2018-12-13 14:37:59 -08001356
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301357 # Create dependency tree
1358 dep_tree = DepTree(UNIT_TEST_PKG)
Patrick Williamsb6535952020-12-15 06:40:10 -06001359 build_dep_tree(UNIT_TEST_PKG, CODE_SCAN_DIR, dep_added, dep_tree, BRANCH)
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001360
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301361 # Reorder Dependency Tree
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301362 for pkg_name, regex_str in DEPENDENCIES_REGEX.items():
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301363 dep_tree.ReorderDeps(pkg_name, regex_str)
1364 if args.verbose:
1365 dep_tree.PrintTree()
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001366
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301367 install_list = dep_tree.GetInstallList()
Andrew Geissler9ced4ed2019-11-18 14:33:58 -06001368
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301369 # We don't want to treat our package as a dependency
1370 install_list.remove(UNIT_TEST_PKG)
James Feist878df5c2018-07-26 14:54:28 -07001371
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301372 # Install reordered dependencies
1373 for dep in install_list:
1374 build_and_install(dep, False)
James Feist878df5c2018-07-26 14:54:28 -07001375
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301376 # Run package unit tests
1377 build_and_install(UNIT_TEST_PKG, True)
James Feist878df5c2018-07-26 14:54:28 -07001378
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301379 os.umask(prev_umask)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001380
1381 # Run any custom CI scripts the repo has, of which there can be
1382 # multiple of and anywhere in the repository.
Patrick Williamse08ffba2022-12-05 10:33:46 -06001383 ci_scripts = find_file(["run-ci.sh", "run-ci"], CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001384 if ci_scripts:
Patrick Williamsb6535952020-12-15 06:40:10 -06001385 os.chdir(CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001386 for ci_script in ci_scripts:
Patrick Williams55448ad2020-12-14 14:28:28 -06001387 check_call_cmd(ci_script)