blob: ceed9208614a054709285096d894ba42bfbb8a94 [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
Matthew Barthccb7f852016-11-23 17:43:02 -060019import os
20import sys
Matthew Barth33df8792016-12-19 14:30:17 -060021import argparse
William A. Kennington IIIa2156732018-06-30 18:38:09 -070022import multiprocessing
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050023import re
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -080024import subprocess
William A. Kennington III3f1d1202018-12-06 18:02:07 -080025import shutil
William A. Kennington III4e1d0a12018-07-16 12:04:03 -070026import platform
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -050027
28
29class DepTree():
30 """
31 Represents package dependency tree, where each node is a DepTree with a
32 name and DepTree children.
33 """
34
35 def __init__(self, name):
36 """
37 Create new DepTree.
38
39 Parameter descriptions:
40 name Name of new tree node.
41 """
42 self.name = name
43 self.children = list()
44
45 def AddChild(self, name):
46 """
47 Add new child node to current node.
48
49 Parameter descriptions:
50 name Name of new child
51 """
52 new_child = DepTree(name)
53 self.children.append(new_child)
54 return new_child
55
56 def AddChildNode(self, node):
57 """
58 Add existing child node to current node.
59
60 Parameter descriptions:
61 node Tree node to add
62 """
63 self.children.append(node)
64
65 def RemoveChild(self, name):
66 """
67 Remove child node.
68
69 Parameter descriptions:
70 name Name of child to remove
71 """
72 for child in self.children:
73 if child.name == name:
74 self.children.remove(child)
75 return
76
77 def GetNode(self, name):
78 """
79 Return node with matching name. Return None if not found.
80
81 Parameter descriptions:
82 name Name of node to return
83 """
84 if self.name == name:
85 return self
86 for child in self.children:
87 node = child.GetNode(name)
88 if node:
89 return node
90 return None
91
92 def GetParentNode(self, name, parent_node=None):
93 """
94 Return parent of node with matching name. Return none if not found.
95
96 Parameter descriptions:
97 name Name of node to get parent of
98 parent_node Parent of current node
99 """
100 if self.name == name:
101 return parent_node
102 for child in self.children:
103 found_node = child.GetParentNode(name, self)
104 if found_node:
105 return found_node
106 return None
107
108 def GetPath(self, name, path=None):
109 """
110 Return list of node names from head to matching name.
111 Return None if not found.
112
113 Parameter descriptions:
114 name Name of node
115 path List of node names from head to current node
116 """
117 if not path:
118 path = []
119 if self.name == name:
120 path.append(self.name)
121 return path
122 for child in self.children:
123 match = child.GetPath(name, path + [self.name])
124 if match:
125 return match
126 return None
127
128 def GetPathRegex(self, name, regex_str, path=None):
129 """
130 Return list of node paths that end in name, or match regex_str.
131 Return empty list if not found.
132
133 Parameter descriptions:
134 name Name of node to search for
135 regex_str Regex string to match node names
136 path Path of node names from head to current node
137 """
138 new_paths = []
139 if not path:
140 path = []
141 match = re.match(regex_str, self.name)
142 if (self.name == name) or (match):
143 new_paths.append(path + [self.name])
144 for child in self.children:
145 return_paths = None
146 full_path = path + [self.name]
147 return_paths = child.GetPathRegex(name, regex_str, full_path)
148 for i in return_paths:
149 new_paths.append(i)
150 return new_paths
151
152 def MoveNode(self, from_name, to_name):
153 """
154 Mode existing from_name node to become child of to_name node.
155
156 Parameter descriptions:
157 from_name Name of node to make a child of to_name
158 to_name Name of node to make parent of from_name
159 """
160 parent_from_node = self.GetParentNode(from_name)
161 from_node = self.GetNode(from_name)
162 parent_from_node.RemoveChild(from_name)
163 to_node = self.GetNode(to_name)
164 to_node.AddChildNode(from_node)
165
166 def ReorderDeps(self, name, regex_str):
167 """
168 Reorder dependency tree. If tree contains nodes with names that
169 match 'name' and 'regex_str', move 'regex_str' nodes that are
170 to the right of 'name' node, so that they become children of the
171 'name' node.
172
173 Parameter descriptions:
174 name Name of node to look for
175 regex_str Regex string to match names to
176 """
177 name_path = self.GetPath(name)
178 if not name_path:
179 return
180 paths = self.GetPathRegex(name, regex_str)
181 is_name_in_paths = False
182 name_index = 0
183 for i in range(len(paths)):
184 path = paths[i]
185 if path[-1] == name:
186 is_name_in_paths = True
187 name_index = i
188 break
189 if not is_name_in_paths:
190 return
191 for i in range(name_index + 1, len(paths)):
192 path = paths[i]
193 if name in path:
194 continue
195 from_name = path[-1]
196 self.MoveNode(from_name, name)
197
198 def GetInstallList(self):
199 """
200 Return post-order list of node names.
201
202 Parameter descriptions:
203 """
204 install_list = []
205 for child in self.children:
206 child_install_list = child.GetInstallList()
207 install_list.extend(child_install_list)
208 install_list.append(self.name)
209 return install_list
210
211 def PrintTree(self, level=0):
212 """
213 Print pre-order node names with indentation denoting node depth level.
214
215 Parameter descriptions:
216 level Current depth level
217 """
218 INDENT_PER_LEVEL = 4
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030219 print(' ' * (level * INDENT_PER_LEVEL) + self.name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500220 for child in self.children:
221 child.PrintTree(level + 1)
Matthew Barth33df8792016-12-19 14:30:17 -0600222
223
William A. Kennington III1fddb972019-02-06 18:03:53 -0800224def check_call_cmd(*cmd):
Matthew Barth33df8792016-12-19 14:30:17 -0600225 """
226 Verbose prints the directory location the given command is called from and
227 the command, then executes the command using check_call.
228
229 Parameter descriptions:
230 dir Directory location command is to be called from
231 cmd List of parameters constructing the complete command
232 """
William A. Kennington III1fddb972019-02-06 18:03:53 -0800233 printline(os.getcwd(), ">", " ".join(cmd))
Matthew Barth33df8792016-12-19 14:30:17 -0600234 check_call(cmd)
Matthew Barthccb7f852016-11-23 17:43:02 -0600235
236
Andrew Geisslera61acb52019-01-03 16:32:44 -0600237def clone_pkg(pkg, branch):
Matthew Barth33df8792016-12-19 14:30:17 -0600238 """
239 Clone the given openbmc package's git repository from gerrit into
240 the WORKSPACE location
241
242 Parameter descriptions:
243 pkg Name of the package to clone
Andrew Geisslera61acb52019-01-03 16:32:44 -0600244 branch Branch to clone from pkg
Matthew Barth33df8792016-12-19 14:30:17 -0600245 """
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030246 pkg_dir = os.path.join(WORKSPACE, pkg)
247 if os.path.exists(os.path.join(pkg_dir, '.git')):
248 return pkg_dir
Andrew Geissler37052412022-05-20 15:28:26 -0400249 pkg_repo = urljoin('https://gerrit.openbmc.org/openbmc/', pkg)
Andrew Jeffery7be94ca2018-03-08 13:15:33 +1030250 os.mkdir(pkg_dir)
Andrew Geisslera61acb52019-01-03 16:32:44 -0600251 printline(pkg_dir, "> git clone", pkg_repo, branch, "./")
252 try:
253 # first try the branch
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030254 clone = Repo.clone_from(pkg_repo, pkg_dir, branch=branch)
255 repo_inst = clone.working_dir
Andrew Geisslera61acb52019-01-03 16:32:44 -0600256 except:
257 printline("Input branch not found, default to master")
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030258 clone = Repo.clone_from(pkg_repo, pkg_dir, branch="master")
259 repo_inst = clone.working_dir
Andrew Geisslera61acb52019-01-03 16:32:44 -0600260 return repo_inst
Matthew Barth33df8792016-12-19 14:30:17 -0600261
262
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030263def make_target_exists(target):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800264 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030265 Runs a check against the makefile in the current directory to determine
266 if the target exists so that it can be built.
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800267
268 Parameter descriptions:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030269 target The make target we are checking
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800270 """
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030271 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030272 cmd = ['make', '-n', target]
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030273 with open(os.devnull, 'w') as devnull:
274 check_call(cmd, stdout=devnull, stderr=devnull)
275 return True
276 except CalledProcessError:
277 return False
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800278
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800279
William A. Kennington IIIa2156732018-06-30 18:38:09 -0700280make_parallel = [
281 'make',
282 # Run enough jobs to saturate all the cpus
283 '-j', str(multiprocessing.cpu_count()),
284 # Don't start more jobs if the load avg is too high
285 '-l', str(multiprocessing.cpu_count()),
286 # Synchronize the output so logs aren't intermixed in stdout / stderr
287 '-O',
288]
289
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800290
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030291def build_and_install(name, build_for_testing=False):
William A. Kennington III780ec092018-12-06 14:46:50 -0800292 """
293 Builds and installs the package in the environment. Optionally
294 builds the examples and test cases for package.
295
296 Parameter description:
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030297 name The name of the package we are building
William A. Kennington IIIa0454912018-12-06 14:47:16 -0800298 build_for_testing Enable options related to testing on the package?
William A. Kennington III780ec092018-12-06 14:46:50 -0800299 """
Andrew Jefferyff5c5d52020-03-13 10:12:14 +1030300 os.chdir(os.path.join(WORKSPACE, name))
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800301
302 # Refresh dynamic linker run time bindings for dependencies
William A. Kennington III1fddb972019-02-06 18:03:53 -0800303 check_call_cmd('sudo', '-n', '--', 'ldconfig')
William A. Kennington III54d4faf2018-12-06 17:46:24 -0800304
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030305 pkg = Package()
306 if build_for_testing:
307 pkg.test()
William A. Kennington III3f1d1202018-12-06 18:02:07 -0800308 else:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030309 pkg.install()
310
William A. Kennington III780ec092018-12-06 14:46:50 -0800311
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030312def build_dep_tree(name, pkgdir, dep_added, head, branch, dep_tree=None):
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500313 """
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030314 For each package (name), starting with the package to be unit tested,
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030315 extract its dependencies. For each package dependency defined, recursively
316 apply the same strategy
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500317
318 Parameter descriptions:
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030319 name Name of the package
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500320 pkgdir Directory where package source is located
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800321 dep_added Current dict of dependencies and added status
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500322 head Head node of the dependency tree
Andrew Geisslera61acb52019-01-03 16:32:44 -0600323 branch Branch to clone from pkg
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500324 dep_tree Current dependency tree node
325 """
326 if not dep_tree:
327 dep_tree = head
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800328
William A. Kennington IIIbe6aab22018-12-06 15:01:54 -0800329 with open("/tmp/depcache", "r") as depcache:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800330 cache = depcache.readline()
331
332 # Read out pkg dependencies
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030333 pkg = Package(name, pkgdir)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800334
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600335 build = pkg.build_system()
336 if build == None:
337 raise Exception(f"Unable to find build system for {name}.")
338
339 for dep in set(build.dependencies()):
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800340 if dep in cache:
341 continue
342 # Dependency package not already known
343 if dep_added.get(dep) is None:
Patrick Williams38c6b7d2021-02-18 07:26:14 -0600344 print(f"Adding {dep} dependency to {name}.")
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800345 # Dependency package not added
346 new_child = dep_tree.AddChild(dep)
347 dep_added[dep] = False
Andrew Jeffery3b92fdd2020-03-13 11:49:18 +1030348 dep_pkgdir = clone_pkg(dep, branch)
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800349 # Determine this dependency package's
350 # dependencies and add them before
351 # returning to add this package
352 dep_added = build_dep_tree(dep,
353 dep_pkgdir,
354 dep_added,
355 head,
Andrew Geisslera61acb52019-01-03 16:32:44 -0600356 branch,
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800357 new_child)
358 else:
359 # Dependency package known and added
360 if dep_added[dep]:
Andrew Jeffery2cb0c7a2018-03-08 13:19:08 +1030361 continue
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500362 else:
William A. Kennington IIIc048cc02018-12-06 15:39:18 -0800363 # Cyclic dependency failure
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030364 raise Exception("Cyclic dependencies found in "+name)
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500365
Andrew Jefferyccf85d62020-03-13 10:25:42 +1030366 if not dep_added[name]:
367 dep_added[name] = True
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -0500368
369 return dep_added
Matthew Barthccb7f852016-11-23 17:43:02 -0600370
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700371
William A. Kennington III90b106a2019-02-06 18:08:24 -0800372def run_cppcheck():
Ed Tanousc7198552022-07-01 08:15:50 -0700373 if not os.path.exists(os.path.join("build", "compile_commands.json")):
Brad Bishop48424d42020-01-07 13:01:31 -0500374 return None
375
Ed Tanousc7198552022-07-01 08:15:50 -0700376 try:
377 os.mkdir("cppcheck-temp")
378 except FileExistsError as e:
379 pass
380
Brad Bishop48424d42020-01-07 13:01:31 -0500381 # http://cppcheck.sourceforge.net/manual.pdf
Ed Tanousc7198552022-07-01 08:15:50 -0700382 try:
383 check_call_cmd(
384 'cppcheck',
385 '-j', str(multiprocessing.cpu_count()),
386 '--enable=style,performance,portability,missingInclude',
387 '--suppress=useStlAlgorithm',
388 '--suppress=unusedStructMember',
389 '--suppress=postfixOperator',
390 '--suppress=unreadVariable',
391 '--suppress=knownConditionTrueFalse',
392 '--library=googletest',
393 '--project=build/compile_commands.json',
394 '--cppcheck-build-dir=cppcheck-temp',
395 )
396 except subprocess.CalledProcessError:
397 print("cppcheck found errors")
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 """
William A. Kennington III0326ded2019-02-07 00:33:28 -0800403 src = 'unit-test-vg.c'
404 exe = './unit-test-vg'
405 with open(src, 'w') as h:
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700406 h.write('#include <errno.h>\n')
407 h.write('#include <stdio.h>\n')
William A. Kennington III0326ded2019-02-07 00:33:28 -0800408 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')
412 h.write('strcpy(heap_str, "RandString");\n')
413 h.write('int res = strcmp("RandString", heap_str);\n')
414 h.write('free(heap_str);\n')
William A. Kennington IIIafb0f982019-04-26 17:30:28 -0700415 h.write('char errstr[64];\n')
416 h.write('strerror_r(EINVAL, errstr, sizeof(errstr));\n')
417 h.write('printf("%s\\n", errstr);\n')
William A. Kennington III0326ded2019-02-07 00:33:28 -0800418 h.write('return res;\n')
419 h.write('}\n')
420 try:
421 with open(os.devnull, 'w') as devnull:
422 check_call(['gcc', '-O2', '-o', exe, src],
423 stdout=devnull, stderr=devnull)
424 check_call(['valgrind', '--error-exitcode=99', exe],
425 stdout=devnull, stderr=devnull)
426 return True
427 except:
428 sys.stderr.write("###### Platform is not valgrind safe ######\n")
429 return False
430 finally:
431 os.remove(src)
432 os.remove(exe)
William A. Kennington III37a89a22018-12-13 14:32:02 -0800433
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030434
William A. Kennington III282e3302019-02-04 16:55:05 -0800435def is_sanitize_safe():
436 """
437 Returns whether it is safe to run sanitizers on our platform
438 """
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800439 src = 'unit-test-sanitize.c'
440 exe = './unit-test-sanitize'
441 with open(src, 'w') as h:
442 h.write('int main() { return 0; }\n')
443 try:
444 with open(os.devnull, 'w') as devnull:
445 check_call(['gcc', '-O2', '-fsanitize=address',
446 '-fsanitize=undefined', '-o', exe, src],
447 stdout=devnull, stderr=devnull)
448 check_call([exe], stdout=devnull, stderr=devnull)
Andrew Geisslercd9578b2021-05-04 10:18:01 -0500449
450 # TODO - Sanitizer not working on ppc64le
451 # https://github.com/openbmc/openbmc-build-scripts/issues/31
452 if (platform.processor() == 'ppc64le'):
453 sys.stderr.write("###### ppc64le is not sanitize safe ######\n")
454 return False
455 else:
456 return True
William A. Kennington III0b7fb2b2019-02-07 00:33:42 -0800457 except:
458 sys.stderr.write("###### Platform is not sanitize safe ######\n")
459 return False
460 finally:
461 os.remove(src)
462 os.remove(exe)
William A. Kennington III282e3302019-02-04 16:55:05 -0800463
William A. Kennington III49d4e592019-02-06 17:59:27 -0800464
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800465def maybe_make_valgrind():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700466 """
467 Potentially runs the unit tests through valgrind for the package
468 via `make check-valgrind`. If the package does not have valgrind testing
469 then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700470 """
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700471 # Valgrind testing is currently broken by an aggressive strcmp optimization
472 # that is inlined into optimized code for POWER by gcc 7+. Until we find
473 # a workaround, just don't run valgrind tests on POWER.
474 # https://github.com/openbmc/openbmc/issues/3315
William A. Kennington III37a89a22018-12-13 14:32:02 -0800475 if not is_valgrind_safe():
William A. Kennington III75130192019-02-07 00:34:14 -0800476 sys.stderr.write("###### Skipping valgrind ######\n")
William A. Kennington III4e1d0a12018-07-16 12:04:03 -0700477 return
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700478 if not make_target_exists('check-valgrind'):
479 return
480
481 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030482 cmd = make_parallel + ['check-valgrind']
William A. Kennington III1fddb972019-02-06 18:03:53 -0800483 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700484 except CalledProcessError:
William A. Kennington III90b106a2019-02-06 18:08:24 -0800485 for root, _, files in os.walk(os.getcwd()):
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700486 for f in files:
487 if re.search('test-suite-[a-z]+.log', f) is None:
488 continue
William A. Kennington III1fddb972019-02-06 18:03:53 -0800489 check_call_cmd('cat', os.path.join(root, f))
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700490 raise Exception('Valgrind tests failed')
491
Andrew Jefferye5fffa02020-03-13 11:51:57 +1030492
William A. Kennington IIIeaff24a2019-02-06 16:57:42 -0800493def maybe_make_coverage():
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700494 """
495 Potentially runs the unit tests through code coverage for the package
496 via `make check-code-coverage`. If the package does not have code coverage
497 testing then it just skips over this.
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700498 """
499 if not make_target_exists('check-code-coverage'):
500 return
501
502 # Actually run code coverage
503 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030504 cmd = make_parallel + ['check-code-coverage']
William A. Kennington III1fddb972019-02-06 18:03:53 -0800505 check_call_cmd(*cmd)
William A. Kennington III0f0a6802018-07-16 11:52:33 -0700506 except CalledProcessError:
507 raise Exception('Code coverage failed')
Matthew Barthccb7f852016-11-23 17:43:02 -0600508
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030509
510class BuildSystem(object):
511 """
512 Build systems generally provide the means to configure, build, install and
513 test software. The BuildSystem class defines a set of interfaces on top of
514 which Autotools, Meson, CMake and possibly other build system drivers can
515 be implemented, separating out the phases to control whether a package
516 should merely be installed or also tested and analyzed.
517 """
Lei YU08d2b922022-04-25 11:21:36 +0800518
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030519 def __init__(self, package, path):
520 """Initialise the driver with properties independent of the build system
521
522 Keyword arguments:
523 package: The name of the package. Derived from the path if None
524 path: The path to the package. Set to the working directory if None
525 """
526 self.path = "." if not path else path
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030527 realpath = os.path.realpath(self.path)
528 self.package = package if package else os.path.basename(realpath)
Andrew Jeffery3f8e5a72020-03-13 11:47:56 +1030529 self.build_for_testing = False
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030530
531 def probe(self):
532 """Test if the build system driver can be applied to the package
533
534 Return True if the driver can drive the package's build system,
535 otherwise False.
536
537 Generally probe() is implemented by testing for the presence of the
538 build system's configuration file(s).
539 """
540 raise NotImplemented
541
542 def dependencies(self):
543 """Provide the package's dependencies
544
545 Returns a list of dependencies. If no dependencies are required then an
546 empty list must be returned.
547
548 Generally dependencies() is implemented by analysing and extracting the
549 data from the build system configuration.
550 """
551 raise NotImplemented
552
553 def configure(self, build_for_testing):
554 """Configure the source ready for building
555
556 Should raise an exception if configuration failed.
557
558 Keyword arguments:
559 build_for_testing: Mark the package as being built for testing rather
560 than for installation as a dependency for the
561 package under test. Setting to True generally
562 implies that the package will be configured to build
563 with debug information, at a low level of
564 optimisation and possibly with sanitizers enabled.
565
566 Generally configure() is implemented by invoking the build system
567 tooling to generate Makefiles or equivalent.
568 """
569 raise NotImplemented
570
571 def build(self):
572 """Build the software ready for installation and/or testing
573
574 Should raise an exception if the build fails
575
576 Generally build() is implemented by invoking `make` or `ninja`.
577 """
578 raise NotImplemented
579
580 def install(self):
581 """Install the software ready for use
582
583 Should raise an exception if installation fails
584
585 Like build(), install() is generally implemented by invoking `make` or
586 `ninja`.
587 """
588 raise NotImplemented
589
590 def test(self):
591 """Build and run the test suite associated with the package
592
593 Should raise an exception if the build or testing fails.
594
595 Like install(), test() is generally implemented by invoking `make` or
596 `ninja`.
597 """
598 raise NotImplemented
599
600 def analyze(self):
601 """Run any supported analysis tools over the codebase
602
603 Should raise an exception if analysis fails.
604
605 Some analysis tools such as scan-build need injection into the build
606 system. analyze() provides the necessary hook to implement such
607 behaviour. Analyzers independent of the build system can also be
608 specified here but at the cost of possible duplication of code between
609 the build system driver implementations.
610 """
611 raise NotImplemented
612
613
614class Autotools(BuildSystem):
615 def __init__(self, package=None, path=None):
616 super(Autotools, self).__init__(package, path)
617
618 def probe(self):
619 return os.path.isfile(os.path.join(self.path, 'configure.ac'))
620
621 def dependencies(self):
622 configure_ac = os.path.join(self.path, 'configure.ac')
623
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030624 contents = ''
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030625 # Prepend some special function overrides so we can parse out
626 # dependencies
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030627 for macro in DEPENDENCIES.keys():
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030628 contents += ('m4_define([' + macro + '], [' + macro + '_START$' +
629 str(DEPENDENCIES_OFFSET[macro] + 1) +
630 macro + '_END])\n')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030631 with open(configure_ac, "rt") as f:
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030632 contents += f.read()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030633
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030634 autoconf_cmdline = ['autoconf', '-Wno-undefined', '-']
635 autoconf_process = subprocess.Popen(autoconf_cmdline,
Andrew Jeffery7d4a26f2020-03-13 11:42:34 +1030636 stdin=subprocess.PIPE,
637 stdout=subprocess.PIPE,
638 stderr=subprocess.PIPE)
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030639 document = contents.encode('utf-8')
640 (stdout, stderr) = autoconf_process.communicate(input=document)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030641 if not stdout:
642 print(stderr)
643 raise Exception("Failed to run autoconf for parsing dependencies")
644
645 # Parse out all of the dependency text
646 matches = []
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030647 for macro in DEPENDENCIES.keys():
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030648 pattern = '(' + macro + ')_START(.*?)' + macro + '_END'
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030649 for match in re.compile(pattern).finditer(stdout.decode('utf-8')):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030650 matches.append((match.group(1), match.group(2)))
651
652 # Look up dependencies from the text
653 found_deps = []
654 for macro, deptext in matches:
655 for potential_dep in deptext.split(' '):
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030656 for known_dep in DEPENDENCIES[macro].keys():
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030657 if potential_dep.startswith(known_dep):
658 found_deps.append(DEPENDENCIES[macro][known_dep])
659
660 return found_deps
661
662 def _configure_feature(self, flag, enabled):
663 """
664 Returns an configure flag as a string
665
666 Parameters:
667 flag The name of the flag
668 enabled Whether the flag is enabled or disabled
669 """
670 return '--' + ('enable' if enabled else 'disable') + '-' + flag
671
672 def configure(self, build_for_testing):
673 self.build_for_testing = build_for_testing
674 conf_flags = [
675 self._configure_feature('silent-rules', False),
676 self._configure_feature('examples', build_for_testing),
677 self._configure_feature('tests', build_for_testing),
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000678 self._configure_feature('itests', INTEGRATION_TEST),
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030679 ]
Andrew Jefferyd0809692021-05-14 16:23:57 +0930680 conf_flags.extend([
681 self._configure_feature('code-coverage', build_for_testing),
682 self._configure_feature('valgrind', build_for_testing),
683 ])
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030684 # Add any necessary configure flags for package
685 if CONFIGURE_FLAGS.get(self.package) is not None:
686 conf_flags.extend(CONFIGURE_FLAGS.get(self.package))
687 for bootstrap in ['bootstrap.sh', 'bootstrap', 'autogen.sh']:
688 if os.path.exists(bootstrap):
689 check_call_cmd('./' + bootstrap)
690 break
691 check_call_cmd('./configure', *conf_flags)
692
693 def build(self):
694 check_call_cmd(*make_parallel)
695
696 def install(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +1030697 check_call_cmd('sudo', '-n', '--', *(make_parallel + ['install']))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030698
699 def test(self):
700 try:
Andrew Jeffery881041f2020-03-13 11:46:30 +1030701 cmd = make_parallel + ['check']
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030702 for i in range(0, args.repeat):
703 check_call_cmd(*cmd)
Andrew Jefferyd0809692021-05-14 16:23:57 +0930704
705 maybe_make_valgrind()
706 maybe_make_coverage()
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030707 except CalledProcessError:
708 for root, _, files in os.walk(os.getcwd()):
709 if 'test-suite.log' not in files:
710 continue
711 check_call_cmd('cat', os.path.join(root, 'test-suite.log'))
712 raise Exception('Unit tests failed')
713
714 def analyze(self):
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030715 run_cppcheck()
716
717
718class CMake(BuildSystem):
719 def __init__(self, package=None, path=None):
720 super(CMake, self).__init__(package, path)
721
722 def probe(self):
723 return os.path.isfile(os.path.join(self.path, 'CMakeLists.txt'))
724
725 def dependencies(self):
726 return []
727
728 def configure(self, build_for_testing):
729 self.build_for_testing = build_for_testing
Ramin Izadpanah298d56b2020-11-17 16:55:28 +0000730 if INTEGRATION_TEST:
731 check_call_cmd('cmake', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
732 '-DITESTS=ON', '.')
733 else:
734 check_call_cmd('cmake', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON', '.')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030735
736 def build(self):
737 check_call_cmd('cmake', '--build', '.', '--', '-j',
738 str(multiprocessing.cpu_count()))
739
740 def install(self):
741 pass
742
743 def test(self):
744 if make_target_exists('test'):
745 check_call_cmd('ctest', '.')
746
747 def analyze(self):
748 if os.path.isfile('.clang-tidy'):
Ed Tanousbc5c1c42020-10-01 17:49:20 -0700749 try:
750 os.mkdir("tidy-build")
751 except FileExistsError as e:
752 pass
753 # clang-tidy needs to run on a clang-specific build
754 check_call_cmd('cmake', '-DCMAKE_C_COMPILER=clang',
755 '-DCMAKE_CXX_COMPILER=clang++',
756 '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
757 '-H.',
758 '-Btidy-build')
759 # we need to cd here because otherwise clang-tidy doesn't find the
760 # .clang-tidy file in the roots of repos. Its arguably a "bug"
761 # with run-clang-tidy at a minimum it's "weird" that it requires
762 # the .clang-tidy to be up a dir
Ed Tanous662890f2020-09-28 17:35:20 -0700763 os.chdir("tidy-build")
764 try:
Patrick Williamsf89c8502021-10-01 11:50:09 -0500765 check_call_cmd('run-clang-tidy', "-header-filter=.*", '-p',
Ed Tanousbc5c1c42020-10-01 17:49:20 -0700766 '.')
Ed Tanous662890f2020-09-28 17:35:20 -0700767 finally:
768 os.chdir("..")
769
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030770 maybe_make_valgrind()
771 maybe_make_coverage()
772 run_cppcheck()
773
774
775class Meson(BuildSystem):
776 def __init__(self, package=None, path=None):
777 super(Meson, self).__init__(package, path)
778
779 def probe(self):
780 return os.path.isfile(os.path.join(self.path, 'meson.build'))
781
782 def dependencies(self):
783 meson_build = os.path.join(self.path, 'meson.build')
784 if not os.path.exists(meson_build):
785 return []
786
787 found_deps = []
788 for root, dirs, files in os.walk(self.path):
789 if 'meson.build' not in files:
790 continue
791 with open(os.path.join(root, 'meson.build'), 'rt') as f:
792 build_contents = f.read()
Nan Zhouef13d532020-07-07 09:52:02 -0700793 pattern = r"dependency\('([^']*)'.*?\),?\n"
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030794 for match in re.finditer(pattern, build_contents):
795 group = match.group(1)
796 maybe_dep = DEPENDENCIES['PKG_CHECK_MODULES'].get(group)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030797 if maybe_dep is not None:
798 found_deps.append(maybe_dep)
799
800 return found_deps
801
802 def _parse_options(self, options_file):
803 """
804 Returns a set of options defined in the provides meson_options.txt file
805
806 Parameters:
807 options_file The file containing options
808 """
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700809 oi = optinterpreter.OptionInterpreter('')
810 oi.process(options_file)
811 return oi.options
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030812
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700813 def _configure_boolean(self, val):
814 """
815 Returns the meson flag which signifies the value
816
817 True is true which requires the boolean.
818 False is false which disables the boolean.
819
820 Parameters:
821 val The value being converted
822 """
823 if val is True:
824 return 'true'
825 elif val is False:
826 return 'false'
827 else:
828 raise Exception("Bad meson boolean value")
829
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030830 def _configure_feature(self, val):
831 """
832 Returns the meson flag which signifies the value
833
834 True is enabled which requires the feature.
835 False is disabled which disables the feature.
836 None is auto which autodetects the feature.
837
838 Parameters:
839 val The value being converted
840 """
841 if val is True:
842 return "enabled"
843 elif val is False:
844 return "disabled"
845 elif val is None:
846 return "auto"
847 else:
848 raise Exception("Bad meson feature value")
849
William A. Kennington IIIfcd70772020-06-04 00:50:23 -0700850 def _configure_option(self, opts, key, val):
851 """
852 Returns the meson flag which signifies the value
853 based on the type of the opt
854
855 Parameters:
856 opt The meson option which we are setting
857 val The value being converted
858 """
859 if isinstance(opts[key], coredata.UserBooleanOption):
860 str_val = self._configure_boolean(val)
861 elif isinstance(opts[key], coredata.UserFeatureOption):
862 str_val = self._configure_feature(val)
863 else:
864 raise Exception('Unknown meson option type')
865 return "-D{}={}".format(key, str_val)
866
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030867 def configure(self, build_for_testing):
868 self.build_for_testing = build_for_testing
William A. Kennington III0ecd0bc2020-06-04 00:44:22 -0700869 meson_options = {}
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030870 if os.path.exists("meson_options.txt"):
871 meson_options = self._parse_options("meson_options.txt")
872 meson_flags = [
873 '-Db_colorout=never',
874 '-Dwerror=true',
875 '-Dwarning_level=3',
876 ]
877 if build_for_testing:
878 meson_flags.append('--buildtype=debug')
879 else:
880 meson_flags.append('--buildtype=debugoptimized')
Patrick Williamse95626d2021-04-13 11:13:53 -0500881 if OptionKey('tests') in meson_options:
Lei YU08d2b922022-04-25 11:21:36 +0800882 meson_flags.append(self._configure_option(
883 meson_options, OptionKey('tests'), build_for_testing))
Patrick Williamse95626d2021-04-13 11:13:53 -0500884 if OptionKey('examples') in meson_options:
Lei YU08d2b922022-04-25 11:21:36 +0800885 meson_flags.append(self._configure_option(
886 meson_options, OptionKey('examples'), build_for_testing))
Patrick Williamse95626d2021-04-13 11:13:53 -0500887 if OptionKey('itests') in meson_options:
Lei YU08d2b922022-04-25 11:21:36 +0800888 meson_flags.append(self._configure_option(
889 meson_options, OptionKey('itests'), INTEGRATION_TEST))
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030890 if MESON_FLAGS.get(self.package) is not None:
891 meson_flags.extend(MESON_FLAGS.get(self.package))
892 try:
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030893 check_call_cmd('meson', 'setup', '--reconfigure', 'build',
894 *meson_flags)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030895 except:
896 shutil.rmtree('build')
897 check_call_cmd('meson', 'setup', 'build', *meson_flags)
898
899 def build(self):
900 check_call_cmd('ninja', '-C', 'build')
901
902 def install(self):
903 check_call_cmd('sudo', '-n', '--', 'ninja', '-C', 'build', 'install')
904
905 def test(self):
Patrick Williams95095f12021-04-14 14:42:35 -0500906 # It is useful to check various settings of the meson.build file
907 # for compatibility, such as meson_version checks. We shouldn't
908 # do this in the configure path though because it affects subprojects
909 # and dependencies as well, but we only want this applied to the
910 # project-under-test (otherwise an upstream dependency could fail
911 # this check without our control).
912 self._extra_meson_checks()
913
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030914 try:
Brad Bishop7b8cef22020-08-26 15:58:09 -0400915 test_args = ('--repeat', str(args.repeat), '-C', 'build')
William A. Kennington IIId9062c42022-05-04 23:24:44 -0700916 check_call_cmd('meson', 'test', '--print-errorlogs', *test_args)
Brad Bishop7b8cef22020-08-26 15:58:09 -0400917
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030918 except CalledProcessError:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030919 raise Exception('Unit tests failed')
920
921 def _setup_exists(self, setup):
922 """
923 Returns whether the meson build supports the named test setup.
924
925 Parameter descriptions:
926 setup The setup target to check
927 """
928 try:
929 with open(os.devnull, 'w') as devnull:
930 output = subprocess.check_output(
Lei YU08d2b922022-04-25 11:21:36 +0800931 ['meson', 'test', '-C', 'build',
932 '--setup', setup, '-t', '0'],
933 stderr=subprocess.STDOUT)
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030934 except CalledProcessError as e:
935 output = e.output
Andrew Jeffery89b64b62020-03-13 12:15:48 +1030936 output = output.decode('utf-8')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030937 return not re.search('Test setup .* not found from project', output)
938
939 def _maybe_valgrind(self):
940 """
941 Potentially runs the unit tests through valgrind for the package
Andrew Jeffery47fbfa52020-03-13 12:05:09 +1030942 via `meson test`. The package can specify custom valgrind
943 configurations by utilizing add_test_setup() in a meson.build
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030944 """
945 if not is_valgrind_safe():
946 sys.stderr.write("###### Skipping valgrind ######\n")
947 return
948 try:
949 if self._setup_exists('valgrind'):
Lei YU08d2b922022-04-25 11:21:36 +0800950 check_call_cmd('meson', 'test', '-t', '10', '-C', 'build',
William A. Kennington IIId9062c42022-05-04 23:24:44 -0700951 '--print-errorlogs', '--setup', 'valgrind')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030952 else:
Lei YU08d2b922022-04-25 11:21:36 +0800953 check_call_cmd('meson', 'test', '-t', '10', '-C', 'build',
William A. Kennington IIId9062c42022-05-04 23:24:44 -0700954 '--print-errorlogs', '--wrapper', 'valgrind')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030955 except CalledProcessError:
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030956 raise Exception('Valgrind tests failed')
957
958 def analyze(self):
959 self._maybe_valgrind()
960
961 # Run clang-tidy only if the project has a configuration
962 if os.path.isfile('.clang-tidy'):
Manojkiran Eda1aa91992020-10-02 14:11:53 +0530963 os.environ["CXX"] = "clang++"
964 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("..")
974
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030975 # Run the basic clang static analyzer otherwise
976 else:
977 check_call_cmd('ninja', '-C', 'build',
978 'scan-build')
979
980 # Run tests through sanitizers
981 # b_lundef is needed if clang++ is CXX since it resolves the
982 # asan symbols at runtime only. We don't want to set it earlier
983 # in the build process to ensure we don't have undefined
984 # runtime code.
985 if is_sanitize_safe():
986 check_call_cmd('meson', 'configure', 'build',
987 '-Db_sanitize=address,undefined',
988 '-Db_lundef=false')
William A. Kennington IIId9062c42022-05-04 23:24:44 -0700989 check_call_cmd('meson', 'test', '-C', 'build', '--print-errorlogs',
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030990 '--logbase', 'testlog-ubasan')
991 # TODO: Fix memory sanitizer
Andrew Jeffery6015ca12020-03-13 11:50:10 +1030992 # check_call_cmd('meson', 'configure', 'build',
993 # '-Db_sanitize=memory')
994 # check_call_cmd('meson', 'test', '-C', 'build'
995 # '--logbase', 'testlog-msan')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030996 check_call_cmd('meson', 'configure', 'build',
Lei YUf684ad62020-09-21 17:02:26 +0800997 '-Db_sanitize=none')
Andrew Jeffery15e423e2020-03-11 16:51:28 +1030998 else:
999 sys.stderr.write("###### Skipping sanitizers ######\n")
1000
1001 # Run coverage checks
1002 check_call_cmd('meson', 'configure', 'build',
1003 '-Db_coverage=true')
1004 self.test()
1005 # Only build coverage HTML if coverage files were produced
1006 for root, dirs, files in os.walk('build'):
1007 if any([f.endswith('.gcda') for f in files]):
1008 check_call_cmd('ninja', '-C', 'build',
1009 'coverage-html')
1010 break
1011 check_call_cmd('meson', 'configure', 'build',
1012 '-Db_coverage=false')
1013 run_cppcheck()
1014
Patrick Williams95095f12021-04-14 14:42:35 -05001015 def _extra_meson_checks(self):
1016 with open(os.path.join(self.path, 'meson.build'), 'rt') as f:
1017 build_contents = f.read()
1018
1019 # Find project's specified meson_version.
1020 meson_version = None
1021 pattern = r"meson_version:[^']*'([^']*)'"
1022 for match in re.finditer(pattern, build_contents):
1023 group = match.group(1)
1024 meson_version = group
1025
1026 # C++20 requires at least Meson 0.57 but Meson itself doesn't
1027 # identify this. Add to our unit-test checks so that we don't
1028 # get a meson.build missing this.
1029 pattern = r"'cpp_std=c\+\+20'"
1030 for match in re.finditer(pattern, build_contents):
1031 if not meson_version or \
1032 not meson_version_compare(meson_version, ">=0.57"):
1033 raise Exception(
1034 "C++20 support requires specifying in meson.build: "
1035 + "meson_version: '>=0.57'"
1036 )
1037
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301038
1039class Package(object):
1040 def __init__(self, name=None, path=None):
Manojkiran Eda29d28252020-06-15 18:58:09 +05301041 self.supported = [Meson, Autotools, CMake]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301042 self.name = name
1043 self.path = path
1044 self.test_only = False
1045
1046 def build_systems(self):
Andrew Jeffery881041f2020-03-13 11:46:30 +10301047 instances = (system(self.name, self.path) for system in self.supported)
1048 return (instance for instance in instances if instance.probe())
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301049
1050 def build_system(self, preferred=None):
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001051 systems = list(self.build_systems())
1052
1053 if not systems:
1054 return None
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301055
1056 if preferred:
Andrew Jeffery881041f2020-03-13 11:46:30 +10301057 return {type(system): system for system in systems}[preferred]
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301058
1059 return next(iter(systems))
1060
1061 def install(self, system=None):
1062 if not system:
1063 system = self.build_system()
1064
1065 system.configure(False)
1066 system.build()
1067 system.install()
1068
Andrew Jeffery19d75672020-03-13 10:42:08 +10301069 def _test_one(self, system):
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301070 system.configure(True)
1071 system.build()
1072 system.install()
1073 system.test()
Andrew Jefferyd0809692021-05-14 16:23:57 +09301074 if not TEST_ONLY:
1075 system.analyze()
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301076
Andrew Jeffery19d75672020-03-13 10:42:08 +10301077 def test(self):
1078 for system in self.build_systems():
1079 self._test_one(system)
1080
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301081
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001082def find_file(filename, basedir):
1083 """
Patrick Williams55448ad2020-12-14 14:28:28 -06001084 Finds all occurrences of a file (or list of files) in the base
1085 directory and passes them back with their relative paths.
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001086
1087 Parameter descriptions:
Patrick Williams55448ad2020-12-14 14:28:28 -06001088 filename The name of the file (or list of files) to
1089 find
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001090 basedir The base directory search in
1091 """
1092
Patrick Williams55448ad2020-12-14 14:28:28 -06001093 if not isinstance(filename, list):
Lei YU08d2b922022-04-25 11:21:36 +08001094 filename = [filename]
Patrick Williams55448ad2020-12-14 14:28:28 -06001095
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001096 filepaths = []
1097 for root, dirs, files in os.walk(basedir):
Brad Bishopeb667262021-08-06 13:54:58 -04001098 if os.path.split(root)[-1] == 'subprojects':
1099 for f in files:
1100 subproject = '.'.join(f.split('.')[0:-1])
1101 if f.endswith('.wrap') and subproject in dirs:
1102 # don't find files in meson subprojects with wraps
1103 dirs.remove(subproject)
Patrick Williams55448ad2020-12-14 14:28:28 -06001104 for f in filename:
1105 if f in files:
1106 filepaths.append(os.path.join(root, f))
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001107 return filepaths
1108
Andrew Jeffery9cc97d82020-03-13 11:57:44 +10301109
Matthew Barthccb7f852016-11-23 17:43:02 -06001110if __name__ == '__main__':
1111 # CONFIGURE_FLAGS = [GIT REPO]:[CONFIGURE FLAGS]
1112 CONFIGURE_FLAGS = {
Matthew Barth1d1c6732017-03-24 10:00:28 -05001113 'phosphor-logging':
Matt Spinler0744bb82020-01-16 08:23:35 -06001114 ['--enable-metadata-processing', '--enable-openpower-pel-extension',
Deepak Kodihalli3a4e1b42017-06-08 09:52:35 -05001115 'YAML_DIR=/usr/local/share/phosphor-dbus-yaml/yaml']
Matthew Barthccb7f852016-11-23 17:43:02 -06001116 }
1117
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001118 # MESON_FLAGS = [GIT REPO]:[MESON FLAGS]
1119 MESON_FLAGS = {
Matt Spinler8448cfd2020-09-10 17:01:48 -05001120 'phosphor-dbus-interfaces':
Patrick Williams13af98a2021-04-21 15:17:00 -05001121 ['-Ddata_com_ibm=true', '-Ddata_org_open_power=true'],
1122 'phosphor-logging':
1123 ['-Dopenpower-pel-extension=enabled']
William A. Kennington III3f1d1202018-12-06 18:02:07 -08001124 }
1125
Matthew Barthccb7f852016-11-23 17:43:02 -06001126 # DEPENDENCIES = [MACRO]:[library/header]:[GIT REPO]
1127 DEPENDENCIES = {
1128 'AC_CHECK_LIB': {'mapper': 'phosphor-objmgr'},
Matthew Barth710f3f02017-01-18 15:20:19 -06001129 'AC_CHECK_HEADER': {
1130 'host-ipmid': 'phosphor-host-ipmid',
Patrick Ventureb41a4462018-10-03 17:27:38 -07001131 'blobs-ipmid': 'phosphor-ipmi-blobs',
Matthew Barth710f3f02017-01-18 15:20:19 -06001132 'sdbusplus': 'sdbusplus',
William A. Kennington IIIb4f730a2018-09-12 11:21:20 -07001133 'sdeventplus': 'sdeventplus',
William A. Kennington III23705242019-01-15 18:17:25 -08001134 'stdplus': 'stdplus',
Patrick Venture22329962018-09-14 10:23:04 -07001135 'gpioplus': 'gpioplus',
Saqib Khan66145052017-02-14 12:02:07 -06001136 'phosphor-logging/log.hpp': 'phosphor-logging',
Patrick Williamseab8a372017-01-30 11:21:32 -06001137 },
Brad Bishopebb49112017-02-13 11:07:26 -05001138 'AC_PATH_PROG': {'sdbus++': 'sdbusplus'},
Patrick Williamseab8a372017-01-30 11:21:32 -06001139 'PKG_CHECK_MODULES': {
Matthew Barth19e261e2017-02-01 12:55:22 -06001140 'phosphor-dbus-interfaces': 'phosphor-dbus-interfaces',
William A. Kennington III993ab332019-02-07 02:12:31 -08001141 'libipmid': 'phosphor-host-ipmid',
1142 'libipmid-host': 'phosphor-host-ipmid',
Brad Bishopebb49112017-02-13 11:07:26 -05001143 'sdbusplus': 'sdbusplus',
William A. Kennington IIIb4f730a2018-09-12 11:21:20 -07001144 'sdeventplus': 'sdeventplus',
William A. Kennington III23705242019-01-15 18:17:25 -08001145 'stdplus': 'stdplus',
Patrick Venture22329962018-09-14 10:23:04 -07001146 'gpioplus': 'gpioplus',
Brad Bishopebb49112017-02-13 11:07:26 -05001147 'phosphor-logging': 'phosphor-logging',
Marri Devender Raoa3eee8a2018-08-13 05:34:27 -05001148 'phosphor-snmp': 'phosphor-snmp',
Patrick Venturee584c3b2019-03-07 09:44:16 -08001149 'ipmiblob': 'ipmi-blob-tool',
Ben Tyner300ed3c2020-05-14 15:30:37 -05001150 'hei': 'openpower-libhei',
Sui Chen40cf5212020-11-24 12:12:23 -08001151 'phosphor-ipmi-blobs': 'phosphor-ipmi-blobs',
Willy Tu01534ec2021-10-14 10:16:45 -07001152 'libcr51sign': 'google-misc',
Brad Bishopebb49112017-02-13 11:07:26 -05001153 },
Matthew Barthccb7f852016-11-23 17:43:02 -06001154 }
1155
William A. Kennington IIIe67f5fc2018-12-06 17:40:30 -08001156 # Offset into array of macro parameters MACRO(0, 1, ...N)
1157 DEPENDENCIES_OFFSET = {
1158 'AC_CHECK_LIB': 0,
1159 'AC_CHECK_HEADER': 0,
1160 'AC_PATH_PROG': 1,
1161 'PKG_CHECK_MODULES': 1,
1162 }
1163
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001164 # DEPENDENCIES_REGEX = [GIT REPO]:[REGEX STRING]
1165 DEPENDENCIES_REGEX = {
Patrick Ventured37b8052018-10-16 16:03:03 -07001166 'phosphor-logging': r'\S+-dbus-interfaces$'
Leonel Gonzaleza62a1a12017-03-24 11:03:47 -05001167 }
1168
Matthew Barth33df8792016-12-19 14:30:17 -06001169 # Set command line arguments
1170 parser = argparse.ArgumentParser()
1171 parser.add_argument("-w", "--workspace", dest="WORKSPACE", required=True,
1172 help="Workspace directory location(i.e. /home)")
1173 parser.add_argument("-p", "--package", dest="PACKAGE", required=True,
1174 help="OpenBMC package to be unit tested")
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001175 parser.add_argument("-t", "--test-only", dest="TEST_ONLY",
1176 action="store_true", required=False, default=False,
1177 help="Only run test cases, no other validation")
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001178 arg_inttests = parser.add_mutually_exclusive_group()
1179 arg_inttests.add_argument("--integration-tests", dest="INTEGRATION_TEST",
Lei YU08d2b922022-04-25 11:21:36 +08001180 action="store_true", required=False, default=True,
1181 help="Enable integration tests [default].")
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001182 arg_inttests.add_argument("--no-integration-tests", dest="INTEGRATION_TEST",
Lei YU08d2b922022-04-25 11:21:36 +08001183 action="store_false", required=False,
1184 help="Disable integration tests.")
Matthew Barth33df8792016-12-19 14:30:17 -06001185 parser.add_argument("-v", "--verbose", action="store_true",
1186 help="Print additional package status messages")
Andrew Jeffery468309d2018-03-08 13:46:33 +10301187 parser.add_argument("-r", "--repeat", help="Repeat tests N times",
1188 type=int, default=1)
Andrew Geisslera61acb52019-01-03 16:32:44 -06001189 parser.add_argument("-b", "--branch", dest="BRANCH", required=False,
1190 help="Branch to target for dependent repositories",
1191 default="master")
Lei YU7ef93302019-11-06 13:53:21 +08001192 parser.add_argument("-n", "--noformat", dest="FORMAT",
1193 action="store_false", required=False,
1194 help="Whether or not to run format code")
Matthew Barth33df8792016-12-19 14:30:17 -06001195 args = parser.parse_args(sys.argv[1:])
1196 WORKSPACE = args.WORKSPACE
1197 UNIT_TEST_PKG = args.PACKAGE
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001198 TEST_ONLY = args.TEST_ONLY
Ramin Izadpanah298d56b2020-11-17 16:55:28 +00001199 INTEGRATION_TEST = args.INTEGRATION_TEST
Andrew Geisslera61acb52019-01-03 16:32:44 -06001200 BRANCH = args.BRANCH
Lei YU7ef93302019-11-06 13:53:21 +08001201 FORMAT_CODE = args.FORMAT
Matthew Barth33df8792016-12-19 14:30:17 -06001202 if args.verbose:
1203 def printline(*line):
1204 for arg in line:
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301205 print(arg, end=' ')
1206 print()
Matthew Barth33df8792016-12-19 14:30:17 -06001207 else:
Andrew Jefferyfce557f2020-03-13 12:12:19 +10301208 def printline(*line):
1209 pass
Matthew Barthccb7f852016-11-23 17:43:02 -06001210
Patrick Williamsb6535952020-12-15 06:40:10 -06001211 CODE_SCAN_DIR = os.path.join(WORKSPACE, UNIT_TEST_PKG)
Lei YU7ef93302019-11-06 13:53:21 +08001212
James Feist878df5c2018-07-26 14:54:28 -07001213 # First validate code formatting if repo has style formatting files.
Adriana Kobylakbcee22b2018-01-10 16:58:27 -06001214 # The format-code.sh checks for these files.
Lei YU7ef93302019-11-06 13:53:21 +08001215 if FORMAT_CODE:
Patrick Williams5b4a1e82020-12-15 06:46:31 -06001216 format_scripts = find_file(['format-code.sh', 'format-code'],
1217 CODE_SCAN_DIR)
Andrew Geissler31502dd2021-01-07 14:04:02 -06001218
1219 # use default format-code.sh if no other found
1220 if not format_scripts:
1221 format_scripts.append(os.path.join(WORKSPACE, "format-code.sh"))
1222
Patrick Williams5b4a1e82020-12-15 06:46:31 -06001223 for f in format_scripts:
1224 check_call_cmd(f, CODE_SCAN_DIR)
Andrew Geisslera28286d2018-01-10 11:00:00 -08001225
Ed Tanous32768b82022-01-05 14:14:06 -08001226 # Check to see if any files changed
1227 check_call_cmd("git", "-C", CODE_SCAN_DIR,
1228 "--no-pager", "diff", "--exit-code")
1229
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001230 # Check if this repo has a supported make infrastructure
Patrick Williamsb6535952020-12-15 06:40:10 -06001231 pkg = Package(UNIT_TEST_PKG, CODE_SCAN_DIR)
Andrew Geissler8cb74fc2020-03-19 14:48:05 -05001232 if not pkg.build_system():
1233 print("No valid build system, exit")
1234 sys.exit(0)
1235
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301236 prev_umask = os.umask(000)
James Feist878df5c2018-07-26 14:54:28 -07001237
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301238 # Determine dependencies and add them
1239 dep_added = dict()
1240 dep_added[UNIT_TEST_PKG] = False
William A. Kennington III40d5c7c2018-12-13 14:37:59 -08001241
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301242 # Create dependency tree
1243 dep_tree = DepTree(UNIT_TEST_PKG)
Patrick Williamsb6535952020-12-15 06:40:10 -06001244 build_dep_tree(UNIT_TEST_PKG, CODE_SCAN_DIR, dep_added, dep_tree, BRANCH)
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001245
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301246 # Reorder Dependency Tree
Andrew Jeffery89b64b62020-03-13 12:15:48 +10301247 for pkg_name, regex_str in DEPENDENCIES_REGEX.items():
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301248 dep_tree.ReorderDeps(pkg_name, regex_str)
1249 if args.verbose:
1250 dep_tree.PrintTree()
William A. Kennington III65b37fa2019-01-31 15:15:17 -08001251
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301252 install_list = dep_tree.GetInstallList()
Andrew Geissler9ced4ed2019-11-18 14:33:58 -06001253
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301254 # We don't want to treat our package as a dependency
1255 install_list.remove(UNIT_TEST_PKG)
James Feist878df5c2018-07-26 14:54:28 -07001256
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301257 # Install reordered dependencies
1258 for dep in install_list:
1259 build_and_install(dep, False)
James Feist878df5c2018-07-26 14:54:28 -07001260
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301261 # Run package unit tests
1262 build_and_install(UNIT_TEST_PKG, True)
James Feist878df5c2018-07-26 14:54:28 -07001263
Andrew Jeffery15e423e2020-03-11 16:51:28 +10301264 os.umask(prev_umask)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001265
1266 # Run any custom CI scripts the repo has, of which there can be
1267 # multiple of and anywhere in the repository.
Patrick Williamsb6535952020-12-15 06:40:10 -06001268 ci_scripts = find_file(['run-ci.sh', 'run-ci'], CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001269 if ci_scripts:
Patrick Williamsb6535952020-12-15 06:40:10 -06001270 os.chdir(CODE_SCAN_DIR)
Matt Spinler9bfaaad2019-10-25 09:51:50 -05001271 for ci_script in ci_scripts:
Patrick Williams55448ad2020-12-14 14:28:28 -06001272 check_call_cmd(ci_script)