blob: e7e73c823cefacd8d167079a03f9d2e497ddf069 [file] [log] [blame]
"""Compilation of utility functions for working with GN."""
from __future__ import absolute_import
import collections
import os
import re
from helpers import branch_utils
import six
# TODO(mbjorge|maasen): generate these based on the base path of the repo
# Default location for GN files
GN_SHA1 = 'chromium/src/buildtools/linux64/gn.sha1'
GN_BINARY = 'chromium/src/buildtools/linux64/gn'
# DEPRECATED: Do not reference this variable in new code. Prefer to use a
# path lookup for the chromium/src project instead.
GN_CHROMIUM_BASE_DIR = 'chromium/src'
GENERATE_GN_HELPER_SCRIPT = 'scripts/helpers/generate_gn_helper.sh'
GN_EXTENSIONS = ('.gn', '.gni')
GN_DONE_MSG = r'Done. Wrote \d+ targets from \d+ files'
GN_HEADER_OK_MSG = r'Header dependency check OK'
GN_ERROR_MSG = (
r'ERROR at //(?P<filename>[^:]+):(?P<line>\d+):\d+: (?P<error>.+?)\n')
GN_ERROR_DIVIDER_MSG = r'___________________'
GN_FORMAT_ERROR_MSG = r':(?P<linenum>\d+):(?P<column>\d+): '
class GnUtilsError(Exception):
"""Gn Utils specific exception."""
def gn_check_error_to_comments(gn_check_output):
"""Convert a gn check error message to comments for a gerrit review.
Args:
gn_check_output: output from the `gn check` command
Returns:
Comment to be added to a review message.
The comment is a dictionary with filenames as keys.
Each filename maps to an array of line-numbered comments for that file.
"""
assert isinstance(gn_check_output, six.string_types)
comments = collections.defaultdict(list)
# Check that GN check did not succeed
if re.search(GN_DONE_MSG, gn_check_output) or re.search(
GN_HEADER_OK_MSG, gn_check_output):
return comments
error_msg = re.compile(GN_ERROR_MSG)
error_divider_msg = re.compile(GN_ERROR_DIVIDER_MSG)
filename, linenum, message = '', '', []
for line in gn_check_output.splitlines(True):
# Look for the start of a new error
match = error_msg.match(line)
if match:
filename = match.group('filename')
linenum = match.group('line')
message = ['ERROR: {}\n'.format(match.group('error'))]
continue
# Look for the end of the current error
match = error_divider_msg.match(line)
if match:
comment = {'line': linenum, 'message': ''.join(message)}
comments[filename].append(comment)
filename, linenum, message = '', '', []
continue
# Add the contents of the error to the current comment
message.append(line)
# The last error message does not have a trailing divider
if message:
comment = {'line': linenum, 'message': ''.join(message)}
comments[filename].append(comment)
return comments
def gn_format_error_to_comments(filename, error_msg):
"""Convert a gn format error message to comments for a gerrit review.
Args:
filename: name of the file that caused the error
error_msg: Output message from gn format. Example:
"ERROR at /path/to/bad/file.gn:10:12: Invalid bar."
Returns:
Comment to be added to a review message.
Example:
{'path/to/bad/file.gn': [{'line': 10, 'message': 'Invalid bar.'}]}
"""
assert error_msg.startswith('ERROR at ')
assert filename in error_msg
lines = error_msg.splitlines(True)
match = re.search(GN_FORMAT_ERROR_MSG, lines[0])
linenum = match.group('linenum')
message = error_msg[match.end():]
return {filename: [{'line': linenum, 'message': message}]}
def filepath_to_gn_path(filename):
"""convert chromium/src/a/b/BUILD.gn to //a/b:*."""
if not filename.endswith('BUILD.gn'):
raise ValueError('Invalid path %s. Must be a BUILD.gn file.', filename)
if not filename.startswith(GN_CHROMIUM_BASE_DIR):
raise ValueError('Invalid path %s. Must start with %s',
filename, GN_CHROMIUM_BASE_DIR)
target_path = os.path.relpath(os.path.dirname(filename), GN_CHROMIUM_BASE_DIR)
if target_path == '.':
target_path = ''
return '//{}:*'.format(target_path)
def gn_label_to_target_name(gn_label):
"""Convert '//foo/bar:baz' to 'baz'."""
if not gn_label:
raise ValueError('Input label must not be empty.')
if gn_label[-1] in [':', '/']:
raise ValueError('A GN label must not end with {!r}: {}'.format(
gn_label[-1], gn_label))
if ':' in gn_label:
return gn_label.split(':')[-1]
if '/' in gn_label:
return gn_label.split('/')[-1]
return gn_label
def get_gn_binary(executor):
"""Returns the path to a gn binary.
This attempts to download the latest version of GN. If the download fails,
then an older version will be used. If GN cannot be downloaded and no
older version can be found, this throws a GnUtilsError.
Args:
executor: Executes the download of GN via executor.exec_subproces
Returns:
A relative path to the GN binary.
Raises:
GnUtilsError: if GN cannot be found or downloaded.
"""
ret, _, _ = _download_gn(GN_SHA1, executor)
# If the download succeeded, trust that the binary is in the correct place
# and return. If the download fails, an old copy could still exist and be
# used.
if ret == 0 or _verify_gn_exists():
return GN_BINARY
raise GnUtilsError('GN binary could not be found or downloaded.')
def _verify_gn_exists():
"""Returns boolean if gn exists."""
return os.path.exists(GN_BINARY)
def _download_gn(sha1_path, executor):
"""Downloads the latest GN from google storage.
Args:
sha1_path: Path to gn.sha1 used to download GN.
Using gn_utils.GN_SHA1 will result in GN
being placed at gn_utils.GN_BINARY
executor: Executes the download via executor.exec_subprocess
Returns:
(return_code, stdout, stderr) from the download process
"""
assert isinstance(sha1_path, str)
assert hasattr(executor, 'exec_subprocess')
command = ['download_from_google_storage',
'--no_resume',
'--platform=linux*',
'--no_auth',
'--bucket', 'chromium-gn',
'-s', sha1_path]
return executor.exec_subprocess(command)