Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | # Recipe creation tool - create command build system handlers |
| 2 | # |
| 3 | # Copyright (C) 2014 Intel Corporation |
| 4 | # |
| 5 | # This program is free software; you can redistribute it and/or modify |
| 6 | # it under the terms of the GNU General Public License version 2 as |
| 7 | # published by the Free Software Foundation. |
| 8 | # |
| 9 | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | # GNU General Public License for more details. |
| 13 | # |
| 14 | # You should have received a copy of the GNU General Public License along |
| 15 | # with this program; if not, write to the Free Software Foundation, Inc., |
| 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| 17 | |
| 18 | import re |
| 19 | import logging |
| 20 | from recipetool.create import RecipeHandler, read_pkgconfig_provides |
| 21 | |
| 22 | logger = logging.getLogger('recipetool') |
| 23 | |
| 24 | tinfoil = None |
| 25 | |
| 26 | def tinfoil_init(instance): |
| 27 | global tinfoil |
| 28 | tinfoil = instance |
| 29 | |
| 30 | class CmakeRecipeHandler(RecipeHandler): |
| 31 | def process(self, srctree, classes, lines_before, lines_after, handled): |
| 32 | if 'buildsystem' in handled: |
| 33 | return False |
| 34 | |
| 35 | if RecipeHandler.checkfiles(srctree, ['CMakeLists.txt']): |
| 36 | classes.append('cmake') |
| 37 | lines_after.append('# Specify any options you want to pass to cmake using EXTRA_OECMAKE:') |
| 38 | lines_after.append('EXTRA_OECMAKE = ""') |
| 39 | lines_after.append('') |
| 40 | handled.append('buildsystem') |
| 41 | return True |
| 42 | return False |
| 43 | |
| 44 | class SconsRecipeHandler(RecipeHandler): |
| 45 | def process(self, srctree, classes, lines_before, lines_after, handled): |
| 46 | if 'buildsystem' in handled: |
| 47 | return False |
| 48 | |
| 49 | if RecipeHandler.checkfiles(srctree, ['SConstruct', 'Sconstruct', 'sconstruct']): |
| 50 | classes.append('scons') |
| 51 | lines_after.append('# Specify any options you want to pass to scons using EXTRA_OESCONS:') |
| 52 | lines_after.append('EXTRA_OESCONS = ""') |
| 53 | lines_after.append('') |
| 54 | handled.append('buildsystem') |
| 55 | return True |
| 56 | return False |
| 57 | |
| 58 | class QmakeRecipeHandler(RecipeHandler): |
| 59 | def process(self, srctree, classes, lines_before, lines_after, handled): |
| 60 | if 'buildsystem' in handled: |
| 61 | return False |
| 62 | |
| 63 | if RecipeHandler.checkfiles(srctree, ['*.pro']): |
| 64 | classes.append('qmake2') |
| 65 | handled.append('buildsystem') |
| 66 | return True |
| 67 | return False |
| 68 | |
| 69 | class AutotoolsRecipeHandler(RecipeHandler): |
| 70 | def process(self, srctree, classes, lines_before, lines_after, handled): |
| 71 | if 'buildsystem' in handled: |
| 72 | return False |
| 73 | |
| 74 | autoconf = False |
| 75 | if RecipeHandler.checkfiles(srctree, ['configure.ac', 'configure.in']): |
| 76 | autoconf = True |
| 77 | values = AutotoolsRecipeHandler.extract_autotools_deps(lines_before, srctree) |
| 78 | classes.extend(values.pop('inherit', '').split()) |
| 79 | for var, value in values.iteritems(): |
| 80 | lines_before.append('%s = "%s"' % (var, value)) |
| 81 | else: |
| 82 | conffile = RecipeHandler.checkfiles(srctree, ['configure']) |
| 83 | if conffile: |
| 84 | # Check if this is just a pre-generated autoconf configure script |
| 85 | with open(conffile[0], 'r') as f: |
| 86 | for i in range(1, 10): |
| 87 | if 'Generated by GNU Autoconf' in f.readline(): |
| 88 | autoconf = True |
| 89 | break |
| 90 | |
| 91 | if autoconf: |
| 92 | lines_before.append('# NOTE: if this software is not capable of being built in a separate build directory') |
| 93 | lines_before.append('# from the source, you should replace autotools with autotools-brokensep in the') |
| 94 | lines_before.append('# inherit line') |
| 95 | classes.append('autotools') |
| 96 | lines_after.append('# Specify any options you want to pass to the configure script using EXTRA_OECONF:') |
| 97 | lines_after.append('EXTRA_OECONF = ""') |
| 98 | lines_after.append('') |
| 99 | handled.append('buildsystem') |
| 100 | return True |
| 101 | |
| 102 | return False |
| 103 | |
| 104 | @staticmethod |
| 105 | def extract_autotools_deps(outlines, srctree, acfile=None): |
| 106 | import shlex |
| 107 | import oe.package |
| 108 | |
| 109 | values = {} |
| 110 | inherits = [] |
| 111 | |
| 112 | # FIXME this mapping is very thin |
| 113 | progmap = {'flex': 'flex-native', |
| 114 | 'bison': 'bison-native', |
| 115 | 'm4': 'm4-native'} |
| 116 | progclassmap = {'gconftool-2': 'gconf', |
| 117 | 'pkg-config': 'pkgconfig'} |
| 118 | |
| 119 | ignoredeps = ['gcc-runtime', 'glibc', 'uclibc'] |
| 120 | |
| 121 | pkg_re = re.compile('PKG_CHECK_MODULES\(\[?[a-zA-Z0-9]*\]?, \[?([^,\]]*)[),].*') |
| 122 | lib_re = re.compile('AC_CHECK_LIB\(\[?([a-zA-Z0-9]*)\]?, .*') |
| 123 | progs_re = re.compile('_PROGS?\(\[?[a-zA-Z0-9]*\]?, \[?([^,\]]*)\]?[),].*') |
| 124 | dep_re = re.compile('([^ ><=]+)( [<>=]+ [^ ><=]+)?') |
| 125 | |
| 126 | # Build up lib library->package mapping |
| 127 | shlib_providers = oe.package.read_shlib_providers(tinfoil.config_data) |
| 128 | libdir = tinfoil.config_data.getVar('libdir', True) |
| 129 | base_libdir = tinfoil.config_data.getVar('base_libdir', True) |
| 130 | libpaths = list(set([base_libdir, libdir])) |
| 131 | libname_re = re.compile('^lib(.+)\.so.*$') |
| 132 | pkglibmap = {} |
| 133 | for lib, item in shlib_providers.iteritems(): |
| 134 | for path, pkg in item.iteritems(): |
| 135 | if path in libpaths: |
| 136 | res = libname_re.match(lib) |
| 137 | if res: |
| 138 | libname = res.group(1) |
| 139 | if not libname in pkglibmap: |
| 140 | pkglibmap[libname] = pkg[0] |
| 141 | else: |
| 142 | logger.debug('unable to extract library name from %s' % lib) |
| 143 | |
| 144 | # Now turn it into a library->recipe mapping |
| 145 | recipelibmap = {} |
| 146 | pkgdata_dir = tinfoil.config_data.getVar('PKGDATA_DIR', True) |
| 147 | for libname, pkg in pkglibmap.iteritems(): |
| 148 | try: |
| 149 | with open(os.path.join(pkgdata_dir, 'runtime', pkg)) as f: |
| 150 | for line in f: |
| 151 | if line.startswith('PN:'): |
| 152 | recipelibmap[libname] = line.split(':', 1)[-1].strip() |
| 153 | break |
| 154 | except IOError as ioe: |
| 155 | if ioe.errno == 2: |
| 156 | logger.warn('unable to find a pkgdata file for package %s' % pkg) |
| 157 | else: |
| 158 | raise |
| 159 | |
| 160 | # Since a configure.ac file is essentially a program, this is only ever going to be |
| 161 | # a hack unfortunately; but it ought to be enough of an approximation |
| 162 | if acfile: |
| 163 | srcfiles = [acfile] |
| 164 | else: |
| 165 | srcfiles = RecipeHandler.checkfiles(srctree, ['configure.ac', 'configure.in']) |
| 166 | pcdeps = [] |
| 167 | deps = [] |
| 168 | unmapped = [] |
| 169 | unmappedlibs = [] |
| 170 | with open(srcfiles[0], 'r') as f: |
| 171 | for line in f: |
| 172 | if 'PKG_CHECK_MODULES' in line: |
| 173 | res = pkg_re.search(line) |
| 174 | if res: |
| 175 | res = dep_re.findall(res.group(1)) |
| 176 | if res: |
| 177 | pcdeps.extend([x[0] for x in res]) |
| 178 | inherits.append('pkgconfig') |
| 179 | if line.lstrip().startswith('AM_GNU_GETTEXT'): |
| 180 | inherits.append('gettext') |
| 181 | elif 'AC_CHECK_PROG' in line or 'AC_PATH_PROG' in line: |
| 182 | res = progs_re.search(line) |
| 183 | if res: |
| 184 | for prog in shlex.split(res.group(1)): |
| 185 | prog = prog.split()[0] |
| 186 | progclass = progclassmap.get(prog, None) |
| 187 | if progclass: |
| 188 | inherits.append(progclass) |
| 189 | else: |
| 190 | progdep = progmap.get(prog, None) |
| 191 | if progdep: |
| 192 | deps.append(progdep) |
| 193 | else: |
| 194 | if not prog.startswith('$'): |
| 195 | unmapped.append(prog) |
| 196 | elif 'AC_CHECK_LIB' in line: |
| 197 | res = lib_re.search(line) |
| 198 | if res: |
| 199 | lib = res.group(1) |
| 200 | libdep = recipelibmap.get(lib, None) |
| 201 | if libdep: |
| 202 | deps.append(libdep) |
| 203 | else: |
| 204 | if libdep is None: |
| 205 | if not lib.startswith('$'): |
| 206 | unmappedlibs.append(lib) |
| 207 | elif 'AC_PATH_X' in line: |
| 208 | deps.append('libx11') |
| 209 | |
| 210 | if unmapped: |
| 211 | outlines.append('# NOTE: the following prog dependencies are unknown, ignoring: %s' % ' '.join(unmapped)) |
| 212 | |
| 213 | if unmappedlibs: |
| 214 | outlines.append('# NOTE: the following library dependencies are unknown, ignoring: %s' % ' '.join(unmappedlibs)) |
| 215 | outlines.append('# (this is based on recipes that have previously been built and packaged)') |
| 216 | |
| 217 | recipemap = read_pkgconfig_provides(tinfoil.config_data) |
| 218 | unmapped = [] |
| 219 | for pcdep in pcdeps: |
| 220 | recipe = recipemap.get(pcdep, None) |
| 221 | if recipe: |
| 222 | deps.append(recipe) |
| 223 | else: |
| 224 | if not pcdep.startswith('$'): |
| 225 | unmapped.append(pcdep) |
| 226 | |
| 227 | deps = set(deps).difference(set(ignoredeps)) |
| 228 | |
| 229 | if unmapped: |
| 230 | outlines.append('# NOTE: unable to map the following pkg-config dependencies: %s' % ' '.join(unmapped)) |
| 231 | outlines.append('# (this is based on recipes that have previously been built and packaged)') |
| 232 | |
| 233 | if deps: |
| 234 | values['DEPENDS'] = ' '.join(deps) |
| 235 | |
| 236 | if inherits: |
| 237 | values['inherit'] = ' '.join(list(set(inherits))) |
| 238 | |
| 239 | return values |
| 240 | |
| 241 | |
| 242 | class MakefileRecipeHandler(RecipeHandler): |
| 243 | def process(self, srctree, classes, lines_before, lines_after, handled): |
| 244 | if 'buildsystem' in handled: |
| 245 | return False |
| 246 | |
| 247 | makefile = RecipeHandler.checkfiles(srctree, ['Makefile']) |
| 248 | if makefile: |
| 249 | lines_after.append('# NOTE: this is a Makefile-only piece of software, so we cannot generate much of the') |
| 250 | lines_after.append('# recipe automatically - you will need to examine the Makefile yourself and ensure') |
| 251 | lines_after.append('# that the appropriate arguments are passed in.') |
| 252 | lines_after.append('') |
| 253 | |
| 254 | scanfile = os.path.join(srctree, 'configure.scan') |
| 255 | skipscan = False |
| 256 | try: |
| 257 | stdout, stderr = bb.process.run('autoscan', cwd=srctree, shell=True) |
| 258 | except bb.process.ExecutionError as e: |
| 259 | skipscan = True |
| 260 | if scanfile and os.path.exists(scanfile): |
| 261 | values = AutotoolsRecipeHandler.extract_autotools_deps(lines_before, srctree, acfile=scanfile) |
| 262 | classes.extend(values.pop('inherit', '').split()) |
| 263 | for var, value in values.iteritems(): |
| 264 | if var == 'DEPENDS': |
| 265 | lines_before.append('# NOTE: some of these dependencies may be optional, check the Makefile and/or upstream documentation') |
| 266 | lines_before.append('%s = "%s"' % (var, value)) |
| 267 | lines_before.append('') |
| 268 | for f in ['configure.scan', 'autoscan.log']: |
| 269 | fp = os.path.join(srctree, f) |
| 270 | if os.path.exists(fp): |
| 271 | os.remove(fp) |
| 272 | |
| 273 | self.genfunction(lines_after, 'do_configure', ['# Specify any needed configure commands here']) |
| 274 | |
| 275 | func = [] |
| 276 | func.append('# You will almost certainly need to add additional arguments here') |
| 277 | func.append('oe_runmake') |
| 278 | self.genfunction(lines_after, 'do_compile', func) |
| 279 | |
| 280 | installtarget = True |
| 281 | try: |
Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame] | 282 | stdout, stderr = bb.process.run('make -n install', cwd=srctree, shell=True) |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 283 | except bb.process.ExecutionError as e: |
| 284 | if e.exitcode != 1: |
| 285 | installtarget = False |
| 286 | func = [] |
| 287 | if installtarget: |
| 288 | func.append('# This is a guess; additional arguments may be required') |
| 289 | makeargs = '' |
| 290 | with open(makefile[0], 'r') as f: |
| 291 | for i in range(1, 100): |
| 292 | if 'DESTDIR' in f.readline(): |
| 293 | makeargs += " 'DESTDIR=${D}'" |
| 294 | break |
| 295 | func.append('oe_runmake install%s' % makeargs) |
| 296 | else: |
| 297 | func.append('# NOTE: unable to determine what to put here - there is a Makefile but no') |
| 298 | func.append('# target named "install", so you will need to define this yourself') |
| 299 | self.genfunction(lines_after, 'do_install', func) |
| 300 | |
| 301 | handled.append('buildsystem') |
| 302 | else: |
| 303 | lines_after.append('# NOTE: no Makefile found, unable to determine what needs to be done') |
| 304 | lines_after.append('') |
| 305 | self.genfunction(lines_after, 'do_configure', ['# Specify any needed configure commands here']) |
| 306 | self.genfunction(lines_after, 'do_compile', ['# Specify compilation commands here']) |
| 307 | self.genfunction(lines_after, 'do_install', ['# Specify install commands here']) |
| 308 | |
| 309 | |
| 310 | def register_recipe_handlers(handlers): |
| 311 | # These are in a specific order so that the right one is detected first |
| 312 | handlers.append(CmakeRecipeHandler()) |
| 313 | handlers.append(AutotoolsRecipeHandler()) |
| 314 | handlers.append(SconsRecipeHandler()) |
| 315 | handlers.append(QmakeRecipeHandler()) |
| 316 | handlers.append(MakefileRecipeHandler()) |