From a96d18b34c25088a9612beabf5d7d4f579eefbe7 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Wed, 10 Apr 2024 09:34:32 -0400 Subject: [PATCH] feature: generating miral and miroil symbols using a new script --- src/miral/CMakeLists.txt | 9 +- src/miral/regenerate-miral-symbols-map.py | 235 ------------ src/miral/symbols.map | 31 +- src/miroil/CMakeLists.txt | 9 +- src/miroil/regenerate-miroil-symbols-map.py | 236 ------------ src/miroil/symbols.map | 15 +- tools/symbols_map_generator.py | 376 ++++++++++++++++++++ 7 files changed, 407 insertions(+), 504 deletions(-) delete mode 100755 src/miral/regenerate-miral-symbols-map.py delete mode 100755 src/miroil/regenerate-miroil-symbols-map.py create mode 100755 tools/symbols_map_generator.py diff --git a/src/miral/CMakeLists.txt b/src/miral/CMakeLists.txt index 80fa50b4721..1c9a0f4963c 100644 --- a/src/miral/CMakeLists.txt +++ b/src/miral/CMakeLists.txt @@ -137,8 +137,7 @@ if (CMAKE_COMPILER_IS_GNUCXX AND # clang generates slightly different symbols endif() add_custom_target(regenerate-miral-debian-symbols ${MIR_CHECK_MIRAL_SYMBOLS_DEFAULT} - DEPENDS miral ${PROJECT_SOURCE_DIR}/debian/libmiral${MIRAL_ABI}.symbols - COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/check-and-update-debian-symbols.py ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} miral ${MIRAL_VERSION} ${MIRAL_ABI} + COMMAND python3 ${PROJECT_SOURCE_DIR}/tools/symbols_map_generator.py --library miral --version ${MIRAL_VERSION} --output_symbols WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" VERBATIM) @@ -153,12 +152,6 @@ configure_file( ${miral_include}/miral/version.h ) -if(TARGET doc) - add_custom_target(regenerate-miral-symbols-map - ${CMAKE_CURRENT_SOURCE_DIR}/regenerate-miral-symbols-map.py ${CMAKE_BINARY_DIR}/doc/sphinx/xml/*.xml > ${symbol_map} - DEPENDS doc) -endif() - install(TARGETS miral LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/miral DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/miral.pc DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") diff --git a/src/miral/regenerate-miral-symbols-map.py b/src/miral/regenerate-miral-symbols-map.py deleted file mode 100755 index 3d2ecd9de17..00000000000 --- a/src/miral/regenerate-miral-symbols-map.py +++ /dev/null @@ -1,235 +0,0 @@ -#! /usr/bin/python3 -"""This script processes the XML generated by "make doc" and produces summary information -on symbols that libmiral intends to make public. - -To use: Go to your build folder and run "make regenerate-miral-symbols-map" """ - -from xml.dom import minidom -from sys import argv - -HELPTEXT = __doc__ -DEBUG = False - -def _get_text(node): - substrings = [] - for node in node.childNodes: - if node.nodeType == node.TEXT_NODE: - substrings.append(node.data) - elif node.nodeType == node.ELEMENT_NODE: - substrings.append(_get_text(node)) - return ''.join(substrings) - -def _get_text_for_element(parent, tagname): - substrings = [] - - for node in parent.getElementsByTagName(tagname): - substrings.append(_get_text(node)) - - return ''.join(substrings) - -def _get_file_location(node): - for node in node.childNodes: - if node.nodeType == node.ELEMENT_NODE and node.tagName == 'location': - return node.attributes['file'].value - if DEBUG: - print('no location in:', node) - return None - -def _has_element(node, tagname): - for node in node.childNodes: - if node.nodeType == node.ELEMENT_NODE and node.tagName in tagname: - return True - return False - -def _print_attribs(node, attribs): - for attrib in attribs: - print(' ', attrib, '=', node.attributes[attrib].value) - -def _concat_text_from_tags(parent, tagnames): - substrings = [] - - for tag in tagnames: - substrings.append(_get_text_for_element(parent, tag)) - - return ''.join(substrings) - -def _print_location(node): - print(' ', 'location', '=', _get_file_location(node)) - -def _get_attribs(node): - kind = node.attributes['kind'].value - static = node.attributes['static'].value - prot = node.attributes['prot'].value - return kind, static, prot - -COMPONENT_MAP = {} -SYMBOLS = {'public' : set(), 'private' : set()} - -def _report(publish, symbol): - symbol = symbol.replace('~', '?') - - if publish: - SYMBOLS['public'].add(symbol) - else: - SYMBOLS['private'].add(symbol) - - if not DEBUG: - return - - if publish: - print(' PUBLISH: {}'.format(symbol)) - else: - print('NOPUBLISH: {}'.format(symbol)) - -OLD_STANZAS = '''MIRAL_5.0 { -global:''' - -END_NEW_STANZA = '''local: *; -};''' - -def _print_report(): - print(OLD_STANZAS) - new_symbols = False; - for symbol in sorted(SYMBOLS['public']): - formatted_symbol = ' {};'.format(symbol) - if formatted_symbol not in OLD_STANZAS and 'miral::' in formatted_symbol: - if not new_symbols: - new_symbols = True; - print(' extern "C++" {') - print(formatted_symbol) - - if new_symbols: print(" };") - print(END_NEW_STANZA) - -def _print_debug_info(node, attributes): - if not DEBUG: - return - print() - _print_attribs(node, attributes) - _print_location(node) - -def _parse_member_def(context_name, node, is_class): - kind = node.attributes['kind'].value - - if (kind in ['enum', 'typedef'] - or _has_element(node, ['templateparamlist']) - or kind in ['function'] and node.attributes['inline'].value == 'yes'): - return - - name = _concat_text_from_tags(node, ['name']) - - if name in ['__attribute__']: - if DEBUG: - print(' ignoring doxygen mis-parsing:', _concat_text_from_tags(node, ['argsstring'])) - return - - if name.startswith('operator'): - name = 'operator' - - if not context_name is None: - symbol = context_name + '::' + name - else: - symbol = name - - is_function = kind == 'function' - - if is_function: - _print_debug_info(node, ['kind', 'prot', 'static', 'virt']) - else: - _print_debug_info(node, ['kind', 'prot', 'static']) - - if DEBUG: - print(' is_class:', is_class) - - publish = _should_publish(is_class, is_function, node) - - _report(publish, symbol + '*') - - if is_function and node.attributes['virt'].value == 'virtual': - _report(publish, 'non-virtual?thunk?to?' + symbol + '*') - - -def _should_publish(is_class, is_function, node): - (kind, static, prot) = _get_attribs(node) - - publish = True - - if publish: - publish = kind != 'define' - - if publish and is_class: - publish = is_function or static == 'yes' - - if publish and prot == 'private': - if is_function: - publish = node.attributes['virt'].value == 'virtual' - else: - publish = False - - if publish and _has_element(node, ['argsstring']): - publish = not _get_text_for_element(node, 'argsstring').endswith('=0') - - return publish - - -def _parse_compound_defs(xmldoc): - compounddefs = xmldoc.getElementsByTagName('compounddef') - for node in compounddefs: - kind = node.attributes['kind'].value - - if kind in ['page', 'file', 'example', 'union']: - continue - - if kind in ['group']: - for member in node.getElementsByTagName('memberdef'): - _parse_member_def(None, member, False) - continue - - if kind in ['namespace']: - symbol = _concat_text_from_tags(node, ['compoundname']) - for member in node.getElementsByTagName('memberdef'): - _parse_member_def(symbol, member, False) - continue - - filename = _get_file_location(node) - - if DEBUG: - print(' from file:', filename) - - if ('/examples/' in filename or '/test/' in filename or '[generated]' in filename - or '[STL]' in filename or _has_element(node, ['templateparamlist'])): - continue - - symbol = _concat_text_from_tags(node, ['compoundname']) - - publish = True - - if publish: - if kind in ['class', 'struct']: - prot = node.attributes['prot'].value - publish = prot != 'private' - _print_debug_info(node, ['kind', 'prot']) - _report(publish, 'vtable?for?' + symbol) - _report(publish, 'typeinfo?for?' + symbol) - - if publish: - for member in node.getElementsByTagName('memberdef'): - _parse_member_def(symbol, member, kind in ['class', 'struct']) - -if __name__ == "__main__": - if len(argv) == 1 or '-h' in argv or '--help' in argv: - print(HELPTEXT) - exit() - - for arg in argv[1:]: - try: - if DEBUG: - print('Processing:', arg) - _parse_compound_defs(minidom.parse(arg)) - except Exception as error: - print('Error:', arg, error) - - if DEBUG: - print('Processing complete') - - _print_report() diff --git a/src/miral/symbols.map b/src/miral/symbols.map index f588c74cb24..a96596cc44d 100644 --- a/src/miral/symbols.map +++ b/src/miral/symbols.map @@ -64,9 +64,11 @@ global: miral::MinimalWindowManager::?MinimalWindowManager*; miral::MinimalWindowManager::MinimalWindowManager*; miral::MinimalWindowManager::advise_delete_app*; + miral::MinimalWindowManager::advise_delete_window*; miral::MinimalWindowManager::advise_focus_gained*; miral::MinimalWindowManager::advise_focus_lost*; miral::MinimalWindowManager::advise_new_app*; + miral::MinimalWindowManager::advise_new_window*; miral::MinimalWindowManager::begin_pointer_move*; miral::MinimalWindowManager::begin_pointer_resize*; miral::MinimalWindowManager::begin_touch_move*; @@ -82,8 +84,6 @@ global: miral::MinimalWindowManager::handle_touch_event*; miral::MinimalWindowManager::handle_window_ready*; miral::MinimalWindowManager::place_new_window*; - miral::MinimalWindowManager::advise_new_window*; - miral::MinimalWindowManager::advise_delete_window*; miral::MirRunner::?MirRunner*; miral::MirRunner::MirRunner*; miral::MirRunner::add_start_callback*; @@ -99,6 +99,7 @@ global: miral::MirRunner::x11_display*; miral::Output::?Output*; miral::Output::Output*; + miral::Output::Type*; miral::Output::attribute*; miral::Output::attributes_map*; miral::Output::connected*; @@ -183,10 +184,10 @@ global: miral::WindowInfo::constrain_resize*; miral::WindowInfo::depth_layer*; miral::WindowInfo::exclusive_rect*; - miral::WindowInfo::ignore_exclusion_zones*; miral::WindowInfo::focus_mode*; miral::WindowInfo::has_output_id*; miral::WindowInfo::height_inc*; + miral::WindowInfo::ignore_exclusion_zones*; miral::WindowInfo::is_visible*; miral::WindowInfo::max_aspect*; miral::WindowInfo::max_height*; @@ -205,6 +206,7 @@ global: miral::WindowInfo::restore_rect*; miral::WindowInfo::shell_chrome*; miral::WindowInfo::state*; + miral::WindowInfo::swap*; miral::WindowInfo::type*; miral::WindowInfo::userdata*; miral::WindowInfo::visible_on_lock_screen*; @@ -242,6 +244,7 @@ global: miral::WindowManagerTools::active_window*; miral::WindowManagerTools::add_tree_to_workspace*; miral::WindowManagerTools::ask_client_to_close*; + miral::WindowManagerTools::can_select_window*; miral::WindowManagerTools::count_applications*; miral::WindowManagerTools::create_workspace*; miral::WindowManagerTools::drag_active_window*; @@ -269,8 +272,8 @@ global: miral::WindowManagerTools::swap_tree_order*; miral::WindowManagerTools::window_at*; miral::WindowManagerTools::window_to_select_application*; - miral::WindowManagerTools::can_select_window*; miral::WindowSpecification::?WindowSpecification*; + miral::WindowSpecification::InputReceptionMode*; miral::WindowSpecification::WindowSpecification*; miral::WindowSpecification::application_id*; miral::WindowSpecification::attached_edges*; @@ -280,9 +283,9 @@ global: miral::WindowSpecification::confine_pointer*; miral::WindowSpecification::depth_layer*; miral::WindowSpecification::exclusive_rect*; - miral::WindowSpecification::ignore_exclusion_zones*; miral::WindowSpecification::focus_mode*; miral::WindowSpecification::height_inc*; + miral::WindowSpecification::ignore_exclusion_zones*; miral::WindowSpecification::input_mode*; miral::WindowSpecification::input_shape*; miral::WindowSpecification::max_aspect*; @@ -364,10 +367,13 @@ global: non-virtual?thunk?to?miral::CanonicalWindowManagerPolicy::handle_window_ready*; non-virtual?thunk?to?miral::CanonicalWindowManagerPolicy::place_new_window*; non-virtual?thunk?to?miral::FdHandle::?FdHandle*; + non-virtual?thunk?to?miral::MinimalWindowManager::?MinimalWindowManager*; non-virtual?thunk?to?miral::MinimalWindowManager::advise_delete_app*; + non-virtual?thunk?to?miral::MinimalWindowManager::advise_delete_window*; non-virtual?thunk?to?miral::MinimalWindowManager::advise_focus_gained*; non-virtual?thunk?to?miral::MinimalWindowManager::advise_focus_lost*; non-virtual?thunk?to?miral::MinimalWindowManager::advise_new_app*; + non-virtual?thunk?to?miral::MinimalWindowManager::advise_new_window*; non-virtual?thunk?to?miral::MinimalWindowManager::confirm_inherited_move*; non-virtual?thunk?to?miral::MinimalWindowManager::confirm_placement_on_display*; non-virtual?thunk?to?miral::MinimalWindowManager::handle_keyboard_event*; @@ -379,8 +385,6 @@ global: non-virtual?thunk?to?miral::MinimalWindowManager::handle_touch_event*; non-virtual?thunk?to?miral::MinimalWindowManager::handle_window_ready*; non-virtual?thunk?to?miral::MinimalWindowManager::place_new_window*; - non-virtual?thunk?to?miral::MinimalWindowManager::advise_new_window*; - non-virtual?thunk?to?miral::MinimalWindowManager::advise_delete_window*; non-virtual?thunk?to?miral::WaylandExtensions::Context::?Context*; non-virtual?thunk?to?miral::WindowManagementPolicy::?WindowManagementPolicy*; non-virtual?thunk?to?miral::WindowManagementPolicy::advise_adding_to_workspace*; @@ -403,6 +407,7 @@ global: non-virtual?thunk?to?miral::WindowManagementPolicy::advise_removing_from_workspace*; non-virtual?thunk?to?miral::WindowManagementPolicy::advise_resize*; non-virtual?thunk?to?miral::WindowManagementPolicy::advise_state_change*; + std::swap*; typeinfo?for?miral::AddInitCallback; typeinfo?for?miral::AppendEventFilter; typeinfo?for?miral::ApplicationAuthorizer; @@ -419,26 +424,26 @@ global: typeinfo?for?miral::Keymap; typeinfo?for?miral::MinimalWindowManager; typeinfo?for?miral::MirRunner; - typeinfo?for?miral::Output; typeinfo?for?miral::Output::PhysicalSizeMM; + typeinfo?for?miral::Output; typeinfo?for?miral::PrependEventFilter; typeinfo?for?miral::SessionLockListener; typeinfo?for?miral::SetCommandLineHandler; typeinfo?for?miral::SetTerminator; typeinfo?for?miral::SetWindowManagementPolicy; typeinfo?for?miral::StartupInternalClient; - typeinfo?for?miral::WaylandExtensions; typeinfo?for?miral::WaylandExtensions::Builder; typeinfo?for?miral::WaylandExtensions::Context; typeinfo?for?miral::WaylandExtensions::EnableInfo; + typeinfo?for?miral::WaylandExtensions; typeinfo?for?miral::Window; typeinfo?for?miral::WindowInfo; typeinfo?for?miral::WindowManagementPolicy; typeinfo?for?miral::WindowManagerOption; typeinfo?for?miral::WindowManagerOptions; typeinfo?for?miral::WindowManagerTools; - typeinfo?for?miral::WindowSpecification; typeinfo?for?miral::WindowSpecification::AspectRatio; + typeinfo?for?miral::WindowSpecification; typeinfo?for?miral::X11Support; typeinfo?for?miral::Zone; vtable?for?miral::AddInitCallback; @@ -457,26 +462,26 @@ global: vtable?for?miral::Keymap; vtable?for?miral::MinimalWindowManager; vtable?for?miral::MirRunner; - vtable?for?miral::Output; vtable?for?miral::Output::PhysicalSizeMM; + vtable?for?miral::Output; vtable?for?miral::PrependEventFilter; vtable?for?miral::SessionLockListener; vtable?for?miral::SetCommandLineHandler; vtable?for?miral::SetTerminator; vtable?for?miral::SetWindowManagementPolicy; vtable?for?miral::StartupInternalClient; - vtable?for?miral::WaylandExtensions; vtable?for?miral::WaylandExtensions::Builder; vtable?for?miral::WaylandExtensions::Context; vtable?for?miral::WaylandExtensions::EnableInfo; + vtable?for?miral::WaylandExtensions; vtable?for?miral::Window; vtable?for?miral::WindowInfo; vtable?for?miral::WindowManagementPolicy; vtable?for?miral::WindowManagerOption; vtable?for?miral::WindowManagerOptions; vtable?for?miral::WindowManagerTools; - vtable?for?miral::WindowSpecification; vtable?for?miral::WindowSpecification::AspectRatio; + vtable?for?miral::WindowSpecification; vtable?for?miral::X11Support; vtable?for?miral::Zone; }; diff --git a/src/miroil/CMakeLists.txt b/src/miroil/CMakeLists.txt index de73650218b..b9bd5e329ef 100644 --- a/src/miroil/CMakeLists.txt +++ b/src/miroil/CMakeLists.txt @@ -83,11 +83,10 @@ configure_file( @ONLY ) -if(TARGET doc) - add_custom_target(regenerate-miroil-symbols-map - ${CMAKE_CURRENT_SOURCE_DIR}/regenerate-miroil-symbols-map.py ${CMAKE_BINARY_DIR}/doc/xml/*.xml > ${symbol_map} - DEPENDS doc) -endif() +add_custom_target(regenerate-miroil-debian-symbols ${MIR_CHECK_MIRAL_SYMBOLS_DEFAULT} + COMMAND python3 ${PROJECT_SOURCE_DIR}/tools/symbols_map_generator.py --library miroil --version ${MIROIL_VERSION} --output_symbols + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + VERBATIM) install(TARGETS miroil LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/miroil DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") diff --git a/src/miroil/regenerate-miroil-symbols-map.py b/src/miroil/regenerate-miroil-symbols-map.py deleted file mode 100755 index 340b6012a08..00000000000 --- a/src/miroil/regenerate-miroil-symbols-map.py +++ /dev/null @@ -1,236 +0,0 @@ -#! /usr/bin/python3 -"""This script processes the XML generated by "make doc" and produces summary information -on symbols that libmiroil intends to make public. - -To use: Go to your build folder and run "make regenerate-miroil-symbols-map" """ - -from xml.dom import minidom -from sys import argv - -HELPTEXT = __doc__ -DEBUG = False - -def _get_text(node): - substrings = [] - for node in node.childNodes: - if node.nodeType == node.TEXT_NODE: - substrings.append(node.data) - elif node.nodeType == node.ELEMENT_NODE: - substrings.append(_get_text(node)) - return ''.join(substrings) - -def _get_text_for_element(parent, tagname): - substrings = [] - - for node in parent.getElementsByTagName(tagname): - substrings.append(_get_text(node)) - - return ''.join(substrings) - -def _get_file_location(node): - for node in node.childNodes: - if node.nodeType == node.ELEMENT_NODE and node.tagName == 'location': - return node.attributes['file'].value - if DEBUG: - print('no location in:', node) - return None - -def _has_element(node, tagname): - for node in node.childNodes: - if node.nodeType == node.ELEMENT_NODE and node.tagName in tagname: - return True - return False - -def _print_attribs(node, attribs): - for attrib in attribs: - print(' ', attrib, '=', node.attributes[attrib].value) - -def _concat_text_from_tags(parent, tagnames): - substrings = [] - - for tag in tagnames: - substrings.append(_get_text_for_element(parent, tag)) - - return ''.join(substrings) - -def _print_location(node): - print(' ', 'location', '=', _get_file_location(node)) - -def _get_attribs(node): - kind = node.attributes['kind'].value - static = node.attributes['static'].value - prot = node.attributes['prot'].value - return kind, static, prot - -COMPONENT_MAP = {} -SYMBOLS = {'public' : set(), 'private' : set()} - -def _report(publish, symbol): - symbol = symbol.replace('~', '?') - - if publish: - SYMBOLS['public'].add(symbol) - else: - SYMBOLS['private'].add(symbol) - - if not DEBUG: - return - - if publish: - print(' PUBLISH: {}'.format(symbol)) - else: - print('NOPUBLISH: {}'.format(symbol)) - -OLD_STANZAS = '''MIROIL_5.0 { -global:''' - -END_NEW_STANZA = ''' -local: *; -};''' - -def _print_report(): - print(OLD_STANZAS) - new_symbols = False; - for symbol in sorted(SYMBOLS['public']): - formatted_symbol = ' {};'.format(symbol) - if formatted_symbol not in OLD_STANZAS and 'miroil::' in formatted_symbol: - if not new_symbols: - new_symbols = True; - print(' extern "C++" {') - print(formatted_symbol) - - if new_symbols: print(" };") - print(END_NEW_STANZA) - -def _print_debug_info(node, attributes): - if not DEBUG: - return - print() - _print_attribs(node, attributes) - _print_location(node) - -def _parse_member_def(context_name, node, is_class): - kind = node.attributes['kind'].value - - if (kind in ['enum', 'typedef'] - or _has_element(node, ['templateparamlist']) - or kind in ['function'] and node.attributes['inline'].value == 'yes'): - return - - name = _concat_text_from_tags(node, ['name']) - - if name in ['__attribute__']: - if DEBUG: - print(' ignoring doxygen mis-parsing:', _concat_text_from_tags(node, ['argsstring'])) - return - - if name.startswith('operator'): - name = 'operator' - - if not context_name is None: - symbol = context_name + '::' + name - else: - symbol = name - - is_function = kind == 'function' - - if is_function: - _print_debug_info(node, ['kind', 'prot', 'static', 'virt']) - else: - _print_debug_info(node, ['kind', 'prot', 'static']) - - if DEBUG: - print(' is_class:', is_class) - - publish = _should_publish(is_class, is_function, node) - - _report(publish, symbol + '*') - - if is_function and node.attributes['virt'].value == 'virtual': - _report(publish, 'non-virtual?thunk?to?' + symbol + '*') - - -def _should_publish(is_class, is_function, node): - (kind, static, prot) = _get_attribs(node) - - publish = True - - if publish: - publish = kind != 'define' - - if publish and is_class: - publish = is_function or static == 'yes' - - if publish and prot == 'private': - if is_function: - publish = node.attributes['virt'].value == 'virtual' - else: - publish = False - - if publish and _has_element(node, ['argsstring']): - publish = not _get_text_for_element(node, 'argsstring').endswith('=0') - - return publish - - -def _parse_compound_defs(xmldoc): - compounddefs = xmldoc.getElementsByTagName('compounddef') - for node in compounddefs: - kind = node.attributes['kind'].value - - if kind in ['page', 'file', 'example', 'union']: - continue - - if kind in ['group']: - for member in node.getElementsByTagName('memberdef'): - _parse_member_def(None, member, False) - continue - - if kind in ['namespace']: - symbol = _concat_text_from_tags(node, ['compoundname']) - for member in node.getElementsByTagName('memberdef'): - _parse_member_def(symbol, member, False) - continue - - filename = _get_file_location(node) - - if DEBUG: - print(' from file:', filename) - - if ('/examples/' in filename or '/test/' in filename or '[generated]' in filename - or '[STL]' in filename or _has_element(node, ['templateparamlist'])): - continue - - symbol = _concat_text_from_tags(node, ['compoundname']) - - publish = True - - if publish: - if kind in ['class', 'struct']: - prot = node.attributes['prot'].value - publish = prot != 'private' - _print_debug_info(node, ['kind', 'prot']) - _report(publish, 'vtable?for?' + symbol) - _report(publish, 'typeinfo?for?' + symbol) - - if publish: - for member in node.getElementsByTagName('memberdef'): - _parse_member_def(symbol, member, kind in ['class', 'struct']) - -if __name__ == "__main__": - if len(argv) == 1 or '-h' in argv or '--help' in argv: - print(HELPTEXT) - exit() - - for arg in argv[1:]: - try: - if DEBUG: - print('Processing:', arg) - _parse_compound_defs(minidom.parse(arg)) - except Exception as error: - print('Error:', arg, error) - - if DEBUG: - print('Processing complete') - - _print_report() diff --git a/src/miroil/symbols.map b/src/miroil/symbols.map index b23a56922ab..0f4dc38440e 100644 --- a/src/miroil/symbols.map +++ b/src/miroil/symbols.map @@ -15,6 +15,7 @@ global: miroil::DisplayListenerWrapper::DisplayListenerWrapper*; miroil::DisplayListenerWrapper::add_display*; miroil::DisplayListenerWrapper::remove_display*; + miroil::Edid::Descriptor::Type*; miroil::Edid::Descriptor::string_value*; miroil::Edid::parse_data*; miroil::EventBuilder::?EventBuilder*; @@ -105,17 +106,17 @@ global: non-virtual?thunk?to?miroil::SurfaceObserver::?SurfaceObserver*; typeinfo?for?miroil::Compositor; typeinfo?for?miroil::DisplayConfigurationControllerWrapper; - typeinfo?for?miroil::DisplayConfigurationOptions; typeinfo?for?miroil::DisplayConfigurationOptions::DisplayMode; + typeinfo?for?miroil::DisplayConfigurationOptions; typeinfo?for?miroil::DisplayConfigurationPolicy; typeinfo?for?miroil::DisplayConfigurationStorage; typeinfo?for?miroil::DisplayId; typeinfo?for?miroil::DisplayListenerWrapper; - typeinfo?for?miroil::Edid; typeinfo?for?miroil::Edid::Descriptor; typeinfo?for?miroil::Edid::PhysicalSizeMM; - typeinfo?for?miroil::EventBuilder; + typeinfo?for?miroil::Edid; typeinfo?for?miroil::EventBuilder::EventInfo; + typeinfo?for?miroil::EventBuilder; typeinfo?for?miroil::GLBuffer; typeinfo?for?miroil::InputDevice; typeinfo?for?miroil::InputDeviceObserver; @@ -130,17 +131,17 @@ global: typeinfo?for?miroil::SurfaceObserver; vtable?for?miroil::Compositor; vtable?for?miroil::DisplayConfigurationControllerWrapper; - vtable?for?miroil::DisplayConfigurationOptions; vtable?for?miroil::DisplayConfigurationOptions::DisplayMode; + vtable?for?miroil::DisplayConfigurationOptions; vtable?for?miroil::DisplayConfigurationPolicy; vtable?for?miroil::DisplayConfigurationStorage; vtable?for?miroil::DisplayId; vtable?for?miroil::DisplayListenerWrapper; - vtable?for?miroil::Edid; vtable?for?miroil::Edid::Descriptor; vtable?for?miroil::Edid::PhysicalSizeMM; - vtable?for?miroil::EventBuilder; + vtable?for?miroil::Edid; vtable?for?miroil::EventBuilder::EventInfo; + vtable?for?miroil::EventBuilder; vtable?for?miroil::GLBuffer; vtable?for?miroil::InputDevice; vtable?for?miroil::InputDeviceObserver; @@ -154,6 +155,6 @@ global: vtable?for?miroil::Surface; vtable?for?miroil::SurfaceObserver; }; - local: *; }; + diff --git a/tools/symbols_map_generator.py b/tools/symbols_map_generator.py new file mode 100755 index 00000000000..685461d922b --- /dev/null +++ b/tools/symbols_map_generator.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 + +# Copyright © Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or 3 +# as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import argparse +from pathlib import Path +from typing import Literal, TypedDict, get_args, Optional +import clang.cindex + +_logger = logging.getLogger(__name__) + +LibraryName = Literal[ + "miral", "mir_common", "mir_core", "miroil", "mir_platform", + "mir_renderer", "mir_renderers", "mir_server", "mir_test", "mir_wayland"] + + +class LibraryInfo(TypedDict): + headers_dir: list[str] + map_file: str + include_headers: Optional[list[str]] + + +# Directory paths are relative to the root of the Mir project +library_to_data_map: dict[LibraryName, LibraryInfo] = { + "miral": { + "headers_dir": [ + "include/miroil" + ], + "map_file": "src/miral/symbols.map", + "include_headers": [ + "include/common", + "include/core", + "include/miral", + "include/renderer", + "include/server", + "include/wayland" + ] + }, + "miroil": { + "headers_dir": [ + "include/miroil" + ], + "map_file": "src/miroil/symbols.map", + "include_headers": [ + "include/platform", + "include/gl", + "include/renderers/gl" + "include/server", + "include/core", + "include/client", + "include/miral", + "src/include/server", + ] + } +} + + +def get_absolute_path_from_project_path(project_path: str) -> Path: + return Path(__file__).parent.parent / project_path + + +def should_generate_as_class_or_struct(node: clang.cindex.Cursor): + return ((node.kind == clang.cindex.CursorKind.CLASS_DECL + or node.kind == clang.cindex.CursorKind.STRUCT_DECL) + and node.is_definition()) + + +def is_operator(node: clang.cindex.Cursor) -> bool: + if node.spelling.startswith("operator"): + remainder = node.spelling[len("operator"):] + if any(not c.isalnum() for c in remainder): + return True + + return False + + +def create_node_symbol_name(node: clang.cindex.Cursor) -> str: + if is_operator(node): + return "operator" + elif node.kind == clang.cindex.CursorKind.DESTRUCTOR: + return f"?{node.spelling[1:]}" + else: + return node.spelling + + +def traverse_ast(node: clang.cindex.Cursor, filename: str, result: set[str]) -> set[str]: + # Ignore private and protected variables + if (node.access_specifier == clang.cindex.AccessSpecifier.PRIVATE): + return result + + # Check if we need to output a symbol + if ((node.kind == clang.cindex.CursorKind.FUNCTION_DECL + or node.kind == clang.cindex.CursorKind.CXX_METHOD + or node.kind == clang.cindex.CursorKind.VAR_DECL + or node.kind == clang.cindex.CursorKind.CONSTRUCTOR + or node.kind == clang.cindex.CursorKind.DESTRUCTOR + or node.kind == clang.cindex.CursorKind.ENUM_DECL + or node.kind == clang.cindex.CursorKind.CONVERSION_FUNCTION + or should_generate_as_class_or_struct(node)) + and node.location.file.name == filename + and not node.is_pure_virtual_method()): + parent = node.semantic_parent + namespace_str = create_node_symbol_name(node) + + # Walk up the tree to build the namespace + has_encountered_namespace = False + while parent is not None: + if parent.kind == clang.cindex.CursorKind.TRANSLATION_UNIT: + break + + if parent.kind == clang.cindex.CursorKind.NAMESPACE: + if has_encountered_namespace and parent.spelling == "std": + # TODO: I am unsure why, but the 'std' namespace isn't closed + # by the time we encounter other symbols. We can ignore std + # in this case + break + else: + has_encountered_namespace = True + + namespace_str = f"{parent.spelling}::{namespace_str}" + parent = parent.semantic_parent + + # Classes and structs have a specific output + if (node.kind == clang.cindex.CursorKind.CLASS_DECL + or node.kind == clang.cindex.CursorKind.STRUCT_DECL): + result.add(f"vtable?for?{namespace_str};") + result.add(f"typeinfo?for?{namespace_str};") + else: + def add_internal(s: str): + result.add(f"{s}*;") + add_internal(namespace_str) + + # Check if we're marked virtual + is_virtual = False + if ((node.kind == clang.cindex.CursorKind.CXX_METHOD + or node.kind == clang.cindex.CursorKind.DESTRUCTOR + or node.kind == clang.cindex.CursorKind.CONSTRUCTOR + or node.kind == clang.cindex.CursorKind.CONVERSION_FUNCTION) + and node.is_virtual_method()): + add_internal(f"non-virtual?thunk?to?{namespace_str}") + is_virtual = True + else: + # Check if we're marked override + for child in node.get_children(): + if child.kind == clang.cindex.CursorKind.CXX_OVERRIDE_ATTR: + add_internal(f"non-virtual?thunk?to?{namespace_str}") + is_virtual = True + break + + # If this is a virtual/override, then we iterate the virtual base + # classes.. If we find find the provided method in those classes + # we need to generate a a virtual?thunk?to? for the method/constructor/deconstructor. + if is_virtual: + def search_class_hierarchy_for_virtual_thunk(derived: clang.cindex.Cursor): + assert (derived.kind == clang.cindex.CursorKind.CLASS_DECL + or derived.kind == clang.cindex.CursorKind.STRUCT_DECL + or derived.kind == clang.cindex.CursorKind.CLASS_TEMPLATE) + + # Find the base classes for the derived class + base_classes: list[clang.cindex.Cursor] = [] + for child in derived.get_children(): + if child.kind != clang.cindex.CursorKind.CXX_BASE_SPECIFIER: + continue + + class_or_struct_node = clang.cindex.conf.lib.clang_getCursorDefinition(child) + if class_or_struct_node is None: + continue + + base_classes.append(class_or_struct_node) + if not clang.cindex.conf.lib.clang_isVirtualBase(child): + continue + + # Search the immediate base classes for the function name + for other_child in class_or_struct_node.get_children(): + if (other_child.displayname == node.displayname): + add_internal(f"virtual?thunk?to?{namespace_str}") + return True + + # Looks like it wasn't in any of our direct ancestors, so let's + # try their ancestors too + for base in base_classes: + if search_class_hierarchy_for_virtual_thunk(base): + return True + + return False + + search_class_hierarchy_for_virtual_thunk(node.semantic_parent) + + + # Traverse down the tree if we can + if (node.kind == clang.cindex.CursorKind.TRANSLATION_UNIT + or node.kind == clang.cindex.CursorKind.CLASS_DECL + or node.kind == clang.cindex.CursorKind.STRUCT_DECL + or node.kind == clang.cindex.CursorKind.NAMESPACE): + for child in node.get_children(): + traverse_ast(child, filename, result) + + return result + + +def process_directory(directory: str, search_dirs: Optional[list[str]]) -> set[str]: + result = set() + + files_dir = get_absolute_path_from_project_path(directory) + _logger.debug(f"Processing external directory: {files_dir}") + files = files_dir.rglob('*.h') + search_variables = [] + for dir in search_dirs: + search_variables.append(f"-I{get_absolute_path_from_project_path(dir).as_posix()}") + args = ['-fsyntax-only', '-std=c++23', '-x', 'c++-header'] + search_variables + for file in files: + idx = clang.cindex.Index.create() + tu = idx.parse( + file.as_posix(), + args=args, + options=0) + root = tu.cursor + list(traverse_ast(root, file.as_posix(), result)) + + return result + + +def read_symbols_from_file(file: Path, library_name: str) -> list[tuple[str, set[str]]]: + """ + Returns a list of tuples that match the version to the list of symbols in that version. + This list is assumed to be ordered. + """ + # WARNING: This is a very naive way to parse these files + library_name = library_name.upper() + "_" + retval: list[tuple[str, set[str]]] = [] + with open(file.as_posix()) as f: + for line in f.readlines(): + if line.startswith(library_name): + # This denotes that a new version + version_str = line[len(library_name):].split()[0] + retval.append((version_str, set())) + elif line.startswith(" "): + assert len(retval) > 0 + line = line.strip() + assert line.endswith(';') + retval[-1][1].add(line) + return retval + + +def main(): + parser = argparse.ArgumentParser(description="This tool parses the header files of a library in the Mir project " + "and outputs a list of new and removed symbols to stdout. " + "With this list, a developer may update the corresponding symbols.map appropriately. " + "The tool uses https://pypi.org/project/libclang/ to process " + "the AST of the project's header files.") + choices = list(get_args(LibraryName)) + parser.add_argument('--library', type=str, + help=f'library to generate symbols for ({", ".join(choices)})', + required=True, + choices=list(choices)) + parser.add_argument('--version', type=str, + help='Current version of the library', + required=True) + parser.add_argument('--diff', action='store_true', + help='If true a diff should be output to the console') + parser.add_argument('--output_symbols', action='store_true', + help='If true, the symbols.map file will be updated with the new new symbols') + + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG) + library = args.library + version = args.version + + # Remove the patch version since we're not interested in it + split_version = version.split('.') + if len(split_version) == 3: + version = f"{split_version[0]}.{split_version[1]}" + + _logger.info(f"Symbols map generation is beginning for library={library} with version={version}") + + try: + library_data: LibraryInfo = library_to_data_map[library] + except KeyError: + _logger.error(f"The provided library has yet to be implmented: {library}") + exit(1) + + if "include_headers" in library_data: + search_dirs = library_data["include_headers"] + else: + search_dirs = [] + + # Create a set that includes all of the available headers + parsed_symbols: set[str] = set() + for header_dir in library_data["headers_dir"]: + parsed_symbols = parsed_symbols.union(process_directory(header_dir, search_dirs)) + + previous_symbols: list[tuple[str, set[str]]] = read_symbols_from_file( + get_absolute_path_from_project_path(library_data["map_file"]), library) + + # New symbols + added_symbols = parsed_symbols + for symbol_tuple in previous_symbols: + added_symbols = added_symbols - symbol_tuple[1] + added_symbols = list(added_symbols) + added_symbols.sort() + + if args.diff: + print("") + print("\033[1mNew Symbols 🟢🟢🟢\033[0m") + for s in added_symbols: + print(f"\033[92m {s}\033[0m") + + # Deleted symbols + deleted_symbols = set() + for symbol_tuple in previous_symbols: + deleted_symbols = deleted_symbols.union(symbol_tuple[1]) + deleted_symbols = deleted_symbols - parsed_symbols + deleted_symbols = list(deleted_symbols) + deleted_symbols.sort() + + print("") + print("\033[1mDeleted Symbols 🔻🔻🔻\033[0m") + for s in deleted_symbols: + print(f"\033[91m {s}\033[0m") + + if args.output_symbols: + _logger.info(f"Outputting the symbols file to: {get_absolute_path_from_project_path(library_data['map_file'])}") + # Add the new symbols to the latest stanza. If the latest version in the map file is + # NOT the latest symbols version, then we create a new one + if previous_symbols[-1][0] != version: + previous_symbols.append((version, set())) + _logger.info(f"Generating new stanza for version: {version}") + + previous_symbols[-1] = (previous_symbols[-1][0], previous_symbols[-1][1].union(added_symbols)) + with open(get_absolute_path_from_project_path(library_data["map_file"]), 'w+') as f: + for i in range(0, len(previous_symbols)): + stanza = previous_symbols[i] + version_str = f"{library.upper()}_{stanza[0]}" + symbols = list(stanza[1]) + symbols.sort() + + if i == 0: + closing_line = "};" + else: + prev_version_str = f"{library.upper()}_{previous_symbols[i - 1][0]}" + closing_line = "} " + prev_version_str + ";" + + joint_str = "\n ".join(symbols) + output_str = f'''{version_str} {"{"} +global: + extern "C++" {"{"} + {joint_str} + {'};'} +local: *; +{closing_line} + +''' + + f.write(output_str) + + + exit(0) + + + +if __name__ == "__main__": + main()