blob: d2997cc24203eda2dc69646b7ba415963d2feba9 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Recipe creation tool - create command plugin
2#
Brad Bishopd7bf8c12018-02-25 22:55:05 -05003# Copyright (C) 2014-2017 Intel Corporation
Patrick Williamsc124f4f2015-09-15 14:41:29 -05004#
Brad Bishopc342db32019-05-15 21:57:59 -04005# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsc124f4f2015-09-15 14:41:29 -05006#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05007
8import sys
9import os
10import argparse
11import glob
12import fnmatch
13import re
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050014import json
Patrick Williamsc124f4f2015-09-15 14:41:29 -050015import logging
16import scriptutils
Patrick Williamsc0f7c042017-02-23 20:41:17 -060017from urllib.parse import urlparse, urldefrag, urlsplit
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050018import hashlib
Brad Bishop6e60e8b2018-02-01 10:27:11 -050019import bb.fetch2
Patrick Williamsc124f4f2015-09-15 14:41:29 -050020logger = logging.getLogger('recipetool')
21
22tinfoil = None
23plugins = None
24
Brad Bishop6e60e8b2018-02-01 10:27:11 -050025def log_error_cond(message, debugonly):
26 if debugonly:
27 logger.debug(message)
28 else:
29 logger.error(message)
30
31def log_info_cond(message, debugonly):
32 if debugonly:
33 logger.debug(message)
34 else:
35 logger.info(message)
36
Patrick Williamsc124f4f2015-09-15 14:41:29 -050037def plugin_init(pluginlist):
38 # Take a reference to the list so we can use it later
39 global plugins
40 plugins = pluginlist
41
42def tinfoil_init(instance):
43 global tinfoil
44 tinfoil = instance
45
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050046class RecipeHandler(object):
47 recipelibmap = {}
48 recipeheadermap = {}
49 recipecmakefilemap = {}
50 recipebinmap = {}
51
Brad Bishop6e60e8b2018-02-01 10:27:11 -050052 def __init__(self):
53 self._devtool = False
54
Patrick Williamsc124f4f2015-09-15 14:41:29 -050055 @staticmethod
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050056 def load_libmap(d):
57 '''Load library->recipe mapping'''
58 import oe.package
59
60 if RecipeHandler.recipelibmap:
61 return
62 # First build up library->package mapping
Brad Bishop96ff1982019-08-19 13:50:42 -040063 d2 = bb.data.createCopy(d)
64 d2.setVar("WORKDIR_PKGDATA", "${PKGDATA_DIR}")
65 shlib_providers = oe.package.read_shlib_providers(d2)
Brad Bishop6e60e8b2018-02-01 10:27:11 -050066 libdir = d.getVar('libdir')
67 base_libdir = d.getVar('base_libdir')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050068 libpaths = list(set([base_libdir, libdir]))
Andrew Geisslerb7d28612020-07-24 16:15:54 -050069 libname_re = re.compile(r'^lib(.+)\.so.*$')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050070 pkglibmap = {}
Patrick Williamsc0f7c042017-02-23 20:41:17 -060071 for lib, item in shlib_providers.items():
72 for path, pkg in item.items():
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050073 if path in libpaths:
74 res = libname_re.match(lib)
75 if res:
76 libname = res.group(1)
77 if not libname in pkglibmap:
78 pkglibmap[libname] = pkg[0]
79 else:
80 logger.debug('unable to extract library name from %s' % lib)
81
82 # Now turn it into a library->recipe mapping
Brad Bishop6e60e8b2018-02-01 10:27:11 -050083 pkgdata_dir = d.getVar('PKGDATA_DIR')
Patrick Williamsc0f7c042017-02-23 20:41:17 -060084 for libname, pkg in pkglibmap.items():
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050085 try:
86 with open(os.path.join(pkgdata_dir, 'runtime', pkg)) as f:
87 for line in f:
88 if line.startswith('PN:'):
89 RecipeHandler.recipelibmap[libname] = line.split(':', 1)[-1].strip()
90 break
91 except IOError as ioe:
92 if ioe.errno == 2:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080093 logger.warning('unable to find a pkgdata file for package %s' % pkg)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050094 else:
95 raise
96
97 # Some overrides - these should be mapped to the virtual
98 RecipeHandler.recipelibmap['GL'] = 'virtual/libgl'
99 RecipeHandler.recipelibmap['EGL'] = 'virtual/egl'
100 RecipeHandler.recipelibmap['GLESv2'] = 'virtual/libgles2'
101
102 @staticmethod
103 def load_devel_filemap(d):
104 '''Build up development file->recipe mapping'''
105 if RecipeHandler.recipeheadermap:
106 return
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500107 pkgdata_dir = d.getVar('PKGDATA_DIR')
108 includedir = d.getVar('includedir')
109 cmakedir = os.path.join(d.getVar('libdir'), 'cmake')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500110 for pkg in glob.glob(os.path.join(pkgdata_dir, 'runtime', '*-dev')):
111 with open(os.path.join(pkgdata_dir, 'runtime', pkg)) as f:
112 pn = None
113 headers = []
114 cmakefiles = []
115 for line in f:
116 if line.startswith('PN:'):
117 pn = line.split(':', 1)[-1].strip()
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500118 elif line.startswith('FILES_INFO:%s:' % pkg):
119 val = line.split(': ', 1)[1].strip()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500120 dictval = json.loads(val)
121 for fullpth in sorted(dictval):
122 if fullpth.startswith(includedir) and fullpth.endswith('.h'):
123 headers.append(os.path.relpath(fullpth, includedir))
124 elif fullpth.startswith(cmakedir) and fullpth.endswith('.cmake'):
125 cmakefiles.append(os.path.relpath(fullpth, cmakedir))
126 if pn and headers:
127 for header in headers:
128 RecipeHandler.recipeheadermap[header] = pn
129 if pn and cmakefiles:
130 for fn in cmakefiles:
131 RecipeHandler.recipecmakefilemap[fn] = pn
132
133 @staticmethod
134 def load_binmap(d):
135 '''Build up native binary->recipe mapping'''
136 if RecipeHandler.recipebinmap:
137 return
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500138 sstate_manifests = d.getVar('SSTATE_MANIFESTS')
139 staging_bindir_native = d.getVar('STAGING_BINDIR_NATIVE')
140 build_arch = d.getVar('BUILD_ARCH')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500141 fileprefix = 'manifest-%s-' % build_arch
142 for fn in glob.glob(os.path.join(sstate_manifests, '%s*-native.populate_sysroot' % fileprefix)):
143 with open(fn, 'r') as f:
144 pn = os.path.basename(fn).rsplit('.', 1)[0][len(fileprefix):]
145 for line in f:
146 if line.startswith(staging_bindir_native):
147 prog = os.path.basename(line.rstrip())
148 RecipeHandler.recipebinmap[prog] = pn
149
150 @staticmethod
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500151 def checkfiles(path, speclist, recursive=False, excludedirs=None):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500152 results = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500153 if recursive:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500154 for root, dirs, files in os.walk(path, topdown=True):
155 if excludedirs:
156 dirs[:] = [d for d in dirs if d not in excludedirs]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500157 for fn in files:
158 for spec in speclist:
159 if fnmatch.fnmatch(fn, spec):
160 results.append(os.path.join(root, fn))
161 else:
162 for spec in speclist:
163 results.extend(glob.glob(os.path.join(path, spec)))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500164 return results
165
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500166 @staticmethod
167 def handle_depends(libdeps, pcdeps, deps, outlines, values, d):
168 if pcdeps:
169 recipemap = read_pkgconfig_provides(d)
170 if libdeps:
171 RecipeHandler.load_libmap(d)
172
173 ignorelibs = ['socket']
174 ignoredeps = ['gcc-runtime', 'glibc', 'uclibc', 'musl', 'tar-native', 'binutils-native', 'coreutils-native']
175
176 unmappedpc = []
177 pcdeps = list(set(pcdeps))
178 for pcdep in pcdeps:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600179 if isinstance(pcdep, str):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500180 recipe = recipemap.get(pcdep, None)
181 if recipe:
182 deps.append(recipe)
183 else:
184 if not pcdep.startswith('$'):
185 unmappedpc.append(pcdep)
186 else:
187 for item in pcdep:
188 recipe = recipemap.get(pcdep, None)
189 if recipe:
190 deps.append(recipe)
191 break
192 else:
193 unmappedpc.append('(%s)' % ' or '.join(pcdep))
194
195 unmappedlibs = []
196 for libdep in libdeps:
197 if isinstance(libdep, tuple):
198 lib, header = libdep
199 else:
200 lib = libdep
201 header = None
202
203 if lib in ignorelibs:
204 logger.debug('Ignoring library dependency %s' % lib)
205 continue
206
207 recipe = RecipeHandler.recipelibmap.get(lib, None)
208 if recipe:
209 deps.append(recipe)
210 elif recipe is None:
211 if header:
212 RecipeHandler.load_devel_filemap(d)
213 recipe = RecipeHandler.recipeheadermap.get(header, None)
214 if recipe:
215 deps.append(recipe)
216 elif recipe is None:
217 unmappedlibs.append(lib)
218 else:
219 unmappedlibs.append(lib)
220
221 deps = set(deps).difference(set(ignoredeps))
222
223 if unmappedpc:
224 outlines.append('# NOTE: unable to map the following pkg-config dependencies: %s' % ' '.join(unmappedpc))
225 outlines.append('# (this is based on recipes that have previously been built and packaged)')
226
227 if unmappedlibs:
228 outlines.append('# NOTE: the following library dependencies are unknown, ignoring: %s' % ' '.join(list(set(unmappedlibs))))
229 outlines.append('# (this is based on recipes that have previously been built and packaged)')
230
231 if deps:
232 values['DEPENDS'] = ' '.join(deps)
233
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500234 @staticmethod
235 def genfunction(outlines, funcname, content, python=False, forcespace=False):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500236 if python:
237 prefix = 'python '
238 else:
239 prefix = ''
240 outlines.append('%s%s () {' % (prefix, funcname))
241 if python or forcespace:
242 indent = ' '
243 else:
244 indent = '\t'
245 addnoop = not python
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500246 for line in content:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500247 outlines.append('%s%s' % (indent, line))
248 if addnoop:
249 strippedline = line.lstrip()
250 if strippedline and not strippedline.startswith('#'):
251 addnoop = False
252 if addnoop:
253 # Without this there'll be a syntax error
254 outlines.append('%s:' % indent)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500255 outlines.append('}')
256 outlines.append('')
257
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500258 def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500259 return False
260
261
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500262def validate_pv(pv):
263 if not pv or '_version' in pv.lower() or pv[0] not in '0123456789':
264 return False
265 return True
266
267def determine_from_filename(srcfile):
268 """Determine name and version from a filename"""
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600269 if is_package(srcfile):
270 # Force getting the value from the package metadata
271 return None, None
272
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500273 if '.tar.' in srcfile:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600274 namepart = srcfile.split('.tar.')[0]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500275 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600276 namepart = os.path.splitext(srcfile)[0]
277 namepart = namepart.lower().replace('_', '-')
278 if namepart.endswith('.src'):
279 namepart = namepart[:-4]
280 if namepart.endswith('.orig'):
281 namepart = namepart[:-5]
282 splitval = namepart.split('-')
283 logger.debug('determine_from_filename: split name %s into: %s' % (srcfile, splitval))
284
285 ver_re = re.compile('^v?[0-9]')
286
287 pv = None
288 pn = None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500289 if len(splitval) == 1:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600290 # Try to split the version out if there is no separator (or a .)
291 res = re.match('^([^0-9]+)([0-9.]+.*)$', namepart)
292 if res:
293 if len(res.group(1)) > 1 and len(res.group(2)) > 1:
294 pn = res.group(1).rstrip('.')
295 pv = res.group(2)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500296 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600297 pn = namepart
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500298 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600299 if splitval[-1] in ['source', 'src']:
300 splitval.pop()
301 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]):
302 pv = '-'.join(splitval[-2:])
303 if pv.endswith('-release'):
304 pv = pv[:-8]
305 splitval = splitval[:-2]
306 elif ver_re.match(splitval[-1]):
307 pv = splitval.pop()
308 pn = '-'.join(splitval)
309 if pv and pv.startswith('v'):
310 pv = pv[1:]
311 logger.debug('determine_from_filename: name = "%s" version = "%s"' % (pn, pv))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500312 return (pn, pv)
313
314def determine_from_url(srcuri):
315 """Determine name and version from a URL"""
316 pn = None
317 pv = None
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600318 parseres = urlparse(srcuri.lower().split(';', 1)[0])
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500319 if parseres.path:
320 if 'github.com' in parseres.netloc:
321 res = re.search(r'.*/(.*?)/archive/(.*)-final\.(tar|zip)', parseres.path)
322 if res:
323 pn = res.group(1).strip().replace('_', '-')
324 pv = res.group(2).strip().replace('_', '.')
325 else:
326 res = re.search(r'.*/(.*?)/archive/v?(.*)\.(tar|zip)', parseres.path)
327 if res:
328 pn = res.group(1).strip().replace('_', '-')
329 pv = res.group(2).strip().replace('_', '.')
330 elif 'bitbucket.org' in parseres.netloc:
331 res = re.search(r'.*/(.*?)/get/[a-zA-Z_-]*([0-9][0-9a-zA-Z_.]*)\.(tar|zip)', parseres.path)
332 if res:
333 pn = res.group(1).strip().replace('_', '-')
334 pv = res.group(2).strip().replace('_', '.')
335
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500336 if not pn and not pv:
337 if parseres.scheme not in ['git', 'gitsm', 'svn', 'hg']:
338 srcfile = os.path.basename(parseres.path.rstrip('/'))
339 pn, pv = determine_from_filename(srcfile)
340 elif parseres.scheme in ['git', 'gitsm']:
341 pn = os.path.basename(parseres.path.rstrip('/')).lower().replace('_', '-')
342 if pn.endswith('.git'):
343 pn = pn[:-4]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500344
345 logger.debug('Determined from source URL: name = "%s", version = "%s"' % (pn, pv))
346 return (pn, pv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500347
348def supports_srcrev(uri):
349 localdata = bb.data.createCopy(tinfoil.config_data)
350 # This is a bit sad, but if you don't have this set there can be some
351 # odd interactions with the urldata cache which lead to errors
352 localdata.setVar('SRCREV', '${AUTOREV}')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600353 try:
354 fetcher = bb.fetch2.Fetch([uri], localdata)
355 urldata = fetcher.ud
356 for u in urldata:
357 if urldata[u].method.supports_srcrev():
358 return True
359 except bb.fetch2.FetchError as e:
360 logger.debug('FetchError in supports_srcrev: %s' % str(e))
361 # Fall back to basic check
362 if uri.startswith(('git://', 'gitsm://')):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500363 return True
364 return False
365
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500366def reformat_git_uri(uri):
367 '''Convert any http[s]://....git URI into git://...;protocol=http[s]'''
368 checkuri = uri.split(';', 1)[0]
Andrew Geissler595f6302022-01-24 19:11:47 +0000369 if checkuri.endswith('.git') or '/git/' in checkuri or re.match('https?://git(hub|lab).com/[^/]+/[^/]+/?$', checkuri):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500370 # Appends scheme if the scheme is missing
371 if not '://' in uri:
372 uri = 'git://' + uri
373 scheme, host, path, user, pswd, parms = bb.fetch2.decodeurl(uri)
374 # Detection mechanism, this is required due to certain URL are formatter with ":" rather than "/"
375 # which causes decodeurl to fail getting the right host and path
376 if len(host.split(':')) > 1:
377 splitslash = host.split(':')
Brad Bishop316dfdd2018-06-25 12:45:53 -0400378 # Port number should not be split from host
379 if not re.match('^[0-9]+$', splitslash[1]):
380 host = splitslash[0]
381 path = '/' + splitslash[1] + path
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500382 #Algorithm:
383 # if user is defined, append protocol=ssh or if a protocol is defined, then honor the user-defined protocol
384 # if no user & password is defined, check for scheme type and append the protocol with the scheme type
385 # finally if protocols or if the url is well-formed, do nothing and rejoin everything back to normal
386 # Need to repackage the arguments for encodeurl, the format is: (scheme, host, path, user, password, OrderedDict([('key', 'value')]))
387 if user:
388 if not 'protocol' in parms:
389 parms.update({('protocol', 'ssh')})
390 elif (scheme == "http" or scheme == 'https' or scheme == 'ssh') and not ('protocol' in parms):
391 parms.update({('protocol', scheme)})
392 # Always append 'git://'
393 fUrl = bb.fetch2.encodeurl(('git', host, path, user, pswd, parms))
394 return fUrl
395 else:
396 return uri
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500397
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600398def is_package(url):
399 '''Check if a URL points to a package'''
400 checkurl = url.split(';', 1)[0]
401 if checkurl.endswith(('.deb', '.ipk', '.rpm', '.srpm')):
402 return True
403 return False
404
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500405def create_recipe(args):
406 import bb.process
407 import tempfile
408 import shutil
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600409 import oe.recipeutils
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500410
411 pkgarch = ""
412 if args.machine:
413 pkgarch = "${MACHINE_ARCH}"
414
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600415 extravalues = {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500416 checksums = {}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500417 tempsrc = ''
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600418 source = args.source
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500419 srcsubdir = ''
420 srcrev = '${AUTOREV}'
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500421 srcbranch = ''
422 scheme = ''
423 storeTagName = ''
424 pv_srcpv = False
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600425
Patrick Williams169d7bc2024-01-05 11:33:25 -0600426 handled = []
427 classes = []
428
429 # Find all plugins that want to register handlers
430 logger.debug('Loading recipe handlers')
431 raw_handlers = []
432 for plugin in plugins:
433 if hasattr(plugin, 'register_recipe_handlers'):
434 plugin.register_recipe_handlers(raw_handlers)
435 # Sort handlers by priority
436 handlers = []
437 for i, handler in enumerate(raw_handlers):
438 if isinstance(handler, tuple):
439 handlers.append((handler[0], handler[1], i))
440 else:
441 handlers.append((handler, 0, i))
442 handlers.sort(key=lambda item: (item[1], -item[2]), reverse=True)
443 for handler, priority, _ in handlers:
444 logger.debug('Handler: %s (priority %d)' % (handler.__class__.__name__, priority))
445 setattr(handler, '_devtool', args.devtool)
446 handlers = [item[0] for item in handlers]
447
448 fetchuri = None
449 for handler in handlers:
450 if hasattr(handler, 'process_url'):
451 ret = handler.process_url(args, classes, handled, extravalues)
452 if 'url' in handled and ret:
453 fetchuri = ret
454 break
455
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600456 if os.path.isfile(source):
457 source = 'file://%s' % os.path.abspath(source)
458
459 if scriptutils.is_src_url(source):
Brad Bishop316dfdd2018-06-25 12:45:53 -0400460 # Warn about github archive URLs
Andrew Geisslerb7d28612020-07-24 16:15:54 -0500461 if re.match(r'https?://github.com/[^/]+/[^/]+/archive/.+(\.tar\..*|\.zip)$', source):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800462 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).')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500463 # Fetch a URL
Patrick Williams169d7bc2024-01-05 11:33:25 -0600464 if not fetchuri:
465 fetchuri = reformat_git_uri(urldefrag(source)[0])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500466 if args.binary:
467 # Assume the archive contains the directory structure verbatim
468 # so we need to extract to a subdirectory
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000469 fetchuri += ';subdir=${BPN}'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500470 srcuri = fetchuri
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500471 rev_re = re.compile(';rev=([^;]+)')
472 res = rev_re.search(srcuri)
473 if res:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500474 if args.srcrev:
475 logger.error('rev= parameter and -S/--srcrev option cannot both be specified - use one or the other')
476 sys.exit(1)
477 if args.autorev:
478 logger.error('rev= parameter and -a/--autorev option cannot both be specified - use one or the other')
479 sys.exit(1)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500480 srcrev = res.group(1)
481 srcuri = rev_re.sub('', srcuri)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500482 elif args.srcrev:
483 srcrev = args.srcrev
484
485 # Check whether users provides any branch info in fetchuri.
486 # If true, we will skip all branch checking process to honor all user's input.
487 scheme, network, path, user, passwd, params = bb.fetch2.decodeurl(fetchuri)
488 srcbranch = params.get('branch')
489 if args.srcbranch:
490 if srcbranch:
491 logger.error('branch= parameter and -B/--srcbranch option cannot both be specified - use one or the other')
492 sys.exit(1)
493 srcbranch = args.srcbranch
Andrew Geisslerd25ed322020-06-27 00:28:28 -0500494 params['branch'] = srcbranch
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500495 nobranch = params.get('nobranch')
496 if nobranch and srcbranch:
497 logger.error('nobranch= cannot be used if you specify a branch')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500498 sys.exit(1)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500499 tag = params.get('tag')
500 if not srcbranch and not nobranch and srcrev != '${AUTOREV}':
501 # Append nobranch=1 in the following conditions:
502 # 1. User did not set 'branch=' in srcuri, and
503 # 2. User did not set 'nobranch=1' in srcuri, and
504 # 3. Source revision is not '${AUTOREV}'
505 params['nobranch'] = '1'
506 if tag:
507 # Keep a copy of tag and append nobranch=1 then remove tag from URL.
508 # Bitbake fetcher unable to fetch when {AUTOREV} and tag is set at the same time.
509 storeTagName = params['tag']
510 params['nobranch'] = '1'
511 del params['tag']
Andrew Geissler595f6302022-01-24 19:11:47 +0000512 # Assume 'master' branch if not set
513 if scheme in ['git', 'gitsm'] and 'branch' not in params and 'nobranch' not in params:
514 params['branch'] = 'master'
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500515 fetchuri = bb.fetch2.encodeurl((scheme, network, path, user, passwd, params))
516
517 tmpparent = tinfoil.config_data.getVar('BASE_WORKDIR')
518 bb.utils.mkdirhier(tmpparent)
519 tempsrc = tempfile.mkdtemp(prefix='recipetool-', dir=tmpparent)
520 srctree = os.path.join(tempsrc, 'source')
521
522 try:
523 checksums, ftmpdir = scriptutils.fetch_url(tinfoil, fetchuri, srcrev, srctree, logger, preserve_tmp=args.keep_temp)
524 except scriptutils.FetchUrlFailure as e:
525 logger.error(str(e))
526 sys.exit(1)
527
528 if ftmpdir and args.keep_temp:
529 logger.info('Fetch temp directory is %s' % ftmpdir)
530
Brad Bishop6dbb3162019-11-25 09:41:34 -0500531 dirlist = scriptutils.filter_src_subdirs(srctree)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500532 logger.debug('Directory listing (excluding filtered out):\n %s' % '\n '.join(dirlist))
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500533 if len(dirlist) == 1:
534 singleitem = os.path.join(srctree, dirlist[0])
535 if os.path.isdir(singleitem):
536 # We unpacked a single directory, so we should use that
537 srcsubdir = dirlist[0]
538 srctree = os.path.join(srctree, srcsubdir)
539 else:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500540 check_single_file(dirlist[0], fetchuri)
541 elif len(dirlist) == 0:
542 if '/' in fetchuri:
543 fn = os.path.join(tinfoil.config_data.getVar('DL_DIR'), fetchuri.split('/')[-1])
544 if os.path.isfile(fn):
545 check_single_file(fn, fetchuri)
546 # If we've got to here then there's no source so we might as well give up
547 logger.error('URL %s resulted in an empty source tree' % fetchuri)
548 sys.exit(1)
549
550 # We need this checking mechanism to improve the recipe created by recipetool and devtool
551 # is able to parse and build by bitbake.
552 # If there is no input for branch name, then check for branch name with SRCREV provided.
553 if not srcbranch and not nobranch and srcrev and (srcrev != '${AUTOREV}') and scheme in ['git', 'gitsm']:
554 try:
555 cmd = 'git branch -r --contains'
556 check_branch, check_branch_err = bb.process.run('%s %s' % (cmd, srcrev), cwd=srctree)
557 except bb.process.ExecutionError as err:
558 logger.error(str(err))
559 sys.exit(1)
560 get_branch = [x.strip() for x in check_branch.splitlines()]
561 # Remove HEAD reference point and drop remote prefix
562 get_branch = [x.split('/', 1)[1] for x in get_branch if not x.startswith('origin/HEAD')]
563 if 'master' in get_branch:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500564 # Even with the case where get_branch has multiple objects, if 'master' is one
565 # of them, we should default take from 'master'
Andrew Geissler595f6302022-01-24 19:11:47 +0000566 srcbranch = 'master'
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500567 elif len(get_branch) == 1:
568 # If 'master' isn't in get_branch and get_branch contains only ONE object, then store result into 'srcbranch'
569 srcbranch = get_branch[0]
570 else:
571 # If get_branch contains more than one objects, then display error and exit.
572 mbrch = '\n ' + '\n '.join(get_branch)
573 logger.error('Revision %s was found on multiple branches: %s\nPlease provide the correct branch with -B/--srcbranch' % (srcrev, mbrch))
574 sys.exit(1)
575
576 # Since we might have a value in srcbranch, we need to
577 # recontruct the srcuri to include 'branch' in params.
578 scheme, network, path, user, passwd, params = bb.fetch2.decodeurl(srcuri)
Andrew Geissler595f6302022-01-24 19:11:47 +0000579 if scheme in ['git', 'gitsm']:
580 params['branch'] = srcbranch or 'master'
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500581
582 if storeTagName and scheme in ['git', 'gitsm']:
583 # Check srcrev using tag and check validity of the tag
584 cmd = ('git rev-parse --verify %s' % (storeTagName))
585 try:
586 check_tag, check_tag_err = bb.process.run('%s' % cmd, cwd=srctree)
587 srcrev = check_tag.split()[0]
588 except bb.process.ExecutionError as err:
589 logger.error(str(err))
590 logger.error("Possibly wrong tag name is provided")
591 sys.exit(1)
592 # Drop tag from srcuri as it will have conflicts with SRCREV during recipe parse.
593 del params['tag']
594 srcuri = bb.fetch2.encodeurl((scheme, network, path, user, passwd, params))
595
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600596 if os.path.exists(os.path.join(srctree, '.gitmodules')) and srcuri.startswith('git://'):
597 srcuri = 'gitsm://' + srcuri[6:]
598 logger.info('Fetching submodules...')
599 bb.process.run('git submodule update --init --recursive', cwd=srctree)
600
601 if is_package(fetchuri):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500602 localdata = bb.data.createCopy(tinfoil.config_data)
603 pkgfile = bb.fetch2.localpath(fetchuri, localdata)
604 if pkgfile:
605 tmpfdir = tempfile.mkdtemp(prefix='recipetool-')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600606 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600607 if pkgfile.endswith(('.deb', '.ipk')):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500608 stdout, _ = bb.process.run('ar x %s' % pkgfile, cwd=tmpfdir)
609 stdout, _ = bb.process.run('tar xf control.tar.gz', cwd=tmpfdir)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600610 values = convert_debian(tmpfdir)
611 extravalues.update(values)
612 elif pkgfile.endswith(('.rpm', '.srpm')):
613 stdout, _ = bb.process.run('rpm -qp --xml %s > pkginfo.xml' % pkgfile, cwd=tmpfdir)
614 values = convert_rpm_xml(os.path.join(tmpfdir, 'pkginfo.xml'))
615 extravalues.update(values)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500616 finally:
617 shutil.rmtree(tmpfdir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500618 else:
619 # Assume we're pointing to an existing source tree
620 if args.extract_to:
621 logger.error('--extract-to cannot be specified if source is a directory')
622 sys.exit(1)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600623 if not os.path.isdir(source):
624 logger.error('Invalid source directory %s' % source)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500625 sys.exit(1)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600626 srctree = source
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500627 srcuri = ''
628 if os.path.exists(os.path.join(srctree, '.git')):
629 # Try to get upstream repo location from origin remote
630 try:
631 stdout, _ = bb.process.run('git remote -v', cwd=srctree, shell=True)
632 except bb.process.ExecutionError as e:
633 stdout = None
634 if stdout:
635 for line in stdout.splitlines():
636 splitline = line.split()
637 if len(splitline) > 1:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600638 if splitline[0] == 'origin' and scriptutils.is_src_url(splitline[1]):
Andrew Geissler595f6302022-01-24 19:11:47 +0000639 srcuri = reformat_git_uri(splitline[1]) + ';branch=master'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500640 srcsubdir = 'git'
641 break
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500642
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500643 if args.src_subdir:
644 srcsubdir = os.path.join(srcsubdir, args.src_subdir)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500645 srctree_use = os.path.abspath(os.path.join(srctree, args.src_subdir))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500646 else:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500647 srctree_use = os.path.abspath(srctree)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500648
649 if args.outfile and os.path.isdir(args.outfile):
650 outfile = None
651 outdir = args.outfile
652 else:
653 outfile = args.outfile
654 outdir = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500655 if outfile and outfile != '-':
656 if os.path.exists(outfile):
657 logger.error('Output file %s already exists' % outfile)
658 sys.exit(1)
659
660 lines_before = []
661 lines_after = []
662
663 lines_before.append('# Recipe created by %s' % os.path.basename(sys.argv[0]))
664 lines_before.append('# This is the basis of a recipe and may need further editing in order to be fully functional.')
665 lines_before.append('# (Feel free to remove these comments when editing.)')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600666 # We need a blank line here so that patch_recipe_lines can rewind before the LICENSE comments
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500667 lines_before.append('')
668
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500669 # We'll come back and replace this later in handle_license_vars()
670 lines_before.append('##LICENSE_PLACEHOLDER##')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600671
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500672
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500673 # FIXME This is kind of a hack, we probably ought to be using bitbake to do this
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500674 pn = None
675 pv = None
676 if outfile:
677 recipefn = os.path.splitext(os.path.basename(outfile))[0]
678 fnsplit = recipefn.split('_')
679 if len(fnsplit) > 1:
680 pn = fnsplit[0]
681 pv = fnsplit[1]
682 else:
683 pn = recipefn
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500684
685 if args.version:
686 pv = args.version
687
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500688 if args.name:
689 pn = args.name
690 if args.name.endswith('-native'):
691 if args.also_native:
692 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')
693 sys.exit(1)
694 classes.append('native')
695 elif args.name.startswith('nativesdk-'):
696 if args.also_native:
697 logger.error('--also-native cannot be specified for a recipe named nativesdk-* (nativesdk-* denotes a recipe that is already only for nativesdk)')
698 sys.exit(1)
699 classes.append('nativesdk')
700
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500701 if pv and pv not in 'git svn hg'.split():
702 realpv = pv
703 else:
704 realpv = None
705
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500706 if not srcuri:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500707 lines_before.append('# No information for SRC_URI yet (only an external source tree was specified)')
708 lines_before.append('SRC_URI = "%s"' % srcuri)
Patrick Williams169d7bc2024-01-05 11:33:25 -0600709 shown_checksums = ["%ssum" % s for s in bb.fetch2.SHOWN_CHECKSUM_LIST]
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500710 for key, value in sorted(checksums.items()):
Patrick Williams169d7bc2024-01-05 11:33:25 -0600711 if key in shown_checksums:
712 lines_before.append('SRC_URI[%s] = "%s"' % (key, value))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500713 if srcuri and supports_srcrev(srcuri):
714 lines_before.append('')
715 lines_before.append('# Modify these as desired')
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500716 # Note: we have code to replace realpv further down if it gets set to some other value
717 scheme, _, _, _, _, _ = bb.fetch2.decodeurl(srcuri)
718 if scheme in ['git', 'gitsm']:
719 srcpvprefix = 'git'
720 elif scheme == 'svn':
721 srcpvprefix = 'svnr'
722 else:
723 srcpvprefix = scheme
Andrew Geissler5082cc72023-09-11 08:41:39 -0400724 lines_before.append('PV = "%s+%s"' % (realpv or '1.0', srcpvprefix))
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500725 pv_srcpv = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600726 if not args.autorev and srcrev == '${AUTOREV}':
727 if os.path.exists(os.path.join(srctree, '.git')):
728 (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
Brad Bishopc342db32019-05-15 21:57:59 -0400729 srcrev = stdout.rstrip()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500730 lines_before.append('SRCREV = "%s"' % srcrev)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500731 if args.provides:
732 lines_before.append('PROVIDES = "%s"' % args.provides)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500733 lines_before.append('')
734
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600735 if srcsubdir and not args.binary:
736 # (for binary packages we explicitly specify subdir= when fetching to
737 # match the default value of S, so we don't need to set it in that case)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500738 lines_before.append('S = "${WORKDIR}/%s"' % srcsubdir)
739 lines_before.append('')
740
741 if pkgarch:
742 lines_after.append('PACKAGE_ARCH = "%s"' % pkgarch)
743 lines_after.append('')
744
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500745 if args.binary:
Patrick Williams213cb262021-08-07 19:21:33 -0500746 lines_after.append('INSANE_SKIP:${PN} += "already-stripped"')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500747 lines_after.append('')
748
Andrew Geissler82c905d2020-04-13 13:39:40 -0500749 if args.npm_dev:
750 extravalues['NPM_INSTALL_DEV'] = 1
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500751
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500752 # Apply the handlers
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500753 if args.binary:
754 classes.append('bin_package')
755 handled.append('buildsystem')
756
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500757 for handler in handlers:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500758 handler.process(srctree_use, classes, lines_before, lines_after, handled, extravalues)
759
Andrew Geissler8f840682023-07-21 09:09:43 -0500760 # native and nativesdk classes are special and must be inherited last
761 # If present, put them at the end of the classes list
762 classes.sort(key=lambda c: c in ("native", "nativesdk"))
763
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500764 extrafiles = extravalues.pop('extrafiles', {})
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600765 extra_pn = extravalues.pop('PN', None)
766 extra_pv = extravalues.pop('PV', None)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500767
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600768 if extra_pv and not realpv:
769 realpv = extra_pv
770 if not validate_pv(realpv):
771 realpv = None
772 else:
773 realpv = realpv.lower().split()[0]
774 if '_' in realpv:
775 realpv = realpv.replace('_', '-')
776 if extra_pn and not pn:
777 pn = extra_pn
778 if pn.startswith('GNU '):
779 pn = pn[4:]
780 if ' ' in pn:
781 # Probably a descriptive identifier rather than a proper name
782 pn = None
783 else:
784 pn = pn.lower()
785 if '_' in pn:
786 pn = pn.replace('_', '-')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500787
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500788 if srcuri and not realpv or not pn:
789 name_pn, name_pv = determine_from_url(srcuri)
790 if name_pn and not pn:
791 pn = name_pn
792 if name_pv and not realpv:
793 realpv = name_pv
794
795 licvalues = handle_license_vars(srctree_use, lines_before, handled, extravalues, tinfoil.config_data)
796
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500797 if not outfile:
798 if not pn:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500799 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)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500800 # devtool looks for this specific exit code, so don't change it
801 sys.exit(15)
802 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600803 if srcuri and srcuri.startswith(('gitsm://', 'git://', 'hg://', 'svn://')):
804 suffix = srcuri.split(':', 1)[0]
805 if suffix == 'gitsm':
806 suffix = 'git'
807 outfile = '%s_%s.bb' % (pn, suffix)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500808 elif realpv:
809 outfile = '%s_%s.bb' % (pn, realpv)
810 else:
811 outfile = '%s.bb' % pn
812 if outdir:
813 outfile = os.path.join(outdir, outfile)
814 # We need to check this again
815 if os.path.exists(outfile):
816 logger.error('Output file %s already exists' % outfile)
817 sys.exit(1)
818
819 # Move any extra files the plugins created to a directory next to the recipe
820 if extrafiles:
821 if outfile == '-':
822 extraoutdir = pn
823 else:
824 extraoutdir = os.path.join(os.path.dirname(outfile), pn)
825 bb.utils.mkdirhier(extraoutdir)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600826 for destfn, extrafile in extrafiles.items():
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500827 shutil.move(extrafile, os.path.join(extraoutdir, destfn))
828
829 lines = lines_before
830 lines_before = []
831 skipblank = True
832 for line in lines:
833 if skipblank:
834 skipblank = False
835 if not line:
836 continue
837 if line.startswith('S = '):
838 if realpv and pv not in 'git svn hg'.split():
839 line = line.replace(realpv, '${PV}')
840 if pn:
841 line = line.replace(pn, '${BPN}')
842 if line == 'S = "${WORKDIR}/${BPN}-${PV}"':
843 skipblank = True
844 continue
845 elif line.startswith('SRC_URI = '):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500846 if realpv and not pv_srcpv:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500847 line = line.replace(realpv, '${PV}')
848 elif line.startswith('PV = '):
849 if realpv:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500850 # Replace the first part of the PV value
Andrew Geisslerb7d28612020-07-24 16:15:54 -0500851 line = re.sub(r'"[^+]*\+', '"%s+' % realpv, line)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500852 lines_before.append(line)
853
854 if args.also_native:
855 lines = lines_after
856 lines_after = []
857 bbclassextend = None
858 for line in lines:
859 if line.startswith('BBCLASSEXTEND ='):
860 splitval = line.split('"')
861 if len(splitval) > 1:
862 bbclassextend = splitval[1].split()
863 if not 'native' in bbclassextend:
864 bbclassextend.insert(0, 'native')
865 line = 'BBCLASSEXTEND = "%s"' % ' '.join(bbclassextend)
866 lines_after.append(line)
867 if not bbclassextend:
868 lines_after.append('BBCLASSEXTEND = "native"')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500869
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500870 postinst = ("postinst", extravalues.pop('postinst', None))
871 postrm = ("postrm", extravalues.pop('postrm', None))
872 preinst = ("preinst", extravalues.pop('preinst', None))
873 prerm = ("prerm", extravalues.pop('prerm', None))
874 funcs = [postinst, postrm, preinst, prerm]
875 for func in funcs:
876 if func[1]:
877 RecipeHandler.genfunction(lines_after, 'pkg_%s_${PN}' % func[0], func[1])
878
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500879 outlines = []
880 outlines.extend(lines_before)
881 if classes:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500882 if outlines[-1] and not outlines[-1].startswith('#'):
883 outlines.append('')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500884 outlines.append('inherit %s' % ' '.join(classes))
885 outlines.append('')
886 outlines.extend(lines_after)
887
Patrick Williams169d7bc2024-01-05 11:33:25 -0600888 outlines = [ line.rstrip('\n') +"\n" for line in outlines]
889
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600890 if extravalues:
Patrick Williams169d7bc2024-01-05 11:33:25 -0600891 _, outlines = oe.recipeutils.patch_recipe_lines(outlines, extravalues, trailing_newline=True)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600892
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500893 if args.extract_to:
894 scriptutils.git_convert_standalone_clone(srctree)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500895 if os.path.isdir(args.extract_to):
896 # If the directory exists we'll move the temp dir into it instead of
897 # its contents - of course, we could try to always move its contents
898 # but that is a pain if there are symlinks; the simplest solution is
899 # to just remove it first
900 os.rmdir(args.extract_to)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500901 shutil.move(srctree, args.extract_to)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500902 if tempsrc == srctree:
903 tempsrc = None
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500904 log_info_cond('Source extracted to %s' % args.extract_to, args.devtool)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500905
906 if outfile == '-':
Patrick Williams169d7bc2024-01-05 11:33:25 -0600907 sys.stdout.write(''.join(outlines) + '\n')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500908 else:
909 with open(outfile, 'w') as f:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600910 lastline = None
911 for line in outlines:
912 if not lastline and not line:
913 # Skip extra blank lines
914 continue
Patrick Williams169d7bc2024-01-05 11:33:25 -0600915 f.write('%s' % line)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600916 lastline = line
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500917 log_info_cond('Recipe %s has been created; further editing may be required to make it fully functional' % outfile, args.devtool)
Andrew Geissler220dafd2023-10-04 10:18:08 -0500918 tinfoil.modified_files()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500919
920 if tempsrc:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600921 if args.keep_temp:
922 logger.info('Preserving temporary directory %s' % tempsrc)
923 else:
924 shutil.rmtree(tempsrc)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500925
926 return 0
927
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500928def check_single_file(fn, fetchuri):
929 """Determine if a single downloaded file is something we can't handle"""
930 with open(fn, 'r', errors='surrogateescape') as f:
931 if '<html' in f.read(100).lower():
932 logger.error('Fetching "%s" returned a single HTML page - check the URL is correct and functional' % fetchuri)
933 sys.exit(1)
934
935def split_value(value):
936 if isinstance(value, str):
937 return value.split()
938 else:
939 return value
940
Andrew Geissler595f6302022-01-24 19:11:47 +0000941def fixup_license(value):
942 # Ensure licenses with OR starts and ends with brackets
943 if '|' in value:
944 return '(' + value + ')'
945 return value
946
947def tidy_licenses(value):
948 """Flat, split and sort licenses"""
949 from oe.license import flattened_licenses
950 def _choose(a, b):
951 str_a, str_b = sorted((" & ".join(a), " & ".join(b)), key=str.casefold)
952 return ["(%s | %s)" % (str_a, str_b)]
953 if not isinstance(value, str):
954 value = " & ".join(value)
955 return sorted(list(set(flattened_licenses(value, _choose))), key=str.casefold)
956
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600957def handle_license_vars(srctree, lines_before, handled, extravalues, d):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500958 lichandled = [x for x in handled if x[0] == 'license']
959 if lichandled:
960 # Someone else has already handled the license vars, just return their value
961 return lichandled[0][1]
962
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600963 licvalues = guess_license(srctree, d)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500964 licenses = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600965 lic_files_chksum = []
966 lic_unknown = []
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500967 lines = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600968 if licvalues:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600969 for licvalue in licvalues:
Andrew Geissler595f6302022-01-24 19:11:47 +0000970 license = licvalue[0]
971 lics = tidy_licenses(fixup_license(license))
972 lics = [lic for lic in lics if lic not in licenses]
973 if len(lics):
974 licenses.extend(lics)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600975 lic_files_chksum.append('file://%s;md5=%s' % (licvalue[1], licvalue[2]))
Andrew Geissler595f6302022-01-24 19:11:47 +0000976 if license == 'Unknown':
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600977 lic_unknown.append(licvalue[1])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600978 if lic_unknown:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500979 lines.append('#')
980 lines.append('# The following license files were not able to be identified and are')
981 lines.append('# represented as "Unknown" below, you will need to check them yourself:')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600982 for licfile in lic_unknown:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500983 lines.append('# %s' % licfile)
984
Andrew Geissler595f6302022-01-24 19:11:47 +0000985 extra_license = tidy_licenses(extravalues.pop('LICENSE', ''))
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500986 if extra_license:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600987 if licenses == ['Unknown']:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500988 licenses = extra_license
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600989 else:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500990 for item in extra_license:
991 if item not in licenses:
992 licenses.append(item)
993 extra_lic_files_chksum = split_value(extravalues.pop('LIC_FILES_CHKSUM', []))
994 for item in extra_lic_files_chksum:
995 if item not in lic_files_chksum:
996 lic_files_chksum.append(item)
997
998 if lic_files_chksum:
999 # We are going to set the vars, so prepend the standard disclaimer
1000 lines.insert(0, '# WARNING: the following LICENSE and LIC_FILES_CHKSUM values are best guesses - it is')
1001 lines.insert(1, '# your responsibility to verify that the values are complete and correct.')
1002 else:
1003 # Without LIC_FILES_CHKSUM we set LICENSE = "CLOSED" to allow the
1004 # user to get started easily
1005 lines.append('# Unable to find any files that looked like license statements. Check the accompanying')
1006 lines.append('# documentation and source headers and set LICENSE and LIC_FILES_CHKSUM accordingly.')
1007 lines.append('#')
1008 lines.append('# NOTE: LICENSE is being set to "CLOSED" to allow you to at least start building - if')
1009 lines.append('# this is not accurate with respect to the licensing of the software being built (it')
1010 lines.append('# will not be in most cases) you must specify the correct value before using this')
1011 lines.append('# recipe for anything other than initial testing/development!')
1012 licenses = ['CLOSED']
1013
1014 if extra_license and sorted(licenses) != sorted(extra_license):
1015 lines.append('# NOTE: Original package / source metadata indicates license is: %s' % ' & '.join(extra_license))
1016
1017 if len(licenses) > 1:
1018 lines.append('#')
1019 lines.append('# NOTE: multiple licenses have been detected; they have been separated with &')
1020 lines.append('# in the LICENSE value for now since it is a reasonable assumption that all')
1021 lines.append('# of the licenses apply. If instead there is a choice between the multiple')
1022 lines.append('# licenses then you should change the value to separate the licenses with |')
1023 lines.append('# instead of &. If there is any doubt, check the accompanying documentation')
1024 lines.append('# to determine which situation is applicable.')
1025
Andrew Geissler595f6302022-01-24 19:11:47 +00001026 lines.append('LICENSE = "%s"' % ' & '.join(sorted(licenses, key=str.casefold)))
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001027 lines.append('LIC_FILES_CHKSUM = "%s"' % ' \\\n '.join(lic_files_chksum))
1028 lines.append('')
1029
1030 # Replace the placeholder so we get the values in the right place in the recipe file
1031 try:
1032 pos = lines_before.index('##LICENSE_PLACEHOLDER##')
1033 except ValueError:
1034 pos = -1
1035 if pos == -1:
1036 lines_before.extend(lines)
1037 else:
1038 lines_before[pos:pos+1] = lines
1039
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001040 handled.append(('license', licvalues))
1041 return licvalues
1042
Andrew Geisslereff27472021-10-29 15:35:00 -05001043def get_license_md5sums(d, static_only=False, linenumbers=False):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001044 import bb.utils
Andrew Geisslereff27472021-10-29 15:35:00 -05001045 import csv
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001046 md5sums = {}
Andrew Geisslereff27472021-10-29 15:35:00 -05001047 if not static_only and not linenumbers:
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001048 # Gather md5sums of license files in common license dir
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001049 commonlicdir = d.getVar('COMMON_LICENSE_DIR')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001050 for fn in os.listdir(commonlicdir):
1051 md5value = bb.utils.md5_file(os.path.join(commonlicdir, fn))
1052 md5sums[md5value] = fn
Andrew Geisslereff27472021-10-29 15:35:00 -05001053
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001054 # The following were extracted from common values in various recipes
1055 # (double checking the license against the license file itself, not just
1056 # the LICENSE value in the recipe)
Andrew Geisslereff27472021-10-29 15:35:00 -05001057
1058 # Read license md5sums from csv file
1059 scripts_path = os.path.dirname(os.path.realpath(__file__))
1060 for path in (d.getVar('BBPATH').split(':')
1061 + [os.path.join(scripts_path, '..', '..')]):
1062 csv_path = os.path.join(path, 'lib', 'recipetool', 'licenses.csv')
1063 if os.path.isfile(csv_path):
1064 with open(csv_path, newline='') as csv_file:
1065 fieldnames = ['md5sum', 'license', 'beginline', 'endline', 'md5']
1066 reader = csv.DictReader(csv_file, delimiter=',', fieldnames=fieldnames)
1067 for row in reader:
1068 if linenumbers:
1069 md5sums[row['md5sum']] = (
1070 row['license'], row['beginline'], row['endline'], row['md5'])
1071 else:
1072 md5sums[row['md5sum']] = row['license']
1073
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001074 return md5sums
1075
Patrick Williams169d7bc2024-01-05 11:33:25 -06001076def crunch_known_licenses(d):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001077 '''
Patrick Williams169d7bc2024-01-05 11:33:25 -06001078 Calculate the MD5 checksums for the crunched versions of all common
1079 licenses. Also add additional known checksums.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001080 '''
Patrick Williams169d7bc2024-01-05 11:33:25 -06001081
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001082 crunched_md5sums = {}
Andrew Geisslereff27472021-10-29 15:35:00 -05001083
1084 # common licenses
Patrick Williams169d7bc2024-01-05 11:33:25 -06001085 crunched_md5sums['ad4e9d34a2e966dfe9837f18de03266d'] = 'GFDL-1.1-only'
1086 crunched_md5sums['d014fb11a34eb67dc717fdcfc97e60ed'] = 'GFDL-1.2-only'
1087 crunched_md5sums['e020ca655b06c112def28e597ab844f1'] = 'GFDL-1.3-only'
Andrew Geisslereff27472021-10-29 15:35:00 -05001088
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001089 # The following two were gleaned from the "forever" npm package
1090 crunched_md5sums['0a97f8e4cbaf889d6fa51f84b89a79f6'] = 'ISC'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001091 # https://github.com/waffle-gl/waffle/blob/master/LICENSE.txt
Andrew Geisslereff27472021-10-29 15:35:00 -05001092 crunched_md5sums['50fab24ce589d69af8964fdbfe414c60'] = 'BSD-2-Clause'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001093 # https://github.com/spigwitmer/fakeds1963s/blob/master/LICENSE
Andrew Geissler9aee5002022-03-30 16:27:02 +00001094 crunched_md5sums['88a4355858a1433fea99fae34a44da88'] = 'GPL-2.0-only'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001095 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
Andrew Geissler9aee5002022-03-30 16:27:02 +00001096 crunched_md5sums['063b5c3ebb5f3aa4c85a2ed18a31fbe7'] = 'GPL-2.0-only'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001097 # https://github.com/FFmpeg/FFmpeg/blob/master/COPYING.LGPLv2.1
Andrew Geissler9aee5002022-03-30 16:27:02 +00001098 crunched_md5sums['7f5202f4d44ed15dcd4915f5210417d8'] = 'LGPL-2.1-only'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001099 # unixODBC-2.3.4 COPYING
Andrew Geissler9aee5002022-03-30 16:27:02 +00001100 crunched_md5sums['3debde09238a8c8e1f6a847e1ec9055b'] = 'LGPL-2.1-only'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001101 # https://github.com/FFmpeg/FFmpeg/blob/master/COPYING.LGPLv3
Andrew Geissler9aee5002022-03-30 16:27:02 +00001102 crunched_md5sums['f90c613c51aa35da4d79dd55fc724ceb'] = 'LGPL-3.0-only'
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001103 # https://raw.githubusercontent.com/eclipse/mosquitto/v1.4.14/epl-v10
1104 crunched_md5sums['efe2cb9a35826992b9df68224e3c2628'] = 'EPL-1.0'
Andrew Geisslereff27472021-10-29 15:35:00 -05001105
1106 # https://raw.githubusercontent.com/jquery/esprima/3.1.3/LICENSE.BSD
1107 crunched_md5sums['80fa7b56a28e8c902e6af194003220a5'] = 'BSD-2-Clause'
1108 # https://raw.githubusercontent.com/npm/npm-install-checks/master/LICENSE
1109 crunched_md5sums['e659f77bfd9002659e112d0d3d59b2c1'] = 'BSD-2-Clause'
1110 # https://raw.githubusercontent.com/silverwind/default-gateway/4.2.0/LICENSE
1111 crunched_md5sums['4c641f2d995c47f5cb08bdb4b5b6ea05'] = 'BSD-2-Clause'
1112 # https://raw.githubusercontent.com/tad-lispy/node-damerau-levenshtein/v1.0.5/LICENSE
1113 crunched_md5sums['2b8c039b2b9a25f0feb4410c4542d346'] = 'BSD-2-Clause'
1114 # https://raw.githubusercontent.com/terser/terser/v3.17.0/LICENSE
1115 crunched_md5sums['8bd23871802951c9ad63855151204c2c'] = 'BSD-2-Clause'
1116 # https://raw.githubusercontent.com/alexei/sprintf.js/1.0.3/LICENSE
1117 crunched_md5sums['008c22318c8ea65928bf730ddd0273e3'] = 'BSD-3-Clause'
1118 # https://raw.githubusercontent.com/Caligatio/jsSHA/v3.2.0/LICENSE
1119 crunched_md5sums['0e46634a01bfef056892949acaea85b1'] = 'BSD-3-Clause'
1120 # https://raw.githubusercontent.com/d3/d3-path/v1.0.9/LICENSE
1121 crunched_md5sums['b5f72aef53d3b2b432702c30b0215666'] = 'BSD-3-Clause'
1122 # https://raw.githubusercontent.com/feross/ieee754/v1.1.13/LICENSE
1123 crunched_md5sums['a39327c997c20da0937955192d86232d'] = 'BSD-3-Clause'
1124 # https://raw.githubusercontent.com/joyent/node-extsprintf/v1.3.0/LICENSE
1125 crunched_md5sums['721f23a96ff4161ca3a5f071bbe18108'] = 'MIT'
1126 # https://raw.githubusercontent.com/pvorb/clone/v0.2.0/LICENSE
1127 crunched_md5sums['b376d29a53c9573006b9970709231431'] = 'MIT'
1128 # https://raw.githubusercontent.com/andris9/encoding/v0.1.12/LICENSE
1129 crunched_md5sums['85d8a977ee9d7c5ab4ac03c9b95431c4'] = 'MIT-0'
1130 # https://raw.githubusercontent.com/faye/websocket-driver-node/0.7.3/LICENSE.md
1131 crunched_md5sums['b66384e7137e41a9b1904ef4d39703b6'] = 'Apache-2.0'
1132 # https://raw.githubusercontent.com/less/less.js/v4.1.1/LICENSE
1133 crunched_md5sums['b27575459e02221ccef97ec0bfd457ae'] = 'Apache-2.0'
1134 # https://raw.githubusercontent.com/microsoft/TypeScript/v3.5.3/LICENSE.txt
1135 crunched_md5sums['a54a1a6a39e7f9dbb4a23a42f5c7fd1c'] = 'Apache-2.0'
1136 # https://raw.githubusercontent.com/request/request/v2.87.0/LICENSE
1137 crunched_md5sums['1034431802e57486b393d00c5d262b8a'] = 'Apache-2.0'
1138 # https://raw.githubusercontent.com/dchest/tweetnacl-js/v0.14.5/LICENSE
1139 crunched_md5sums['75605e6bdd564791ab698fca65c94a4f'] = 'Unlicense'
1140 # https://raw.githubusercontent.com/stackgl/gl-mat3/v2.0.0/LICENSE.md
1141 crunched_md5sums['75512892d6f59dddb6d1c7e191957e9c'] = 'Zlib'
1142
Patrick Williams169d7bc2024-01-05 11:33:25 -06001143 commonlicdir = d.getVar('COMMON_LICENSE_DIR')
1144 for fn in sorted(os.listdir(commonlicdir)):
1145 md5value, lictext = crunch_license(os.path.join(commonlicdir, fn))
1146 if md5value not in crunched_md5sums:
1147 crunched_md5sums[md5value] = fn
1148 elif fn != crunched_md5sums[md5value]:
1149 bb.debug(2, "crunched_md5sums['%s'] is already set to '%s' rather than '%s'" % (md5value, crunched_md5sums[md5value], fn))
1150 else:
1151 bb.debug(2, "crunched_md5sums['%s'] is already set to '%s'" % (md5value, crunched_md5sums[md5value]))
1152
1153 return crunched_md5sums
1154
1155def crunch_license(licfile):
1156 '''
1157 Remove non-material text from a license file and then calculate its
1158 md5sum. This works well for licenses that contain a copyright statement,
1159 but is also a useful way to handle people's insistence upon reformatting
1160 the license text slightly (with no material difference to the text of the
1161 license).
1162 '''
1163
1164 import oe.utils
1165
1166 # Note: these are carefully constructed!
1167 license_title_re = re.compile(r'^#*\(? *(This is )?([Tt]he )?.{0,15} ?[Ll]icen[sc]e( \(.{1,10}\))?\)?[:\.]? ?#*$')
1168 license_statement_re = re.compile(r'^((This (project|software)|.{1,10}) is( free software)? (released|licen[sc]ed)|(Released|Licen[cs]ed)) under the .{1,10} [Ll]icen[sc]e:?$')
1169 copyright_re = re.compile('^ *[#\*]* *(Modified work |MIT LICENSED )?Copyright ?(\([cC]\))? .*$')
1170 disclaimer_re = re.compile('^ *\*? ?All [Rr]ights [Rr]eserved\.$')
1171 email_re = re.compile('^.*<[\w\.-]*@[\w\.\-]*>$')
1172 header_re = re.compile('^(\/\**!?)? ?[\-=\*]* ?(\*\/)?$')
1173 tag_re = re.compile('^ *@?\(?([Ll]icense|MIT)\)?$')
1174 url_re = re.compile('^ *[#\*]* *https?:\/\/[\w\.\/\-]+$')
1175
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001176 lictext = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001177 with open(licfile, 'r', errors='surrogateescape') as f:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001178 for line in f:
1179 # Drop opening statements
1180 if copyright_re.match(line):
1181 continue
Andrew Geisslereff27472021-10-29 15:35:00 -05001182 elif disclaimer_re.match(line):
1183 continue
1184 elif email_re.match(line):
1185 continue
1186 elif header_re.match(line):
1187 continue
1188 elif tag_re.match(line):
1189 continue
1190 elif url_re.match(line):
1191 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001192 elif license_title_re.match(line):
1193 continue
1194 elif license_statement_re.match(line):
1195 continue
Andrew Geisslereff27472021-10-29 15:35:00 -05001196 # Strip comment symbols
1197 line = line.replace('*', '') \
1198 .replace('#', '')
1199 # Unify spelling
1200 line = line.replace('sub-license', 'sublicense')
1201 # Squash spaces
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001202 line = oe.utils.squashspaces(line.strip())
Andrew Geisslereff27472021-10-29 15:35:00 -05001203 # Replace smart quotes, double quotes and backticks with single quotes
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001204 line = line.replace(u"\u2018", "'").replace(u"\u2019", "'").replace(u"\u201c","'").replace(u"\u201d", "'").replace('"', '\'').replace('`', '\'')
Andrew Geisslereff27472021-10-29 15:35:00 -05001205 # Unify brackets
1206 line = line.replace("{", "[").replace("}", "]")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001207 if line:
1208 lictext.append(line)
1209
1210 m = hashlib.md5()
1211 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001212 m.update(' '.join(lictext).encode('utf-8'))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001213 md5val = m.hexdigest()
1214 except UnicodeEncodeError:
1215 md5val = None
1216 lictext = ''
Patrick Williams169d7bc2024-01-05 11:33:25 -06001217 return md5val, lictext
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001218
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001219def guess_license(srctree, d):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001220 import bb
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001221 md5sums = get_license_md5sums(d)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001222
Patrick Williams169d7bc2024-01-05 11:33:25 -06001223 crunched_md5sums = crunch_known_licenses(d)
1224
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001225 licenses = []
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001226 licspecs = ['*LICEN[CS]E*', 'COPYING*', '*[Ll]icense*', 'LEGAL*', '[Ll]egal*', '*GPL*', 'README.lic*', 'COPYRIGHT*', '[Cc]opyright*', 'e[dp]l-v10']
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001227 skip_extensions = (".html", ".js", ".json", ".svg", ".ts", ".go")
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001228 licfiles = []
1229 for root, dirs, files in os.walk(srctree):
1230 for fn in files:
Andrew Geisslereff27472021-10-29 15:35:00 -05001231 if fn.endswith(skip_extensions):
1232 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001233 for spec in licspecs:
1234 if fnmatch.fnmatch(fn, spec):
1235 fullpath = os.path.join(root, fn)
1236 if not fullpath in licfiles:
1237 licfiles.append(fullpath)
Andrew Geissler595f6302022-01-24 19:11:47 +00001238 for licfile in sorted(licfiles):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001239 md5value = bb.utils.md5_file(licfile)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001240 license = md5sums.get(md5value, None)
1241 if not license:
Patrick Williams169d7bc2024-01-05 11:33:25 -06001242 crunched_md5, lictext = crunch_license(licfile)
1243 license = crunched_md5sums.get(crunched_md5, None)
Andrew Geisslereff27472021-10-29 15:35:00 -05001244 if lictext and not license:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001245 license = 'Unknown'
Andrew Geisslereff27472021-10-29 15:35:00 -05001246 logger.info("Please add the following line for '%s' to a 'lib/recipetool/licenses.csv' " \
1247 "and replace `Unknown` with the license:\n" \
1248 "%s,Unknown" % (os.path.relpath(licfile, srctree), md5value))
1249 if license:
1250 licenses.append((license, os.path.relpath(licfile, srctree), md5value))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001251
1252 # FIXME should we grab at least one source file with a license header and add that too?
1253
1254 return licenses
1255
Patrick Williams03907ee2022-05-01 06:28:52 -05001256def split_pkg_licenses(licvalues, packages, outlines, fallback_licenses=None, pn='${PN}'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001257 """
1258 Given a list of (license, path, md5sum) as returned by guess_license(),
1259 a dict of package name to path mappings, write out a set of
1260 package-specific LICENSE values.
1261 """
1262 pkglicenses = {pn: []}
1263 for license, licpath, _ in licvalues:
Andrew Geissler595f6302022-01-24 19:11:47 +00001264 license = fixup_license(license)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001265 for pkgname, pkgpath in packages.items():
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001266 if licpath.startswith(pkgpath + '/'):
1267 if pkgname in pkglicenses:
1268 pkglicenses[pkgname].append(license)
1269 else:
1270 pkglicenses[pkgname] = [license]
1271 break
1272 else:
1273 # Accumulate on the main package
1274 pkglicenses[pn].append(license)
1275 outlicenses = {}
1276 for pkgname in packages:
Andrew Geissler595f6302022-01-24 19:11:47 +00001277 # Assume AND operator between license files
1278 license = ' & '.join(list(set(pkglicenses.get(pkgname, ['Unknown'])))) or 'Unknown'
Patrick Williams03907ee2022-05-01 06:28:52 -05001279 if license == 'Unknown' and fallback_licenses and pkgname in fallback_licenses:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001280 license = fallback_licenses[pkgname]
Andrew Geissler595f6302022-01-24 19:11:47 +00001281 licenses = tidy_licenses(license)
1282 license = ' & '.join(licenses)
Patrick Williams213cb262021-08-07 19:21:33 -05001283 outlines.append('LICENSE:%s = "%s"' % (pkgname, license))
Andrew Geissler595f6302022-01-24 19:11:47 +00001284 outlicenses[pkgname] = licenses
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001285 return outlicenses
1286
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001287def read_pkgconfig_provides(d):
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001288 pkgdatadir = d.getVar('PKGDATA_DIR')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001289 pkgmap = {}
1290 for fn in glob.glob(os.path.join(pkgdatadir, 'shlibs2', '*.pclist')):
1291 with open(fn, 'r') as f:
1292 for line in f:
1293 pkgmap[os.path.basename(line.rstrip())] = os.path.splitext(os.path.basename(fn))[0]
1294 recipemap = {}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001295 for pc, pkg in pkgmap.items():
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001296 pkgdatafile = os.path.join(pkgdatadir, 'runtime', pkg)
1297 if os.path.exists(pkgdatafile):
1298 with open(pkgdatafile, 'r') as f:
1299 for line in f:
1300 if line.startswith('PN: '):
1301 recipemap[pc] = line.split(':', 1)[1].strip()
1302 return recipemap
1303
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001304def convert_debian(debpath):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001305 value_map = {'Package': 'PN',
1306 'Version': 'PV',
1307 'Section': 'SECTION',
1308 'License': 'LICENSE',
1309 'Homepage': 'HOMEPAGE'}
1310
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001311 # FIXME extend this mapping - perhaps use distro_alias.inc?
1312 depmap = {'libz-dev': 'zlib'}
1313
1314 values = {}
1315 depends = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001316 with open(os.path.join(debpath, 'control'), 'r', errors='surrogateescape') as f:
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001317 indesc = False
1318 for line in f:
1319 if indesc:
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001320 if line.startswith(' '):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001321 if line.startswith(' This package contains'):
1322 indesc = False
1323 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001324 if 'DESCRIPTION' in values:
1325 values['DESCRIPTION'] += ' ' + line.strip()
1326 else:
1327 values['DESCRIPTION'] = line.strip()
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001328 else:
1329 indesc = False
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001330 if not indesc:
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001331 splitline = line.split(':', 1)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001332 if len(splitline) < 2:
1333 continue
1334 key = splitline[0]
1335 value = splitline[1].strip()
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001336 if key == 'Build-Depends':
1337 for dep in value.split(','):
1338 dep = dep.split()[0]
1339 mapped = depmap.get(dep, '')
1340 if mapped:
1341 depends.append(mapped)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001342 elif key == 'Description':
1343 values['SUMMARY'] = value
1344 indesc = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001345 else:
1346 varname = value_map.get(key, None)
1347 if varname:
1348 values[varname] = value
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001349 postinst = os.path.join(debpath, 'postinst')
1350 postrm = os.path.join(debpath, 'postrm')
1351 preinst = os.path.join(debpath, 'preinst')
1352 prerm = os.path.join(debpath, 'prerm')
1353 sfiles = [postinst, postrm, preinst, prerm]
1354 for sfile in sfiles:
1355 if os.path.isfile(sfile):
1356 logger.info("Converting %s file to recipe function..." %
1357 os.path.basename(sfile).upper())
1358 content = []
1359 with open(sfile) as f:
1360 for line in f:
1361 if "#!/" in line:
1362 continue
1363 line = line.rstrip("\n")
1364 if line.strip():
1365 content.append(line)
1366 if content:
1367 values[os.path.basename(f.name)] = content
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001368
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001369 #if depends:
1370 # values['DEPENDS'] = ' '.join(depends)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001371
1372 return values
1373
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001374def convert_rpm_xml(xmlfile):
1375 '''Converts the output from rpm -qp --xml to a set of variable values'''
1376 import xml.etree.ElementTree as ElementTree
1377 rpmtag_map = {'Name': 'PN',
1378 'Version': 'PV',
1379 'Summary': 'SUMMARY',
1380 'Description': 'DESCRIPTION',
1381 'License': 'LICENSE',
1382 'Url': 'HOMEPAGE'}
1383
1384 values = {}
1385 tree = ElementTree.parse(xmlfile)
1386 root = tree.getroot()
1387 for child in root:
1388 if child.tag == 'rpmTag':
1389 name = child.attrib.get('name', None)
1390 if name:
1391 varname = rpmtag_map.get(name, None)
1392 if varname:
1393 values[varname] = child[0].text
1394 return values
1395
1396
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001397def register_commands(subparsers):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001398 parser_create = subparsers.add_parser('create',
1399 help='Create a new recipe',
1400 description='Creates a new recipe from a source tree')
1401 parser_create.add_argument('source', help='Path or URL to source')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001402 parser_create.add_argument('-o', '--outfile', help='Specify filename for recipe to create')
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001403 parser_create.add_argument('-p', '--provides', help='Specify an alias for the item provided by the recipe')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001404 parser_create.add_argument('-m', '--machine', help='Make recipe machine-specific as opposed to architecture-specific', action='store_true')
1405 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 -05001406 parser_create.add_argument('-N', '--name', help='Name to use within recipe (PN)')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001407 parser_create.add_argument('-V', '--version', help='Version to use within recipe (PV)')
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001408 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 -05001409 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')
1410 parser_create.add_argument('--src-subdir', help='Specify subdirectory within source tree to use', metavar='SUBDIR')
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001411 group = parser_create.add_mutually_exclusive_group()
1412 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")
1413 group.add_argument('-S', '--srcrev', help='Source revision to fetch if fetching from an SCM such as git (default latest)')
1414 parser_create.add_argument('-B', '--srcbranch', help='Branch in source repository if fetching from an SCM such as git (default master)')
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001415 parser_create.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
Andrew Geissler82c905d2020-04-13 13:39:40 -05001416 parser_create.add_argument('--npm-dev', action="store_true", help='For npm, also fetch devDependencies')
Patrick Williams169d7bc2024-01-05 11:33:25 -06001417 parser_create.add_argument('--no-pypi', action="store_true", help='Do not inherit pypi class')
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001418 parser_create.add_argument('--devtool', action="store_true", help=argparse.SUPPRESS)
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001419 parser_create.add_argument('--mirrors', action="store_true", help='Enable PREMIRRORS and MIRRORS for source tree fetching (disabled by default).')
1420 parser_create.set_defaults(func=create_recipe)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001421