| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: Apache-2.0 | |
| # Copyright 2018 The Meson development team | |
| ''' | |
| Regenerate markdown docs by using `meson.py` from the root dir | |
| ''' | |
| import argparse | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| import textwrap | |
| import json | |
| import typing as T | |
| from pathlib import Path | |
| from urllib.request import urlopen | |
| PathLike = T.Union[Path,str] | |
| def _get_meson_output(root_dir: Path, args: T.List) -> str: | |
| env = os.environ.copy() | |
| env['COLUMNS'] = '80' | |
| return subprocess.run([str(sys.executable), str(root_dir/'meson.py')] + args, check=True, capture_output=True, text=True, env=env).stdout.strip() | |
| def get_commands(help_output: str) -> T.Set[str]: | |
| # Python's argument parser might put the command list to its own line. Or it might not. | |
| assert(help_output.startswith('usage: ')) | |
| lines = help_output.split('\n') | |
| line1 = lines[0] | |
| line2 = lines[1] | |
| if '{' in line1: | |
| cmndline = line1 | |
| else: | |
| assert('{' in line2) | |
| cmndline = line2 | |
| cmndstr = cmndline.split('{')[1] | |
| assert('}' in cmndstr) | |
| help_commands = set(cmndstr.split('}')[0].split(',')) | |
| assert(len(help_commands) > 0) | |
| return {c.strip() for c in help_commands} | |
| def get_commands_data(root_dir: Path) -> T.Dict[str, T.Any]: | |
| usage_start_pattern = re.compile(r'^usage: ', re.MULTILINE) | |
| positional_start_pattern = re.compile(r'^positional arguments:[\t ]*[\r\n]+', re.MULTILINE) | |
| options_start_pattern = re.compile(r'^(optional arguments|options):[\t ]*[\r\n]+', re.MULTILINE) | |
| commands_start_pattern = re.compile(r'^[A-Za-z ]*[Cc]ommands:[\t ]*[\r\n]+', re.MULTILINE) | |
| def get_next_start(iterators: T.Sequence[T.Any], end: T.Optional[int]) -> int: | |
| return next((i.start() for i in iterators if i), end) | |
| def normalize_text(text: str) -> str: | |
| # clean up formatting | |
| out = text | |
| out = re.sub(r'\r\n', r'\r', out, flags=re.MULTILINE) # replace newlines with a linux EOL | |
| out = re.sub(r'^ +$', '', out, flags=re.MULTILINE) # remove trailing whitespace | |
| out = re.sub(r'(?:^\n+|\n+$)', '', out) # remove trailing empty lines | |
| return out | |
| def parse_cmd(cmd: str) -> T.Dict[str, str]: | |
| cmd_len = len(cmd) | |
| usage = usage_start_pattern.search(cmd) | |
| positionals = positional_start_pattern.search(cmd) | |
| options = options_start_pattern.search(cmd) | |
| commands = commands_start_pattern.search(cmd) | |
| arguments_start = get_next_start([positionals, options, commands], None) | |
| assert arguments_start | |
| # replace `usage:` with `$` and dedent | |
| dedent_size = (usage.end() - usage.start()) - len('$ ') | |
| usage_text = textwrap.dedent(f'{dedent_size * " "}$ {normalize_text(cmd[usage.end():arguments_start])}') | |
| return { | |
| 'usage': usage_text, | |
| 'arguments': normalize_text(cmd[arguments_start:cmd_len]), | |
| } | |
| def clean_dir_arguments(text: str) -> str: | |
| # Remove platform specific defaults | |
| args = [ | |
| 'prefix', | |
| 'bindir', | |
| 'datadir', | |
| 'includedir', | |
| 'infodir', | |
| 'libdir', | |
| 'libexecdir', | |
| 'localedir', | |
| 'localstatedir', | |
| 'mandir', | |
| 'sbindir', | |
| 'sharedstatedir', | |
| 'sysconfdir' | |
| ] | |
| out = text | |
| for a in args: | |
| out = re.sub(r'(--' + a + r' .+?)\s+\(default:.+?\)(\.)?', r'\1\2', out, flags=re.MULTILINE|re.DOTALL) | |
| return out | |
| output = _get_meson_output(root_dir, ['--help']) | |
| commands = get_commands(output) | |
| commands.remove('help') | |
| cmd_data = dict() | |
| for cmd in commands: | |
| cmd_output = _get_meson_output(root_dir, [cmd, '--help']) | |
| cmd_data[cmd] = parse_cmd(cmd_output) | |
| if cmd in ['setup', 'configure']: | |
| cmd_data[cmd]['arguments'] = clean_dir_arguments(cmd_data[cmd]['arguments']) | |
| return cmd_data | |
| def generate_hotdoc_includes(root_dir: Path, output_dir: Path) -> None: | |
| cmd_data = get_commands_data(root_dir) | |
| for cmd, parsed in cmd_data.items(): | |
| for typ in parsed.keys(): | |
| with open(output_dir / (cmd+'_'+typ+'.inc'), 'w', encoding='utf-8') as f: | |
| f.write(parsed[typ]) | |
| def generate_wrapdb_table(output_dir: Path) -> None: | |
| url = urlopen('https://wrapdb.mesonbuild.com/v2/releases.json') | |
| releases = json.loads(url.read().decode()) | |
| with open(output_dir / 'wrapdb-table.md', 'w', encoding='utf-8') as f: | |
| f.write('| Project | Versions | Provided dependencies | Provided programs |\n') | |
| f.write('| ------- | -------- | --------------------- | ----------------- |\n') | |
| for name, info in releases.items(): | |
| versions = [] | |
| added_tags = set() | |
| for v in info['versions']: | |
| tag, build = v.rsplit('-', 1) | |
| if tag not in added_tags: | |
| added_tags.add(tag) | |
| versions.append(f'[{v}](https://wrapdb.mesonbuild.com/v2/{name}_{v}/{name}.wrap)') | |
| # Highlight latest version. | |
| versions_str = f'<big>**{versions[0]}**</big><br/>' + ', '.join(versions[1:]) | |
| dependency_names = info.get('dependency_names', []) | |
| dependency_names_str = ', '.join(dependency_names) | |
| program_names = info.get('program_names', []) | |
| program_names_str = ', '.join(program_names) | |
| f.write(f'| {name} | {versions_str} | {dependency_names_str} | {program_names_str} |\n') | |
| def regenerate_docs(output_dir: PathLike, | |
| dummy_output_file: T.Optional[PathLike]) -> None: | |
| if not output_dir: | |
| raise ValueError(f'Output directory value is not set') | |
| output_dir = Path(output_dir).resolve() | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| root_dir = Path(__file__).resolve().parent.parent | |
| generate_hotdoc_includes(root_dir, output_dir) | |
| generate_wrapdb_table(output_dir) | |
| if dummy_output_file: | |
| with open(output_dir/dummy_output_file, 'w', encoding='utf-8') as f: | |
| f.write('dummy file for custom_target output') | |
| if __name__ == '__main__': | |
| parser = argparse.ArgumentParser(description='Generate meson docs') | |
| parser.add_argument('--output-dir', required=True) | |
| parser.add_argument('--dummy-output-file', type=str) | |
| args = parser.parse_args() | |
| regenerate_docs(output_dir=args.output_dir, | |
| dummy_output_file=args.dummy_output_file) |