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 os
9import re
10import logging
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(r'project\s*\(([^)]*)\)', re.IGNORECASE)
141        pkgcm_re = re.compile(r'pkg_check_modules\s*\(\s*[a-zA-Z0-9-_]+\s*(REQUIRED)?\s+([^)\s]+)\s*\)', re.IGNORECASE)
142        pkgsm_re = re.compile(r'pkg_search_module\s*\(\s*[a-zA-Z0-9-_]+\s*(REQUIRED)?((\s+[^)\s]+)+)\s*\)', re.IGNORECASE)
143        findpackage_re = re.compile(r'find_package\s*\(\s*([a-zA-Z0-9-_]+)\s*.*', re.IGNORECASE)
144        findlibrary_re = re.compile(r'find_library\s*\(\s*[a-zA-Z0-9-_]+\s*(NAMES\s+)?([a-zA-Z0-9-_ ]+)\s*.*')
145        checklib_re = re.compile(r'check_library_exists\s*\(\s*([^\s)]+)\s*.*', re.IGNORECASE)
146        include_re = re.compile(r'include\s*\(\s*([^)\s]*)\s*\)', re.IGNORECASE)
147        subdir_re = re.compile(r'add_subdirectory\s*\(\s*([^)\s]*)\s*([^)\s]*)\s*\)', re.IGNORECASE)
148        dep_re = re.compile(r'([^ ><=]+)( *[<>=]+ *[^ ><=]+)?')
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(r'PKG_CHECK_MODULES\(\s*\[?[a-zA-Z0-9_]*\]?,\s*\[?([^,\]]*)\]?[),].*')
427        pkgce_re = re.compile(r'PKG_CHECK_EXISTS\(\s*\[?([^,\]]*)\]?[),].*')
428        lib_re = re.compile(r'AC_CHECK_LIB\(\s*\[?([^,\]]*)\]?,.*')
429        libx_re = re.compile(r'AX_CHECK_LIBRARY\(\s*\[?[^,\]]*\]?,\s*\[?([^,\]]*)\]?,\s*\[?([a-zA-Z0-9-]*)\]?,.*')
430        progs_re = re.compile(r'_PROGS?\(\s*\[?[a-zA-Z0-9_]*\]?,\s*\[?([^,\]]*)\]?[),].*')
431        dep_re = re.compile(r'([^ ><=]+)( [<>=]+ [^ ><=]+)?')
432        ac_init_re = re.compile(r'AC_INIT\(\s*([^,]+),\s*([^,]+)[,)].*')
433        am_init_re = re.compile(r'AM_INIT_AUTOMAKE\(\s*([^,]+),\s*([^,]+)[,)].*')
434        define_re = re.compile(r'\s*(m4_)?define\(\s*([^,]+),\s*([^,]+)\)')
435        version_re = re.compile(r'([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 in ('AX_LIB_CURL', 'LIBCURL_CHECK_CONFIG'):
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                    'LIBCURL_CHECK_CONFIG',
628                    'AX_LIB_BEECRYPT',
629                    'AX_LIB_EXPAT',
630                    'AX_LIB_GCRYPT',
631                    'AX_LIB_NETTLE',
632                    'AX_LIB_READLINE'
633                    'AX_LIB_SQLITE3',
634                    'AX_LIB_TAGLIB',
635                    'AX_PKG_SWIG',
636                    'AC_PROG_SWIG',
637                    'AX_PROG_XSLTPROC',
638                    'AC_PYTHON_DEVEL',
639                    'AX_PYTHON_DEVEL',
640                    'AM_PATH_PYTHON',
641                    'AX_WITH_CURSES',
642                    'AX_PATH_BDB',
643                    'AX_PATH_LIB_PCRE',
644                    'AC_INIT',
645                    'AM_INIT_AUTOMAKE',
646                    'define(',
647                    ]
648
649        for handler in handlers:
650            handler.extend_keywords(keywords)
651
652        for srcfile in srcfiles:
653            nesting = 0
654            in_keyword = ''
655            partial = ''
656            with open(srcfile, 'r', errors='surrogateescape') as f:
657                for line in f:
658                    if in_keyword:
659                        partial += ' ' + line.strip()
660                        if partial.endswith('\\'):
661                            partial = partial[:-1]
662                        nesting = nesting + line.count('(') - line.count(')')
663                        if nesting == 0:
664                            process_macro(in_keyword, partial)
665                            partial = ''
666                            in_keyword = ''
667                    else:
668                        for keyword in keywords:
669                            if keyword in line:
670                                nesting = line.count('(') - line.count(')')
671                                if nesting > 0:
672                                    partial = line.strip()
673                                    if partial.endswith('\\'):
674                                        partial = partial[:-1]
675                                    in_keyword = keyword
676                                else:
677                                    process_macro(keyword, line.strip())
678                                break
679
680            if in_keyword:
681                process_macro(in_keyword, partial)
682
683        if extravalues:
684            for k,v in list(extravalues.items()):
685                if v:
686                    if v.startswith('$') or v.startswith('@') or v.startswith('%'):
687                        del extravalues[k]
688                    else:
689                        extravalues[k] = v.strip('"\'').rstrip('()')
690
691        if unmapped:
692            outlines.append('# NOTE: the following prog dependencies are unknown, ignoring: %s' % ' '.join(list(set(unmapped))))
693
694        RecipeHandler.handle_depends(libdeps, pcdeps, deps, outlines, values, tinfoil.config_data)
695
696        for handler in handlers:
697            handler.post_process(srctree, libdeps, pcdeps, deps, outlines, inherits, values)
698
699        if inherits:
700            values['inherit'] = ' '.join(list(set(inherits)))
701
702        return values
703
704
705class AutotoolsExtensionHandler(object):
706    '''Base class for Autotools extension handlers'''
707    def process_macro(self, srctree, keyword, value, process_value, libdeps, pcdeps, deps, outlines, inherits, values):
708        '''
709        Handle a macro parsed out of an autotools file. Note that if you want this to be called
710        for any macro other than the ones AutotoolsRecipeHandler already looks for, you'll need
711        to add it to the keywords list in extend_keywords().
712        Return True if you've completely handled the passed in macro, otherwise return False.
713        '''
714        return False
715
716    def extend_keywords(self, keywords):
717        '''Adds keywords to be recognised by the parser (so that you get a call to process_macro)'''
718        return
719
720    def process_prog(self, srctree, keyword, value, prog, deps, outlines, inherits, values):
721        '''
722        Handle an AC_PATH_PROG, AC_CHECK_PROG etc. line
723        Return True if you've completely handled the passed in macro, otherwise return False.
724        '''
725        return False
726
727    def post_process(self, srctree, fn, pkg, deps, outlines, inherits, values):
728        '''
729        Apply any desired post-processing on the output
730        '''
731        return
732
733
734class MakefileRecipeHandler(RecipeHandler):
735    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
736        if 'buildsystem' in handled:
737            return False
738
739        makefile = RecipeHandler.checkfiles(srctree, ['Makefile', 'makefile', 'GNUmakefile'])
740        if makefile:
741            lines_after.append('# NOTE: this is a Makefile-only piece of software, so we cannot generate much of the')
742            lines_after.append('# recipe automatically - you will need to examine the Makefile yourself and ensure')
743            lines_after.append('# that the appropriate arguments are passed in.')
744            lines_after.append('')
745
746            scanfile = os.path.join(srctree, 'configure.scan')
747            skipscan = False
748            try:
749                stdout, stderr = bb.process.run('autoscan', cwd=srctree, shell=True)
750            except bb.process.ExecutionError as e:
751                skipscan = True
752            if scanfile and os.path.exists(scanfile):
753                values = AutotoolsRecipeHandler.extract_autotools_deps(lines_before, srctree, acfile=scanfile)
754                classes.extend(values.pop('inherit', '').split())
755                for var, value in values.items():
756                    if var == 'DEPENDS':
757                        lines_before.append('# NOTE: some of these dependencies may be optional, check the Makefile and/or upstream documentation')
758                    lines_before.append('%s = "%s"' % (var, value))
759                lines_before.append('')
760                for f in ['configure.scan', 'autoscan.log']:
761                    fp = os.path.join(srctree, f)
762                    if os.path.exists(fp):
763                        os.remove(fp)
764
765            self.genfunction(lines_after, 'do_configure', ['# Specify any needed configure commands here'])
766
767            func = []
768            func.append('# You will almost certainly need to add additional arguments here')
769            func.append('oe_runmake')
770            self.genfunction(lines_after, 'do_compile', func)
771
772            installtarget = True
773            try:
774                stdout, stderr = bb.process.run('make -n install', cwd=srctree, shell=True)
775            except bb.process.ExecutionError as e:
776                if e.exitcode != 1:
777                    installtarget = False
778            func = []
779            if installtarget:
780                func.append('# This is a guess; additional arguments may be required')
781                makeargs = ''
782                with open(makefile[0], 'r', errors='surrogateescape') as f:
783                    for i in range(1, 100):
784                        if 'DESTDIR' in f.readline():
785                            makeargs += " 'DESTDIR=${D}'"
786                            break
787                func.append('oe_runmake install%s' % makeargs)
788            else:
789                func.append('# NOTE: unable to determine what to put here - there is a Makefile but no')
790                func.append('# target named "install", so you will need to define this yourself')
791            self.genfunction(lines_after, 'do_install', func)
792
793            handled.append('buildsystem')
794        else:
795            lines_after.append('# NOTE: no Makefile found, unable to determine what needs to be done')
796            lines_after.append('')
797            self.genfunction(lines_after, 'do_configure', ['# Specify any needed configure commands here'])
798            self.genfunction(lines_after, 'do_compile', ['# Specify compilation commands here'])
799            self.genfunction(lines_after, 'do_install', ['# Specify install commands here'])
800
801
802class VersionFileRecipeHandler(RecipeHandler):
803    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
804        if 'PV' not in extravalues:
805            # Look for a VERSION or version file containing a single line consisting
806            # only of a version number
807            filelist = RecipeHandler.checkfiles(srctree, ['VERSION', 'version'])
808            version = None
809            for fileitem in filelist:
810                linecount = 0
811                with open(fileitem, 'r', errors='surrogateescape') as f:
812                    for line in f:
813                        line = line.rstrip().strip('"\'')
814                        linecount += 1
815                        if line:
816                            if linecount > 1:
817                                version = None
818                                break
819                            else:
820                                if validate_pv(line):
821                                    version = line
822                if version:
823                    extravalues['PV'] = version
824                    break
825
826
827class SpecFileRecipeHandler(RecipeHandler):
828    def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
829        if 'PV' in extravalues and 'PN' in extravalues:
830            return
831        filelist = RecipeHandler.checkfiles(srctree, ['*.spec'], recursive=True)
832        valuemap = {'Name': 'PN',
833                    'Version': 'PV',
834                    'Summary': 'SUMMARY',
835                    'Url': 'HOMEPAGE',
836                    'License': 'LICENSE'}
837        foundvalues = {}
838        for fileitem in filelist:
839            linecount = 0
840            with open(fileitem, 'r', errors='surrogateescape') as f:
841                for line in f:
842                    for value, varname in valuemap.items():
843                        if line.startswith(value + ':') and not varname in foundvalues:
844                            foundvalues[varname] = line.split(':', 1)[1].strip()
845                            break
846                    if len(foundvalues) == len(valuemap):
847                        break
848        # Drop values containing unexpanded RPM macros
849        for k in list(foundvalues.keys()):
850            if '%' in foundvalues[k]:
851                del foundvalues[k]
852        if 'PV' in foundvalues:
853            if not validate_pv(foundvalues['PV']):
854                del foundvalues['PV']
855        license = foundvalues.pop('LICENSE', None)
856        if license:
857            liccomment = '# NOTE: spec file indicates the license may be "%s"' % license
858            for i, line in enumerate(lines_before):
859                if line.startswith('LICENSE ='):
860                    lines_before.insert(i, liccomment)
861                    break
862            else:
863                lines_before.append(liccomment)
864        extravalues.update(foundvalues)
865
866def register_recipe_handlers(handlers):
867    # Set priorities with some gaps so that other plugins can insert
868    # their own handlers (so avoid changing these numbers)
869    handlers.append((CmakeRecipeHandler(), 50))
870    handlers.append((AutotoolsRecipeHandler(), 40))
871    handlers.append((SconsRecipeHandler(), 30))
872    handlers.append((QmakeRecipeHandler(), 20))
873    handlers.append((MakefileRecipeHandler(), 10))
874    handlers.append((VersionFileRecipeHandler(), -1))
875    handlers.append((SpecFileRecipeHandler(), -1))
876