Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | # Recipe creation tool - create command plugin |
| 2 | # |
| 3 | # Copyright (C) 2014-2015 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 | |
| 18 | import sys |
| 19 | import os |
| 20 | import argparse |
| 21 | import glob |
| 22 | import fnmatch |
| 23 | import re |
| 24 | import logging |
| 25 | import scriptutils |
| 26 | |
| 27 | logger = logging.getLogger('recipetool') |
| 28 | |
| 29 | tinfoil = None |
| 30 | plugins = None |
| 31 | |
| 32 | def plugin_init(pluginlist): |
| 33 | # Take a reference to the list so we can use it later |
| 34 | global plugins |
| 35 | plugins = pluginlist |
| 36 | |
| 37 | def tinfoil_init(instance): |
| 38 | global tinfoil |
| 39 | tinfoil = instance |
| 40 | |
| 41 | class RecipeHandler(): |
| 42 | @staticmethod |
| 43 | def checkfiles(path, speclist): |
| 44 | results = [] |
| 45 | for spec in speclist: |
| 46 | results.extend(glob.glob(os.path.join(path, spec))) |
| 47 | return results |
| 48 | |
| 49 | def genfunction(self, outlines, funcname, content): |
| 50 | outlines.append('%s () {' % funcname) |
| 51 | for line in content: |
| 52 | outlines.append('\t%s' % line) |
| 53 | outlines.append('}') |
| 54 | outlines.append('') |
| 55 | |
| 56 | def process(self, srctree, classes, lines_before, lines_after, handled): |
| 57 | return False |
| 58 | |
| 59 | |
| 60 | |
| 61 | def supports_srcrev(uri): |
| 62 | localdata = bb.data.createCopy(tinfoil.config_data) |
| 63 | # This is a bit sad, but if you don't have this set there can be some |
| 64 | # odd interactions with the urldata cache which lead to errors |
| 65 | localdata.setVar('SRCREV', '${AUTOREV}') |
| 66 | bb.data.update_data(localdata) |
| 67 | fetcher = bb.fetch2.Fetch([uri], localdata) |
| 68 | urldata = fetcher.ud |
| 69 | for u in urldata: |
| 70 | if urldata[u].method.supports_srcrev(): |
| 71 | return True |
| 72 | return False |
| 73 | |
| 74 | def create_recipe(args): |
| 75 | import bb.process |
| 76 | import tempfile |
| 77 | import shutil |
| 78 | |
| 79 | pkgarch = "" |
| 80 | if args.machine: |
| 81 | pkgarch = "${MACHINE_ARCH}" |
| 82 | |
| 83 | checksums = (None, None) |
| 84 | tempsrc = '' |
| 85 | srcsubdir = '' |
| 86 | srcrev = '${AUTOREV}' |
| 87 | if '://' in args.source: |
| 88 | # Fetch a URL |
| 89 | srcuri = args.source |
| 90 | rev_re = re.compile(';rev=([^;]+)') |
| 91 | res = rev_re.search(srcuri) |
| 92 | if res: |
| 93 | srcrev = res.group(1) |
| 94 | srcuri = rev_re.sub('', srcuri) |
| 95 | tempsrc = tempfile.mkdtemp(prefix='recipetool-') |
| 96 | srctree = tempsrc |
| 97 | logger.info('Fetching %s...' % srcuri) |
| 98 | checksums = scriptutils.fetch_uri(tinfoil.config_data, args.source, srctree, srcrev) |
| 99 | dirlist = os.listdir(srctree) |
| 100 | if 'git.indirectionsymlink' in dirlist: |
| 101 | dirlist.remove('git.indirectionsymlink') |
| 102 | if len(dirlist) == 1 and os.path.isdir(os.path.join(srctree, dirlist[0])): |
| 103 | # We unpacked a single directory, so we should use that |
| 104 | srcsubdir = dirlist[0] |
| 105 | srctree = os.path.join(srctree, srcsubdir) |
| 106 | else: |
| 107 | # Assume we're pointing to an existing source tree |
| 108 | if args.extract_to: |
| 109 | logger.error('--extract-to cannot be specified if source is a directory') |
| 110 | sys.exit(1) |
| 111 | if not os.path.isdir(args.source): |
| 112 | logger.error('Invalid source directory %s' % args.source) |
| 113 | sys.exit(1) |
| 114 | srcuri = '' |
| 115 | srctree = args.source |
| 116 | |
| 117 | outfile = args.outfile |
| 118 | if outfile and outfile != '-': |
| 119 | if os.path.exists(outfile): |
| 120 | logger.error('Output file %s already exists' % outfile) |
| 121 | sys.exit(1) |
| 122 | |
| 123 | lines_before = [] |
| 124 | lines_after = [] |
| 125 | |
| 126 | lines_before.append('# Recipe created by %s' % os.path.basename(sys.argv[0])) |
| 127 | lines_before.append('# This is the basis of a recipe and may need further editing in order to be fully functional.') |
| 128 | lines_before.append('# (Feel free to remove these comments when editing.)') |
| 129 | lines_before.append('#') |
| 130 | |
| 131 | licvalues = guess_license(srctree) |
| 132 | lic_files_chksum = [] |
| 133 | if licvalues: |
| 134 | licenses = [] |
| 135 | for licvalue in licvalues: |
| 136 | if not licvalue[0] in licenses: |
| 137 | licenses.append(licvalue[0]) |
| 138 | lic_files_chksum.append('file://%s;md5=%s' % (licvalue[1], licvalue[2])) |
| 139 | lines_before.append('# WARNING: the following LICENSE and LIC_FILES_CHKSUM values are best guesses - it is') |
| 140 | lines_before.append('# your responsibility to verify that the values are complete and correct.') |
| 141 | if len(licvalues) > 1: |
| 142 | lines_before.append('#') |
| 143 | lines_before.append('# NOTE: multiple licenses have been detected; if that is correct you should separate') |
| 144 | lines_before.append('# these in the LICENSE value using & if the multiple licenses all apply, or | if there') |
| 145 | lines_before.append('# is a choice between the multiple licenses. If in doubt, check the accompanying') |
| 146 | lines_before.append('# documentation to determine which situation is applicable.') |
| 147 | else: |
| 148 | lines_before.append('# Unable to find any files that looked like license statements. Check the accompanying') |
| 149 | lines_before.append('# documentation and source headers and set LICENSE and LIC_FILES_CHKSUM accordingly.') |
| 150 | lines_before.append('#') |
| 151 | lines_before.append('# NOTE: LICENSE is being set to "CLOSED" to allow you to at least start building - if') |
| 152 | lines_before.append('# this is not accurate with respect to the licensing of the software being built (it') |
| 153 | lines_before.append('# will not be in most cases) you must specify the correct value before using this') |
| 154 | lines_before.append('# recipe for anything other than initial testing/development!') |
| 155 | licenses = ['CLOSED'] |
| 156 | lines_before.append('LICENSE = "%s"' % ' '.join(licenses)) |
| 157 | lines_before.append('LIC_FILES_CHKSUM = "%s"' % ' \\\n '.join(lic_files_chksum)) |
| 158 | lines_before.append('') |
| 159 | |
| 160 | # FIXME This is kind of a hack, we probably ought to be using bitbake to do this |
| 161 | # we'd also want a way to automatically set outfile based upon auto-detecting these values from the source if possible |
| 162 | recipefn = os.path.splitext(os.path.basename(outfile))[0] |
| 163 | fnsplit = recipefn.split('_') |
| 164 | if len(fnsplit) > 1: |
| 165 | pn = fnsplit[0] |
| 166 | pv = fnsplit[1] |
| 167 | else: |
| 168 | pn = recipefn |
| 169 | pv = None |
| 170 | |
| 171 | if args.version: |
| 172 | pv = args.version |
| 173 | |
| 174 | if pv and pv not in 'git svn hg'.split(): |
| 175 | realpv = pv |
| 176 | else: |
| 177 | realpv = None |
| 178 | |
| 179 | if srcuri: |
| 180 | if realpv: |
| 181 | srcuri = srcuri.replace(realpv, '${PV}') |
| 182 | else: |
| 183 | lines_before.append('# No information for SRC_URI yet (only an external source tree was specified)') |
| 184 | lines_before.append('SRC_URI = "%s"' % srcuri) |
| 185 | (md5value, sha256value) = checksums |
| 186 | if md5value: |
| 187 | lines_before.append('SRC_URI[md5sum] = "%s"' % md5value) |
| 188 | if sha256value: |
| 189 | lines_before.append('SRC_URI[sha256sum] = "%s"' % sha256value) |
| 190 | if srcuri and supports_srcrev(srcuri): |
| 191 | lines_before.append('') |
| 192 | lines_before.append('# Modify these as desired') |
| 193 | lines_before.append('PV = "%s+git${SRCPV}"' % (realpv or '1.0')) |
| 194 | lines_before.append('SRCREV = "%s"' % srcrev) |
| 195 | lines_before.append('') |
| 196 | |
| 197 | if srcsubdir and pv: |
| 198 | if srcsubdir == "%s-%s" % (pn, pv): |
| 199 | # This would be the default, so we don't need to set S in the recipe |
| 200 | srcsubdir = '' |
| 201 | if srcsubdir: |
| 202 | if pv and pv not in 'git svn hg'.split(): |
| 203 | srcsubdir = srcsubdir.replace(pv, '${PV}') |
| 204 | lines_before.append('S = "${WORKDIR}/%s"' % srcsubdir) |
| 205 | lines_before.append('') |
| 206 | |
| 207 | if pkgarch: |
| 208 | lines_after.append('PACKAGE_ARCH = "%s"' % pkgarch) |
| 209 | lines_after.append('') |
| 210 | |
| 211 | # Find all plugins that want to register handlers |
| 212 | handlers = [] |
| 213 | for plugin in plugins: |
| 214 | if hasattr(plugin, 'register_recipe_handlers'): |
| 215 | plugin.register_recipe_handlers(handlers) |
| 216 | |
| 217 | # Apply the handlers |
| 218 | classes = [] |
| 219 | handled = [] |
| 220 | for handler in handlers: |
| 221 | handler.process(srctree, classes, lines_before, lines_after, handled) |
| 222 | |
| 223 | outlines = [] |
| 224 | outlines.extend(lines_before) |
| 225 | if classes: |
| 226 | outlines.append('inherit %s' % ' '.join(classes)) |
| 227 | outlines.append('') |
| 228 | outlines.extend(lines_after) |
| 229 | |
| 230 | if args.extract_to: |
| 231 | scriptutils.git_convert_standalone_clone(srctree) |
| 232 | shutil.move(srctree, args.extract_to) |
| 233 | logger.info('Source extracted to %s' % args.extract_to) |
| 234 | |
| 235 | if outfile == '-': |
| 236 | sys.stdout.write('\n'.join(outlines) + '\n') |
| 237 | else: |
| 238 | with open(outfile, 'w') as f: |
| 239 | f.write('\n'.join(outlines) + '\n') |
| 240 | logger.info('Recipe %s has been created; further editing may be required to make it fully functional' % outfile) |
| 241 | |
| 242 | if tempsrc: |
| 243 | shutil.rmtree(tempsrc) |
| 244 | |
| 245 | return 0 |
| 246 | |
| 247 | def get_license_md5sums(d, static_only=False): |
| 248 | import bb.utils |
| 249 | md5sums = {} |
| 250 | if not static_only: |
| 251 | # Gather md5sums of license files in common license dir |
| 252 | commonlicdir = d.getVar('COMMON_LICENSE_DIR', True) |
| 253 | for fn in os.listdir(commonlicdir): |
| 254 | md5value = bb.utils.md5_file(os.path.join(commonlicdir, fn)) |
| 255 | md5sums[md5value] = fn |
| 256 | # The following were extracted from common values in various recipes |
| 257 | # (double checking the license against the license file itself, not just |
| 258 | # the LICENSE value in the recipe) |
| 259 | md5sums['94d55d512a9ba36caa9b7df079bae19f'] = 'GPLv2' |
| 260 | md5sums['b234ee4d69f5fce4486a80fdaf4a4263'] = 'GPLv2' |
| 261 | md5sums['59530bdf33659b29e73d4adb9f9f6552'] = 'GPLv2' |
| 262 | md5sums['0636e73ff0215e8d672dc4c32c317bb3'] = 'GPLv2' |
| 263 | md5sums['eb723b61539feef013de476e68b5c50a'] = 'GPLv2' |
| 264 | md5sums['751419260aa954499f7abaabaa882bbe'] = 'GPLv2' |
| 265 | md5sums['393a5ca445f6965873eca0259a17f833'] = 'GPLv2' |
| 266 | md5sums['12f884d2ae1ff87c09e5b7ccc2c4ca7e'] = 'GPLv2' |
| 267 | md5sums['8ca43cbc842c2336e835926c2166c28b'] = 'GPLv2' |
| 268 | md5sums['ebb5c50ab7cab4baeffba14977030c07'] = 'GPLv2' |
| 269 | md5sums['c93c0550bd3173f4504b2cbd8991e50b'] = 'GPLv2' |
| 270 | md5sums['9ac2e7cff1ddaf48b6eab6028f23ef88'] = 'GPLv2' |
| 271 | md5sums['4325afd396febcb659c36b49533135d4'] = 'GPLv2' |
| 272 | md5sums['18810669f13b87348459e611d31ab760'] = 'GPLv2' |
| 273 | md5sums['d7810fab7487fb0aad327b76f1be7cd7'] = 'GPLv2' # the Linux kernel's COPYING file |
| 274 | md5sums['bbb461211a33b134d42ed5ee802b37ff'] = 'LGPLv2.1' |
| 275 | md5sums['7fbc338309ac38fefcd64b04bb903e34'] = 'LGPLv2.1' |
| 276 | md5sums['4fbd65380cdd255951079008b364516c'] = 'LGPLv2.1' |
| 277 | md5sums['2d5025d4aa3495befef8f17206a5b0a1'] = 'LGPLv2.1' |
| 278 | md5sums['fbc093901857fcd118f065f900982c24'] = 'LGPLv2.1' |
| 279 | md5sums['a6f89e2100d9b6cdffcea4f398e37343'] = 'LGPLv2.1' |
| 280 | md5sums['d8045f3b8f929c1cb29a1e3fd737b499'] = 'LGPLv2.1' |
| 281 | md5sums['fad9b3332be894bab9bc501572864b29'] = 'LGPLv2.1' |
| 282 | md5sums['3bf50002aefd002f49e7bb854063f7e7'] = 'LGPLv2' |
| 283 | md5sums['9f604d8a4f8e74f4f5140845a21b6674'] = 'LGPLv2' |
| 284 | md5sums['5f30f0716dfdd0d91eb439ebec522ec2'] = 'LGPLv2' |
| 285 | md5sums['55ca817ccb7d5b5b66355690e9abc605'] = 'LGPLv2' |
| 286 | md5sums['252890d9eee26aab7b432e8b8a616475'] = 'LGPLv2' |
| 287 | md5sums['d32239bcb673463ab874e80d47fae504'] = 'GPLv3' |
| 288 | md5sums['f27defe1e96c2e1ecd4e0c9be8967949'] = 'GPLv3' |
| 289 | md5sums['6a6a8e020838b23406c81b19c1d46df6'] = 'LGPLv3' |
| 290 | md5sums['3b83ef96387f14655fc854ddc3c6bd57'] = 'Apache-2.0' |
| 291 | md5sums['385c55653886acac3821999a3ccd17b3'] = 'Artistic-1.0 | GPL-2.0' # some perl modules |
| 292 | return md5sums |
| 293 | |
| 294 | def guess_license(srctree): |
| 295 | import bb |
| 296 | md5sums = get_license_md5sums(tinfoil.config_data) |
| 297 | |
| 298 | licenses = [] |
| 299 | licspecs = ['LICENSE*', 'COPYING*', '*[Ll]icense*', 'LICENCE*', 'LEGAL*', '[Ll]egal*', '*GPL*', 'README.lic*', 'COPYRIGHT*', '[Cc]opyright*'] |
| 300 | licfiles = [] |
| 301 | for root, dirs, files in os.walk(srctree): |
| 302 | for fn in files: |
| 303 | for spec in licspecs: |
| 304 | if fnmatch.fnmatch(fn, spec): |
| 305 | fullpath = os.path.join(root, fn) |
| 306 | if not fullpath in licfiles: |
| 307 | licfiles.append(fullpath) |
| 308 | for licfile in licfiles: |
| 309 | md5value = bb.utils.md5_file(licfile) |
| 310 | license = md5sums.get(md5value, 'Unknown') |
| 311 | licenses.append((license, os.path.relpath(licfile, srctree), md5value)) |
| 312 | |
| 313 | # FIXME should we grab at least one source file with a license header and add that too? |
| 314 | |
| 315 | return licenses |
| 316 | |
| 317 | def read_pkgconfig_provides(d): |
| 318 | pkgdatadir = d.getVar('PKGDATA_DIR', True) |
| 319 | pkgmap = {} |
| 320 | for fn in glob.glob(os.path.join(pkgdatadir, 'shlibs2', '*.pclist')): |
| 321 | with open(fn, 'r') as f: |
| 322 | for line in f: |
| 323 | pkgmap[os.path.basename(line.rstrip())] = os.path.splitext(os.path.basename(fn))[0] |
| 324 | recipemap = {} |
| 325 | for pc, pkg in pkgmap.iteritems(): |
| 326 | pkgdatafile = os.path.join(pkgdatadir, 'runtime', pkg) |
| 327 | if os.path.exists(pkgdatafile): |
| 328 | with open(pkgdatafile, 'r') as f: |
| 329 | for line in f: |
| 330 | if line.startswith('PN: '): |
| 331 | recipemap[pc] = line.split(':', 1)[1].strip() |
| 332 | return recipemap |
| 333 | |
| 334 | def convert_pkginfo(pkginfofile): |
| 335 | values = {} |
| 336 | with open(pkginfofile, 'r') as f: |
| 337 | indesc = False |
| 338 | for line in f: |
| 339 | if indesc: |
| 340 | if line.strip(): |
| 341 | values['DESCRIPTION'] += ' ' + line.strip() |
| 342 | else: |
| 343 | indesc = False |
| 344 | else: |
| 345 | splitline = line.split(': ', 1) |
| 346 | key = line[0] |
| 347 | value = line[1] |
| 348 | if key == 'LICENSE': |
| 349 | for dep in value.split(','): |
| 350 | dep = dep.split()[0] |
| 351 | mapped = depmap.get(dep, '') |
| 352 | if mapped: |
| 353 | depends.append(mapped) |
| 354 | elif key == 'License': |
| 355 | values['LICENSE'] = value |
| 356 | elif key == 'Summary': |
| 357 | values['SUMMARY'] = value |
| 358 | elif key == 'Description': |
| 359 | values['DESCRIPTION'] = value |
| 360 | indesc = True |
| 361 | return values |
| 362 | |
| 363 | def convert_debian(debpath): |
| 364 | # FIXME extend this mapping - perhaps use distro_alias.inc? |
| 365 | depmap = {'libz-dev': 'zlib'} |
| 366 | |
| 367 | values = {} |
| 368 | depends = [] |
| 369 | with open(os.path.join(debpath, 'control')) as f: |
| 370 | indesc = False |
| 371 | for line in f: |
| 372 | if indesc: |
| 373 | if line.strip(): |
| 374 | if line.startswith(' This package contains'): |
| 375 | indesc = False |
| 376 | else: |
| 377 | values['DESCRIPTION'] += ' ' + line.strip() |
| 378 | else: |
| 379 | indesc = False |
| 380 | else: |
| 381 | splitline = line.split(':', 1) |
| 382 | key = line[0] |
| 383 | value = line[1] |
| 384 | if key == 'Build-Depends': |
| 385 | for dep in value.split(','): |
| 386 | dep = dep.split()[0] |
| 387 | mapped = depmap.get(dep, '') |
| 388 | if mapped: |
| 389 | depends.append(mapped) |
| 390 | elif key == 'Section': |
| 391 | values['SECTION'] = value |
| 392 | elif key == 'Description': |
| 393 | values['SUMMARY'] = value |
| 394 | indesc = True |
| 395 | |
| 396 | if depends: |
| 397 | values['DEPENDS'] = ' '.join(depends) |
| 398 | |
| 399 | return values |
| 400 | |
| 401 | |
| 402 | def register_command(subparsers): |
| 403 | parser_create = subparsers.add_parser('create', |
| 404 | help='Create a new recipe', |
| 405 | description='Creates a new recipe from a source tree') |
| 406 | parser_create.add_argument('source', help='Path or URL to source') |
| 407 | parser_create.add_argument('-o', '--outfile', help='Specify filename for recipe to create', required=True) |
| 408 | parser_create.add_argument('-m', '--machine', help='Make recipe machine-specific as opposed to architecture-specific', action='store_true') |
| 409 | 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') |
| 410 | parser_create.add_argument('-V', '--version', help='Version to use within recipe (PV)') |
| 411 | parser_create.set_defaults(func=create_recipe) |
| 412 | |