#!/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', 'libfuzzer'] DEFAULT_SANITIZERS = ['address', 'undefined'] def get_modified_buildable_projects(): """Returns a list of all the projects modified in this commit that have a build.sh file.""" master_head_sha = subprocess.check_output( ['git', 'merge-base', 'HEAD', 'FETCH_HEAD']).decode().strip() output = subprocess.check_output( ['git', 'diff', '--name-only', 'HEAD', master_head_sha]).decode() projects_regex = '.*projects/(?P.*)/.*\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(project_yaml): """Is the build specified by travis enabled in the |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('TRAVIS_ENGINE', 'fuzzing_engines', DEFAULT_ENGINES) and is_enabled('TRAVIS_SANITIZER', 'sanitizers', DEFAULT_SANITIZERS) and is_enabled('TRAVIS_ARCHITECTURE', 'architectures', DEFAULT_ARCHITECTURES)) def build_project(project): """Do the build of |project| that is specified by the TRAVIS_* environment variables (TRAVIS_SANITIZER, TRAVIS_ENGINE, and TRAVIS_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('TRAVIS_ENGINE') sanitizer = os.getenv('TRAVIS_SANITIZER') architecture = os.getenv('TRAVIS_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': check_build(project, engine, sanitizer, architecture) def main(): """Build modified projects on travis.""" 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())