|
| 1 | +"""A linting utility for targets.json |
| 2 | +
|
| 3 | +This linting utility may be called as follows: |
| 4 | +python <path-to>/lint.py targets TARGET [TARGET ...] |
| 5 | +
|
| 6 | +all targets will be linted |
| 7 | +""" |
| 8 | + |
| 9 | +# mbed SDK |
| 10 | +# Copyright (c) 2017 ARM Limited |
| 11 | +# |
| 12 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 13 | +# you may not use this file except in compliance with the License. |
| 14 | +# You may obtain a copy of the License at |
| 15 | +# |
| 16 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 17 | +# |
| 18 | +# Unless required by applicable law or agreed to in writing, software |
| 19 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 20 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 21 | +# See the License for the specific language governing permissions and |
| 22 | +# limitations under the License. |
| 23 | + |
| 24 | +from os.path import join, abspath, dirname |
| 25 | +if __name__ == "__main__": |
| 26 | + import sys |
| 27 | + ROOT = abspath(join(dirname(__file__), "..", "..")) |
| 28 | + sys.path.insert(0, ROOT) |
| 29 | +from copy import copy |
| 30 | +from yaml import dump_all |
| 31 | +import argparse |
| 32 | + |
| 33 | +from tools.targets import Target, set_targets_json_location, TARGET_MAP |
| 34 | + |
| 35 | +def must_have_keys(keys, dict): |
| 36 | + """Require keys in an MCU/Board |
| 37 | +
|
| 38 | + is a generator for errors |
| 39 | + """ |
| 40 | + for key in keys: |
| 41 | + if key not in dict: |
| 42 | + yield "%s not found, and is required" % key |
| 43 | + |
| 44 | +def may_have_keys(keys, dict): |
| 45 | + """Disable all other keys in an MCU/Board |
| 46 | +
|
| 47 | + is a generator for errors |
| 48 | + """ |
| 49 | + for key in dict.keys(): |
| 50 | + if key not in keys: |
| 51 | + yield "%s found, and is not allowed" % key |
| 52 | + |
| 53 | +def check_extra_labels(dict): |
| 54 | + """Check that extra_labels does not contain any Target names |
| 55 | +
|
| 56 | + is a generator for errors |
| 57 | + """ |
| 58 | + for label in (dict.get("extra_labels", []) + |
| 59 | + dict.get("extra_labels_add", [])): |
| 60 | + if label in Target.get_json_target_data(): |
| 61 | + yield "%s is not allowed in extra_labels" % label |
| 62 | + |
| 63 | +def check_release_version(dict): |
| 64 | + """Verify that release version 5 is combined with support for all toolcahins |
| 65 | +
|
| 66 | + is a generator for errors |
| 67 | + """ |
| 68 | + if ("release_versions" in dict and |
| 69 | + "5" in dict["release_versions"] and |
| 70 | + "supported_toolchains" in dict): |
| 71 | + for toolc in ["GCC_ARM", "ARM", "IAR"]: |
| 72 | + if toolc not in dict["supported_toolchains"]: |
| 73 | + yield ("%s not found in supported_toolchains, and is " |
| 74 | + "required by mbed OS 5" % toolc) |
| 75 | + |
| 76 | +def check_inherits(dict): |
| 77 | + if ("inherits" in dict and len(dict["inherits"]) > 1): |
| 78 | + yield "multiple inheritance is forbidden" |
| 79 | + |
| 80 | +DEVICE_HAS_ALLOWED = ["ANALOGIN", "ANALOGOUT", "CAN", "ETHERNET", "EMAC", |
| 81 | + "FLASH", "I2C", "I2CSLAVE", "I2C_ASYNCH", "INTERRUPTIN", |
| 82 | + "LOWPOWERTIMER", "PORTIN", "PORTINOUT", "PORTOUT", |
| 83 | + "PWMOUT", "RTC", "TRNG","SERIAL", "SERIAL_ASYNCH", |
| 84 | + "SERIAL_FC", "SLEEP", "SPI", "SPI_ASYNCH", "SPISLAVE", |
| 85 | + "STORAGE"] |
| 86 | +def check_device_has(dict): |
| 87 | + for name in dict.get("device_has", []): |
| 88 | + if name not in DEVICE_HAS_ALLOWED: |
| 89 | + yield "%s is not allowed in device_has" % name |
| 90 | + |
| 91 | +MCU_REQUIRED_KEYS = ["release_versions", "supported_toolchains", |
| 92 | + "default_lib", "public", "inherits", "device_has"] |
| 93 | +MCU_ALLOWED_KEYS = ["device_has_add", "device_has_remove", "core", |
| 94 | + "extra_labels", "features", "features_add", |
| 95 | + "features_remove", "bootloader_supported", "device_name", |
| 96 | + "post_binary_hook", "default_toolchain", "config", |
| 97 | + "extra_labels_add", "extra_labels_remove", |
| 98 | + "target_overrides"] + MCU_REQUIRED_KEYS |
| 99 | +def check_mcu(mcu_json, strict=False): |
| 100 | + """Generate a list of problems with an MCU |
| 101 | +
|
| 102 | + :param: mcu_json the MCU's dict to check |
| 103 | + :param: strict enforce required keys |
| 104 | + """ |
| 105 | + errors = list(may_have_keys(MCU_ALLOWED_KEYS, mcu_json)) |
| 106 | + if strict: |
| 107 | + errors.extend(must_have_keys(MCU_REQUIRED_KEYS, mcu_json)) |
| 108 | + errors.extend(check_extra_labels(mcu_json)) |
| 109 | + errors.extend(check_release_version(mcu_json)) |
| 110 | + errors.extend(check_inherits(mcu_json)) |
| 111 | + errors.extend(check_device_has(mcu_json)) |
| 112 | + if 'public' in mcu_json and mcu_json['public']: |
| 113 | + errors.append("public must be false") |
| 114 | + return errors |
| 115 | + |
| 116 | +BOARD_REQUIRED_KEYS = ["inherits"] |
| 117 | +BOARD_ALLOWED_KEYS = ["supported_form_factors", "is_disk_virtual", |
| 118 | + "detect_code", "extra_labels", "extra_labels_add", |
| 119 | + "extra_labels_remove", "public", "config", |
| 120 | + "forced_reset_timeout", "target_overrides"] + BOARD_REQUIRED_KEYS |
| 121 | +def check_board(board_json, strict=False): |
| 122 | + """Generate a list of problems with an board |
| 123 | +
|
| 124 | + :param: board_json the mcus dict to check |
| 125 | + :param: strict enforce required keys |
| 126 | + """ |
| 127 | + errors = list(may_have_keys(BOARD_ALLOWED_KEYS, board_json)) |
| 128 | + if strict: |
| 129 | + errors.extend(must_have_keys(BOARD_REQUIRED_KEYS, board_json)) |
| 130 | + errors.extend(check_extra_labels(board_json)) |
| 131 | + errors.extend(check_inherits(board_json)) |
| 132 | + return errors |
| 133 | + |
| 134 | +def add_if(dict, key, val): |
| 135 | + """Add a value to a dict if it's non-empty""" |
| 136 | + if val: |
| 137 | + dict[key] = val |
| 138 | + |
| 139 | +def _split_boards(resolution_order, tgt): |
| 140 | + """Split the resolution order between boards and mcus""" |
| 141 | + mcus = [] |
| 142 | + boards = [] |
| 143 | + iterable = iter(resolution_order) |
| 144 | + for name in iterable: |
| 145 | + mcu_json = tgt.json_data[name] |
| 146 | + if (len(list(check_mcu(mcu_json, True))) > |
| 147 | + len(list(check_board(mcu_json, True)))): |
| 148 | + boards.append(name) |
| 149 | + else: |
| 150 | + mcus.append(name) |
| 151 | + break |
| 152 | + mcus.extend(iterable) |
| 153 | + mcus.reverse() |
| 154 | + boards.reverse() |
| 155 | + return mcus, boards |
| 156 | + |
| 157 | + |
| 158 | +MCU_FORMAT_STRING = {1: "MCU (%s) ->", |
| 159 | + 2: "Family (%s) -> MCU (%s) ->", |
| 160 | + 3: "Family (%s) -> SubFamily (%s) -> MCU (%s) ->"} |
| 161 | +BOARD_FORMAT_STRING = {1: "Board (%s)", |
| 162 | + 2: "Module (%s) -> Board (%s)"} |
| 163 | +def _generate_hierarchy_string(mcus, boards): |
| 164 | + global_errors = [] |
| 165 | + if len(mcus) < 1: |
| 166 | + global_errors.append("No MCUS found in heirarchy") |
| 167 | + mcus_string = "??? ->" |
| 168 | + elif len(mcus) > 3: |
| 169 | + global_errors.append("No name for targets %s" % ", ".join(mcus[3:])) |
| 170 | + mcus_string = MCU_FORMAT_STRING[3] % tuple(mcus[:3]) |
| 171 | + for name in mcus[3:]: |
| 172 | + mcus_string += " ??? (%s) ->" % name |
| 173 | + else: |
| 174 | + mcus_string = MCU_FORMAT_STRING[len(mcus)] % tuple(mcus) |
| 175 | + |
| 176 | + if len(boards) < 1: |
| 177 | + global_errors.append("no boards found in heirarchy") |
| 178 | + boards_string = "???" |
| 179 | + elif len(boards) > 2: |
| 180 | + global_errors.append("no name for targets %s" % ", ".join(boards[2:])) |
| 181 | + boards_string = BOARD_FORMAT_STRING[2] % tuple(boards[:2]) |
| 182 | + for name in boards[2:]: |
| 183 | + boards_string += " -> ??? (%s)" % name |
| 184 | + else: |
| 185 | + boards_string = BOARD_FORMAT_STRING[len(boards)] % tuple(boards) |
| 186 | + return mcus_string + " " + boards_string, global_errors |
| 187 | + |
| 188 | + |
| 189 | +def check_hierarchy(tgt): |
| 190 | + """Atempts to assign labels to the heirarchy""" |
| 191 | + resolution_order = copy(tgt.resolution_order_names[:-1]) |
| 192 | + mcus, boards = _split_boards(resolution_order, tgt) |
| 193 | + |
| 194 | + target_errors = {} |
| 195 | + hierachy_string, hierachy_errors = _generate_hierarchy_string(mcus, boards) |
| 196 | + to_ret = {"hierarchy": hierachy_string} |
| 197 | + add_if(to_ret, "hierarchy errors", hierachy_errors) |
| 198 | + |
| 199 | + for name in mcus[:-1]: |
| 200 | + add_if(target_errors, name, list(check_mcu(tgt.json_data[name]))) |
| 201 | + if len(mcus) >= 1: |
| 202 | + add_if(target_errors, mcus[-1], |
| 203 | + list(check_mcu(tgt.json_data[mcus[-1]], True))) |
| 204 | + for name in boards: |
| 205 | + add_if(target_errors, name, list(check_board(tgt.json_data[name]))) |
| 206 | + if len(boards) >= 1: |
| 207 | + add_if(target_errors, boards[-1], |
| 208 | + list(check_board(tgt.json_data[boards[-1]], True))) |
| 209 | + add_if(to_ret, "target errors", target_errors) |
| 210 | + return to_ret |
| 211 | + |
| 212 | +PARSER = argparse.ArgumentParser(prog="targets/lint.py") |
| 213 | +SUBPARSERS = PARSER.add_subparsers(title="Commands") |
| 214 | + |
| 215 | +def subcommand(name, *args, **kwargs): |
| 216 | + def __subcommand(command): |
| 217 | + kwargs['description'] = command.__doc__ |
| 218 | + subparser = SUBPARSERS.add_parser(name, **kwargs) |
| 219 | + for arg in args: |
| 220 | + arg = dict(arg) |
| 221 | + opt = arg['name'] |
| 222 | + del arg['name'] |
| 223 | + |
| 224 | + if isinstance(opt, basestring): |
| 225 | + subparser.add_argument(opt, **arg) |
| 226 | + else: |
| 227 | + subparser.add_argument(*opt, **arg) |
| 228 | + |
| 229 | + def _thunk(parsed_args): |
| 230 | + argv = [arg['dest'] if 'dest' in arg else arg['name'] |
| 231 | + for arg in args] |
| 232 | + argv = [(arg if isinstance(arg, basestring) |
| 233 | + else arg[-1]).strip('-').replace('-', '_') |
| 234 | + for arg in argv] |
| 235 | + argv = {arg: vars(parsed_args)[arg] for arg in argv |
| 236 | + if vars(parsed_args)[arg] is not None} |
| 237 | + |
| 238 | + return command(**argv) |
| 239 | + |
| 240 | + subparser.set_defaults(command=_thunk) |
| 241 | + return command |
| 242 | + return __subcommand |
| 243 | + |
| 244 | +@subcommand("targets", |
| 245 | + dict(name="mcus", nargs="+", metavar="MCU", |
| 246 | + choices=TARGET_MAP.keys(), type=str.upper)) |
| 247 | +def targets_cmd(mcus=[]): |
| 248 | + """Find and print errors about specific targets""" |
| 249 | + print dump_all([check_hierarchy(TARGET_MAP[m]) for m in mcus], |
| 250 | + default_flow_style=False) |
| 251 | + |
| 252 | +@subcommand("all-targets") |
| 253 | +def all_targets_cmd(): |
| 254 | + """Print all errors about all parts""" |
| 255 | + print dump_all([check_hierarchy(m) for m in TARGET_MAP.values()], |
| 256 | + default_flow_style=False) |
| 257 | + |
| 258 | +@subcommand("orphans") |
| 259 | +def orphans_cmd(): |
| 260 | + """Find and print all orphan targets""" |
| 261 | + orphans = Target.get_json_target_data().keys() |
| 262 | + for tgt in TARGET_MAP.values(): |
| 263 | + for name in tgt.resolution_order_names: |
| 264 | + if name in orphans: |
| 265 | + orphans.remove(name) |
| 266 | + if orphans: |
| 267 | + print dump_all([orphans], default_flow_style=False) |
| 268 | + return len(orphans) |
| 269 | + |
| 270 | +def main(): |
| 271 | + """entry point""" |
| 272 | + options = PARSER.parse_args() |
| 273 | + return options.command(options) |
| 274 | + |
| 275 | +if __name__ == "__main__": |
| 276 | + sys.exit(main()) |
| 277 | + |
0 commit comments