blob: 2c44afa517a268b59758613d01309ac4e915cd80 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2018 The Fuchsia Authors
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT
"""
This tool uses the contents of the abigen-generated syscalls/definitions.json
to update docs/syscalls/.
It is not run automatically as part of the build for now (to allow confirmation
of what it does). So it should be run manually after updating syscalls.abigen
and building zircon, followed by uploading the changes to docs/ as a CL.
Currently, it only updates the rights annotations, but in the future it should
update the signature, arguments, etc. too.
"""
import argparse
import json
import os
import subprocess
import sys
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
STANDARD_BLOCK_HEADER = [
'', '<!-- Updated by update-docs-from-abigen, do not edit. -->', ''
]
def parse_args():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
'--json',
default=os.path.normpath(
os.path.join(SCRIPT_DIR, os.pardir, 'build-x64', 'gen', 'global',
'include', 'zircon', 'syscalls', 'definitions.json')),
help='path to abigen .json output')
parser.add_argument(
'--docroot',
default=os.path.normpath(
os.path.join(SCRIPT_DIR, os.pardir, 'docs', 'syscalls')),
help='root of docs/syscalls/ to be updated')
parser.add_argument(
'--generate-missing',
default=False,
action="store_true",
help='if set, generate stubs for any syscalls that are missing')
return parser.parse_args()
def break_into_sentences(stream):
"""Partition on '.' to break into chunks. '.' can't appear elsewhere
in the input stream."""
sentences = []
cur = []
for tok in stream:
cur.append(tok)
if tok == '.':
sentences.append(cur)
cur = []
assert not cur, cur
return sentences
def match_sentence_form(sentence, arg_names):
"""Matches a known sentence form, returning a format string and a dict for
substitution. The values in dict are converted to markdown format.
Certain TERMINALS are special:
- ARG must appear in arg_names
- RIGHT must be a valid ZX_RIGHT_
- TYPE must be a valid ZX_OBJ_TYPE_
- RSRC must be a valid ZX_RSRC_KIND_
VALUE is a generic unchecked value type, used for masks, options, etc.
"""
sentence_forms = [
['None', '.'],
['ARG', 'must', 'have', 'RIGHT', '.'],
['ARG', 'must', 'have', 'resource', 'kind', 'RSRC', '.'],
['ARG', 'must', 'be', 'of', 'type', 'TYPE', '.'],
[
'ARG', 'must', 'be', 'of', 'type', 'TYPE', 'and', 'have', 'RIGHT1',
'and', 'have', 'RIGHT2', '.'
],
[
'ARG', 'must', 'be', 'of', 'type', 'TYPE', 'and', 'have', 'RIGHT',
'.'
],
[
'ARG', 'must', 'be', 'of', 'type', 'TYPE1', 'or', 'TYPE2', 'and', 'have', 'RIGHT',
'.'
],
[
'If', 'ARG1', 'is', 'VALUE', ',', 'ARG2', 'must', 'have', 'RIGHT',
'.'
],
[
'If', 'ARG1', 'is', 'VALUE', ',', 'ARG2', 'must', 'have',
'resource', 'kind', 'RSRC', '.'
],
[
'If', 'ARG1', 'is', 'VALUE', ',', 'ARG2', 'must', 'be', 'of',
'type', 'TYPE', '.'
],
[
'If', 'ARG1', 'is', 'VALUE', ',', 'ARG2', 'must', 'be', 'of',
'type', 'TYPE', 'and', 'have', 'RIGHT', '.'
],
[
'If', 'ARG1', '&', 'VALUE', ',', 'ARG2', 'must', 'be', 'of', 'type',
'TYPE', 'and', 'have', 'RIGHT', '.'
],
['Every', 'entry', 'of', 'ARG', 'must', 'have', 'RIGHT', '.'],
[
'Every', 'entry', 'of', 'ARG', 'must', 'have', 'a',
'WAITITEMMEMBER', 'field', 'with', 'RIGHT', '.'
],
# TODO(ZX-2399) TODO(scottmg): This is a hack specifically for
# zx_channel_call_args_t. Trying to make a pseudo-generic case (that
# handles the length from wr_num_handles, etc.) for this doesn't seem
# worth the trouble at the moment, since it's only checking that the
# handles have TRANSFER anyway. Revisit if/when there's more instances
# like this.
['All', 'wr_handles', 'of', 'ARG', 'must', 'have', 'RIGHT', '.'],
]
all_rights = set([
'ZX_RIGHT_NONE',
'ZX_RIGHT_DUPLICATE',
'ZX_RIGHT_TRANSFER',
'ZX_RIGHT_READ',
'ZX_RIGHT_WRITE',
'ZX_RIGHT_EXECUTE',
'ZX_RIGHT_MAP',
'ZX_RIGHT_GET_PROPERTY',
'ZX_RIGHT_SET_PROPERTY',
'ZX_RIGHT_ENUMERATE',
'ZX_RIGHT_DESTROY',
'ZX_RIGHT_SET_POLICY',
'ZX_RIGHT_GET_POLICY',
'ZX_RIGHT_SIGNAL',
'ZX_RIGHT_SIGNAL_PEER',
'ZX_RIGHT_WAIT',
'ZX_RIGHT_INSPECT',
'ZX_RIGHT_MANAGE_JOB',
'ZX_RIGHT_MANAGE_PROCESS',
'ZX_RIGHT_MANAGE_THREAD',
'ZX_RIGHT_APPLY_PROFILE',
])
all_types = set([
'ZX_OBJ_TYPE_PROCESS',
'ZX_OBJ_TYPE_THREAD',
'ZX_OBJ_TYPE_VMO',
'ZX_OBJ_TYPE_CHANNEL',
'ZX_OBJ_TYPE_EVENT',
'ZX_OBJ_TYPE_PORT',
'ZX_OBJ_TYPE_INTERRUPT',
'ZX_OBJ_TYPE_PCI_DEVICE',
'ZX_OBJ_TYPE_LOG',
'ZX_OBJ_TYPE_SOCKET',
'ZX_OBJ_TYPE_RESOURCE',
'ZX_OBJ_TYPE_EVENTPAIR',
'ZX_OBJ_TYPE_JOB',
'ZX_OBJ_TYPE_VMAR',
'ZX_OBJ_TYPE_FIFO',
'ZX_OBJ_TYPE_GUEST',
'ZX_OBJ_TYPE_VCPU',
'ZX_OBJ_TYPE_TIMER',
'ZX_OBJ_TYPE_IOMMU',
'ZX_OBJ_TYPE_BTI',
'ZX_OBJ_TYPE_PROFILE',
'ZX_OBJ_TYPE_PMT',
'ZX_OBJ_TYPE_SUSPEND_TOKEN',
'ZX_OBJ_TYPE_PAGER',
])
all_rsrcs = set([
'ZX_RSRC_KIND_MMIO',
'ZX_RSRC_KIND_IRQ',
'ZX_RSRC_KIND_IOPORT',
'ZX_RSRC_KIND_HYPERVISOR',
'ZX_RSRC_KIND_ROOT',
'ZX_RSRC_KIND_VMEX',
'ZX_RSRC_KIND_SMC',
])
# There's only two structs in zircon/types.h, so hardcoding this here is
# a bit stinky, but probably OK.
members_of_zx_wait_item_t = set([
'handle',
'waitfor',
'pending',
])
for form in sentence_forms:
result_fmt = ''
result_values = {}
for f, s in zip(form, sentence):
# Literal match.
if s == f:
if f == '.' or f == ',' or f == '->':
result_fmt += f
elif f == '[':
result_fmt += '\['
else:
result_fmt += ' ' + f
elif f.startswith('ARG'):
if s not in arg_names:
break
else:
result_values[f] = '*' + s + '*'
result_fmt += ' %(' + f + ')s'
elif f.startswith('VALUE'):
# TODO(scottmg): Worth checking these in some way?
result_fmt += ' %(' + f + ')s'
result_values[f] = '**' + s + '**'
elif f.startswith('RIGHT'):
if s not in all_rights:
break
result_fmt += ' %(' + f + ')s'
result_values[f] = '**' + s + '**'
elif f.startswith('RSRC'):
if s not in all_rsrcs:
break
result_fmt += ' %(' + f + ')s'
result_values[f] = '**' + s + '**'
elif f.startswith('TYPE'):
if s not in all_types:
break
result_fmt += ' %(' + f + ')s'
result_values[f] = '**' + s + '**'
elif f.startswith('WAITITEMMEMBER'):
if s not in members_of_zx_wait_item_t:
break
result_fmt += ' %(' + f + ')s'
result_values[f] = '*' + s + '*'
else:
break
else:
if result_fmt[0] == ' ':
result_fmt = result_fmt[1:]
return result_fmt, result_values
else:
return None, None
def to_markdown(req, arguments, warn):
"""Parses a few known forms of rules (see match_sentence_forms).
Converts |req| to formatted markdown.
"""
sentences = break_into_sentences(req)
if not sentences:
rights = ['TODO(ZX-2399)', '']
else:
rights = []
for sentence in sentences:
match_fmt, match_values = match_sentence_form(
sentence, [x['name'] for x in arguments])
if not match_fmt:
warn('failed to parse: ' + repr(sentence))
raise SystemExit(1)
else:
rights.append(match_fmt % match_values)
rights.append('')
return STANDARD_BLOCK_HEADER + rights
def find_block(lines, name):
"""Finds a .md block with the given name, and returns (start, end) line
indices.
"""
start_index = -1
end_index = -1
for i, line in enumerate(lines):
if line == '## ' + name:
start_index = i + 1
elif start_index >= 0 and line.startswith('## '):
end_index = i
break
return start_index, end_index
def update_rights(lines, syscall_data, warn):
"""Updates the RIGHTS block of the .md file in lines.
"""
rights_start_index, rights_end_index = find_block(lines, 'RIGHTS')
if rights_start_index == -1 or rights_end_index == -1:
warn('did not find RIGHTS section, skipping update')
return
lines[rights_start_index:rights_end_index] = to_markdown(
syscall_data['requirements'], syscall_data['arguments'], warn)
def make_name_block(syscall_data):
start = syscall_data['name'] + ' - '
desc = ''
for x in syscall_data['top_description']:
# TODO(scottmg): This is gross, we should change the abigen parser to
# give us a string instead of tokens.
if x in (',', '.', '-', '/', '\'', ')'):
desc += x
else:
if desc and desc[-1] not in ('-', '/', '\'', '('):
desc += ' '
desc += x
if not desc:
desc = 'TODO(ZX-3106)'
return STANDARD_BLOCK_HEADER + [start + desc, '']
def update_name(lines, syscall_data, warn):
"""Updates the NAME block of the .md file in lines.
"""
name_start_index, name_end_index = find_block(lines, 'NAME')
if name_start_index == -1 or name_end_index == -1:
warn('did not find NAME section, skipping update')
return
lines[name_start_index:name_end_index] = make_name_block(syscall_data)
def make_synopsis_block(syscall_data, warn):
headers = set([
'#include <zircon/syscalls.h>',
])
extra_headers = {}
for arg in syscall_data['arguments']:
if arg['type'] == 'zx_port_packet_t':
headers.add('#include <zircon/syscalls/port.h>')
elif (arg['type'] == 'zx_smc_parameters_t' or
arg['type'] == 'zx_smc_result_t'):
headers.add('#include <zircon/syscalls/smc.h>')
header = ['```'] + sorted(list(headers)) + ['']
def format_arg(x):
ret = ''
if 'IN' in x['attributes']:
ret += 'const '
if x['type'] == 'any':
ret += 'void '
else:
ret += x['type'] + ' '
if x['is_array']:
ret += ' * '
ret += ' ' + x['name']
return ret
no_return = ''
if 'noreturn' in syscall_data['attributes']: no_return = '[[noreturn]]'
to_format = (no_return + syscall_data['return_type'] + ' zx_' +
syscall_data['name'] + '(')
args = ','.join(format_arg(x) for x in syscall_data['arguments'])
if not args:
args = 'void'
to_format += args + ');'
CLANG_FORMAT_PATH = os.path.join(SCRIPT_DIR, os.pardir, 'prebuilt',
'downloads', 'clang', 'bin',
'clang-format')
clang_format = subprocess.Popen([
CLANG_FORMAT_PATH,
'-style={BasedOnStyle: Google, BinPackParameters: false}'
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
formatted, _ = clang_format.communicate(to_format)
if clang_format.returncode != 0:
warn('formatting synopsis failed, skipping update')
return None
footer = [
'```',
'',
]
return STANDARD_BLOCK_HEADER + header + [formatted] + footer
def update_synopsis(lines, syscall_data, warn):
"""Updates the SYNOPSIS block of the .md file in lines.
"""
start_index, end_index = find_block(lines, 'SYNOPSIS')
if start_index == -1 or end_index == -1:
warn('did not find SYNOPSIS section, skipping update')
return
syn = make_synopsis_block(syscall_data, warn)
if not syn:
return
lines[start_index:end_index] = syn
def update_title(lines, syscall_data, _):
"""Updates the main title of the .md file given by |filename|.
"""
correct_title = '# zx_' + syscall_data['name']
if lines[0] != correct_title:
lines[0] = correct_title
def generate_stub(md):
"""Makes a mostly-empty file that can then be filled out by later update
functions."""
stub = '''\
# zx_xyz
## NAME
## SYNOPSIS
## DESCRIPTION
TODO(ZX-3106)
## RIGHTS
## RETURN VALUE
TODO(ZX-3106)
## ERRORS
TODO(ZX-3106)
## SEE ALSO
TODO(ZX-3106)
'''
with open(md, 'wb') as f:
f.write(stub)
def main():
args = parse_args()
inf = os.path.relpath(args.json)
outf = os.path.relpath(args.docroot)
print 'using %s as input and updating %s...' % (inf, outf)
data = json.loads(open(inf, 'rb').read())
missing_count = 0
for syscall in data['syscalls']:
name = syscall['name']
md = os.path.join(outf, name + '.md')
if not os.path.exists(md) and args.generate_missing:
generate_stub(md)
if not os.path.exists(md):
print >> sys.stderr, (
'warning: %s not found for updating, skipping update' % md)
missing_count += 1
else:
with open(md, 'rb') as f:
lines = f.read().splitlines()
assert (lines)
def warn(msg):
print >> sys.stderr, 'warning: %s: %s' % (md, msg)
update_title(lines, syscall, warn)
update_name(lines, syscall, warn)
update_synopsis(lines, syscall, warn)
update_rights(lines, syscall, warn)
with open(md, 'wb') as f:
f.write('\n'.join(lines) + '\n')
if missing_count > 0:
print >> sys.stderr, 'warning: %d missing .md files' % missing_count
return missing_count
if __name__ == '__main__':
sys.exit(main())