bitbake.conf: use rootfs/${PN} for IMAGE_ROOTFS
[vuplus_openembedded] / classes / insane.bbclass
1 # BB Class inspired by ebuild.sh
2 #
3 # This class will test files after installation for certain
4 # security issues and other kind of issues.
5 #
6 # Checks we do:
7 #  -Check the ownership and permissions
8 #  -Check the RUNTIME path for the $TMPDIR
9 #  -Check if .la files wrongly point to workdir
10 #  -Check if .pc files wrongly point to workdir
11 #  -Check if packages contains .debug directories or .so files
12 #   where they should be in -dev or -dbg
13 #  -Check if config.log contains traces to broken autoconf tests
14
15
16 #
17 # We need to have the scanelf utility as soon as
18 # possible and this is contained within the pax-utils-native.
19 # The package.bbclass can help us here.
20 #
21 inherit package
22 PACKAGE_DEPENDS += "pax-utils-native desktop-file-utils-native"
23 PACKAGEFUNCS += " do_package_qa "
24
25
26 #
27 # dictionary for elf headers
28 #
29 # feel free to add and correct.
30 #
31 #           TARGET_OS  TARGET_ARCH   MACHINE, OSABI, ABIVERSION, Little Endian, 32bit?
32 def package_qa_get_machine_dict():
33     return {
34             "darwin9" : { 
35                         "arm" :       (40,     0,    0,          True,          True),
36                       },
37             "linux" : { 
38                         "arm" :       (40,    97,    0,          True,          True),
39                         "armeb":      (40,    97,    0,          False,         True),
40                         "powerpc":    (20,     0,    0,          False,         True),
41                         "i386":       ( 3,     0,    0,          True,          True),
42                         "i486":       ( 3,     0,    0,          True,          True),
43                         "i586":       ( 3,     0,    0,          True,          True),
44                         "i686":       ( 3,     0,    0,          True,          True),
45                         "x86_64":     (62,     0,    0,          True,          False),
46                         "ia64":       (50,     0,    0,          True,          False),
47                         "alpha":      (36902,  0,    0,          True,          False),
48                         "hppa":       (15,     3,    0,          False,         True),
49                         "m68k":       ( 4,     0,    0,          False,         True),
50                         "mips":       ( 8,     0,    0,          False,         True),
51                         "mipsel":     ( 8,     0,    0,          True,          True),
52                         "s390":       (22,     0,    0,          False,         True),
53                         "sh4":        (42,     0,    0,          True,          True),
54                         "sparc":      ( 2,     0,    0,          False,         True),
55                       },
56             "linux-uclibc" : { 
57                         "arm" :       (  40,    97,    0,          True,          True),
58                         "armeb":      (  40,    97,    0,          False,         True),
59                         "powerpc":    (  20,     0,    0,          False,         True),
60                         "i386":       (   3,     0,    0,          True,          True),
61                         "i486":       (   3,     0,    0,          True,          True),
62                         "i586":       (   3,     0,    0,          True,          True),
63                         "i686":       (   3,     0,    0,          True,          True),
64                         "mipsel":     (   8,     0,    0,          True,          True),
65                         "avr32":      (6317,     0,    0,          False,         True),
66                         "sh4":        (42,       0,    0,          True,          True),
67
68                       },
69             "uclinux-uclibc" : {
70                         "bfin":       ( 106,     0,    0,          True,         True),
71                       }, 
72             "linux-gnueabi" : {
73                         "arm" :       (40,     0,    0,          True,          True),
74                         "armeb" :     (40,     0,    0,          False,         True),
75                       },
76             "linux-uclibcgnueabi" : {
77                         "arm" :       (40,     0,    0,          True,          True),
78                         "armeb" :     (40,     0,    0,          False,         True),
79                       },
80             "linux-gnuspe" : {
81                         "powerpc":    (20,     0,    0,          False,         True),
82                       },
83
84        }
85
86 # factory for a class, embedded in a method
87 def package_qa_get_elf(path, bits32):
88     class ELFFile:
89         EI_NIDENT = 16
90
91         EI_CLASS      = 4
92         EI_DATA       = 5
93         EI_VERSION    = 6
94         EI_OSABI      = 7
95         EI_ABIVERSION = 8
96
97         # possible values for EI_CLASS
98         ELFCLASSNONE = 0
99         ELFCLASS32   = 1
100         ELFCLASS64   = 2
101
102         # possible value for EI_VERSION
103         EV_CURRENT   = 1
104
105         # possible values for EI_DATA
106         ELFDATANONE  = 0
107         ELFDATA2LSB  = 1
108         ELFDATA2MSB  = 2
109
110         def my_assert(self, expectation, result):
111             if not expectation == result:
112                 #print "'%x','%x' %s" % (ord(expectation), ord(result), self.name)
113                 raise Exception("This does not work as expected")
114
115         def __init__(self, name):
116             self.name = name
117
118         def open(self):
119             self.file = file(self.name, "r")
120             self.data = self.file.read(ELFFile.EI_NIDENT+4)
121
122             self.my_assert(len(self.data), ELFFile.EI_NIDENT+4)
123             self.my_assert(self.data[0], chr(0x7f) )
124             self.my_assert(self.data[1], 'E')
125             self.my_assert(self.data[2], 'L')
126             self.my_assert(self.data[3], 'F')
127             if bits32 :
128                 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS32))
129             else:
130                 self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS64))
131             self.my_assert(self.data[ELFFile.EI_VERSION], chr(ELFFile.EV_CURRENT) )
132
133             self.sex = self.data[ELFFile.EI_DATA]
134             if self.sex == chr(ELFFile.ELFDATANONE):
135                 raise Exception("self.sex == ELFDATANONE")
136             elif self.sex == chr(ELFFile.ELFDATA2LSB):
137                 self.sex = "<"
138             elif self.sex == chr(ELFFile.ELFDATA2MSB):
139                 self.sex = ">"
140             else:
141                 raise Exception("Unknown self.sex")
142
143         def osAbi(self):
144             return ord(self.data[ELFFile.EI_OSABI])
145
146         def abiVersion(self):
147             return ord(self.data[ELFFile.EI_ABIVERSION])
148
149         def isLittleEndian(self):
150             return self.sex == "<"
151
152         def isBigEngian(self):
153             return self.sex == ">"
154
155         def machine(self):
156             """
157             We know the sex stored in self.sex and we
158             know the position
159             """
160             import struct
161             (a,) = struct.unpack(self.sex+"H", self.data[18:20])
162             return a
163
164     return ELFFile(path)
165
166
167 # Known Error classes
168 # 0 - non dev contains .so
169 # 1 - package contains a dangerous RPATH
170 # 2 - package depends on debug package
171 # 3 - non dbg contains .so
172 # 4 - wrong architecture
173 # 5 - .la contains installed=yes or reference to the workdir
174 # 6 - .pc contains reference to /usr/include or workdir
175 # 7 - the desktop file is not valid
176 # 8 - .la contains reference to the workdir
177 # 9 - LDFLAGS ignored
178
179 def package_qa_clean_path(path,d):
180     """ Remove the common prefix from the path. In this case it is the TMPDIR"""
181     import bb
182     return path.replace(bb.data.getVar('TMPDIR',d,True),"")
183
184 def package_qa_make_fatal_error(error_class, name, path,d):
185     """
186     decide if an error is fatal
187
188     TODO: Load a whitelist of known errors
189     """
190     return not error_class in [0, 5, 7]
191
192 def package_qa_write_error(error_class, name, path, d):
193     """
194     Log the error
195     """
196     import bb, os
197     if not bb.data.getVar('QA_LOG', d):
198         bb.note("a QA error occured but will not be logged because QA_LOG is not set")
199         return
200
201     ERROR_NAMES =[
202         "non dev contains .so",
203         "package contains RPATH",
204         "package depends on debug package",
205         "non dbg contains .debug",
206         "wrong architecture",
207         "evil hides inside the .la",
208         "evil hides inside the .pc",
209         "the desktop file is not valid",
210         ".la contains reference to the workdir",
211         "LDFLAGS ignored",
212     ]
213
214     log_path = os.path.join( bb.data.getVar('T', d, True), "log.qa_package" )
215     f = file( log_path, "a+")
216     print >> f, "%s, %s, %s" % \
217              (ERROR_NAMES[error_class], name, package_qa_clean_path(path,d))
218     f.close()
219
220 def package_qa_handle_error(error_class, error_msg, name, path, d):
221     import bb
222     bb.error("QA Issue: %s" % error_msg)
223     package_qa_write_error(error_class, name, path, d)
224     return not package_qa_make_fatal_error(error_class, name, path, d)
225
226 def package_qa_check_rpath(file,name,d, elf):
227     """
228     Check for dangerous RPATHs
229     """
230     if not elf:
231         return True
232
233     import bb, os
234     sane = True
235     scanelf = os.path.join(bb.data.getVar('STAGING_BINDIR_NATIVE',d,True),'scanelf')
236     bad_dir = bb.data.getVar('TMPDIR', d, True) + "/work"
237     bad_dir_test = bb.data.getVar('TMPDIR', d, True)
238     if not os.path.exists(scanelf):
239         bb.fatal("Can not check RPATH, scanelf (part of pax-utils-native) not found")
240
241     if not bad_dir in bb.data.getVar('WORKDIR', d, True):
242         bb.fatal("This class assumed that WORKDIR is ${TMPDIR}/work... Not doing any check")
243
244     output = os.popen("%s -B -F%%r#F '%s'" % (scanelf,file))
245     txt    = output.readline().split()
246     for line in txt:
247         if bad_dir in line:
248             error_msg = "package %s contains bad RPATH %s in file %s" % (name, line, file)
249             sane = package_qa_handle_error(1, error_msg, name, file, d)
250
251     return sane
252
253 def package_qa_check_devdbg(path, name,d, elf):
254     """
255     Check for debug remains inside the binary or
256     non dev packages containing
257     """
258
259     import bb, os
260     sane = True
261
262     if not "-dev" in name:
263         if path[-3:] == ".so" and os.path.islink(path):
264             error_msg = "non -dev package contains symlink .so: %s path '%s'" % \
265                      (name, package_qa_clean_path(path,d))
266             sane = package_qa_handle_error(0, error_msg, name, path, d)
267
268     if not "-dbg" in name:
269         if '.debug' in path:
270             error_msg = "non debug package contains .debug directory: %s path %s" % \
271                      (name, package_qa_clean_path(path,d))
272             sane = package_qa_handle_error(3, error_msg, name, path, d)
273
274     return sane
275
276 def package_qa_check_perm(path,name,d, elf):
277     """
278     Check the permission of files
279     """
280     sane = True
281     return sane
282
283 def package_qa_check_arch(path,name,d, elf):
284     """
285     Check if archs are compatible
286     """
287     if not elf:
288         return True
289
290     import bb, os
291     sane = True
292     target_os   = bb.data.getVar('TARGET_OS',   d, True)
293     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
294
295     # FIXME: Cross package confuse this check, so just skip them
296     for s in ['cross', 'sdk', 'canadian-cross', 'canadian-sdk']:
297         if bb.data.inherits_class(s, d):
298             return True
299
300     # avoid following links to /usr/bin (e.g. on udev builds)
301     # we will check the files pointed to anyway...
302     if os.path.islink(path):
303         return True
304
305     #if this will throw an exception, then fix the dict above
306     (machine, osabi, abiversion, littleendian, bits32) \
307         = package_qa_get_machine_dict()[target_os][target_arch]
308
309     # Check the architecture and endiannes of the binary
310     if not machine == elf.machine():
311         error_msg = "Architecture did not match (%d to %d) on %s" % \
312                  (machine, elf.machine(), package_qa_clean_path(path,d))
313         sane = package_qa_handle_error(4, error_msg, name, path, d)
314     elif not littleendian == elf.isLittleEndian():
315         error_msg = "Endiannes did not match (%d to %d) on %s" % \
316                  (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d))
317         sane = package_qa_handle_error(4, error_msg, name, path, d)
318
319     return sane
320
321 def package_qa_check_desktop(path, name, d, elf):
322     """
323     Run all desktop files through desktop-file-validate.
324     """
325     import bb, os
326     sane = True
327     if path.endswith(".desktop"):
328         output = os.popen("desktop-file-validate %s" % path)
329         # This only produces output on errors
330         for l in output:
331             sane = package_qa_handle_error(7, l.strip(), name, path, d)
332
333     return sane
334
335 def package_qa_hash_style(path, name, d, elf):
336     """
337     Check if the binary has the right hash style...
338     """
339     import bb, os
340
341     if not elf:
342         return True
343
344     if os.path.islink(path):
345         return True
346
347     gnu_hash = "--hash-style=gnu" in bb.data.getVar('LDFLAGS', d, True)
348     if not gnu_hash:
349         gnu_hash = "--hash-style=both" in bb.data.getVar('LDFLAGS', d, True)
350     if not gnu_hash:
351         return True
352
353     objdump = bb.data.getVar('OBJDUMP', d, True)
354     env_path = bb.data.getVar('PATH', d, True)
355
356     sane = True
357     elf = False
358     # A bit hacky. We do not know if path is an elf binary or not
359     # we will search for 'NEEDED' or 'INIT' as this should be printed...
360     # and come before the HASH section (guess!!!) and works on split out
361     # debug symbols too
362     for line in os.popen("LC_ALL=C PATH=%s %s -p '%s' 2> /dev/null" % (env_path, objdump, path), "r"):
363         if "NEEDED" in line or "INIT" in line:
364             sane = False
365             elf = True
366         if "GNU_HASH" in line:
367             sane = True
368         if "[mips32]" in line or "[mips64]" in line:
369             sane = True
370
371     if elf and not sane:
372         error_msg = "No GNU_HASH in the elf binary: '%s'" % path
373         return package_qa_handle_error(9, error_msg, name, path, d)
374
375     return True
376
377 def package_qa_check_staged(path,d):
378     """
379     Check staged la and pc files for sanity
380       -e.g. installed being false
381
382         As this is run after every stage we should be able
383         to find the one responsible for the errors easily even
384         if we look at every .pc and .la file
385     """
386     import os, bb
387
388     sane = True
389     tmpdir = bb.data.getVar('TMPDIR', d, True)
390     workdir = os.path.join(tmpdir, "work")
391
392     installed = "installed=yes"
393     iscrossnative = False
394     pkgconfigcheck = tmpdir
395     for s in ['cross', 'native', 'canadian-cross', 'canadian-native']:
396         if bb.data.inherits_class(s, d):
397             pkgconfigcheck = workdir
398             iscrossnative = True
399
400     # find all .la and .pc files
401     # read the content
402     # and check for stuff that looks wrong
403     for root, dirs, files in os.walk(path):
404         for file in files:
405             path = os.path.join(root,file)
406             if file[-2:] == "la":
407                 file_content = open(path).read()
408                 # Don't check installed status for native/cross packages
409                 if not iscrossnative:
410                     if installed in file_content:
411                         error_msg = "%s failed sanity test (installed) in path %s" % (file,root)
412                         sane = package_qa_handle_error(5, error_msg, "staging", path, d)
413                 if workdir in file_content:
414                     error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
415                     sane = package_qa_handle_error(8, error_msg, "staging", path, d)
416             elif file[-2:] == "pc":
417                 file_content = open(path).read()
418                 if pkgconfigcheck in file_content:
419                     error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
420                     sane = package_qa_handle_error(6, error_msg, "staging", path, d)
421
422     return sane
423
424 # Walk over all files in a directory and call func
425 def package_qa_walk(path, funcs, package,d):
426     import bb, os
427     sane = True
428
429     #if this will throw an exception, then fix the dict above
430     target_os   = bb.data.getVar('TARGET_OS',   d, True)
431     target_arch = bb.data.getVar('TARGET_ARCH', d, True)
432     (machine, osabi, abiversion, littleendian, bits32) \
433         = package_qa_get_machine_dict()[target_os][target_arch]
434
435     for root, dirs, files in os.walk(path):
436         for file in files:
437             path = os.path.join(root,file)
438             elf = package_qa_get_elf(path, bits32)
439             try:
440                 elf.open()
441             except:
442                 elf = None
443             for func in funcs:
444                 if not func(path, package,d, elf):
445                     sane = False
446
447     return sane
448
449 def package_qa_check_rdepends(pkg, workdir, d):
450     import bb
451     sane = True
452     if not "-dbg" in pkg and not "task-" in pkg and not "-image" in pkg:
453         # Copied from package_ipk.bbclass
454         # boiler plate to update the data
455         localdata = bb.data.createCopy(d)
456         root = "%s/install/%s" % (workdir, pkg)
457
458         bb.data.setVar('ROOT', '', localdata) 
459         bb.data.setVar('ROOT_%s' % pkg, root, localdata)
460         pkgname = bb.data.getVar('PKG_%s' % pkg, localdata, True)
461         if not pkgname:
462             pkgname = pkg
463         bb.data.setVar('PKG', pkgname, localdata)
464
465         overrides = bb.data.getVar('OVERRIDES', localdata)
466         if not overrides:
467             raise bb.build.FuncFailed('OVERRIDES not defined')
468         overrides = bb.data.expand(overrides, localdata)
469         bb.data.setVar('OVERRIDES', overrides + ':' + pkg, localdata)
470
471         bb.data.update_data(localdata)
472
473         # Now check the RDEPENDS
474         rdepends = explode_deps(bb.data.getVar('RDEPENDS', localdata, True) or "")
475
476
477         # Now do the sanity check!!!
478         for rdepend in rdepends:
479             if "-dbg" in rdepend:
480                 error_msg = "%s rdepends on %s" % (pkgname,rdepend)
481                 sane = package_qa_handle_error(2, error_msg, pkgname, rdepend, d)
482
483     return sane
484
485 # The PACKAGE FUNC to scan each package
486 python do_package_qa () {
487     import bb
488     bb.note("DO PACKAGE QA")
489     workdir = bb.data.getVar('WORKDIR', d, True)
490     packages = bb.data.getVar('PACKAGES',d, True)
491
492     # no packages should be scanned
493     if not packages:
494         return
495
496     checks = [package_qa_check_rpath, package_qa_check_devdbg,
497               package_qa_check_perm, package_qa_check_arch,
498               package_qa_check_desktop, package_qa_hash_style]
499     walk_sane = True
500     rdepends_sane = True
501     for package in packages.split():
502         if bb.data.getVar('INSANE_SKIP_' + package, d, True):
503             bb.note("Package: %s (skipped)" % package)
504             continue
505
506         bb.note("Checking Package: %s" % package)
507         path = "%s/install/%s" % (workdir, package)
508         if not package_qa_walk(path, checks, package, d):
509             walk_sane  = False
510         if not package_qa_check_rdepends(package, workdir, d):
511             rdepends_sane = False
512
513     if not walk_sane or not rdepends_sane:
514         bb.fatal("QA run found fatal errors. Please consider fixing them.")
515     bb.note("DONE with PACKAGE QA")
516 }
517
518
519 # The Staging Func, to check all staging
520 addtask qa_staging after do_populate_staging before do_build
521 python do_qa_staging() {
522     bb.note("QA checking staging")
523
524     if not package_qa_check_staged(bb.data.getVar('STAGING_LIBDIR',d,True), d):
525         bb.fatal("QA staging was broken by the package built above")
526 }
527
528 # Check broken config.log files
529 addtask qa_configure after do_configure before do_compile
530 python do_qa_configure() {
531     bb.note("Checking sanity of the config.log file")
532     import os
533     for root, dirs, files in os.walk(bb.data.getVar('WORKDIR', d, True)):
534         statement = "grep 'CROSS COMPILE Badness:' %s > /dev/null" % \
535                     os.path.join(root,"config.log")
536         if "config.log" in files:
537             if os.system(statement) == 0:
538                 bb.fatal("""This autoconf log indicates errors, it looked at host includes.
539 Rerun configure task after fixing this. The path was '%s'""" % root)
540 }