chromium/tools/win/update_idl.py

# 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.
"""A tool for updating IDL COM headers/TLB after updating IDL template.

This tool must be run from a Windows machine at the source root directory.

Example:
    python3 tools/win/update_idl.py
"""

import os
import platform
import subprocess

class IDLUpdateError(Exception):
    """Module exception class."""


class IDLUpdater:
    """A class to update IDL COM headers/TLB files based on config."""

    def __init__(self, idl_gn_target: str, target_cpu: str,
                 is_chrome_branded: bool):
        self.idl_gn_target = idl_gn_target
        self.target_cpu = target_cpu
        self.is_chrome_branded = str(is_chrome_branded).lower()
        self.output_dir = r'out\idl_update'

    def update(self) -> None:
        print('Updating', self.idl_gn_target, 'IDL files for', self.target_cpu,
              'CPU, chrome_branded:', self.is_chrome_branded, '...')
        self._make_output_dir()
        self._gen_gn_args()
        self._autoninja_and_update()

    def _make_output_dir(self) -> None:
        if not os.path.exists(self.output_dir):
            os.makedirs(self.output_dir)

    def _gen_gn_args(self) -> None:
        # If the gn_args file already exists and has the desired values then
        # don't touch it - this avoids unnecessary and expensive gn gen
        # invocations.
        gn_args_path = os.path.join(self.output_dir, 'args.gn')
        contents = (f'target_cpu="{self.target_cpu}"\n'
                    f'is_chrome_branded={self.is_chrome_branded}\n'
                    f'is_debug=true\n'
                    f'enable_nacl=false\n'
                    f'blink_symbol_level=0\n'
                    f'v8_symbol_level=0\n').format()
        if os.path.exists(gn_args_path):
            with open(gn_args_path, 'rt', newline='') as f:
                new_contents = f.read()
                if new_contents == contents:
                    return

        # `subprocess` may interpret the complex config values passed via
        # `--args` differently than intended. Generate the default gn.args first
        # and then update it by writing directly.

        # gen args with default values.
        print('Generating', gn_args_path, 'with default values.')
        subprocess.run(['gn.bat', 'gen', self.output_dir], check=True)

        # Manually update args.gn
        print('Write', gn_args_path, 'with desired config.')
        with open(gn_args_path, 'wt', newline='') as f:
            f.write(contents)
        print('Done.')

    def _autoninja_and_update(self) -> None:
        print('Check if update is needed by building the target...')
        # Use -j 1 since otherwise the exact build output is not deterministic.
        proc = subprocess.run([
            'autoninja.bat', '-j', '1', '-C', self.output_dir,
            self.idl_gn_target
        ],
                              capture_output=True,
                              check=False,
                              universal_newlines=True)
        if proc.returncode == 0:
            print('No update is needed.\n')
            return

        cmd = self._extract_update_command(proc.stdout)
        print('Updating IDL COM headers/TLB by running: [', cmd, ']...')
        subprocess.run(cmd, shell=True, capture_output=True, check=True)
        print('Done.\n')

    def _extract_update_command(self, stdout: str) -> str:
        # Exclude blank lines.
        lines = list(filter(None, stdout.splitlines()))

        if (len(lines) < 3
                or 'ninja: build stopped: subcommand failed.' not in lines[-1]
                or 'copy /y' not in lines[-2]
                or 'To rebaseline:' not in lines[-3]):
            print('-' * 80)
            print('STDOUT:')
            print(stdout)
            print('-' * 80)

            raise IDLUpdateError(
                'Unexpected autoninja error, or update this tool if the output '
                'format is changed.')

        return lines[-2].strip().replace('..\\..\\', '')


def check_running_environment() -> None:
    if 'Windows' not in platform.system():
        raise IDLUpdateError('This tool must run from Windows platform.')

    proc = subprocess.run(['git.bat', 'rev-parse', '--show-toplevel'],
                          capture_output=True,
                          check=True)

    if proc.returncode != 0:
        raise IDLUpdateError(
            'Failed to run git for finding source root directory.')

    source_root = os.path.abspath(proc.stdout.decode('utf-8').strip()).lower()
    if not os.path.exists(source_root):
        raise IDLUpdateError('Unexpected failure to get source root directory')

    cwd = os.getcwd().lower()
    if cwd != source_root:
        raise IDLUpdateError(f'This tool must run from project root folder. '
                             f'CWD: [{cwd}] vs ACTUAL:[{source_root}]')

    # Build performance output interferes with error parsing. Silence it.
    os.environ['NINJA_SUMMARIZE_BUILD'] = '0'


def main():
    check_running_environment()

    for target_cpu in ['arm64', 'x64', 'x86']:
        for idl_target in [
                'updater_idl',
                'updater_idl_user',
                'updater_idl_system',
                'updater_internal_idl',
                'updater_internal_idl_user',
                'updater_internal_idl_system',
                'updater_legacy_idl',
                'updater_legacy_idl_user',
                'updater_legacy_idl_system',
                'google_update',
                'elevation_service_idl',
                'gaia_credential_provider_idl',
                'iaccessible2',
                'ichromeaccessible',
                'isimpledom',
                'remoting_lib_idl',
        ]:
            IDLUpdater(idl_target + '_idl_action', target_cpu, False).update()


if __name__ == '__main__':
    main()