#!/usr/bin/env python2

"""Script to do basic sanity checking for target_link_libraries() commands in
CMakeLists.txt files.

Scans C++ sources specified in add_library() commands for includes that look
like they are in the Quickstep source tree, then makes sure that the
corresponding libraries appear in the target_link_libraries() command for the
library.

TODO List / Known Issues & Limitations:
    - Script skips over targets that are built conditionally (i.e. that have
      multiple add_library() commands) and just prints a warning to the user.
    - Script only validates libraries, not executables.
    - Script only checks quickstep includes and libraries, so it will not
      detect missing third party libraries.
"""

#   Copyright 2011-2015 Quickstep Technologies LLC.
#   Copyright 2015 Pivotal Software, Inc.
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import os
import sys

# Don't scan these directories for quickstep modules.
EXCLUDED_TOP_LEVEL_DIRS = ["build", "third_party"]

# Explicitly ignored dependencies (special headers with no other quickstep
# dependencies).
IGNORED_DEPENDENCIES = frozenset(
    ["quickstep_threading_WinThreadsAPI",
     "quickstep_utility_textbasedtest_TextBasedTest",
     "quickstep_utility_textbasedtest_TextBasedTestDriver",
     "quickstep_storage_bitweaving_BitWeavingHIndexSubBlock",
     "quickstep_storage_bitweaving_BitWeavingIndexSubBlock",
     "quickstep_storage_bitweaving_BitWeavingVIndexSubBlock"])

# States when scanning a CMakeLists.txt file.
CMAKE_SCANNING_NONE = 0
CMAKE_SCANNING_LIBRARY = 1
CMAKE_SCANNING_TARGET_LINK_LIBRARIES = 2
CMAKE_SCANNING_IGNORE = 3

def convert_path_to_targetname(include_path):
    """Convert an included header file's path to a quickstep library target in
    cmake.

    Args:
        include_path (str): A header file path taken from a C++ include
            statement.

    Returns:
        str: The target name in CMake that corresponds to the specified header.
    """
    path_components = include_path.split("/")
    for idx in range(len(path_components) - 1):
        path_components[idx] = path_components[idx].replace("_", "")
    if path_components[-1].endswith("_gen.hpp"):
        # Generated header (e.g. parser or lexer).
        path_components[-1] = path_components[-1][:-8]
    elif path_components[-1].endswith(".hpp"):
        # Regular header.
        path_components[-1] = path_components[-1][:-4]
    elif path_components[-1].endswith(".pb.h"):
        # Generated protobuf header.
        path_components[-1] = path_components[-1][:-5] + "_proto"
    return "quickstep_" + "_".join(path_components)

def convert_proto_path_to_targetname(import_path):
    """Convert an imported proto's path to a quickstep library target in CMake.

    Args:
        import_path (str): A proto definition file path taken from a protobuf
            import statement.

    Returns:
        str: The target name in CMake that corresponds to the specified proto
            definition.
    """
    path_components = import_path.split("/")
    for idx in range(len(path_components) - 1):
        path_components[idx] = path_components[idx].replace("_", "")
    if path_components[-1].endswith(".proto"):
        path_components[-1] = path_components[-1][:-6] + "_proto"
    return "quickstep_" + "_".join(path_components)

def get_module_targetname_for_cmakelists(cmakelists_filename):
    """Determine what the name for the all-in-one module target should be based
    on the CMakeLists.txt filename with path.

    Args:
        cmakelists_filename (str): CMakeLists.txt filename with path from
            quickstep root.

    Returns:
        str: The target name in CMake that corresponds to the special
            all-in-one library for the module described by the CMakeLists.txt
            file.
    """
    components = []
    (head, tail) = os.path.split(cmakelists_filename)
    while head != "":
        (head, tail) = os.path.split(head)
        if tail != ".":
            components.append(tail.replace("_", ""))
    components.append("quickstep")
    components.reverse()
    return "_".join(components)

def get_dependency_set_from_cpp_src(src_filename, qs_module_dirs):
    """Read the C++ source file at 'src_filename' and return a set of all
    quickstep libraries it includes headers for.

    Args:
        src_filename (str): A path to a C++ source file (may be header or
            implementation).
        qs_module_dirs (List[str]): List of directories for top-level quickstep
            modules

    Returns:
        Set[str]: A set of CMake target names for the quickstep library targets
            that the C++ file includes.
    """
    dependency_set = set()
    with open(src_filename, "r") as src_file:
        for line in src_file:
            if line.startswith("#include \""):
                include_filename = line[len("#include \""):]
                include_filename = (
                    include_filename[:include_filename.find("\"")])
                # Skip over CMake-generated config headers and -inl companion
                # headers.
                if not (include_filename.endswith("Config.h")
                        or include_filename.endswith("-inl.hpp")):
                    for module_dir in qs_module_dirs:
                        if include_filename.startswith(module_dir):
                            dependency_set.add(
                                convert_path_to_targetname(include_filename))
                            break
    return dependency_set

def get_dependency_set_from_proto_src(src_filename, qs_module_dirs):
    """Read the protobuf definition file at 'src_filename' and return a set of
    all other Quickstep proto libraries it imports.

    Args:
        src_filename (str): A path to a proto definition file.
        qs_module_dirs (List[str]): List of directories for top-level quickstep
            modules

    Returns:
        Set[str]: A set of CMake target names for the quickstep library targets
            that the proto file imports.
    """
    dependency_set = set()
    with open(src_filename, "r") as src_file:
        for line in src_file:
            if line.startswith("import \""):
                import_filename = line[len("import \""):]
                import_filename = import_filename[:import_filename.find("\"")]
                for module_dir in qs_module_dirs:
                    if import_filename.startswith(module_dir):
                        dependency_set.add(
                            convert_proto_path_to_targetname(import_filename))
                        break
    return dependency_set

def process_add_library(qs_module_dirs,
                        directory,
                        add_library_args,
                        deps_from_includes,
                        skipped_targets,
                        generated_targets):
    """Process a CMake add_library() command while scanning a CMakeLists.txt
    file.

    Args:
        qs_module_dirs (List[str]): List of directories for top-level quickstep
            modules
        directory (str): The directory that the CMakeLists.txt file we are
            currently scanning resides in.
        add_library_args (str): The arguments to an add_library() command in
            CMakeLists.txt
        deps_from_includes (Map[str, Set[str]]): A map from a CMake target name
            to the set of other CMake targets it depends on, deduced based on
            what headers the C++/proto sources for the target include. A new
            entry will be added to this map for the target specified by the
            add_library() command.