summaryrefslogtreecommitdiff
path: root/zephyr/firmware_builder.py
diff options
context:
space:
mode:
Diffstat (limited to 'zephyr/firmware_builder.py')
-rwxr-xr-xzephyr/firmware_builder.py501
1 files changed, 405 insertions, 96 deletions
diff --git a/zephyr/firmware_builder.py b/zephyr/firmware_builder.py
index 21767d635a..436760361a 100755
--- a/zephyr/firmware_builder.py
+++ b/zephyr/firmware_builder.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# Copyright 2021 The Chromium OS Authors. All rights reserved.
+# Copyright 2021 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Build and test all of the Zephyr boards.
@@ -12,73 +12,120 @@ import argparse
import multiprocessing
import pathlib
import re
+import shlex
import subprocess
import sys
-from google.protobuf import json_format # pylint: disable=import-error
import zmake.project
-
from chromite.api.gen_sdk.chromite.api import firmware_pb2
+from google.protobuf import json_format # pylint: disable=import-error
-
-DEFAULT_BUNDLE_DIRECTORY = '/tmp/artifact_bundles'
-DEFAULT_BUNDLE_METADATA_FILE = '/tmp/artifact_bundle_metadata'
+DEFAULT_BUNDLE_DIRECTORY = "/tmp/artifact_bundles"
+DEFAULT_BUNDLE_METADATA_FILE = "/tmp/artifact_bundle_metadata"
+
+# Boards that we want to track the coverage of our own files specifically.
+SPECIAL_BOARDS = ["herobrine"]
+
+
+def log_cmd(cmd):
+ """Log subprocess command."""
+ print(" ".join(shlex.quote(str(x)) for x in cmd))
+ sys.stdout.flush()
+
+
+def run_twister(platform_ec, code_coverage=False, extra_args=None):
+ """Build the tests using twister."""
+ cmd = [
+ platform_ec / "twister",
+ "--outdir",
+ platform_ec / "twister-out",
+ "-v",
+ "-i",
+ "-p",
+ "native_posix",
+ "-p",
+ "unit_testing",
+ "--no-upload-cros-rdb",
+ ]
+
+ if extra_args:
+ cmd.extend(extra_args)
+
+ if code_coverage:
+ # Tell Twister to collect coverage data. We must specify an explicit platform
+ # type in this case, as well.
+ cmd.extend(
+ [
+ "--coverage",
+ ]
+ )
+ log_cmd(cmd)
+ subprocess.run(cmd, check=True, cwd=platform_ec, stdin=subprocess.DEVNULL)
def build(opts):
"""Builds all Zephyr firmware targets"""
metric_list = firmware_pb2.FwBuildMetricList()
- cmd = ['zmake', '-D', 'build', '-a']
+ zephyr_dir = pathlib.Path(__file__).parent.resolve()
+ platform_ec = zephyr_dir.parent
+ subprocess.run(
+ [platform_ec / "util" / "check_clang_format.py"],
+ check=True,
+ cwd=platform_ec,
+ stdin=subprocess.DEVNULL,
+ )
+
+ cmd = ["zmake", "-D", "build", "-a"]
if opts.code_coverage:
- cmd.append('--coverage')
- subprocess.run(cmd, cwd=pathlib.Path(__file__).parent, check=True)
+ cmd.append("--coverage")
+ log_cmd(cmd)
+ subprocess.run(cmd, cwd=zephyr_dir, check=True, stdin=subprocess.DEVNULL)
if not opts.code_coverage:
- zephyr_dir = pathlib.Path(__file__).parent
- platform_ec = zephyr_dir.resolve().parent
for project in zmake.project.find_projects(zephyr_dir).values():
if project.config.is_test:
continue
build_dir = (
- platform_ec / 'build' / 'zephyr' / project.config.project_name
+ platform_ec / "build" / "zephyr" / project.config.project_name
)
metric = metric_list.value.add()
metric.target_name = project.config.project_name
metric.platform_name = project.config.zephyr_board
for (variant, _) in project.iter_builds():
- build_log = build_dir / f'build-{variant}' / 'build.log'
+ build_log = build_dir / f"build-{variant}" / "build.log"
parse_buildlog(build_log, metric, variant.upper())
- with open(opts.metrics, 'w') as file:
+ with open(opts.metrics, "w") as file:
file.write(json_format.MessageToJson(metric_list))
- return 0
+
+ run_twister(platform_ec, opts.code_coverage, ["--build-only"])
UNITS = {
- 'B': 1,
- 'KB': 1024,
- 'MB': 1024 * 1024,
- 'GB': 1024 * 1024 * 1024,
+ "B": 1,
+ "KB": 1024,
+ "MB": 1024 * 1024,
+ "GB": 1024 * 1024 * 1024,
}
def parse_buildlog(filename, metric, variant):
"""Parse the build.log generated by zmake to find the size of the image."""
- with open(filename, 'r') as infile:
+ with open(filename, "r") as infile:
# Skip over all lines until the memory report is found
while True:
line = infile.readline()
if not line:
return
- if line.startswith('Memory region'):
+ if line.startswith("Memory region"):
break
for line in infile.readlines():
# Skip any lines that are not part of the report
- if not line.startswith(' '):
+ if not line.startswith(" "):
continue
parts = line.split()
fw_section = metric.fw_section.add()
- fw_section.region = variant + '_' + parts[0][:-1]
+ fw_section.region = variant + "_" + parts[0][:-1]
fw_section.used = int(parts[1]) * UNITS[parts[2]]
fw_section.total = int(parts[3]) * UNITS[parts[4]]
fw_section.track_on_gerrit = False
@@ -112,7 +159,7 @@ def write_metadata(opts, info):
bundle_metadata_file = (
opts.metadata if opts.metadata else DEFAULT_BUNDLE_METADATA_FILE
)
- with open(bundle_metadata_file, 'w') as file:
+ with open(bundle_metadata_file, "w") as file:
file.write(json_format.MessageToJson(info))
@@ -121,18 +168,29 @@ def bundle_coverage(opts):
info = firmware_pb2.FirmwareArtifactInfo()
info.bcs_version_info.version_string = opts.bcs_version
bundle_dir = get_bundle_dir(opts)
- zephyr_dir = pathlib.Path(__file__).parent
- platform_ec = zephyr_dir.resolve().parent
- build_dir = platform_ec / 'build' / 'zephyr'
- tarball_name = 'coverage.tbz2'
+ zephyr_dir = pathlib.Path(__file__).parent.resolve()
+ platform_ec = zephyr_dir.parent
+ build_dir = platform_ec / "build" / "zephyr"
+ tarball_name = "coverage.tbz2"
tarball_path = bundle_dir / tarball_name
- cmd = ['tar', 'cvfj', tarball_path, 'lcov.info']
- subprocess.run(cmd, cwd=build_dir, check=True)
+ cmd = ["tar", "cvfj", tarball_path, "lcov.info"]
+ log_cmd(cmd)
+ subprocess.run(cmd, cwd=build_dir, check=True, stdin=subprocess.DEVNULL)
meta = info.objects.add()
meta.file_name = tarball_name
meta.lcov_info.type = (
firmware_pb2.FirmwareArtifactInfo.LcovTarballInfo.LcovType.LCOV
)
+ (bundle_dir / "html").mkdir(exist_ok=True)
+ cmd = ["mv", "lcov_rpt"]
+ for board in SPECIAL_BOARDS:
+ cmd.append(board + "_rpt")
+ cmd.append(bundle_dir / "html/")
+ log_cmd(cmd)
+ subprocess.run(cmd, cwd=build_dir, check=True, stdin=subprocess.DEVNULL)
+ meta = info.objects.add()
+ meta.file_name = "html"
+ meta.coverage_html.SetInParent()
write_metadata(opts, info)
@@ -142,19 +200,22 @@ def bundle_firmware(opts):
info = firmware_pb2.FirmwareArtifactInfo()
info.bcs_version_info.version_string = opts.bcs_version
bundle_dir = get_bundle_dir(opts)
- zephyr_dir = pathlib.Path(__file__).parent
- platform_ec = zephyr_dir.resolve().parent
+ zephyr_dir = pathlib.Path(__file__).parent.resolve()
+ platform_ec = zephyr_dir.parent
for project in zmake.project.find_projects(zephyr_dir).values():
if project.config.is_test:
continue
build_dir = (
- platform_ec / 'build' / 'zephyr' / project.config.project_name
+ platform_ec / "build" / "zephyr" / project.config.project_name
)
- artifacts_dir = build_dir / 'output'
- tarball_name = f'{project.config.project_name}.firmware.tbz2'
+ artifacts_dir = build_dir / "output"
+ tarball_name = f"{project.config.project_name}.firmware.tbz2"
tarball_path = bundle_dir.joinpath(tarball_name)
- cmd = ['tar', 'cvfj', tarball_path, '.']
- subprocess.run(cmd, cwd=artifacts_dir, check=True)
+ cmd = ["tar", "cvfj", tarball_path, "."]
+ log_cmd(cmd)
+ subprocess.run(
+ cmd, cwd=artifacts_dir, check=True, stdin=subprocess.DEVNULL
+ )
meta = info.objects.add()
meta.file_name = tarball_name
meta.tarball_info.type = (
@@ -174,50 +235,296 @@ def test(opts):
# Run zmake tests to ensure we have a fully working zmake before
# proceeding.
- subprocess.run([zephyr_dir / 'zmake' / 'run_tests.sh'], check=True)
+ subprocess.run(
+ [zephyr_dir / "zmake" / "run_tests.sh"],
+ check=True,
+ cwd=zephyr_dir,
+ stdin=subprocess.DEVNULL,
+ )
- # Run formatting checks on all BUILD.py files.
- config_files = zephyr_dir.rglob('**/BUILD.py')
- subprocess.run(['black', '--diff', '--check', *config_files], check=True)
+ # Twister-based tests
+ platform_ec = zephyr_dir.parent
+ third_party = platform_ec.parent.parent / "third_party"
+ run_twister(platform_ec, opts.code_coverage, ["--test-only"])
- cmd = ['zmake', '-D', 'test', '-a', '--no-rebuild']
if opts.code_coverage:
- cmd.append('--coverage')
- ret = subprocess.run(cmd, check=True).returncode
- if ret:
- return ret
- if opts.code_coverage:
- platform_ec = zephyr_dir.parent
- build_dir = platform_ec / 'build' / 'zephyr'
+ build_dir = platform_ec / "build" / "zephyr"
# Merge lcov files here because bundle failures are "infra" failures.
+ output = subprocess.run(
+ [
+ "/usr/bin/lcov",
+ "--summary",
+ platform_ec / "twister-out" / "coverage.info",
+ ],
+ cwd=zephyr_dir,
+ check=True,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ stdin=subprocess.DEVNULL,
+ ).stdout
+ _extract_lcov_summary("EC_ZEPHYR_TESTS", metrics, output)
+
+ cmd = ["make", "test-coverage", f"-j{opts.cpus}"]
+ log_cmd(cmd)
+ subprocess.run(
+ cmd, cwd=platform_ec, check=True, stdin=subprocess.DEVNULL
+ )
+
+ output = subprocess.run(
+ [
+ "/usr/bin/lcov",
+ "--summary",
+ platform_ec / "build/coverage/lcov.info",
+ ],
+ cwd=zephyr_dir,
+ check=True,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ stdin=subprocess.DEVNULL,
+ ).stdout
+ _extract_lcov_summary("EC_LEGACY_TESTS", metrics, output)
+
cmd = [
- '/usr/bin/lcov',
- '-o',
- build_dir / 'lcov.info',
- '--rc',
- 'lcov_branch_coverage=1',
- '-a',
- build_dir / 'all_tests.info',
- '-a',
- build_dir / 'all_builds.info',
+ "/usr/bin/lcov",
+ "-o",
+ build_dir / "all_tests.info",
+ "--rc",
+ "lcov_branch_coverage=1",
+ "-a",
+ platform_ec / "build/coverage/lcov.info",
+ "-a",
+ platform_ec / "twister-out" / "coverage.info",
]
+ log_cmd(cmd)
output = subprocess.run(
- cmd, cwd=pathlib.Path(__file__).parent, check=True,
- stdout=subprocess.PIPE, universal_newlines=True).stdout
- _extract_lcov_summary('EC_ZEPHYR_MERGED', metrics, output)
+ cmd,
+ cwd=zephyr_dir,
+ check=True,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ stdin=subprocess.DEVNULL,
+ ).stdout
+ _extract_lcov_summary("ALL_TESTS", metrics, output)
+ cmd = [
+ "/usr/bin/lcov",
+ "-o",
+ build_dir / "zephyr_merged.info",
+ "--rc",
+ "lcov_branch_coverage=1",
+ "-a",
+ build_dir / "all_builds.info",
+ "-a",
+ build_dir / "all_tests.info",
+ ]
+ log_cmd(cmd)
output = subprocess.run(
- ['/usr/bin/lcov', '--summary', build_dir / 'all_tests.info'],
- cwd=pathlib.Path(__file__).parent, check=True,
- stdout=subprocess.PIPE, universal_newlines=True).stdout
- _extract_lcov_summary('EC_ZEPHYR_TESTS', metrics, output)
+ cmd,
+ cwd=zephyr_dir,
+ check=True,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ stdin=subprocess.DEVNULL,
+ ).stdout
+ _extract_lcov_summary("EC_ZEPHYR_MERGED", metrics, output)
- with open(opts.metrics, 'w') as file:
- file.write(json_format.MessageToJson(metrics))
- return 0
+ cmd = [
+ "/usr/bin/lcov",
+ "-o",
+ build_dir / "lcov_unfiltered.info",
+ "--rc",
+ "lcov_branch_coverage=1",
+ "-a",
+ build_dir / "zephyr_merged.info",
+ "-a",
+ platform_ec / "build/coverage/lcov.info",
+ ]
+ log_cmd(cmd)
+ subprocess.run(
+ cmd,
+ cwd=zephyr_dir,
+ check=True,
+ stdin=subprocess.DEVNULL,
+ )
+
+ test_patterns = [
+ platform_ec / "test/**",
+ platform_ec / "private/fingerprint/google-fpalg/mcutest/**",
+ zephyr_dir / "test/**",
+ zephyr_dir / "emul/**",
+ zephyr_dir / "mock/**",
+ third_party / "zephyr/main/subsys/emul/**",
+ third_party / "zephyr/main/subsys/testsuite/**",
+ ]
+
+ generated_and_system_patterns = [
+ platform_ec / "build/**",
+ platform_ec / "twister-out*/**",
+ "/usr/include/**",
+ "/usr/lib/**",
+ ]
+
+ cmd = [
+ "/usr/bin/lcov",
+ "-o",
+ build_dir / "lcov.info",
+ "--rc",
+ "lcov_branch_coverage=1",
+ "-r",
+ build_dir / "lcov_unfiltered.info",
+ ] + generated_and_system_patterns
+ log_cmd(cmd)
+ output = subprocess.run(
+ cmd,
+ cwd=zephyr_dir,
+ check=True,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ stdin=subprocess.DEVNULL,
+ ).stdout
+ _extract_lcov_summary("ALL_MERGED", metrics, output)
+
+ # Create an info file without any test code, just for the metric.
+ cmd = [
+ "/usr/bin/lcov",
+ "-o",
+ build_dir / "lcov_no_tests.info",
+ "--rc",
+ "lcov_branch_coverage=1",
+ "-r",
+ build_dir / "lcov.info",
+ ] + test_patterns
+ log_cmd(cmd)
+ output = subprocess.run(
+ cmd,
+ cwd=zephyr_dir,
+ check=True,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ stdin=subprocess.DEVNULL,
+ ).stdout
+ _extract_lcov_summary("ALL_FILTERED", metrics, output)
+
+ subprocess.run(
+ [
+ "/usr/bin/genhtml",
+ "--branch-coverage",
+ "-q",
+ "-o",
+ build_dir / "lcov_rpt",
+ "-t",
+ "All boards and tests merged",
+ "-s",
+ build_dir / "lcov.info",
+ ],
+ cwd=zephyr_dir,
+ check=True,
+ stdin=subprocess.DEVNULL,
+ )
+
+ for board in SPECIAL_BOARDS:
+ # Merge board coverage with tests
+ cmd = [
+ "/usr/bin/lcov",
+ "-o",
+ build_dir / (board + "_merged.info"),
+ "--rc",
+ "lcov_branch_coverage=1",
+ "-a",
+ build_dir / "all_tests.info",
+ "-a",
+ build_dir / board / "output/zephyr.info",
+ ]
+ log_cmd(cmd)
+ subprocess.run(
+ cmd,
+ cwd=zephyr_dir,
+ check=True,
+ stdin=subprocess.DEVNULL,
+ )
+ # Exclude file patterns we don't want
+ cmd = (
+ [
+ "/usr/bin/lcov",
+ "-o",
+ build_dir / (board + "_filtered.info"),
+ "--rc",
+ "lcov_branch_coverage=1",
+ "-r",
+ build_dir / (board + "_merged.info"),
+ # Exclude third_party code (specifically zephyr)
+ third_party / "**",
+ # These are questionable, but they are essentially untestable
+ zephyr_dir / "drivers/**",
+ zephyr_dir / "include/drivers/**",
+ zephyr_dir / "projects/**",
+ zephyr_dir / "shim/chip/**",
+ zephyr_dir / "shim/chip/npcx/npcx_monitor/**",
+ zephyr_dir / "shim/core/**",
+ ]
+ + generated_and_system_patterns
+ + test_patterns
+ )
+ log_cmd(cmd)
+ subprocess.run(
+ cmd,
+ cwd=zephyr_dir,
+ check=True,
+ stdin=subprocess.DEVNULL,
+ )
+ # Then keep only files present in the board build
+ filenames = set()
+ with open(
+ build_dir / board / "output/zephyr.info", "r"
+ ) as board_cov:
+ for line in board_cov.readlines():
+ if line.startswith("SF:"):
+ filenames.add(line[3:-1])
+ cmd = [
+ "/usr/bin/lcov",
+ "-o",
+ build_dir / (board + "_final.info"),
+ "--rc",
+ "lcov_branch_coverage=1",
+ "-e",
+ build_dir / (board + "_filtered.info"),
+ ] + list(filenames)
+ log_cmd(cmd)
+ output = subprocess.run(
+ cmd,
+ cwd=zephyr_dir,
+ check=True,
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ stdin=subprocess.DEVNULL,
+ ).stdout
+ _extract_lcov_summary(f"BOARD_{board}".upper(), metrics, output)
+ subprocess.run(
+ [
+ "/usr/bin/genhtml",
+ "--branch-coverage",
+ "-q",
+ "-o",
+ build_dir / (board + "_rpt"),
+ "-t",
+ f"{board} ec code only",
+ "-s",
+ build_dir / (board + "_final.info"),
+ ],
+ cwd=zephyr_dir,
+ check=True,
+ stdin=subprocess.DEVNULL,
+ )
+
+ with open(opts.metrics, "w") as file:
+ file.write(json_format.MessageToJson(metrics)) # type: ignore
+
+
+COVERAGE_RE = re.compile(
+ r"lines\.*: *([0-9\.]+)% \(([0-9]+) of ([0-9]+) lines\)"
+)
-COVERAGE_RE = re.compile(r'lines\.*: *([0-9\.]+)% \(([0-9]+) of ([0-9]+) lines\)')
def _extract_lcov_summary(name, metrics, output):
re_match = COVERAGE_RE.search(output)
if re_match:
@@ -227,16 +534,18 @@ def _extract_lcov_summary(name, metrics, output):
metric.covered_lines = int(re_match.group(2))
metric.total_lines = int(re_match.group(3))
+
def main(args):
"""Builds and tests all of the Zephyr targets and reports build metrics"""
opts = parse_args(args)
- if not hasattr(opts, 'func'):
- print('Must select a valid sub command!')
+ if not hasattr(opts, "func"):
+ print("Must select a valid sub command!")
return -1
# Run selected sub command function
- return opts.func(opts)
+ opts.func(opts)
+ return 0
def parse_args(args):
@@ -244,70 +553,70 @@ def parse_args(args):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
- '--cpus',
+ "--cpus",
default=multiprocessing.cpu_count(),
- help='The number of cores to use.',
+ help="The number of cores to use.",
)
parser.add_argument(
- '--metrics',
- dest='metrics',
+ "--metrics",
+ dest="metrics",
required=True,
- help='File to write the json-encoded MetricsList proto message.',
+ help="File to write the json-encoded MetricsList proto message.",
)
parser.add_argument(
- '--metadata',
+ "--metadata",
required=False,
help=(
- 'Full pathname for the file in which to write build artifact '
- 'metadata.'
+ "Full pathname for the file in which to write build artifact "
+ "metadata."
),
)
parser.add_argument(
- '--output-dir',
+ "--output-dir",
required=False,
help=(
- 'Full pathname for the directory in which to bundle build '
- 'artifacts.'
+ "Full pathname for the directory in which to bundle build "
+ "artifacts."
),
)
parser.add_argument(
- '--code-coverage',
+ "--code-coverage",
required=False,
- action='store_true',
- help='Build host-based unit tests for code coverage.',
+ action="store_true",
+ help="Build host-based unit tests for code coverage.",
)
parser.add_argument(
- '--bcs-version',
- dest='bcs_version',
- default='',
+ "--bcs-version",
+ dest="bcs_version",
+ default="",
required=False,
# TODO(b/180008931): make this required=True.
- help='BCS version to include in metadata.',
+ help="BCS version to include in metadata.",
)
# Would make this required=True, but not available until 3.7
sub_cmds = parser.add_subparsers()
- build_cmd = sub_cmds.add_parser('build', help='Builds all firmware targets')
+ build_cmd = sub_cmds.add_parser("build", help="Builds all firmware targets")
build_cmd.set_defaults(func=build)
build_cmd = sub_cmds.add_parser(
- 'bundle',
- help='Creates a tarball containing build '
- 'artifacts from all firmware targets',
+ "bundle",
+ help="Creates a tarball containing build "
+ "artifacts from all firmware targets",
)
build_cmd.set_defaults(func=bundle)
- test_cmd = sub_cmds.add_parser('test', help='Runs all firmware unit tests')
+ test_cmd = sub_cmds.add_parser("test", help="Runs all firmware unit tests")
test_cmd.set_defaults(func=test)
return parser.parse_args(args)
-if __name__ == '__main__':
+if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))