diff --git a/.gitignore b/.gitignore index 1fae2080e..af7e90900 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ spack* # Tooling /.clangd/ /compile_commands.json + +# Generated files +*podio_generated_files.cmake diff --git a/cmake/podioMacros.cmake b/cmake/podioMacros.cmake index 3afb05244..a8a3c815c 100644 --- a/cmake/podioMacros.cmake +++ b/cmake/podioMacros.cmake @@ -134,27 +134,37 @@ function(PODIO_GENERATE_DATAMODEL datamodel YAML_FILE RETURN_HEADERS RETURN_SOUR # At least build the ROOT selection.xml by default for now SET(ARG_IO_BACKEND_HANDLERS "ROOT") ENDIF() + + # Make sure that we re run the generation process everytime either the + # templates or the yaml file changes. + include(${podio_PYTHON_DIR}/templates/CMakeLists.txt) + set_property( + DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + ${YAML_FILE} + ${PODIO_TEMPLATES} + ${podio_PYTHON_DIR}/podio_class_generator.py + ${podio_PYTHON_DIR}/generator_utils.py + ${podio_PYTHON_DIR}/podio_config_reader.py + ) + + message(STATUS "Creating '${datamodel}' datamodel") # we need to boostrap the data model, so this has to be executed in the cmake run execute_process( - COMMAND ${CMAKE_COMMAND} -E echo "Creating \"${datamodel}\" data model" COMMAND python ${podio_PYTHON_DIR}/podio_class_generator.py ${YAML_FILE} ${ARG_OUTPUT_FOLDER} ${datamodel} ${ARG_IO_BACKEND_HANDLERS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE podio_generate_command_retval ) - file(GLOB headers ${ARG_OUTPUT_FOLDER}/${datamodel}/*.h) - file(GLOB sources ${ARG_OUTPUT_FOLDER}/src/*.cc) + IF(NOT ${podio_generate_command_retval} EQUAL 0) + message(FATAL_ERROR "Could not generate datamodel '${datamodel}'. Check your definition in '${YAML_FILE}'") + ENDIF() + + # Get the generated headers and source files + include(${ARG_OUTPUT_FOLDER}/podio_generated_files.cmake) set (${RETURN_HEADERS} ${headers} PARENT_SCOPE) set (${RETURN_SOURCES} ${sources} PARENT_SCOPE) - add_custom_target(create_${datamodel} - COMMENT "Re-Creating \"${datamodel}\" data model" - DEPENDS ${YAML_FILE} - BYPRODUCTS ${sources} ${headers} - COMMAND python ${podio_PYTHON_DIR}/podio_class_generator.py --quiet ${YAML_FILE} ${ARG_OUTPUT_FOLDER} ${datamodel} ${ARG_IO_BACKEND_HANDLERS} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - endfunction() diff --git a/python/podio_class_generator.py b/python/podio_class_generator.py index 328904a99..979c028b6 100755 --- a/python/podio_class_generator.py +++ b/python/podio_class_generator.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals, absolute_import, print_function import os -import errno import sys import subprocess from io import open @@ -56,6 +55,24 @@ def get_clang_format(): return [] +def write_file_if_changed(filename, content, force_write=False): + """Write the file contents only if it has changed or if the file does not exist + yet. Return whether the file has been written or not""" + try: + with open(filename, 'r') as f: + existing_content = f.read() + changed = existing_content != content + except FileNotFoundError: + changed = True + + if changed or force_write: + with open(filename, 'w') as f: + f.write(content) + return True + + return False + + class ClassGenerator(object): def __init__(self, yamlfile, install_dir, package_name, io_handlers, verbose, dryrun): self.install_dir = install_dir @@ -84,6 +101,8 @@ def __init__(self, yamlfile, install_dir, package_name, io_handlers, verbose, dr self.expose_pod_members = self.reader.options["exposePODMembers"] self.clang_format = [] + self.generated_files = [] + self.any_changes = False def process(self): for name, component in self.reader.components.items(): @@ -96,6 +115,8 @@ def process(self): self._create_selection_xml() self.print_report() + self._write_cmake_lists_file() + def print_report(self): if not self.verbose: return @@ -126,26 +147,13 @@ def _write_file(self, name, content): else: fullname = os.path.join(self.install_dir, "src", name) if not self.dryrun: + self.generated_files.append(fullname) if self.clang_format: cfproc = subprocess.Popen(self.clang_format, stdin=subprocess.PIPE, stdout=subprocess.PIPE) content = cfproc.communicate(input=content.encode())[0].decode() - try: - with open(fullname, 'r') as f: - existing_content = f.read() - changed = existing_content != content - - except EnvironmentError as e: - # If we deprecate python2 support, FileNotFoundError becomes available - # and this can be using it. For now we keep it compatible with both - # versions - if e.errno != errno.ENOENT: - raise - changed = True - - if changed: - with open(fullname, 'w') as f: - f.write(content) + changed = write_file_if_changed(fullname, content) + self.any_changes = changed or self.any_changes @staticmethod def _get_filenames_templates(template_base, name): @@ -377,6 +385,42 @@ def _get_member_includes(self, members): return self._sort_includes(includes) + def _write_cmake_lists_file(self): + """Write the names of all generated header and src files into cmake lists""" + header_files = (f for f in self.generated_files if f.endswith('.h')) + src_files = (f for f in self.generated_files if f.endswith('.cc')) + xml_files = (f for f in self.generated_files if f.endswith('.xml')) + + def _write_list(name, target_folder, files, comment): + """Write all files into a cmake variable using the target_folder as path to the + file""" + list_cont = [] + + list_cont.append(f'# {comment}') + list_cont.append(f'SET({name}') + for full_file in files: + fname = os.path.basename(full_file) + list_cont.append(f' {os.path.join(target_folder, fname)}') + + list_cont.append(')') + list_cont.append(f'SET_PROPERTY(SOURCE ${{{name}}} PROPERTY GENERATED TRUE)\n') + + return '\n'.join(list_cont) + + full_contents = ['#-- AUTOMATICALLY GENERATED FILE - DO NOT EDIT -- \n'] + full_contents.append(_write_list('headers', r'${ARG_OUTPUT_FOLDER}/${datamodel}', + header_files, 'Generated header files')) + + full_contents.append(_write_list('sources', r'${ARG_OUTPUT_FOLDER}/src', + src_files, 'Generated source files')) + + full_contents.append(_write_list('selection_xml', r'${ARG_OUTPUT_FOLDER}/src', + xml_files, 'Generated xml files')) + + write_file_if_changed(f'{self.install_dir}/podio_generated_files.cmake', + '\n'.join(full_contents), + self.any_changes) + @staticmethod def _is_pod_type(members): """Check if the members of the class define a POD type""" diff --git a/python/templates/CMakeLists.txt b/python/templates/CMakeLists.txt new file mode 100644 index 000000000..94f803a8a --- /dev/null +++ b/python/templates/CMakeLists.txt @@ -0,0 +1,23 @@ +set(PODIO_TEMPLATES + ${CMAKE_CURRENT_LIST_DIR}/Collection.cc.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/Collection.h.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/CollectionData.cc.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/CollectionData.h.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/Component.h.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/ConstObject.cc.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/ConstObject.h.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/Data.h.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/Obj.cc.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/Object.cc.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/Object.h.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/Obj.h.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/selection.xml.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/SIOBlock.cc.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/SIOBlock.h.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/macros/collections.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/macros/declarations.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/macros/implementations.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/macros/iterator.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/macros/sioblocks.jinja2 + ${CMAKE_CURRENT_LIST_DIR}/macros/utils.jinja2 +)