309 lines
10 KiB
Python
309 lines
10 KiB
Python
import json
|
|
import logging
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Pattern
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _tidy_doc(doc: str) -> str:
|
|
doc = doc.strip()
|
|
doc = re.sub(r':.+?:`(.+?)`', r'\1', doc)
|
|
doc = re.sub(r'``([^`]+)``', r'`\1`', doc)
|
|
doc = doc.replace('\n', ' ')
|
|
doc = doc.replace('. ', '. ')
|
|
return doc
|
|
|
|
|
|
class API(object):
|
|
_cmake: str
|
|
_build: Path
|
|
_uuid: uuid.UUID
|
|
_builtin_commands: Dict[str, str]
|
|
_builtin_variables: Dict[str, str]
|
|
_builtin_variable_template: Dict[Pattern, str]
|
|
_builtin_modules: Dict[str, str]
|
|
_targets: List[str]
|
|
_cached_variables: Dict[str, str]
|
|
_generated_list_parsed: bool
|
|
|
|
def __init__(self, cmake: str, build: Path):
|
|
self._cmake = cmake
|
|
self._build = Path(build)
|
|
self._uuid = uuid.uuid4()
|
|
|
|
self._builtin_commands = {}
|
|
self._builtin_variables = {}
|
|
self._builtin_variable_template = {}
|
|
self._builtin_modules = {}
|
|
self._targets = []
|
|
self._cached_variables = {}
|
|
self._generated_list_parsed = False
|
|
|
|
def query(self) -> bool:
|
|
if not self.cmake_cache.exists():
|
|
return False
|
|
|
|
self.query_json.parent.mkdir(parents=True, exist_ok=True)
|
|
with self.query_json.open('w') as fp:
|
|
fp.write('''\
|
|
{
|
|
"requests": [
|
|
{"kind": "codemodel", "version": 2},
|
|
{"kind": "cache", "version": 2},
|
|
{"kind": "cmakeFiles", "version": 1}
|
|
]
|
|
}''')
|
|
|
|
proc = subprocess.run([self._cmake, str(self._build)],
|
|
universal_newlines=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
self.query_json.unlink()
|
|
self.query_json.parent.rmdir()
|
|
if proc.returncode != 0:
|
|
logging.error(
|
|
f'cmake exited with {proc.returncode}: {proc.stderr}')
|
|
return False
|
|
|
|
return True
|
|
|
|
def read_reply(self) -> bool:
|
|
reply = self._build / '.cmake' / 'api' / 'v1' / 'reply'
|
|
indices = sorted(reply.glob('index-*.json'))
|
|
if not indices:
|
|
logger.error('no reply')
|
|
return False
|
|
with indices[-1].open() as fp:
|
|
index = json.load(fp)
|
|
try:
|
|
responses = index['reply'][f'client-{self._uuid}']['query.json'][
|
|
'responses']
|
|
except KeyError:
|
|
logger.error('no rensponse')
|
|
return False
|
|
for response in responses:
|
|
if response['kind'] == 'codemodel':
|
|
self._read_codemodel(reply / response['jsonFile'])
|
|
elif response['kind'] == 'cache':
|
|
self._read_cache(reply / response['jsonFile'])
|
|
elif response['kind'] == 'cmakeFiles':
|
|
self._read_cmake_files(reply / response['jsonFile'])
|
|
|
|
return True
|
|
|
|
def _read_codemodel(self, codemodelpath: Path):
|
|
with (codemodelpath).open() as fp:
|
|
codemodel = json.load(fp)
|
|
config = codemodel['configurations'][0]
|
|
self._targets[:] = [x['name'] for x in config['targets']]
|
|
|
|
def _read_cache(self, cachepath: Path):
|
|
with cachepath.open() as fp:
|
|
cache = json.load(fp)
|
|
self._cached_variables.clear()
|
|
for entry in cache['entries']:
|
|
name = entry['name']
|
|
value = self._truncate_variable(entry['value'])
|
|
properties = {x['name']: x['value'] for x in entry['properties']}
|
|
helpstring = properties.get('HELPSTRING', '')
|
|
doc = []
|
|
if helpstring:
|
|
doc.append(helpstring)
|
|
if value:
|
|
doc.append(f'`{value}`')
|
|
self._cached_variables[name] = '\n\n'.join(doc)
|
|
|
|
def _read_cmake_files(self, jsonpath: Path):
|
|
'''inspect generated list files'''
|
|
|
|
if not self._builtin_variables or self._generated_list_parsed:
|
|
return
|
|
|
|
with jsonpath.open() as fp:
|
|
cmake_files = json.load(fp)
|
|
|
|
# inspect generated list files
|
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
|
tmplist = Path(tmpdirname) / 'dump.cmake'
|
|
with tmplist.open('w') as fp:
|
|
for listfile in cmake_files['inputs']:
|
|
if not listfile.get('isGenerated', False):
|
|
continue
|
|
path = listfile['path']
|
|
fp.write(f'include({path})\n')
|
|
fp.write('''
|
|
get_cmake_property(variables VARIABLES)
|
|
foreach (variable ${variables})
|
|
message("${variable}=${${variable}}")
|
|
endforeach()
|
|
''')
|
|
p = subprocess.run(
|
|
[self._cmake, '-P', str(tmplist)],
|
|
cwd=cmake_files['paths']['source'],
|
|
universal_newlines=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
if p.returncode != 0:
|
|
return
|
|
|
|
for line in p.stderr.split('\n'):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
k, v = line.split('=', 1)
|
|
if k.startswith('CMAKE_ARG'):
|
|
continue
|
|
v = self._truncate_variable(v)
|
|
if k in self._builtin_variables:
|
|
self._builtin_variables[k] += f'\n\n`{v}`'
|
|
else:
|
|
for pattern, doc in self._builtin_variable_template.items(
|
|
):
|
|
if pattern.fullmatch(k):
|
|
self._builtin_variables[k] = f'{doc}\n\n`{v}`'
|
|
break
|
|
else:
|
|
# ignore variable with no document
|
|
pass
|
|
|
|
self._generated_list_parsed = True
|
|
|
|
@property
|
|
def query_json(self) -> Path:
|
|
return (self._build / '.cmake' / 'api' / 'v1' / 'query' /
|
|
f'client-{self._uuid}' / 'query.json')
|
|
|
|
@property
|
|
def cmake_cache(self) -> Path:
|
|
return self._build / 'CMakeCache.txt'
|
|
|
|
def parse_doc(self) -> None:
|
|
self._parse_commands()
|
|
self._parse_variables()
|
|
self._parse_modules()
|
|
|
|
def _parse_commands(self) -> None:
|
|
p = subprocess.run([self._cmake, '--help-commands'],
|
|
stdout=subprocess.PIPE,
|
|
universal_newlines=True)
|
|
|
|
if p.returncode != 0:
|
|
return
|
|
|
|
matches = re.finditer(
|
|
r'''
|
|
(?P<command>.+)\n
|
|
-+\n+?
|
|
[\s\S]*?
|
|
(?P<signature>(?P=command)\s*\([^)]*\))
|
|
''', p.stdout, re.VERBOSE)
|
|
self._builtin_commands.clear()
|
|
for match in matches:
|
|
command = match.group('command')
|
|
signature = match.group('signature')
|
|
signature = re.sub(r'^ ', r'', signature, flags=re.MULTILINE)
|
|
self._builtin_commands[
|
|
command] = '```cmake\n' + signature + '\n```'
|
|
|
|
def _parse_variables(self) -> None:
|
|
p = subprocess.run([self._cmake, '--help-variables'],
|
|
stdout=subprocess.PIPE,
|
|
universal_newlines=True)
|
|
|
|
if p.returncode != 0:
|
|
return
|
|
|
|
matches = re.finditer(
|
|
r'''
|
|
(?P<variable>.+)\n
|
|
-+\n\n
|
|
(?P<doc>[\s\S]+?)(?:\n\n|$)
|
|
''', p.stdout, re.VERBOSE)
|
|
self._builtin_variables.clear()
|
|
for match in matches:
|
|
variable = match.group('variable')
|
|
doc = _tidy_doc(match.group('doc'))
|
|
if variable == 'CMAKE_MATCH_<n>':
|
|
for i in range(10):
|
|
self._builtin_variables[f'CMAKE_MATCH_{i}'] = doc
|
|
elif '<' in variable:
|
|
variable = re.sub(r'<[^>]+>', r'[^_]+', variable)
|
|
pattern = re.compile(variable)
|
|
self._builtin_variable_template[pattern] = doc
|
|
else:
|
|
self._builtin_variables[variable] = doc
|
|
|
|
def _parse_modules(self) -> None:
|
|
p = subprocess.run([self._cmake, '--help-modules'],
|
|
stdout=subprocess.PIPE,
|
|
universal_newlines=True)
|
|
|
|
if p.returncode != 0:
|
|
return
|
|
|
|
matches = re.finditer(
|
|
r'''
|
|
(?P<module>.+)\n
|
|
-+\n+?
|
|
(?:(?P<header>\w[\w\s]+)\n\^+\n+?)?
|
|
(?P<doc>.(?:.|\n)+?\n\n)
|
|
''', p.stdout + '\n\n', re.VERBOSE)
|
|
self._builtin_modules.clear()
|
|
for match in matches:
|
|
module = match.group('module')
|
|
header = match.group('header')
|
|
doc = _tidy_doc(match.group('doc'))
|
|
if header is not None and header != 'Overview':
|
|
doc = ''
|
|
self._builtin_modules[module] = doc
|
|
|
|
def get_command_doc(self, command: str) -> Optional[str]:
|
|
return self._builtin_commands.get(command)
|
|
|
|
def search_command(self, command: str) -> List[str]:
|
|
command = command.lower()
|
|
return [x for x in self._builtin_commands if x.startswith(command)]
|
|
|
|
def get_variable_doc(self, variable: str) -> Optional[str]:
|
|
doc = self._cached_variables.get(variable)
|
|
if doc:
|
|
return doc
|
|
return self._builtin_variables.get(variable)
|
|
|
|
def search_variable(self, variable: str) -> List[str]:
|
|
cached = frozenset(x for x in self._cached_variables
|
|
if x.startswith(variable))
|
|
builtin = frozenset(x for x in self._builtin_variables
|
|
if x.startswith(variable))
|
|
return list(cached | builtin)
|
|
|
|
def get_module_doc(self, module: str, package: bool) -> Optional[str]:
|
|
if package:
|
|
return self._builtin_modules.get('Find' + module)
|
|
|
|
return self._builtin_modules.get(module)
|
|
|
|
def search_module(self, module: str, package: bool) -> List[str]:
|
|
if package:
|
|
module = 'Find' + module
|
|
return [
|
|
x[4:] for x in self._builtin_modules if x.startswith(module)
|
|
]
|
|
|
|
return [
|
|
x for x in self._builtin_modules
|
|
if x.startswith(module) and not x.startswith('Find')
|
|
]
|
|
|
|
def search_target(self, target: str) -> List[str]:
|
|
return [x for x in self._targets if x.startswith(target)]
|
|
|
|
def _truncate_variable(self, v: str) -> str:
|
|
width = 70
|
|
return v[:width] + (v[width:] and '...')
|