blob: bb9fb9b049272a27b18405fbd523a9591738c281 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Recipe creation tool - create command plugin
2#
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05003# Copyright (C) 2014-2016 Intel Corporation
Patrick Williamsc124f4f2015-09-15 14:41:29 -05004#
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
18import sys
19import os
20import argparse
21import glob
22import fnmatch
23import re
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024import json
Patrick Williamsc124f4f2015-09-15 14:41:29 -050025import logging
26import scriptutils
Patrick Williamsf1e5d692016-03-30 15:21:19 -050027import urlparse
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050028import hashlib
Patrick Williamsc124f4f2015-09-15 14:41:29 -050029
30logger = logging.getLogger('recipetool')
31
32tinfoil = None
33plugins = None
34
35def plugin_init(pluginlist):
36 # Take a reference to the list so we can use it later
37 global plugins
38 plugins = pluginlist
39
40def tinfoil_init(instance):
41 global tinfoil
42 tinfoil = instance
43
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050044class RecipeHandler(object):
45 recipelibmap = {}
46 recipeheadermap = {}
47 recipecmakefilemap = {}
48 recipebinmap = {}
49
Patrick Williamsc124f4f2015-09-15 14:41:29 -050050 @staticmethod
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050051 def load_libmap(d):
52 '''Load library->recipe mapping'''
53 import oe.package
54
55 if RecipeHandler.recipelibmap:
56 return
57 # First build up library->package mapping
58 shlib_providers = oe.package.read_shlib_providers(d)
59 libdir = d.getVar('libdir', True)
60 base_libdir = d.getVar('base_libdir', True)
61 libpaths = list(set([base_libdir, libdir]))
62 libname_re = re.compile('^lib(.+)\.so.*$')
63 pkglibmap = {}
64 for lib, item in shlib_providers.iteritems():
65 for path, pkg in item.iteritems():
66 if path in libpaths:
67 res = libname_re.match(lib)
68 if res:
69 libname = res.group(1)
70 if not libname in pkglibmap:
71 pkglibmap[libname] = pkg[0]
72 else:
73 logger.debug('unable to extract library name from %s' % lib)
74
75 # Now turn it into a library->recipe mapping
76 pkgdata_dir = d.getVar('PKGDATA_DIR', True)
77 for libname, pkg in pkglibmap.iteritems():
78 try:
79 with open(os.path.join(pkgdata_dir, 'runtime', pkg)) as f:
80 for line in f:
81 if line.startswith('PN:'):
82 RecipeHandler.recipelibmap[libname] = line.split(':', 1)[-1].strip()
83 break
84 except IOError as ioe:
85 if ioe.errno == 2:
86 logger.warn('unable to find a pkgdata file for package %s' % pkg)
87 else:
88 raise
89
90 # Some overrides - these should be mapped to the virtual
91 RecipeHandler.recipelibmap['GL'] = 'virtual/libgl'
92 RecipeHandler.recipelibmap['EGL'] = 'virtual/egl'
93 RecipeHandler.recipelibmap['GLESv2'] = 'virtual/libgles2'
94
95 @staticmethod
96 def load_devel_filemap(d):
97 '''Build up development file->recipe mapping'''
98 if RecipeHandler.recipeheadermap:
99 return
100 pkgdata_dir = d.getVar('PKGDATA_DIR', True)
101 includedir = d.getVar('includedir', True)
102 cmakedir = os.path.join(d.getVar('libdir', True), 'cmake')
103 for pkg in glob.glob(os.path.join(pkgdata_dir, 'runtime', '*-dev')):
104 with open(os.path.join(pkgdata_dir, 'runtime', pkg)) as f:
105 pn = None
106 headers = []
107 cmakefiles = []
108 for line in f:
109 if line.startswith('PN:'):
110 pn = line.split(':', 1)[-1].strip()
111 elif line.startswith('FILES_INFO:'):
112 val = line.split(':', 1)[1].strip()
113 dictval = json.loads(val)
114 for fullpth in sorted(dictval):
115 if fullpth.startswith(includedir) and fullpth.endswith('.h'):
116 headers.append(os.path.relpath(fullpth, includedir))
117 elif fullpth.startswith(cmakedir) and fullpth.endswith('.cmake'):
118 cmakefiles.append(os.path.relpath(fullpth, cmakedir))
119 if pn and headers:
120 for header in headers:
121 RecipeHandler.recipeheadermap[header] = pn
122 if pn and cmakefiles:
123 for fn in cmakefiles:
124 RecipeHandler.recipecmakefilemap[fn] = pn
125
126 @staticmethod
127 def load_binmap(d):
128 '''Build up native binary->recipe mapping'''
129 if RecipeHandler.recipebinmap:
130 return
131 sstate_manifests = d.getVar('SSTATE_MANIFESTS', True)
132 staging_bindir_native = d.getVar('STAGING_BINDIR_NATIVE', True)
133 build_arch = d.getVar('BUILD_ARCH', True)
134 fileprefix = 'manifest-%s-' % build_arch
135 for fn in glob.glob(os.path.join(sstate_manifests, '%s*-native.populate_sysroot' % fileprefix)):
136 with open(fn, 'r') as f:
137 pn = os.path.basename(fn).rsplit('.', 1)[0][len(fileprefix):]
138 for line in f:
139 if line.startswith(staging_bindir_native):
140 prog = os.path.basename(line.rstrip())
141 RecipeHandler.recipebinmap[prog] = pn
142
143 @staticmethod
144 def checkfiles(path, speclist, recursive=False):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500145 results = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500146 if recursive:
147 for root, _, files in os.walk(path):
148 for fn in files:
149 for spec in speclist:
150 if fnmatch.fnmatch(fn, spec):
151 results.append(os.path.join(root, fn))
152 else:
153 for spec in speclist:
154 results.extend(glob.glob(os.path.join(path, spec)))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500155 return results
156
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500157 @staticmethod
158 def handle_depends(libdeps, pcdeps, deps, outlines, values, d):
159 if pcdeps:
160 recipemap = read_pkgconfig_provides(d)
161 if libdeps:
162 RecipeHandler.load_libmap(d)
163
164 ignorelibs = ['socket']
165 ignoredeps = ['gcc-runtime', 'glibc', 'uclibc', 'musl', 'tar-native', 'binutils-native', 'coreutils-native']
166
167 unmappedpc = []
168 pcdeps = list(set(pcdeps))
169 for pcdep in pcdeps:
170 if isinstance(pcdep, basestring):
171 recipe = recipemap.get(pcdep, None)
172 if recipe:
173 deps.append(recipe)
174 else:
175 if not pcdep.startswith('$'):
176 unmappedpc.append(pcdep)
177 else:
178 for item in pcdep:
179 recipe = recipemap.get(pcdep, None)
180 if recipe:
181 deps.append(recipe)
182 break
183 else:
184 unmappedpc.append('(%s)' % ' or '.join(pcdep))
185
186 unmappedlibs = []
187 for libdep in libdeps:
188 if isinstance(libdep, tuple):
189 lib, header = libdep
190 else:
191 lib = libdep
192 header = None
193
194 if lib in ignorelibs:
195 logger.debug('Ignoring library dependency %s' % lib)
196 continue
197
198 recipe = RecipeHandler.recipelibmap.get(lib, None)
199 if recipe:
200 deps.append(recipe)
201 elif recipe is None:
202 if header:
203 RecipeHandler.load_devel_filemap(d)
204 recipe = RecipeHandler.recipeheadermap.get(header, None)
205 if recipe:
206 deps.append(recipe)
207 elif recipe is None:
208 unmappedlibs.append(lib)
209 else:
210 unmappedlibs.append(lib)
211
212 deps = set(deps).difference(set(ignoredeps))
213
214 if unmappedpc:
215 outlines.append('# NOTE: unable to map the following pkg-config dependencies: %s' % ' '.join(unmappedpc))
216 outlines.append('# (this is based on recipes that have previously been built and packaged)')
217
218 if unmappedlibs:
219 outlines.append('# NOTE: the following library dependencies are unknown, ignoring: %s' % ' '.join(list(set(unmappedlibs))))
220 outlines.append('# (this is based on recipes that have previously been built and packaged)')
221
222 if deps:
223 values['DEPENDS'] = ' '.join(deps)
224
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500225 def genfunction(self, outlines, funcname, content, python=False, forcespace=False):
226 if python:
227 prefix = 'python '
228 else:
229 prefix = ''
230 outlines.append('%s%s () {' % (prefix, funcname))
231 if python or forcespace:
232 indent = ' '
233 else:
234 indent = '\t'
235 addnoop = not python
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500236 for line in content:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500237 outlines.append('%s%s' % (indent, line))
238 if addnoop:
239 strippedline = line.lstrip()
240 if strippedline and not strippedline.startswith('#'):
241 addnoop = False
242 if addnoop:
243 # Without this there'll be a syntax error
244 outlines.append('%s:' % indent)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500245 outlines.append('}')
246 outlines.append('')
247
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500248 def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500249 return False
250
251
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500252def validate_pv(pv):
253 if not pv or '_version' in pv.lower() or pv[0] not in '0123456789':
254 return False
255 return True
256
257def determine_from_filename(srcfile):
258 """Determine name and version from a filename"""
259 part = ''
260 if '.tar.' in srcfile:
261 namepart = srcfile.split('.tar.')[0].lower()
262 else:
263 namepart = os.path.splitext(srcfile)[0].lower()
264 splitval = namepart.rsplit('_', 1)
265 if len(splitval) == 1:
266 splitval = namepart.rsplit('-', 1)
267 pn = splitval[0].replace('_', '-')
268 if len(splitval) > 1:
269 if splitval[1][0] in '0123456789':
270 pv = splitval[1]
271 else:
272 pn = '-'.join(splitval).replace('_', '-')
273 pv = None
274 else:
275 pv = None
276 return (pn, pv)
277
278def determine_from_url(srcuri):
279 """Determine name and version from a URL"""
280 pn = None
281 pv = None
282 parseres = urlparse.urlparse(srcuri.lower().split(';', 1)[0])
283 if parseres.path:
284 if 'github.com' in parseres.netloc:
285 res = re.search(r'.*/(.*?)/archive/(.*)-final\.(tar|zip)', parseres.path)
286 if res:
287 pn = res.group(1).strip().replace('_', '-')
288 pv = res.group(2).strip().replace('_', '.')
289 else:
290 res = re.search(r'.*/(.*?)/archive/v?(.*)\.(tar|zip)', parseres.path)
291 if res:
292 pn = res.group(1).strip().replace('_', '-')
293 pv = res.group(2).strip().replace('_', '.')
294 elif 'bitbucket.org' in parseres.netloc:
295 res = re.search(r'.*/(.*?)/get/[a-zA-Z_-]*([0-9][0-9a-zA-Z_.]*)\.(tar|zip)', parseres.path)
296 if res:
297 pn = res.group(1).strip().replace('_', '-')
298 pv = res.group(2).strip().replace('_', '.')
299
300 if not pn and not pv:
301 srcfile = os.path.basename(parseres.path.rstrip('/'))
302 pn, pv = determine_from_filename(srcfile)
303
304 logger.debug('Determined from source URL: name = "%s", version = "%s"' % (pn, pv))
305 return (pn, pv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500306
307def supports_srcrev(uri):
308 localdata = bb.data.createCopy(tinfoil.config_data)
309 # This is a bit sad, but if you don't have this set there can be some
310 # odd interactions with the urldata cache which lead to errors
311 localdata.setVar('SRCREV', '${AUTOREV}')
312 bb.data.update_data(localdata)
313 fetcher = bb.fetch2.Fetch([uri], localdata)
314 urldata = fetcher.ud
315 for u in urldata:
316 if urldata[u].method.supports_srcrev():
317 return True
318 return False
319
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500320def reformat_git_uri(uri):
321 '''Convert any http[s]://....git URI into git://...;protocol=http[s]'''
322 checkuri = uri.split(';', 1)[0]
323 if checkuri.endswith('.git') or '/git/' in checkuri:
324 res = re.match('(https?)://([^;]+(\.git)?)(;.*)?$', uri)
325 if res:
326 # Need to switch the URI around so that the git fetcher is used
327 return 'git://%s;protocol=%s%s' % (res.group(2), res.group(1), res.group(4) or '')
328 return uri
329
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500330def create_recipe(args):
331 import bb.process
332 import tempfile
333 import shutil
334
335 pkgarch = ""
336 if args.machine:
337 pkgarch = "${MACHINE_ARCH}"
338
339 checksums = (None, None)
340 tempsrc = ''
341 srcsubdir = ''
342 srcrev = '${AUTOREV}'
343 if '://' in args.source:
344 # Fetch a URL
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500345 fetchuri = reformat_git_uri(urlparse.urldefrag(args.source)[0])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500346 if args.binary:
347 # Assume the archive contains the directory structure verbatim
348 # so we need to extract to a subdirectory
349 fetchuri += ';subdir=%s' % os.path.splitext(os.path.basename(urlparse.urlsplit(fetchuri).path))[0]
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500350 srcuri = fetchuri
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500351 rev_re = re.compile(';rev=([^;]+)')
352 res = rev_re.search(srcuri)
353 if res:
354 srcrev = res.group(1)
355 srcuri = rev_re.sub('', srcuri)
356 tempsrc = tempfile.mkdtemp(prefix='recipetool-')
357 srctree = tempsrc
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500358 if fetchuri.startswith('npm://'):
359 # Check if npm is available
360 npm = bb.utils.which(tinfoil.config_data.getVar('PATH', True), 'npm')
361 if not npm:
362 logger.error('npm:// URL requested but npm is not available - you need to either build nodejs-native or install npm using your package manager')
363 sys.exit(1)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500364 logger.info('Fetching %s...' % srcuri)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500365 try:
366 checksums = scriptutils.fetch_uri(tinfoil.config_data, fetchuri, srctree, srcrev)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500367 except bb.fetch2.BBFetchException as e:
368 logger.error(str(e).rstrip())
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500369 sys.exit(1)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500370 dirlist = os.listdir(srctree)
371 if 'git.indirectionsymlink' in dirlist:
372 dirlist.remove('git.indirectionsymlink')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500373 if len(dirlist) == 1:
374 singleitem = os.path.join(srctree, dirlist[0])
375 if os.path.isdir(singleitem):
376 # We unpacked a single directory, so we should use that
377 srcsubdir = dirlist[0]
378 srctree = os.path.join(srctree, srcsubdir)
379 else:
380 with open(singleitem, 'r') as f:
381 if '<html' in f.read(100).lower():
382 logger.error('Fetching "%s" returned a single HTML page - check the URL is correct and functional' % fetchuri)
383 sys.exit(1)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500384 else:
385 # Assume we're pointing to an existing source tree
386 if args.extract_to:
387 logger.error('--extract-to cannot be specified if source is a directory')
388 sys.exit(1)
389 if not os.path.isdir(args.source):
390 logger.error('Invalid source directory %s' % args.source)
391 sys.exit(1)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500392 srctree = args.source
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500393 srcuri = ''
394 if os.path.exists(os.path.join(srctree, '.git')):
395 # Try to get upstream repo location from origin remote
396 try:
397 stdout, _ = bb.process.run('git remote -v', cwd=srctree, shell=True)
398 except bb.process.ExecutionError as e:
399 stdout = None
400 if stdout:
401 for line in stdout.splitlines():
402 splitline = line.split()
403 if len(splitline) > 1:
404 if splitline[0] == 'origin' and '://' in splitline[1]:
405 srcuri = reformat_git_uri(splitline[1])
406 srcsubdir = 'git'
407 break
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500408
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500409 if args.src_subdir:
410 srcsubdir = os.path.join(srcsubdir, args.src_subdir)
411 srctree_use = os.path.join(srctree, args.src_subdir)
412 else:
413 srctree_use = srctree
414
415 if args.outfile and os.path.isdir(args.outfile):
416 outfile = None
417 outdir = args.outfile
418 else:
419 outfile = args.outfile
420 outdir = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500421 if outfile and outfile != '-':
422 if os.path.exists(outfile):
423 logger.error('Output file %s already exists' % outfile)
424 sys.exit(1)
425
426 lines_before = []
427 lines_after = []
428
429 lines_before.append('# Recipe created by %s' % os.path.basename(sys.argv[0]))
430 lines_before.append('# This is the basis of a recipe and may need further editing in order to be fully functional.')
431 lines_before.append('# (Feel free to remove these comments when editing.)')
432 lines_before.append('#')
433
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500434 licvalues = guess_license(srctree_use)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500435 lic_files_chksum = []
436 if licvalues:
437 licenses = []
438 for licvalue in licvalues:
439 if not licvalue[0] in licenses:
440 licenses.append(licvalue[0])
441 lic_files_chksum.append('file://%s;md5=%s' % (licvalue[1], licvalue[2]))
442 lines_before.append('# WARNING: the following LICENSE and LIC_FILES_CHKSUM values are best guesses - it is')
443 lines_before.append('# your responsibility to verify that the values are complete and correct.')
444 if len(licvalues) > 1:
445 lines_before.append('#')
446 lines_before.append('# NOTE: multiple licenses have been detected; if that is correct you should separate')
447 lines_before.append('# these in the LICENSE value using & if the multiple licenses all apply, or | if there')
448 lines_before.append('# is a choice between the multiple licenses. If in doubt, check the accompanying')
449 lines_before.append('# documentation to determine which situation is applicable.')
450 else:
451 lines_before.append('# Unable to find any files that looked like license statements. Check the accompanying')
452 lines_before.append('# documentation and source headers and set LICENSE and LIC_FILES_CHKSUM accordingly.')
453 lines_before.append('#')
454 lines_before.append('# NOTE: LICENSE is being set to "CLOSED" to allow you to at least start building - if')
455 lines_before.append('# this is not accurate with respect to the licensing of the software being built (it')
456 lines_before.append('# will not be in most cases) you must specify the correct value before using this')
457 lines_before.append('# recipe for anything other than initial testing/development!')
458 licenses = ['CLOSED']
459 lines_before.append('LICENSE = "%s"' % ' '.join(licenses))
460 lines_before.append('LIC_FILES_CHKSUM = "%s"' % ' \\\n '.join(lic_files_chksum))
461 lines_before.append('')
462
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500463 classes = []
464
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500465 # FIXME This is kind of a hack, we probably ought to be using bitbake to do this
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500466 pn = None
467 pv = None
468 if outfile:
469 recipefn = os.path.splitext(os.path.basename(outfile))[0]
470 fnsplit = recipefn.split('_')
471 if len(fnsplit) > 1:
472 pn = fnsplit[0]
473 pv = fnsplit[1]
474 else:
475 pn = recipefn
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500476
477 if args.version:
478 pv = args.version
479
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500480 if args.name:
481 pn = args.name
482 if args.name.endswith('-native'):
483 if args.also_native:
484 logger.error('--also-native cannot be specified for a recipe named *-native (*-native denotes a recipe that is already only for native) - either remove the -native suffix from the name or drop --also-native')
485 sys.exit(1)
486 classes.append('native')
487 elif args.name.startswith('nativesdk-'):
488 if args.also_native:
489 logger.error('--also-native cannot be specified for a recipe named nativesdk-* (nativesdk-* denotes a recipe that is already only for nativesdk)')
490 sys.exit(1)
491 classes.append('nativesdk')
492
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500493 if pv and pv not in 'git svn hg'.split():
494 realpv = pv
495 else:
496 realpv = None
497
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500498 if srcuri and not realpv or not pn:
499 name_pn, name_pv = determine_from_url(srcuri)
500 if name_pn and not pn:
501 pn = name_pn
502 if name_pv and not realpv:
503 realpv = name_pv
504
505
506 if not srcuri:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500507 lines_before.append('# No information for SRC_URI yet (only an external source tree was specified)')
508 lines_before.append('SRC_URI = "%s"' % srcuri)
509 (md5value, sha256value) = checksums
510 if md5value:
511 lines_before.append('SRC_URI[md5sum] = "%s"' % md5value)
512 if sha256value:
513 lines_before.append('SRC_URI[sha256sum] = "%s"' % sha256value)
514 if srcuri and supports_srcrev(srcuri):
515 lines_before.append('')
516 lines_before.append('# Modify these as desired')
517 lines_before.append('PV = "%s+git${SRCPV}"' % (realpv or '1.0'))
518 lines_before.append('SRCREV = "%s"' % srcrev)
519 lines_before.append('')
520
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500521 if srcsubdir:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500522 lines_before.append('S = "${WORKDIR}/%s"' % srcsubdir)
523 lines_before.append('')
524
525 if pkgarch:
526 lines_after.append('PACKAGE_ARCH = "%s"' % pkgarch)
527 lines_after.append('')
528
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500529 if args.binary:
530 lines_after.append('INSANE_SKIP_${PN} += "already-stripped"')
531 lines_after.append('')
532
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500533 # Find all plugins that want to register handlers
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500534 logger.debug('Loading recipe handlers')
535 raw_handlers = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500536 for plugin in plugins:
537 if hasattr(plugin, 'register_recipe_handlers'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500538 plugin.register_recipe_handlers(raw_handlers)
539 # Sort handlers by priority
540 handlers = []
541 for i, handler in enumerate(raw_handlers):
542 if isinstance(handler, tuple):
543 handlers.append((handler[0], handler[1], i))
544 else:
545 handlers.append((handler, 0, i))
546 handlers.sort(key=lambda item: (item[1], -item[2]), reverse=True)
547 for handler, priority, _ in handlers:
548 logger.debug('Handler: %s (priority %d)' % (handler.__class__.__name__, priority))
549 handlers = [item[0] for item in handlers]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500550
551 # Apply the handlers
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500552 handled = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500553 handled.append(('license', licvalues))
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500554
555 if args.binary:
556 classes.append('bin_package')
557 handled.append('buildsystem')
558
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500559 extravalues = {}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500560 for handler in handlers:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500561 handler.process(srctree_use, classes, lines_before, lines_after, handled, extravalues)
562
563 extrafiles = extravalues.pop('extrafiles', {})
564
565 if not realpv:
566 realpv = extravalues.get('PV', None)
567 if realpv:
568 if not validate_pv(realpv):
569 realpv = None
570 else:
571 realpv = realpv.lower().split()[0]
572 if '_' in realpv:
573 realpv = realpv.replace('_', '-')
574 if not pn:
575 pn = extravalues.get('PN', None)
576 if pn:
577 if pn.startswith('GNU '):
578 pn = pn[4:]
579 if ' ' in pn:
580 # Probably a descriptive identifier rather than a proper name
581 pn = None
582 else:
583 pn = pn.lower()
584 if '_' in pn:
585 pn = pn.replace('_', '-')
586
587 if not outfile:
588 if not pn:
589 logger.error('Unable to determine short program name from source tree - please specify name with -N/--name or output file name with -o/--outfile')
590 # devtool looks for this specific exit code, so don't change it
591 sys.exit(15)
592 else:
593 if srcuri and srcuri.startswith(('git://', 'hg://', 'svn://')):
594 outfile = '%s_%s.bb' % (pn, srcuri.split(':', 1)[0])
595 elif realpv:
596 outfile = '%s_%s.bb' % (pn, realpv)
597 else:
598 outfile = '%s.bb' % pn
599 if outdir:
600 outfile = os.path.join(outdir, outfile)
601 # We need to check this again
602 if os.path.exists(outfile):
603 logger.error('Output file %s already exists' % outfile)
604 sys.exit(1)
605
606 # Move any extra files the plugins created to a directory next to the recipe
607 if extrafiles:
608 if outfile == '-':
609 extraoutdir = pn
610 else:
611 extraoutdir = os.path.join(os.path.dirname(outfile), pn)
612 bb.utils.mkdirhier(extraoutdir)
613 for destfn, extrafile in extrafiles.iteritems():
614 shutil.move(extrafile, os.path.join(extraoutdir, destfn))
615
616 lines = lines_before
617 lines_before = []
618 skipblank = True
619 for line in lines:
620 if skipblank:
621 skipblank = False
622 if not line:
623 continue
624 if line.startswith('S = '):
625 if realpv and pv not in 'git svn hg'.split():
626 line = line.replace(realpv, '${PV}')
627 if pn:
628 line = line.replace(pn, '${BPN}')
629 if line == 'S = "${WORKDIR}/${BPN}-${PV}"':
630 skipblank = True
631 continue
632 elif line.startswith('SRC_URI = '):
633 if realpv:
634 line = line.replace(realpv, '${PV}')
635 elif line.startswith('PV = '):
636 if realpv:
637 line = re.sub('"[^+]*\+', '"%s+' % realpv, line)
638 lines_before.append(line)
639
640 if args.also_native:
641 lines = lines_after
642 lines_after = []
643 bbclassextend = None
644 for line in lines:
645 if line.startswith('BBCLASSEXTEND ='):
646 splitval = line.split('"')
647 if len(splitval) > 1:
648 bbclassextend = splitval[1].split()
649 if not 'native' in bbclassextend:
650 bbclassextend.insert(0, 'native')
651 line = 'BBCLASSEXTEND = "%s"' % ' '.join(bbclassextend)
652 lines_after.append(line)
653 if not bbclassextend:
654 lines_after.append('BBCLASSEXTEND = "native"')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500655
656 outlines = []
657 outlines.extend(lines_before)
658 if classes:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500659 if outlines[-1] and not outlines[-1].startswith('#'):
660 outlines.append('')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500661 outlines.append('inherit %s' % ' '.join(classes))
662 outlines.append('')
663 outlines.extend(lines_after)
664
665 if args.extract_to:
666 scriptutils.git_convert_standalone_clone(srctree)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500667 if os.path.isdir(args.extract_to):
668 # If the directory exists we'll move the temp dir into it instead of
669 # its contents - of course, we could try to always move its contents
670 # but that is a pain if there are symlinks; the simplest solution is
671 # to just remove it first
672 os.rmdir(args.extract_to)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500673 shutil.move(srctree, args.extract_to)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500674 if tempsrc == srctree:
675 tempsrc = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500676 logger.info('Source extracted to %s' % args.extract_to)
677
678 if outfile == '-':
679 sys.stdout.write('\n'.join(outlines) + '\n')
680 else:
681 with open(outfile, 'w') as f:
682 f.write('\n'.join(outlines) + '\n')
683 logger.info('Recipe %s has been created; further editing may be required to make it fully functional' % outfile)
684
685 if tempsrc:
686 shutil.rmtree(tempsrc)
687
688 return 0
689
690def get_license_md5sums(d, static_only=False):
691 import bb.utils
692 md5sums = {}
693 if not static_only:
694 # Gather md5sums of license files in common license dir
695 commonlicdir = d.getVar('COMMON_LICENSE_DIR', True)
696 for fn in os.listdir(commonlicdir):
697 md5value = bb.utils.md5_file(os.path.join(commonlicdir, fn))
698 md5sums[md5value] = fn
699 # The following were extracted from common values in various recipes
700 # (double checking the license against the license file itself, not just
701 # the LICENSE value in the recipe)
702 md5sums['94d55d512a9ba36caa9b7df079bae19f'] = 'GPLv2'
703 md5sums['b234ee4d69f5fce4486a80fdaf4a4263'] = 'GPLv2'
704 md5sums['59530bdf33659b29e73d4adb9f9f6552'] = 'GPLv2'
705 md5sums['0636e73ff0215e8d672dc4c32c317bb3'] = 'GPLv2'
706 md5sums['eb723b61539feef013de476e68b5c50a'] = 'GPLv2'
707 md5sums['751419260aa954499f7abaabaa882bbe'] = 'GPLv2'
708 md5sums['393a5ca445f6965873eca0259a17f833'] = 'GPLv2'
709 md5sums['12f884d2ae1ff87c09e5b7ccc2c4ca7e'] = 'GPLv2'
710 md5sums['8ca43cbc842c2336e835926c2166c28b'] = 'GPLv2'
711 md5sums['ebb5c50ab7cab4baeffba14977030c07'] = 'GPLv2'
712 md5sums['c93c0550bd3173f4504b2cbd8991e50b'] = 'GPLv2'
713 md5sums['9ac2e7cff1ddaf48b6eab6028f23ef88'] = 'GPLv2'
714 md5sums['4325afd396febcb659c36b49533135d4'] = 'GPLv2'
715 md5sums['18810669f13b87348459e611d31ab760'] = 'GPLv2'
716 md5sums['d7810fab7487fb0aad327b76f1be7cd7'] = 'GPLv2' # the Linux kernel's COPYING file
717 md5sums['bbb461211a33b134d42ed5ee802b37ff'] = 'LGPLv2.1'
718 md5sums['7fbc338309ac38fefcd64b04bb903e34'] = 'LGPLv2.1'
719 md5sums['4fbd65380cdd255951079008b364516c'] = 'LGPLv2.1'
720 md5sums['2d5025d4aa3495befef8f17206a5b0a1'] = 'LGPLv2.1'
721 md5sums['fbc093901857fcd118f065f900982c24'] = 'LGPLv2.1'
722 md5sums['a6f89e2100d9b6cdffcea4f398e37343'] = 'LGPLv2.1'
723 md5sums['d8045f3b8f929c1cb29a1e3fd737b499'] = 'LGPLv2.1'
724 md5sums['fad9b3332be894bab9bc501572864b29'] = 'LGPLv2.1'
725 md5sums['3bf50002aefd002f49e7bb854063f7e7'] = 'LGPLv2'
726 md5sums['9f604d8a4f8e74f4f5140845a21b6674'] = 'LGPLv2'
727 md5sums['5f30f0716dfdd0d91eb439ebec522ec2'] = 'LGPLv2'
728 md5sums['55ca817ccb7d5b5b66355690e9abc605'] = 'LGPLv2'
729 md5sums['252890d9eee26aab7b432e8b8a616475'] = 'LGPLv2'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500730 md5sums['3214f080875748938ba060314b4f727d'] = 'LGPLv2'
731 md5sums['db979804f025cf55aabec7129cb671ed'] = 'LGPLv2'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500732 md5sums['d32239bcb673463ab874e80d47fae504'] = 'GPLv3'
733 md5sums['f27defe1e96c2e1ecd4e0c9be8967949'] = 'GPLv3'
734 md5sums['6a6a8e020838b23406c81b19c1d46df6'] = 'LGPLv3'
735 md5sums['3b83ef96387f14655fc854ddc3c6bd57'] = 'Apache-2.0'
736 md5sums['385c55653886acac3821999a3ccd17b3'] = 'Artistic-1.0 | GPL-2.0' # some perl modules
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500737 md5sums['54c7042be62e169199200bc6477f04d1'] = 'BSD-3-Clause'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500738 return md5sums
739
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500740def crunch_license(licfile):
741 '''
742 Remove non-material text from a license file and then check
743 its md5sum against a known list. This works well for licenses
744 which contain a copyright statement, but is also a useful way
745 to handle people's insistence upon reformatting the license text
746 slightly (with no material difference to the text of the
747 license).
748 '''
749
750 import oe.utils
751
752 # Note: these are carefully constructed!
753 license_title_re = re.compile('^\(?(#+ *)?(The )?.{1,10} [Ll]icen[sc]e( \(.{1,10}\))?\)?:?$')
754 license_statement_re = re.compile('^This (project|software) is( free software)? released under the .{1,10} [Ll]icen[sc]e:?$')
755 copyright_re = re.compile('^(#+)? *Copyright .*$')
756
757 crunched_md5sums = {}
758 # The following two were gleaned from the "forever" npm package
759 crunched_md5sums['0a97f8e4cbaf889d6fa51f84b89a79f6'] = 'ISC'
760 crunched_md5sums['eecf6429523cbc9693547cf2db790b5c'] = 'MIT'
761 # https://github.com/vasi/pixz/blob/master/LICENSE
762 crunched_md5sums['2f03392b40bbe663597b5bd3cc5ebdb9'] = 'BSD-2-Clause'
763 # https://github.com/waffle-gl/waffle/blob/master/LICENSE.txt
764 crunched_md5sums['e72e5dfef0b1a4ca8a3d26a60587db66'] = 'BSD-2-Clause'
765 # https://github.com/spigwitmer/fakeds1963s/blob/master/LICENSE
766 crunched_md5sums['8be76ac6d191671f347ee4916baa637e'] = 'GPLv2'
767 # https://github.com/datto/dattobd/blob/master/COPYING
768 # http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/GPLv2.TXT
769 crunched_md5sums['1d65c5ad4bf6489f85f4812bf08ae73d'] = 'GPLv2'
770 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
771 # http://git.neil.brown.name/?p=mdadm.git;a=blob;f=COPYING;h=d159169d1050894d3ea3b98e1c965c4058208fe1;hb=HEAD
772 crunched_md5sums['fb530f66a7a89ce920f0e912b5b66d4b'] = 'GPLv2'
773 # https://github.com/gkos/nrf24/blob/master/COPYING
774 crunched_md5sums['7b6aaa4daeafdfa6ed5443fd2684581b'] = 'GPLv2'
775 # https://github.com/josch09/resetusb/blob/master/COPYING
776 crunched_md5sums['8b8ac1d631a4d220342e83bcf1a1fbc3'] = 'GPLv3'
777 # https://github.com/FFmpeg/FFmpeg/blob/master/COPYING.LGPLv2.1
778 crunched_md5sums['2ea316ed973ae176e502e2297b574bb3'] = 'LGPLv2.1'
779 # unixODBC-2.3.4 COPYING
780 crunched_md5sums['1daebd9491d1e8426900b4fa5a422814'] = 'LGPLv2.1'
781 # https://github.com/FFmpeg/FFmpeg/blob/master/COPYING.LGPLv3
782 crunched_md5sums['2ebfb3bb49b9a48a075cc1425e7f4129'] = 'LGPLv3'
783 lictext = []
784 with open(licfile, 'r') as f:
785 for line in f:
786 # Drop opening statements
787 if copyright_re.match(line):
788 continue
789 elif license_title_re.match(line):
790 continue
791 elif license_statement_re.match(line):
792 continue
793 # Squash spaces, and replace smart quotes, double quotes
794 # and backticks with single quotes
795 line = oe.utils.squashspaces(line.strip()).decode("utf-8")
796 line = line.replace(u"\u2018", "'").replace(u"\u2019", "'").replace(u"\u201c","'").replace(u"\u201d", "'").replace('"', '\'').replace('`', '\'')
797 if line:
798 lictext.append(line)
799
800 m = hashlib.md5()
801 try:
802 m.update(' '.join(lictext))
803 md5val = m.hexdigest()
804 except UnicodeEncodeError:
805 md5val = None
806 lictext = ''
807 license = crunched_md5sums.get(md5val, None)
808 return license, md5val, lictext
809
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500810def guess_license(srctree):
811 import bb
812 md5sums = get_license_md5sums(tinfoil.config_data)
813
814 licenses = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500815 licspecs = ['*LICEN[CS]E*', 'COPYING*', '*[Ll]icense*', 'LEGAL*', '[Ll]egal*', '*GPL*', 'README.lic*', 'COPYRIGHT*', '[Cc]opyright*']
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500816 licfiles = []
817 for root, dirs, files in os.walk(srctree):
818 for fn in files:
819 for spec in licspecs:
820 if fnmatch.fnmatch(fn, spec):
821 fullpath = os.path.join(root, fn)
822 if not fullpath in licfiles:
823 licfiles.append(fullpath)
824 for licfile in licfiles:
825 md5value = bb.utils.md5_file(licfile)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500826 license = md5sums.get(md5value, None)
827 if not license:
828 license, crunched_md5, lictext = crunch_license(licfile)
829 if not license:
830 license = 'Unknown'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500831 licenses.append((license, os.path.relpath(licfile, srctree), md5value))
832
833 # FIXME should we grab at least one source file with a license header and add that too?
834
835 return licenses
836
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500837def split_pkg_licenses(licvalues, packages, outlines, fallback_licenses=None, pn='${PN}'):
838 """
839 Given a list of (license, path, md5sum) as returned by guess_license(),
840 a dict of package name to path mappings, write out a set of
841 package-specific LICENSE values.
842 """
843 pkglicenses = {pn: []}
844 for license, licpath, _ in licvalues:
845 for pkgname, pkgpath in packages.iteritems():
846 if licpath.startswith(pkgpath + '/'):
847 if pkgname in pkglicenses:
848 pkglicenses[pkgname].append(license)
849 else:
850 pkglicenses[pkgname] = [license]
851 break
852 else:
853 # Accumulate on the main package
854 pkglicenses[pn].append(license)
855 outlicenses = {}
856 for pkgname in packages:
857 license = ' '.join(list(set(pkglicenses.get(pkgname, ['Unknown']))))
858 if license == 'Unknown' and pkgname in fallback_licenses:
859 license = fallback_licenses[pkgname]
860 outlines.append('LICENSE_%s = "%s"' % (pkgname, license))
861 outlicenses[pkgname] = license.split()
862 return outlicenses
863
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500864def read_pkgconfig_provides(d):
865 pkgdatadir = d.getVar('PKGDATA_DIR', True)
866 pkgmap = {}
867 for fn in glob.glob(os.path.join(pkgdatadir, 'shlibs2', '*.pclist')):
868 with open(fn, 'r') as f:
869 for line in f:
870 pkgmap[os.path.basename(line.rstrip())] = os.path.splitext(os.path.basename(fn))[0]
871 recipemap = {}
872 for pc, pkg in pkgmap.iteritems():
873 pkgdatafile = os.path.join(pkgdatadir, 'runtime', pkg)
874 if os.path.exists(pkgdatafile):
875 with open(pkgdatafile, 'r') as f:
876 for line in f:
877 if line.startswith('PN: '):
878 recipemap[pc] = line.split(':', 1)[1].strip()
879 return recipemap
880
881def convert_pkginfo(pkginfofile):
882 values = {}
883 with open(pkginfofile, 'r') as f:
884 indesc = False
885 for line in f:
886 if indesc:
887 if line.strip():
888 values['DESCRIPTION'] += ' ' + line.strip()
889 else:
890 indesc = False
891 else:
892 splitline = line.split(': ', 1)
893 key = line[0]
894 value = line[1]
895 if key == 'LICENSE':
896 for dep in value.split(','):
897 dep = dep.split()[0]
898 mapped = depmap.get(dep, '')
899 if mapped:
900 depends.append(mapped)
901 elif key == 'License':
902 values['LICENSE'] = value
903 elif key == 'Summary':
904 values['SUMMARY'] = value
905 elif key == 'Description':
906 values['DESCRIPTION'] = value
907 indesc = True
908 return values
909
910def convert_debian(debpath):
911 # FIXME extend this mapping - perhaps use distro_alias.inc?
912 depmap = {'libz-dev': 'zlib'}
913
914 values = {}
915 depends = []
916 with open(os.path.join(debpath, 'control')) as f:
917 indesc = False
918 for line in f:
919 if indesc:
920 if line.strip():
921 if line.startswith(' This package contains'):
922 indesc = False
923 else:
924 values['DESCRIPTION'] += ' ' + line.strip()
925 else:
926 indesc = False
927 else:
928 splitline = line.split(':', 1)
929 key = line[0]
930 value = line[1]
931 if key == 'Build-Depends':
932 for dep in value.split(','):
933 dep = dep.split()[0]
934 mapped = depmap.get(dep, '')
935 if mapped:
936 depends.append(mapped)
937 elif key == 'Section':
938 values['SECTION'] = value
939 elif key == 'Description':
940 values['SUMMARY'] = value
941 indesc = True
942
943 if depends:
944 values['DEPENDS'] = ' '.join(depends)
945
946 return values
947
948
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500949def register_commands(subparsers):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500950 parser_create = subparsers.add_parser('create',
951 help='Create a new recipe',
952 description='Creates a new recipe from a source tree')
953 parser_create.add_argument('source', help='Path or URL to source')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500954 parser_create.add_argument('-o', '--outfile', help='Specify filename for recipe to create')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500955 parser_create.add_argument('-m', '--machine', help='Make recipe machine-specific as opposed to architecture-specific', action='store_true')
956 parser_create.add_argument('-x', '--extract-to', metavar='EXTRACTPATH', help='Assuming source is a URL, fetch it and extract it to the directory specified as %(metavar)s')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500957 parser_create.add_argument('-N', '--name', help='Name to use within recipe (PN)')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500958 parser_create.add_argument('-V', '--version', help='Version to use within recipe (PV)')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500959 parser_create.add_argument('-b', '--binary', help='Treat the source tree as something that should be installed verbatim (no compilation, same directory structure)', action='store_true')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500960 parser_create.add_argument('--also-native', help='Also add native variant (i.e. support building recipe for the build host as well as the target machine)', action='store_true')
961 parser_create.add_argument('--src-subdir', help='Specify subdirectory within source tree to use', metavar='SUBDIR')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500962 parser_create.set_defaults(func=create_recipe)
963