blob: 1a70f14a92c080f1007e10d1ae10b6f2ebfcc2e1 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
2# Toaster helper class
3#
4# Copyright (C) 2013 Intel Corporation
5#
6# Released under the MIT license (see COPYING.MIT)
7#
8# This bbclass is designed to extract data used by OE-Core during the build process,
9# for recording in the Toaster system.
10# The data access is synchronous, preserving the build data integrity across
11# different builds.
12#
13# The data is transferred through the event system, using the MetadataEvent objects.
14#
15# The model is to enable the datadump functions as postfuncs, and have the dump
16# executed after the real taskfunc has been executed. This prevents task signature changing
17# is toaster is enabled or not. Build performance is not affected if Toaster is not enabled.
18#
19# To enable, use INHERIT in local.conf:
20#
21# INHERIT += "toaster"
22#
23#
24#
25#
26
27# Find and dump layer info when we got the layers parsed
28
29
30
31python toaster_layerinfo_dumpdata() {
32 import subprocess
33
34 def _get_git_branch(layer_path):
35 branch = subprocess.Popen("git symbolic-ref HEAD 2>/dev/null ", cwd=layer_path, shell=True, stdout=subprocess.PIPE).communicate()[0]
36 branch = branch.replace('refs/heads/', '').rstrip()
37 return branch
38
39 def _get_git_revision(layer_path):
40 revision = subprocess.Popen("git rev-parse HEAD 2>/dev/null ", cwd=layer_path, shell=True, stdout=subprocess.PIPE).communicate()[0].rstrip()
41 return revision
42
43 def _get_url_map_name(layer_name):
44 """ Some layers have a different name on openembedded.org site,
45 this method returns the correct name to use in the URL
46 """
47
48 url_name = layer_name
49 url_mapping = {'meta': 'openembedded-core'}
50
51 for key in url_mapping.keys():
52 if key == layer_name:
53 url_name = url_mapping[key]
54
55 return url_name
56
57 def _get_layer_version_information(layer_path):
58
59 layer_version_info = {}
60 layer_version_info['branch'] = _get_git_branch(layer_path)
61 layer_version_info['commit'] = _get_git_revision(layer_path)
62 layer_version_info['priority'] = 0
63
64 return layer_version_info
65
66
67 def _get_layer_dict(layer_path):
68
69 layer_info = {}
70 layer_name = layer_path.split('/')[-1]
71 layer_url = 'http://layers.openembedded.org/layerindex/layer/{layer}/'
72 layer_url_name = _get_url_map_name(layer_name)
73
74 layer_info['name'] = layer_url_name
75 layer_info['local_path'] = layer_path
76 layer_info['layer_index_url'] = layer_url.format(layer=layer_url_name)
77 layer_info['version'] = _get_layer_version_information(layer_path)
78
79 return layer_info
80
81
82 bblayers = e.data.getVar("BBLAYERS", True)
83
84 llayerinfo = {}
85
86 for layer in { l for l in bblayers.strip().split(" ") if len(l) }:
87 llayerinfo[layer] = _get_layer_dict(layer)
88
89
90 bb.event.fire(bb.event.MetadataEvent("LayerInfo", llayerinfo), e.data)
91}
92
93# Dump package file info data
94
95def _toaster_load_pkgdatafile(dirpath, filepath):
96 import json
97 import re
98 pkgdata = {}
99 with open(os.path.join(dirpath, filepath), "r") as fin:
100 for line in fin:
101 try:
102 kn, kv = line.strip().split(": ", 1)
103 m = re.match(r"^PKG_([^A-Z:]*)", kn)
104 if m:
105 pkgdata['OPKGN'] = m.group(1)
106 kn = "_".join([x for x in kn.split("_") if x.isupper()])
107 pkgdata[kn] = kv.strip()
108 if kn == 'FILES_INFO':
109 pkgdata[kn] = json.loads(kv)
110
111 except ValueError:
112 pass # ignore lines without valid key: value pairs
113 return pkgdata
114
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500115python toaster_package_dumpdata() {
116 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500117 Dumps the data about the packages created by a recipe
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500118 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500119
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500120 # No need to try and dumpdata if the recipe isn't generating packages
121 if not d.getVar('PACKAGES', True):
122 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500123
124 pkgdatadir = d.getVar('PKGDESTWORK', True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500125 lpkgdata = {}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500126 datadir = os.path.join(pkgdatadir, 'runtime')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500127
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500128 # scan and send data for each generated package
129 for datafile in os.listdir(datadir):
130 if not datafile.endswith('.packaged'):
131 lpkgdata = _toaster_load_pkgdatafile(datadir, datafile)
132 # Fire an event containing the pkg data
133 bb.event.fire(bb.event.MetadataEvent("SinglePackageInfo", lpkgdata), d)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500134}
135
136# 2. Dump output image files information
137
138python toaster_image_dumpdata() {
139 """
140 Image filename for output images is not standardized.
141 image_types.bbclass will spell out IMAGE_CMD_xxx variables that actually
142 have hardcoded ways to create image file names in them.
143 So we look for files starting with the set name.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500144
145 We also look for other files in the images/ directory which don't
146 match IMAGE_NAME, such as the kernel bzImage, modules tarball etc.
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500147 """
148
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500149 dir_to_walk = d.getVar('DEPLOY_DIR_IMAGE', True);
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500150 image_name = d.getVar('IMAGE_NAME', True);
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500151 image_info_data = {}
152 artifact_info_data = {}
153
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500154 # collect all images and artifacts in the images directory
155 for dirpath, dirnames, filenames in os.walk(dir_to_walk):
156 for filename in filenames:
157 full_path = os.path.join(dirpath, filename)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500158 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500159 if filename.startswith(image_name):
160 # image
161 image_info_data[full_path] = os.stat(full_path).st_size
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500162 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500163 # other non-image artifact
164 if not os.path.islink(full_path):
165 artifact_info_data[full_path] = os.stat(full_path).st_size
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500166 except OSError as e:
167 bb.event.fire(bb.event.MetadataEvent("OSErrorException", e), d)
168
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500169 bb.event.fire(bb.event.MetadataEvent("ImageFileSize", image_info_data), d)
170 bb.event.fire(bb.event.MetadataEvent("ArtifactFileSize", artifact_info_data), d)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500171}
172
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500173python toaster_artifact_dumpdata() {
174 """
175 Dump data about artifacts in the SDK_DEPLOY directory
176 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500177
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500178 dir_to_walk = d.getVar("SDK_DEPLOY", True)
179 artifact_info_data = {}
180
181 # collect all artifacts in the sdk directory
182 for dirpath, dirnames, filenames in os.walk(dir_to_walk):
183 for filename in filenames:
184 full_path = os.path.join(dirpath, filename)
185 try:
186 if not os.path.islink(full_path):
187 artifact_info_data[full_path] = os.stat(full_path).st_size
188 except OSError as e:
189 bb.event.fire(bb.event.MetadataEvent("OSErrorException", e), d)
190
191 bb.event.fire(bb.event.MetadataEvent("ArtifactFileSize", artifact_info_data), d)
192}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500193
194# collect list of buildstats files based on fired events; when the build completes, collect all stats and fire an event with collected data
195
196python toaster_collect_task_stats() {
197 import bb.build
198 import bb.event
199 import bb.data
200 import bb.utils
201 import os
202
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500203 toaster_statlist_file = os.path.join(e.data.getVar('BUILDSTATS_BASE', True), "toasterstatlist")
204
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500205 if not e.data.getVar('BUILDSTATS_BASE', True):
206 return # if we don't have buildstats, we cannot collect stats
207
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500208 def stat_to_float(value):
209 return float(value.strip('% \n\r'))
210
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500211 def _append_read_list(v):
212 lock = bb.utils.lockfile(e.data.expand("${TOPDIR}/toaster.lock"), False, True)
213
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500214 with open(toaster_statlist_file, "a") as fout:
215 taskdir = e.data.expand("${BUILDSTATS_BASE}/${BUILDNAME}/${PF}")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500216 fout.write("%s::%s::%s::%s\n" % (e.taskfile, e.taskname, os.path.join(taskdir, e.task), e.data.expand("${PN}")))
217
218 bb.utils.unlockfile(lock)
219
220 def _read_stats(filename):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500221 # seconds
222 cpu_time_user = 0
223 cpu_time_system = 0
224
225 # bytes
226 disk_io_read = 0
227 disk_io_write = 0
228
229 started = 0
230 ended = 0
231
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500232 taskname = ''
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500233
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500234 statinfo = {}
235
236 with open(filename, 'r') as task_bs:
237 for line in task_bs.readlines():
238 k,v = line.strip().split(": ", 1)
239 statinfo[k] = v
240
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500241 if "Started" in statinfo:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500242 started = stat_to_float(statinfo["Started"])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500243
244 if "Ended" in statinfo:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500245 ended = stat_to_float(statinfo["Ended"])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500246
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500247 if "Child rusage ru_utime" in statinfo:
248 cpu_time_user = cpu_time_user + stat_to_float(statinfo["Child rusage ru_utime"])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500249
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500250 if "Child rusage ru_stime" in statinfo:
251 cpu_time_system = cpu_time_system + stat_to_float(statinfo["Child rusage ru_stime"])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500252
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500253 if "IO write_bytes" in statinfo:
254 write_bytes = int(statinfo["IO write_bytes"].strip('% \n\r'))
255 disk_io_write = disk_io_write + write_bytes
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500256
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500257 if "IO read_bytes" in statinfo:
258 read_bytes = int(statinfo["IO read_bytes"].strip('% \n\r'))
259 disk_io_read = disk_io_read + read_bytes
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500260
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500261 return {
262 'stat_file': filename,
263 'cpu_time_user': cpu_time_user,
264 'cpu_time_system': cpu_time_system,
265 'disk_io_read': disk_io_read,
266 'disk_io_write': disk_io_write,
267 'started': started,
268 'ended': ended
269 }
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500270
271 if isinstance(e, (bb.build.TaskSucceeded, bb.build.TaskFailed)):
272 _append_read_list(e)
273 pass
274
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500275 if isinstance(e, bb.event.BuildCompleted) and os.path.exists(toaster_statlist_file):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500276 events = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500277 with open(toaster_statlist_file, "r") as fin:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500278 for line in fin:
279 (taskfile, taskname, filename, recipename) = line.strip().split("::")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500280 stats = _read_stats(filename)
281 events.append((taskfile, taskname, stats, recipename))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500282 bb.event.fire(bb.event.MetadataEvent("BuildStatsList", events), e.data)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500283 os.unlink(toaster_statlist_file)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500284}
285
286# dump relevant build history data as an event when the build is completed
287
288python toaster_buildhistory_dump() {
289 import re
290 BUILDHISTORY_DIR = e.data.expand("${TOPDIR}/buildhistory")
291 BUILDHISTORY_DIR_IMAGE_BASE = e.data.expand("%s/images/${MACHINE_ARCH}/${TCLIBC}/"% BUILDHISTORY_DIR)
292 pkgdata_dir = e.data.getVar("PKGDATA_DIR", True)
293
294
295 # scan the build targets for this build
296 images = {}
297 allpkgs = {}
298 files = {}
299 for target in e._pkgs:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500300 target = target.split(':')[0] # strip ':<task>' suffix from the target
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500301 installed_img_path = e.data.expand(os.path.join(BUILDHISTORY_DIR_IMAGE_BASE, target))
302 if os.path.exists(installed_img_path):
303 images[target] = {}
304 files[target] = {}
305 files[target]['dirs'] = []
306 files[target]['syms'] = []
307 files[target]['files'] = []
308 with open("%s/installed-package-sizes.txt" % installed_img_path, "r") as fin:
309 for line in fin:
310 line = line.rstrip(";")
311 psize, px = line.split("\t")
312 punit, pname = px.split(" ")
313 # this size is "installed-size" as it measures how much space it takes on disk
314 images[target][pname.strip()] = {'size':int(psize)*1024, 'depends' : []}
315
316 with open("%s/depends.dot" % installed_img_path, "r") as fin:
317 p = re.compile(r' -> ')
318 dot = re.compile(r'.*style=dotted')
319 for line in fin:
320 line = line.rstrip(';')
321 linesplit = p.split(line)
322 if len(linesplit) == 2:
323 pname = linesplit[0].rstrip('"').strip('"')
324 dependsname = linesplit[1].split(" ")[0].strip().strip(";").strip('"').rstrip('"')
325 deptype = "depends"
326 if dot.match(line):
327 deptype = "recommends"
328 if not pname in images[target]:
329 images[target][pname] = {'size': 0, 'depends' : []}
330 if not dependsname in images[target]:
331 images[target][dependsname] = {'size': 0, 'depends' : []}
332 images[target][pname]['depends'].append((dependsname, deptype))
333
334 with open("%s/files-in-image.txt" % installed_img_path, "r") as fin:
335 for line in fin:
336 lc = [ x for x in line.strip().split(" ") if len(x) > 0 ]
337 if lc[0].startswith("l"):
338 files[target]['syms'].append(lc)
339 elif lc[0].startswith("d"):
340 files[target]['dirs'].append(lc)
341 else:
342 files[target]['files'].append(lc)
343
344 for pname in images[target]:
345 if not pname in allpkgs:
346 try:
347 pkgdata = _toaster_load_pkgdatafile("%s/runtime-reverse/" % pkgdata_dir, pname)
348 except IOError as err:
349 if err.errno == 2:
350 # We expect this e.g. for RRECOMMENDS that are unsatisfied at runtime
351 continue
352 else:
353 raise
354 allpkgs[pname] = pkgdata
355
356
357 data = { 'pkgdata' : allpkgs, 'imgdata' : images, 'filedata' : files }
358
359 bb.event.fire(bb.event.MetadataEvent("ImagePkgList", data), e.data)
360
361}
362
363# dump information related to license manifest path
364
365python toaster_licensemanifest_dump() {
366 deploy_dir = d.getVar('DEPLOY_DIR', True);
367 image_name = d.getVar('IMAGE_NAME', True);
368
369 data = { 'deploy_dir' : deploy_dir, 'image_name' : image_name }
370
371 bb.event.fire(bb.event.MetadataEvent("LicenseManifestPath", data), d)
372}
373
374# set event handlers
375addhandler toaster_layerinfo_dumpdata
376toaster_layerinfo_dumpdata[eventmask] = "bb.event.TreeDataPreparationCompleted"
377
378addhandler toaster_collect_task_stats
379toaster_collect_task_stats[eventmask] = "bb.event.BuildCompleted bb.build.TaskSucceeded bb.build.TaskFailed"
380
381addhandler toaster_buildhistory_dump
382toaster_buildhistory_dump[eventmask] = "bb.event.BuildCompleted"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500383
384do_packagedata_setscene[postfuncs] += "toaster_package_dumpdata "
385do_packagedata_setscene[vardepsexclude] += "toaster_package_dumpdata "
386
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500387do_package[postfuncs] += "toaster_package_dumpdata "
388do_package[vardepsexclude] += "toaster_package_dumpdata "
389
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500390do_image_complete[postfuncs] += "toaster_image_dumpdata "
391do_image_complete[vardepsexclude] += "toaster_image_dumpdata "
392
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500393do_rootfs[postfuncs] += "toaster_licensemanifest_dump "
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500394do_rootfs[vardepsexclude] += "toaster_licensemanifest_dump "
395
396do_populate_sdk[postfuncs] += "toaster_artifact_dumpdata "
397do_populate_sdk[vardepsexclude] += "toaster_artifact_dumpdata "