| #!/usr/bin/env python3 |
| |
| |
| # Copyright 2018 The Meson development team |
| |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| ''' |
| 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) |