chromium/third_party/android_build_tools/lint/rebuild_baselines.py

#!/usr/bin/env python3
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Rebuilds baseline files by deleting the xml and re-building lint targets."""

import argparse
import logging
import pathlib
import subprocess
import sys
from typing import List, Optional

_SRC_PATH = pathlib.Path(__file__).resolve().parents[3]
_CLANK_PATH = _SRC_PATH / 'clank'
_OUTPUT_DIR_ROOT = _SRC_PATH / 'out'
_NINJA_PATH = _SRC_PATH / 'third_party' / 'ninja' / 'ninja'
_GN_PATH = _SRC_PATH / 'buildtools' / 'linux64' / 'gn'

sys.path.insert(1, str(_SRC_PATH / 'build'))
import gn_helpers


def build_all_lint_targets(
        out_dir: pathlib.Path,
        args_list: List[str],
        *,
        verbose: bool,
        built_targets: Optional[List[str]] = None) -> List[str]:
    logging.info(f'Create output directory: {out_dir}')
    out_dir.mkdir(parents=True, exist_ok=True)

    args_gn_path = out_dir / 'args.gn'
    args_gn_content = '\n'.join(args_list)
    logging.info(f'Populating {args_gn_path}:\n{args_gn_content}')
    with open(args_gn_path, 'w') as f:
        f.write(args_gn_content)

    logging.info('Run `gn gen`.')
    subprocess.run([_GN_PATH, 'gen', out_dir], check=True)

    if built_targets is None:
        built_targets = []

    logging.info('Finding all lint targets.')
    target_names = []
    output = subprocess.run([_NINJA_PATH, '-C', out_dir, '-t', 'targets'],
                            check=True,
                            text=True,
                            capture_output=True).stdout
    for line in output.splitlines():
        ninja_target = line.rsplit(':', 1)[0]
        # Ensure only full-path targets are used (path:name) instead of the
        # shorthand (name).
        if ':' in ninja_target and ninja_target.endswith('__lint'):
            if ninja_target in built_targets:
                logging.info(
                    f'> Skipping {ninja_target} since it was already built.')
                continue
            logging.info(f'> Found {ninja_target}')
            target_names.append(ninja_target)

    if not target_names:
        logging.info('Did not find any targets to build.')
    else:
        logging.info(f'Re-building lint targets: {target_names}')
        cmd = gn_helpers.CreateBuildCommand(str(out_dir)) + target_names
        # Do not show output by default since all lint warnings are printed.
        result = subprocess.run(cmd, check=False, capture_output=not verbose)
        if result.returncode:
            print('Build failed.')
            print(result.stdout)
            print(result.stderr)
            sys.exit(1)

    return built_targets + target_names


def _remove_redundant_lint_error(path: pathlib.Path):
    with open(path) as f:
        original_content = f.read()

    # Lint error is always the first issue.
    first_issue_idx = original_content.find('    <issue\n')
    if first_issue_idx == -1:
        return

    lint_error_idx = original_content.find('id="LintError"', first_issue_idx)
    if lint_error_idx == -1:
        return

    # If the lint error isn't the first issue, we should keep it.
    if lint_error_idx - first_issue_idx > 20:
        return

    issue_end_idx = original_content.find('</issue>\n', lint_error_idx)
    if issue_end_idx == -1:
        return

    # Replace 10 characters past the start of </issue> to remove extra newlines.
    replaced_content = original_content[:first_issue_idx] + original_content[
        issue_end_idx + 10:]

    with open(path, 'w') as f:
        f.write(replaced_content)


def main():
    logging.basicConfig(
        level=logging.INFO,
        format='%(levelname).1s %(relativeCreated)7d %(message)s')

    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('-v',
                        '--verbose',
                        action='store_true',
                        help='Used to show ninja output.')
    parser.add_argument('-q',
                        '--quiet',
                        action='store_true',
                        help='Used to print only warnings and errors.')
    args = parser.parse_args()

    if args.quiet:
        level = logging.WARNING
    elif args.verbose:
        level = logging.DEBUG
    else:
        level = logging.INFO
    logging.basicConfig(
        level=level, format='%(levelname).1s %(relativeCreated)7d %(message)s')

    include_clank = _CLANK_PATH.exists()

    git_roots = [_SRC_PATH]
    if include_clank:
        git_roots.append(_CLANK_PATH)

    logging.info(f'Removing "lint-baseline.xml" files under {git_roots}.')

    repo_file_paths = []
    for git_root in git_roots:
        git_file_paths = subprocess.run(
            ['git', '-C', str(git_root), 'ls-files'],
            check=True,
            capture_output=True,
            text=True).stdout.splitlines()
        repo_file_paths.extend((git_root, p) for p in git_file_paths)

    for git_root, repo_path in repo_file_paths:
        path = git_root / repo_path
        if path.name == 'lint-baseline.xml' and path.exists():
            logging.info(f'> Deleting: {path}')
            path.unlink()

    out_dir = _OUTPUT_DIR_ROOT / 'Lint-Default'
    gn_args = [
        'use_remoteexec=true',
        'target_os="android"',
        'treat_warnings_as_errors=false',
        'is_component_build=false',
        'enable_chrome_android_internal=false',
    ]
    built_targets = build_all_lint_targets(out_dir,
                                           gn_args,
                                           verbose=args.verbose)

    if include_clank:
        out_dir = _OUTPUT_DIR_ROOT / 'Lint-Clank'
        gn_args = [
            'use_remoteexec=true',
            'target_os="android"',
            'treat_warnings_as_errors=false',
            'is_component_build=false',
        ]
        built_targets = build_all_lint_targets(out_dir,
                                               gn_args,
                                               verbose=args.verbose,
                                               built_targets=built_targets)

    out_dir = _OUTPUT_DIR_ROOT / 'Lint-Cast'
    gn_args = [
        'use_remoteexec=true',
        'target_os="android"',
        'treat_warnings_as_errors=false',
        'is_component_build=false',
        'enable_chrome_android_internal=false',
        'is_cast_android=true',
        'enable_cast_receiver=true',
    ]
    built_targets = build_all_lint_targets(out_dir,
                                           gn_args,
                                           verbose=args.verbose,
                                           built_targets=built_targets)

    out_dir = _OUTPUT_DIR_ROOT / 'Lint-Cronet'
    gn_args = [
        'use_remoteexec=true',
        'target_os="android"',
        'treat_warnings_as_errors=false',
        'is_component_build=false',
        'is_cronet_build = true',
    ]
    built_targets = build_all_lint_targets(out_dir,
                                           gn_args,
                                           verbose=args.verbose,
                                           built_targets=built_targets)

    logging.info('Cleaning up new lint-baseline.xml files.')
    for git_root, repo_path in repo_file_paths:
        path = git_root / repo_path
        if path.name == 'lint-baseline.xml':
            logging.info(f'> Removing redundant LintError: {path}')
            _remove_redundant_lint_error(path)


if __name__ == '__main__':
    main()