blob: 477bdfc709eff54a4e85e48e53ac9de9756f01b6 [file] [log] [blame]
Brad Bishopbec4ebc2022-08-03 09:55:16 -04001#! /usr/bin/env python3
2
3import argparse
4import datetime
5import os
6import pathlib
7import re
8import sys
9
10import jinja2
11
12def trim_pv(pv):
13 """
14 Strip anything after +git from the PV
15 """
16 return "".join(pv.partition("+git")[:2])
17
18def needs_update(version, upstream):
19 """
20 Do a dumb comparison to determine if the version needs to be updated.
21 """
22 if "+git" in version:
23 # strip +git and see if this is a post-release snapshot
24 version = version.replace("+git", "")
25 return version != upstream
26
27def safe_patches(patches):
28 for info in patches:
29 if info["status"] in ("Denied", "Pending", "Unknown"):
30 return False
31 return True
32
33def layer_path(layername: str, d) -> pathlib.Path:
34 """
35 Return the path to the specified layer, or None if the layer isn't present.
36 """
37 if not hasattr(layer_path, "cache"):
38 # Don't use functools.lru_cache as we don't want d changing to invalidate the cache
39 layer_path.cache = {}
40
41 if layername in layer_path.cache:
42 return layer_path.cache[layername]
43
44 bbpath = d.getVar("BBPATH").split(":")
45 pattern = d.getVar('BBFILE_PATTERN_' + layername)
46 for path in reversed(sorted(bbpath)):
47 if re.match(pattern, path + "/"):
48 layer_path.cache[layername] = pathlib.Path(path)
49 return layer_path.cache[layername]
50 return None
51
52def get_url_for_patch(layer: str, localpath: pathlib.Path, d) -> str:
53 relative = localpath.relative_to(layer_path(layer, d))
54
55 # TODO: use layerindexlib
56 # TODO: assumes default branch
57 if layer == "core":
58 return f"https://git.openembedded.org/openembedded-core/tree/meta/{relative}"
Andrew Geissler517393d2023-01-13 08:55:19 -060059 elif layer in ("meta-arm", "meta-arm-bsp", "arm-toolchain"):
Brad Bishopbec4ebc2022-08-03 09:55:16 -040060 return f"https://git.yoctoproject.org/meta-arm/tree/{layer}/{relative}"
61 else:
62 print(f"WARNING: Don't know web URL for layer {layer}", file=sys.stderr)
63 return None
64
65def extract_patch_info(src_uri, d):
66 """
67 Parse the specified patch entry from a SRC_URI and return (base name, layer name, status) tuple
68 """
69 import bb.fetch, bb.utils
70
71 info = {}
72 localpath = pathlib.Path(bb.fetch.decodeurl(src_uri)[2])
73 info["name"] = localpath.name
74 info["layer"] = bb.utils.get_file_layer(str(localpath), d)
75 info["url"] = get_url_for_patch(info["layer"], localpath, d)
76
77 status = "Unknown"
78 with open(localpath, errors="ignore") as f:
79 m = re.search(r"^[\t ]*Upstream[-_ ]Status:?[\t ]*(\w*)", f.read(), re.IGNORECASE | re.MULTILINE)
80 if m:
81 # TODO: validate
82 status = m.group(1)
83 info["status"] = status
84 return info
85
86def harvest_data(machines, recipes):
87 import bb.tinfoil
88 with bb.tinfoil.Tinfoil() as tinfoil:
89 tinfoil.prepare(config_only=True)
90 corepath = layer_path("core", tinfoil.config_data)
91 sys.path.append(os.path.join(corepath, "lib"))
92 import oe.recipeutils
93 import oe.patch
94
95 # Queue of recipes that we're still looking for upstream releases for
96 to_check = list(recipes)
97
98 # Upstream releases
99 upstreams = {}
100 # Machines to recipes to versions
101 versions = {}
102
103 for machine in machines:
104 print(f"Gathering data for {machine}...")
105 os.environ["MACHINE"] = machine
106 with bb.tinfoil.Tinfoil() as tinfoil:
107 versions[machine] = {}
108
109 tinfoil.prepare(quiet=2)
110 for recipe in recipes:
111 try:
112 d = tinfoil.parse_recipe(recipe)
113 except bb.providers.NoProvider:
114 continue
115
116 if recipe in to_check:
117 try:
118 info = oe.recipeutils.get_recipe_upstream_version(d)
119 upstreams[recipe] = info["version"]
120 to_check.remove(recipe)
121 except (bb.providers.NoProvider, KeyError):
122 pass
123
124 details = versions[machine][recipe] = {}
125 details["recipe"] = d.getVar("PN")
126 details["version"] = trim_pv(d.getVar("PV"))
127 details["fullversion"] = d.getVar("PV")
128 details["patches"] = [extract_patch_info(p, d) for p in oe.patch.src_patches(d)]
129 details["patched"] = bool(details["patches"])
130 details["patches_safe"] = safe_patches(details["patches"])
131
132 # Now backfill the upstream versions
133 for machine in versions:
134 for recipe in versions[machine]:
135 data = versions[machine][recipe]
136 data["upstream"] = upstreams[recipe]
137 data["needs_update"] = needs_update(data["version"], data["upstream"])
138 return upstreams, versions
139
140# TODO can this be inferred from the list of recipes in the layer
141recipes = ("virtual/kernel",
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600142 "sbsa-acs",
Brad Bishopbec4ebc2022-08-03 09:55:16 -0400143 "scp-firmware",
144 "trusted-firmware-a",
145 "trusted-firmware-m",
146 "edk2-firmware",
147 "u-boot",
148 "optee-os",
Andrew Geissler9347dd42023-03-03 12:38:41 -0600149 "hafnium",
150 "boot-wrapper-aarch64",
151 "gator-daemon",
152 "opencsd",
Brad Bishopbec4ebc2022-08-03 09:55:16 -0400153 "gcc-aarch64-none-elf-native",
154 "gcc-arm-none-eabi-native")
155
156
157class Format:
158 """
159 The name of this format
160 """
161 name = None
162 """
163 Registry of names to classes
164 """
165 registry = {}
166
167 def __init_subclass__(cls, **kwargs):
168 super().__init_subclass__(**kwargs)
169 assert cls.name
170 cls.registry[cls.name] = cls
171
172 @classmethod
173 def get_format(cls, name):
174 return cls.registry[name]()
175
176 def render(self, context, output: pathlib.Path):
177 pass
178
179 def get_template(self, name):
180 template_dir = os.path.dirname(os.path.abspath(__file__))
181 env = jinja2.Environment(
182 loader=jinja2.FileSystemLoader(template_dir),
183 extensions=['jinja2.ext.i18n'],
184 autoescape=jinja2.select_autoescape(),
185 trim_blocks=True,
186 lstrip_blocks=True
187 )
188
189 # We only need i18n for plurals
190 env.install_null_translations()
191
192 return env.get_template(name)
193
194class TextOverview(Format):
195 name = "overview.txt"
196
197 def render(self, context, output: pathlib.Path):
198 with open(output, "wt") as f:
199 f.write(self.get_template(f"machine-summary-overview.txt.jinja").render(context))
200
201class HtmlUpdates(Format):
202 name = "report"
203
204 def render(self, context, output: pathlib.Path):
205 if output.exists() and not output.is_dir():
206 print(f"{output} is not a directory", file=sys.stderr)
207 sys.exit(1)
208
209 if not output.exists():
210 output.mkdir(parents=True)
211
212 with open(output / "index.html", "wt") as f:
213 f.write(self.get_template(f"report-index.html.jinja").render(context))
214
215 subcontext = context.copy()
216 del subcontext["data"]
217 for machine, subdata in context["data"].items():
218 subcontext["machine"] = machine
219 subcontext["data"] = subdata
220 with open(output / f"{machine}.html", "wt") as f:
221 f.write(self.get_template(f"report-details.html.jinja").render(subcontext))
222
223if __name__ == "__main__":
224 parser = argparse.ArgumentParser(description="machine-summary")
225 parser.add_argument("machines", nargs="+", help="machine names", metavar="MACHINE")
226 parser.add_argument("-t", "--type", required=True, choices=Format.registry.keys())
227 parser.add_argument("-o", "--output", type=pathlib.Path, required=True)
228 args = parser.parse_args()
229
230 context = {}
231 # TODO: include git describe for meta-arm
232 context["timestamp"] = str(datetime.datetime.now().strftime("%c"))
233 context["recipes"] = sorted(recipes)
234 context["releases"], context["data"] = harvest_data(args.machines, recipes)
235
236 formatter = Format.get_format(args.type)
237 formatter.render(context, args.output)