machine-summary.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. #! /usr/bin/env python3
  2. import argparse
  3. import datetime
  4. import os
  5. import pathlib
  6. import re
  7. import sys
  8. import jinja2
  9. def trim_pv(pv):
  10. """
  11. Strip anything after +git from the PV
  12. """
  13. return "".join(pv.partition("+git")[:2])
  14. def needs_update(version, upstream):
  15. """
  16. Do a dumb comparison to determine if the version needs to be updated.
  17. """
  18. if "+git" in version:
  19. # strip +git and see if this is a post-release snapshot
  20. version = version.replace("+git", "")
  21. return version != upstream
  22. def layer_path(layername, d):
  23. """
  24. Return the path to the specified layer, or None if the layer isn't present.
  25. """
  26. import re
  27. bbpath = d.getVar("BBPATH").split(":")
  28. pattern = d.getVar('BBFILE_PATTERN_' + layername)
  29. for path in reversed(sorted(bbpath)):
  30. if re.match(pattern, path + "/"):
  31. return path
  32. return None
  33. def extract_patch_info(src_uri, d):
  34. """
  35. Parse the specified patch entry from a SRC_URI and return (base name, layer name, status) tuple
  36. """
  37. import bb.fetch, bb.utils
  38. info = {}
  39. localpath = bb.fetch.decodeurl(src_uri)[2]
  40. info["name"] = os.path.basename(localpath)
  41. info["layer"] = bb.utils.get_file_layer(localpath, d)
  42. status = "Unknown"
  43. with open(localpath, errors="ignore") as f:
  44. m = re.search(r"^[\t ]*Upstream[-_ ]Status:?[\t ]*(\w*)", f.read(), re.IGNORECASE | re.MULTILINE)
  45. if m:
  46. # TODO: validate
  47. status = m.group(1)
  48. info["status"] = status
  49. return info
  50. def harvest_data(machines, recipes):
  51. import bb.tinfoil
  52. with bb.tinfoil.Tinfoil() as tinfoil:
  53. tinfoil.prepare(config_only=True)
  54. corepath = layer_path("core", tinfoil.config_data)
  55. sys.path.append(os.path.join(corepath, "lib"))
  56. import oe.recipeutils
  57. import oe.patch
  58. # Queue of recipes that we're still looking for upstream releases for
  59. to_check = list(recipes)
  60. # Upstream releases
  61. upstreams = {}
  62. # Machines to recipes to versions
  63. versions = {}
  64. for machine in machines:
  65. print(f"Gathering data for {machine}...")
  66. os.environ["MACHINE"] = machine
  67. with bb.tinfoil.Tinfoil() as tinfoil:
  68. versions[machine] = {}
  69. tinfoil.prepare(quiet=2)
  70. for recipe in recipes:
  71. try:
  72. d = tinfoil.parse_recipe(recipe)
  73. except bb.providers.NoProvider:
  74. continue
  75. if recipe in to_check:
  76. try:
  77. info = oe.recipeutils.get_recipe_upstream_version(d)
  78. upstreams[recipe] = info["version"]
  79. to_check.remove(recipe)
  80. except (bb.providers.NoProvider, KeyError):
  81. pass
  82. details = versions[machine][recipe] = {}
  83. details["recipe"] = d.getVar("PN")
  84. details["version"] = trim_pv(d.getVar("PV"))
  85. details["fullversion"] = d.getVar("PV")
  86. details["patches"] = [extract_patch_info(p, d) for p in oe.patch.src_patches(d)]
  87. details["patched"] = bool(details["patches"])
  88. # Now backfill the upstream versions
  89. for machine in versions:
  90. for recipe in versions[machine]:
  91. data = versions[machine][recipe]
  92. data["upstream"] = upstreams[recipe]
  93. data["needs_update"] = needs_update(data["version"], data["upstream"])
  94. return upstreams, versions
  95. # TODO can this be inferred from the list of recipes in the layer
  96. recipes = ("virtual/kernel",
  97. "scp-firmware",
  98. "trusted-firmware-a",
  99. "trusted-firmware-m",
  100. "edk2-firmware",
  101. "u-boot",
  102. "optee-os",
  103. "armcompiler-native",
  104. "gcc-aarch64-none-elf-native",
  105. "gcc-arm-none-eabi-native")
  106. class Format:
  107. """
  108. The name of this format
  109. """
  110. name = None
  111. """
  112. Registry of names to classes
  113. """
  114. registry = {}
  115. def __init_subclass__(cls, **kwargs):
  116. super().__init_subclass__(**kwargs)
  117. assert cls.name
  118. cls.registry[cls.name] = cls
  119. @classmethod
  120. def get_format(cls, name):
  121. return cls.registry[name]()
  122. def render(self, context, output: pathlib.Path):
  123. # Default implementation for convenience
  124. with open(output, "wt") as f:
  125. f.write(self.get_template(f"machine-summary-{self.name}.jinja").render(context))
  126. def get_template(self, name):
  127. template_dir = os.path.dirname(os.path.abspath(__file__))
  128. env = jinja2.Environment(
  129. loader=jinja2.FileSystemLoader(template_dir),
  130. autoescape=jinja2.select_autoescape(),
  131. trim_blocks=True,
  132. lstrip_blocks=True
  133. )
  134. def is_old(version, upstream):
  135. if "+git" in version:
  136. # strip +git and see if this is a post-release snapshot
  137. version = version.replace("+git", "")
  138. return version != upstream
  139. env.tests["old"] = is_old
  140. return env.get_template(name)
  141. class TextOverview(Format):
  142. name = "overview.txt"
  143. class HtmlUpdates(Format):
  144. name = "updates.html"
  145. if __name__ == "__main__":
  146. parser = argparse.ArgumentParser(description="machine-summary")
  147. parser.add_argument("machines", nargs="+", help="machine names", metavar="MACHINE")
  148. parser.add_argument("-t", "--type", required=True, choices=Format.registry.keys())
  149. parser.add_argument("-o", "--output", type=pathlib.Path, required=True)
  150. args = parser.parse_args()
  151. context = {}
  152. # TODO: include git describe for meta-arm
  153. context["timestamp"] = str(datetime.datetime.now().strftime("%c"))
  154. context["recipes"] = sorted(recipes)
  155. context["releases"], context["data"] = harvest_data(args.machines, recipes)
  156. formatter = Format.get_format(args.type)
  157. formatter.render(context, args.output)