1# 2# Copyright (C) 2012 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16 17""" 18A set of helpers for rendering Mako templates with a Metadata model. 19""" 20 21import metadata_model 22import re 23import markdown 24import textwrap 25import sys 26import bs4 27# Monkey-patch BS4. WBR element must not have an end tag. 28bs4.builder.HTMLTreeBuilder.empty_element_tags.add("wbr") 29 30from collections import OrderedDict 31 32# Relative path from HTML file to the base directory used by <img> tags 33IMAGE_SRC_METADATA="images/camera2/metadata/" 34 35# Prepend this path to each <img src="foo"> in javadocs 36JAVADOC_IMAGE_SRC_METADATA="/reference/" + IMAGE_SRC_METADATA 37NDKDOC_IMAGE_SRC_METADATA="../" + IMAGE_SRC_METADATA 38 39_context_buf = None 40_hal_major_version = None 41_hal_minor_version = None 42 43def _is_sec_or_ins(x): 44 return isinstance(x, metadata_model.Section) or \ 45 isinstance(x, metadata_model.InnerNamespace) 46 47## 48## Metadata Helpers 49## 50 51def find_all_sections(root): 52 """ 53 Find all descendants that are Section or InnerNamespace instances. 54 55 Args: 56 root: a Metadata instance 57 58 Returns: 59 A list of Section/InnerNamespace instances 60 61 Remarks: 62 These are known as "sections" in the generated C code. 63 """ 64 return root.find_all(_is_sec_or_ins) 65 66def find_parent_section(entry): 67 """ 68 Find the closest ancestor that is either a Section or InnerNamespace. 69 70 Args: 71 entry: an Entry or Clone node 72 73 Returns: 74 An instance of Section or InnerNamespace 75 """ 76 return entry.find_parent_first(_is_sec_or_ins) 77 78# find uniquely named entries (w/o recursing through inner namespaces) 79def find_unique_entries(node): 80 """ 81 Find all uniquely named entries, without recursing through inner namespaces. 82 83 Args: 84 node: a Section or InnerNamespace instance 85 86 Yields: 87 A sequence of MergedEntry nodes representing an entry 88 89 Remarks: 90 This collapses multiple entries with the same fully qualified name into 91 one entry (e.g. if there are multiple entries in different kinds). 92 """ 93 if not isinstance(node, metadata_model.Section) and \ 94 not isinstance(node, metadata_model.InnerNamespace): 95 raise TypeError("expected node to be a Section or InnerNamespace") 96 97 d = OrderedDict() 98 # remove the 'kinds' from the path between sec and the closest entries 99 # then search the immediate children of the search path 100 search_path = isinstance(node, metadata_model.Section) and node.kinds \ 101 or [node] 102 for i in search_path: 103 for entry in i.entries: 104 d[entry.name] = entry 105 106 for k,v in d.iteritems(): 107 yield v.merge() 108 109def path_name(node): 110 """ 111 Calculate a period-separated string path from the root to this element, 112 by joining the names of each node and excluding the Metadata/Kind nodes 113 from the path. 114 115 Args: 116 node: a Node instance 117 118 Returns: 119 A string path 120 """ 121 122 isa = lambda x,y: isinstance(x, y) 123 fltr = lambda x: not isa(x, metadata_model.Metadata) and \ 124 not isa(x, metadata_model.Kind) 125 126 path = node.find_parents(fltr) 127 path = list(path) 128 path.reverse() 129 path.append(node) 130 131 return ".".join((i.name for i in path)) 132 133def ndk(name): 134 """ 135 Return the NDK version of given name, which replace 136 the leading "android" to "acamera" 137 138 Args: 139 name: name string of an entry 140 141 Returns: 142 A NDK version name string of the input name 143 """ 144 name_list = name.split(".") 145 if name_list[0] == "android": 146 name_list[0] = "acamera" 147 return ".".join(name_list) 148 149def protobuf_type(entry): 150 """ 151 Return the protocol buffer message type for input metadata entry. 152 Only support types used by static metadata right now 153 154 Returns: 155 A string of protocol buffer type. Ex: "optional int32" or "repeated RangeInt" 156 """ 157 typeName = None 158 if entry.typedef is None: 159 typeName = entry.type 160 else: 161 typeName = entry.typedef.name 162 163 typename_to_protobuftype = { 164 "rational" : "Rational", 165 "size" : "Size", 166 "sizeF" : "SizeF", 167 "rectangle" : "Rect", 168 "streamConfigurationMap" : "StreamConfigurations", 169 "rangeInt" : "RangeInt", 170 "rangeLong" : "RangeLong", 171 "colorSpaceTransform" : "ColorSpaceTransform", 172 "blackLevelPattern" : "BlackLevelPattern", 173 "byte" : "int32", # protocol buffer don't support byte 174 "boolean" : "bool", 175 "float" : "float", 176 "double" : "double", 177 "int32" : "int32", 178 "int64" : "int64", 179 "enumList" : "int32", 180 "string" : "string" 181 } 182 183 if typeName not in typename_to_protobuftype: 184 print >> sys.stderr,\ 185 " ERROR: Could not find protocol buffer type for {%s} type {%s} typedef {%s}" % \ 186 (entry.name, entry.type, entry.typedef) 187 188 proto_type = typename_to_protobuftype[typeName] 189 190 prefix = "optional" 191 if entry.container == 'array': 192 has_variable_size = False 193 for size in entry.container_sizes: 194 try: 195 size_int = int(size) 196 except ValueError: 197 has_variable_size = True 198 199 if has_variable_size: 200 prefix = "repeated" 201 202 return "%s %s" %(prefix, proto_type) 203 204 205def protobuf_name(entry): 206 """ 207 Return the protocol buffer field name for input metadata entry 208 209 Returns: 210 A string. Ex: "android_colorCorrection_availableAberrationModes" 211 """ 212 return entry.name.replace(".", "_") 213 214def has_descendants_with_enums(node): 215 """ 216 Determine whether or not the current node is or has any descendants with an 217 Enum node. 218 219 Args: 220 node: a Node instance 221 222 Returns: 223 True if it finds an Enum node in the subtree, False otherwise 224 """ 225 return bool(node.find_first(lambda x: isinstance(x, metadata_model.Enum))) 226 227def get_children_by_throwing_away_kind(node, member='entries'): 228 """ 229 Get the children of this node by compressing the subtree together by removing 230 the kind and then combining any children nodes with the same name together. 231 232 Args: 233 node: An instance of Section, InnerNamespace, or Kind 234 235 Returns: 236 An iterable over the combined children of the subtree of node, 237 as if the Kinds never existed. 238 239 Remarks: 240 Not recursive. Call this function repeatedly on each child. 241 """ 242 243 if isinstance(node, metadata_model.Section): 244 # Note that this makes jump from Section to Kind, 245 # skipping the Kind entirely in the tree. 246 node_to_combine = node.combine_kinds_into_single_node() 247 else: 248 node_to_combine = node 249 250 combined_kind = node_to_combine.combine_children_by_name() 251 252 return (i for i in getattr(combined_kind, member)) 253 254def get_children_by_filtering_kind(section, kind_name, member='entries'): 255 """ 256 Takes a section and yields the children of the merged kind under this section. 257 258 Args: 259 section: An instance of Section 260 kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls' 261 262 Returns: 263 An iterable over the children of the specified merged kind. 264 """ 265 266 matched_kind = next((i for i in section.merged_kinds if i.name == kind_name), None) 267 268 if matched_kind: 269 return getattr(matched_kind, member) 270 else: 271 return () 272 273## 274## Filters 275## 276 277# abcDef.xyz -> ABC_DEF_XYZ 278def csym(name): 279 """ 280 Convert an entry name string into an uppercase C symbol. 281 282 Returns: 283 A string 284 285 Example: 286 csym('abcDef.xyz') == 'ABC_DEF_XYZ' 287 """ 288 newstr = name 289 newstr = "".join([i.isupper() and ("_" + i) or i for i in newstr]).upper() 290 newstr = newstr.replace(".", "_") 291 return newstr 292 293# abcDef.xyz -> abc_def_xyz 294def csyml(name): 295 """ 296 Convert an entry name string into a lowercase C symbol. 297 298 Returns: 299 A string 300 301 Example: 302 csyml('abcDef.xyz') == 'abc_def_xyz' 303 """ 304 return csym(name).lower() 305 306# pad with spaces to make string len == size. add new line if too big 307def ljust(size, indent=4): 308 """ 309 Creates a function that given a string will pad it with spaces to make 310 the string length == size. Adds a new line if the string was too big. 311 312 Args: 313 size: an integer representing how much spacing should be added 314 indent: an integer representing the initial indendation level 315 316 Returns: 317 A function that takes a string and returns a string. 318 319 Example: 320 ljust(8)("hello") == 'hello ' 321 322 Remarks: 323 Deprecated. Use pad instead since it works for non-first items in a 324 Mako template. 325 """ 326 def inner(what): 327 newstr = what.ljust(size) 328 if len(newstr) > size: 329 return what + "\n" + "".ljust(indent + size) 330 else: 331 return newstr 332 return inner 333 334def _find_new_line(): 335 336 if _context_buf is None: 337 raise ValueError("Context buffer was not set") 338 339 buf = _context_buf 340 x = -1 # since the first read is always '' 341 cur_pos = buf.tell() 342 while buf.tell() > 0 and buf.read(1) != '\n': 343 buf.seek(cur_pos - x) 344 x = x + 1 345 346 buf.seek(cur_pos) 347 348 return int(x) 349 350# Pad the string until the buffer reaches the desired column. 351# If string is too long, insert a new line with 'col' spaces instead 352def pad(col): 353 """ 354 Create a function that given a string will pad it to the specified column col. 355 If the string overflows the column, put the string on a new line and pad it. 356 357 Args: 358 col: an integer specifying the column number 359 360 Returns: 361 A function that given a string will produce a padded string. 362 363 Example: 364 pad(8)("hello") == 'hello ' 365 366 Remarks: 367 This keeps track of the line written by Mako so far, so it will always 368 align to the column number correctly. 369 """ 370 def inner(what): 371 wut = int(col) 372 current_col = _find_new_line() 373 374 if len(what) > wut - current_col: 375 return what + "\n".ljust(col) 376 else: 377 return what.ljust(wut - current_col) 378 return inner 379 380# int32 -> TYPE_INT32, byte -> TYPE_BYTE, etc. note that enum -> TYPE_INT32 381def ctype_enum(what): 382 """ 383 Generate a camera_metadata_type_t symbol from a type string. 384 385 Args: 386 what: a type string 387 388 Returns: 389 A string representing the camera_metadata_type_t 390 391 Example: 392 ctype_enum('int32') == 'TYPE_INT32' 393 ctype_enum('int64') == 'TYPE_INT64' 394 ctype_enum('float') == 'TYPE_FLOAT' 395 396 Remarks: 397 An enum is coerced to a byte since the rest of the camera_metadata 398 code doesn't support enums directly yet. 399 """ 400 return 'TYPE_%s' %(what.upper()) 401 402 403# Calculate a java type name from an entry with a Typedef node 404def _jtypedef_type(entry): 405 typedef = entry.typedef 406 additional = '' 407 408 # Hacky way to deal with arrays. Assume that if we have 409 # size 'Constant x N' the Constant is part of the Typedef size. 410 # So something sized just 'Constant', 'Constant1 x Constant2', etc 411 # is not treated as a real java array. 412 if entry.container == 'array': 413 has_variable_size = False 414 for size in entry.container_sizes: 415 try: 416 size_int = int(size) 417 except ValueError: 418 has_variable_size = True 419 420 if has_variable_size: 421 additional = '[]' 422 423 try: 424 name = typedef.languages['java'] 425 426 return "%s%s" %(name, additional) 427 except KeyError: 428 return None 429 430# Box if primitive. Otherwise leave unboxed. 431def _jtype_box(type_name): 432 mapping = { 433 'boolean': 'Boolean', 434 'byte': 'Byte', 435 'int': 'Integer', 436 'float': 'Float', 437 'double': 'Double', 438 'long': 'Long' 439 } 440 441 return mapping.get(type_name, type_name) 442 443def jtype_unboxed(entry): 444 """ 445 Calculate the Java type from an entry type string, to be used whenever we 446 need the regular type in Java. It's not boxed, so it can't be used as a 447 generic type argument when the entry type happens to resolve to a primitive. 448 449 Remarks: 450 Since Java generics cannot be instantiated with primitives, this version 451 is not applicable in that case. Use jtype_boxed instead for that. 452 453 Returns: 454 The string representing the Java type. 455 """ 456 if not isinstance(entry, metadata_model.Entry): 457 raise ValueError("Expected entry to be an instance of Entry") 458 459 metadata_type = entry.type 460 461 java_type = None 462 463 if entry.typedef: 464 typedef_name = _jtypedef_type(entry) 465 if typedef_name: 466 java_type = typedef_name # already takes into account arrays 467 468 if not java_type: 469 if not java_type and entry.enum and metadata_type == 'byte': 470 # Always map byte enums to Java ints, unless there's a typedef override 471 base_type = 'int' 472 473 else: 474 mapping = { 475 'int32': 'int', 476 'int64': 'long', 477 'float': 'float', 478 'double': 'double', 479 'byte': 'byte', 480 'rational': 'Rational' 481 } 482 483 base_type = mapping[metadata_type] 484 485 # Convert to array (enums, basic types) 486 if entry.container == 'array': 487 additional = '[]' 488 else: 489 additional = '' 490 491 java_type = '%s%s' %(base_type, additional) 492 493 # Now box this sucker. 494 return java_type 495 496def jtype_boxed(entry): 497 """ 498 Calculate the Java type from an entry type string, to be used as a generic 499 type argument in Java. The type is guaranteed to inherit from Object. 500 501 It will only box when absolutely necessary, i.e. int -> Integer[], but 502 int[] -> int[]. 503 504 Remarks: 505 Since Java generics cannot be instantiated with primitives, this version 506 will use boxed types when absolutely required. 507 508 Returns: 509 The string representing the boxed Java type. 510 """ 511 unboxed_type = jtype_unboxed(entry) 512 return _jtype_box(unboxed_type) 513 514def _is_jtype_generic(entry): 515 """ 516 Determine whether or not the Java type represented by the entry type 517 string and/or typedef is a Java generic. 518 519 For example, "Range<Integer>" would be considered a generic, whereas 520 a "MeteringRectangle" or a plain "Integer" would not be considered a generic. 521 522 Args: 523 entry: An instance of an Entry node 524 525 Returns: 526 True if it's a java generic, False otherwise. 527 """ 528 if entry.typedef: 529 local_typedef = _jtypedef_type(entry) 530 if local_typedef: 531 match = re.search(r'<.*>', local_typedef) 532 return bool(match) 533 return False 534 535def _jtype_primitive(what): 536 """ 537 Calculate the Java type from an entry type string. 538 539 Remarks: 540 Makes a special exception for Rational, since it's a primitive in terms of 541 the C-library camera_metadata type system. 542 543 Returns: 544 The string representing the primitive type 545 """ 546 mapping = { 547 'int32': 'int', 548 'int64': 'long', 549 'float': 'float', 550 'double': 'double', 551 'byte': 'byte', 552 'rational': 'Rational' 553 } 554 555 try: 556 return mapping[what] 557 except KeyError as e: 558 raise ValueError("Can't map '%s' to a primitive, not supported" %what) 559 560def jclass(entry): 561 """ 562 Calculate the java Class reference string for an entry. 563 564 Args: 565 entry: an Entry node 566 567 Example: 568 <entry name="some_int" type="int32"/> 569 <entry name="some_int_array" type="int32" container='array'/> 570 571 jclass(some_int) == 'int.class' 572 jclass(some_int_array) == 'int[].class' 573 574 Returns: 575 The ClassName.class string 576 """ 577 578 return "%s.class" %jtype_unboxed(entry) 579 580def jkey_type_token(entry): 581 """ 582 Calculate the java type token compatible with a Key constructor. 583 This will be the Java Class<T> for non-generic classes, and a 584 TypeReference<T> for generic classes. 585 586 Args: 587 entry: An entry node 588 589 Returns: 590 The ClassName.class string, or 'new TypeReference<ClassName>() {{ }}' string 591 """ 592 if _is_jtype_generic(entry): 593 return "new TypeReference<%s>() {{ }}" %(jtype_boxed(entry)) 594 else: 595 return jclass(entry) 596 597def jidentifier(what): 598 """ 599 Convert the input string into a valid Java identifier. 600 601 Args: 602 what: any identifier string 603 604 Returns: 605 String with added underscores if necessary. 606 """ 607 if re.match("\d", what): 608 return "_%s" %what 609 else: 610 return what 611 612def enum_calculate_value_string(enum_value): 613 """ 614 Calculate the value of the enum, even if it does not have one explicitly 615 defined. 616 617 This looks back for the first enum value that has a predefined value and then 618 applies addition until we get the right value, using C-enum semantics. 619 620 Args: 621 enum_value: an EnumValue node with a valid Enum parent 622 623 Example: 624 <enum> 625 <value>X</value> 626 <value id="5">Y</value> 627 <value>Z</value> 628 </enum> 629 630 enum_calculate_value_string(X) == '0' 631 enum_calculate_Value_string(Y) == '5' 632 enum_calculate_value_string(Z) == '6' 633 634 Returns: 635 String that represents the enum value as an integer literal. 636 """ 637 638 enum_value_siblings = list(enum_value.parent.values) 639 this_index = enum_value_siblings.index(enum_value) 640 641 def is_hex_string(instr): 642 return bool(re.match('0x[a-f0-9]+$', instr, re.IGNORECASE)) 643 644 base_value = 0 645 base_offset = 0 646 emit_as_hex = False 647 648 this_id = enum_value_siblings[this_index].id 649 while this_index != 0 and not this_id: 650 this_index -= 1 651 base_offset += 1 652 this_id = enum_value_siblings[this_index].id 653 654 if this_id: 655 base_value = int(this_id, 0) # guess base 656 emit_as_hex = is_hex_string(this_id) 657 658 if emit_as_hex: 659 return "0x%X" %(base_value + base_offset) 660 else: 661 return "%d" %(base_value + base_offset) 662 663def enumerate_with_last(iterable): 664 """ 665 Enumerate a sequence of iterable, while knowing if this element is the last in 666 the sequence or not. 667 668 Args: 669 iterable: an Iterable of some sequence 670 671 Yields: 672 (element, bool) where the bool is True iff the element is last in the seq. 673 """ 674 it = (i for i in iterable) 675 676 first = next(it) # OK: raises exception if it is empty 677 678 second = first # for when we have only 1 element in iterable 679 680 try: 681 while True: 682 second = next(it) 683 # more elements remaining. 684 yield (first, False) 685 first = second 686 except StopIteration: 687 # last element. no more elements left 688 yield (second, True) 689 690def pascal_case(what): 691 """ 692 Convert the first letter of a string to uppercase, to make the identifier 693 conform to PascalCase. 694 695 If there are dots, remove the dots, and capitalize the letter following 696 where the dot was. Letters that weren't following dots are left unchanged, 697 except for the first letter of the string (which is made upper-case). 698 699 Args: 700 what: a string representing some identifier 701 702 Returns: 703 String with first letter capitalized 704 705 Example: 706 pascal_case("helloWorld") == "HelloWorld" 707 pascal_case("foo") == "Foo" 708 pascal_case("hello.world") = "HelloWorld" 709 pascal_case("fooBar.fooBar") = "FooBarFooBar" 710 """ 711 return "".join([s[0:1].upper() + s[1:] for s in what.split('.')]) 712 713def jkey_identifier(what): 714 """ 715 Return a Java identifier from a property name. 716 717 Args: 718 what: a string representing a property name. 719 720 Returns: 721 Java identifier corresponding to the property name. May need to be 722 prepended with the appropriate Java class name by the caller of this 723 function. Note that the outer namespace is stripped from the property 724 name. 725 726 Example: 727 jkey_identifier("android.lens.facing") == "LENS_FACING" 728 """ 729 return csym(what[what.find('.') + 1:]) 730 731def jenum_value(enum_entry, enum_value): 732 """ 733 Calculate the Java name for an integer enum value 734 735 Args: 736 enum: An enum-typed Entry node 737 value: An EnumValue node for the enum 738 739 Returns: 740 String representing the Java symbol 741 """ 742 743 cname = csym(enum_entry.name) 744 return cname[cname.find('_') + 1:] + '_' + enum_value.name 745 746def generate_extra_javadoc_detail(entry): 747 """ 748 Returns a function to add extra details for an entry into a string for inclusion into 749 javadoc. Adds information about units, the list of enum values for this key, and the valid 750 range. 751 """ 752 def inner(text): 753 if entry.units: 754 text += '\n\n<b>Units</b>: %s\n' % (dedent(entry.units)) 755 if entry.enum and not (entry.typedef and entry.typedef.languages.get('java')): 756 text += '\n\n<b>Possible values:</b>\n<ul>\n' 757 for value in entry.enum.values: 758 if not value.hidden: 759 text += ' <li>{@link #%s %s}</li>\n' % ( jenum_value(entry, value ), value.name ) 760 text += '</ul>\n' 761 if entry.range: 762 if entry.enum and not (entry.typedef and entry.typedef.languages.get('java')): 763 text += '\n\n<b>Available values for this device:</b><br>\n' 764 else: 765 text += '\n\n<b>Range of valid values:</b><br>\n' 766 text += '%s\n' % (dedent(entry.range)) 767 if entry.hwlevel != 'legacy': # covers any of (None, 'limited', 'full') 768 text += '\n\n<b>Optional</b> - This value may be {@code null} on some devices.\n' 769 if entry.hwlevel == 'full': 770 text += \ 771 '\n<b>Full capability</b> - \n' + \ 772 'Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the\n' + \ 773 'android.info.supportedHardwareLevel key\n' 774 if entry.hwlevel == 'limited': 775 text += \ 776 '\n<b>Limited capability</b> - \n' + \ 777 'Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the\n' + \ 778 'android.info.supportedHardwareLevel key\n' 779 if entry.hwlevel == 'legacy': 780 text += "\nThis key is available on all devices." 781 782 return text 783 return inner 784 785 786def javadoc(metadata, indent = 4): 787 """ 788 Returns a function to format a markdown syntax text block as a 789 javadoc comment section, given a set of metadata 790 791 Args: 792 metadata: A Metadata instance, representing the top-level root 793 of the metadata for cross-referencing 794 indent: baseline level of indentation for javadoc block 795 Returns: 796 A function that transforms a String text block as follows: 797 - Indent and * for insertion into a Javadoc comment block 798 - Trailing whitespace removed 799 - Entire body rendered via markdown to generate HTML 800 - All tag names converted to appropriate Javadoc {@link} with @see 801 for each tag 802 803 Example: 804 "This is a comment for Javadoc\n" + 805 " with multiple lines, that should be \n" + 806 " formatted better\n" + 807 "\n" + 808 " That covers multiple lines as well\n" 809 " And references android.control.mode\n" 810 811 transforms to 812 " * <p>This is a comment for Javadoc\n" + 813 " * with multiple lines, that should be\n" + 814 " * formatted better</p>\n" + 815 " * <p>That covers multiple lines as well</p>\n" + 816 " * and references {@link CaptureRequest#CONTROL_MODE android.control.mode}\n" + 817 " *\n" + 818 " * @see CaptureRequest#CONTROL_MODE\n" 819 """ 820 def javadoc_formatter(text): 821 comment_prefix = " " * indent + " * " 822 823 # render with markdown => HTML 824 javatext = md(text, JAVADOC_IMAGE_SRC_METADATA) 825 826 # Identity transform for javadoc links 827 def javadoc_link_filter(target, target_ndk, shortname): 828 return '{@link %s %s}' % (target, shortname) 829 830 javatext = filter_links(javatext, javadoc_link_filter) 831 832 # Crossref tag names 833 kind_mapping = { 834 'static': 'CameraCharacteristics', 835 'dynamic': 'CaptureResult', 836 'controls': 'CaptureRequest' } 837 838 # Convert metadata entry "android.x.y.z" to form 839 # "{@link CaptureRequest#X_Y_Z android.x.y.z}" 840 def javadoc_crossref_filter(node): 841 if node.applied_visibility in ('public', 'java_public'): 842 return '{@link %s#%s %s}' % (kind_mapping[node.kind], 843 jkey_identifier(node.name), 844 node.name) 845 else: 846 return node.name 847 848 # For each public tag "android.x.y.z" referenced, add a 849 # "@see CaptureRequest#X_Y_Z" 850 def javadoc_crossref_see_filter(node_set): 851 node_set = (x for x in node_set if x.applied_visibility in ('public', 'java_public')) 852 853 text = '\n' 854 for node in node_set: 855 text = text + '\n@see %s#%s' % (kind_mapping[node.kind], 856 jkey_identifier(node.name)) 857 858 return text if text != '\n' else '' 859 860 javatext = filter_tags(javatext, metadata, javadoc_crossref_filter, javadoc_crossref_see_filter) 861 862 def line_filter(line): 863 # Indent each line 864 # Add ' * ' to it for stylistic reasons 865 # Strip right side of trailing whitespace 866 return (comment_prefix + line).rstrip() 867 868 # Process each line with above filter 869 javatext = "\n".join(line_filter(i) for i in javatext.split("\n")) + "\n" 870 871 return javatext 872 873 return javadoc_formatter 874 875def ndkdoc(metadata, indent = 4): 876 """ 877 Returns a function to format a markdown syntax text block as a 878 NDK camera API C/C++ comment section, given a set of metadata 879 880 Args: 881 metadata: A Metadata instance, representing the top-level root 882 of the metadata for cross-referencing 883 indent: baseline level of indentation for comment block 884 Returns: 885 A function that transforms a String text block as follows: 886 - Indent and * for insertion into a comment block 887 - Trailing whitespace removed 888 - Entire body rendered via markdown 889 - All tag names converted to appropriate NDK tag name for each tag 890 891 Example: 892 "This is a comment for NDK\n" + 893 " with multiple lines, that should be \n" + 894 " formatted better\n" + 895 "\n" + 896 " That covers multiple lines as well\n" 897 " And references android.control.mode\n" 898 899 transforms to 900 " * This is a comment for NDK\n" + 901 " * with multiple lines, that should be\n" + 902 " * formatted better\n" + 903 " * That covers multiple lines as well\n" + 904 " * and references ACAMERA_CONTROL_MODE\n" + 905 " *\n" + 906 " * @see ACAMERA_CONTROL_MODE\n" 907 """ 908 def ndkdoc_formatter(text): 909 # render with markdown => HTML 910 # Turn off the table plugin since doxygen doesn't recognize generated <thead> <tbody> tags 911 ndktext = md(text, NDKDOC_IMAGE_SRC_METADATA, False) 912 913 # Simple transform for ndk doc links 914 def ndkdoc_link_filter(target, target_ndk, shortname): 915 if target_ndk is not None: 916 return '{@link %s %s}' % (target_ndk, shortname) 917 918 # Create HTML link to Javadoc 919 if shortname == '': 920 lastdot = target.rfind('.') 921 if lastdot == -1: 922 shortname = target 923 else: 924 shortname = target[lastdot + 1:] 925 926 target = target.replace('.','/') 927 if target.find('#') != -1: 928 target = target.replace('#','.html#') 929 else: 930 target = target + '.html' 931 932 return '<a href="https://developer.android.com/reference/%s">%s</a>' % (target, shortname) 933 934 ndktext = filter_links(ndktext, ndkdoc_link_filter) 935 936 # Convert metadata entry "android.x.y.z" to form 937 # NDK tag format of "ACAMERA_X_Y_Z" 938 def ndkdoc_crossref_filter(node): 939 if node.applied_ndk_visible == 'true': 940 return csym(ndk(node.name)) 941 else: 942 return node.name 943 944 # For each public tag "android.x.y.z" referenced, add a 945 # "@see ACAMERA_X_Y_Z" 946 def ndkdoc_crossref_see_filter(node_set): 947 node_set = (x for x in node_set if x.applied_ndk_visible == 'true') 948 949 text = '\n' 950 for node in node_set: 951 text = text + '\n@see %s' % (csym(ndk(node.name))) 952 953 return text if text != '\n' else '' 954 955 ndktext = filter_tags(ndktext, metadata, ndkdoc_crossref_filter, ndkdoc_crossref_see_filter) 956 957 ndktext = ndk_replace_tag_wildcards(ndktext, metadata) 958 959 comment_prefix = " " * indent + " * "; 960 961 def line_filter(line): 962 # Indent each line 963 # Add ' * ' to it for stylistic reasons 964 # Strip right side of trailing whitespace 965 return (comment_prefix + line).rstrip() 966 967 # Process each line with above filter 968 ndktext = "\n".join(line_filter(i) for i in ndktext.split("\n")) + "\n" 969 970 return ndktext 971 972 return ndkdoc_formatter 973 974def hidldoc(metadata, indent = 4): 975 """ 976 Returns a function to format a markdown syntax text block as a 977 HIDL camera HAL module C/C++ comment section, given a set of metadata 978 979 Args: 980 metadata: A Metadata instance, representing the top-level root 981 of the metadata for cross-referencing 982 indent: baseline level of indentation for comment block 983 Returns: 984 A function that transforms a String text block as follows: 985 - Indent and * for insertion into a comment block 986 - Trailing whitespace removed 987 - Entire body rendered via markdown 988 - All tag names converted to appropriate HIDL tag name for each tag 989 990 Example: 991 "This is a comment for NDK\n" + 992 " with multiple lines, that should be \n" + 993 " formatted better\n" + 994 "\n" + 995 " That covers multiple lines as well\n" 996 " And references android.control.mode\n" 997 998 transforms to 999 " * This is a comment for NDK\n" + 1000 " * with multiple lines, that should be\n" + 1001 " * formatted better\n" + 1002 " * That covers multiple lines as well\n" + 1003 " * and references ANDROID_CONTROL_MODE\n" + 1004 " *\n" + 1005 " * @see ANDROID_CONTROL_MODE\n" 1006 """ 1007 def hidldoc_formatter(text): 1008 # render with markdown => HTML 1009 # Turn off the table plugin since doxygen doesn't recognize generated <thead> <tbody> tags 1010 hidltext = md(text, NDKDOC_IMAGE_SRC_METADATA, False) 1011 1012 # Simple transform for hidl doc links 1013 def hidldoc_link_filter(target, target_ndk, shortname): 1014 if target_ndk is not None: 1015 return '{@link %s %s}' % (target_ndk, shortname) 1016 1017 # Create HTML link to Javadoc 1018 if shortname == '': 1019 lastdot = target.rfind('.') 1020 if lastdot == -1: 1021 shortname = target 1022 else: 1023 shortname = target[lastdot + 1:] 1024 1025 target = target.replace('.','/') 1026 if target.find('#') != -1: 1027 target = target.replace('#','.html#') 1028 else: 1029 target = target + '.html' 1030 1031 return '<a href="https://developer.android.com/reference/%s">%s</a>' % (target, shortname) 1032 1033 hidltext = filter_links(hidltext, hidldoc_link_filter) 1034 1035 # Convert metadata entry "android.x.y.z" to form 1036 # HIDL tag format of "ANDROID_X_Y_Z" 1037 def hidldoc_crossref_filter(node): 1038 return csym(node.name) 1039 1040 # For each public tag "android.x.y.z" referenced, add a 1041 # "@see ANDROID_X_Y_Z" 1042 def hidldoc_crossref_see_filter(node_set): 1043 text = '\n' 1044 for node in node_set: 1045 text = text + '\n@see %s' % (csym(node.name)) 1046 1047 return text if text != '\n' else '' 1048 1049 hidltext = filter_tags(hidltext, metadata, hidldoc_crossref_filter, hidldoc_crossref_see_filter) 1050 1051 comment_prefix = " " * indent + " * "; 1052 1053 def line_filter(line): 1054 # Indent each line 1055 # Add ' * ' to it for stylistic reasons 1056 # Strip right side of trailing whitespace 1057 return (comment_prefix + line).rstrip() 1058 1059 # Process each line with above filter 1060 hidltext = "\n".join(line_filter(i) for i in hidltext.split("\n")) + "\n" 1061 1062 return hidltext 1063 1064 return hidldoc_formatter 1065 1066def dedent(text): 1067 """ 1068 Remove all common indentation from every line but the 0th. 1069 This will avoid getting <code> blocks when rendering text via markdown. 1070 Ignoring the 0th line will also allow the 0th line not to be aligned. 1071 1072 Args: 1073 text: A string of text to dedent. 1074 1075 Returns: 1076 String dedented by above rules. 1077 1078 For example: 1079 assertEquals("bar\nline1\nline2", dedent("bar\n line1\n line2")) 1080 assertEquals("bar\nline1\nline2", dedent(" bar\n line1\n line2")) 1081 assertEquals("bar\n line1\nline2", dedent(" bar\n line1\n line2")) 1082 """ 1083 text = textwrap.dedent(text) 1084 text_lines = text.split('\n') 1085 text_not_first = "\n".join(text_lines[1:]) 1086 text_not_first = textwrap.dedent(text_not_first) 1087 text = text_lines[0] + "\n" + text_not_first 1088 1089 return text 1090 1091def md(text, img_src_prefix="", table_ext=True): 1092 """ 1093 Run text through markdown to produce HTML. 1094 1095 This also removes all common indentation from every line but the 0th. 1096 This will avoid getting <code> blocks in markdown. 1097 Ignoring the 0th line will also allow the 0th line not to be aligned. 1098 1099 Args: 1100 text: A markdown-syntax using block of text to format. 1101 img_src_prefix: An optional string to prepend to each <img src="target"/> 1102 1103 Returns: 1104 String rendered by markdown and other rules applied (see above). 1105 1106 For example, this avoids the following situation: 1107 1108 <!-- Input --> 1109 1110 <!--- can't use dedent directly since 'foo' has no indent --> 1111 <notes>foo 1112 bar 1113 bar 1114 </notes> 1115 1116 <!-- Bad Output -- > 1117 <!-- if no dedent is done generated code looks like --> 1118 <p>foo 1119 <code><pre> 1120 bar 1121 bar</pre></code> 1122 </p> 1123 1124 Instead we get the more natural expected result: 1125 1126 <!-- Good Output --> 1127 <p>foo 1128 bar 1129 bar</p> 1130 1131 """ 1132 text = dedent(text) 1133 1134 # full list of extensions at http://pythonhosted.org/Markdown/extensions/ 1135 md_extensions = ['tables'] if table_ext else []# make <table> with ASCII |_| tables 1136 # render with markdown 1137 text = markdown.markdown(text, md_extensions) 1138 1139 # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo"> 1140 text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text) 1141 return text 1142 1143def filter_tags(text, metadata, filter_function, summary_function = None): 1144 """ 1145 Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in 1146 the provided text, and pass them through filter_function and summary_function. 1147 1148 Used to linkify entry names in HMTL, javadoc output. 1149 1150 Args: 1151 text: A string representing a block of text destined for output 1152 metadata: A Metadata instance, the root of the metadata properties tree 1153 filter_function: A Node->string function to apply to each node 1154 when found in text; the string returned replaces the tag name in text. 1155 summary_function: A Node list->string function that is provided the list of 1156 unique tag nodes found in text, and which must return a string that is 1157 then appended to the end of the text. The list is sorted alphabetically 1158 by node name. 1159 """ 1160 1161 tag_set = set() 1162 def name_match(name): 1163 return lambda node: node.name == name 1164 1165 # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure 1166 # to grab .z and not just outer_namespace.x.y. (sloppy, but since we 1167 # check for validity, a few false positives don't hurt). 1168 # Try to ignore items of the form {@link <outer_namespace>... 1169 for outer_namespace in metadata.outer_namespaces: 1170 1171 tag_match = r"(?<!\{@link\s)" + outer_namespace.name + \ 1172 r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?([/]?)" 1173 1174 def filter_sub(match): 1175 whole_match = match.group(0) 1176 section1 = match.group(1) 1177 section2 = match.group(2) 1178 section3 = match.group(3) 1179 end_slash = match.group(4) 1180 1181 # Don't linkify things ending in slash (urls, for example) 1182 if end_slash: 1183 return whole_match 1184 1185 candidate = "" 1186 1187 # First try a two-level match 1188 candidate2 = "%s.%s.%s" % (outer_namespace.name, section1, section2) 1189 got_two_level = False 1190 1191 node = metadata.find_first(name_match(candidate2.replace('\n',''))) 1192 if not node and '\n' in section2: 1193 # Linefeeds are ambiguous - was the intent to add a space, 1194 # or continue a lengthy name? Try the former now. 1195 candidate2b = "%s.%s.%s" % (outer_namespace.name, section1, section2[:section2.find('\n')]) 1196 node = metadata.find_first(name_match(candidate2b)) 1197 if node: 1198 candidate2 = candidate2b 1199 1200 if node: 1201 # Have two-level match 1202 got_two_level = True 1203 candidate = candidate2 1204 elif section3: 1205 # Try three-level match 1206 candidate3 = "%s%s" % (candidate2, section3) 1207 node = metadata.find_first(name_match(candidate3.replace('\n',''))) 1208 1209 if not node and '\n' in section3: 1210 # Linefeeds are ambiguous - was the intent to add a space, 1211 # or continue a lengthy name? Try the former now. 1212 candidate3b = "%s%s" % (candidate2, section3[:section3.find('\n')]) 1213 node = metadata.find_first(name_match(candidate3b)) 1214 if node: 1215 candidate3 = candidate3b 1216 1217 if node: 1218 # Have 3-level match 1219 candidate = candidate3 1220 1221 # Replace match with crossref or complain if a likely match couldn't be matched 1222 1223 if node: 1224 tag_set.add(node) 1225 return whole_match.replace(candidate,filter_function(node)) 1226 else: 1227 print >> sys.stderr,\ 1228 " WARNING: Could not crossref likely reference {%s}" % (match.group(0)) 1229 return whole_match 1230 1231 text = re.sub(tag_match, filter_sub, text) 1232 1233 if summary_function is not None: 1234 return text + summary_function(sorted(tag_set, key=lambda x: x.name)) 1235 else: 1236 return text 1237 1238def ndk_replace_tag_wildcards(text, metadata): 1239 """ 1240 Find all references to tags in the form android.xxx.* or android.xxx.yyy.* 1241 in the provided text, and replace them by NDK format of "ACAMERA_XXX_*" or 1242 "ACAMERA_XXX_YYY_*" 1243 1244 Args: 1245 text: A string representing a block of text destined for output 1246 metadata: A Metadata instance, the root of the metadata properties tree 1247 """ 1248 tag_match = r"android\.([a-zA-Z0-9\n]+)\.\*" 1249 tag_match_2 = r"android\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)\*" 1250 1251 def filter_sub(match): 1252 return "ACAMERA_" + match.group(1).upper() + "_*" 1253 def filter_sub_2(match): 1254 return "ACAMERA_" + match.group(1).upper() + match.group(2).upper() + "_*" 1255 1256 text = re.sub(tag_match, filter_sub, text) 1257 text = re.sub(tag_match_2, filter_sub_2, text) 1258 return text 1259 1260def filter_links(text, filter_function, summary_function = None): 1261 """ 1262 Find all references to tags in the form {@link xxx#yyy [zzz]} in the 1263 provided text, and pass them through filter_function and 1264 summary_function. 1265 1266 Used to linkify documentation cross-references in HMTL, javadoc output. 1267 1268 Args: 1269 text: A string representing a block of text destined for output 1270 metadata: A Metadata instance, the root of the metadata properties tree 1271 filter_function: A (string, string)->string function to apply to each 'xxx#yyy', 1272 zzz pair when found in text; the string returned replaces the tag name in text. 1273 summary_function: A string list->string function that is provided the list of 1274 unique targets found in text, and which must return a string that is 1275 then appended to the end of the text. The list is sorted alphabetically 1276 by node name. 1277 1278 """ 1279 1280 target_set = set() 1281 def name_match(name): 1282 return lambda node: node.name == name 1283 1284 tag_match = r"\{@link\s+([^\s\}\|]+)(?:\|([^\s\}]+))*([^\}]*)\}" 1285 1286 def filter_sub(match): 1287 whole_match = match.group(0) 1288 target = match.group(1) 1289 target_ndk = match.group(2) 1290 shortname = match.group(3).strip() 1291 1292 #print "Found link '%s' ndk '%s' as '%s' -> '%s'" % (target, target_ndk, shortname, filter_function(target, target_ndk, shortname)) 1293 1294 # Replace match with crossref 1295 target_set.add(target) 1296 return filter_function(target, target_ndk, shortname) 1297 1298 text = re.sub(tag_match, filter_sub, text) 1299 1300 if summary_function is not None: 1301 return text + summary_function(sorted(target_set)) 1302 else: 1303 return text 1304 1305def any_visible(section, kind_name, visibilities): 1306 """ 1307 Determine if entries in this section have an applied visibility that's in 1308 the list of given visibilities. 1309 1310 Args: 1311 section: A section of metadata 1312 kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls' 1313 visibilities: An iterable of visibilities to match against 1314 1315 Returns: 1316 True if the section has any entries with any of the given visibilities. False otherwise. 1317 """ 1318 1319 for inner_namespace in get_children_by_filtering_kind(section, kind_name, 1320 'namespaces'): 1321 if any(filter_visibility(inner_namespace.merged_entries, visibilities)): 1322 return True 1323 1324 return any(filter_visibility(get_children_by_filtering_kind(section, kind_name, 1325 'merged_entries'), 1326 visibilities)) 1327 1328 1329def filter_visibility(entries, visibilities): 1330 """ 1331 Remove entries whose applied visibility is not in the supplied visibilities. 1332 1333 Args: 1334 entries: An iterable of Entry nodes 1335 visibilities: An iterable of visibilities to filter against 1336 1337 Yields: 1338 An iterable of Entry nodes 1339 """ 1340 return (e for e in entries if e.applied_visibility in visibilities) 1341 1342def remove_synthetic(entries): 1343 """ 1344 Filter the given entries by removing those that are synthetic. 1345 1346 Args: 1347 entries: An iterable of Entry nodes 1348 1349 Yields: 1350 An iterable of Entry nodes 1351 """ 1352 return (e for e in entries if not e.synthetic) 1353 1354def filter_added_in_hal_version(entries, hal_major_version, hal_minor_version): 1355 """ 1356 Filter the given entries to those added in the given HIDL HAL version 1357 1358 Args: 1359 entries: An iterable of Entry nodes 1360 hal_major_version: Major HIDL version to filter for 1361 hal_minor_version: Minor HIDL version to filter for 1362 1363 Yields: 1364 An iterable of Entry nodes 1365 """ 1366 return (e for e in entries if e.hal_major_version == hal_major_version and e.hal_minor_version == hal_minor_version) 1367 1368def filter_has_enum_values_added_in_hal_version(entries, hal_major_version, hal_minor_version): 1369 """ 1370 Filter the given entries to those that have a new enum value added in the given HIDL HAL version 1371 1372 Args: 1373 entries: An iterable of Entry nodes 1374 hal_major_version: Major HIDL version to filter for 1375 hal_minor_version: Minor HIDL version to filter for 1376 1377 Yields: 1378 An iterable of Entry nodes 1379 """ 1380 return (e for e in entries if e.has_new_values_added_in_hal_version(hal_major_version, hal_minor_version)) 1381 1382def filter_ndk_visible(entries): 1383 """ 1384 Filter the given entries by removing those that are not NDK visible. 1385 1386 Args: 1387 entries: An iterable of Entry nodes 1388 1389 Yields: 1390 An iterable of Entry nodes 1391 """ 1392 return (e for e in entries if e.applied_ndk_visible == 'true') 1393 1394def wbr(text): 1395 """ 1396 Insert word break hints for the browser in the form of <wbr> HTML tags. 1397 1398 Word breaks are inserted inside an HTML node only, so the nodes themselves 1399 will not be changed. Attributes are also left unchanged. 1400 1401 The following rules apply to insert word breaks: 1402 - For characters in [ '.', '/', '_' ] 1403 - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts) 1404 1405 Args: 1406 text: A string of text containing HTML content. 1407 1408 Returns: 1409 A string with <wbr> inserted by the above rules. 1410 """ 1411 SPLIT_CHARS_LIST = ['.', '_', '/'] 1412 SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters 1413 CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z 1414 def wbr_filter(text): 1415 new_txt = text 1416 1417 # for johnyOrange.appleCider.redGuardian also insert wbr before the caps 1418 # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian 1419 for words in text.split(" "): 1420 for char in SPLIT_CHARS_LIST: 1421 # match at least x.y.z, don't match x or x.y 1422 if len(words.split(char)) >= CAP_LETTER_MIN: 1423 new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words) 1424 new_txt = new_txt.replace(words, new_word) 1425 1426 # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z. 1427 new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt) 1428 1429 return new_txt 1430 1431 # Do not mangle HTML when doing the replace by using BeatifulSoup 1432 # - Use the 'html.parser' to avoid inserting <html><body> when decoding 1433 soup = bs4.BeautifulSoup(text, features='html.parser') 1434 wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time 1435 1436 for navigable_string in soup.findAll(text=True): 1437 parent = navigable_string.parent 1438 1439 # Insert each '$text<wbr>$foo' before the old '$text$foo' 1440 split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>") 1441 for (split_string, last) in enumerate_with_last(split_by_wbr_list): 1442 navigable_string.insert_before(split_string) 1443 1444 if not last: 1445 # Note that 'insert' will move existing tags to this spot 1446 # so make a new tag instead 1447 navigable_string.insert_before(wbr_tag()) 1448 1449 # Remove the old unmodified text 1450 navigable_string.extract() 1451 1452 return soup.decode() 1453 1454def hal_major_version(): 1455 return _hal_major_version 1456 1457def hal_minor_version(): 1458 return _hal_minor_version 1459 1460def first_hal_minor_version(hal_major_version): 1461 return 2 if hal_major_version == 3 else 0 1462 1463def find_all_sections_added_in_hal(root, hal_major_version, hal_minor_version): 1464 """ 1465 Find all descendants that are Section or InnerNamespace instances, which 1466 were added in HIDL HAL version major.minor. The section is defined to be 1467 added in a HAL version iff the lowest HAL version number of its entries is 1468 that HAL version. 1469 1470 Args: 1471 root: a Metadata instance 1472 hal_major/minor_version: HAL version numbers 1473 1474 Returns: 1475 A list of Section/InnerNamespace instances 1476 1477 Remarks: 1478 These are known as "sections" in the generated C code. 1479 """ 1480 all_sections = find_all_sections(root) 1481 new_sections = [] 1482 for section in all_sections: 1483 min_major_version = None 1484 min_minor_version = None 1485 for entry in remove_synthetic(find_unique_entries(section)): 1486 min_major_version = (min_major_version or entry.hal_major_version) 1487 min_minor_version = (min_minor_version or entry.hal_minor_version) 1488 if entry.hal_major_version < min_major_version or \ 1489 (entry.hal_major_version == min_major_version and entry.hal_minor_version < min_minor_version): 1490 min_minor_version = entry.hal_minor_version 1491 min_major_version = entry.hal_major_version 1492 if min_major_version == hal_major_version and min_minor_version == hal_minor_version: 1493 new_sections.append(section) 1494 return new_sections 1495 1496def find_first_older_used_hal_version(section, hal_major_version, hal_minor_version): 1497 hal_version = (0, 0) 1498 for v in section.hal_versions: 1499 if (v[0] > hal_version[0] or (v[0] == hal_version[0] and v[1] > hal_version[1])) and \ 1500 (v[0] < hal_major_version or (v[0] == hal_major_version and v[1] < hal_minor_version)): 1501 hal_version = v 1502 return hal_version 1503