#!/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'**{versions[0]}**
' + ', '.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)