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