blob: 02b5846e1bc79d69bdb0918a9ad523c6df4aac54 [file] [log] [blame]
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301#!/usr/bin/env python3
Matthew Barthccb7f852016-11-23 17:43:02 -06002
3"""
4This script determines the given package's openbmc dependencies from its
5configure.ac file where it downloads, configures, builds, and installs each of
6these dependencies. Then the given package is configured, built, and installed
7prior to executing its unit tests.
8"""
9
Matthew Barthd1810372016-12-19 16:57:21 -060010from git import Repo
William A. Kennington III3992d102021-05-17 01:41:04 -070011# interpreter is not used directly but this resolves dependency ordering
12# that would be broken if we didn't include it.
13from mesonbuild import interpreter
William A. Kennington IIIfcd70772020-06-04 00:50:23 -070014from mesonbuild import coredata, optinterpreter
Patrick Williamse95626d2021-04-13 11:13:53 -050015from mesonbuild.mesonlib import OptionKey
Patrick Williams95095f12021-04-14 14:42:35 -050016from mesonbuild.mesonlib import version_compare as meson_version_compare
Andrew Jeffery89b64b62020-03-13 12:15:48 +103017from urllib.parse import urljoin
Andrew Jefferya4e31c62018-03-08 13:45:28 +103018from subprocess import check_call, call, CalledProcessError
Patrick Williams485a0922022-08-12 11:44:55 -050019from tempfile import TemporaryDirectory
Matthew Barthccb7f852016-11-23 17:43:02 -060020import os
21import sys
Matthew Barth33df8792016-12-19 14:30:17 -060022import argparse
William A. Kennington IIIa2156732018-06-30 18:38:09 -070023import multiprocessing
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050024import re
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -080025import subprocess
William A. Kennington III3f1d1202018-12-06 18:02:07 -080026import shutil
William A. Kennington III4e1d0a12018-07-16 12:04:03 -070027import platform
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050028
29
30class DepTree():
31 """
32 Represents package dependency tree, where each node is a DepTree with a
33 name and DepTree children.
34 """
35
36 def __init__(self, name):
37 """
38 Create new DepTree.
39
40 Parameter descriptions:
41 name Name of new tree node.
42 """
43 self.name = name
44 self.children = list()
45
46 def AddChild(self, name):
47 """
48 Add new child node to current node.
49
50 Parameter descriptions:
51 name Name of new child
52 """
53 new_child = DepTree(name)
54 self.children.append(new_child)
55 return new_child
56
57 def AddChildNode(self, node):
58 """
59 Add existing child node to current node.
60
61 Parameter descriptions:
62 node Tree node to add
63 """
64 self.children.append(node)
65
66 def RemoveChild(self, name):
67 """
68 Remove child node.
69
70 Parameter descriptions:
71 name Name of child to remove
72 """
73 for child in self.children:
74 if child.name == name:
75 self.children.remove(child)
76 return
77
78 def GetNode(self, name):
79 """
80 Return node with matching name. Return None if not found.
81
82 Parameter descriptions:
83 name Name of node to return
84 """
85 if self.name == name:
86 return self
87 for child in self.children:
88 node = child.GetNode(name)
89 if node:
90 return node
91 return None
92
93 def GetParentNode(self, name, parent_node=None):
94 """
95 Return parent of node with matching name. Return none if not found.
96
97 Parameter descriptions:
98 name Name of node to get parent of
99 parent_node Parent of current node
100 """
101 if self.name == name:
102 return parent_node
103 for child in self.children:
104 found_node = child.GetParentNode(name, self)
105 if found_node:
106 return found_node
107 return None
108
109 def GetPath(self, name, path=None):
110 """
111 Return list of node names from head to matching name.
112 Return None if not found.
113
114 Parameter descriptions:
115 name Name of node
116 path List of node names from head to current node
117 """
118 if not path:
119 path = []
120 if self.name == name:
121 path.append(self.name)
122 return path
123 for child in self.children:
124 match = child.GetPath(name, path + [self.name])
125 if match:
126 return match
127 return None
128
129 def GetPathRegex(self, name, regex_str, path=None):
130 """
131 Return list of node paths that end in name, or match regex_str.
132 Return empty list if not found.
133
134 Parameter descriptions:
135 name Name of node to search for
136 regex_str Regex string to match node names
137 path Path of node names from head to current node
138 """
139 new_paths = []
140 if not path:
141 path = []
142 match = re.match(regex_str, self.name)
143 if (self.name == name) or (match):
144 new_paths.append(path + [self.name])
145 for child in self.children:
146 return_paths = None
147 full_path = path + [self.name]
148 return_paths = child.GetPathRegex(name, regex_str, full_path)
149 for i in return_paths:
150 new_paths.append(i)
151 return new_paths
152
153 def MoveNode(self, from_name, to_name):
154 """
155 Mode existing from_name node to become child of to_name node.
156
157 Parameter descriptions:
158 from_name Name of node to make a child of to_name
159 to_name Name of node to make parent of from_name
160 """
161 parent_from_node = self.GetParentNode(from_name)
162 from_node = self.GetNode(from_name)
163 parent_from_node.RemoveChild(from_name)
164 to_node = self.GetNode(to_name)
165 to_node.AddChildNode(from_node)
166
167 def ReorderDeps(self, name, regex_str):
168 """
169 Reorder dependency tree. If tree contains nodes with names that
170 match 'name' and 'regex_str', move 'regex_str' nodes that are
171 to the right of 'name' node, so that they become children of the
172 'name' node.
173
174 Parameter descriptions:
175 name Name of node to look for
176 regex_str Regex string to match names to
177 """
178 name_path = self.GetPath(name)
179 if not name_path:
180 return
181 paths = self.GetPathRegex(name, regex_str)
182 is_name_in_paths = False
183 name_index = 0
184 for i in range(len(paths)):
185 path = paths[i]
186 if path[-1] == name:
187 is_name_in_paths = True
188 name_index = i
189 break
190 if not is_name_in_paths:
191 return
192 for i in range(name_index + 1, len(paths)):
193 path = paths[i]
194 if name in path:
195 continue
196 from_name = path[-1]
197 self.MoveNode(from_name, name)
198
199 def GetInstallList(self):
200 """
201 Return post-order list of node names.
202
203 Parameter descriptions:
204 """
205 install_list = []
206 for child in self.children:
207 child_install_list = child.GetInstallList()
208 install_list.extend(child_install_list)
209 install_list.append(self.name)
210 return install_list
211
212 def PrintTree(self, level=0):
213 """
214 Print pre-order node names with indentation denoting node depth level.
215
216 Parameter descriptions:
217 level Current depth level
218 """
219 INDENT_PER_LEVEL = 4
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030220 print(' ' * (level * INDENT_PER_LEVEL) + self.name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500221 for child in self.children:
222 child.PrintTree(level + 1)
Matthew Barth33df8792016-12-19 14:30:17 -0600223
224
William A. Kennington III1fddb972019-02-06 18:03:53 -0800225def check_call_cmd(*cmd):
Matthew Barth33df8792016-12-19 14:30:17 -0600226 """
227 Verbose prints the directory location the given command is called from and
228 the command, then executes the command using check_call.
229
230 Parameter descriptions:
231 dir Directory location command is to be called from
232 cmd List of parameters constructing the complete command
233 """
William A. Kennington III1fddb972019-02-06 18:03:53 -0800234 printline(os.getcwd(), ">", " ".join(cmd))
Matthew Barth33df8792016-12-19 14:30:17 -0600235 check_call(cmd)
Matthew Barthccb7f852016-11-23 17:43:02 -0600236
237
Andrew Geisslera61acb52019-01-03 16:32:44 -0600238def clone_pkg(pkg, branch):
Matthew Barth33df8792016-12-19 14:30:17 -0600239 """
240 Clone the given openbmc package's git repository from gerrit into
241 the WORKSPACE location
242
243 Parameter descriptions:
244 pkg Name of the package to clone
Andrew Geisslera61acb52019-01-03 16:32:44 -0600245 branch Branch to clone from pkg
Matthew Barth33df8792016-12-19 14:30:17 -0600246 """
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030247 pkg_dir = os.path.join(WORKSPACE, pkg)
248 if os.path.exists(os.path.join(pkg_dir, '.git')):
249 return pkg_dir
Andrew Geissler37052412022-05-20 15:28:26 -0400250 pkg_repo = urljoin('https://gerrit.openbmc.org/openbmc/', pkg)
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030251 os.mkdir(pkg_dir)
Andrew Geisslera61acb52019-01-03 16:32:44 -0600252 printline(pkg_dir, "> git clone", pkg_repo, branch, "./")
253 try:
254 # first try the branch
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030255 clone = Repo.clone_from(pkg_repo, pkg_dir, branch=branch)
256 repo_inst = clone.working_dir
Andrew Geisslera61acb52019-01-03 16:32:44 -0600257 except:
258 printline("Input branch not found, default to master")
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030259 clone = Repo.clone_from(pkg_repo, pkg_dir, branch="master")
260 repo_inst = clone.working_dir
Andrew Geisslera61acb52019-01-03 16:32:44 -0600261 return repo_inst
Matthew Barth33df8792016-12-19 14:30:17 -0600262
263
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030264def make_target_exists(target):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800265 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030266 Runs a check against the makefile in the current directory to determine
267 if the target exists so that it can be built.
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800268
269 Parameter descriptions:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030270 target The make target we are checking
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800271 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030272 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030273 cmd = ['make', '-n', target]
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030274 with open(os.devnull, 'w') as devnull:
275 check_call(cmd, stdout=devnull, stderr=devnull)
276 return True
277 except CalledProcessError:
278 return False
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800279
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800280
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700281make_parallel = [
282 'make',
283 # Run enough jobs to saturate all the cpus
284 '-j', str(multiprocessing.cpu_count()),
285 # Don't start more jobs if the load avg is too high
286 '-l', str(multiprocessing.cpu_count()),
287 # Synchronize the output so logs aren't intermixed in stdout / stderr
288 '-O',
289]
290
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800291
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030292def build_and_install(name, build_for_testing=False):
William A. Kennington III780ec092018-12-06 14:46:50 -0800293 """
294 Builds and installs the package in the environment. Optionally
295 builds the examples and test cases for package.
296
297 Parameter description:
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030298 name The name of the package we are building
William A. Kennington IIIa0454912018-12-06 14:47:16 -0800299 build_for_testing Enable options related to testing on the package?
William A. Kennington III780ec092018-12-06 14:46:50 -0800300 """
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030301 os.chdir(os.path.join(WORKSPACE, name))
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800302
303 # Refresh dynamic linker run time bindings for dependencies
William A. Kennington III1fddb972019-02-06 18:03:53 -0800304 check_call_cmd('sudo', '-n', '--', 'ldconfig')
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800305
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030306 pkg = Package()
307 if build_for_testing:
308 pkg.test()
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800309 else:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030310 pkg.install()
311
William A. Kennington III780ec092018-12-06 14:46:50 -0800312
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030313def build_dep_tree(name, pkgdir, dep_added, head, branch, dep_tree=None):
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500314 """
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030315 For each package (name), starting with the package to be unit tested,
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030316 extract its dependencies. For each package dependency defined, recursively
317 apply the same strategy
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500318
319 Parameter descriptions:
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030320 name Name of the package
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500321 pkgdir Directory where package source is located
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800322 dep_added Current dict of dependencies and added status
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500323 head Head node of the dependency tree
Andrew Geisslera61acb52019-01-03 16:32:44 -0600324 branch Branch to clone from pkg
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500325 dep_tree Current dependency tree node
326 """
327 if not dep_tree:
328 dep_tree = head
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800329
William A. Kennington IIIbe6aab22018-12-06 15:01:54 -0800330 with open("/tmp/depcache", "r") as depcache:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800331 cache = depcache.readline()
332
333 # Read out pkg dependencies
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030334 pkg = Package(name, pkgdir)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800335
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600336 build = pkg.build_system()
337 if build == None:
338 raise Exception(f"Unable to find build system for {name}.")
339
340 for dep in set(build.dependencies()):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800341 if dep in cache:
342 continue
343 # Dependency package not already known
344 if dep_added.get(dep) is None:
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600345 print(f"Adding {dep} dependency to {name}.")
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800346 # Dependency package not added
347 new_child = dep_tree.AddChild(dep)
348 dep_added[dep] = False
Andrew Jeffery3b92fdd2020-03-13 11:49:18 +1030349 dep_pkgdir = clone_pkg(dep, branch)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800350 # Determine this dependency package's
351 # dependencies and add them before
352 # returning to add this package
353 dep_added = build_dep_tree(dep,
354 dep_pkgdir,
355 dep_added,
356 head,
Andrew Geisslera61acb52019-01-03 16:32:44 -0600357 branch,
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800358 new_child)
359 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
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030365 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:
Ed Tanousc7198552022-07-01 08:15:50 -0700378
Patrick Williams485a0922022-08-12 11:44:55 -0500379 # http://cppcheck.sourceforge.net/manual.pdf
380 try:
381 check_call_cmd(
382 'cppcheck',
383 '-j', 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}',
393 )
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 """
William A. Kennington III0326ded2019-02-07 00:33:28 -0800402 src = 'unit-test-vg.c'
403 exe = './unit-test-vg'
404 with open(src, 'w') as h:
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700405 h.write('#include <errno.h>\n')
406 h.write('#include <stdio.h>\n')
William A. Kennington III0326ded2019-02-07 00:33:28 -0800407 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')
411 h.write('strcpy(heap_str, "RandString");\n')
412 h.write('int res = strcmp("RandString", heap_str);\n')
413 h.write('free(heap_str);\n')
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700414 h.write('char errstr[64];\n')
415 h.write('strerror_r(EINVAL, errstr, sizeof(errstr));\n')
416 h.write('printf("%s\\n", errstr);\n')
William A. Kennington III0326ded2019-02-07 00:33:28 -0800417 h.write('return res;\n')
418 h.write('}\n')
419 try:
420 with open(os.devnull, 'w') as devnull:
421 check_call(['gcc', '-O2', '-o', exe, src],
422 stdout=devnull, stderr=devnull)
423 check_call(['valgrind', '--error-exitcode=99', exe],
424 stdout=devnull, stderr=devnull)
425 return True
426 except:
427 sys.stderr.write("###### Platform is not valgrind safe ######\n")
428 return False
429 finally:
430 os.remove(src)
431 os.remove(exe)
William A. Kennington III37a89a22018-12-13 14:32:02 -0800432
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030433
William A. Kennington III282e3302019-02-04 16:55:05 -0800434def is_sanitize_safe():
435 """
436 Returns whether it is safe to run sanitizers on our platform
437 """
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800438 src = 'unit-test-sanitize.c'
439 exe = './unit-test-sanitize'
440 with open(src, 'w') as h:
441 h.write('int main() { return 0; }\n')
442 try:
443 with open(os.devnull, 'w') as devnull:
444 check_call(['gcc', '-O2', '-fsanitize=address',
445 '-fsanitize=undefined', '-o', exe, src],
446 stdout=devnull, stderr=devnull)
447 check_call([exe], stdout=devnull, stderr=devnull)
Andrew Geisslercd9578b2021-05-04 10:18:01 -0500448
449 # TODO - Sanitizer not working on ppc64le
450 # https://github.com/openbmc/openbmc-build-scripts/issues/31
451 if (platform.processor() == 'ppc64le'):
452 sys.stderr.write("###### ppc64le is not sanitize safe ######\n")
453 return False
454 else:
455 return True
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800456 except:
457 sys.stderr.write("###### Platform is not sanitize safe ######\n")
458 return False
459 finally:
460 os.remove(src)
461 os.remove(exe)
William A. Kennington III282e3302019-02-04 16:55:05 -0800462
William A. Kennington III49d4e592019-02-06 17:59:27 -0800463
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800464def maybe_make_valgrind():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700465 """
466 Potentially runs the unit tests through valgrind for the package
467 via `make check-valgrind`. If the package does not have valgrind testing
468 then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700469 """
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700470 # Valgrind testing is currently broken by an aggressive strcmp optimization
471 # that is inlined into optimized code for POWER by gcc 7+. Until we find
472 # a workaround, just don't run valgrind tests on POWER.
473 # https://github.com/openbmc/openbmc/issues/3315
William A. Kennington III37a89a22018-12-13 14:32:02 -0800474 if not is_valgrind_safe():
William A. Kennington III75130192019-02-07 00:34:14 -0800475 sys.stderr.write("###### Skipping valgrind ######\n")
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700476 return
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700477 if not make_target_exists('check-valgrind'):
478 return
479
480 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030481 cmd = make_parallel + ['check-valgrind']
William A. Kennington III1fddb972019-02-06 18:03:53 -0800482 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700483 except CalledProcessError:
William A. Kennington III90b106a2019-02-06 18:08:24 -0800484 for root, _, files in os.walk(os.getcwd()):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700485 for f in files:
486 if re.search('test-suite-[a-z]+.log', f) is None:
487 continue
William A. Kennington III1fddb972019-02-06 18:03:53 -0800488 check_call_cmd('cat', os.path.join(root, f))
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700489 raise Exception('Valgrind tests failed')
490
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030491
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800492def maybe_make_coverage():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700493 """
494 Potentially runs the unit tests through code coverage for the package
495 via `make check-code-coverage`. If the package does not have code coverage
496 testing then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700497 """
498 if not make_target_exists('check-code-coverage'):
499 return
500
501 # Actually run code coverage
502 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030503 cmd = make_parallel + ['check-code-coverage']
William A. Kennington III1fddb972019-02-06 18:03:53 -0800504 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700505 except CalledProcessError:
506 raise Exception('Code coverage failed')
Matthew Barthccb7f852016-11-23 17:43:02 -0600507
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030508
509class BuildSystem(object):
510 """
511 Build systems generally provide the means to configure, build, install and
512 test software. The BuildSystem class defines a set of interfaces on top of
513 which Autotools, Meson, CMake and possibly other build system drivers can
514 be implemented, separating out the phases to control whether a package
515 should merely be installed or also tested and analyzed.
516 """
Lei YU08d2b922022-04-25 11:21:36 +0800517
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030518 def __init__(self, package, path):
519 """Initialise the driver with properties independent of the build system
520
521 Keyword arguments:
522 package: The name of the package. Derived from the path if None
523 path: The path to the package. Set to the working directory if None
524 """
525 self.path = "." if not path else path
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030526 realpath = os.path.realpath(self.path)
527 self.package = package if package else os.path.basename(realpath)
Andrew Jeffery3f8e5a72020-03-13 11:47:56 +1030528 self.build_for_testing = False
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030529
530 def probe(self):
531 """Test if the build system driver can be applied to the package
532
533 Return True if the driver can drive the package's build system,
534 otherwise False.
535
536 Generally probe() is implemented by testing for the presence of the
537 build system's configuration file(s).
538 """
539 raise NotImplemented
540
541 def dependencies(self):
542 """Provide the package's dependencies
543
544 Returns a list of dependencies. If no dependencies are required then an
545 empty list must be returned.
546
547 Generally dependencies() is implemented by analysing and extracting the
548 data from the build system configuration.
549 """
550 raise NotImplemented
551
552 def configure(self, build_for_testing):
553 """Configure the source ready for building
554
555 Should raise an exception if configuration failed.
556
557 Keyword arguments:
558 build_for_testing: Mark the package as being built for testing rather
559 than for installation as a dependency for the
560 package under test. Setting to True generally
561 implies that the package will be configured to build
562 with debug information, at a low level of
563 optimisation and possibly with sanitizers enabled.
564
565 Generally configure() is implemented by invoking the build system
566 tooling to generate Makefiles or equivalent.
567 """
568 raise NotImplemented
569
570 def build(self):
571 """Build the software ready for installation and/or testing
572
573 Should raise an exception if the build fails
574
575 Generally build() is implemented by invoking `make` or `ninja`.
576 """
577 raise NotImplemented
578
579 def install(self):
580 """Install the software ready for use
581
582 Should raise an exception if installation fails
583
584 Like build(), install() is generally implemented by invoking `make` or
585 `ninja`.
586 """
587 raise NotImplemented
588
589 def test(self):
590 """Build and run the test suite associated with the package
591
592 Should raise an exception if the build or testing fails.
593
594 Like install(), test() is generally implemented by invoking `make` or
595 `ninja`.
596 """
597 raise NotImplemented
598
599 def analyze(self):
600 """Run any supported analysis tools over the codebase
601
602 Should raise an exception if analysis fails.
603
604 Some analysis tools such as scan-build need injection into the build
605 system. analyze() provides the necessary hook to implement such
606 behaviour. Analyzers independent of the build system can also be
607 specified here but at the cost of possible duplication of code between
608 the build system driver implementations.
609 """
610 raise NotImplemented
611
612
613class Autotools(BuildSystem):
614 def __init__(self, package=None, path=None):
615 super(Autotools, self).__init__(package, path)
616
617 def probe(self):
618 return os.path.isfile(os.path.join(self.path, 'configure.ac'))
619
620 def dependencies(self):
621 configure_ac = os.path.join(self.path, 'configure.ac')
622
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030623 contents = ''
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030624 # Prepend some special function overrides so we can parse out
625 # dependencies
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030626 for macro in DEPENDENCIES.keys():
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030627 contents += ('m4_define([' + macro + '], [' + macro + '_START$' +
628 str(DEPENDENCIES_OFFSET[macro] + 1) +
629 macro + '_END])\n')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030630 with open(configure_ac, "rt") as f:
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030631 contents += f.read()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030632
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030633 autoconf_cmdline = ['autoconf', '-Wno-undefined', '-']
634 autoconf_process = subprocess.Popen(autoconf_cmdline,
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030635 stdin=subprocess.PIPE,
636 stdout=subprocess.PIPE,
637 stderr=subprocess.PIPE)
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030638 document = contents.encode('utf-8')
639 (stdout, stderr) = autoconf_process.communicate(input=document)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030640 if not stdout:
641 print(stderr)
642 raise Exception("Failed to run autoconf for parsing dependencies")
643
644 # Parse out all of the dependency text
645 matches = []
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030646 for macro in DEPENDENCIES.keys():
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030647 pattern = '(' + macro + ')_START(.*?)' + macro + '_END'
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030648 for match in re.compile(pattern).finditer(stdout.decode('utf-8')):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030649 matches.append((match.group(1), match.group(2)))
650
651 # Look up dependencies from the text
652 found_deps = []
653 for macro, deptext in matches:
654 for potential_dep in deptext.split(' '):
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030655 for known_dep in DEPENDENCIES[macro].keys():
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030656 if potential_dep.startswith(known_dep):
657 found_deps.append(DEPENDENCIES[macro][known_dep])
658
659 return found_deps
660
661 def _configure_feature(self, flag, enabled):
662 """
663 Returns an configure flag as a string
664
665 Parameters:
666 flag The name of the flag
667 enabled Whether the flag is enabled or disabled
668 """
669 return '--' + ('enable' if enabled else 'disable') + '-' + flag
670
671 def configure(self, build_for_testing):
672 self.build_for_testing = build_for_testing
673 conf_flags = [
674 self._configure_feature('silent-rules', False),
675 self._configure_feature('examples', build_for_testing),
676 self._configure_feature('tests', build_for_testing),
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000677 self._configure_feature('itests', INTEGRATION_TEST),
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030678 ]
Andrew Jefferyd0809692021-05-14 16:23:57 +0930679 conf_flags.extend([
680 self._configure_feature('code-coverage', build_for_testing),
681 self._configure_feature('valgrind', build_for_testing),
682 ])
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030683 # Add any necessary configure flags for package
684 if CONFIGURE_FLAGS.get(self.package) is not None:
685 conf_flags.extend(CONFIGURE_FLAGS.get(self.package))
686 for bootstrap in ['bootstrap.sh', 'bootstrap', 'autogen.sh']:
687 if os.path.exists(bootstrap):
688 check_call_cmd('./' + bootstrap)
689 break
690 check_call_cmd('./configure', *conf_flags)
691
692 def build(self):
693 check_call_cmd(*make_parallel)
694
695 def install(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +1030696 check_call_cmd('sudo', '-n', '--', *(make_parallel + ['install']))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030697
698 def test(self):
699 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030700 cmd = make_parallel + ['check']
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030701 for i in range(0, args.repeat):
702 check_call_cmd(*cmd)
Andrew Jefferyd0809692021-05-14 16:23:57 +0930703
704 maybe_make_valgrind()
705 maybe_make_coverage()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030706 except CalledProcessError:
707 for root, _, files in os.walk(os.getcwd()):
708 if 'test-suite.log' not in files:
709 continue
710 check_call_cmd('cat', os.path.join(root, 'test-suite.log'))
711 raise Exception('Unit tests failed')
712
713 def analyze(self):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030714 run_cppcheck()
715
716
717class CMake(BuildSystem):
718 def __init__(self, package=None, path=None):
719 super(CMake, self).__init__(package, path)
720
721 def probe(self):
722 return os.path.isfile(os.path.join(self.path, 'CMakeLists.txt'))
723
724 def dependencies(self):
725 return []
726
727 def configure(self, build_for_testing):
728 self.build_for_testing = build_for_testing
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000729 if INTEGRATION_TEST:
730 check_call_cmd('cmake', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
731 '-DITESTS=ON', '.')
732 else:
733 check_call_cmd('cmake', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON', '.')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030734
735 def build(self):
736 check_call_cmd('cmake', '--build', '.', '--', '-j',
737 str(multiprocessing.cpu_count()))
738
739 def install(self):
740 pass
741
742 def test(self):
743 if make_target_exists('test'):
744 check_call_cmd('ctest', '.')
745
746 def analyze(self):
747 if os.path.isfile('.clang-tidy'):
Nan Zhou524ebee2022-08-27 23:26:13 +0000748 shutil.rmtree("tidy-build", ignore_errors=True)
749 os.mkdir("tidy-build")
750
Ed Tanousbc5c1c42020-10-01 17:49:20 -0700751 # clang-tidy needs to run on a clang-specific build
752 check_call_cmd('cmake', '-DCMAKE_C_COMPILER=clang',
753 '-DCMAKE_CXX_COMPILER=clang++',
754 '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
755 '-H.',
756 '-Btidy-build')
757 # we need to cd here because otherwise clang-tidy doesn't find the
Nan Zhou524ebee2022-08-27 23:26:13 +0000758 # .clang-tidy file in the roots of repos. It's arguably a "bug"
Ed Tanousbc5c1c42020-10-01 17:49:20 -0700759 # with run-clang-tidy at a minimum it's "weird" that it requires
760 # the .clang-tidy to be up a dir
Ed Tanous662890f2020-09-28 17:35:20 -0700761 os.chdir("tidy-build")
762 try:
Patrick Williamsf89c8502021-10-01 11:50:09 -0500763 check_call_cmd('run-clang-tidy', "-header-filter=.*", '-p',
Ed Tanousbc5c1c42020-10-01 17:49:20 -0700764 '.')
Ed Tanous662890f2020-09-28 17:35:20 -0700765 finally:
766 os.chdir("..")
Nan Zhou524ebee2022-08-27 23:26:13 +0000767 shutil.rmtree("tidy-build", ignore_errors=True)
Ed Tanous662890f2020-09-28 17:35:20 -0700768
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030769 maybe_make_valgrind()
770 maybe_make_coverage()
771 run_cppcheck()
772
773
774class Meson(BuildSystem):
775 def __init__(self, package=None, path=None):
776 super(Meson, self).__init__(package, path)
777
778 def probe(self):
779 return os.path.isfile(os.path.join(self.path, 'meson.build'))
780
781 def dependencies(self):
782 meson_build = os.path.join(self.path, 'meson.build')
783 if not os.path.exists(meson_build):
784 return []
785
786 found_deps = []
787 for root, dirs, files in os.walk(self.path):
788 if 'meson.build' not in files:
789 continue
790 with open(os.path.join(root, 'meson.build'), 'rt') as f:
791 build_contents = f.read()
Nan Zhouef13d532020-07-07 09:52:02 -0700792 pattern = r"dependency\('([^']*)'.*?\),?\n"
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030793 for match in re.finditer(pattern, build_contents):
794 group = match.group(1)
795 maybe_dep = DEPENDENCIES['PKG_CHECK_MODULES'].get(group)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030796 if maybe_dep is not None:
797 found_deps.append(maybe_dep)
798
799 return found_deps
800
801 def _parse_options(self, options_file):
802 """
803 Returns a set of options defined in the provides meson_options.txt file
804
805 Parameters:
806 options_file The file containing options
807 """
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700808 oi = optinterpreter.OptionInterpreter('')
809 oi.process(options_file)
810 return oi.options
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030811
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700812 def _configure_boolean(self, val):
813 """
814 Returns the meson flag which signifies the value
815
816 True is true which requires the boolean.
817 False is false which disables the boolean.
818
819 Parameters:
820 val The value being converted
821 """
822 if val is True:
823 return 'true'
824 elif val is False:
825 return 'false'
826 else:
827 raise Exception("Bad meson boolean value")
828
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030829 def _configure_feature(self, val):
830 """
831 Returns the meson flag which signifies the value
832
833 True is enabled which requires the feature.
834 False is disabled which disables the feature.
835 None is auto which autodetects the feature.
836
837 Parameters:
838 val The value being converted
839 """
840 if val is True:
841 return "enabled"
842 elif val is False:
843 return "disabled"
844 elif val is None:
845 return "auto"
846 else:
847 raise Exception("Bad meson feature value")
848
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700849 def _configure_option(self, opts, key, val):
850 """
851 Returns the meson flag which signifies the value
852 based on the type of the opt
853
854 Parameters:
855 opt The meson option which we are setting
856 val The value being converted
857 """
858 if isinstance(opts[key], coredata.UserBooleanOption):
859 str_val = self._configure_boolean(val)
860 elif isinstance(opts[key], coredata.UserFeatureOption):
861 str_val = self._configure_feature(val)
862 else:
863 raise Exception('Unknown meson option type')
864 return "-D{}={}".format(key, str_val)
865
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030866 def configure(self, build_for_testing):
867 self.build_for_testing = build_for_testing
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700868 meson_options = {}
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030869 if os.path.exists("meson_options.txt"):
870 meson_options = self._parse_options("meson_options.txt")
871 meson_flags = [
872 '-Db_colorout=never',
873 '-Dwerror=true',
874 '-Dwarning_level=3',
875 ]
876 if build_for_testing:
877 meson_flags.append('--buildtype=debug')
878 else:
879 meson_flags.append('--buildtype=debugoptimized')
Patrick Williamse95626d2021-04-13 11:13:53 -0500880 if OptionKey('tests') in meson_options:
Lei YU08d2b922022-04-25 11:21:36 +0800881 meson_flags.append(self._configure_option(
882 meson_options, OptionKey('tests'), build_for_testing))
Patrick Williamse95626d2021-04-13 11:13:53 -0500883 if OptionKey('examples') in meson_options:
Lei YU08d2b922022-04-25 11:21:36 +0800884 meson_flags.append(self._configure_option(
885 meson_options, OptionKey('examples'), build_for_testing))
Patrick Williamse95626d2021-04-13 11:13:53 -0500886 if OptionKey('itests') in meson_options:
Lei YU08d2b922022-04-25 11:21:36 +0800887 meson_flags.append(self._configure_option(
888 meson_options, OptionKey('itests'), INTEGRATION_TEST))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030889 if MESON_FLAGS.get(self.package) is not None:
890 meson_flags.extend(MESON_FLAGS.get(self.package))
891 try:
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030892 check_call_cmd('meson', 'setup', '--reconfigure', 'build',
893 *meson_flags)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030894 except:
Nan Zhou524ebee2022-08-27 23:26:13 +0000895 shutil.rmtree('build', ignore_errors=True)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030896 check_call_cmd('meson', 'setup', 'build', *meson_flags)
897
898 def build(self):
899 check_call_cmd('ninja', '-C', 'build')
900
901 def install(self):
902 check_call_cmd('sudo', '-n', '--', 'ninja', '-C', 'build', 'install')
903
904 def test(self):
Patrick Williams95095f12021-04-14 14:42:35 -0500905 # It is useful to check various settings of the meson.build file
906 # for compatibility, such as meson_version checks. We shouldn't
907 # do this in the configure path though because it affects subprojects
908 # and dependencies as well, but we only want this applied to the
909 # project-under-test (otherwise an upstream dependency could fail
910 # this check without our control).
911 self._extra_meson_checks()
912
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030913 try:
Brad Bishop7b8cef22020-08-26 15:58:09 -0400914 test_args = ('--repeat', str(args.repeat), '-C', 'build')
William A. Kennington IIId9062c42022-05-04 23:24:44 -0700915 check_call_cmd('meson', 'test', '--print-errorlogs', *test_args)
Brad Bishop7b8cef22020-08-26 15:58:09 -0400916
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030917 except CalledProcessError:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030918 raise Exception('Unit tests failed')
919
920 def _setup_exists(self, setup):
921 """
922 Returns whether the meson build supports the named test setup.
923
924 Parameter descriptions:
925 setup The setup target to check
926 """
927 try:
928 with open(os.devnull, 'w') as devnull:
929 output = subprocess.check_output(
Lei YU08d2b922022-04-25 11:21:36 +0800930 ['meson', 'test', '-C', 'build',
931 '--setup', setup, '-t', '0'],
932 stderr=subprocess.STDOUT)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030933 except CalledProcessError as e:
934 output = e.output
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030935 output = output.decode('utf-8')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030936 return not re.search('Test setup .* not found from project', output)
937
938 def _maybe_valgrind(self):
939 """
940 Potentially runs the unit tests through valgrind for the package
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030941 via `meson test`. The package can specify custom valgrind
942 configurations by utilizing add_test_setup() in a meson.build
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030943 """
944 if not is_valgrind_safe():
945 sys.stderr.write("###### Skipping valgrind ######\n")
946 return
947 try:
948 if self._setup_exists('valgrind'):
Lei YU08d2b922022-04-25 11:21:36 +0800949 check_call_cmd('meson', 'test', '-t', '10', '-C', 'build',
William A. Kennington IIId9062c42022-05-04 23:24:44 -0700950 '--print-errorlogs', '--setup', 'valgrind')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030951 else:
Lei YU08d2b922022-04-25 11:21:36 +0800952 check_call_cmd('meson', 'test', '-t', '10', '-C', 'build',
William A. Kennington IIId9062c42022-05-04 23:24:44 -0700953 '--print-errorlogs', '--wrapper', 'valgrind')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030954 except CalledProcessError:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030955 raise Exception('Valgrind tests failed')
956
957 def analyze(self):
958 self._maybe_valgrind()
959
960 # Run clang-tidy only if the project has a configuration
961 if os.path.isfile('.clang-tidy'):
Manojkiran Eda1aa91992020-10-02 14:11:53 +0530962 os.environ["CXX"] = "clang++"
Nan Zhou524ebee2022-08-27 23:26:13 +0000963 shutil.rmtree("build-clang", ignore_errors=True)
Manojkiran Eda1aa91992020-10-02 14:11:53 +0530964 check_call_cmd('meson', 'setup', 'build-clang')
Ed Tanous88db4c22022-01-25 15:15:59 -0800965 os.chdir("build-clang")
966 try:
967 check_call_cmd('run-clang-tidy', '-fix', '-format', '-p', '.')
968 except subprocess.CalledProcessError:
969 check_call_cmd("git", "-C", CODE_SCAN_DIR,
Lei YU08d2b922022-04-25 11:21:36 +0800970 "--no-pager", "diff")
Ed Tanous88db4c22022-01-25 15:15:59 -0800971 raise
972 finally:
973 os.chdir("..")
Nan Zhou524ebee2022-08-27 23:26:13 +0000974 shutil.rmtree("build-clang", ignore_errors=True)
Ed Tanous88db4c22022-01-25 15:15:59 -0800975
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030976 # Run the basic clang static analyzer otherwise
977 else:
978 check_call_cmd('ninja', '-C', 'build',
979 'scan-build')
980
981 # Run tests through sanitizers
982 # b_lundef is needed if clang++ is CXX since it resolves the
983 # asan symbols at runtime only. We don't want to set it earlier
984 # in the build process to ensure we don't have undefined
985 # runtime code.
986 if is_sanitize_safe():
987 check_call_cmd('meson', 'configure', 'build',
988 '-Db_sanitize=address,undefined',
989 '-Db_lundef=false')
William A. Kennington IIId9062c42022-05-04 23:24:44 -0700990 check_call_cmd('meson', 'test', '-C', 'build', '--print-errorlogs',
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030991 '--logbase', 'testlog-ubasan')
992 # TODO: Fix memory sanitizer
Andrew Jeffery6015ca12020-03-13 11:50:10 +1030993 # check_call_cmd('meson', 'configure', 'build',
994 # '-Db_sanitize=memory')
995 # check_call_cmd('meson', 'test', '-C', 'build'
996 # '--logbase', 'testlog-msan')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030997 check_call_cmd('meson', 'configure', 'build',
Lei YUf684ad62020-09-21 17:02:26 +0800998 '-Db_sanitize=none')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030999 else:
1000 sys.stderr.write("###### Skipping sanitizers ######\n")
1001
1002 # Run coverage checks
1003 check_call_cmd('meson', 'configure', 'build',
1004 '-Db_coverage=true')
1005 self.test()
1006 # Only build coverage HTML if coverage files were produced
1007 for root, dirs, files in os.walk('build'):
1008 if any([f.endswith('.gcda') for f in files]):
1009 check_call_cmd('ninja', '-C', 'build',
1010 'coverage-html')
1011 break
1012 check_call_cmd('meson', 'configure', 'build',
1013 '-Db_coverage=false')
1014 run_cppcheck()
1015
Patrick Williams95095f12021-04-14 14:42:35 -05001016 def _extra_meson_checks(self):
1017 with open(os.path.join(self.path, 'meson.build'), 'rt') as f:
1018 build_contents = f.read()
1019
1020 # Find project's specified meson_version.
1021 meson_version = None
1022 pattern = r"meson_version:[^']*'([^']*)'"
1023 for match in re.finditer(pattern, build_contents):
1024 group = match.group(1)
1025 meson_version = group
1026
1027 # C++20 requires at least Meson 0.57 but Meson itself doesn't
1028 # identify this. Add to our unit-test checks so that we don't
1029 # get a meson.build missing this.
1030 pattern = r"'cpp_std=c\+\+20'"
1031 for match in re.finditer(pattern, build_contents):
1032 if not meson_version or \
1033 not meson_version_compare(meson_version, ">=0.57"):
1034 raise Exception(
1035 "C++20 support requires specifying in meson.build: "
1036 + "meson_version: '>=0.57'"
1037 )
1038
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301039
1040class Package(object):
1041 def __init__(self, name=None, path=None):
Manojkiran Eda29d28252020-06-15 18:58:09 +05301042 self.supported = [Meson, Autotools, CMake]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301043 self.name = name
1044 self.path = path
1045 self.test_only = False
1046
1047 def build_systems(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +10301048 instances = (system(self.name, self.path) for system in self.supported)
1049 return (instance for instance in instances if instance.probe())
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301050
1051 def build_system(self, preferred=None):
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001052 systems = list(self.build_systems())
1053
1054 if not systems:
1055 return None
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301056
1057 if preferred:
Andrew Jeffery881041f2020-03-13 11:46:30 +10301058 return {type(system): system for system in systems}[preferred]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301059
1060 return next(iter(systems))
1061
1062 def install(self, system=None):
1063 if not system:
1064 system = self.build_system()
1065
1066 system.configure(False)
1067 system.build()
1068 system.install()
1069
Andrew Jeffery19d75672020-03-13 10:42:08 +10301070 def _test_one(self, system):
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301071 system.configure(True)
1072 system.build()
1073 system.install()
1074 system.test()
Andrew Jefferyd0809692021-05-14 16:23:57 +09301075 if not TEST_ONLY:
1076 system.analyze()
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301077
Andrew Jeffery19d75672020-03-13 10:42:08 +10301078 def test(self):
1079 for system in self.build_systems():
1080 self._test_one(system)
1081
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301082
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001083def find_file(filename, basedir):
1084 """
Patrick Williams55448ad2020-12-14 14:28:28 -06001085 Finds all occurrences of a file (or list of files) in the base
1086 directory and passes them back with their relative paths.
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001087
1088 Parameter descriptions:
Patrick Williams55448ad2020-12-14 14:28:28 -06001089 filename The name of the file (or list of files) to
1090 find
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001091 basedir The base directory search in
1092 """
1093
Patrick Williams55448ad2020-12-14 14:28:28 -06001094 if not isinstance(filename, list):
Lei YU08d2b922022-04-25 11:21:36 +08001095 filename = [filename]
Patrick Williams55448ad2020-12-14 14:28:28 -06001096
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001097 filepaths = []
1098 for root, dirs, files in os.walk(basedir):
Brad Bishopeb667262021-08-06 13:54:58 -04001099 if os.path.split(root)[-1] == 'subprojects':
1100 for f in files:
1101 subproject = '.'.join(f.split('.')[0:-1])
1102 if f.endswith('.wrap') and subproject in dirs:
1103 # don't find files in meson subprojects with wraps
1104 dirs.remove(subproject)
Patrick Williams55448ad2020-12-14 14:28:28 -06001105 for f in filename:
1106 if f in files:
1107 filepaths.append(os.path.join(root, f))
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001108 return filepaths
1109
Andrew Jeffery9cc97d82020-03-13 11:57:44 +10301110
Matthew Barthccb7f852016-11-23 17:43:02 -06001111if __name__ == '__main__':
1112 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
1113 CONFIGURE_FLAGS = {
Matthew Barth1d1c6732017-03-24 10:00:28 -05001114 'phosphor-logging':
Matt Spinler0744bb82020-01-16 08:23:35 -06001115 ['--enable-metadata-processing', '--enable-openpower-pel-extension',
Deepak Kodihalli3a4e1b42017-06-08 09:52:35 -05001116 'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml']
Matthew Barthccb7f852016-11-23 17:43:02 -06001117 }
1118
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001119 # MESON_FLAGS = [GIT REPO]:[MESON FLAGS]
1120 MESON_FLAGS = {
Matt Spinler8448cfd2020-09-10 17:01:48 -05001121 'phosphor-dbus-interfaces':
Patrick Williams13af98a2021-04-21 15:17:00 -05001122 ['-Ddata_com_ibm=true', '-Ddata_org_open_power=true'],
1123 'phosphor-logging':
1124 ['-Dopenpower-pel-extension=enabled']
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001125 }
1126
Matthew Barthccb7f852016-11-23 17:43:02 -06001127 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
1128 DEPENDENCIES = {
1129 'AC_CHECK_LIB': {'mapper': 'phosphor-objmgr'},
Matthew Barth710f3f02017-01-18 15:20:19 -06001130 'AC_CHECK_HEADER': {
1131 'host-ipmid': 'phosphor-host-ipmid',
Patrick Ventureb41a4462018-10-03 17:27:38 -07001132 'blobs-ipmid': 'phosphor-ipmi-blobs',
Matthew Barth710f3f02017-01-18 15:20:19 -06001133 'sdbusplus': 'sdbusplus',
William A. Kennington IIIb4f730a2018-09-12 11:21:20 -07001134 'sdeventplus': 'sdeventplus',
William A. Kennington III23705242019-01-15 18:17:25 -08001135 'stdplus': 'stdplus',
Patrick Venture22329962018-09-14 10:23:04 -07001136 'gpioplus': 'gpioplus',
Saqib Khan66145052017-02-14 12:02:07 -06001137 'phosphor-logging/log.hpp': 'phosphor-logging',
Patrick Williamseab8a372017-01-30 11:21:32 -06001138 },
Brad Bishopebb49112017-02-13 11:07:26 -05001139 'AC_PATH_PROG': {'sdbus++': 'sdbusplus'},
Patrick Williamseab8a372017-01-30 11:21:32 -06001140 'PKG_CHECK_MODULES': {
Matthew Barth19e261e2017-02-01 12:55:22 -06001141 'phosphor-dbus-interfaces': 'phosphor-dbus-interfaces',
William A. Kennington III993ab332019-02-07 02:12:31 -08001142 'libipmid': 'phosphor-host-ipmid',
1143 'libipmid-host': 'phosphor-host-ipmid',
Brad Bishopebb49112017-02-13 11:07:26 -05001144 'sdbusplus': 'sdbusplus',
William A. Kennington IIIb4f730a2018-09-12 11:21:20 -07001145 'sdeventplus': 'sdeventplus',
William A. Kennington III23705242019-01-15 18:17:25 -08001146 'stdplus': 'stdplus',
Patrick Venture22329962018-09-14 10:23:04 -07001147 'gpioplus': 'gpioplus',
Brad Bishopebb49112017-02-13 11:07:26 -05001148 'phosphor-logging': 'phosphor-logging',
Marri Devender Raoa3eee8a2018-08-13 05:34:27 -05001149 'phosphor-snmp': 'phosphor-snmp',
Patrick Venturee584c3b2019-03-07 09:44:16 -08001150 'ipmiblob': 'ipmi-blob-tool',
Ben Tyner300ed3c2020-05-14 15:30:37 -05001151 'hei': 'openpower-libhei',
Sui Chen40cf5212020-11-24 12:12:23 -08001152 'phosphor-ipmi-blobs': 'phosphor-ipmi-blobs',
Willy Tu01534ec2021-10-14 10:16:45 -07001153 'libcr51sign': 'google-misc',
Brad Bishopebb49112017-02-13 11:07:26 -05001154 },
Matthew Barthccb7f852016-11-23 17:43:02 -06001155 }
1156
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001157 # Offset into array of macro parameters MACRO(0, 1, ...N)
1158 DEPENDENCIES_OFFSET = {
1159 'AC_CHECK_LIB': 0,
1160 'AC_CHECK_HEADER': 0,
1161 'AC_PATH_PROG': 1,
1162 'PKG_CHECK_MODULES': 1,
1163 }
1164
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001165 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
1166 DEPENDENCIES_REGEX = {
Patrick Ventured37b8052018-10-16 16:03:03 -07001167 'phosphor-logging': r'\S+-dbus-interfaces$'
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001168 }
1169
Matthew Barth33df8792016-12-19 14:30:17 -06001170 # Set command line arguments
1171 parser = argparse.ArgumentParser()
1172 parser.add_argument("-w", "--workspace", dest="WORKSPACE", required=True,
1173 help="Workspace directory location(i.e. /home)")
1174 parser.add_argument("-p", "--package", dest="PACKAGE", required=True,
1175 help="OpenBMC package to be unit tested")
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001176 parser.add_argument("-t", "--test-only", dest="TEST_ONLY",
1177 action="store_true", required=False, default=False,
1178 help="Only run test cases, no other validation")
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001179 arg_inttests = parser.add_mutually_exclusive_group()
1180 arg_inttests.add_argument("--integration-tests", dest="INTEGRATION_TEST",
Lei YU08d2b922022-04-25 11:21:36 +08001181 action="store_true", required=False, default=True,
1182 help="Enable integration tests [default].")
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001183 arg_inttests.add_argument("--no-integration-tests", dest="INTEGRATION_TEST",
Lei YU08d2b922022-04-25 11:21:36 +08001184 action="store_false", required=False,
1185 help="Disable integration tests.")
Matthew Barth33df8792016-12-19 14:30:17 -06001186 parser.add_argument("-v", "--verbose", action="store_true",
1187 help="Print additional package status messages")
Andrew Jeffery468309d2018-03-08 13:46:33 +10301188 parser.add_argument("-r", "--repeat", help="Repeat tests N times",
1189 type=int, default=1)
Andrew Geisslera61acb52019-01-03 16:32:44 -06001190 parser.add_argument("-b", "--branch", dest="BRANCH", required=False,
1191 help="Branch to target for dependent repositories",
1192 default="master")
Lei YU7ef93302019-11-06 13:53:21 +08001193 parser.add_argument("-n", "--noformat", dest="FORMAT",
1194 action="store_false", required=False,
1195 help="Whether or not to run format code")
Matthew Barth33df8792016-12-19 14:30:17 -06001196 args = parser.parse_args(sys.argv[1:])
1197 WORKSPACE = args.WORKSPACE
1198 UNIT_TEST_PKG = args.PACKAGE
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001199 TEST_ONLY = args.TEST_ONLY
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001200 INTEGRATION_TEST = args.INTEGRATION_TEST
Andrew Geisslera61acb52019-01-03 16:32:44 -06001201 BRANCH = args.BRANCH
Lei YU7ef93302019-11-06 13:53:21 +08001202 FORMAT_CODE = args.FORMAT
Matthew Barth33df8792016-12-19 14:30:17 -06001203 if args.verbose:
1204 def printline(*line):
1205 for arg in line:
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301206 print(arg, end=' ')
1207 print()
Matthew Barth33df8792016-12-19 14:30:17 -06001208 else:
Andrew Jefferyfce557f2020-03-13 12:12:19 +10301209 def printline(*line):
1210 pass
Matthew Barthccb7f852016-11-23 17:43:02 -06001211
Patrick Williamsb6535952020-12-15 06:40:10 -06001212 CODE_SCAN_DIR = os.path.join(WORKSPACE, UNIT_TEST_PKG)
Lei YU7ef93302019-11-06 13:53:21 +08001213
James Feist878df5c2018-07-26 14:54:28 -07001214 # First validate code formatting if repo has style formatting files.
Adriana Kobylakbcee22b2018-01-10 16:58:27 -06001215 # The format-code.sh checks for these files.
Lei YU7ef93302019-11-06 13:53:21 +08001216 if FORMAT_CODE:
Patrick Williams5b4a1e82020-12-15 06:46:31 -06001217 format_scripts = find_file(['format-code.sh', 'format-code'],
1218 CODE_SCAN_DIR)
Andrew Geissler31502dd2021-01-07 14:04:02 -06001219
1220 # use default format-code.sh if no other found
1221 if not format_scripts:
1222 format_scripts.append(os.path.join(WORKSPACE, "format-code.sh"))
1223
Patrick Williams5b4a1e82020-12-15 06:46:31 -06001224 for f in format_scripts:
1225 check_call_cmd(f, CODE_SCAN_DIR)
Andrew Geisslera28286d2018-01-10 11:00:00 -08001226
Ed Tanous32768b82022-01-05 14:14:06 -08001227 # Check to see if any files changed
1228 check_call_cmd("git", "-C", CODE_SCAN_DIR,
1229 "--no-pager", "diff", "--exit-code")
1230
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001231 # Check if this repo has a supported make infrastructure
Patrick Williamsb6535952020-12-15 06:40:10 -06001232 pkg = Package(UNIT_TEST_PKG, CODE_SCAN_DIR)
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001233 if not pkg.build_system():
1234 print("No valid build system, exit")
1235 sys.exit(0)
1236
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301237 prev_umask = os.umask(000)
James Feist878df5c2018-07-26 14:54:28 -07001238
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301239 # Determine dependencies and add them
1240 dep_added = dict()
1241 dep_added[UNIT_TEST_PKG] = False
William A. Kennington III40d5c7c2018-12-13 14:37:59 -08001242
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301243 # Create dependency tree
1244 dep_tree = DepTree(UNIT_TEST_PKG)
Patrick Williamsb6535952020-12-15 06:40:10 -06001245 build_dep_tree(UNIT_TEST_PKG, CODE_SCAN_DIR, dep_added, dep_tree, BRANCH)
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001246
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301247 # Reorder Dependency Tree
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301248 for pkg_name, regex_str in DEPENDENCIES_REGEX.items():
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301249 dep_tree.ReorderDeps(pkg_name, regex_str)
1250 if args.verbose:
1251 dep_tree.PrintTree()
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001252
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301253 install_list = dep_tree.GetInstallList()
Andrew Geissler9ced4ed2019-11-18 14:33:58 -06001254
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301255 # We don't want to treat our package as a dependency
1256 install_list.remove(UNIT_TEST_PKG)
James Feist878df5c2018-07-26 14:54:28 -07001257
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301258 # Install reordered dependencies
1259 for dep in install_list:
1260 build_and_install(dep, False)
James Feist878df5c2018-07-26 14:54:28 -07001261
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301262 # Run package unit tests
1263 build_and_install(UNIT_TEST_PKG, True)
James Feist878df5c2018-07-26 14:54:28 -07001264
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301265 os.umask(prev_umask)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001266
1267 # Run any custom CI scripts the repo has, of which there can be
1268 # multiple of and anywhere in the repository.
Patrick Williamsb6535952020-12-15 06:40:10 -06001269 ci_scripts = find_file(['run-ci.sh', 'run-ci'], CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001270 if ci_scripts:
Patrick Williamsb6535952020-12-15 06:40:10 -06001271 os.chdir(CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001272 for ci_script in ci_scripts:
Patrick Williams55448ad2020-12-14 14:28:28 -06001273 check_call_cmd(ci_script)