bitbake/utils.py: Ensure the last lines of functions are printed in tracebacks
[bitbake.git] / lib / bb / utils.py
1 # ex:ts=4:sw=4:sts=4:et
2 # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3 """
4 BitBake Utility Functions
5 """
6
7 # Copyright (C) 2004 Michael Lauer
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License version 2 as
11 # published by the Free Software Foundation.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License along
19 # with this program; if not, write to the Free Software Foundation, Inc.,
20 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22 import re, fcntl, os, string, stat, shutil, time
23 import sys
24 import errno
25 import logging
26 import bb
27 import bb.msg
28 from commands import getstatusoutput
29 from contextlib import contextmanager
30
31 logger = logging.getLogger("BitBake.Util")
32
33 # Version comparison
34 separators = ".-"
35
36 # Context used in better_exec, eval
37 _context = {
38     "os": os,
39     "bb": bb,
40     "time": time,
41 }
42
43 def explode_version(s):
44     r = []
45     alpha_regexp = re.compile('^([a-zA-Z]+)(.*)$')
46     numeric_regexp = re.compile('^(\d+)(.*)$')
47     while (s != ''):
48         if s[0] in string.digits:
49             m = numeric_regexp.match(s)
50             r.append(int(m.group(1)))
51             s = m.group(2)
52             continue
53         if s[0] in string.letters:
54             m = alpha_regexp.match(s)
55             r.append(m.group(1))
56             s = m.group(2)
57             continue
58         r.append(s[0])
59         s = s[1:]
60     return r
61
62 def vercmp_part(a, b):
63     va = explode_version(a)
64     vb = explode_version(b)
65     sa = False
66     sb = False
67     while True:
68         if va == []:
69             ca = None
70         else:
71             ca = va.pop(0)
72         if vb == []:
73             cb = None
74         else:
75             cb = vb.pop(0)
76         if ca == None and cb == None:
77             return 0
78
79         if isinstance(ca, basestring):
80             sa = ca in separators
81         if isinstance(cb, basestring):
82             sb = cb in separators
83         if sa and not sb:
84             return -1
85         if not sa and sb:
86             return 1
87
88         if ca > cb:
89             return 1
90         if ca < cb:
91             return -1
92
93 def vercmp(ta, tb):
94     (ea, va, ra) = ta
95     (eb, vb, rb) = tb
96
97     r = int(ea or 0) - int(eb or 0)
98     if (r == 0):
99         r = vercmp_part(va, vb)
100     if (r == 0):
101         r = vercmp_part(ra, rb)
102     return r
103
104 _package_weights_ = {"pre":-2, "p":0, "alpha":-4, "beta":-3, "rc":-1}    # dicts are unordered
105 _package_ends_ = ["pre", "p", "alpha", "beta", "rc", "cvs", "bk", "HEAD" ]            # so we need ordered list
106
107 def relparse(myver):
108     """Parses the last elements of a version number into a triplet, that can
109     later be compared.
110     """
111
112     number = 0
113     p1 = 0
114     p2 = 0
115     mynewver = myver.split('_')
116     if len(mynewver) == 2:
117         # an _package_weights_
118         number = float(mynewver[0])
119         match = 0
120         for x in _package_ends_:
121             elen = len(x)
122             if mynewver[1][:elen] == x:
123                 match = 1
124                 p1 = _package_weights_[x]
125                 try:
126                     p2 = float(mynewver[1][elen:])
127                 except:
128                     p2 = 0
129                 break
130         if not match:
131             # normal number or number with letter at end
132             divider = len(myver)-1
133             if myver[divider:] not in "1234567890":
134                 # letter at end
135                 p1 = ord(myver[divider:])
136                 number = float(myver[0:divider])
137             else:
138                 number = float(myver)
139     else:
140         # normal number or number with letter at end
141         divider = len(myver)-1
142         if myver[divider:] not in "1234567890":
143             #letter at end
144             p1 = ord(myver[divider:])
145             number = float(myver[0:divider])
146         else:
147             number = float(myver)
148     return [number, p1, p2]
149
150 __vercmp_cache__ = {}
151
152 def vercmp_string(val1, val2):
153     """This takes two version strings and returns an integer to tell you whether
154     the versions are the same, val1>val2 or val2>val1.
155     """
156
157     # quick short-circuit
158     if val1 == val2:
159         return 0
160     valkey = val1 + " " + val2
161
162     # cache lookup
163     try:
164         return __vercmp_cache__[valkey]
165         try:
166             return - __vercmp_cache__[val2 + " " + val1]
167         except KeyError:
168             pass
169     except KeyError:
170         pass
171
172     # consider 1_p2 vc 1.1
173     # after expansion will become (1_p2,0) vc (1,1)
174     # then 1_p2 is compared with 1 before 0 is compared with 1
175     # to solve the bug we need to convert it to (1,0_p2)
176     # by splitting _prepart part and adding it back _after_expansion
177
178     val1_prepart = val2_prepart = ''
179     if val1.count('_'):
180         val1, val1_prepart = val1.split('_', 1)
181     if val2.count('_'):
182         val2, val2_prepart = val2.split('_', 1)
183
184     # replace '-' by '.'
185     # FIXME: Is it needed? can val1/2 contain '-'?
186
187     val1 = val1.split("-")
188     if len(val1) == 2:
189         val1[0] = val1[0] + "." + val1[1]
190     val2 = val2.split("-")
191     if len(val2) == 2:
192         val2[0] = val2[0] + "." + val2[1]
193
194     val1 = val1[0].split('.')
195     val2 = val2[0].split('.')
196
197     # add back decimal point so that .03 does not become "3" !
198     for x in xrange(1, len(val1)):
199         if val1[x][0] == '0' :
200             val1[x] = '.' + val1[x]
201     for x in xrange(1, len(val2)):
202         if val2[x][0] == '0' :
203             val2[x] = '.' + val2[x]
204
205     # extend varion numbers
206     if len(val2) < len(val1):
207         val2.extend(["0"]*(len(val1)-len(val2)))
208     elif len(val1) < len(val2):
209         val1.extend(["0"]*(len(val2)-len(val1)))
210
211     # add back _prepart tails
212     if val1_prepart:
213         val1[-1] += '_' + val1_prepart
214     if val2_prepart:
215         val2[-1] += '_' + val2_prepart
216     # The above code will extend version numbers out so they
217     # have the same number of digits.
218     for x in xrange(0, len(val1)):
219         cmp1 = relparse(val1[x])
220         cmp2 = relparse(val2[x])
221         for y in xrange(0, 3):
222             myret = cmp1[y] - cmp2[y]
223             if myret != 0:
224                 __vercmp_cache__[valkey] = myret
225                 return myret
226     __vercmp_cache__[valkey] = 0
227     return 0
228
229 def explode_deps(s):
230     """
231     Take an RDEPENDS style string of format:
232     "DEPEND1 (optional version) DEPEND2 (optional version) ..."
233     and return a list of dependencies.
234     Version information is ignored.
235     """
236     r = []
237     l = s.split()
238     flag = False
239     for i in l:
240         if i[0] == '(':
241             flag = True
242             #j = []
243         if not flag:
244             r.append(i)
245         #else:
246         #    j.append(i)
247         if flag and i.endswith(')'):
248             flag = False
249             # Ignore version
250             #r[-1] += ' ' + ' '.join(j)
251     return r
252
253 def explode_dep_versions(s):
254     """
255     Take an RDEPENDS style string of format:
256     "DEPEND1 (optional version) DEPEND2 (optional version) ..."
257     and return a dictionary of dependencies and versions.
258     """
259     r = {}
260     l = s.replace(",", "").split()
261     lastdep = None
262     lastver = ""
263     inversion = False
264     for i in l:
265         if i[0] == '(':
266             inversion = True
267             lastver = i[1:] or ""
268             #j = []
269         elif inversion and i.endswith(')'):
270             inversion = False
271             lastver = lastver + " " + (i[:-1] or "")
272             r[lastdep] = lastver
273         elif not inversion:
274             r[i] = None
275             lastdep = i
276             lastver = ""
277         elif inversion:
278             lastver = lastver + " " + i
279
280     return r
281
282 def join_deps(deps):
283     """
284     Take the result from explode_dep_versions and generate a dependency string
285     """
286     result = []
287     for dep in deps:
288         if deps[dep]:
289             result.append(dep + " (" + deps[dep] + ")")
290         else:
291             result.append(dep)
292     return ", ".join(result)
293
294 def _print_trace(body, line):
295     """
296     Print the Environment of a Text Body
297     """
298     # print the environment of the method
299     min_line = max(1, line-4)
300     max_line = min(line + 4, len(body))
301     for i in xrange(min_line, max_line + 1):
302         if line == i:
303             logger.error(' *** %.4d:%s', i, body[i-1])
304         else:
305             logger.error('     %.4d:%s', i, body[i-1])
306
307 def better_compile(text, file, realfile, mode = "exec"):
308     """
309     A better compile method. This method
310     will print  the offending lines.
311     """
312     try:
313         return compile(text, file, mode)
314     except Exception as e:
315         # split the text into lines again
316         body = text.split('\n')
317         logger.error("Error in compiling python function in %s", realfile)
318         logger.error(str(e))
319         if e.lineno:
320             logger.error("The lines leading to this error were:")
321             logger.error("\t%d:%s:'%s'", e.lineno, e.__class__.__name__, body[e.lineno-1])
322             _print_trace(body, e.lineno)
323         else:
324             logger.error("The function causing this error was:")
325             for line in body:
326                 logger.error(line)
327
328         raise
329
330 def better_exec(code, context, text, realfile = "<code>"):
331     """
332     Similiar to better_compile, better_exec will
333     print the lines that are responsible for the
334     error.
335     """
336     import bb.parse
337     if not hasattr(code, "co_filename"):
338         code = better_compile(code, realfile, realfile)
339     try:
340         exec(code, _context, context)
341     except Exception:
342         (t, value, tb) = sys.exc_info()
343
344         if t in [bb.parse.SkipPackage, bb.build.FuncFailed]:
345             raise
346
347         logger.error('There was an error when executing a python function in: %s', realfile)
348
349         # Strip 'us' from the stack (better_exec call)
350         tb = tb.tb_next
351
352         import traceback
353         textarray = text.split('\n')
354         linefailed = traceback.tb_lineno(tb)
355
356         tbextract = traceback.extract_tb(tb)
357         tbformat = "\n".join(traceback.format_list(tbextract))
358         logger.error("The stack trace of python calls that resulted in thie exception/failure was:")
359         for line in tbformat.split('\n'):
360             logger.error(line)
361
362         logger.error("The code that was being executed was:")
363         _print_trace(textarray, linefailed)
364         logger.error("(file: '%s', lineno: %s, function: %s)", tbextract[0][0], tbextract[0][1], tbextract[0][2])
365
366         # See if this is a function we constructed and has calls back into other functions in
367         # "text". If so, try and improve the context of the error by diving down the trace
368         level = 0
369         nexttb = tb.tb_next
370         while nexttb is not None:
371             if tbextract[level][0] == tbextract[level+1][0] and tbextract[level+1][2] == tbextract[level][0]:
372                 _print_trace(textarray, tbextract[level+1][1])
373                 logger.error("(file: '%s', lineno: %s, function: %s)", tbextract[level+1][0], tbextract[level+1][1], tbextract[level+1][2])
374             else:
375                  break
376             nexttb = tb.tb_next
377             level = level + 1
378
379         raise
380
381 def simple_exec(code, context):
382     exec(code, _context, context)
383
384 def better_eval(source, locals):
385     return eval(source, _context, locals)
386
387 @contextmanager
388 def fileslocked(files):
389     """Context manager for locking and unlocking file locks."""
390     locks = []
391     if files:
392         for lockfile in files:
393             locks.append(bb.utils.lockfile(lockfile))
394
395     yield
396
397     for lock in locks:
398         bb.utils.unlockfile(lock)
399
400 def lockfile(name):
401     """
402     Use the file fn as a lock file, return when the lock has been acquired.
403     Returns a variable to pass to unlockfile().
404     """
405     path = os.path.dirname(name)
406     if not os.path.isdir(path):
407         logger.error("Lockfile destination directory '%s' does not exist", path)
408         sys.exit(1)
409
410     while True:
411         # If we leave the lockfiles lying around there is no problem
412         # but we should clean up after ourselves. This gives potential
413         # for races though. To work around this, when we acquire the lock
414         # we check the file we locked was still the lock file on disk.
415         # by comparing inode numbers. If they don't match or the lockfile
416         # no longer exists, we start again.
417
418         # This implementation is unfair since the last person to request the
419         # lock is the most likely to win it.
420
421         try:
422             lf = open(name, 'a+')
423             fileno = lf.fileno()
424             fcntl.flock(fileno, fcntl.LOCK_EX)
425             statinfo = os.fstat(fileno)
426             if os.path.exists(lf.name):
427                 statinfo2 = os.stat(lf.name)
428                 if statinfo.st_ino == statinfo2.st_ino:
429                     return lf
430             lf.close()
431         except Exception:
432             continue
433
434 def unlockfile(lf):
435     """
436     Unlock a file locked using lockfile()
437     """
438     os.unlink(lf.name)
439     fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
440     lf.close()
441
442 def md5_file(filename):
443     """
444     Return the hex string representation of the MD5 checksum of filename.
445     """
446     try:
447         import hashlib
448         m = hashlib.md5()
449     except ImportError:
450         import md5
451         m = md5.new()
452
453     for line in open(filename):
454         m.update(line)
455     return m.hexdigest()
456
457 def sha256_file(filename):
458     """
459     Return the hex string representation of the 256-bit SHA checksum of
460     filename.  On Python 2.4 this will return None, so callers will need to
461     handle that by either skipping SHA checks, or running a standalone sha256sum
462     binary.
463     """
464     try:
465         import hashlib
466     except ImportError:
467         return None
468
469     s = hashlib.sha256()
470     for line in open(filename):
471         s.update(line)
472     return s.hexdigest()
473
474 def preserved_envvars_list():
475     return [
476         'BBPATH',
477         'BB_PRESERVE_ENV',
478         'BB_ENV_WHITELIST',
479         'BB_ENV_EXTRAWHITE',
480         'COLORTERM',
481         'DBUS_SESSION_BUS_ADDRESS',
482         'DESKTOP_SESSION',
483         'DESKTOP_STARTUP_ID',
484         'DISPLAY',
485         'GNOME_KEYRING_PID',
486         'GNOME_KEYRING_SOCKET',
487         'GPG_AGENT_INFO',
488         'GTK_RC_FILES',
489         'HOME',
490         'LANG',
491         'LOGNAME',
492         'PATH',
493         'PWD',
494         'SESSION_MANAGER',
495         'SHELL',
496         'SSH_AUTH_SOCK',
497         'TERM',
498         'USER',
499         'USERNAME',
500         '_',
501         'XAUTHORITY',
502         'XDG_DATA_DIRS',
503         'XDG_SESSION_COOKIE',
504     ]
505
506 def filter_environment(good_vars):
507     """
508     Create a pristine environment for bitbake. This will remove variables that
509     are not known and may influence the build in a negative way.
510     """
511
512     removed_vars = []
513     for key in os.environ.keys():
514         if key in good_vars:
515             continue
516
517         removed_vars.append(key)
518         os.unsetenv(key)
519         del os.environ[key]
520
521     if len(removed_vars):
522         logger.debug(1, "Removed the following variables from the environment: %s", ", ".join(removed_vars))
523
524     return removed_vars
525
526 def clean_environment():
527     """
528     Clean up any spurious environment variables. This will remove any
529     variables the user hasn't chose to preserve.
530     """
531     if 'BB_PRESERVE_ENV' not in os.environ:
532         if 'BB_ENV_WHITELIST' in os.environ:
533             good_vars = os.environ['BB_ENV_WHITELIST'].split()
534         else:
535             good_vars = preserved_envvars_list()
536         if 'BB_ENV_EXTRAWHITE' in os.environ:
537             good_vars.extend(os.environ['BB_ENV_EXTRAWHITE'].split())
538         filter_environment(good_vars)
539
540 def empty_environment():
541     """
542     Remove all variables from the environment.
543     """
544     for s in os.environ.keys():
545         os.unsetenv(s)
546         del os.environ[s]
547
548 def build_environment(d):
549     """
550     Build an environment from all exported variables.
551     """
552     import bb.data
553     for var in bb.data.keys(d):
554         export = bb.data.getVarFlag(var, "export", d)
555         if export:
556             os.environ[var] = bb.data.getVar(var, d, True) or ""
557
558 def remove(path, recurse=False):
559     """Equivalent to rm -f or rm -rf"""
560     import os, errno, shutil
561     try:
562         os.unlink(path)
563     except OSError, exc:
564         if recurse and exc.errno == errno.EISDIR:
565             shutil.rmtree(path)
566         elif exc.errno != errno.ENOENT:
567             raise
568
569 def prunedir(topdir):
570     # Delete everything reachable from the directory named in 'topdir'.
571     # CAUTION:  This is dangerous!
572     for root, dirs, files in os.walk(topdir, topdown = False):
573         for name in files:
574             os.remove(os.path.join(root, name))
575         for name in dirs:
576             if os.path.islink(os.path.join(root, name)):
577                 os.remove(os.path.join(root, name))
578             else:
579                 os.rmdir(os.path.join(root, name))
580     os.rmdir(topdir)
581
582 #
583 # Could also use return re.compile("(%s)" % "|".join(map(re.escape, suffixes))).sub(lambda mo: "", var)
584 # but thats possibly insane and suffixes is probably going to be small
585 #
586 def prune_suffix(var, suffixes, d):
587     # See if var ends with any of the suffixes listed and
588     # remove it if found
589     for suffix in suffixes:
590         if var.endswith(suffix):
591             return var.replace(suffix, "")
592     return var
593
594 def mkdirhier(directory):
595     """Create a directory like 'mkdir -p', but does not complain if
596     directory already exists like os.makedirs
597     """
598
599     try:
600         os.makedirs(directory)
601     except OSError as e:
602         if e.errno != errno.EEXIST:
603             raise e
604
605 def movefile(src, dest, newmtime = None, sstat = None):
606     """Moves a file from src to dest, preserving all permissions and
607     attributes; mtime will be preserved even when moving across
608     filesystems.  Returns true on success and false on failure. Move is
609     atomic.
610     """
611
612     #print "movefile(" + src + "," + dest + "," + str(newmtime) + "," + str(sstat) + ")"
613     try:
614         if not sstat:
615             sstat = os.lstat(src)
616     except Exception as e:
617         print("movefile: Stating source file failed...", e)
618         return None
619
620     destexists = 1
621     try:
622         dstat = os.lstat(dest)
623     except:
624         dstat = os.lstat(os.path.dirname(dest))
625         destexists = 0
626
627     if destexists:
628         if stat.S_ISLNK(dstat[stat.ST_MODE]):
629             try:
630                 os.unlink(dest)
631                 destexists = 0
632             except Exception as e:
633                 pass
634
635     if stat.S_ISLNK(sstat[stat.ST_MODE]):
636         try:
637             target = os.readlink(src)
638             if destexists and not stat.S_ISDIR(dstat[stat.ST_MODE]):
639                 os.unlink(dest)
640             os.symlink(target, dest)
641             #os.lchown(dest,sstat[stat.ST_UID],sstat[stat.ST_GID])
642             os.unlink(src)
643             return os.lstat(dest)
644         except Exception as e:
645             print("movefile: failed to properly create symlink:", dest, "->", target, e)
646             return None
647
648     renamefailed = 1
649     if sstat[stat.ST_DEV] == dstat[stat.ST_DEV]:
650         try:
651             os.rename(src, dest)
652             renamefailed = 0
653         except Exception as e:
654             if e[0] != errno.EXDEV:
655                 # Some random error.
656                 print("movefile: Failed to move", src, "to", dest, e)
657                 return None
658             # Invalid cross-device-link 'bind' mounted or actually Cross-Device
659
660     if renamefailed:
661         didcopy = 0
662         if stat.S_ISREG(sstat[stat.ST_MODE]):
663             try: # For safety copy then move it over.
664                 shutil.copyfile(src, dest + "#new")
665                 os.rename(dest + "#new", dest)
666                 didcopy = 1
667             except Exception as e:
668                 print('movefile: copy', src, '->', dest, 'failed.', e)
669                 return None
670         else:
671             #we don't yet handle special, so we need to fall back to /bin/mv
672             a = getstatusoutput("/bin/mv -f " + "'" + src + "' '" + dest + "'")
673             if a[0] != 0:
674                 print("movefile: Failed to move special file:" + src + "' to '" + dest + "'", a)
675                 return None # failure
676         try:
677             if didcopy:
678                 os.lchown(dest, sstat[stat.ST_UID], sstat[stat.ST_GID])
679                 os.chmod(dest, stat.S_IMODE(sstat[stat.ST_MODE])) # Sticky is reset on chown
680                 os.unlink(src)
681         except Exception as e:
682             print("movefile: Failed to chown/chmod/unlink", dest, e)
683             return None
684
685     if newmtime:
686         os.utime(dest, (newmtime, newmtime))
687     else:
688         os.utime(dest, (sstat[stat.ST_ATIME], sstat[stat.ST_MTIME]))
689         newmtime = sstat[stat.ST_MTIME]
690     return newmtime
691
692 def copyfile(src, dest, newmtime = None, sstat = None):
693     """
694     Copies a file from src to dest, preserving all permissions and
695     attributes; mtime will be preserved even when moving across
696     filesystems.  Returns true on success and false on failure.
697     """
698     #print "copyfile(" + src + "," + dest + "," + str(newmtime) + "," + str(sstat) + ")"
699     try:
700         if not sstat:
701             sstat = os.lstat(src)
702     except Exception as e:
703         print("copyfile: Stating source file failed...", e)
704         return False
705
706     destexists = 1
707     try:
708         dstat = os.lstat(dest)
709     except:
710         dstat = os.lstat(os.path.dirname(dest))
711         destexists = 0
712
713     if destexists:
714         if stat.S_ISLNK(dstat[stat.ST_MODE]):
715             try:
716                 os.unlink(dest)
717                 destexists = 0
718             except Exception as e:
719                 pass
720
721     if stat.S_ISLNK(sstat[stat.ST_MODE]):
722         try:
723             target = os.readlink(src)
724             if destexists and not stat.S_ISDIR(dstat[stat.ST_MODE]):
725                 os.unlink(dest)
726             os.symlink(target, dest)
727             #os.lchown(dest,sstat[stat.ST_UID],sstat[stat.ST_GID])
728             return os.lstat(dest)
729         except Exception as e:
730             print("copyfile: failed to properly create symlink:", dest, "->", target, e)
731             return False
732
733     if stat.S_ISREG(sstat[stat.ST_MODE]):
734         try: # For safety copy then move it over.
735             shutil.copyfile(src, dest + "#new")
736             os.rename(dest + "#new", dest)
737         except Exception as e:
738             print('copyfile: copy', src, '->', dest, 'failed.', e)
739             return False
740         finally:
741             os.chmod(src, sstat[stat.ST_MODE])
742             os.utime(src, (sstat[stat.ST_ATIME], sstat[stat.ST_MTIME]))
743
744     else:
745         #we don't yet handle special, so we need to fall back to /bin/mv
746         a = getstatusoutput("/bin/cp -f " + "'" + src + "' '" + dest + "'")
747         if a[0] != 0:
748             print("copyfile: Failed to copy special file:" + src + "' to '" + dest + "'", a)
749             return False # failure
750     try:
751         os.lchown(dest, sstat[stat.ST_UID], sstat[stat.ST_GID])
752         os.chmod(dest, stat.S_IMODE(sstat[stat.ST_MODE])) # Sticky is reset on chown
753     except Exception as e:
754         print("copyfile: Failed to chown/chmod/unlink", dest, e)
755         return False
756
757     if newmtime:
758         os.utime(dest, (newmtime, newmtime))
759     else:
760         os.utime(dest, (sstat[stat.ST_ATIME], sstat[stat.ST_MTIME]))
761         newmtime = sstat[stat.ST_MTIME]
762     return newmtime
763
764 def which(path, item, direction = 0):
765     """
766     Locate a file in a PATH
767     """
768
769     paths = (path or "").split(':')
770     if direction != 0:
771         paths.reverse()
772
773     for p in paths:
774         next = os.path.join(p, item)
775         if os.path.exists(next):
776             return next
777
778     return ""
779
780 def init_logger(logger, verbose, debug, debug_domains):
781     """
782     Set verbosity and debug levels in the logger
783     """
784
785     if debug:
786         bb.msg.set_debug_level(debug)
787     elif verbose:
788         bb.msg.set_verbose(True)
789     else:
790         bb.msg.set_debug_level(0)
791
792     if debug_domains:
793         bb.msg.set_debug_domains(debug_domains)
794
795 @contextmanager
796 def redirected_fds(from_files, to_files):
797     assert len(from_files) == len(to_files), 'from_files / to_files length mismatch'
798
799     old_fds = []
800     for position, fobj in enumerate(from_files):
801         fd = fobj.fileno()
802         old_fds.append(os.dup(fd))
803         os.dup2(to_files[position].fileno(), fd)
804
805     yield
806
807     for position, fd in enumerate(old_fds):
808         os.dup2(fd, from_files[position].fileno())