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 enum 22import os 23import re 24import sys 25import subprocess 26import yaml 27 28# pylint: disable=wrong-import-position,import-error 29sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 30 31import constants 32 33CANARY_PROJECT = 'skcms' 34 35DEFAULT_ARCHITECTURES = ['x86_64'] 36DEFAULT_ENGINES = ['afl', 'honggfuzz', 'libfuzzer'] 37DEFAULT_SANITIZERS = ['address', 'undefined'] 38 39 40def get_changed_files_output(): 41 """Returns the output of a git command that discovers changed files.""" 42 branch_commit_hash = subprocess.check_output( 43 ['git', 'merge-base', 'HEAD', 'origin/HEAD']).strip().decode() 44 45 return subprocess.check_output( 46 ['git', 'diff', '--name-only', branch_commit_hash + '..']).decode() 47 48 49def get_modified_buildable_projects(): 50 """Returns a list of all the projects modified in this commit that have a 51 build.sh file.""" 52 git_output = get_changed_files_output() 53 projects_regex = '.*projects/(?P<name>.*)/.*\n' 54 modified_projects = set(re.findall(projects_regex, git_output)) 55 projects_dir = os.path.join(get_oss_fuzz_root(), 'projects') 56 # Filter out projects without Dockerfile files since new projects and reverted 57 # projects frequently don't have them. In these cases we don't want Travis's 58 # builds to fail. 59 modified_buildable_projects = [] 60 for project in modified_projects: 61 if not os.path.exists(os.path.join(projects_dir, project, 'Dockerfile')): 62 print('Project {0} does not have Dockerfile. skipping build.'.format( 63 project)) 64 continue 65 modified_buildable_projects.append(project) 66 return modified_buildable_projects 67 68 69def get_oss_fuzz_root(): 70 """Get the absolute path of the root of the oss-fuzz checkout.""" 71 script_path = os.path.realpath(__file__) 72 return os.path.abspath( 73 os.path.dirname(os.path.dirname(os.path.dirname(script_path)))) 74 75 76def execute_helper_command(helper_command): 77 """Execute |helper_command| using helper.py.""" 78 root = get_oss_fuzz_root() 79 script_path = os.path.join(root, 'infra', 'helper.py') 80 command = ['python', script_path] + helper_command 81 print('Running command: %s' % ' '.join(command)) 82 subprocess.check_call(command) 83 84 85def build_fuzzers(project, engine, sanitizer, architecture): 86 """Execute helper.py's build_fuzzers command on |project|. Build the fuzzers 87 with |engine| and |sanitizer| for |architecture|.""" 88 execute_helper_command([ 89 'build_fuzzers', project, '--engine', engine, '--sanitizer', sanitizer, 90 '--architecture', architecture 91 ]) 92 93 94def check_build(project, engine, sanitizer, architecture): 95 """Execute helper.py's check_build command on |project|, assuming it was most 96 recently built with |engine| and |sanitizer| for |architecture|.""" 97 execute_helper_command([ 98 'check_build', project, '--engine', engine, '--sanitizer', sanitizer, 99 '--architecture', architecture 100 ]) 101 102 103def should_build_coverage(project_yaml): 104 """Returns True if a coverage build should be done based on project.yaml 105 contents.""" 106 # Enable coverage builds on projects that use engines. Those that don't use 107 # engines shouldn't get coverage builds. 108 engines = project_yaml.get('fuzzing_engines', DEFAULT_ENGINES) 109 engineless = 'none' in engines 110 if engineless: 111 assert_message = ('Forbidden to specify multiple engines for ' 112 '"fuzzing_engines" if "none" is specified.') 113 assert len(engines) == 1, assert_message 114 return False 115 116 language = project_yaml.get('language') 117 if language not in constants.LANGUAGES_WITH_COVERAGE_SUPPORT: 118 print(('Project is written in "{language}", ' 119 'coverage is not supported yet.').format(language=language)) 120 return False 121 122 return True 123 124 125def should_build(project_yaml): 126 """Returns True on if the build specified is enabled in the project.yaml.""" 127 128 if os.getenv('SANITIZER') == 'coverage': 129 # This assumes we only do coverage builds with libFuzzer on x86_64. 130 return should_build_coverage(project_yaml) 131 132 def is_enabled(env_var, yaml_name, defaults): 133 """Is the value of |env_var| enabled in |project_yaml| (in the |yaml_name| 134 section)? Uses |defaults| if |yaml_name| section is unspecified.""" 135 return os.getenv(env_var) in project_yaml.get(yaml_name, defaults) 136 137 return (is_enabled('ENGINE', 'fuzzing_engines', DEFAULT_ENGINES) and 138 is_enabled('SANITIZER', 'sanitizers', DEFAULT_SANITIZERS) and 139 is_enabled('ARCHITECTURE', 'architectures', DEFAULT_ARCHITECTURES)) 140 141 142def build_project(project): 143 """Do the build of |project| that is specified by the environment variables - 144 SANITIZER, ENGINE, and ARCHITECTURE.""" 145 root = get_oss_fuzz_root() 146 project_yaml_path = os.path.join(root, 'projects', project, 'project.yaml') 147 with open(project_yaml_path) as file_handle: 148 project_yaml = yaml.safe_load(file_handle) 149 150 if project_yaml.get('disabled', False): 151 print('Project {0} is disabled, skipping build.'.format(project)) 152 return 153 154 engine = os.getenv('ENGINE') 155 sanitizer = os.getenv('SANITIZER') 156 architecture = os.getenv('ARCHITECTURE') 157 158 if not should_build(project_yaml): 159 print(('Specified build: engine: {0}, sanitizer: {1}, architecture: {2} ' 160 'not enabled for this project: {3}. Skipping build.').format( 161 engine, sanitizer, architecture, project)) 162 163 return 164 165 print('Building project', project) 166 build_fuzzers(project, engine, sanitizer, architecture) 167 168 if engine != 'none' and sanitizer != 'coverage': 169 check_build(project, engine, sanitizer, architecture) 170 171 172class BuildModifiedProjectsResult(enum.Enum): 173 """Enum containing the return values of build_modified_projects().""" 174 NONE_BUILT = 0 175 BUILD_SUCCESS = 1 176 BUILD_FAIL = 2 177 178 179def build_modified_projects(): 180 """Build modified projects. Returns BuildModifiedProjectsResult.NONE_BUILT if 181 no builds were attempted. Returns BuildModifiedProjectsResult.BUILD_SUCCESS if 182 all attempts succeed, otherwise returns 183 BuildModifiedProjectsResult.BUILD_FAIL.""" 184 projects = get_modified_buildable_projects() 185 if not projects: 186 return BuildModifiedProjectsResult.NONE_BUILT 187 188 failed_projects = [] 189 for project in projects: 190 try: 191 build_project(project) 192 except subprocess.CalledProcessError: 193 failed_projects.append(project) 194 195 if failed_projects: 196 print('Failed projects:', ' '.join(failed_projects)) 197 return BuildModifiedProjectsResult.BUILD_FAIL 198 199 return BuildModifiedProjectsResult.BUILD_SUCCESS 200 201 202def is_infra_changed(): 203 """Returns True if the infra directory was changed.""" 204 git_output = get_changed_files_output() 205 infra_code_regex = '.*infra/.*\n' 206 return re.search(infra_code_regex, git_output) is not None 207 208 209def build_base_images(): 210 """Builds base images.""" 211 # TODO(jonathanmetzman): Investigate why caching fails so often and 212 # when we improve it, build base-clang as well. Also, move this function 213 # to a helper command when we can support base-clang. 214 execute_helper_command(['pull_images']) 215 images = [ 216 'base-image', 217 'base-builder', 218 'base-builder-go', 219 'base-builder-jvm', 220 'base-builder-python', 221 'base-builder-rust', 222 'base-builder-swift', 223 'base-runner', 224 ] 225 for image in images: 226 try: 227 execute_helper_command(['build_image', image, '--no-pull', '--cache']) 228 except subprocess.CalledProcessError: 229 return 1 230 231 return 0 232 233 234def build_canary_project(): 235 """Builds a specific project when infra/ is changed to verify that infra/ 236 changes don't break things. Returns False if build was attempted but 237 failed.""" 238 239 try: 240 build_project('skcms') 241 except subprocess.CalledProcessError: 242 return False 243 244 return True 245 246 247def main(): 248 """Build modified projects or canary project.""" 249 os.environ['OSS_FUZZ_CI'] = '1' 250 infra_changed = is_infra_changed() 251 if infra_changed: 252 print('Pulling and building base images first.') 253 if build_base_images(): 254 return 1 255 256 result = build_modified_projects() 257 if result == BuildModifiedProjectsResult.BUILD_FAIL: 258 return 1 259 260 # It's unnecessary to build the canary if we've built any projects already. 261 no_projects_built = result == BuildModifiedProjectsResult.NONE_BUILT 262 should_build_canary = no_projects_built and infra_changed 263 if should_build_canary and not build_canary_project(): 264 return 1 265 266 return 0 267 268 269if __name__ == '__main__': 270 sys.exit(main()) 271