#!/usr/bin/env python3 # # Copyright 2020, Data61, CSIRO (ABN 41 687 119 230) # # SPDX-License-Identifier: GPL-2.0-only # """ Extract information of interest to the seL4 image build process from the `platform_gen.yaml` file. THIS IS NOT A STABLE API. Use as a script, not a module. """ import argparse import sys import yaml from typing import Any, Dict, List, Tuple program_name = sys.argv[0] # You can run the doctests with `python3 -m doctest $THIS_FILE`. def is_valid(data: Dict[str, Any]) -> Tuple[bool, List[str]]: """ Verify that the `data` (which should be obtained from a YAML file using `load_data()` contains a well-formed List of disjunct memory regions ordered by increasing addresses. Returns a tuple of a `bool` and a list of strings. The list is empty if there were no problems, and describes one validation issue per element otherwise. >>> is_valid(None) (False, ['no data in file']) >>> is_valid({'devices': [{'end': 9699328, 'start': 9437184}]}) (False, ['no description of memory in file (no "memory" key)']) >>> is_valid({'memory': 1}) (False, ['bad description of memory in file ("memory" is not a list)']) >>> is_valid({'memory': []}) (False, ['memory described as empty in file (list is zero-length)']) >>> is_valid({'memory': [{'end': 1342177280, 'start': 268435456}]}) (True, []) >>> is_valid({'memory': [{'end': 1342177280}]}) (False, ['region 0 is missing its start bound']) >>> is_valid({'memory': [{'start': 268435456}]}) (False, ['region 0 is missing its end bound']) >>> is_valid({'memory': [{'junk': 'foo'}]}) (False, ['region 0 is missing its start bound', 'region 0 is missing its end bound']) >>> is_valid({'memory': [{'start': 'foo'}]}) (False, ['region start "foo" is not an integer', 'region 0 is missing its end bound']) >>> is_valid({'memory': [{'start': 'foo', 'end': 'bar'}]}) (False, ['region start "foo" is not an integer', 'region end "bar" is not an integer']) >>> is_valid({'memory': [{'start': 2048, 'end': 1024}]}) (False, ['region bounds are not in strictly increasing order (1024 not > 2048)']) >>> is_valid({'memory': [{'end': 4095, 'start': 0}, {'end': 65535, 'start': 32768}, {'end': 1342177280, 'start': 268435456}]}) (True, []) >>> is_valid({'memory': [{'end': 4095, 'start': 0}, {'end': 65535, 'start': 32768}, {'end': 1342177280, 'start': 268435456}, {'end': 16384, 'start': 32768}]}) (False, ['region bounds are not in strictly increasing order (32768 not > 1342177280)', 'region bounds are not in strictly increasing order (16384 not > 1342177280)']) """ problems = [] if data is None: problems.append('no data in file') elif 'memory' not in data: problems.append('no description of memory in file (no "memory" key)') elif not isinstance(data['memory'], list): problems.append('bad description of memory in file' ' ("memory" is not a list)') elif len(data['memory']) == 0: problems.append('memory described as empty in file' ' (list is zero-length)') else: # The initialization of last_seen_bound works with the "increasing # bounds" comparison below to require that all addresses be nonnegative. last_seen_bound = -1 region_counter = 0 for region in data['memory']: for bound in ('start', 'end'): if bound not in region: problems.append('region {n} is missing its {name} bound' .format(n=region_counter, name=bound)) elif not isinstance(region[bound], int): problems.append('region {name} "{value}" is not an integer' .format(name=bound, value=region[bound])) elif not region[bound] > last_seen_bound: problems.append('region bounds are not in strictly' ' increasing order ({this} not > {last})' .format(this=region[bound], last=last_seen_bound)) else: last_seen_bound = region[bound] region_counter += 1 if problems: return (False, problems) return (True, []) def report(data=None, c_symbols: Dict[str, str] = {}, use_c=False) -> str: """ Return a (typically multi-line) string with information about memory regions described in `data`. The string is empty if `is_valid()` rejects the data. The default string contents are human-readable; if `use_c` is `True`, C syntax is emitted instead. The `c_symbols` dict describes the C symbol names to be emitted. """ if not is_valid(data): return '' n = len(data['memory']) if use_c: # Extract C symbol names from the dict for convenience. (array, length, tag) = ( c_symbols['array_symbol'], c_symbols['array_length_symbol'], c_symbols['structure_tag_symbol'] ) # We want to mark generated code with a comment. For best comprehension # (by the reader of the generated code), we want to clearly indicate (1) # what generated the code and (2) where the generated section begins and # ends. We also want the comments to otherwise be as similar as # possible to facilitate any desired post-processing. To avoid # repeating ourselves here (in Python), we generate a _template_ # string containing a C comment with the name of the generating program # embedded. The tag ("BEGIN" or "END") is then expanded when written to # the appropriate place in the generated code. comment_template = '/* generated by {} {{tag}} */'.format(program_name) head = '''{comment_begin} int {length} = {n}; struct {tag} {{ size_t start; size_t end; }} {array}[{n}] = {{ '''.format(comment_begin=comment_template.format(tag='BEGIN'), length=length, tag=tag, array=array, n=n) regions = [] for r in range(n): regions.append('''\t{{ .start = {start}, .end = {end} }},\ '''.format(start=data['memory'][r]['start'], end=data['memory'][r]['end'])) body = '\n'.join(regions) tail = '\n}};\n{}'''.format(comment_template.format(tag='END')) report = '{head}{body}{tail}'.format(head=head, body=body, tail=tail) else: head = 'number of memory regions: {}\n'.format(n) regions = [] for r in range(n): regions.append('''region {r}: \tstart: {start} \tend: {end}'''.format(r=r, start=data['memory'][r]['start'], end=data['memory'][r]['end'])) report = '{head}{body}'.format(head=head, body='\n'.join(regions)) return report def load_data(yaml_filename: str) -> Dict[str, Any]: """ Call `yaml_load()` (from `pyyaml`) on `yaml_filename` and return a Dict containing what was found there. """ with open(yaml_filename, 'r') as f: data = yaml.safe_load(f) return data def _process_operand(yaml_filename: str, c_symbols: Dict[str, str], use_c: bool) -> bool: """ Handle one non-optional command-line argument; called by `main()`. """ data = load_data(yaml_filename) (is_good_data, problems) = is_valid(data) if is_good_data: print(report(data, c_symbols, use_c=use_c)) else: # Set up a prefix for diagnostic messages. Diagnostics should always # identify who is talking (`program_name`) and if operating on a file, # should name the file in which trouble is encountered. Both of these # make grep more effective. prefix = "{pn}: file \"{fn}\":".format(pn=program_name, fn=yaml_filename) if len(problems) == 1: sys.stderr.write("{} {}\n".format(prefix, problems[0])) else: sys.stderr.write("{} has multiple problems:\n".format(prefix)) [sys.stderr.write('{}\t{}\n'.format(prefix, p)) for p in problems] return False return True def main() -> int: """ Executable entry point. """ parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=""" Extract information of interest to the seL4 image build process from one or more files generated by `platform_gen.yaml`. If a YAML file lacks a description of memory, or fails to parse, a diagnostic is emitted and an exit status of 1 returned. Exit status 2 indicates a problem while attempting to parse arguments. Note that when `--emit-c-syntax` is specified, C99 designated initialisers are used in the generated code. This code can be used directly (e.g., inside a function body) or in a header file. An example of usage follows. Note the symbol names used, including those of the structure members. An array of structures is always used, even if there is only one region and therefore array element. The length of the array is explicitly exposed, rather than using values like "NULL, NULL" to mark the end of the list. ``` #include "output_of_this_tool.h" int main(int argc, char *argv[]) { for (int i = 0; i < num_memory_regions; i++) { (void) printf("memory region %d: 0x%08lx - 0x%08lx\\n", i, memory_region[i].start, memory_region[i].end); } } ``` """) parser.add_argument('platform_filename', nargs='+', type=str, help='YAML description of platform parameters') parser.add_argument('--emit-c-syntax', action='store_true', help='emit C syntax instead of human-readable output') parser.add_argument('--array_symbol', type=str, default='memory_region', help='desired C identifier for struct array') parser.add_argument('--array_length_symbol', type=str, default='num_memory_regions', help='desired C identifier for length of struct array') parser.add_argument('--structure_tag_symbol', type=str, default='memory_region', help='desired C identifier for structure tag') args = parser.parse_args() there_was_any_trouble = False c_symbols = { 'array_symbol': args.array_symbol, 'array_length_symbol': args.array_length_symbol, 'structure_tag_symbol': args.structure_tag_symbol, } for yaml_filename in args.platform_filename: if not _process_operand(yaml_filename, c_symbols, use_c=args.emit_c_syntax): there_was_any_trouble = True return 1 if there_was_any_trouble else 0 if __name__ == '__main__': sys.exit(main())