1# Recipe creation tool - create command build system handlers
2#
3# Copyright (C) 2014-2016 Intel Corporation
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8import re
9import logging
10import glob
11from recipetool.create import RecipeHandler, validate_pv
12
13logger = logging.getLogger('recipetool')
14
15tinfoil = None
16plugins = None
17
18def plugin_init(pluginlist):
19    # Take a reference to the list so we can use it later
20    global plugins
21    plugins = pluginlist
22
23def tinfoil_init(instance):
24    global tinfoil
25    tinfoil = instance
26
27
28class CmakeRecipeHandler(RecipeHandler):
29    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
30        if 'buildsystem' in handled:
31            return False
32
33        if RecipeHandler.checkfiles(srctree, ['CMakeLists.txt']):
34            classes.append('cmake')
35            values = CmakeRecipeHandler.extract_cmake_deps(lines_before, srctree, extravalues)
36            classes.extend(values.pop('inherit', '').split())
37            for var, value in values.items():
38                lines_before.append('%s = "%s"' % (var, value))
39            lines_after.append('# Specify any options you want to pass to cmake using EXTRA_OECMAKE:')
40            lines_after.append('EXTRA_OECMAKE = ""')
41            lines_after.append('')
42            handled.append('buildsystem')
43            return True
44        return False
45
46    @staticmethod
47    def extract_cmake_deps(outlines, srctree, extravalues, cmakelistsfile=None):
48        # Find all plugins that want to register handlers
49        logger.debug('Loading cmake handlers')
50        handlers = []
51        for plugin in plugins:
52            if hasattr(plugin, 'register_cmake_handlers'):
53                plugin.register_cmake_handlers(handlers)
54
55        values = {}
56        inherits = []
57
58        if cmakelistsfile:
59            srcfiles = [cmakelistsfile]
60        else:
61            srcfiles = RecipeHandler.checkfiles(srctree, ['CMakeLists.txt'])
62
63        # Note that some of these are non-standard, but probably better to
64        # be able to map them anyway if we see them
65        cmake_pkgmap = {'alsa': 'alsa-lib',
66                        'aspell': 'aspell',
67                        'atk': 'atk',
68                        'bison': 'bison-native',
69                        'boost': 'boost',
70                        'bzip2': 'bzip2',
71                        'cairo': 'cairo',
72                        'cups': 'cups',
73                        'curl': 'curl',
74                        'curses': 'ncurses',
75                        'cvs': 'cvs',
76                        'drm': 'libdrm',
77                        'dbus': 'dbus',
78                        'dbusglib': 'dbus-glib',
79                        'egl': 'virtual/egl',
80                        'expat': 'expat',
81                        'flex': 'flex-native',
82                        'fontconfig': 'fontconfig',
83                        'freetype': 'freetype',
84                        'gettext': '',
85                        'git': '',
86                        'gio': 'glib-2.0',
87                        'giounix': 'glib-2.0',
88                        'glew': 'glew',
89                        'glib': 'glib-2.0',
90                        'glib2': 'glib-2.0',
91                        'glu': 'libglu',
92                        'glut': 'freeglut',
93                        'gobject': 'glib-2.0',
94                        'gperf': 'gperf-native',
95                        'gnutls': 'gnutls',
96                        'gtk2': 'gtk+',
97                        'gtk3': 'gtk+3',
98                        'gtk': 'gtk+3',
99                        'harfbuzz': 'harfbuzz',
100                        'icu': 'icu',
101                        'intl': 'virtual/libintl',
102                        'jpeg': 'jpeg',
103                        'libarchive': 'libarchive',
104                        'libiconv': 'virtual/libiconv',
105                        'liblzma': 'xz',
106                        'libxml2': 'libxml2',
107                        'libxslt': 'libxslt',
108                        'opengl': 'virtual/libgl',
109                        'openmp': '',
110                        'openssl': 'openssl',
111                        'pango': 'pango',
112                        'perl': '',
113                        'perllibs': '',
114                        'pkgconfig': '',
115                        'png': 'libpng',
116                        'pthread': '',
117                        'pythoninterp': '',
118                        'pythonlibs': '',
119                        'ruby': 'ruby-native',
120                        'sdl': 'libsdl',
121                        'sdl2': 'libsdl2',
122                        'subversion': 'subversion-native',
123                        'swig': 'swig-native',
124                        'tcl': 'tcl-native',
125                        'threads': '',
126                        'tiff': 'tiff',
127                        'wget': 'wget',
128                        'x11': 'libx11',
129                        'xcb': 'libxcb',
130                        'xext': 'libxext',
131                        'xfixes': 'libxfixes',
132                        'zlib': 'zlib',
133                        }
134
135        pcdeps = []
136        libdeps = []
137        deps = []
138        unmappedpkgs = []
139
140        proj_re = re.compile('project\s*\(([^)]*)\)', re.IGNORECASE)
141        pkgcm_re = re.compile('pkg_check_modules\s*\(\s*[a-zA-Z0-9-_]+\s*(REQUIRED)?\s+([^)\s]+)\s*\)', re.IGNORECASE)
142        pkgsm_re = re.compile('pkg_search_module\s*\(\s*[a-zA-Z0-9-_]+\s*(REQUIRED)?((\s+[^)\s]+)+)\s*\)', re.IGNORECASE)
143        findpackage_re = re.compile('find_package\s*\(\s*([a-zA-Z0-9-_]+)\s*.*', re.IGNORECASE)
144        findlibrary_re = re.compile('find_library\s*\(\s*[a-zA-Z0-9-_]+\s*(NAMES\s+)?([a-zA-Z0-9-_ ]+)\s*.*')
145        checklib_re = re.compile('check_library_exists\s*\(\s*([^\s)]+)\s*.*', re.IGNORECASE)
146        include_re = re.compile('include\s*\(\s*([^)\s]*)\s*\)', re.IGNORECASE)
147        subdir_re = re.compile('add_subdirectory\s*\(\s*([^)\s]*)\s*([^)\s]*)\s*\)', re.IGNORECASE)
148        dep_re = re.compile('([^ ><=]+)( *[<>=]+ *[^ ><=]+)?')
149
150        def find_cmake_package(pkg):
151            RecipeHandler.load_devel_filemap(tinfoil.config_data)
152            for fn, pn in RecipeHandler.recipecmakefilemap.items():
153                splitname = fn.split('/')
154                if len(splitname) > 1:
155                    if splitname[0].lower().startswith(pkg.lower()):
156                        if splitname[1] == '%s-config.cmake' % pkg.lower() or splitname[1] == '%sConfig.cmake' % pkg or splitname[1] == 'Find%s.cmake' % pkg:
157                            return pn
158            return None
159
160        def interpret_value(value):
161            return value.strip('"')
162
163        def parse_cmake_file(fn, paths=None):
164            searchpaths = (paths or []) + [os.path.dirname(fn)]
165            logger.debug('Parsing file %s' % fn)
166            with open(fn, 'r', errors='surrogateescape') as f:
167                for line in f:
168                    line = line.strip()
169                    for handler in handlers:
170                        if handler.process_line(srctree, fn, line, libdeps, pcdeps, deps, outlines, inherits, values):
171                            continue
172                    res = include_re.match(line)
173                    if res:
174                        includefn = bb.utils.which(':'.join(searchpaths), res.group(1))
175                        if includefn:
176                            parse_cmake_file(includefn, searchpaths)
177                        else:
178                            logger.debug('Unable to recurse into include file %s' % res.group(1))
179                        continue
180                    res = subdir_re.match(line)
181                    if res:
182                        subdirfn = os.path.join(os.path.dirname(fn), res.group(1), 'CMakeLists.txt')
183                        if os.path.exists(subdirfn):
184                            parse_cmake_file(subdirfn, searchpaths)
185                        else:
186                            logger.debug('Unable to recurse into subdirectory file %s' % subdirfn)
187                        continue
188                    res = proj_re.match(line)
189                    if res:
190                        extravalues['PN'] = interpret_value(res.group(1).split()[0])
191                        continue
192                    res = pkgcm_re.match(line)
193                    if res:
194                        res = dep_re.findall(res.group(2))
195                        if res:
196                            pcdeps.extend([interpret_value(x[0]) for x in res])
197                        inherits.append('pkgconfig')
198                        continue
199                    res = pkgsm_re.match(line)
200                    if res:
201                        res = dep_re.findall(res.group(2))
202                        if res:
203                            # Note: appending a tuple here!
204                            item = tuple((interpret_value(x[0]) for x in res))
205                            if len(item) == 1:
206                                item = item[0]
207                            pcdeps.append(item)
208                        inherits.append('pkgconfig')
209                        continue
210                    res = findpackage_re.match(line)
211                    if res:
212                        origpkg = res.group(1)
213                        pkg = interpret_value(origpkg)
214                        found = False
215                        for handler in handlers:
216                            if handler.process_findpackage(srctree, fn, pkg, deps, outlines, inherits, values):
217                                logger.debug('Mapped CMake package %s via handler %s' % (pkg, handler.__class__.__name__))
218                                found = True
219                                break
220                        if found:
221                            continue
222                        elif pkg == 'Gettext':
223                            inherits.append('gettext')
224                        elif pkg == 'Perl':
225                            inherits.append('perlnative')
226                        elif pkg == 'PkgConfig':
227                            inherits.append('pkgconfig')
228                        elif pkg == 'PythonInterp':
229                            inherits.append('python3native')
230                        elif pkg == 'PythonLibs':
231                            inherits.append('python3-dir')
232                        else:
233                            # Try to map via looking at installed CMake packages in pkgdata
234                            dep = find_cmake_package(pkg)
235                            if dep:
236                                logger.debug('Mapped CMake package %s to recipe %s via pkgdata' % (pkg, dep))
237                                deps.append(dep)
238                            else:
239                                dep = cmake_pkgmap.get(pkg.lower(), None)
240                                if dep:
241                                    logger.debug('Mapped CMake package %s to recipe %s via internal list' % (pkg, dep))
242                                    deps.append(dep)
243                                elif dep is None:
244                                    unmappedpkgs.append(origpkg)
245                        continue
246                    res = checklib_re.match(line)
247                    if res:
248                        lib = interpret_value(res.group(1))
249                        if not lib.startswith('$'):
250                            libdeps.append(lib)
251                    res = findlibrary_re.match(line)
252                    if res:
253                        libs = res.group(2).split()
254                        for lib in libs:
255                            if lib in ['HINTS', 'PATHS', 'PATH_SUFFIXES', 'DOC', 'NAMES_PER_DIR'] or lib.startswith(('NO_', 'CMAKE_', 'ONLY_CMAKE_')):
256                                break
257                            lib = interpret_value(lib)
258                            if not lib.startswith('$'):
259                                libdeps.append(lib)
260                    if line.lower().startswith('useswig'):
261                        deps.append('swig-native')
262                        continue
263
264        parse_cmake_file(srcfiles[0])
265
266        if unmappedpkgs:
267            outlines.append('# NOTE: unable to map the following CMake package dependencies: %s' % ' '.join(list(set(unmappedpkgs))))
268
269        RecipeHandler.handle_depends(libdeps, pcdeps, deps, outlines, values, tinfoil.config_data)
270
271        for handler in handlers:
272            handler.post_process(srctree, libdeps, pcdeps, deps, outlines, inherits, values)
273
274        if inherits:
275            values['inherit'] = ' '.join(list(set(inherits)))
276
277        return values
278
279
280class CmakeExtensionHandler(object):
281    '''Base class for CMake extension handlers'''
282    def process_line(self, srctree, fn, line, libdeps, pcdeps, deps, outlines, inherits, values):
283        '''
284        Handle a line parsed out of an CMake file.
285        Return True if you've completely handled the passed in line, otherwise return False.
286        '''
287        return False
288
289    def process_findpackage(self, srctree, fn, pkg, deps, outlines, inherits, values):
290        '''
291        Handle a find_package package parsed out of a CMake file.
292        Return True if you've completely handled the passed in package, otherwise return False.
293        '''
294        return False
295
296    def post_process(self, srctree, fn, pkg, deps, outlines, inherits, values):
297        '''
298        Apply any desired post-processing on the output
299        '''
300        return
301
302
303
304class SconsRecipeHandler(RecipeHandler):
305    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
306        if 'buildsystem' in handled:
307            return False
308
309        if RecipeHandler.checkfiles(srctree, ['SConstruct', 'Sconstruct', 'sconstruct']):
310            classes.append('scons')
311            lines_after.append('# Specify any options you want to pass to scons using EXTRA_OESCONS:')
312            lines_after.append('EXTRA_OESCONS = ""')
313            lines_after.append('')
314            handled.append('buildsystem')
315            return True
316        return False
317
318
319class QmakeRecipeHandler(RecipeHandler):
320    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
321        if 'buildsystem' in handled:
322            return False
323
324        if RecipeHandler.checkfiles(srctree, ['*.pro']):
325            classes.append('qmake2')
326            handled.append('buildsystem')
327            return True
328        return False
329
330
331class AutotoolsRecipeHandler(RecipeHandler):
332    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
333        if 'buildsystem' in handled:
334            return False
335
336        autoconf = False
337        if RecipeHandler.checkfiles(srctree, ['configure.ac', 'configure.in']):
338            autoconf = True
339            values = AutotoolsRecipeHandler.extract_autotools_deps(lines_before, srctree, extravalues)
340            classes.extend(values.pop('inherit', '').split())
341            for var, value in values.items():
342                lines_before.append('%s = "%s"' % (var, value))
343        else:
344            conffile = RecipeHandler.checkfiles(srctree, ['configure'])
345            if conffile:
346                # Check if this is just a pre-generated autoconf configure script
347                with open(conffile[0], 'r', errors='surrogateescape') as f:
348                    for i in range(1, 10):
349                        if 'Generated by GNU Autoconf' in f.readline():
350                            autoconf = True
351                            break
352
353        if autoconf and not ('PV' in extravalues and 'PN' in extravalues):
354            # Last resort
355            conffile = RecipeHandler.checkfiles(srctree, ['configure'])
356            if conffile:
357                with open(conffile[0], 'r', errors='surrogateescape') as f:
358                    for line in f:
359                        line = line.strip()
360                        if line.startswith('VERSION=') or line.startswith('PACKAGE_VERSION='):
361                            pv = line.split('=')[1].strip('"\'')
362                            if pv and not 'PV' in extravalues and validate_pv(pv):
363                                extravalues['PV'] = pv
364                        elif line.startswith('PACKAGE_NAME=') or line.startswith('PACKAGE='):
365                            pn = line.split('=')[1].strip('"\'')
366                            if pn and not 'PN' in extravalues:
367                                extravalues['PN'] = pn
368
369        if autoconf:
370            lines_before.append('')
371            lines_before.append('# NOTE: if this software is not capable of being built in a separate build directory')
372            lines_before.append('# from the source, you should replace autotools with autotools-brokensep in the')
373            lines_before.append('# inherit line')
374            classes.append('autotools')
375            lines_after.append('# Specify any options you want to pass to the configure script using EXTRA_OECONF:')
376            lines_after.append('EXTRA_OECONF = ""')
377            lines_after.append('')
378            handled.append('buildsystem')
379            return True
380
381        return False
382
383    @staticmethod
384    def extract_autotools_deps(outlines, srctree, extravalues=None, acfile=None):
385        import shlex
386
387        # Find all plugins that want to register handlers
388        logger.debug('Loading autotools handlers')
389        handlers = []
390        for plugin in plugins:
391            if hasattr(plugin, 'register_autotools_handlers'):
392                plugin.register_autotools_handlers(handlers)
393
394        values = {}
395        inherits = []
396
397        # Hardcoded map, we also use a dynamic one based on what's in the sysroot
398        progmap = {'flex': 'flex-native',
399                'bison': 'bison-native',
400                'm4': 'm4-native',
401                'tar': 'tar-native',
402                'ar': 'binutils-native',
403                'ranlib': 'binutils-native',
404                'ld': 'binutils-native',
405                'strip': 'binutils-native',
406                'libtool': '',
407                'autoconf': '',
408                'autoheader': '',
409                'automake': '',
410                'uname': '',
411                'rm': '',
412                'cp': '',
413                'mv': '',
414                'find': '',
415                'awk': '',
416                'sed': '',
417                }
418        progclassmap = {'gconftool-2': 'gconf',
419                'pkg-config': 'pkgconfig',
420                'python': 'python3native',
421                'python3': 'python3native',
422                'perl': 'perlnative',
423                'makeinfo': 'texinfo',
424                }
425
426        pkg_re = re.compile('PKG_CHECK_MODULES\(\s*\[?[a-zA-Z0-9_]*\]?,\s*\[?([^,\]]*)\]?[),].*')
427        pkgce_re = re.compile('PKG_CHECK_EXISTS\(\s*\[?([^,\]]*)\]?[),].*')
428        lib_re = re.compile('AC_CHECK_LIB\(\s*\[?([^,\]]*)\]?,.*')
429        libx_re = re.compile('AX_CHECK_LIBRARY\(\s*\[?[^,\]]*\]?,\s*\[?([^,\]]*)\]?,\s*\[?([a-zA-Z0-9-]*)\]?,.*')
430        progs_re = re.compile('_PROGS?\(\s*\[?[a-zA-Z0-9_]*\]?,\s*\[?([^,\]]*)\]?[),].*')
431        dep_re = re.compile('([^ ><=]+)( [<>=]+ [^ ><=]+)?')
432        ac_init_re = re.compile('AC_INIT\(\s*([^,]+),\s*([^,]+)[,)].*')
433        am_init_re = re.compile('AM_INIT_AUTOMAKE\(\s*([^,]+),\s*([^,]+)[,)].*')
434        define_re = re.compile('\s*(m4_)?define\(\s*([^,]+),\s*([^,]+)\)')
435        version_re = re.compile('([0-9.]+)')
436
437        defines = {}
438        def subst_defines(value):
439            newvalue = value
440            for define, defval in defines.items():
441                newvalue = newvalue.replace(define, defval)
442            if newvalue != value:
443                return subst_defines(newvalue)
444            return value
445
446        def process_value(value):
447            value = value.replace('[', '').replace(']', '')
448            if value.startswith('m4_esyscmd(') or value.startswith('m4_esyscmd_s('):
449                cmd = subst_defines(value[value.index('(')+1:-1])
450                try:
451                    if '|' in cmd:
452                        cmd = 'set -o pipefail; ' + cmd
453                    stdout, _ = bb.process.run(cmd, cwd=srctree, shell=True)
454                    ret = stdout.rstrip()
455                except bb.process.ExecutionError as e:
456                    ret = ''
457            elif value.startswith('m4_'):
458                return None
459            ret = subst_defines(value)
460            if ret:
461                ret = ret.strip('"\'')
462            return ret
463
464        # Since a configure.ac file is essentially a program, this is only ever going to be
465        # a hack unfortunately; but it ought to be enough of an approximation
466        if acfile:
467            srcfiles = [acfile]
468        else:
469            srcfiles = RecipeHandler.checkfiles(srctree, ['acinclude.m4', 'configure.ac', 'configure.in'])
470
471        pcdeps = []
472        libdeps = []
473        deps = []
474        unmapped = []
475
476        RecipeHandler.load_binmap(tinfoil.config_data)
477
478        def process_macro(keyword, value):
479            for handler in handlers:
480                if handler.process_macro(srctree, keyword, value, process_value, libdeps, pcdeps, deps, outlines, inherits, values):
481                    return
482            logger.debug('Found keyword %s with value "%s"' % (keyword, value))
483            if keyword == 'PKG_CHECK_MODULES':
484                res = pkg_re.search(value)
485                if res:
486                    res = dep_re.findall(res.group(1))
487                    if res:
488                        pcdeps.extend([x[0] for x in res])
489                inherits.append('pkgconfig')
490            elif keyword == 'PKG_CHECK_EXISTS':
491                res = pkgce_re.search(value)
492                if res:
493                    res = dep_re.findall(res.group(1))
494                    if res:
495                        pcdeps.extend([x[0] for x in res])
496                inherits.append('pkgconfig')
497            elif keyword in ('AM_GNU_GETTEXT', 'AM_GLIB_GNU_GETTEXT', 'GETTEXT_PACKAGE'):
498                inherits.append('gettext')
499            elif keyword in ('AC_PROG_INTLTOOL', 'IT_PROG_INTLTOOL'):
500                deps.append('intltool-native')
501            elif keyword == 'AM_PATH_GLIB_2_0':
502                deps.append('glib-2.0')
503            elif keyword in ('AC_CHECK_PROG', 'AC_PATH_PROG', 'AX_WITH_PROG'):
504                res = progs_re.search(value)
505                if res:
506                    for prog in shlex.split(res.group(1)):
507                        prog = prog.split()[0]
508                        for handler in handlers:
509                            if handler.process_prog(srctree, keyword, value, prog, deps, outlines, inherits, values):
510                                return
511                        progclass = progclassmap.get(prog, None)
512                        if progclass:
513                            inherits.append(progclass)
514                        else:
515                            progdep = RecipeHandler.recipebinmap.get(prog, None)
516                            if not progdep:
517                                progdep = progmap.get(prog, None)
518                            if progdep:
519                                deps.append(progdep)
520                            elif progdep is None:
521                                if not prog.startswith('$'):
522                                    unmapped.append(prog)
523            elif keyword == 'AC_CHECK_LIB':
524                res = lib_re.search(value)
525                if res:
526                    lib = res.group(1)
527                    if not lib.startswith('$'):
528                        libdeps.append(lib)
529            elif keyword == 'AX_CHECK_LIBRARY':
530                res = libx_re.search(value)
531                if res:
532                    lib = res.group(2)
533                    if not lib.startswith('$'):
534                        header = res.group(1)
535                        libdeps.append((lib, header))
536            elif keyword == 'AC_PATH_X':
537                deps.append('libx11')
538            elif keyword in ('AX_BOOST', 'BOOST_REQUIRE'):
539                deps.append('boost')
540            elif keyword in ('AC_PROG_LEX', 'AM_PROG_LEX', 'AX_PROG_FLEX'):
541                deps.append('flex-native')
542            elif keyword in ('AC_PROG_YACC', 'AX_PROG_BISON'):
543                deps.append('bison-native')
544            elif keyword == 'AX_CHECK_ZLIB':
545                deps.append('zlib')
546            elif keyword in ('AX_CHECK_OPENSSL', 'AX_LIB_CRYPTO'):
547                deps.append('openssl')
548            elif keyword == 'AX_LIB_CURL':
549                deps.append('curl')
550            elif keyword == 'AX_LIB_BEECRYPT':
551                deps.append('beecrypt')
552            elif keyword == 'AX_LIB_EXPAT':
553                deps.append('expat')
554            elif keyword == 'AX_LIB_GCRYPT':
555                deps.append('libgcrypt')
556            elif keyword == 'AX_LIB_NETTLE':
557                deps.append('nettle')
558            elif keyword == 'AX_LIB_READLINE':
559                deps.append('readline')
560            elif keyword == 'AX_LIB_SQLITE3':
561                deps.append('sqlite3')
562            elif keyword == 'AX_LIB_TAGLIB':
563                deps.append('taglib')
564            elif keyword in ['AX_PKG_SWIG', 'AC_PROG_SWIG']:
565                deps.append('swig-native')
566            elif keyword == 'AX_PROG_XSLTPROC':
567                deps.append('libxslt-native')
568            elif keyword in ['AC_PYTHON_DEVEL', 'AX_PYTHON_DEVEL', 'AM_PATH_PYTHON']:
569                pythonclass = 'python3native'
570            elif keyword == 'AX_WITH_CURSES':
571                deps.append('ncurses')
572            elif keyword == 'AX_PATH_BDB':
573                deps.append('db')
574            elif keyword == 'AX_PATH_LIB_PCRE':
575                deps.append('libpcre')
576            elif keyword == 'AC_INIT':
577                if extravalues is not None:
578                    res = ac_init_re.match(value)
579                    if res:
580                        extravalues['PN'] = process_value(res.group(1))
581                        pv = process_value(res.group(2))
582                        if validate_pv(pv):
583                            extravalues['PV'] = pv
584            elif keyword == 'AM_INIT_AUTOMAKE':
585                if extravalues is not None:
586                    if 'PN' not in extravalues:
587                        res = am_init_re.match(value)
588                        if res:
589                            if res.group(1) != 'AC_PACKAGE_NAME':
590                                extravalues['PN'] = process_value(res.group(1))
591                            pv = process_value(res.group(2))
592                            if validate_pv(pv):
593                                extravalues['PV'] = pv
594            elif keyword == 'define(':
595                res = define_re.match(value)
596                if res:
597                    key = res.group(2).strip('[]')
598                    value = process_value(res.group(3))
599                    if value is not None:
600                        defines[key] = value
601
602        keywords = ['PKG_CHECK_MODULES',
603                    'PKG_CHECK_EXISTS',
604                    'AM_GNU_GETTEXT',
605                    'AM_GLIB_GNU_GETTEXT',
606                    'GETTEXT_PACKAGE',
607                    'AC_PROG_INTLTOOL',
608                    'IT_PROG_INTLTOOL',
609                    'AM_PATH_GLIB_2_0',
610                    'AC_CHECK_PROG',
611                    'AC_PATH_PROG',
612                    'AX_WITH_PROG',
613                    'AC_CHECK_LIB',
614                    'AX_CHECK_LIBRARY',
615                    'AC_PATH_X',
616                    'AX_BOOST',
617                    'BOOST_REQUIRE',
618                    'AC_PROG_LEX',
619                    'AM_PROG_LEX',
620                    'AX_PROG_FLEX',
621                    'AC_PROG_YACC',
622                    'AX_PROG_BISON',
623                    'AX_CHECK_ZLIB',
624                    'AX_CHECK_OPENSSL',
625                    'AX_LIB_CRYPTO',
626                    'AX_LIB_CURL',
627                    'AX_LIB_BEECRYPT',
628                    'AX_LIB_EXPAT',
629                    'AX_LIB_GCRYPT',
630                    'AX_LIB_NETTLE',
631                    'AX_LIB_READLINE'
632                    'AX_LIB_SQLITE3',
633                    'AX_LIB_TAGLIB',
634                    'AX_PKG_SWIG',
635                    'AC_PROG_SWIG',
636                    'AX_PROG_XSLTPROC',
637                    'AC_PYTHON_DEVEL',
638                    'AX_PYTHON_DEVEL',
639                    'AM_PATH_PYTHON',
640                    'AX_WITH_CURSES',
641                    'AX_PATH_BDB',
642                    'AX_PATH_LIB_PCRE',
643                    'AC_INIT',
644                    'AM_INIT_AUTOMAKE',
645                    'define(',
646                    ]
647
648        for handler in handlers:
649            handler.extend_keywords(keywords)
650
651        for srcfile in srcfiles:
652            nesting = 0
653            in_keyword = ''
654            partial = ''
655            with open(srcfile, 'r', errors='surrogateescape') as f:
656                for line in f:
657                    if in_keyword:
658                        partial += ' ' + line.strip()
659                        if partial.endswith('\\'):
660                            partial = partial[:-1]
661                        nesting = nesting + line.count('(') - line.count(')')
662                        if nesting == 0:
663                            process_macro(in_keyword, partial)
664                            partial = ''
665                            in_keyword = ''
666                    else:
667                        for keyword in keywords:
668                            if keyword in line:
669                                nesting = line.count('(') - line.count(')')
670                                if nesting > 0:
671                                    partial = line.strip()
672                                    if partial.endswith('\\'):
673                                        partial = partial[:-1]
674                                    in_keyword = keyword
675                                else:
676                                    process_macro(keyword, line.strip())
677                                break
678
679            if in_keyword:
680                process_macro(in_keyword, partial)
681
682        if extravalues:
683            for k,v in list(extravalues.items()):
684                if v:
685                    if v.startswith('$') or v.startswith('@') or v.startswith('%'):
686                        del extravalues[k]
687                    else:
688                        extravalues[k] = v.strip('"\'').rstrip('()')
689
690        if unmapped:
691            outlines.append('# NOTE: the following prog dependencies are unknown, ignoring: %s' % ' '.join(list(set(unmapped))))
692
693        RecipeHandler.handle_depends(libdeps, pcdeps, deps, outlines, values, tinfoil.config_data)
694
695        for handler in handlers:
696            handler.post_process(srctree, libdeps, pcdeps, deps, outlines, inherits, values)
697
698        if inherits:
699            values['inherit'] = ' '.join(list(set(inherits)))
700
701        return values
702
703
704class AutotoolsExtensionHandler(object):
705    '''Base class for Autotools extension handlers'''
706    def process_macro(self, srctree, keyword, value, process_value, libdeps, pcdeps, deps, outlines, inherits, values):
707        '''
708        Handle a macro parsed out of an autotools file. Note that if you want this to be called
709        for any macro other than the ones AutotoolsRecipeHandler already looks for, you'll need
710        to add it to the keywords list in extend_keywords().
711        Return True if you've completely handled the passed in macro, otherwise return False.
712        '''
713        return False
714
715    def extend_keywords(self, keywords):
716        '''Adds keywords to be recognised by the parser (so that you get a call to process_macro)'''
717        return
718
719    def process_prog(self, srctree, keyword, value, prog, deps, outlines, inherits, values):
720        '''
721        Handle an AC_PATH_PROG, AC_CHECK_PROG etc. line
722        Return True if you've completely handled the passed in macro, otherwise return False.
723        '''
724        return False
725
726    def post_process(self, srctree, fn, pkg, deps, outlines, inherits, values):
727        '''
728        Apply any desired post-processing on the output
729        '''
730        return
731
732
733class MakefileRecipeHandler(RecipeHandler):
734    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
735        if 'buildsystem' in handled:
736            return False
737
738        makefile = RecipeHandler.checkfiles(srctree, ['Makefile', 'makefile', 'GNUmakefile'])
739        if makefile:
740            lines_after.append('# NOTE: this is a Makefile-only piece of software, so we cannot generate much of the')
741            lines_after.append('# recipe automatically - you will need to examine the Makefile yourself and ensure')
742            lines_after.append('# that the appropriate arguments are passed in.')
743            lines_after.append('')
744
745            scanfile = os.path.join(srctree, 'configure.scan')
746            skipscan = False
747            try:
748                stdout, stderr = bb.process.run('autoscan', cwd=srctree, shell=True)
749            except bb.process.ExecutionError as e:
750                skipscan = True
751            if scanfile and os.path.exists(scanfile):
752                values = AutotoolsRecipeHandler.extract_autotools_deps(lines_before, srctree, acfile=scanfile)
753                classes.extend(values.pop('inherit', '').split())
754                for var, value in values.items():
755                    if var == 'DEPENDS':
756                        lines_before.append('# NOTE: some of these dependencies may be optional, check the Makefile and/or upstream documentation')
757                    lines_before.append('%s = "%s"' % (var, value))
758                lines_before.append('')
759                for f in ['configure.scan', 'autoscan.log']:
760                    fp = os.path.join(srctree, f)
761                    if os.path.exists(fp):
762                        os.remove(fp)
763
764            self.genfunction(lines_after, 'do_configure', ['# Specify any needed configure commands here'])
765
766            func = []
767            func.append('# You will almost certainly need to add additional arguments here')
768            func.append('oe_runmake')
769            self.genfunction(lines_after, 'do_compile', func)
770
771            installtarget = True
772            try:
773                stdout, stderr = bb.process.run('make -n install', cwd=srctree, shell=True)
774            except bb.process.ExecutionError as e:
775                if e.exitcode != 1:
776                    installtarget = False
777            func = []
778            if installtarget:
779                func.append('# This is a guess; additional arguments may be required')
780                makeargs = ''
781                with open(makefile[0], 'r', errors='surrogateescape') as f:
782                    for i in range(1, 100):
783                        if 'DESTDIR' in f.readline():
784                            makeargs += " 'DESTDIR=${D}'"
785                            break
786                func.append('oe_runmake install%s' % makeargs)
787            else:
788                func.append('# NOTE: unable to determine what to put here - there is a Makefile but no')
789                func.append('# target named "install", so you will need to define this yourself')
790            self.genfunction(lines_after, 'do_install', func)
791
792            handled.append('buildsystem')
793        else:
794            lines_after.append('# NOTE: no Makefile found, unable to determine what needs to be done')
795            lines_after.append('')
796            self.genfunction(lines_after, 'do_configure', ['# Specify any needed configure commands here'])
797            self.genfunction(lines_after, 'do_compile', ['# Specify compilation commands here'])
798            self.genfunction(lines_after, 'do_install', ['# Specify install commands here'])
799
800
801class VersionFileRecipeHandler(RecipeHandler):
802    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
803        if 'PV' not in extravalues:
804            # Look for a VERSION or version file containing a single line consisting
805            # only of a version number
806            filelist = RecipeHandler.checkfiles(srctree, ['VERSION', 'version'])
807            version = None
808            for fileitem in filelist:
809                linecount = 0
810                with open(fileitem, 'r', errors='surrogateescape') as f:
811                    for line in f:
812                        line = line.rstrip().strip('"\'')
813                        linecount += 1
814                        if line:
815                            if linecount > 1:
816                                version = None
817                                break
818                            else:
819                                if validate_pv(line):
820                                    version = line
821                if version:
822                    extravalues['PV'] = version
823                    break
824
825
826class SpecFileRecipeHandler(RecipeHandler):
827    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
828        if 'PV' in extravalues and 'PN' in extravalues:
829            return
830        filelist = RecipeHandler.checkfiles(srctree, ['*.spec'], recursive=True)
831        valuemap = {'Name': 'PN',
832                    'Version': 'PV',
833                    'Summary': 'SUMMARY',
834                    'Url': 'HOMEPAGE',
835                    'License': 'LICENSE'}
836        foundvalues = {}
837        for fileitem in filelist:
838            linecount = 0
839            with open(fileitem, 'r', errors='surrogateescape') as f:
840                for line in f:
841                    for value, varname in valuemap.items():
842                        if line.startswith(value + ':') and not varname in foundvalues:
843                            foundvalues[varname] = line.split(':', 1)[1].strip()
844                            break
845                    if len(foundvalues) == len(valuemap):
846                        break
847        # Drop values containing unexpanded RPM macros
848        for k in list(foundvalues.keys()):
849            if '%' in foundvalues[k]:
850                del foundvalues[k]
851        if 'PV' in foundvalues:
852            if not validate_pv(foundvalues['PV']):
853                del foundvalues['PV']
854        license = foundvalues.pop('LICENSE', None)
855        if license:
856            liccomment = '# NOTE: spec file indicates the license may be "%s"' % license
857            for i, line in enumerate(lines_before):
858                if line.startswith('LICENSE ='):
859                    lines_before.insert(i, liccomment)
860                    break
861            else:
862                lines_before.append(liccomment)
863        extravalues.update(foundvalues)
864
865def register_recipe_handlers(handlers):
866    # Set priorities with some gaps so that other plugins can insert
867    # their own handlers (so avoid changing these numbers)
868    handlers.append((CmakeRecipeHandler(), 50))
869    handlers.append((AutotoolsRecipeHandler(), 40))
870    handlers.append((SconsRecipeHandler(), 30))
871    handlers.append((QmakeRecipeHandler(), 20))
872    handlers.append((MakefileRecipeHandler(), 10))
873    handlers.append((VersionFileRecipeHandler(), -1))
874    handlers.append((SpecFileRecipeHandler(), -1))
875