api_info.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. # Copyright 2023 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the 'License');
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an 'AS IS' BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import json
  15. import argparse
  16. import logging
  17. import os
  18. import subprocess
  19. import icore_module
  20. from urllib.parse import unquote
  21. from bs4 import BeautifulSoup
  22. API_INFO_FILE_NAME = 'api_info.json'
  23. def main():
  24. logging.getLogger().setLevel(logging.INFO)
  25. # Parse command-line arguments
  26. args = parse_cmdline_args()
  27. output_dir = os.path.expanduser(args.output_dir)
  28. if not os.path.exists(output_dir):
  29. os.makedirs(output_dir)
  30. # Detect changed modules based on changed files
  31. changed_api_files = get_api_files(args.file_list)
  32. if not changed_api_files:
  33. logging.info('No Changed API File Detected')
  34. exit(0)
  35. changed_modules = icore_module.detect_changed_modules(changed_api_files)
  36. if not changed_modules:
  37. logging.info('No Changed Module Detected')
  38. exit(0)
  39. # Generate API documentation and parse API declarations
  40. # for each changed module
  41. api_container = {}
  42. for _, module in changed_modules.items():
  43. api_doc_dir = os.path.join(output_dir, 'doc', module['name'])
  44. build_api_doc(module, api_doc_dir)
  45. if os.path.exists(api_doc_dir):
  46. module_api_container = parse_module(api_doc_dir)
  47. api_container[module['name']] = {
  48. 'path': api_doc_dir,
  49. 'api_types': module_api_container
  50. }
  51. else: # api doc fail to build.
  52. api_container[module['name']] = {'path': '', 'api_types': {}}
  53. api_info_path = os.path.join(output_dir, API_INFO_FILE_NAME)
  54. logging.info(f'Writing API data to {api_info_path}')
  55. with open(api_info_path, 'w') as f:
  56. f.write(json.dumps(api_container, indent=2))
  57. def get_api_files(file_list):
  58. """Filter out non api files."""
  59. return [
  60. f for f in file_list
  61. if f.endswith('.swift') or (f.endswith('.h') and 'Public' in f)
  62. ]
  63. def build_api_doc(module, output_dir):
  64. """Use Jazzy to build API documentation for a specific module's source
  65. code."""
  66. if module['language'] == icore_module.SWIFT:
  67. logging.info('------------')
  68. cmd = f'jazzy --module {module["name"]}'\
  69. + ' --swift-build-tool xcodebuild'\
  70. + ' --build-tool-arguments'\
  71. + f' -scheme,{module["scheme"]}'\
  72. + ',-destination,generic/platform=iOS,build'\
  73. + f' --output {output_dir}'
  74. logging.info(cmd)
  75. result = subprocess.Popen(cmd,
  76. universal_newlines=True,
  77. shell=True,
  78. stdout=subprocess.PIPE)
  79. logging.info(result.stdout.read())
  80. elif module['language'] == icore_module.OBJECTIVE_C:
  81. logging.info('------------')
  82. cmd = 'jazzy --objc'\
  83. + f' --framework-root {module["root_dir"]}'\
  84. + f' --umbrella-header {module["umbrella_header"]}'\
  85. + f' --output {output_dir}'
  86. logging.info(cmd)
  87. result = subprocess.Popen(cmd,
  88. universal_newlines=True,
  89. shell=True,
  90. stdout=subprocess.PIPE)
  91. logging.info(result.stdout.read())
  92. def parse_module(api_doc_path):
  93. """Parse "${module}/index.html" and extract necessary information
  94. e.g.
  95. {
  96. $(api_type_1): {
  97. "api_type_link": $(api_type_link),
  98. "apis": {
  99. $(api_name_1): {
  100. "api_link": $(api_link_1),
  101. "declaration": [$(swift_declaration), $(objc_declaration)],
  102. "sub_apis": {
  103. $(sub_api_name_1): {"declaration": [$(swift_declaration)]},
  104. $(sub_api_name_2): {"declaration": [$(objc_declaration)]},
  105. ...
  106. }
  107. },
  108. $(api_name_2): {
  109. ...
  110. },
  111. }
  112. },
  113. $(api_type_2): {
  114. ..
  115. },
  116. }
  117. """
  118. module_api_container = {}
  119. # Read the HTML content from the file
  120. index_link = f'{api_doc_path}/index.html'
  121. with open(index_link, 'r') as file:
  122. html_content = file.read()
  123. # Parse the HTML content
  124. soup = BeautifulSoup(html_content, 'html.parser')
  125. # Locate the element with class="nav-groups"
  126. nav_groups_element = soup.find('ul', class_='nav-groups')
  127. # Extract data and convert to JSON format
  128. for nav_group in nav_groups_element.find_all('li', class_='nav-group-name'):
  129. api_type = nav_group.find('a').text
  130. api_type_link = nav_group.find('a')['href']
  131. apis = {}
  132. for nav_group_task in nav_group.find_all('li', class_='nav-group-task'):
  133. api_name = nav_group_task.find('a').text
  134. api_link = nav_group_task.find('a')['href']
  135. apis[api_name] = {'api_link': api_link, 'declaration': [], 'sub_apis': {}}
  136. module_api_container[api_type] = {
  137. 'api_type_link': api_type_link,
  138. 'apis': apis
  139. }
  140. parse_api(api_doc_path, module_api_container)
  141. return module_api_container
  142. def parse_api(doc_path, module_api_container):
  143. """Parse API html and extract necessary information.
  144. e.g. ${module}/Classes.html
  145. """
  146. for api_type, api_type_abstract in module_api_container.items():
  147. api_type_link = f'{doc_path}/{unquote(api_type_abstract["api_type_link"])}'
  148. api_data_container = module_api_container[api_type]['apis']
  149. with open(api_type_link, 'r') as file:
  150. html_content = file.read()
  151. # Parse the HTML content
  152. soup = BeautifulSoup(html_content, 'html.parser')
  153. for api in soup.find('div', class_='task-group').find_all('li',
  154. class_='item'):
  155. api_name = api.find('a', class_='token').text
  156. for api_declaration in api.find_all('div', class_='language'):
  157. api_declaration_text = ' '.join(api_declaration.stripped_strings)
  158. api_data_container[api_name]['declaration'].append(api_declaration_text)
  159. for api, api_abstruct in api_type_abstract['apis'].items():
  160. if api_abstruct['api_link'].endswith('.html'):
  161. parse_sub_api(f'{doc_path}/{unquote(api_abstruct["api_link"])}',
  162. api_data_container[api]['sub_apis'])
  163. def parse_sub_api(api_link, sub_api_data_container):
  164. """Parse SUB_API html and extract necessary information.
  165. e.g. ${module}/Classes/${class_name}.html
  166. """
  167. with open(api_link, 'r') as file:
  168. html_content = file.read()
  169. # Parse the HTML content
  170. soup = BeautifulSoup(html_content, 'html.parser')
  171. for s_api_group in soup.find_all('div', class_='task-group'):
  172. for s_api in s_api_group.find_all('li', class_='item'):
  173. api_name = s_api.find('a', class_='token').text
  174. sub_api_data_container[api_name] = {'declaration': []}
  175. for api_declaration in s_api.find_all('div', class_='language'):
  176. api_declaration_text = ' '.join(api_declaration.stripped_strings)
  177. sub_api_data_container[api_name]['declaration'].append(
  178. api_declaration_text)
  179. def parse_cmdline_args():
  180. parser = argparse.ArgumentParser()
  181. parser.add_argument('-f', '--file_list', nargs='+', default=[])
  182. parser.add_argument('-o', '--output_dir', default='output_dir')
  183. args = parser.parse_args()
  184. return args
  185. if __name__ == '__main__':
  186. main()