oss-fuzz/infra/ci/build.py

178 lines
6.2 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright 2019 Google Inc.
#
# 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.
#
################################################################################
"""Build modified projects."""
from __future__ import print_function
import os
import re
import sys
import subprocess
import yaml
DEFAULT_ARCHITECTURES = ['x86_64']
DEFAULT_ENGINES = ['afl', 'honggfuzz', 'libfuzzer']
DEFAULT_SANITIZERS = ['address', 'undefined']
# Languages from project.yaml that have code coverage support.
LANGUAGES_WITH_COVERAGE_SUPPORT = ['c', 'c++']
def get_modified_buildable_projects():
"""Returns a list of all the projects modified in this commit that have a
build.sh file."""
output = subprocess.check_output(['git', 'diff', '--name-only',
'FETCH_HEAD']).decode()
projects_regex = '.*projects/(?P<name>.*)/.*\n'
modified_projects = set(re.findall(projects_regex, output))
projects_dir = os.path.join(get_oss_fuzz_root(), 'projects')
# Filter out projects without Dockerfile files since new projects and reverted
# projects frequently don't have them. In these cases we don't want Travis's
# builds to fail.
modified_buildable_projects = []
for project in modified_projects:
if not os.path.exists(os.path.join(projects_dir, project, 'Dockerfile')):
print('Project {0} does not have Dockerfile. skipping build.'.format(
project))
continue
modified_buildable_projects.append(project)
return modified_buildable_projects
def get_oss_fuzz_root():
"""Get the absolute path of the root of the oss-fuzz checkout."""
script_path = os.path.realpath(__file__)
return os.path.abspath(
os.path.dirname(os.path.dirname(os.path.dirname(script_path))))
def execute_helper_command(helper_command):
"""Execute |helper_command| using helper.py."""
root = get_oss_fuzz_root()
script_path = os.path.join(root, 'infra', 'helper.py')
command = ['python', script_path] + helper_command
print('Running command: %s' % ' '.join(command))
subprocess.check_call(command)
def build_fuzzers(project, engine, sanitizer, architecture):
"""Execute helper.py's build_fuzzers command on |project|. Build the fuzzers
with |engine| and |sanitizer| for |architecture|."""
execute_helper_command([
'build_fuzzers', project, '--engine', engine, '--sanitizer', sanitizer,
'--architecture', architecture
])
def check_build(project, engine, sanitizer, architecture):
"""Execute helper.py's check_build command on |project|, assuming it was most
recently built with |engine| and |sanitizer| for |architecture|."""
execute_helper_command([
'check_build', project, '--engine', engine, '--sanitizer', sanitizer,
'--architecture', architecture
])
def should_build_coverage(project_yaml):
"""Returns True if a coverage build should be done based on project.yaml
contents."""
# Enable coverage builds on projects that use engines. Those that don't use
# engines shouldn't get coverage builds.
engines = project_yaml.get('fuzzing_engines', DEFAULT_ENGINES)
engineless = 'none' in engines
if engineless:
assert_message = ('Forbidden to specify multiple engines for '
'"fuzzing_engines" if "none" is specified.')
assert len(engines) == 1, assert_message
return False
language = project_yaml.get('language')
if language not in LANGUAGES_WITH_COVERAGE_SUPPORT:
print(('Project is written in "{language}", '
'coverage is not supported yet.').format(language=language))
return False
return True
def should_build(project_yaml):
"""Returns True on if the build specified is enabled in the project.yaml."""
if os.getenv('SANITIZER') == 'coverage':
# This assumes we only do coverage builds with libFuzzer on x86_64.
return should_build_coverage(project_yaml)
def is_enabled(env_var, yaml_name, defaults):
"""Is the value of |env_var| enabled in |project_yaml| (in the |yaml_name|
section)? Uses |defaults| if |yaml_name| section is unspecified."""
return os.getenv(env_var) in project_yaml.get(yaml_name, defaults)
return (is_enabled('ENGINE', 'fuzzing_engines', DEFAULT_ENGINES) and
is_enabled('SANITIZER', 'sanitizers', DEFAULT_SANITIZERS) and
is_enabled('ARCHITECTURE', 'architectures', DEFAULT_ARCHITECTURES))
def build_project(project):
"""Do the build of |project| that is specified by the environment variables -
SANITIZER, ENGINE, and ARCHITECTURE."""
root = get_oss_fuzz_root()
project_yaml_path = os.path.join(root, 'projects', project, 'project.yaml')
with open(project_yaml_path) as file_handle:
project_yaml = yaml.safe_load(file_handle)
if project_yaml.get('disabled', False):
print('Project {0} is disabled, skipping build.'.format(project))
return
engine = os.getenv('ENGINE')
sanitizer = os.getenv('SANITIZER')
architecture = os.getenv('ARCHITECTURE')
if not should_build(project_yaml):
print(('Specified build: engine: {0}, sanitizer: {1}, architecture: {2} '
'not enabled for this project: {3}. Skipping build.').format(
engine, sanitizer, architecture, project))
return
print('Building project', project)
build_fuzzers(project, engine, sanitizer, architecture)
if engine != 'none' and sanitizer != 'coverage':
check_build(project, engine, sanitizer, architecture)
def main():
"""Build modified projects."""
projects = get_modified_buildable_projects()
failed_projects = []
for project in projects:
try:
build_project(project)
except subprocess.CalledProcessError:
failed_projects.append(project)
if failed_projects:
print('Failed projects:', ' '.join(failed_projects))
return 1
return 0
if __name__ == '__main__':
sys.exit(main())