#!/usr/bin/env python

"""Helps to keep BUILD.gn files in sync with the corresponding CMakeLists.txt.

For each BUILD.gn file in the tree, checks if the list of cpp files in
it is identical to the list of cpp files in the corresponding CMakeLists.txt
file, and prints the difference if not.

Also checks that each CMakeLists.txt file below unittests/ folders that define
binaries have corresponding BUILD.gn files.
"""

from __future__ import print_function

import os
import re
import subprocess
import sys


def sync_source_lists():
    # Use shell=True on Windows in case git is a bat file.
    gn_files = subprocess.check_output(['git', 'ls-files', '*BUILD.gn'],
                                       shell=os.name == 'nt').splitlines()

    # Matches e.g. |   "foo.cpp",|, captures |foo| in group 1.
    gn_cpp_re = re.compile(r'^\s*"([^"]+\.(?:cpp|c|h|S))",$', re.MULTILINE)
    # Matches e.g. |   foo.cpp|, captures |foo| in group 1.
    cmake_cpp_re = re.compile(r'^\s*([A-Za-z_0-9./-]+\.(?:cpp|c|h|S))$',
                              re.MULTILINE)

    changed = False
    for gn_file in gn_files:
        # The CMakeLists.txt for llvm/utils/gn/secondary/foo/BUILD.gn is
        # directly at foo/CMakeLists.txt.
        strip_prefix = 'llvm/utils/gn/secondary/'
        if not gn_file.startswith(strip_prefix):
            continue
        cmake_file = os.path.join(
                os.path.dirname(gn_file[len(strip_prefix):]), 'CMakeLists.txt')
        if not os.path.exists(cmake_file):
            continue

        def get_sources(source_re, text):
            return set([m.group(1) for m in source_re.finditer(text)])
        gn_cpp = get_sources(gn_cpp_re, open(gn_file).read())
        cmake_cpp = get_sources(cmake_cpp_re, open(cmake_file).read())

        if gn_cpp == cmake_cpp:
            continue

        changed = True
        print(gn_file)
        add = sorted(cmake_cpp - gn_cpp)
        if add:
            print('add:\n' + '\n'.join('    "%s",' % a for a in add))
        remove = sorted(gn_cpp - cmake_cpp)
        if remove:
            print('remove:\n' + '\n'.join(remove))
        print()
    return changed


def sync_unittests():
    # Matches e.g. |add_llvm_unittest_with_input_files|.
    unittest_re = re.compile(r'^add_\S+_unittest', re.MULTILINE)

    checked = [ 'clang', 'clang-tools-extra', 'lld', 'llvm' ]
    changed = False
    for c in checked:
        for root, _, _ in os.walk(os.path.join(c, 'unittests')):
            cmake_file = os.path.join(root, 'CMakeLists.txt')
            if not os.path.exists(cmake_file):
                continue
            if not unittest_re.search(open(cmake_file).read()):
                continue  # Skip CMake files that just add subdirectories.
            gn_file = os.path.join('llvm/utils/gn/secondary', root, 'BUILD.gn')
            if not os.path.exists(gn_file):
                changed = True
                print('missing GN file %s for unittest CMake file %s' %
                      (gn_file, cmake_file))
    return changed


def main():
    src = sync_source_lists()
    tests = sync_unittests()
    if src or tests:
        sys.exit(1)



if __name__ == '__main__':
    main()
