blob: bb8fd9eea2c9c6d798e1c8314c8577ed36ab6ce9 [file]
"""Build step class for running `gn check`."""
import logging
import os
from helpers import git_utils
from helpers import gn_utils
from helpers import py_env_utils
from slave import base_step
_GN_ROOT_DIR_PROJECT = 'chromium/src'
_GN_CHECK_OUT_DIR = 'out_gn_check'
_MESH_TAG = '-mesh-eng'
# Bare bones GN configs that allow for basic smoke tests to run during
# presubmit. All the full builds also do GN checks as part of the build,
# so the full GN configs will also be tested.
_DESKTOP_VIDEO_GN_ARGS = ['is_chromecast=true',
'chromecast_branding="internal"']
_DESKTOP_AUDIO_GN_ARGS = ['is_chromecast=true',
'chromecast_branding="internal"',
'is_cast_audio_only=true']
_GN_CONFIGS = [_DESKTOP_VIDEO_GN_ARGS,
_DESKTOP_AUDIO_GN_ARGS,]
class GnCheckStepBase(base_step.BaseStep):
"""Base step for running GnCheck.
The key part here being shared is the method for parsing gn check output
and making it into a comment.
"""
def __init__(self, name:str='gn check', **kwargs):
super().__init__(name=name, **kwargs)
def run(self):
raise NotImplementedError
def gn_check_to_review(self, stdout):
"""Converts gn_check |stdout| into comments and adds them to the review."""
comments = gn_utils.gn_check_error_to_comments(stdout)
# GN makes comments relative to the root GN directory,
# but we want the file comments to be relative to |self.directory|
directory_prefix = self.directory
gn_root_dir = self.get_project_path(_GN_ROOT_DIR_PROJECT)
if directory_prefix.startswith(gn_root_dir):
directory_prefix = directory_prefix[len(gn_root_dir + os.path.sep):]
# GN can generate comments for files not directly included in the CL, so
# split these comments out to the review message
files_in_cl = git_utils.changed_files(self, directory=self.directory)
file_comments, review_comments = {}, {}
for filename, comment_list in comments.items():
full_filename = filename
if directory_prefix and filename.startswith(directory_prefix):
filename = filename[len(directory_prefix + os.path.sep):]
if filename in files_in_cl:
file_comments[filename] = comment_list
else:
review_comments[full_filename] = comment_list
# To limit comment filesize, only add the first comment. In theory, seeing
# one GN check issue will inspire people to run gn check locally to clean
# up the other issues.
logging.info('All GN check comments: %s', comments)
break
# Generate the review message
review_message = [
'Run `gn check out_chromecast_$PRODUCT/release` or ',
'`lunch $PRODUCT && make build.ninja ENABLE_GN_CHECK=1`',
]
for filename, comments in review_comments.items():
review_message.append(filename)
for comment in comments:
review_message.append('LINE: ' + comment['line'])
review_message.append(comment['message'])
review_message.append('----------')
review_message.append('')
self.add_review({
'message': '\n'.join(review_message),
'comments': file_comments
})
class GnCheckStep(GnCheckStepBase):
"""Build step class for running gn check via generate_gn helper."""
def __init__(self, name: str='gn check', **kwargs):
"""Creates a GnCheckStep instance.
Args:
**kwargs: Any additional args to pass to BaseStep.
"""
super().__init__(name=name, **kwargs)
def run(self):
"""Runs gn gen --check and reports any errors.
Sets a build property "review" with the output if there are errors.
Returns:
True iff gn check reported no errors.
"""
out_dir = os.path.join(self.get_tmp_dir(), _GN_CHECK_OUT_DIR)
root_dir = self.get_project_path(_GN_ROOT_DIR_PROJECT)
all_checks_passed = True
for gn_config in _GN_CONFIGS:
command = [gn_utils.GN_BINARY,
'gen', out_dir,
'--root={}'.format(root_dir),
'--args={}'.format(' '.join(gn_config)),
'--check']
returncode, stdout, _ = self.exec_subprocess(command, quiet=True)
if returncode != 0:
all_checks_passed = False
self.gn_check_to_review(stdout)
return all_checks_passed
class GnCheckForOta(GnCheckStepBase):
"""Step for running gn check on OTA builds."""
def __init__(self, build_name, **kwargs):
"""Creats a GnCheckForOta instance.
Args:
build_name: Name of build (e.g. pepperoni-eng, tvdefault-eng)
**kwargs: Any additional args to pass to BaseStep.
"""
GnCheckStepBase.__init__(self, **kwargs)
self._enable_gn_check = gn_utils.is_gn_check_enabled(self.manifest_branch)
self._product = build_name.split('-')[0]
self._is_mesh_build = build_name.endswith(_MESH_TAG)
self._build_name = build_name.replace(_MESH_TAG, '-eng')
def rerun_if_necessary(self, returncode, stdout, stderr, command, env):
"""Do some cleanup and rerun |command| if it failed previously.
In the majority of runs, this function will just be a no-op.
Occasionally, running `make` will produce a corrupted
AllocationTestHarness.P file. This bad file will stay around until it is
cleaned out, causing builds to fail unnecessarily. This checks for this case
and fixes it. (b/31939016)
Args:
returncode: Returncode from previous run of |command|
stdout: Stdout from previous run of |command|
stderr: Stderr from previous run of |command|
command: Command to potentially re-run (as a list of strings)
env: Extra environment variables to use
Returns:
(returncode, stdout, stderr) from |command|
"""
if returncode != 0 and 'AllocationTestHarness.P' in stderr:
logging.warning('Bad AllocationTestHarness.P file was detected'
' (b/31939016). Cleaning and then re-trying the previous'
' command.')
clean_cmd = ['make', 'clean']
self.exec_subprocess(clean_cmd)
returncode, stdout, stderr = self.exec_subprocess(command, env=env)
return returncode, stdout, stderr
def run(self):
""""Builds the build.ninja target, passing --check to `gn gen`.
Returns:
True iff there were no gn check errors.
"""
# `make PRODUCT-$product-build.ninja` is essentally equivalent to
# `lunch $product-eng && make build.ninja`. The build.ninja target
# then invokes `gn gen --check`.
command = [
'make', '-j{}'.format(self.get_num_jobs()),
'PRODUCT-' + self._product + '-build.ninja',
'ENABLE_GN_CHECK=1',
'DISABLE_AUTO_INSTALLCLEAN=true',
]
if self._is_mesh_build:
command += ['USE_GWIFI=1']
command += self.build_accelerator.make_flags
env = self.build_accelerator.environment_variables
returncode, stdout, stderr = self.exec_subprocess(command, env=env)
returncode, stdout, _ = self.rerun_if_necessary(returncode, stdout, stderr,
command, env)
if returncode == 0:
logging.info('gn check reported no issues.')
return True
self.gn_check_to_review(stdout)
return False