#!/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)