api_info.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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 argparse
  15. import json
  16. import logging
  17. import os
  18. import subprocess
  19. from urllib.parse import unquote
  20. from bs4 import BeautifulSoup
  21. import icore_module
  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
  61. for f in file_list
  62. if f.endswith('.podspec')
  63. or f.endswith('.swift')
  64. or (f.endswith('.h') and 'Public' in f)
  65. ]
  66. def build_api_doc(module, output_dir):
  67. """Use Jazzy to build API documentation for a specific module's source
  68. code.
  69. """
  70. if module['language'] == icore_module.SWIFT:
  71. build_api_doc_swift(module, output_dir)
  72. elif module['language'] == icore_module.OBJECTIVE_C:
  73. build_api_doc_objc(module, output_dir)
  74. elif module['language'] == icore_module.MIXED:
  75. build_api_doc_mixed(module, output_dir)
  76. def build_api_doc_swift(module, output_dir):
  77. logging.info('------------')
  78. cmd = (
  79. f'jazzy --module {module["name"]}'
  80. + ' --swift-build-tool xcodebuild'
  81. + ' --build-tool-arguments'
  82. + f' -scheme,{module["scheme"]}'
  83. + ',-destination,generic/platform=iOS,build'
  84. + f' --output {output_dir}'
  85. )
  86. logging.info(cmd)
  87. result = subprocess.Popen(
  88. cmd, universal_newlines=True, shell=True, stdout=subprocess.PIPE
  89. )
  90. logging.info(result.stdout.read())
  91. def build_api_doc_objc(module, output_dir):
  92. logging.info('------------')
  93. cmd = (
  94. 'jazzy --objc'
  95. + f' --framework-root {module["root_dir"]}'
  96. + f' --umbrella-header {module["umbrella_header"]}'
  97. + f' --output {output_dir}'
  98. )
  99. logging.info(cmd)
  100. result = subprocess.Popen(
  101. cmd, universal_newlines=True, shell=True, stdout=subprocess.PIPE
  102. )
  103. logging.info(result.stdout.read())
  104. def build_api_doc_mixed(module, output_dir):
  105. logging.info('------------')
  106. cmd = (
  107. f'sourcekitten doc --module-name {module["name"]}'
  108. + f' > {module["name"]}swiftDoc.json'
  109. )
  110. logging.info(cmd)
  111. result = subprocess.Popen(
  112. cmd, universal_newlines=True, shell=True, stdout=subprocess.PIPE
  113. )
  114. logging.info(result.stdout.read())
  115. cmd = (
  116. f'sourcekitten doc --objc {module["umbrella_header"]}'
  117. + ' -- -x objective-c -isysroot'
  118. + ' $(xcrun --sdk iphoneos --show-sdk-path)'
  119. + f' -I {module["root_dir"][0]} > {module["name"]}objcDoc.json'
  120. )
  121. logging.info(cmd)
  122. result = subprocess.Popen(
  123. cmd, universal_newlines=True, shell=True, stdout=subprocess.PIPE
  124. )
  125. logging.info(result.stdout.read())
  126. cmd = (
  127. f'jazzy --module {module["name"]} --sourcekitten-sourcefile'
  128. + f' {module["name"]}swiftDoc.json,{module["name"]}objcDoc.json'
  129. + f' --output {output_dir}'
  130. )
  131. logging.info(cmd)
  132. result = subprocess.Popen(
  133. cmd, universal_newlines=True, shell=True, stdout=subprocess.PIPE
  134. )
  135. logging.info(result.stdout.read())
  136. def parse_module(api_doc_path):
  137. """Parse "${module}/index.html" and extract necessary information e.g.
  138. {
  139. $(api_type_1): {
  140. "api_type_link": $(api_type_link),
  141. "apis": {
  142. $(api_name_1): {
  143. "api_link": $(api_link_1),
  144. "declaration": [$(swift_declaration), $(objc_declaration)],
  145. "sub_apis": {
  146. $(sub_api_name_1): {"declaration": [$(swift_declaration)]},
  147. $(sub_api_name_2): {"declaration": [$(objc_declaration)]},
  148. ...
  149. }
  150. },
  151. $(api_name_2): {
  152. ...
  153. },
  154. }
  155. },
  156. $(api_type_2): {
  157. ..
  158. },
  159. }
  160. """
  161. module_api_container = {}
  162. # Read the HTML content from the file
  163. index_link = f'{api_doc_path}/index.html'
  164. with open(index_link, 'r') as file:
  165. html_content = file.read()
  166. # Parse the HTML content
  167. soup = BeautifulSoup(html_content, 'html.parser')
  168. # Locate the element with class="nav-groups"
  169. nav_groups_element = soup.find('ul', class_='nav-groups')
  170. # Extract data and convert to JSON format
  171. for nav_group in nav_groups_element.find_all('li', class_='nav-group-name'):
  172. api_type = nav_group.find('a').text
  173. api_type_link = nav_group.find('a')['href']
  174. apis = {}
  175. for nav_group_task in nav_group.find_all('li', class_='nav-group-task'):
  176. api_name = nav_group_task.find('a').text
  177. api_link = nav_group_task.find('a')['href']
  178. apis[api_name] = {'api_link': api_link, 'declaration': [], 'sub_apis': {}}
  179. module_api_container[api_type] = {
  180. 'api_type_link': api_type_link,
  181. 'apis': apis,
  182. }
  183. parse_api(api_doc_path, module_api_container)
  184. return module_api_container
  185. def parse_api(doc_path, module_api_container):
  186. """Parse API html and extract necessary information.
  187. e.g. ${module}/Classes.html
  188. """
  189. for api_type, api_type_abstract in module_api_container.items():
  190. api_type_link = f'{doc_path}/{unquote(api_type_abstract["api_type_link"])}'
  191. api_data_container = module_api_container[api_type]['apis']
  192. with open(api_type_link, 'r') as file:
  193. html_content = file.read()
  194. # Parse the HTML content
  195. soup = BeautifulSoup(html_content, 'html.parser')
  196. for api in soup.find('div', class_='task-group').find_all(
  197. 'li', class_='item'
  198. ):
  199. api_name = api.find('a', class_='token').text
  200. for api_declaration in api.find_all('div', class_='language'):
  201. api_declaration_text = ' '.join(api_declaration.stripped_strings)
  202. api_data_container[api_name]['declaration'].append(api_declaration_text)
  203. for api, api_abstruct in api_type_abstract['apis'].items():
  204. if api_abstruct['api_link'].endswith('.html'):
  205. parse_sub_api(
  206. f'{doc_path}/{unquote(api_abstruct["api_link"])}',
  207. api_data_container[api]['sub_apis'],
  208. )
  209. def parse_sub_api(api_link, sub_api_data_container):
  210. """Parse SUB_API html and extract necessary information.
  211. e.g. ${module}/Classes/${class_name}.html
  212. """
  213. with open(api_link, 'r') as file:
  214. html_content = file.read()
  215. # Parse the HTML content
  216. soup = BeautifulSoup(html_content, 'html.parser')
  217. for s_api_group in soup.find_all('div', class_='task-group'):
  218. for s_api in s_api_group.find_all('li', class_='item'):
  219. api_name = s_api.find('a', class_='token').text
  220. sub_api_data_container[api_name] = {'declaration': []}
  221. for api_declaration in s_api.find_all('div', class_='language'):
  222. api_declaration_text = ' '.join(api_declaration.stripped_strings)
  223. sub_api_data_container[api_name]['declaration'].append(
  224. api_declaration_text
  225. )
  226. def parse_cmdline_args():
  227. parser = argparse.ArgumentParser()
  228. parser.add_argument('-f', '--file_list', nargs='+', default=[])
  229. parser.add_argument('-o', '--output_dir', default='output_dir')
  230. args = parser.parse_args()
  231. return args
  232. if __name__ == '__main__':
  233. main()