| #!/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 scripts/update-docs-from-abigen, ' |
| 'do not edit this section manually. -->', '' |
| ] |
| |
| |
| 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') |
| 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', |
| '.' |
| ], |
| [ |
| '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 is_special_and_warn(syscall_data, what, warn): |
| name = syscall_data['name'] |
| # TODO(ZX-2399): These files multiplex multiple functions into one doc. I'm |
| # not sure if this is worth it, but don't break them for now. |
| if name in ('channel_read', 'object_get_property', 'object_signal'): |
| warn('not updating ' + what + ' for special file') |
| return True |
| return False |
| |
| |
| 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 |
| return STANDARD_BLOCK_HEADER + [start + desc, ''] |
| |
| |
| def update_name(lines, syscall_data, warn): |
| """Updates the NAME block of the .md file in lines. |
| """ |
| if is_special_and_warn(syscall_data, 'name', warn): |
| return |
| |
| 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): |
| header = [ |
| '```', |
| '#include <zircon/syscalls.h>', |
| '', |
| ] |
| |
| 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 |
| |
| to_format = ( |
| 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 |
| |
| |
| # TODO(ZX-2399): This is a temporary worklist as each is manually audited. Many |
| # argument names between syscalls.abigen and the existing documentation differ, |
| # which makes the hand written DESCRIPTION block wrong if they're changed. This |
| # will be deleted once all can be generated. |
| synopsis_whitelist = ( |
| 'bti_create', |
| 'channel_create', |
| 'channel_write', |
| 'clock_get_monotonic', |
| 'cprng_draw', |
| 'deadline_after', |
| 'event_create', |
| 'eventpair_create', |
| 'fifo_create', |
| 'guest_create', |
| 'handle_close', |
| 'handle_close_many', |
| 'handle_duplicate', |
| 'handle_replace', |
| 'interrupt_ack', |
| 'interrupt_bind_vcpu', |
| 'interrupt_destroy', |
| 'interrupt_trigger', |
| 'interrupt_wait', |
| 'nanosleep', |
| 'object_get_child', |
| 'object_get_cookie', |
| 'object_set_cookie', |
| 'object_set_property', |
| 'object_wait_async', |
| 'object_wait_one', |
| 'port_create', |
| 'process_create', |
| 'process_start', |
| 'socket_create', |
| 'socket_read', |
| 'socket_shutdown', |
| 'socket_write', |
| 'system_get_num_cpus', |
| 'system_get_version', |
| 'task_kill', |
| 'thread_create', |
| 'timer_cancel', |
| 'timer_set', |
| 'vmar_allocate', |
| 'vmo_clone', |
| 'vmo_create', |
| 'vmo_create_physical', |
| 'vmo_get_size', |
| 'vmo_op_range', |
| 'vmo_read', |
| 'vmo_set_cache_policy', |
| 'vmo_set_size', |
| 'vmo_write', |
| ) |
| |
| |
| 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, warn): |
| """Updates the main title of the .md file given by |filename|. |
| """ |
| if is_special_and_warn(syscall_data, 'title', warn): |
| return |
| |
| correct_title = '# zx_' + syscall_data['name'] |
| if lines[0] != correct_title: |
| lines[0] = correct_title |
| |
| |
| 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): |
| 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) |
| if name in synopsis_whitelist: |
| update_synopsis(lines, syscall, warn) |
| else: |
| warn('not in synopsis_whitelist') |
| 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()) |