Skip to content

VTKModulesForCxx

Repository source: VTKModulesForCxx

Description

Use this to generate a find_package(VTK COMPONENTS ...) command for CMake.

It requires modules.json, found in your VTK build folder, and your source files. After running, it will generate a find_package(VTK COMPONENTS ...) command listing all the vtk modules needed by the C++ source and header files in your code.

Paths for more than one source path can be specified. If there are spaces in the paths, enclose the path in quotes.

If it is unable to find modules for your headers then a list of these, along with the files they are in, is produced so you can manually add the corresponding modules or rebuild VTK to include the missing modules.

You will need to manually add any third-party modules (if used) to the find_package command.

Question

If you have a question about this example, please use the VTK Discourse Forum

Code

VTKModulesForCxx.py

#!/usr/bin/env python

import collections
import json
import os
import re
from pathlib import Path


def get_program_parameters(argv):
    import argparse
    description = 'Generate a find_package(VTK COMPONENTS ...) command for CMake.'
    epilogue = '''
Uses modules.json and your source files to generate a
  find_package(VTK COMPONENTS ...) command listing all the vtk modules
  needed by the C++ source and header files in your code.

Paths for more than one source path can be specified.

Note than if there are spaces in the paths, enclose the path in quotes.

If it is unable to find modules for your headers then
  a list of these, along with the files they are in, is produced
  so you can manually add the corresponding modules or rebuild VTK
  to include the missing modules.

You will need to manually add any third-party modules
   (if used) to the find_package command.
    '''
    parser = argparse.ArgumentParser(description=description, epilog=epilogue,
                                     formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('json', default=['modules.json'], help='The path to the VTK JSON file (modules.json).')
    parser.add_argument('sources', nargs='+', help='The path to the source files.')
    parser.add_argument('-f', '--file', help='The file name to write the output too.')
    args = parser.parse_args()
    return args.json, args.sources, args.file


class Patterns:
    header_pattern = re.compile(r'^#include *[<\"](\S+)[>\"]')
    vtk_include_pattern = re.compile(r'^(vtk\S+)')
    vtk_qt_include_pattern = re.compile(r'^(QVTK\S+)')


def get_headers_modules(json_data):
    """
    From the parsed JSON data file make a dictionary whose key is the
     header filename and value is the module.
    :param json_data: The parsed JSON file modules.json.
    :return:
    """

    # The headers should be unique to a module, however we will not assume this.
    res = collections.defaultdict(set)
    for k, v in json_data['modules'].items():
        if 'headers' in v:
            for k1 in v['headers']:
                res[k1].add(k)
    return res


def get_vtk_components(jpath, paths):
    """
    Get the VTK components
    :param jpath: The path to the JSON file.
    :param paths: The C++ file paths.
    :return:
    """

    with open(jpath) as data_file:
        json_data = json.load(data_file)
    vtk_headers_modules = get_headers_modules(json_data)

    modules = set()
    inc_no_mod = set()
    inc_no_mod_headers = collections.defaultdict(set)
    mod_implements = collections.defaultdict(set)
    headers = collections.defaultdict(set)

    for path in paths:
        if path.is_file():
            content = path.read_text().split('\n')
            for line in content:
                m = Patterns.header_pattern.match(line.strip())
                if m:
                    # We have a header name, split it from its path (if the path exists).
                    header_parts = os.path.split(m.group(1))
                    m = Patterns.vtk_include_pattern.match(header_parts[1])
                    if m:
                        headers[m.group(1)].add(path)
                        continue
                    m = Patterns.vtk_qt_include_pattern.match(header_parts[1])
                    if m:
                        headers[m.group(1)].add(path)
    for incl in headers:
        if incl in vtk_headers_modules:
            m = vtk_headers_modules[incl]
            for v in m:
                modules.add(v)
        else:
            inc_no_mod.add(incl)
            inc_no_mod_headers[incl] = headers[incl]

    if headers:
        for m in modules:
            if not json_data['modules'][m]['implementable']:
                continue
            for i in json_data['modules']:
                if i in modules:
                    continue
                if m in json_data['modules'][i]['implements']:
                    # Suggest module i since it implements m
                    mod_implements[i].add(m)

    return modules, mod_implements, inc_no_mod, inc_no_mod_headers


def disp_components(modules, module_implements):
    """
    For the found modules display them in a form that the user can
     copy/paste into their CMakeLists.txt file.
    :param modules: The modules.
    :param module_implements: Modules implementing other modules.
    :return:
    """
    res = ['find_package(VTK\n COMPONENTS']
    for m in sorted(modules):
        res.append('    {:s}'.format(m.split('::')[1]))
    if module_implements:
        keys = sorted(module_implements)
        max_width = len(max(keys, key=len).split('::')[1])
        comments = [
            '    #',
            '    # These modules are suggested since they implement an existing module.',
            '    # You may need to uncomment one or more of these.',
            '    # If vtkRenderWindow is used and you want to use OpenGL,',
            '    #   you also need the RenderingOpenGL2 module.',
            '    # If vtkRenderWindowInteractor is used,',
            '    #    uncomment RenderingUI and possibly InteractionStyle.',
            '    # If text rendering is used, uncomment RenderingFreeType',
            '    #'
        ]
        res.extend(comments)
        for key in keys:
            res.append(
                f'    # {key.split("::")[1]:<{max_width}} # implements {", ".join(sorted(module_implements[key]))}')
    res.append(')\n')

    return res


def disp_missing_components(inc_no_mod, inc_no_mod_headers):
    """
    Display the headers along with the missing VTK modules.

    :param inc_no_mod: Missing modules.
    :param inc_no_mod_headers: Headers with missing modules.
    :return:
    """
    if inc_no_mod:
        res = [''
               '*' * 64,
               'You will need to manually add the modules that',
               '  use these headers to the find_package command.',
               'These could be external modules not in the modules.json file.',
               'Or you may need to rebuild VTK to include the missing modules.',
               '',
               'Here are the vtk headers and corresponding files:']
        sinmd = sorted(inc_no_mod)
        for i in sinmd:
            sh = sorted(list(inc_no_mod_headers[i]))
            res.append(f'in {i}:')
            for j in sh:
                res.append(f'   {j}')
        res.append('*' * 64)

        return res
    else:
        return None


def main(json_path, src_paths, ofn):
    jpath = Path(json_path)
    if jpath.is_dir():
        jpath = jpath / 'modules.json'
    if not jpath.is_file():
        print(f'Non existent JSON file: {jpath}')
        return

    paths = list()
    valid_ext = ['.h', '.hxx', '.cxx', '.cpp', '.txx']
    path_list = list()
    for fn in src_paths:
        path = Path(fn)
        if path.is_file() and path.suffix in valid_ext:
            paths.append(path)
        elif path.is_dir():
            for e in valid_ext:
                path_list += list(Path(fn).rglob(f'*{e}'))
            program_path = Path(__file__)
            for path in path_list:
                if path.resolve() != program_path.resolve():
                    paths.append(path)
        else:
            print(f'Non existent path: {path}')

    modules, mod_implements, inc_no_mod, inc_no_mod_headers = get_vtk_components(jpath, paths)

    res = '\n'.join(disp_components(modules, mod_implements))
    if inc_no_mod:
        res += '\n'.join(disp_missing_components(inc_no_mod, inc_no_mod_headers))

    if ofn:
        path = Path(ofn)
        if path.suffix == '':
            path = Path(ofn).with_suffix('.txt')
        path.write_text(res)
    else:
        print(res)


if __name__ == '__main__':
    import sys

    json_paths, src_paths, ofn = get_program_parameters(sys.argv)
    main(json_paths, src_paths, ofn)