diff options
author | Paul Eggleton <paul.eggleton@linux.intel.com> | 2015-12-22 17:03:02 +1300 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2015-12-28 09:25:13 +0000 |
commit | db5f9645ad3ffb4e9be7075d71cb1b13341f5195 (patch) | |
tree | 3c4bfcfc698840c310d069f26b57863fde877528 /scripts/lib/recipetool/create_buildsys.py | |
parent | 6a7661b8005fadad10bde494131e27406e1e45b8 (diff) | |
download | poky-db5f9645ad3ffb4e9be7075d71cb1b13341f5195.tar.gz |
recipetool: create: support extracting name and version from build scripts
Some build systems (notably autotools) support declaring the name and
version of the program being built; since we need those for the recipe
we can attempt to extract them. It's a little fuzzy as they are often
omitted or may not be appropriately formatted for our purposes, but it
does work on a reasonable number of software packages to be useful.
(From OE-Core rev: 3b3fd33190d89c09e62126eea0e45aa84fe5442e)
Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'scripts/lib/recipetool/create_buildsys.py')
-rw-r--r-- | scripts/lib/recipetool/create_buildsys.py | 282 |
1 files changed, 237 insertions, 45 deletions
diff --git a/scripts/lib/recipetool/create_buildsys.py b/scripts/lib/recipetool/create_buildsys.py index 931ef3b33f..0aff59e229 100644 --- a/scripts/lib/recipetool/create_buildsys.py +++ b/scripts/lib/recipetool/create_buildsys.py | |||
@@ -17,7 +17,7 @@ | |||
17 | 17 | ||
18 | import re | 18 | import re |
19 | import logging | 19 | import logging |
20 | from recipetool.create import RecipeHandler, read_pkgconfig_provides | 20 | from recipetool.create import RecipeHandler, read_pkgconfig_provides, validate_pv |
21 | 21 | ||
22 | logger = logging.getLogger('recipetool') | 22 | logger = logging.getLogger('recipetool') |
23 | 23 | ||
@@ -27,13 +27,17 @@ def tinfoil_init(instance): | |||
27 | global tinfoil | 27 | global tinfoil |
28 | tinfoil = instance | 28 | tinfoil = instance |
29 | 29 | ||
30 | |||
30 | class CmakeRecipeHandler(RecipeHandler): | 31 | class CmakeRecipeHandler(RecipeHandler): |
31 | def process(self, srctree, classes, lines_before, lines_after, handled): | 32 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): |
32 | if 'buildsystem' in handled: | 33 | if 'buildsystem' in handled: |
33 | return False | 34 | return False |
34 | 35 | ||
35 | if RecipeHandler.checkfiles(srctree, ['CMakeLists.txt']): | 36 | if RecipeHandler.checkfiles(srctree, ['CMakeLists.txt']): |
36 | classes.append('cmake') | 37 | classes.append('cmake') |
38 | values = CmakeRecipeHandler.extract_cmake_deps(lines_before, srctree, extravalues) | ||
39 | for var, value in values.iteritems(): | ||
40 | lines_before.append('%s = "%s"' % (var, value)) | ||
37 | lines_after.append('# Specify any options you want to pass to cmake using EXTRA_OECMAKE:') | 41 | lines_after.append('# Specify any options you want to pass to cmake using EXTRA_OECMAKE:') |
38 | lines_after.append('EXTRA_OECMAKE = ""') | 42 | lines_after.append('EXTRA_OECMAKE = ""') |
39 | lines_after.append('') | 43 | lines_after.append('') |
@@ -41,8 +45,26 @@ class CmakeRecipeHandler(RecipeHandler): | |||
41 | return True | 45 | return True |
42 | return False | 46 | return False |
43 | 47 | ||
48 | @staticmethod | ||
49 | def extract_cmake_deps(outlines, srctree, extravalues, cmakelistsfile=None): | ||
50 | values = {} | ||
51 | |||
52 | if cmakelistsfile: | ||
53 | srcfiles = [cmakelistsfile] | ||
54 | else: | ||
55 | srcfiles = RecipeHandler.checkfiles(srctree, ['CMakeLists.txt']) | ||
56 | |||
57 | proj_re = re.compile('project\(([^)]*)\)', re.IGNORECASE) | ||
58 | with open(srcfiles[0], 'r') as f: | ||
59 | for line in f: | ||
60 | res = proj_re.match(line.strip()) | ||
61 | if res: | ||
62 | extravalues['PN'] = res.group(1).split()[0] | ||
63 | |||
64 | return values | ||
65 | |||
44 | class SconsRecipeHandler(RecipeHandler): | 66 | class SconsRecipeHandler(RecipeHandler): |
45 | def process(self, srctree, classes, lines_before, lines_after, handled): | 67 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): |
46 | if 'buildsystem' in handled: | 68 | if 'buildsystem' in handled: |
47 | return False | 69 | return False |
48 | 70 | ||
@@ -56,7 +78,7 @@ class SconsRecipeHandler(RecipeHandler): | |||
56 | return False | 78 | return False |
57 | 79 | ||
58 | class QmakeRecipeHandler(RecipeHandler): | 80 | class QmakeRecipeHandler(RecipeHandler): |
59 | def process(self, srctree, classes, lines_before, lines_after, handled): | 81 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): |
60 | if 'buildsystem' in handled: | 82 | if 'buildsystem' in handled: |
61 | return False | 83 | return False |
62 | 84 | ||
@@ -67,14 +89,14 @@ class QmakeRecipeHandler(RecipeHandler): | |||
67 | return False | 89 | return False |
68 | 90 | ||
69 | class AutotoolsRecipeHandler(RecipeHandler): | 91 | class AutotoolsRecipeHandler(RecipeHandler): |
70 | def process(self, srctree, classes, lines_before, lines_after, handled): | 92 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): |
71 | if 'buildsystem' in handled: | 93 | if 'buildsystem' in handled: |
72 | return False | 94 | return False |
73 | 95 | ||
74 | autoconf = False | 96 | autoconf = False |
75 | if RecipeHandler.checkfiles(srctree, ['configure.ac', 'configure.in']): | 97 | if RecipeHandler.checkfiles(srctree, ['configure.ac', 'configure.in']): |
76 | autoconf = True | 98 | autoconf = True |
77 | values = AutotoolsRecipeHandler.extract_autotools_deps(lines_before, srctree) | 99 | values = AutotoolsRecipeHandler.extract_autotools_deps(lines_before, srctree, extravalues) |
78 | classes.extend(values.pop('inherit', '').split()) | 100 | classes.extend(values.pop('inherit', '').split()) |
79 | for var, value in values.iteritems(): | 101 | for var, value in values.iteritems(): |
80 | lines_before.append('%s = "%s"' % (var, value)) | 102 | lines_before.append('%s = "%s"' % (var, value)) |
@@ -88,6 +110,22 @@ class AutotoolsRecipeHandler(RecipeHandler): | |||
88 | autoconf = True | 110 | autoconf = True |
89 | break | 111 | break |
90 | 112 | ||
113 | if autoconf and not ('PV' in extravalues and 'PN' in extravalues): | ||
114 | # Last resort | ||
115 | conffile = RecipeHandler.checkfiles(srctree, ['configure']) | ||
116 | if conffile: | ||
117 | with open(conffile[0], 'r') as f: | ||
118 | for line in f: | ||
119 | line = line.strip() | ||
120 | if line.startswith('VERSION=') or line.startswith('PACKAGE_VERSION='): | ||
121 | pv = line.split('=')[1].strip('"\'') | ||
122 | if pv and not 'PV' in extravalues and validate_pv(pv): | ||
123 | extravalues['PV'] = pv | ||
124 | elif line.startswith('PACKAGE_NAME=') or line.startswith('PACKAGE='): | ||
125 | pn = line.split('=')[1].strip('"\'') | ||
126 | if pn and not 'PN' in extravalues: | ||
127 | extravalues['PN'] = pn | ||
128 | |||
91 | if autoconf: | 129 | if autoconf: |
92 | lines_before.append('# NOTE: if this software is not capable of being built in a separate build directory') | 130 | 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') | 131 | lines_before.append('# from the source, you should replace autotools with autotools-brokensep in the') |
@@ -102,7 +140,7 @@ class AutotoolsRecipeHandler(RecipeHandler): | |||
102 | return False | 140 | return False |
103 | 141 | ||
104 | @staticmethod | 142 | @staticmethod |
105 | def extract_autotools_deps(outlines, srctree, acfile=None): | 143 | def extract_autotools_deps(outlines, srctree, extravalues=None, acfile=None): |
106 | import shlex | 144 | import shlex |
107 | import oe.package | 145 | import oe.package |
108 | 146 | ||
@@ -122,6 +160,9 @@ class AutotoolsRecipeHandler(RecipeHandler): | |||
122 | lib_re = re.compile('AC_CHECK_LIB\(\[?([a-zA-Z0-9]*)\]?, .*') | 160 | lib_re = re.compile('AC_CHECK_LIB\(\[?([a-zA-Z0-9]*)\]?, .*') |
123 | progs_re = re.compile('_PROGS?\(\[?[a-zA-Z0-9]*\]?, \[?([^,\]]*)\]?[),].*') | 161 | progs_re = re.compile('_PROGS?\(\[?[a-zA-Z0-9]*\]?, \[?([^,\]]*)\]?[),].*') |
124 | dep_re = re.compile('([^ ><=]+)( [<>=]+ [^ ><=]+)?') | 162 | dep_re = re.compile('([^ ><=]+)( [<>=]+ [^ ><=]+)?') |
163 | ac_init_re = re.compile('AC_INIT\(([^,]+), *([^,]+)[,)].*') | ||
164 | am_init_re = re.compile('AM_INIT_AUTOMAKE\(([^,]+), *([^,]+)[,)].*') | ||
165 | define_re = re.compile(' *(m4_)?define\(([^,]+), *([^,]+)\)') | ||
125 | 166 | ||
126 | # Build up lib library->package mapping | 167 | # Build up lib library->package mapping |
127 | shlib_providers = oe.package.read_shlib_providers(tinfoil.config_data) | 168 | shlib_providers = oe.package.read_shlib_providers(tinfoil.config_data) |
@@ -157,55 +198,157 @@ class AutotoolsRecipeHandler(RecipeHandler): | |||
157 | else: | 198 | else: |
158 | raise | 199 | raise |
159 | 200 | ||
201 | defines = {} | ||
202 | def subst_defines(value): | ||
203 | newvalue = value | ||
204 | for define, defval in defines.iteritems(): | ||
205 | newvalue = newvalue.replace(define, defval) | ||
206 | if newvalue != value: | ||
207 | return subst_defines(newvalue) | ||
208 | return value | ||
209 | |||
210 | def process_value(value): | ||
211 | value = value.replace('[', '').replace(']', '') | ||
212 | if value.startswith('m4_esyscmd(') or value.startswith('m4_esyscmd_s('): | ||
213 | cmd = subst_defines(value[value.index('(')+1:-1]) | ||
214 | try: | ||
215 | if '|' in cmd: | ||
216 | cmd = 'set -o pipefail; ' + cmd | ||
217 | stdout, _ = bb.process.run(cmd, cwd=srctree, shell=True) | ||
218 | ret = stdout.rstrip() | ||
219 | except bb.process.ExecutionError as e: | ||
220 | ret = '' | ||
221 | elif value.startswith('m4_'): | ||
222 | return None | ||
223 | ret = subst_defines(value) | ||
224 | if ret: | ||
225 | ret = ret.strip('"\'') | ||
226 | return ret | ||
227 | |||
160 | # Since a configure.ac file is essentially a program, this is only ever going to be | 228 | # 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 | 229 | # a hack unfortunately; but it ought to be enough of an approximation |
162 | if acfile: | 230 | if acfile: |
163 | srcfiles = [acfile] | 231 | srcfiles = [acfile] |
164 | else: | 232 | else: |
165 | srcfiles = RecipeHandler.checkfiles(srctree, ['configure.ac', 'configure.in']) | 233 | srcfiles = RecipeHandler.checkfiles(srctree, ['acinclude.m4', 'configure.ac', 'configure.in']) |
234 | |||
166 | pcdeps = [] | 235 | pcdeps = [] |
167 | deps = [] | 236 | deps = [] |
168 | unmapped = [] | 237 | unmapped = [] |
169 | unmappedlibs = [] | 238 | unmappedlibs = [] |
170 | with open(srcfiles[0], 'r') as f: | 239 | |
171 | for line in f: | 240 | def process_macro(keyword, value): |
172 | if 'PKG_CHECK_MODULES' in line: | 241 | if keyword == 'PKG_CHECK_MODULES': |
173 | res = pkg_re.search(line) | 242 | res = pkg_re.search(value) |
174 | if res: | 243 | if res: |
175 | res = dep_re.findall(res.group(1)) | 244 | 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: | 245 | if res: |
184 | for prog in shlex.split(res.group(1)): | 246 | pcdeps.extend([x[0] for x in res]) |
185 | prog = prog.split()[0] | 247 | inherits.append('pkgconfig') |
186 | progclass = progclassmap.get(prog, None) | 248 | elif keyword == 'AM_GNU_GETTEXT': |
187 | if progclass: | 249 | inherits.append('gettext') |
188 | inherits.append(progclass) | 250 | elif keyword == 'AC_CHECK_PROG' or keyword == 'AC_PATH_PROG': |
251 | res = progs_re.search(value) | ||
252 | if res: | ||
253 | for prog in shlex.split(res.group(1)): | ||
254 | prog = prog.split()[0] | ||
255 | progclass = progclassmap.get(prog, None) | ||
256 | if progclass: | ||
257 | inherits.append(progclass) | ||
258 | else: | ||
259 | progdep = progmap.get(prog, None) | ||
260 | if progdep: | ||
261 | deps.append(progdep) | ||
189 | else: | 262 | else: |
190 | progdep = progmap.get(prog, None) | 263 | if not prog.startswith('$'): |
191 | if progdep: | 264 | unmapped.append(prog) |
192 | deps.append(progdep) | 265 | elif keyword == 'AC_CHECK_LIB': |
193 | else: | 266 | res = lib_re.search(value) |
194 | if not prog.startswith('$'): | 267 | if res: |
195 | unmapped.append(prog) | 268 | lib = res.group(1) |
196 | elif 'AC_CHECK_LIB' in line: | 269 | libdep = recipelibmap.get(lib, None) |
197 | res = lib_re.search(line) | 270 | if libdep: |
271 | deps.append(libdep) | ||
272 | else: | ||
273 | if libdep is None: | ||
274 | if not lib.startswith('$'): | ||
275 | unmappedlibs.append(lib) | ||
276 | elif keyword == 'AC_PATH_X': | ||
277 | deps.append('libx11') | ||
278 | elif keyword == 'AC_INIT': | ||
279 | if extravalues is not None: | ||
280 | res = ac_init_re.match(value) | ||
198 | if res: | 281 | if res: |
199 | lib = res.group(1) | 282 | extravalues['PN'] = process_value(res.group(1)) |
200 | libdep = recipelibmap.get(lib, None) | 283 | pv = process_value(res.group(2)) |
201 | if libdep: | 284 | if validate_pv(pv): |
202 | deps.append(libdep) | 285 | extravalues['PV'] = pv |
203 | else: | 286 | elif keyword == 'AM_INIT_AUTOMAKE': |
204 | if libdep is None: | 287 | if extravalues is not None: |
205 | if not lib.startswith('$'): | 288 | if 'PN' not in extravalues: |
206 | unmappedlibs.append(lib) | 289 | res = am_init_re.match(value) |
207 | elif 'AC_PATH_X' in line: | 290 | if res: |
208 | deps.append('libx11') | 291 | if res.group(1) != 'AC_PACKAGE_NAME': |
292 | extravalues['PN'] = process_value(res.group(1)) | ||
293 | pv = process_value(res.group(2)) | ||
294 | if validate_pv(pv): | ||
295 | extravalues['PV'] = pv | ||
296 | elif keyword == 'define(': | ||
297 | res = define_re.match(value) | ||
298 | if res: | ||
299 | key = res.group(2).strip('[]') | ||
300 | value = process_value(res.group(3)) | ||
301 | if value is not None: | ||
302 | defines[key] = value | ||
303 | |||
304 | keywords = ['PKG_CHECK_MODULES', | ||
305 | 'AM_GNU_GETTEXT', | ||
306 | 'AC_CHECK_PROG', | ||
307 | 'AC_PATH_PROG', | ||
308 | 'AC_CHECK_LIB', | ||
309 | 'AC_PATH_X', | ||
310 | 'AC_INIT', | ||
311 | 'AM_INIT_AUTOMAKE', | ||
312 | 'define(', | ||
313 | ] | ||
314 | for srcfile in srcfiles: | ||
315 | nesting = 0 | ||
316 | in_keyword = '' | ||
317 | partial = '' | ||
318 | with open(srcfile, 'r') as f: | ||
319 | for line in f: | ||
320 | if in_keyword: | ||
321 | partial += ' ' + line.strip() | ||
322 | if partial.endswith('\\'): | ||
323 | partial = partial[:-1] | ||
324 | nesting = nesting + line.count('(') - line.count(')') | ||
325 | if nesting == 0: | ||
326 | process_macro(in_keyword, partial) | ||
327 | partial = '' | ||
328 | in_keyword = '' | ||
329 | else: | ||
330 | for keyword in keywords: | ||
331 | if keyword in line: | ||
332 | nesting = line.count('(') - line.count(')') | ||
333 | if nesting > 0: | ||
334 | partial = line.strip() | ||
335 | if partial.endswith('\\'): | ||
336 | partial = partial[:-1] | ||
337 | in_keyword = keyword | ||
338 | else: | ||
339 | process_macro(keyword, line.strip()) | ||
340 | break | ||
341 | |||
342 | if in_keyword: | ||
343 | process_macro(in_keyword, partial) | ||
344 | |||
345 | if extravalues: | ||
346 | for k,v in extravalues.items(): | ||
347 | if v: | ||
348 | if v.startswith('$') or v.startswith('@') or v.startswith('%'): | ||
349 | del extravalues[k] | ||
350 | else: | ||
351 | extravalues[k] = v.strip('"\'').rstrip('()') | ||
209 | 352 | ||
210 | if unmapped: | 353 | if unmapped: |
211 | outlines.append('# NOTE: the following prog dependencies are unknown, ignoring: %s' % ' '.join(unmapped)) | 354 | outlines.append('# NOTE: the following prog dependencies are unknown, ignoring: %s' % ' '.join(unmapped)) |
@@ -240,7 +383,7 @@ class AutotoolsRecipeHandler(RecipeHandler): | |||
240 | 383 | ||
241 | 384 | ||
242 | class MakefileRecipeHandler(RecipeHandler): | 385 | class MakefileRecipeHandler(RecipeHandler): |
243 | def process(self, srctree, classes, lines_before, lines_after, handled): | 386 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): |
244 | if 'buildsystem' in handled: | 387 | if 'buildsystem' in handled: |
245 | return False | 388 | return False |
246 | 389 | ||
@@ -307,6 +450,53 @@ class MakefileRecipeHandler(RecipeHandler): | |||
307 | self.genfunction(lines_after, 'do_install', ['# Specify install commands here']) | 450 | self.genfunction(lines_after, 'do_install', ['# Specify install commands here']) |
308 | 451 | ||
309 | 452 | ||
453 | class VersionFileRecipeHandler(RecipeHandler): | ||
454 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): | ||
455 | if 'PV' not in extravalues: | ||
456 | # Look for a VERSION or version file containing a single line consisting | ||
457 | # only of a version number | ||
458 | filelist = RecipeHandler.checkfiles(srctree, ['VERSION', 'version']) | ||
459 | version = None | ||
460 | for fileitem in filelist: | ||
461 | linecount = 0 | ||
462 | with open(fileitem, 'r') as f: | ||
463 | for line in f: | ||
464 | line = line.rstrip().strip('"\'') | ||
465 | linecount += 1 | ||
466 | if line: | ||
467 | if linecount > 1: | ||
468 | version = None | ||
469 | break | ||
470 | else: | ||
471 | if validate_pv(line): | ||
472 | version = line | ||
473 | if version: | ||
474 | extravalues['PV'] = version | ||
475 | break | ||
476 | |||
477 | |||
478 | class SpecFileRecipeHandler(RecipeHandler): | ||
479 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): | ||
480 | if 'PV' in extravalues and 'PN' in extravalues: | ||
481 | return | ||
482 | filelist = RecipeHandler.checkfiles(srctree, ['*.spec'], recursive=True) | ||
483 | pn = None | ||
484 | pv = None | ||
485 | for fileitem in filelist: | ||
486 | linecount = 0 | ||
487 | with open(fileitem, 'r') as f: | ||
488 | for line in f: | ||
489 | if line.startswith('Name:') and not pn: | ||
490 | pn = line.split(':')[1].strip() | ||
491 | if line.startswith('Version:') and not pv: | ||
492 | pv = line.split(':')[1].strip() | ||
493 | if pv or pn: | ||
494 | if pv and not 'PV' in extravalues and validate_pv(pv): | ||
495 | extravalues['PV'] = pv | ||
496 | if pn and not 'PN' in extravalues: | ||
497 | extravalues['PN'] = pn | ||
498 | break | ||
499 | |||
310 | def register_recipe_handlers(handlers): | 500 | def register_recipe_handlers(handlers): |
311 | # These are in a specific order so that the right one is detected first | 501 | # These are in a specific order so that the right one is detected first |
312 | handlers.append(CmakeRecipeHandler()) | 502 | handlers.append(CmakeRecipeHandler()) |
@@ -314,3 +504,5 @@ def register_recipe_handlers(handlers): | |||
314 | handlers.append(SconsRecipeHandler()) | 504 | handlers.append(SconsRecipeHandler()) |
315 | handlers.append(QmakeRecipeHandler()) | 505 | handlers.append(QmakeRecipeHandler()) |
316 | handlers.append(MakefileRecipeHandler()) | 506 | handlers.append(MakefileRecipeHandler()) |
507 | handlers.append((VersionFileRecipeHandler(), -1)) | ||
508 | handlers.append((SpecFileRecipeHandler(), -1)) | ||