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