1#!/usr/bin/env python 2# Copyright 2019 Google Inc. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16################################################################################ 17"""Build modified projects.""" 18 19from __future__ import print_function 20 21import os 22import re 23import subprocess 24import yaml 25 26DEFAULT_ARCHITECTURES = ['x86_64'] 27DEFAULT_ENGINES = ['afl', 'libfuzzer'] 28DEFAULT_SANITIZERS = ['address', 'undefined'] 29 30 31def get_modified_buildable_projects(): 32 """Returns a list of all the projects modified in this commit that have a 33 build.sh file.""" 34 master_head_sha = subprocess.check_output( 35 ['git', 'merge-base', 'HEAD', 'FETCH_HEAD']).decode().strip() 36 output = subprocess.check_output( 37 ['git', 'diff', '--name-only', 'HEAD', master_head_sha]).decode() 38 projects_regex = '.*projects/(?P<name>.*)/.*\n' 39 modified_projects = set(re.findall(projects_regex, output)) 40 projects_dir = os.path.join(get_oss_fuzz_root(), 'projects') 41 # Filter out projects without build.sh files since new projects and reverted 42 # projects frequently don't have them. In these cases we don't want Travis's 43 # builds to fail. 44 modified_buildable_projects = [] 45 for project in modified_projects: 46 if not os.path.exists(os.path.join(projects_dir, project, 'build.sh')): 47 print('Project {0} does not have a build.sh. skipping build.'.format( 48 project)) 49 continue 50 modified_buildable_projects.append(project) 51 return modified_buildable_projects 52 53 54def get_oss_fuzz_root(): 55 """Get the absolute path of the root of the oss-fuzz checkout.""" 56 script_path = os.path.realpath(__file__) 57 return os.path.abspath( 58 os.path.dirname(os.path.dirname(os.path.dirname(script_path)))) 59 60 61def execute_helper_command(helper_command): 62 """Execute |helper_command| using helper.py.""" 63 root = get_oss_fuzz_root() 64 script_path = os.path.join(root, 'infra', 'helper.py') 65 command = ['python', script_path] + helper_command 66 print('Running command: %s' % ' '.join(command)) 67 subprocess.check_call(command) 68 69 70def build_fuzzers(project, engine, sanitizer, architecture): 71 """Execute helper.py's build_fuzzers command on |project|. Build the fuzzers 72 with |engine| and |sanitizer| for |architecture|.""" 73 execute_helper_command([ 74 'build_fuzzers', project, '--engine', engine, '--sanitizer', sanitizer, 75 '--architecture', architecture 76 ]) 77 78 79def check_build(project, engine, sanitizer, architecture): 80 """Execute helper.py's check_build command on |project|, assuming it was most 81 recently built with |engine| and |sanitizer| for |architecture|.""" 82 execute_helper_command([ 83 'check_build', project, '--engine', engine, '--sanitizer', sanitizer, 84 '--architecture', architecture 85 ]) 86 87 88def should_build(project_yaml): 89 """Is the build specified by travis enabled in the |project_yaml|?""" 90 91 def is_enabled(env_var, yaml_name, defaults): 92 """Is the value of |env_var| enabled in |project_yaml| (in the |yaml_name| 93 section)? Uses |defaults| if |yaml_name| section is unspecified.""" 94 return os.getenv(env_var) in project_yaml.get(yaml_name, defaults) 95 96 return (is_enabled('TRAVIS_ENGINE', 'fuzzing_engines', DEFAULT_ENGINES) and 97 is_enabled('TRAVIS_SANITIZER', 'sanitizers', DEFAULT_SANITIZERS) and 98 is_enabled('TRAVIS_ARCHITECTURE', 'architectures', 99 DEFAULT_ARCHITECTURES)) 100 101 102def build_project(project): 103 """Do the build of |project| that is specified by the TRAVIS_* environment 104 variables (TRAVIS_SANITIZER, TRAVIS_ENGINE, and TRAVIS_ARCHITECTURE).""" 105 root = get_oss_fuzz_root() 106 project_yaml_path = os.path.join(root, 'projects', project, 'project.yaml') 107 with open(project_yaml_path) as fp: 108 project_yaml = yaml.safe_load(fp) 109 110 if project_yaml.get('disabled', False): 111 print('Project {0} is disabled, skipping build.'.format(project)) 112 return 113 114 engine = os.getenv('TRAVIS_ENGINE') 115 sanitizer = os.getenv('TRAVIS_SANITIZER') 116 architecture = os.getenv('TRAVIS_ARCHITECTURE') 117 118 if not should_build(project_yaml): 119 print(('Specified build: engine: {0}, sanitizer: {1}, architecture: {2} ' 120 'not enabled for this project: {3}. skipping build.').format( 121 engine, sanitizer, architecture, project)) 122 123 return 124 125 print('Building project', project) 126 build_fuzzers(project, engine, sanitizer, architecture) 127 if engine != 'none': 128 check_build(project, engine, sanitizer, architecture) 129 130 131def main(): 132 projects = get_modified_buildable_projects() 133 failed_projects = [] 134 for project in projects: 135 try: 136 build_project(project) 137 except subprocess.CalledProcessError: 138 failed_projects.append(project) 139 140 if failed_projects: 141 print('Failed projects:', ' '.join(failed_projects)) 142 exit(1) 143 144 145if __name__ == '__main__': 146 main() 147