1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
|
#!/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)
|