| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 1 | # Recipe creation tool - node.js NPM module support plugin | 
|  | 2 | # | 
|  | 3 | # Copyright (C) 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 |  | 
|  | 18 | import os | 
|  | 19 | import logging | 
|  | 20 | import subprocess | 
|  | 21 | import tempfile | 
|  | 22 | import shutil | 
|  | 23 | import json | 
|  | 24 | from recipetool.create import RecipeHandler, split_pkg_licenses | 
|  | 25 |  | 
|  | 26 | logger = logging.getLogger('recipetool') | 
|  | 27 |  | 
|  | 28 |  | 
|  | 29 | tinfoil = None | 
|  | 30 |  | 
|  | 31 | def tinfoil_init(instance): | 
|  | 32 | global tinfoil | 
|  | 33 | tinfoil = instance | 
|  | 34 |  | 
|  | 35 |  | 
|  | 36 | class NpmRecipeHandler(RecipeHandler): | 
|  | 37 | lockdownpath = None | 
|  | 38 |  | 
|  | 39 | def _handle_license(self, data): | 
|  | 40 | ''' | 
|  | 41 | Handle the license value from an npm package.json file | 
|  | 42 | ''' | 
|  | 43 | license = None | 
|  | 44 | if 'license' in data: | 
|  | 45 | license = data['license'] | 
|  | 46 | if isinstance(license, dict): | 
|  | 47 | license = license.get('type', None) | 
|  | 48 | return None | 
|  | 49 |  | 
|  | 50 | def _shrinkwrap(self, srctree, localfilesdir, extravalues, lines_before): | 
|  | 51 | try: | 
|  | 52 | runenv = dict(os.environ, PATH=tinfoil.config_data.getVar('PATH', True)) | 
|  | 53 | bb.process.run('npm shrinkwrap', cwd=srctree, stderr=subprocess.STDOUT, env=runenv, shell=True) | 
|  | 54 | except bb.process.ExecutionError as e: | 
|  | 55 | logger.warn('npm shrinkwrap failed:\n%s' % e.stdout) | 
|  | 56 | return | 
|  | 57 |  | 
|  | 58 | tmpfile = os.path.join(localfilesdir, 'npm-shrinkwrap.json') | 
|  | 59 | shutil.move(os.path.join(srctree, 'npm-shrinkwrap.json'), tmpfile) | 
|  | 60 | extravalues.setdefault('extrafiles', {}) | 
|  | 61 | extravalues['extrafiles']['npm-shrinkwrap.json'] = tmpfile | 
|  | 62 | lines_before.append('NPM_SHRINKWRAP := "${THISDIR}/${PN}/npm-shrinkwrap.json"') | 
|  | 63 |  | 
|  | 64 | def _lockdown(self, srctree, localfilesdir, extravalues, lines_before): | 
|  | 65 | runenv = dict(os.environ, PATH=tinfoil.config_data.getVar('PATH', True)) | 
|  | 66 | if not NpmRecipeHandler.lockdownpath: | 
|  | 67 | NpmRecipeHandler.lockdownpath = tempfile.mkdtemp('recipetool-npm-lockdown') | 
|  | 68 | bb.process.run('npm install lockdown --prefix %s' % NpmRecipeHandler.lockdownpath, | 
|  | 69 | cwd=srctree, stderr=subprocess.STDOUT, env=runenv, shell=True) | 
|  | 70 | relockbin = os.path.join(NpmRecipeHandler.lockdownpath, 'node_modules', 'lockdown', 'relock.js') | 
|  | 71 | if not os.path.exists(relockbin): | 
|  | 72 | logger.warn('Could not find relock.js within lockdown directory; skipping lockdown') | 
|  | 73 | return | 
|  | 74 | try: | 
|  | 75 | bb.process.run('node %s' % relockbin, cwd=srctree, stderr=subprocess.STDOUT, env=runenv, shell=True) | 
|  | 76 | except bb.process.ExecutionError as e: | 
|  | 77 | logger.warn('lockdown-relock failed:\n%s' % e.stdout) | 
|  | 78 | return | 
|  | 79 |  | 
|  | 80 | tmpfile = os.path.join(localfilesdir, 'lockdown.json') | 
|  | 81 | shutil.move(os.path.join(srctree, 'lockdown.json'), tmpfile) | 
|  | 82 | extravalues.setdefault('extrafiles', {}) | 
|  | 83 | extravalues['extrafiles']['lockdown.json'] = tmpfile | 
|  | 84 | lines_before.append('NPM_LOCKDOWN := "${THISDIR}/${PN}/lockdown.json"') | 
|  | 85 |  | 
|  | 86 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): | 
|  | 87 | import bb.utils | 
|  | 88 | import oe | 
|  | 89 | from collections import OrderedDict | 
|  | 90 |  | 
|  | 91 | if 'buildsystem' in handled: | 
|  | 92 | return False | 
|  | 93 |  | 
|  | 94 | def read_package_json(fn): | 
|  | 95 | with open(fn, 'r') as f: | 
|  | 96 | return json.loads(f.read()) | 
|  | 97 |  | 
|  | 98 | files = RecipeHandler.checkfiles(srctree, ['package.json']) | 
|  | 99 | if files: | 
|  | 100 | data = read_package_json(files[0]) | 
|  | 101 | if 'name' in data and 'version' in data: | 
|  | 102 | extravalues['PN'] = data['name'] | 
|  | 103 | extravalues['PV'] = data['version'] | 
|  | 104 | classes.append('npm') | 
|  | 105 | handled.append('buildsystem') | 
|  | 106 | if 'description' in data: | 
|  | 107 | lines_before.append('SUMMARY = "%s"' % data['description']) | 
|  | 108 | if 'homepage' in data: | 
|  | 109 | lines_before.append('HOMEPAGE = "%s"' % data['homepage']) | 
|  | 110 |  | 
|  | 111 | # Shrinkwrap | 
|  | 112 | localfilesdir = tempfile.mkdtemp(prefix='recipetool-npm') | 
|  | 113 | self._shrinkwrap(srctree, localfilesdir, extravalues, lines_before) | 
|  | 114 |  | 
|  | 115 | # Lockdown | 
|  | 116 | self._lockdown(srctree, localfilesdir, extravalues, lines_before) | 
|  | 117 |  | 
|  | 118 | # Split each npm module out to is own package | 
|  | 119 | npmpackages = oe.package.npm_split_package_dirs(srctree) | 
|  | 120 | for item in handled: | 
|  | 121 | if isinstance(item, tuple): | 
|  | 122 | if item[0] == 'license': | 
|  | 123 | licvalues = item[1] | 
|  | 124 | break | 
|  | 125 | if licvalues: | 
|  | 126 | # Augment the license list with information we have in the packages | 
|  | 127 | licenses = {} | 
|  | 128 | license = self._handle_license(data) | 
|  | 129 | if license: | 
|  | 130 | licenses['${PN}'] = license | 
|  | 131 | for pkgname, pkgitem in npmpackages.iteritems(): | 
|  | 132 | _, pdata = pkgitem | 
|  | 133 | license = self._handle_license(pdata) | 
|  | 134 | if license: | 
|  | 135 | licenses[pkgname] = license | 
|  | 136 | # Now write out the package-specific license values | 
|  | 137 | # We need to strip out the json data dicts for this since split_pkg_licenses | 
|  | 138 | # isn't expecting it | 
|  | 139 | packages = OrderedDict((x,y[0]) for x,y in npmpackages.iteritems()) | 
|  | 140 | packages['${PN}'] = '' | 
|  | 141 | pkglicenses = split_pkg_licenses(licvalues, packages, lines_after, licenses) | 
|  | 142 | all_licenses = list(set([item for pkglicense in pkglicenses.values() for item in pkglicense])) | 
|  | 143 | # Go back and update the LICENSE value since we have a bit more | 
|  | 144 | # information than when that was written out (and we know all apply | 
|  | 145 | # vs. there being a choice, so we can join them with &) | 
|  | 146 | for i, line in enumerate(lines_before): | 
|  | 147 | if line.startswith('LICENSE = '): | 
|  | 148 | lines_before[i] = 'LICENSE = "%s"' % ' & '.join(all_licenses) | 
|  | 149 | break | 
|  | 150 |  | 
|  | 151 | return True | 
|  | 152 |  | 
|  | 153 | return False | 
|  | 154 |  | 
|  | 155 | def register_recipe_handlers(handlers): | 
|  | 156 | handlers.append((NpmRecipeHandler(), 60)) |