machine-summary.py 7.7 KB

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