devtool / recipetool fixes for node.js modules
cover devtool / recipetool fixes for node.js modules
This fixes a few regressions in master in handling devtool add on node.js modules relative to morty.
The following changes since commit 210c518ba8f8d6ec6e9d34e0df8b963a3b2e0593:
ptest-runner: Upgrade to minor version 2.0.2 (2017-04-11 18:09:21 +0100)
are available in the git repository at:
git://git.openembedded.org/openembedded-core-contrib paule/npm-fixes3
http://cgit.openembedded.org/openembedded-core-contrib/log/?h=paule/npm-fixes3
Paul Eggleton (4):
recipetool: create: fix for regression in npm license handling
devtool: add: fix node.js/npm handling with recipe specific sysroots
devtool: add: prevent repeatedly running recipetool
recipetool: create: hide missing npm error when called from devtool
scripts/lib/devtool/__init__.py | 25 ++++++++++++++----------- scripts/lib/devtool/standard.py | 6 +++++- scripts/lib/recipetool/create.py | 32 ++++++++++++++++++++++++++------ scripts/lib/recipetool/create_npm.py | 23 +++++++++++++---------- 4 files changed, 58 insertions(+), 28 deletions(-)
Signature · 1 line
2.9.3
PATCH 1/4 recipetool: create: fix for regression in npm license handling
OE-Core commit c0cfd9b1d54b05ad048f444d6fe248aa0500159e added handling for AND / OR in license strings coming from npm, but made the assumption that an & would always be present in the license value. Check if it's there first so we don't fail if it isn't.
--- scripts/lib/recipetool/create_npm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-)
scripts/lib/recipetool/create_npm.py +2 -1
@@ -240,7 +240,8 @@ class NpmRecipeHandler(RecipeHandler): packages['${PN}'] = '' pkglicenses = split_pkg_licenses(licvalues, packages, lines_after, licenses) all_licenses = list(set([item.replace('_', ' ') for pkglicense in pkglicenses.values() for item in pkglicense])) - all_licenses.remove('&') + if '&' in all_licenses: + all_licenses.remove('&') # Go back and update the LICENSE value since we have a bit more # information than when that was written out (and we know all apply # vs. there being a choice, so we can join them with &)
Signature · 1 line
2.9.3
PATCH 2/4 devtool: add: fix node.js/npm handling with recipe specific sysroots
In reply to Paul EggletonThe change over to recipe specific sysroots means that we can no longer get a known location simply from configuration for the npm binary - we need to get the recipe sysroot for nodejs-native, look there for npm if we need to check it's present, and add that to PATH when calling out to npm. Unfortunately this means anywhere we need to get that path we have to have parsed all recipes, otherwise we have no reliable way of resolving nodejs-native. Thus we have to change recipetool create to always parse all recipes (the structure of the code does not allow us to do this conditionally).
In the worst case, if npm hasn't already been added to its own sysroot and we are fetching from a source repository rather than an npm registry, this gets a bit ugly because we end up parsing recipes three times:
1) recipetool startup, which then fetches the code and determines it's a node.js module, finds that npm isn't available and then exits with a specific error to tell devtool it needs to build npm
2) when we invoke bitbake -c addto_recipe_sysroot nodejs-native
3) when we re-invoke recipetool
This code is badly in need of refactoring, but now is unfortunately not the time to do that, so we're going to have to live with this ugliness for now.
Fixes [YOCTO #10992].
--- scripts/lib/devtool/__init__.py | 25 ++++++++++++++----------- scripts/lib/devtool/standard.py | 2 +- scripts/lib/recipetool/create.py | 28 ++++++++++++++++++++++------ scripts/lib/recipetool/create_npm.py | 20 +++++++++++--------- 4 files changed, 48 insertions(+), 27 deletions(-)
scripts/lib/devtool/__init__.py +14 -11
@@ -261,23 +261,28 @@ def get_bbclassextend_targets(recipefile, pn): targets.append('%s-%s' % (pn, variant)) return targets -def ensure_npm(config, basepath, fixed_setup=False): +def ensure_npm(config, basepath, fixed_setup=False, check_exists=True): """ Ensure that npm is available and either build it or show a reasonable error message """ - tinfoil = setup_tinfoil(config_only=True, basepath=basepath) - try: - nativepath = tinfoil.config_data.getVar('STAGING_BINDIR_NATIVE') - finally: - tinfoil.shutdown() + if check_exists: + tinfoil = setup_tinfoil(config_only=False, basepath=basepath) + try: + rd = tinfoil.parse_recipe('nodejs-native') + nativepath = rd.getVar('STAGING_BINDIR_NATIVE') + finally: + tinfoil.shutdown() + npmpath = os.path.join(nativepath, 'npm') + build_npm = not os.path.exists(npmpath) + else: + build_npm = True - npmpath = os.path.join(nativepath, 'npm') - if not os.path.exists(npmpath): + if build_npm: logger.info('Building nodejs-native') try: exec_build_env_command(config.init_path, basepath, - 'bitbake -q nodejs-native', watch=True) + 'bitbake -q nodejs-native -c addto_recipe_sysroot', watch=True) except bb.process.ExecutionError as e: if "Nothing PROVIDES 'nodejs-native'" in e.stdout: if fixed_setup:
@@ -287,5 +292,3 @@ def ensure_npm(config, basepath, fixed_setup=False): raise DevtoolError(msg) else: raise - if not os.path.exists(npmpath): - raise DevtoolError('Built nodejs-native but npm binary still could not be found at %s' % npmpath)
scripts/lib/devtool/standard.py +1 -1
@@ -165,7 +165,7 @@ def add(args, config, basepath, workspace): # inside recipetool since recipetool keeps tinfoil active # with references to it throughout the code, so we have # to exit out and come back here to do it. - ensure_npm(config, basepath, args.fixed_setup) + ensure_npm(config, basepath, args.fixed_setup, check_exists=False) logger.info('Re-running recipe creation process after building nodejs') continue elif e.exitcode == 15:
scripts/lib/recipetool/create.py +22 -6
@@ -416,12 +416,14 @@ def create_recipe(args): srcuri = rev_re.sub('', srcuri) tempsrc = tempfile.mkdtemp(prefix='recipetool-') srctree = tempsrc + d = bb.data.createCopy(tinfoil.config_data) if fetchuri.startswith('npm://'): # Check if npm is available - check_npm(tinfoil.config_data, args.devtool) + npm_bindir = check_npm(tinfoil, args.devtool) + d.prependVar('PATH', '%s:' % npm_bindir) logger.info('Fetching %s...' % srcuri) try: - checksums = scriptutils.fetch_uri(tinfoil.config_data, fetchuri, srctree, srcrev) + checksums = scriptutils.fetch_uri(d, fetchuri, srctree, srcrev) except bb.fetch2.BBFetchException as e: logger.error(str(e).rstrip()) sys.exit(1)
@@ -1119,10 +1121,21 @@ def convert_rpm_xml(xmlfile): return values -def check_npm(d, debugonly=False): - if not os.path.exists(os.path.join(d.getVar('STAGING_BINDIR_NATIVE'), 'npm')): - log_error_cond('npm required to process specified source, but npm is not available - you need to build nodejs-native first', debugonly) +def check_npm(tinfoil, debugonly=False): + try: + rd = tinfoil.parse_recipe('nodejs-native') + except bb.providers.NoProvider: + # We still conditionally show the message and exit with the special + # return code, otherwise we can't show the proper message for eSDK + # users + log_error_cond('nodejs-native is required for npm but is not available - you will likely need to add a layer that provides nodejs', debugonly) + sys.exit(14) + bindir = rd.getVar('STAGING_BINDIR_NATIVE') + npmpath = os.path.join(bindir, 'npm') + if not os.path.exists(npmpath): + log_error_cond('npm required to process specified source, but npm is not available - you need to run bitbake -c addto_recipe_sysroot nodejs-native first', debugonly) sys.exit(14) + return bindir def register_commands(subparsers): parser_create = subparsers.add_parser('create',
@@ -1141,5 +1154,8 @@ def register_commands(subparsers): parser_create.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)') parser_create.add_argument('--fetch-dev', action="store_true", help='For npm, also fetch devDependencies') parser_create.add_argument('--devtool', action="store_true", help=argparse.SUPPRESS) - parser_create.set_defaults(func=create_recipe) + # FIXME I really hate having to set parserecipes for this, but given we may need + # to call into npm (and we don't know in advance if we will or not) and in order + # to do so we need to know npm's recipe sysroot path, there's not much alternative + parser_create.set_defaults(func=create_recipe, parserecipes=True)
scripts/lib/recipetool/create_npm.py +11 -9
@@ -65,9 +65,9 @@ class NpmRecipeHandler(RecipeHandler): 'SEE-LICENSE-IN-EULA') return license - def _shrinkwrap(self, srctree, localfilesdir, extravalues, lines_before): + def _shrinkwrap(self, srctree, localfilesdir, extravalues, lines_before, d): try: - runenv = dict(os.environ, PATH=tinfoil.config_data.getVar('PATH')) + runenv = dict(os.environ, PATH=d.getVar('PATH')) bb.process.run('npm shrinkwrap', cwd=srctree, stderr=subprocess.STDOUT, env=runenv, shell=True) except bb.process.ExecutionError as e: logger.warn('npm shrinkwrap failed:\n%s' % e.stdout)
@@ -79,8 +79,8 @@ class NpmRecipeHandler(RecipeHandler): extravalues['extrafiles']['npm-shrinkwrap.json'] = tmpfile lines_before.append('NPM_SHRINKWRAP := "${THISDIR}/${PN}/npm-shrinkwrap.json"') - def _lockdown(self, srctree, localfilesdir, extravalues, lines_before): - runenv = dict(os.environ, PATH=tinfoil.config_data.getVar('PATH')) + def _lockdown(self, srctree, localfilesdir, extravalues, lines_before, d): + runenv = dict(os.environ, PATH=d.getVar('PATH')) if not NpmRecipeHandler.lockdownpath: NpmRecipeHandler.lockdownpath = tempfile.mkdtemp('recipetool-npm-lockdown') bb.process.run('npm install lockdown --prefix %s' % NpmRecipeHandler.lockdownpath,
@@ -188,7 +188,9 @@ class NpmRecipeHandler(RecipeHandler): files = RecipeHandler.checkfiles(srctree, ['package.json']) if files: - check_npm(tinfoil.config_data) + d = bb.data.createCopy(tinfoil.config_data) + npm_bindir = check_npm(tinfoil) + d.prependVar('PATH', '%s:' % npm_bindir) data = read_package_json(files[0]) if 'name' in data and 'version' in data:
@@ -203,17 +205,17 @@ class NpmRecipeHandler(RecipeHandler): fetchdev = extravalues['fetchdev'] or None deps, optdeps, devdeps = self.get_npm_package_dependencies(data, fetchdev) - updated = self._handle_dependencies(tinfoil.config_data, deps, optdeps, devdeps, lines_before, srctree) + updated = self._handle_dependencies(d, deps, optdeps, devdeps, lines_before, srctree) if updated: # We need to redo the license stuff - self._replace_license_vars(srctree, lines_before, handled, extravalues, tinfoil.config_data) + self._replace_license_vars(srctree, lines_before, handled, extravalues, d) # Shrinkwrap localfilesdir = tempfile.mkdtemp(prefix='recipetool-npm') - self._shrinkwrap(srctree, localfilesdir, extravalues, lines_before) + self._shrinkwrap(srctree, localfilesdir, extravalues, lines_before, d) # Lockdown - self._lockdown(srctree, localfilesdir, extravalues, lines_before) + self._lockdown(srctree, localfilesdir, extravalues, lines_before, d) # Split each npm module out to is own package npmpackages = oe.package.npm_split_package_dirs(srctree)
Signature · 1 line
2.9.3
PATCH 3/4 devtool: add: prevent repeatedly running recipetool
In reply to Paul EggletonIf recipetool returns with exit code 14 this means devtool needs to build nodejs-native and then call it again. If recipetool returns exit code 14 again then clearly something has gone wrong and we should just quit with an error.
--- scripts/lib/devtool/standard.py | 4 ++++ 1 file changed, 4 insertions(+)
scripts/lib/devtool/standard.py +4 -0
@@ -155,11 +155,14 @@ def add(args, config, basepath, workspace): tempdir = tempfile.mkdtemp(prefix='devtool') try: + builtnpm = False while True: try: stdout, _ = exec_build_env_command(config.init_path, basepath, 'recipetool --color=%s create --devtool -o %s \'%s\' %s' % (color, tempdir, source, extracmdopts), watch=True) except bb.process.ExecutionError as e: if e.exitcode == 14: + if builtnpm: + raise DevtoolError('Re-running recipetool still failed to find npm') # FIXME this is a horrible hack that is unfortunately # necessary due to the fact that we can't run bitbake from # inside recipetool since recipetool keeps tinfoil active
@@ -167,6 +170,7 @@ def add(args, config, basepath, workspace): # to exit out and come back here to do it. ensure_npm(config, basepath, args.fixed_setup, check_exists=False) logger.info('Re-running recipe creation process after building nodejs') + builtnpm = True continue elif e.exitcode == 15: raise DevtoolError('Could not auto-determine recipe name, please specify it on the command line')
Signature · 1 line
2.9.3
PATCH 4/4 recipetool: create: hide missing npm error when called from devtool
In reply to Paul EggletonIf devtool is called with a URL to a source repository containing a node.js module, we don't know that until recipetool has fetched it, and due to the structure of the code we have to exit with a special code in order to let devtool know it needs to build nodejs-native. We also want to suppress the error message that recipetool would normally print under these circumstances; there is already a mechanism for this but it wasn't operative in the case where we're pointed to a source repository rather than an npm:// URL, so create some plumbing so that we know to hide the message.
--- scripts/lib/recipetool/create.py | 4 ++++ scripts/lib/recipetool/create_npm.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-)
scripts/lib/recipetool/create.py +4 -0
@@ -59,6 +59,9 @@ class RecipeHandler(object): recipecmakefilemap = {} recipebinmap = {} + def __init__(self): + self._devtool = False + @staticmethod def load_libmap(d): '''Load library->recipe mapping'''
@@ -622,6 +625,7 @@ def create_recipe(args): handlers.sort(key=lambda item: (item[1], -item[2]), reverse=True) for handler, priority, _ in handlers: logger.debug('Handler: %s (priority %d)' % (handler.__class__.__name__, priority)) + setattr(handler, '_devtool', args.devtool) handlers = [item[0] for item in handlers] # Apply the handlers
scripts/lib/recipetool/create_npm.py +1 -1
@@ -189,7 +189,7 @@ class NpmRecipeHandler(RecipeHandler): files = RecipeHandler.checkfiles(srctree, ['package.json']) if files: d = bb.data.createCopy(tinfoil.config_data) - npm_bindir = check_npm(tinfoil) + npm_bindir = check_npm(tinfoil, self._devtool) d.prependVar('PATH', '%s:' % npm_bindir) data = read_package_json(files[0])
Signature · 1 line
2.9.3