blob: 4e740aea41f1525ac57f67506a31b3bf2d53d8f4 [file] [log] [blame]
#!/usr/bin/env python2.7
#
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Invoke trusty build system and run tests."""
import argparse
import getpass
import multiprocessing
import os
import re
import shutil
import subprocess
import sys
import run_tests
import trusty_build_config
script_dir = os.path.dirname(os.path.abspath(__file__))
def get_new_build_id(build_root):
"""Increment build-id file and return new build-id number."""
path = os.path.join(build_root, "BUILDID")
try:
with open(path, "r") as f:
num = int(f.read()) + 1
except IOError:
num = 1
with open(path, "w") as f:
f.write(str(num))
f.truncate()
# Return buildid string: <user>@<hostname>-<num>
# Use getpass.getuser() to avoid non-portability/failure of
# os.getlogin()
return getpass.getuser() + "@" + os.uname()[1] + "-" + str(num)
def mkdir(path):
"""Create directory includig parents if it does not already exist."""
try:
os.makedirs(path)
except OSError:
if not os.path.isdir(path):
raise
def copy_file(src, dest, optional=False):
"""Copy a file.
Copy a file or exit if the file cannot be copied.
Args:
src: Path of file to copy.
dest: Path to copy file to.
optional: Optional boolean argument. If True don't exit if source file
does not exist.
"""
if not os.path.exists(src) and optional:
return
print "Copy:", repr(src), "->", repr(dest)
shutil.copy(src, dest)
def archive_build_file(args, project, src, dest=None, optional=False):
"""Copy a file to build archive directory.
Construct src and dest path and call copy_file.
Args:
args: Program arguments.
project: Project name.
src: Source path relative to project build dir.
dest: Optional dest path relative to archive dir. Can be ommitted if src
is a simple filename.
optional: Optional boolean argument. If True don't exit if source file
does not exist.
"""
if not dest:
dest = src
src = os.path.join(args.build_root, "build-" + project, src)
# dest must be a fixed path for repeated builds of the same artifact
# for compatibility with prebuilt update scripts.
# Project is fine because that specifies what artifact is being looked
# for - LK for a specific target.
# BUILD_ID or feature selections that may change are not, because the
# prebuilt update script cannot predict the path at which the artifact
# will live.
dest = os.path.join(args.archive, project + "." + dest)
copy_file(src, dest, optional=optional)
def build(args):
"""Call build system and copy build files to archive dir."""
mkdir(args.build_root)
mkdir(args.archive)
if args.buildid is None:
args.buildid = get_new_build_id(args.build_root)
print "BuildID", args.buildid
# build projects
failed = []
for project in args.project:
cmd = "export BUILDROOT=" + args.build_root
cmd += "; export BUILDID=" + args.buildid
if args.clang is not None:
cmd += "; export CLANGBUILD=" + str(args.clang).lower()
cmd += "; nice make " + project + " -j " + str(args.jobs)
# Call envsetup. If it fails, abort.
cmd = "source %s && (%s)" % (os.path.join(script_dir, "envsetup.sh"),
cmd)
status = subprocess.call(cmd, shell=True, executable="/bin/bash")
print "cmd: '" + cmd + "' returned", status
if status:
failed.append(project)
if failed:
print
print "some projects have failed to build:"
print str(failed)
exit(1)
# Copy the files we care about to the archive directory
for project in args.project:
# copy out tos.img if it exists
archive_build_file(args, project, "tos.img", optional=True)
# copy out monitor if it exists
archive_build_file(args, project, "monitor/monitor.bin", "monitor.bin",
optional=True)
# copy out trusty.padded if it exists
archive_build_file(args, project, "trusty.padded", optional=True)
# copy out trusty.signed if it exists
archive_build_file(args, project, "trusty.signed", optional=True)
# copy out trusty_usb.signed if it exists
archive_build_file(args, project, "trusty_usb.signed", optional=True)
# copy out lk image
archive_build_file(args, project, "lk.bin")
# collect and save all .lst
subprocess.call("cd " +
os.path.join(args.build_root, "build-" + project) +
';find . -name "*.lst" -print ' +
"| zip " + os.path.join(args.archive, project + "-" +
args.buildid + ".lst.zip") +
" -@", shell=True, executable="/bin/bash")
def get_build_deps(project_name, project, project_names, already_built):
if project_name not in already_built:
already_built.add(project_name)
for dep_project_name, dep_project in project.also_build.items():
get_build_deps(dep_project_name, dep_project, project_names,
already_built)
project_names.append(project_name)
def main(default_config=None):
top = os.path.abspath(os.path.join(script_dir, "../../../../.."))
os.chdir(top)
parser = argparse.ArgumentParser()
parser.add_argument("project", type=str, nargs="*", default=[".test.all"],
help="Project to build and/or test.")
parser.add_argument("--build-root", type=str,
default=os.path.join(top, "build-root"),
help="Root of intermediate build directory.")
parser.add_argument("--archive", type=str, default=None,
help="Location of build results directory.")
parser.add_argument("--buildid", type=str, help="Server build id")
parser.add_argument("--jobs", type=str, default=multiprocessing.cpu_count(),
help="Max number of build jobs.")
parser.add_argument("--test", type=str, action="append",
help="Manually specify test(s) to run. "
"Only build projects that have test(s) enabled that "
"matches a listed regex.")
parser.add_argument("--verbose", action="store_true",
help="Verbose debug output from test(s).")
parser.add_argument("--debug-on-error", action="store_true",
help="Wait for debugger connection if test fails.")
parser.add_argument("--clang", action="store_true", default=None,
help="Build with clang.")
parser.add_argument("--gcc", action="store_false", dest="clang",
help="Build with GCC.")
parser.add_argument("--skip-build", action="store_true", help="Skip build.")
parser.add_argument("--skip-tests", action="store_true",
help="Skip running tests.")
parser.add_argument("--run-disabled-tests", action="store_true",
help="Also run disabled tests.")
parser.add_argument("--skip-project", action="append", default=[],
help="Remove project from projects being built.")
parser.add_argument("--config", type=str, help="Path to an alternate "
"build-config file.", default=default_config)
args = parser.parse_args()
if args.archive is None:
args.archive = os.path.join(args.build_root, "archive")
build_config = trusty_build_config.TrustyBuildConfig(
config_file=args.config)
projects = []
for project in args.project:
if project == ".test.all":
projects += build_config.get_projects(build=True)
elif project == ".test":
projects += build_config.get_projects(build=True, have_tests=True)
else:
projects.append(project)
# skip specific projects
ok = True
for skip in args.skip_project:
if skip in projects:
projects.remove(skip)
else:
sys.stderr.write(
"ERROR unknown project --skip-project={}\n".format(skip))
ok = False
if not ok:
sys.exit(1)
if args.test:
args.test = [re.compile(testpattern) for testpattern in args.test]
def has_test(project_name):
"""filter function to check if a project has args.test."""
project = build_config.get_project(project_name)
for test in project.tests:
if not test.enabled and not args.run_disabled_tests:
continue
if run_tests.test_should_run(test.name, args.test):
return True
return False
projects = filter(has_test, projects)
# find build dependencies
projects_old = projects
projects = []
built_projects = set()
for project_name in projects_old:
get_build_deps(project_name,
build_config.get_project(project_name),
projects,
built_projects)
args.project = projects
print "Projects", str(projects)
if args.skip_build:
print "Skip build for", args.project
else:
build(args)
# Run tests
if not args.skip_tests:
test_failed = []
test_results = []
tests_passed = 0
tests_failed = 0
projects_passed = 0
projects_failed = 0
for project in projects:
test_result = run_tests.run_tests(build_config, args.build_root,
project, run_disabled_tests=
args.run_disabled_tests,
test_filter=args.test,
verbose=args.verbose,
debug_on_error=
args.debug_on_error)
if not test_result.passed:
test_failed.append(project)
if test_result.passed_count:
projects_passed += 1
tests_passed += test_result.passed_count
if test_result.failed_count:
projects_failed += 1
tests_failed += test_result.failed_count
test_results.append(test_result)
for test_result in test_results:
test_result.print_results()
sys.stdout.write("\n")
if projects_passed:
sys.stdout.write("[ PASSED ] {} tests in {} projects.\n".format(
tests_passed, projects_passed))
if projects_failed:
sys.stdout.write("[ FAILED ] {} tests in {} projects.\n".format(
tests_failed, projects_failed))
sys.stdout.flush()
# Print the failed tests again to stderr as the build server will
# store this in a separate file with a direct link from the build
# status page. The full build long page on the build server, buffers
# stdout and stderr and interleaves them at random. By printing
# the summary to both stderr and stdout, we get at least one of them
# at the bottom of that file.
for test_result in test_results:
test_result.print_results(print_failed_only=True)
sys.stderr.write("[ FAILED ] {} tests in {} projects.\n".format(
tests_failed, projects_failed))
if test_failed:
sys.exit(1)
if __name__ == "__main__":
main()