• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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