machine-summary.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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 safe_patches(patches):
  23. for info in patches:
  24. if info["status"] in ("Denied", "Pending", "Unknown"):
  25. return False
  26. return True
  27. def layer_path(layername, d):
  28. """
  29. Return the path to the specified layer, or None if the layer isn't present.
  30. """
  31. import re
  32. bbpath = d.getVar("BBPATH").split(":")
  33. pattern = d.getVar('BBFILE_PATTERN_' + layername)
  34. for path in reversed(sorted(bbpath)):
  35. if re.match(pattern, path + "/"):
  36. return path
  37. return None
  38. def extract_patch_info(src_uri, d):
  39. """
  40. Parse the specified patch entry from a SRC_URI and return (base name, layer name, status) tuple
  41. """
  42. import bb.fetch, bb.utils
  43. info = {}
  44. localpath = bb.fetch.decodeurl(src_uri)[2]
  45. info["name"] = os.path.basename(localpath)
  46. info["layer"] = bb.utils.get_file_layer(localpath, d)
  47. status = "Unknown"
  48. with open(localpath, errors="ignore") as f:
  49. m = re.search(r"^[\t ]*Upstream[-_ ]Status:?[\t ]*(\w*)", f.read(), re.IGNORECASE | re.MULTILINE)
  50. if m:
  51. # TODO: validate
  52. status = m.group(1)
  53. info["status"] = status
  54. return info
  55. def harvest_data(machines, recipes):
  56. import bb.tinfoil
  57. with bb.tinfoil.Tinfoil() as tinfoil:
  58. tinfoil.prepare(config_only=True)
  59. corepath = layer_path("core", tinfoil.config_data)
  60. sys.path.append(os.path.join(corepath, "lib"))
  61. import oe.recipeutils
  62. import oe.patch
  63. # Queue of recipes that we're still looking for upstream releases for
  64. to_check = list(recipes)
  65. # Upstream releases
  66. upstreams = {}
  67. # Machines to recipes to versions
  68. versions = {}
  69. for machine in machines:
  70. print(f"Gathering data for {machine}...")
  71. os.environ["MACHINE"] = machine
  72. with bb.tinfoil.Tinfoil() as tinfoil:
  73. versions[machine] = {}
  74. tinfoil.prepare(quiet=2)
  75. for recipe in recipes:
  76. try:
  77. d = tinfoil.parse_recipe(recipe)
  78. except bb.providers.NoProvider:
  79. continue
  80. if recipe in to_check:
  81. try:
  82. info = oe.recipeutils.get_recipe_upstream_version(d)
  83. upstreams[recipe] = info["version"]
  84. to_check.remove(recipe)
  85. except (bb.providers.NoProvider, KeyError):
  86. pass
  87. details = versions[machine][recipe] = {}
  88. details["recipe"] = d.getVar("PN")
  89. details["version"] = trim_pv(d.getVar("PV"))
  90. details["fullversion"] = d.getVar("PV")
  91. details["patches"] = [extract_patch_info(p, d) for p in oe.patch.src_patches(d)]
  92. details["patched"] = bool(details["patches"])
  93. details["patches_safe"] = safe_patches(details["patches"])
  94. # Now backfill the upstream versions
  95. for machine in versions:
  96. for recipe in versions[machine]:
  97. data = versions[machine][recipe]
  98. data["upstream"] = upstreams[recipe]
  99. data["needs_update"] = needs_update(data["version"], data["upstream"])
  100. return upstreams, versions
  101. # TODO can this be inferred from the list of recipes in the layer
  102. recipes = ("virtual/kernel",
  103. "scp-firmware",
  104. "trusted-firmware-a",
  105. "trusted-firmware-m",
  106. "edk2-firmware",
  107. "u-boot",
  108. "optee-os",
  109. "armcompiler-native",
  110. "gcc-aarch64-none-elf-native",
  111. "gcc-arm-none-eabi-native")
  112. class Format:
  113. """
  114. The name of this format
  115. """
  116. name = None
  117. """
  118. Registry of names to classes
  119. """
  120. registry = {}
  121. def __init_subclass__(cls, **kwargs):
  122. super().__init_subclass__(**kwargs)
  123. assert cls.name
  124. cls.registry[cls.name] = cls
  125. @classmethod
  126. def get_format(cls, name):
  127. return cls.registry[name]()
  128. def render(self, context, output: pathlib.Path):
  129. pass
  130. def get_template(self, name):
  131. template_dir = os.path.dirname(os.path.abspath(__file__))
  132. env = jinja2.Environment(
  133. loader=jinja2.FileSystemLoader(template_dir),
  134. extensions=['jinja2.ext.i18n'],
  135. autoescape=jinja2.select_autoescape(),
  136. trim_blocks=True,
  137. lstrip_blocks=True
  138. )
  139. # We only need i18n for plurals
  140. env.install_null_translations()
  141. return env.get_template(name)
  142. class TextOverview(Format):
  143. name = "overview.txt"
  144. def render(self, context, output: pathlib.Path):
  145. with open(output, "wt") as f:
  146. f.write(self.get_template(f"machine-summary-overview.txt.jinja").render(context))
  147. class HtmlUpdates(Format):
  148. name = "report"
  149. def render(self, context, output: pathlib.Path):
  150. if output.exists() and not output.is_dir():
  151. print(f"{output} is not a directory", file=sys.stderr)
  152. sys.exit(1)
  153. if not output.exists():
  154. output.mkdir(parents=True)
  155. with open(output / "index.html", "wt") as f:
  156. f.write(self.get_template(f"report-index.html.jinja").render(context))
  157. subcontext = context.copy()
  158. del subcontext["data"]
  159. for machine, subdata in context["data"].items():
  160. subcontext["machine"] = machine
  161. subcontext["data"] = subdata
  162. with open(output / f"{machine}.html", "wt") as f:
  163. f.write(self.get_template(f"report-details.html.jinja").render(subcontext))
  164. if __name__ == "__main__":
  165. parser = argparse.ArgumentParser(description="machine-summary")
  166. parser.add_argument("machines", nargs="+", help="machine names", metavar="MACHINE")
  167. parser.add_argument("-t", "--type", required=True, choices=Format.registry.keys())
  168. parser.add_argument("-o", "--output", type=pathlib.Path, required=True)
  169. args = parser.parse_args()
  170. context = {}
  171. # TODO: include git describe for meta-arm
  172. context["timestamp"] = str(datetime.datetime.now().strftime("%c"))
  173. context["recipes"] = sorted(recipes)
  174. context["releases"], context["data"] = harvest_data(args.machines, recipes)
  175. formatter = Format.get_format(args.type)
  176. formatter.render(context, args.output)