create.py 43 KB

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