blob: dbd74a1ca3df2b1f69c7491948560747961cce75 [file] [log] [blame]
rjw1f884582022-01-06 17:20:42 +08001# Recipe creation tool - create command plugin
2#
3# Copyright (C) 2014-2017 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
18import sys
19import os
20import argparse
21import glob
22import fnmatch
23import re
24import json
25import logging
26import scriptutils
27from urllib.parse import urlparse, urldefrag, urlsplit
28import hashlib
29import bb.fetch2
30logger = logging.getLogger('recipetool')
31
32tinfoil = None
33plugins = None
34
35def log_error_cond(message, debugonly):
36 if debugonly:
37 logger.debug(message)
38 else:
39 logger.error(message)
40
41def log_info_cond(message, debugonly):
42 if debugonly:
43 logger.debug(message)
44 else:
45 logger.info(message)
46
47def plugin_init(pluginlist):
48 # Take a reference to the list so we can use it later
49 global plugins
50 plugins = pluginlist
51
52def tinfoil_init(instance):
53 global tinfoil
54 tinfoil = instance
55
56class RecipeHandler(object):
57 recipelibmap = {}
58 recipeheadermap = {}
59 recipecmakefilemap = {}
60 recipebinmap = {}
61
62 def __init__(self):
63 self._devtool = False
64
65 @staticmethod
66 def load_libmap(d):
67 '''Load library->recipe mapping'''
68 import oe.package
69
70 if RecipeHandler.recipelibmap:
71 return
72 # First build up library->package mapping
73 shlib_providers = oe.package.read_shlib_providers(d)
74 libdir = d.getVar('libdir')
75 base_libdir = d.getVar('base_libdir')
76 libpaths = list(set([base_libdir, libdir]))
77 libname_re = re.compile('^lib(.+)\.so.*$')
78 pkglibmap = {}
79 for lib, item in shlib_providers.items():
80 for path, pkg in item.items():
81 if path in libpaths:
82 res = libname_re.match(lib)
83 if res:
84 libname = res.group(1)
85 if not libname in pkglibmap:
86 pkglibmap[libname] = pkg[0]
87 else:
88 logger.debug('unable to extract library name from %s' % lib)
89
90 # Now turn it into a library->recipe mapping
91 pkgdata_dir = d.getVar('PKGDATA_DIR')
92 for libname, pkg in pkglibmap.items():
93 try:
94 with open(os.path.join(pkgdata_dir, 'runtime', pkg)) as f:
95 for line in f:
96 if line.startswith('PN:'):
97 RecipeHandler.recipelibmap[libname] = line.split(':', 1)[-1].strip()
98 break
99 except IOError as ioe:
100 if ioe.errno == 2:
101 logger.warning('unable to find a pkgdata file for package %s' % pkg)
102 else:
103 raise
104
105 # Some overrides - these should be mapped to the virtual
106 RecipeHandler.recipelibmap['GL'] = 'virtual/libgl'
107 RecipeHandler.recipelibmap['EGL'] = 'virtual/egl'
108 RecipeHandler.recipelibmap['GLESv2'] = 'virtual/libgles2'
109
110 @staticmethod
111 def load_devel_filemap(d):
112 '''Build up development file->recipe mapping'''
113 if RecipeHandler.recipeheadermap:
114 return
115 pkgdata_dir = d.getVar('PKGDATA_DIR')
116 includedir = d.getVar('includedir')
117 cmakedir = os.path.join(d.getVar('libdir'), 'cmake')
118 for pkg in glob.glob(os.path.join(pkgdata_dir, 'runtime', '*-dev')):
119 with open(os.path.join(pkgdata_dir, 'runtime', pkg)) as f:
120 pn = None
121 headers = []
122 cmakefiles = []
123 for line in f:
124 if line.startswith('PN:'):
125 pn = line.split(':', 1)[-1].strip()
126 elif line.startswith('FILES_INFO:'):
127 val = line.split(':', 1)[1].strip()
128 dictval = json.loads(val)
129 for fullpth in sorted(dictval):
130 if fullpth.startswith(includedir) and fullpth.endswith('.h'):
131 headers.append(os.path.relpath(fullpth, includedir))
132 elif fullpth.startswith(cmakedir) and fullpth.endswith('.cmake'):
133 cmakefiles.append(os.path.relpath(fullpth, cmakedir))
134 if pn and headers:
135 for header in headers:
136 RecipeHandler.recipeheadermap[header] = pn
137 if pn and cmakefiles:
138 for fn in cmakefiles:
139 RecipeHandler.recipecmakefilemap[fn] = pn
140
141 @staticmethod
142 def load_binmap(d):
143 '''Build up native binary->recipe mapping'''
144 if RecipeHandler.recipebinmap:
145 return
146 sstate_manifests = d.getVar('SSTATE_MANIFESTS')
147 staging_bindir_native = d.getVar('STAGING_BINDIR_NATIVE')
148 build_arch = d.getVar('BUILD_ARCH')
149 fileprefix = 'manifest-%s-' % build_arch
150 for fn in glob.glob(os.path.join(sstate_manifests, '%s*-native.populate_sysroot' % fileprefix)):
151 with open(fn, 'r') as f:
152 pn = os.path.basename(fn).rsplit('.', 1)[0][len(fileprefix):]
153 for line in f:
154 if line.startswith(staging_bindir_native):
155 prog = os.path.basename(line.rstrip())
156 RecipeHandler.recipebinmap[prog] = pn
157
158 @staticmethod
159 def checkfiles(path, speclist, recursive=False, excludedirs=None):
160 results = []
161 if recursive:
162 for root, dirs, files in os.walk(path, topdown=True):
163 if excludedirs:
164 dirs[:] = [d for d in dirs if d not in excludedirs]
165 for fn in files:
166 for spec in speclist:
167 if fnmatch.fnmatch(fn, spec):
168 results.append(os.path.join(root, fn))
169 else:
170 for spec in speclist:
171 results.extend(glob.glob(os.path.join(path, spec)))
172 return results
173
174 @staticmethod
175 def handle_depends(libdeps, pcdeps, deps, outlines, values, d):
176 if pcdeps:
177 recipemap = read_pkgconfig_provides(d)
178 if libdeps:
179 RecipeHandler.load_libmap(d)
180
181 ignorelibs = ['socket']
182 ignoredeps = ['gcc-runtime', 'glibc', 'uclibc', 'musl', 'tar-native', 'binutils-native', 'coreutils-native']
183
184 unmappedpc = []
185 pcdeps = list(set(pcdeps))
186 for pcdep in pcdeps:
187 if isinstance(pcdep, str):
188 recipe = recipemap.get(pcdep, None)
189 if recipe:
190 deps.append(recipe)
191 else:
192 if not pcdep.startswith('$'):
193 unmappedpc.append(pcdep)
194 else:
195 for item in pcdep:
196 recipe = recipemap.get(pcdep, None)
197 if recipe:
198 deps.append(recipe)
199 break
200 else:
201 unmappedpc.append('(%s)' % ' or '.join(pcdep))
202
203 unmappedlibs = []
204 for libdep in libdeps:
205 if isinstance(libdep, tuple):
206 lib, header = libdep
207 else:
208 lib = libdep
209 header = None
210
211 if lib in ignorelibs:
212 logger.debug('Ignoring library dependency %s' % lib)
213 continue
214
215 recipe = RecipeHandler.recipelibmap.get(lib, None)
216 if recipe:
217 deps.append(recipe)
218 elif recipe is None:
219 if header:
220 RecipeHandler.load_devel_filemap(d)
221 recipe = RecipeHandler.recipeheadermap.get(header, None)
222 if recipe:
223 deps.append(recipe)
224 elif recipe is None:
225 unmappedlibs.append(lib)
226 else:
227 unmappedlibs.append(lib)
228
229 deps = set(deps).difference(set(ignoredeps))
230
231 if unmappedpc:
232 outlines.append('# NOTE: unable to map the following pkg-config dependencies: %s' % ' '.join(unmappedpc))
233 outlines.append('# (this is based on recipes that have previously been built and packaged)')
234
235 if unmappedlibs:
236 outlines.append('# NOTE: the following library dependencies are unknown, ignoring: %s' % ' '.join(list(set(unmappedlibs))))
237 outlines.append('# (this is based on recipes that have previously been built and packaged)')
238
239 if deps:
240 values['DEPENDS'] = ' '.join(deps)
241
242 @staticmethod
243 def genfunction(outlines, funcname, content, python=False, forcespace=False):
244 if python:
245 prefix = 'python '
246 else:
247 prefix = ''
248 outlines.append('%s%s () {' % (prefix, funcname))
249 if python or forcespace:
250 indent = ' '
251 else:
252 indent = '\t'
253 addnoop = not python
254 for line in content:
255 outlines.append('%s%s' % (indent, line))
256 if addnoop:
257 strippedline = line.lstrip()
258 if strippedline and not strippedline.startswith('#'):
259 addnoop = False
260 if addnoop:
261 # Without this there'll be a syntax error
262 outlines.append('%s:' % indent)
263 outlines.append('}')
264 outlines.append('')
265
266 def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
267 return False
268
269
270def validate_pv(pv):
271 if not pv or '_version' in pv.lower() or pv[0] not in '0123456789':
272 return False
273 return True
274
275def determine_from_filename(srcfile):
276 """Determine name and version from a filename"""
277 if is_package(srcfile):
278 # Force getting the value from the package metadata
279 return None, None
280
281 if '.tar.' in srcfile:
282 namepart = srcfile.split('.tar.')[0]
283 else:
284 namepart = os.path.splitext(srcfile)[0]
285 namepart = namepart.lower().replace('_', '-')
286 if namepart.endswith('.src'):
287 namepart = namepart[:-4]
288 if namepart.endswith('.orig'):
289 namepart = namepart[:-5]
290 splitval = namepart.split('-')
291 logger.debug('determine_from_filename: split name %s into: %s' % (srcfile, splitval))
292
293 ver_re = re.compile('^v?[0-9]')
294
295 pv = None
296 pn = None
297 if len(splitval) == 1:
298 # Try to split the version out if there is no separator (or a .)
299 res = re.match('^([^0-9]+)([0-9.]+.*)$', namepart)
300 if res:
301 if len(res.group(1)) > 1 and len(res.group(2)) > 1:
302 pn = res.group(1).rstrip('.')
303 pv = res.group(2)
304 else:
305 pn = namepart
306 else:
307 if splitval[-1] in ['source', 'src']:
308 splitval.pop()
309 if len(splitval) > 2 and re.match('^(alpha|beta|stable|release|rc[0-9]|pre[0-9]|p[0-9]|[0-9]{8})', splitval[-1]) and ver_re.match(splitval[-2]):
310 pv = '-'.join(splitval[-2:])
311 if pv.endswith('-release'):
312 pv = pv[:-8]
313 splitval = splitval[:-2]
314 elif ver_re.match(splitval[-1]):
315 pv = splitval.pop()
316 pn = '-'.join(splitval)
317 if pv and pv.startswith('v'):
318 pv = pv[1:]
319 logger.debug('determine_from_filename: name = "%s" version = "%s"' % (pn, pv))
320 return (pn, pv)
321
322def determine_from_url(srcuri):
323 """Determine name and version from a URL"""
324 pn = None
325 pv = None
326 parseres = urlparse(srcuri.lower().split(';', 1)[0])
327 if parseres.path:
328 if 'github.com' in parseres.netloc:
329 res = re.search(r'.*/(.*?)/archive/(.*)-final\.(tar|zip)', parseres.path)
330 if res:
331 pn = res.group(1).strip().replace('_', '-')
332 pv = res.group(2).strip().replace('_', '.')
333 else:
334 res = re.search(r'.*/(.*?)/archive/v?(.*)\.(tar|zip)', parseres.path)
335 if res:
336 pn = res.group(1).strip().replace('_', '-')
337 pv = res.group(2).strip().replace('_', '.')
338 elif 'bitbucket.org' in parseres.netloc:
339 res = re.search(r'.*/(.*?)/get/[a-zA-Z_-]*([0-9][0-9a-zA-Z_.]*)\.(tar|zip)', parseres.path)
340 if res:
341 pn = res.group(1).strip().replace('_', '-')
342 pv = res.group(2).strip().replace('_', '.')
343
344 if not pn and not pv:
345 if parseres.scheme not in ['git', 'gitsm', 'svn', 'hg']:
346 srcfile = os.path.basename(parseres.path.rstrip('/'))
347 pn, pv = determine_from_filename(srcfile)
348 elif parseres.scheme in ['git', 'gitsm']:
349 pn = os.path.basename(parseres.path.rstrip('/')).lower().replace('_', '-')
350 if pn.endswith('.git'):
351 pn = pn[:-4]
352
353 logger.debug('Determined from source URL: name = "%s", version = "%s"' % (pn, pv))
354 return (pn, pv)
355
356def supports_srcrev(uri):
357 localdata = bb.data.createCopy(tinfoil.config_data)
358 # This is a bit sad, but if you don't have this set there can be some
359 # odd interactions with the urldata cache which lead to errors
360 localdata.setVar('SRCREV', '${AUTOREV}')
361 try:
362 fetcher = bb.fetch2.Fetch([uri], localdata)
363 urldata = fetcher.ud
364 for u in urldata:
365 if urldata[u].method.supports_srcrev():
366 return True
367 except bb.fetch2.FetchError as e:
368 logger.debug('FetchError in supports_srcrev: %s' % str(e))
369 # Fall back to basic check
370 if uri.startswith(('git://', 'gitsm://')):
371 return True
372 return False
373
374def reformat_git_uri(uri):
375 '''Convert any http[s]://....git URI into git://...;protocol=http[s]'''
376 checkuri = uri.split(';', 1)[0]
377 if checkuri.endswith('.git') or '/git/' in checkuri or re.match('https?://github.com/[^/]+/[^/]+/?$', checkuri):
378 # Appends scheme if the scheme is missing
379 if not '://' in uri:
380 uri = 'git://' + uri
381 scheme, host, path, user, pswd, parms = bb.fetch2.decodeurl(uri)
382 # Detection mechanism, this is required due to certain URL are formatter with ":" rather than "/"
383 # which causes decodeurl to fail getting the right host and path
384 if len(host.split(':')) > 1:
385 splitslash = host.split(':')
386 # Port number should not be split from host
387 if not re.match('^[0-9]+$', splitslash[1]):
388 host = splitslash[0]
389 path = '/' + splitslash[1] + path
390 #Algorithm:
391 # if user is defined, append protocol=ssh or if a protocol is defined, then honor the user-defined protocol
392 # if no user & password is defined, check for scheme type and append the protocol with the scheme type
393 # finally if protocols or if the url is well-formed, do nothing and rejoin everything back to normal
394 # Need to repackage the arguments for encodeurl, the format is: (scheme, host, path, user, password, OrderedDict([('key', 'value')]))
395 if user:
396 if not 'protocol' in parms:
397 parms.update({('protocol', 'ssh')})
398 elif (scheme == "http" or scheme == 'https' or scheme == 'ssh') and not ('protocol' in parms):
399 parms.update({('protocol', scheme)})
400 # Always append 'git://'
401 fUrl = bb.fetch2.encodeurl(('git', host, path, user, pswd, parms))
402 return fUrl
403 else:
404 return uri
405
406def is_package(url):
407 '''Check if a URL points to a package'''
408 checkurl = url.split(';', 1)[0]
409 if checkurl.endswith(('.deb', '.ipk', '.rpm', '.srpm')):
410 return True
411 return False
412
413def create_recipe(args):
414 import bb.process
415 import tempfile
416 import shutil
417 import oe.recipeutils
418
419 pkgarch = ""
420 if args.machine:
421 pkgarch = "${MACHINE_ARCH}"
422
423 extravalues = {}
424 checksums = {}
425 tempsrc = ''
426 source = args.source
427 srcsubdir = ''
428 srcrev = '${AUTOREV}'
429 srcbranch = ''
430 scheme = ''
431 storeTagName = ''
432 pv_srcpv = False
433
434 if os.path.isfile(source):
435 source = 'file://%s' % os.path.abspath(source)
436
437 if scriptutils.is_src_url(source):
438 # Warn about github archive URLs
439 if re.match('https?://github.com/[^/]+/[^/]+/archive/.+(\.tar\..*|\.zip)$', source):
440 logger.warning('github archive files are not guaranteed to be stable and may be re-generated over time. If the latter occurs, the checksums will likely change and the recipe will fail at do_fetch. It is recommended that you point to an actual commit or tag in the repository instead (using the repository URL in conjunction with the -S/--srcrev option).')
441 # Fetch a URL
442 fetchuri = reformat_git_uri(urldefrag(source)[0])
443 if args.binary:
444 # Assume the archive contains the directory structure verbatim
445 # so we need to extract to a subdirectory
446 fetchuri += ';subdir=${BP}'
447 srcuri = fetchuri
448 rev_re = re.compile(';rev=([^;]+)')
449 res = rev_re.search(srcuri)
450 if res:
451 if args.srcrev:
452 logger.error('rev= parameter and -S/--srcrev option cannot both be specified - use one or the other')
453 sys.exit(1)
454 if args.autorev:
455 logger.error('rev= parameter and -a/--autorev option cannot both be specified - use one or the other')
456 sys.exit(1)
457 srcrev = res.group(1)
458 srcuri = rev_re.sub('', srcuri)
459 elif args.srcrev:
460 srcrev = args.srcrev
461
462 # Check whether users provides any branch info in fetchuri.
463 # If true, we will skip all branch checking process to honor all user's input.
464 scheme, network, path, user, passwd, params = bb.fetch2.decodeurl(fetchuri)
465 srcbranch = params.get('branch')
466 if args.srcbranch:
467 if srcbranch:
468 logger.error('branch= parameter and -B/--srcbranch option cannot both be specified - use one or the other')
469 sys.exit(1)
470 srcbranch = args.srcbranch
471 nobranch = params.get('nobranch')
472 if nobranch and srcbranch:
473 logger.error('nobranch= cannot be used if you specify a branch')
474 sys.exit(1)
475 tag = params.get('tag')
476 if not srcbranch and not nobranch and srcrev != '${AUTOREV}':
477 # Append nobranch=1 in the following conditions:
478 # 1. User did not set 'branch=' in srcuri, and
479 # 2. User did not set 'nobranch=1' in srcuri, and
480 # 3. Source revision is not '${AUTOREV}'
481 params['nobranch'] = '1'
482 if tag:
483 # Keep a copy of tag and append nobranch=1 then remove tag from URL.
484 # Bitbake fetcher unable to fetch when {AUTOREV} and tag is set at the same time.
485 storeTagName = params['tag']
486 params['nobranch'] = '1'
487 del params['tag']
488 if scheme == 'npm':
489 params['noverify'] = '1'
490 fetchuri = bb.fetch2.encodeurl((scheme, network, path, user, passwd, params))
491
492 tmpparent = tinfoil.config_data.getVar('BASE_WORKDIR')
493 bb.utils.mkdirhier(tmpparent)
494 tempsrc = tempfile.mkdtemp(prefix='recipetool-', dir=tmpparent)
495 srctree = os.path.join(tempsrc, 'source')
496
497 try:
498 checksums, ftmpdir = scriptutils.fetch_url(tinfoil, fetchuri, srcrev, srctree, logger, preserve_tmp=args.keep_temp)
499 except scriptutils.FetchUrlFailure as e:
500 logger.error(str(e))
501 sys.exit(1)
502
503 if ftmpdir and args.keep_temp:
504 logger.info('Fetch temp directory is %s' % ftmpdir)
505
506 dirlist = os.listdir(srctree)
507 filterout = ['git.indirectionsymlink']
508 dirlist = [x for x in dirlist if x not in filterout]
509 logger.debug('Directory listing (excluding filtered out):\n %s' % '\n '.join(dirlist))
510 if len(dirlist) == 1:
511 singleitem = os.path.join(srctree, dirlist[0])
512 if os.path.isdir(singleitem):
513 # We unpacked a single directory, so we should use that
514 srcsubdir = dirlist[0]
515 srctree = os.path.join(srctree, srcsubdir)
516 else:
517 check_single_file(dirlist[0], fetchuri)
518 elif len(dirlist) == 0:
519 if '/' in fetchuri:
520 fn = os.path.join(tinfoil.config_data.getVar('DL_DIR'), fetchuri.split('/')[-1])
521 if os.path.isfile(fn):
522 check_single_file(fn, fetchuri)
523 # If we've got to here then there's no source so we might as well give up
524 logger.error('URL %s resulted in an empty source tree' % fetchuri)
525 sys.exit(1)
526
527 # We need this checking mechanism to improve the recipe created by recipetool and devtool
528 # is able to parse and build by bitbake.
529 # If there is no input for branch name, then check for branch name with SRCREV provided.
530 if not srcbranch and not nobranch and srcrev and (srcrev != '${AUTOREV}') and scheme in ['git', 'gitsm']:
531 try:
532 cmd = 'git branch -r --contains'
533 check_branch, check_branch_err = bb.process.run('%s %s' % (cmd, srcrev), cwd=srctree)
534 except bb.process.ExecutionError as err:
535 logger.error(str(err))
536 sys.exit(1)
537 get_branch = [x.strip() for x in check_branch.splitlines()]
538 # Remove HEAD reference point and drop remote prefix
539 get_branch = [x.split('/', 1)[1] for x in get_branch if not x.startswith('origin/HEAD')]
540 if 'master' in get_branch:
541 # If it is master, we do not need to append 'branch=master' as this is default.
542 # Even with the case where get_branch has multiple objects, if 'master' is one
543 # of them, we should default take from 'master'
544 srcbranch = ''
545 elif len(get_branch) == 1:
546 # If 'master' isn't in get_branch and get_branch contains only ONE object, then store result into 'srcbranch'
547 srcbranch = get_branch[0]
548 else:
549 # If get_branch contains more than one objects, then display error and exit.
550 mbrch = '\n ' + '\n '.join(get_branch)
551 logger.error('Revision %s was found on multiple branches: %s\nPlease provide the correct branch with -B/--srcbranch' % (srcrev, mbrch))
552 sys.exit(1)
553
554 # Since we might have a value in srcbranch, we need to
555 # recontruct the srcuri to include 'branch' in params.
556 scheme, network, path, user, passwd, params = bb.fetch2.decodeurl(srcuri)
557 if srcbranch:
558 params['branch'] = srcbranch
559
560 if storeTagName and scheme in ['git', 'gitsm']:
561 # Check srcrev using tag and check validity of the tag
562 cmd = ('git rev-parse --verify %s' % (storeTagName))
563 try:
564 check_tag, check_tag_err = bb.process.run('%s' % cmd, cwd=srctree)
565 srcrev = check_tag.split()[0]
566 except bb.process.ExecutionError as err:
567 logger.error(str(err))
568 logger.error("Possibly wrong tag name is provided")
569 sys.exit(1)
570 # Drop tag from srcuri as it will have conflicts with SRCREV during recipe parse.
571 del params['tag']
572 srcuri = bb.fetch2.encodeurl((scheme, network, path, user, passwd, params))
573
574 if os.path.exists(os.path.join(srctree, '.gitmodules')) and srcuri.startswith('git://'):
575 srcuri = 'gitsm://' + srcuri[6:]
576 logger.info('Fetching submodules...')
577 bb.process.run('git submodule update --init --recursive', cwd=srctree)
578
579 if is_package(fetchuri):
580 localdata = bb.data.createCopy(tinfoil.config_data)
581 pkgfile = bb.fetch2.localpath(fetchuri, localdata)
582 if pkgfile:
583 tmpfdir = tempfile.mkdtemp(prefix='recipetool-')
584 try:
585 if pkgfile.endswith(('.deb', '.ipk')):
586 stdout, _ = bb.process.run('ar x %s' % pkgfile, cwd=tmpfdir)
587 stdout, _ = bb.process.run('tar xf control.tar.gz', cwd=tmpfdir)
588 values = convert_debian(tmpfdir)
589 extravalues.update(values)
590 elif pkgfile.endswith(('.rpm', '.srpm')):
591 stdout, _ = bb.process.run('rpm -qp --xml %s > pkginfo.xml' % pkgfile, cwd=tmpfdir)
592 values = convert_rpm_xml(os.path.join(tmpfdir, 'pkginfo.xml'))
593 extravalues.update(values)
594 finally:
595 shutil.rmtree(tmpfdir)
596 else:
597 # Assume we're pointing to an existing source tree
598 if args.extract_to:
599 logger.error('--extract-to cannot be specified if source is a directory')
600 sys.exit(1)
601 if not os.path.isdir(source):
602 logger.error('Invalid source directory %s' % source)
603 sys.exit(1)
604 srctree = source
605 srcuri = ''
606 if os.path.exists(os.path.join(srctree, '.git')):
607 # Try to get upstream repo location from origin remote
608 try:
609 stdout, _ = bb.process.run('git remote -v', cwd=srctree, shell=True)
610 except bb.process.ExecutionError as e:
611 stdout = None
612 if stdout:
613 for line in stdout.splitlines():
614 splitline = line.split()
615 if len(splitline) > 1:
616 if splitline[0] == 'origin' and scriptutils.is_src_url(splitline[1]):
617 srcuri = reformat_git_uri(splitline[1])
618 srcsubdir = 'git'
619 break
620
621 if args.src_subdir:
622 srcsubdir = os.path.join(srcsubdir, args.src_subdir)
623 srctree_use = os.path.abspath(os.path.join(srctree, args.src_subdir))
624 else:
625 srctree_use = os.path.abspath(srctree)
626
627 if args.outfile and os.path.isdir(args.outfile):
628 outfile = None
629 outdir = args.outfile
630 else:
631 outfile = args.outfile
632 outdir = None
633 if outfile and outfile != '-':
634 if os.path.exists(outfile):
635 logger.error('Output file %s already exists' % outfile)
636 sys.exit(1)
637
638 lines_before = []
639 lines_after = []
640
641 lines_before.append('# Recipe created by %s' % os.path.basename(sys.argv[0]))
642 lines_before.append('# This is the basis of a recipe and may need further editing in order to be fully functional.')
643 lines_before.append('# (Feel free to remove these comments when editing.)')
644 # We need a blank line here so that patch_recipe_lines can rewind before the LICENSE comments
645 lines_before.append('')
646
647 # We'll come back and replace this later in handle_license_vars()
648 lines_before.append('##LICENSE_PLACEHOLDER##')
649
650 handled = []
651 classes = []
652
653 # FIXME This is kind of a hack, we probably ought to be using bitbake to do this
654 pn = None
655 pv = None
656 if outfile:
657 recipefn = os.path.splitext(os.path.basename(outfile))[0]
658 fnsplit = recipefn.split('_')
659 if len(fnsplit) > 1:
660 pn = fnsplit[0]
661 pv = fnsplit[1]
662 else:
663 pn = recipefn
664
665 if args.version:
666 pv = args.version
667
668 if args.name:
669 pn = args.name
670 if args.name.endswith('-native'):
671 if args.also_native:
672 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')
673 sys.exit(1)
674 classes.append('native')
675 elif args.name.startswith('nativesdk-'):
676 if args.also_native:
677 logger.error('--also-native cannot be specified for a recipe named nativesdk-* (nativesdk-* denotes a recipe that is already only for nativesdk)')
678 sys.exit(1)
679 classes.append('nativesdk')
680
681 if pv and pv not in 'git svn hg'.split():
682 realpv = pv
683 else:
684 realpv = None
685
686 if not srcuri:
687 lines_before.append('# No information for SRC_URI yet (only an external source tree was specified)')
688 lines_before.append('SRC_URI = "%s"' % srcuri)
689 for key, value in sorted(checksums.items()):
690 lines_before.append('SRC_URI[%s] = "%s"' % (key, value))
691 if srcuri and supports_srcrev(srcuri):
692 lines_before.append('')
693 lines_before.append('# Modify these as desired')
694 # Note: we have code to replace realpv further down if it gets set to some other value
695 scheme, _, _, _, _, _ = bb.fetch2.decodeurl(srcuri)
696 if scheme in ['git', 'gitsm']:
697 srcpvprefix = 'git'
698 elif scheme == 'svn':
699 srcpvprefix = 'svnr'
700 else:
701 srcpvprefix = scheme
702 lines_before.append('PV = "%s+%s${SRCPV}"' % (realpv or '1.0', srcpvprefix))
703 pv_srcpv = True
704 if not args.autorev and srcrev == '${AUTOREV}':
705 if os.path.exists(os.path.join(srctree, '.git')):
706 (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
707 srcrev = stdout.rstrip()
708 lines_before.append('SRCREV = "%s"' % srcrev)
709 if args.provides:
710 lines_before.append('PROVIDES = "%s"' % args.provides)
711 lines_before.append('')
712
713 if srcsubdir and not args.binary:
714 # (for binary packages we explicitly specify subdir= when fetching to
715 # match the default value of S, so we don't need to set it in that case)
716 lines_before.append('S = "${WORKDIR}/%s"' % srcsubdir)
717 lines_before.append('')
718
719 if pkgarch:
720 lines_after.append('PACKAGE_ARCH = "%s"' % pkgarch)
721 lines_after.append('')
722
723 if args.binary:
724 lines_after.append('INSANE_SKIP_${PN} += "already-stripped"')
725 lines_after.append('')
726
727 if args.fetch_dev:
728 extravalues['fetchdev'] = True
729 else:
730 extravalues['fetchdev'] = None
731
732 # Find all plugins that want to register handlers
733 logger.debug('Loading recipe handlers')
734 raw_handlers = []
735 for plugin in plugins:
736 if hasattr(plugin, 'register_recipe_handlers'):
737 plugin.register_recipe_handlers(raw_handlers)
738 # Sort handlers by priority
739 handlers = []
740 for i, handler in enumerate(raw_handlers):
741 if isinstance(handler, tuple):
742 handlers.append((handler[0], handler[1], i))
743 else:
744 handlers.append((handler, 0, i))
745 handlers.sort(key=lambda item: (item[1], -item[2]), reverse=True)
746 for handler, priority, _ in handlers:
747 logger.debug('Handler: %s (priority %d)' % (handler.__class__.__name__, priority))
748 setattr(handler, '_devtool', args.devtool)
749 handlers = [item[0] for item in handlers]
750
751 # Apply the handlers
752 if args.binary:
753 classes.append('bin_package')
754 handled.append('buildsystem')
755
756 for handler in handlers:
757 handler.process(srctree_use, classes, lines_before, lines_after, handled, extravalues)
758
759 extrafiles = extravalues.pop('extrafiles', {})
760 extra_pn = extravalues.pop('PN', None)
761 extra_pv = extravalues.pop('PV', None)
762
763 if extra_pv and not realpv:
764 realpv = extra_pv
765 if not validate_pv(realpv):
766 realpv = None
767 else:
768 realpv = realpv.lower().split()[0]
769 if '_' in realpv:
770 realpv = realpv.replace('_', '-')
771 if extra_pn and not pn:
772 pn = extra_pn
773 if pn.startswith('GNU '):
774 pn = pn[4:]
775 if ' ' in pn:
776 # Probably a descriptive identifier rather than a proper name
777 pn = None
778 else:
779 pn = pn.lower()
780 if '_' in pn:
781 pn = pn.replace('_', '-')
782
783 if srcuri and not realpv or not pn:
784 name_pn, name_pv = determine_from_url(srcuri)
785 if name_pn and not pn:
786 pn = name_pn
787 if name_pv and not realpv:
788 realpv = name_pv
789
790 licvalues = handle_license_vars(srctree_use, lines_before, handled, extravalues, tinfoil.config_data)
791
792 if not outfile:
793 if not pn:
794 log_error_cond('Unable to determine short program name from source tree - please specify name with -N/--name or output file name with -o/--outfile', args.devtool)
795 # devtool looks for this specific exit code, so don't change it
796 sys.exit(15)
797 else:
798 if srcuri and srcuri.startswith(('gitsm://', 'git://', 'hg://', 'svn://')):
799 suffix = srcuri.split(':', 1)[0]
800 if suffix == 'gitsm':
801 suffix = 'git'
802 outfile = '%s_%s.bb' % (pn, suffix)
803 elif realpv:
804 outfile = '%s_%s.bb' % (pn, realpv)
805 else:
806 outfile = '%s.bb' % pn
807 if outdir:
808 outfile = os.path.join(outdir, outfile)
809 # We need to check this again
810 if os.path.exists(outfile):
811 logger.error('Output file %s already exists' % outfile)
812 sys.exit(1)
813
814 # Move any extra files the plugins created to a directory next to the recipe
815 if extrafiles:
816 if outfile == '-':
817 extraoutdir = pn
818 else:
819 extraoutdir = os.path.join(os.path.dirname(outfile), pn)
820 bb.utils.mkdirhier(extraoutdir)
821 for destfn, extrafile in extrafiles.items():
822 shutil.move(extrafile, os.path.join(extraoutdir, destfn))
823
824 lines = lines_before
825 lines_before = []
826 skipblank = True
827 for line in lines:
828 if skipblank:
829 skipblank = False
830 if not line:
831 continue
832 if line.startswith('S = '):
833 if realpv and pv not in 'git svn hg'.split():
834 line = line.replace(realpv, '${PV}')
835 if pn:
836 line = line.replace(pn, '${BPN}')
837 if line == 'S = "${WORKDIR}/${BPN}-${PV}"':
838 skipblank = True
839 continue
840 elif line.startswith('SRC_URI = '):
841 if realpv and not pv_srcpv:
842 line = line.replace(realpv, '${PV}')
843 elif line.startswith('PV = '):
844 if realpv:
845 # Replace the first part of the PV value
846 line = re.sub('"[^+]*\+', '"%s+' % realpv, line)
847 lines_before.append(line)
848
849 if args.also_native:
850 lines = lines_after
851 lines_after = []
852 bbclassextend = None
853 for line in lines:
854 if line.startswith('BBCLASSEXTEND ='):
855 splitval = line.split('"')
856 if len(splitval) > 1:
857 bbclassextend = splitval[1].split()
858 if not 'native' in bbclassextend:
859 bbclassextend.insert(0, 'native')
860 line = 'BBCLASSEXTEND = "%s"' % ' '.join(bbclassextend)
861 lines_after.append(line)
862 if not bbclassextend:
863 lines_after.append('BBCLASSEXTEND = "native"')
864
865 postinst = ("postinst", extravalues.pop('postinst', None))
866 postrm = ("postrm", extravalues.pop('postrm', None))
867 preinst = ("preinst", extravalues.pop('preinst', None))
868 prerm = ("prerm", extravalues.pop('prerm', None))
869 funcs = [postinst, postrm, preinst, prerm]
870 for func in funcs:
871 if func[1]:
872 RecipeHandler.genfunction(lines_after, 'pkg_%s_${PN}' % func[0], func[1])
873
874 outlines = []
875 outlines.extend(lines_before)
876 if classes:
877 if outlines[-1] and not outlines[-1].startswith('#'):
878 outlines.append('')
879 outlines.append('inherit %s' % ' '.join(classes))
880 outlines.append('')
881 outlines.extend(lines_after)
882
883 if extravalues:
884 _, outlines = oe.recipeutils.patch_recipe_lines(outlines, extravalues, trailing_newline=False)
885
886 if args.extract_to:
887 scriptutils.git_convert_standalone_clone(srctree)
888 if os.path.isdir(args.extract_to):
889 # If the directory exists we'll move the temp dir into it instead of
890 # its contents - of course, we could try to always move its contents
891 # but that is a pain if there are symlinks; the simplest solution is
892 # to just remove it first
893 os.rmdir(args.extract_to)
894 shutil.move(srctree, args.extract_to)
895 if tempsrc == srctree:
896 tempsrc = None
897 log_info_cond('Source extracted to %s' % args.extract_to, args.devtool)
898
899 if outfile == '-':
900 sys.stdout.write('\n'.join(outlines) + '\n')
901 else:
902 with open(outfile, 'w') as f:
903 lastline = None
904 for line in outlines:
905 if not lastline and not line:
906 # Skip extra blank lines
907 continue
908 f.write('%s\n' % line)
909 lastline = line
910 log_info_cond('Recipe %s has been created; further editing may be required to make it fully functional' % outfile, args.devtool)
911
912 if tempsrc:
913 if args.keep_temp:
914 logger.info('Preserving temporary directory %s' % tempsrc)
915 else:
916 shutil.rmtree(tempsrc)
917
918 return 0
919
920def check_single_file(fn, fetchuri):
921 """Determine if a single downloaded file is something we can't handle"""
922 with open(fn, 'r', errors='surrogateescape') as f:
923 if '<html' in f.read(100).lower():
924 logger.error('Fetching "%s" returned a single HTML page - check the URL is correct and functional' % fetchuri)
925 sys.exit(1)
926
927def split_value(value):
928 if isinstance(value, str):
929 return value.split()
930 else:
931 return value
932
933def handle_license_vars(srctree, lines_before, handled, extravalues, d):
934 lichandled = [x for x in handled if x[0] == 'license']
935 if lichandled:
936 # Someone else has already handled the license vars, just return their value
937 return lichandled[0][1]
938
939 licvalues = guess_license(srctree, d)
940 licenses = []
941 lic_files_chksum = []
942 lic_unknown = []
943 lines = []
944 if licvalues:
945 for licvalue in licvalues:
946 if not licvalue[0] in licenses:
947 licenses.append(licvalue[0])
948 lic_files_chksum.append('file://%s;md5=%s' % (licvalue[1], licvalue[2]))
949 if licvalue[0] == 'Unknown':
950 lic_unknown.append(licvalue[1])
951 if lic_unknown:
952 lines.append('#')
953 lines.append('# The following license files were not able to be identified and are')
954 lines.append('# represented as "Unknown" below, you will need to check them yourself:')
955 for licfile in lic_unknown:
956 lines.append('# %s' % licfile)
957
958 extra_license = split_value(extravalues.pop('LICENSE', []))
959 if '&' in extra_license:
960 extra_license.remove('&')
961 if extra_license:
962 if licenses == ['Unknown']:
963 licenses = extra_license
964 else:
965 for item in extra_license:
966 if item not in licenses:
967 licenses.append(item)
968 extra_lic_files_chksum = split_value(extravalues.pop('LIC_FILES_CHKSUM', []))
969 for item in extra_lic_files_chksum:
970 if item not in lic_files_chksum:
971 lic_files_chksum.append(item)
972
973 if lic_files_chksum:
974 # We are going to set the vars, so prepend the standard disclaimer
975 lines.insert(0, '# WARNING: the following LICENSE and LIC_FILES_CHKSUM values are best guesses - it is')
976 lines.insert(1, '# your responsibility to verify that the values are complete and correct.')
977 else:
978 # Without LIC_FILES_CHKSUM we set LICENSE = "CLOSED" to allow the
979 # user to get started easily
980 lines.append('# Unable to find any files that looked like license statements. Check the accompanying')
981 lines.append('# documentation and source headers and set LICENSE and LIC_FILES_CHKSUM accordingly.')
982 lines.append('#')
983 lines.append('# NOTE: LICENSE is being set to "CLOSED" to allow you to at least start building - if')
984 lines.append('# this is not accurate with respect to the licensing of the software being built (it')
985 lines.append('# will not be in most cases) you must specify the correct value before using this')
986 lines.append('# recipe for anything other than initial testing/development!')
987 licenses = ['CLOSED']
988
989 if extra_license and sorted(licenses) != sorted(extra_license):
990 lines.append('# NOTE: Original package / source metadata indicates license is: %s' % ' & '.join(extra_license))
991
992 if len(licenses) > 1:
993 lines.append('#')
994 lines.append('# NOTE: multiple licenses have been detected; they have been separated with &')
995 lines.append('# in the LICENSE value for now since it is a reasonable assumption that all')
996 lines.append('# of the licenses apply. If instead there is a choice between the multiple')
997 lines.append('# licenses then you should change the value to separate the licenses with |')
998 lines.append('# instead of &. If there is any doubt, check the accompanying documentation')
999 lines.append('# to determine which situation is applicable.')
1000
1001 lines.append('LICENSE = "%s"' % ' & '.join(licenses))
1002 lines.append('LIC_FILES_CHKSUM = "%s"' % ' \\\n '.join(lic_files_chksum))
1003 lines.append('')
1004
1005 # Replace the placeholder so we get the values in the right place in the recipe file
1006 try:
1007 pos = lines_before.index('##LICENSE_PLACEHOLDER##')
1008 except ValueError:
1009 pos = -1
1010 if pos == -1:
1011 lines_before.extend(lines)
1012 else:
1013 lines_before[pos:pos+1] = lines
1014
1015 handled.append(('license', licvalues))
1016 return licvalues
1017
1018def get_license_md5sums(d, static_only=False):
1019 import bb.utils
1020 md5sums = {}
1021 if not static_only:
1022 # Gather md5sums of license files in common license dir
1023 commonlicdir = d.getVar('COMMON_LICENSE_DIR')
1024 for fn in os.listdir(commonlicdir):
1025 md5value = bb.utils.md5_file(os.path.join(commonlicdir, fn))
1026 md5sums[md5value] = fn
1027 # The following were extracted from common values in various recipes
1028 # (double checking the license against the license file itself, not just
1029 # the LICENSE value in the recipe)
1030 md5sums['94d55d512a9ba36caa9b7df079bae19f'] = 'GPLv2'
1031 md5sums['b234ee4d69f5fce4486a80fdaf4a4263'] = 'GPLv2'
1032 md5sums['59530bdf33659b29e73d4adb9f9f6552'] = 'GPLv2'
1033 md5sums['0636e73ff0215e8d672dc4c32c317bb3'] = 'GPLv2'
1034 md5sums['eb723b61539feef013de476e68b5c50a'] = 'GPLv2'
1035 md5sums['751419260aa954499f7abaabaa882bbe'] = 'GPLv2'
1036 md5sums['393a5ca445f6965873eca0259a17f833'] = 'GPLv2'
1037 md5sums['12f884d2ae1ff87c09e5b7ccc2c4ca7e'] = 'GPLv2'
1038 md5sums['8ca43cbc842c2336e835926c2166c28b'] = 'GPLv2'
1039 md5sums['ebb5c50ab7cab4baeffba14977030c07'] = 'GPLv2'
1040 md5sums['c93c0550bd3173f4504b2cbd8991e50b'] = 'GPLv2'
1041 md5sums['9ac2e7cff1ddaf48b6eab6028f23ef88'] = 'GPLv2'
1042 md5sums['4325afd396febcb659c36b49533135d4'] = 'GPLv2'
1043 md5sums['18810669f13b87348459e611d31ab760'] = 'GPLv2'
1044 md5sums['d7810fab7487fb0aad327b76f1be7cd7'] = 'GPLv2' # the Linux kernel's COPYING file
1045 md5sums['bbb461211a33b134d42ed5ee802b37ff'] = 'LGPLv2.1'
1046 md5sums['7fbc338309ac38fefcd64b04bb903e34'] = 'LGPLv2.1'
1047 md5sums['4fbd65380cdd255951079008b364516c'] = 'LGPLv2.1'
1048 md5sums['2d5025d4aa3495befef8f17206a5b0a1'] = 'LGPLv2.1'
1049 md5sums['fbc093901857fcd118f065f900982c24'] = 'LGPLv2.1'
1050 md5sums['a6f89e2100d9b6cdffcea4f398e37343'] = 'LGPLv2.1'
1051 md5sums['d8045f3b8f929c1cb29a1e3fd737b499'] = 'LGPLv2.1'
1052 md5sums['fad9b3332be894bab9bc501572864b29'] = 'LGPLv2.1'
1053 md5sums['3bf50002aefd002f49e7bb854063f7e7'] = 'LGPLv2'
1054 md5sums['9f604d8a4f8e74f4f5140845a21b6674'] = 'LGPLv2'
1055 md5sums['5f30f0716dfdd0d91eb439ebec522ec2'] = 'LGPLv2'
1056 md5sums['55ca817ccb7d5b5b66355690e9abc605'] = 'LGPLv2'
1057 md5sums['252890d9eee26aab7b432e8b8a616475'] = 'LGPLv2'
1058 md5sums['3214f080875748938ba060314b4f727d'] = 'LGPLv2'
1059 md5sums['db979804f025cf55aabec7129cb671ed'] = 'LGPLv2'
1060 md5sums['d32239bcb673463ab874e80d47fae504'] = 'GPLv3'
1061 md5sums['f27defe1e96c2e1ecd4e0c9be8967949'] = 'GPLv3'
1062 md5sums['6a6a8e020838b23406c81b19c1d46df6'] = 'LGPLv3'
1063 md5sums['3b83ef96387f14655fc854ddc3c6bd57'] = 'Apache-2.0'
1064 md5sums['385c55653886acac3821999a3ccd17b3'] = 'Artistic-1.0 | GPL-2.0' # some perl modules
1065 md5sums['54c7042be62e169199200bc6477f04d1'] = 'BSD-3-Clause'
1066 return md5sums
1067
1068def crunch_license(licfile):
1069 '''
1070 Remove non-material text from a license file and then check
1071 its md5sum against a known list. This works well for licenses
1072 which contain a copyright statement, but is also a useful way
1073 to handle people's insistence upon reformatting the license text
1074 slightly (with no material difference to the text of the
1075 license).
1076 '''
1077
1078 import oe.utils
1079
1080 # Note: these are carefully constructed!
1081 license_title_re = re.compile('^\(?(#+ *)?(The )?.{1,10} [Ll]icen[sc]e( \(.{1,10}\))?\)?:?$')
1082 license_statement_re = re.compile('^(This (project|software) is( free software)? (released|licen[sc]ed)|(Released|Licen[cs]ed)) under the .{1,10} [Ll]icen[sc]e:?$')
1083 copyright_re = re.compile('^(#+)? *Copyright .*$')
1084
1085 crunched_md5sums = {}
1086 # The following two were gleaned from the "forever" npm package
1087 crunched_md5sums['0a97f8e4cbaf889d6fa51f84b89a79f6'] = 'ISC'
1088 crunched_md5sums['eecf6429523cbc9693547cf2db790b5c'] = 'MIT'
1089 # https://github.com/vasi/pixz/blob/master/LICENSE
1090 crunched_md5sums['2f03392b40bbe663597b5bd3cc5ebdb9'] = 'BSD-2-Clause'
1091 # https://github.com/waffle-gl/waffle/blob/master/LICENSE.txt
1092 crunched_md5sums['e72e5dfef0b1a4ca8a3d26a60587db66'] = 'BSD-2-Clause'
1093 # https://github.com/spigwitmer/fakeds1963s/blob/master/LICENSE
1094 crunched_md5sums['8be76ac6d191671f347ee4916baa637e'] = 'GPLv2'
1095 # https://github.com/datto/dattobd/blob/master/COPYING
1096 # http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/GPLv2.TXT
1097 crunched_md5sums['1d65c5ad4bf6489f85f4812bf08ae73d'] = 'GPLv2'
1098 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
1099 # http://git.neil.brown.name/?p=mdadm.git;a=blob;f=COPYING;h=d159169d1050894d3ea3b98e1c965c4058208fe1;hb=HEAD
1100 crunched_md5sums['fb530f66a7a89ce920f0e912b5b66d4b'] = 'GPLv2'
1101 # https://github.com/gkos/nrf24/blob/master/COPYING
1102 crunched_md5sums['7b6aaa4daeafdfa6ed5443fd2684581b'] = 'GPLv2'
1103 # https://github.com/josch09/resetusb/blob/master/COPYING
1104 crunched_md5sums['8b8ac1d631a4d220342e83bcf1a1fbc3'] = 'GPLv3'
1105 # https://github.com/FFmpeg/FFmpeg/blob/master/COPYING.LGPLv2.1
1106 crunched_md5sums['2ea316ed973ae176e502e2297b574bb3'] = 'LGPLv2.1'
1107 # unixODBC-2.3.4 COPYING
1108 crunched_md5sums['1daebd9491d1e8426900b4fa5a422814'] = 'LGPLv2.1'
1109 # https://github.com/FFmpeg/FFmpeg/blob/master/COPYING.LGPLv3
1110 crunched_md5sums['2ebfb3bb49b9a48a075cc1425e7f4129'] = 'LGPLv3'
1111 # https://raw.githubusercontent.com/eclipse/mosquitto/v1.4.14/epl-v10
1112 crunched_md5sums['efe2cb9a35826992b9df68224e3c2628'] = 'EPL-1.0'
1113 # https://raw.githubusercontent.com/eclipse/mosquitto/v1.4.14/edl-v10
1114 crunched_md5sums['0a9c78c0a398d1bbce4a166757d60387'] = 'EDL-1.0'
1115 lictext = []
1116 with open(licfile, 'r', errors='surrogateescape') as f:
1117 for line in f:
1118 # Drop opening statements
1119 if copyright_re.match(line):
1120 continue
1121 elif license_title_re.match(line):
1122 continue
1123 elif license_statement_re.match(line):
1124 continue
1125 # Squash spaces, and replace smart quotes, double quotes
1126 # and backticks with single quotes
1127 line = oe.utils.squashspaces(line.strip())
1128 line = line.replace(u"\u2018", "'").replace(u"\u2019", "'").replace(u"\u201c","'").replace(u"\u201d", "'").replace('"', '\'').replace('`', '\'')
1129 if line:
1130 lictext.append(line)
1131
1132 m = hashlib.md5()
1133 try:
1134 m.update(' '.join(lictext).encode('utf-8'))
1135 md5val = m.hexdigest()
1136 except UnicodeEncodeError:
1137 md5val = None
1138 lictext = ''
1139 license = crunched_md5sums.get(md5val, None)
1140 return license, md5val, lictext
1141
1142def guess_license(srctree, d):
1143 import bb
1144 md5sums = get_license_md5sums(d)
1145
1146 licenses = []
1147 licspecs = ['*LICEN[CS]E*', 'COPYING*', '*[Ll]icense*', 'LEGAL*', '[Ll]egal*', '*GPL*', 'README.lic*', 'COPYRIGHT*', '[Cc]opyright*', 'e[dp]l-v10']
1148 licfiles = []
1149 for root, dirs, files in os.walk(srctree):
1150 for fn in files:
1151 for spec in licspecs:
1152 if fnmatch.fnmatch(fn, spec):
1153 fullpath = os.path.join(root, fn)
1154 if not fullpath in licfiles:
1155 licfiles.append(fullpath)
1156 for licfile in licfiles:
1157 md5value = bb.utils.md5_file(licfile)
1158 license = md5sums.get(md5value, None)
1159 if not license:
1160 license, crunched_md5, lictext = crunch_license(licfile)
1161 if not license:
1162 license = 'Unknown'
1163 licenses.append((license, os.path.relpath(licfile, srctree), md5value))
1164
1165 # FIXME should we grab at least one source file with a license header and add that too?
1166
1167 return licenses
1168
1169def split_pkg_licenses(licvalues, packages, outlines, fallback_licenses=None, pn='${PN}'):
1170 """
1171 Given a list of (license, path, md5sum) as returned by guess_license(),
1172 a dict of package name to path mappings, write out a set of
1173 package-specific LICENSE values.
1174 """
1175 pkglicenses = {pn: []}
1176 for license, licpath, _ in licvalues:
1177 for pkgname, pkgpath in packages.items():
1178 if licpath.startswith(pkgpath + '/'):
1179 if pkgname in pkglicenses:
1180 pkglicenses[pkgname].append(license)
1181 else:
1182 pkglicenses[pkgname] = [license]
1183 break
1184 else:
1185 # Accumulate on the main package
1186 pkglicenses[pn].append(license)
1187 outlicenses = {}
1188 for pkgname in packages:
1189 license = ' '.join(list(set(pkglicenses.get(pkgname, ['Unknown'])))) or 'Unknown'
1190 if license == 'Unknown' and pkgname in fallback_licenses:
1191 license = fallback_licenses[pkgname]
1192 outlines.append('LICENSE_%s = "%s"' % (pkgname, license))
1193 outlicenses[pkgname] = license.split()
1194 return outlicenses
1195
1196def read_pkgconfig_provides(d):
1197 pkgdatadir = d.getVar('PKGDATA_DIR')
1198 pkgmap = {}
1199 for fn in glob.glob(os.path.join(pkgdatadir, 'shlibs2', '*.pclist')):
1200 with open(fn, 'r') as f:
1201 for line in f:
1202 pkgmap[os.path.basename(line.rstrip())] = os.path.splitext(os.path.basename(fn))[0]
1203 recipemap = {}
1204 for pc, pkg in pkgmap.items():
1205 pkgdatafile = os.path.join(pkgdatadir, 'runtime', pkg)
1206 if os.path.exists(pkgdatafile):
1207 with open(pkgdatafile, 'r') as f:
1208 for line in f:
1209 if line.startswith('PN: '):
1210 recipemap[pc] = line.split(':', 1)[1].strip()
1211 return recipemap
1212
1213def convert_debian(debpath):
1214 value_map = {'Package': 'PN',
1215 'Version': 'PV',
1216 'Section': 'SECTION',
1217 'License': 'LICENSE',
1218 'Homepage': 'HOMEPAGE'}
1219
1220 # FIXME extend this mapping - perhaps use distro_alias.inc?
1221 depmap = {'libz-dev': 'zlib'}
1222
1223 values = {}
1224 depends = []
1225 with open(os.path.join(debpath, 'control'), 'r', errors='surrogateescape') as f:
1226 indesc = False
1227 for line in f:
1228 if indesc:
1229 if line.startswith(' '):
1230 if line.startswith(' This package contains'):
1231 indesc = False
1232 else:
1233 if 'DESCRIPTION' in values:
1234 values['DESCRIPTION'] += ' ' + line.strip()
1235 else:
1236 values['DESCRIPTION'] = line.strip()
1237 else:
1238 indesc = False
1239 if not indesc:
1240 splitline = line.split(':', 1)
1241 if len(splitline) < 2:
1242 continue
1243 key = splitline[0]
1244 value = splitline[1].strip()
1245 if key == 'Build-Depends':
1246 for dep in value.split(','):
1247 dep = dep.split()[0]
1248 mapped = depmap.get(dep, '')
1249 if mapped:
1250 depends.append(mapped)
1251 elif key == 'Description':
1252 values['SUMMARY'] = value
1253 indesc = True
1254 else:
1255 varname = value_map.get(key, None)
1256 if varname:
1257 values[varname] = value
1258 postinst = os.path.join(debpath, 'postinst')
1259 postrm = os.path.join(debpath, 'postrm')
1260 preinst = os.path.join(debpath, 'preinst')
1261 prerm = os.path.join(debpath, 'prerm')
1262 sfiles = [postinst, postrm, preinst, prerm]
1263 for sfile in sfiles:
1264 if os.path.isfile(sfile):
1265 logger.info("Converting %s file to recipe function..." %
1266 os.path.basename(sfile).upper())
1267 content = []
1268 with open(sfile) as f:
1269 for line in f:
1270 if "#!/" in line:
1271 continue
1272 line = line.rstrip("\n")
1273 if line.strip():
1274 content.append(line)
1275 if content:
1276 values[os.path.basename(f.name)] = content
1277
1278 #if depends:
1279 # values['DEPENDS'] = ' '.join(depends)
1280
1281 return values
1282
1283def convert_rpm_xml(xmlfile):
1284 '''Converts the output from rpm -qp --xml to a set of variable values'''
1285 import xml.etree.ElementTree as ElementTree
1286 rpmtag_map = {'Name': 'PN',
1287 'Version': 'PV',
1288 'Summary': 'SUMMARY',
1289 'Description': 'DESCRIPTION',
1290 'License': 'LICENSE',
1291 'Url': 'HOMEPAGE'}
1292
1293 values = {}
1294 tree = ElementTree.parse(xmlfile)
1295 root = tree.getroot()
1296 for child in root:
1297 if child.tag == 'rpmTag':
1298 name = child.attrib.get('name', None)
1299 if name:
1300 varname = rpmtag_map.get(name, None)
1301 if varname:
1302 values[varname] = child[0].text
1303 return values
1304
1305
1306def register_commands(subparsers):
1307 parser_create = subparsers.add_parser('create',
1308 help='Create a new recipe',
1309 description='Creates a new recipe from a source tree')
1310 parser_create.add_argument('source', help='Path or URL to source')
1311 parser_create.add_argument('-o', '--outfile', help='Specify filename for recipe to create')
1312 parser_create.add_argument('-p', '--provides', help='Specify an alias for the item provided by the recipe')
1313 parser_create.add_argument('-m', '--machine', help='Make recipe machine-specific as opposed to architecture-specific', action='store_true')
1314 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')
1315 parser_create.add_argument('-N', '--name', help='Name to use within recipe (PN)')
1316 parser_create.add_argument('-V', '--version', help='Version to use within recipe (PV)')
1317 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')
1318 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')
1319 parser_create.add_argument('--src-subdir', help='Specify subdirectory within source tree to use', metavar='SUBDIR')
1320 group = parser_create.add_mutually_exclusive_group()
1321 group.add_argument('-a', '--autorev', help='When fetching from a git repository, set SRCREV in the recipe to a floating revision instead of fixed', action="store_true")
1322 group.add_argument('-S', '--srcrev', help='Source revision to fetch if fetching from an SCM such as git (default latest)')
1323 parser_create.add_argument('-B', '--srcbranch', help='Branch in source repository if fetching from an SCM such as git (default master)')
1324 parser_create.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
1325 parser_create.add_argument('--fetch-dev', action="store_true", help='For npm, also fetch devDependencies')
1326 parser_create.add_argument('--devtool', action="store_true", help=argparse.SUPPRESS)
1327 parser_create.add_argument('--mirrors', action="store_true", help='Enable PREMIRRORS and MIRRORS for source tree fetching (disabled by default).')
1328 parser_create.set_defaults(func=create_recipe)
1329