|  | # Development tool - sdk-update command plugin | 
|  | # | 
|  | # Copyright (C) 2015-2016 Intel Corporation | 
|  | # | 
|  | # This program is free software; you can redistribute it and/or modify | 
|  | # it under the terms of the GNU General Public License version 2 as | 
|  | # published by the Free Software Foundation. | 
|  | # | 
|  | # This program is distributed in the hope that it will be useful, | 
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
|  | # GNU General Public License for more details. | 
|  | # | 
|  | # You should have received a copy of the GNU General Public License along | 
|  | # with this program; if not, write to the Free Software Foundation, Inc., | 
|  | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | 
|  |  | 
|  | import os | 
|  | import subprocess | 
|  | import logging | 
|  | import glob | 
|  | import shutil | 
|  | import errno | 
|  | import sys | 
|  | import tempfile | 
|  | import re | 
|  | from devtool import exec_build_env_command, setup_tinfoil, parse_recipe, DevtoolError | 
|  |  | 
|  | logger = logging.getLogger('devtool') | 
|  |  | 
|  | def parse_locked_sigs(sigfile_path): | 
|  | """Return <pn:task>:<hash> dictionary""" | 
|  | sig_dict = {} | 
|  | with open(sigfile_path) as f: | 
|  | lines = f.readlines() | 
|  | for line in lines: | 
|  | if ':' in line: | 
|  | taskkey, _, hashval = line.rpartition(':') | 
|  | sig_dict[taskkey.strip()] = hashval.split()[0] | 
|  | return sig_dict | 
|  |  | 
|  | def generate_update_dict(sigfile_new, sigfile_old): | 
|  | """Return a dict containing <pn:task>:<hash> which indicates what need to be updated""" | 
|  | update_dict = {} | 
|  | sigdict_new = parse_locked_sigs(sigfile_new) | 
|  | sigdict_old = parse_locked_sigs(sigfile_old) | 
|  | for k in sigdict_new: | 
|  | if k not in sigdict_old: | 
|  | update_dict[k] = sigdict_new[k] | 
|  | continue | 
|  | if sigdict_new[k] != sigdict_old[k]: | 
|  | update_dict[k] = sigdict_new[k] | 
|  | continue | 
|  | return update_dict | 
|  |  | 
|  | def get_sstate_objects(update_dict, sstate_dir): | 
|  | """Return a list containing sstate objects which are to be installed""" | 
|  | sstate_objects = [] | 
|  | for k in update_dict: | 
|  | files = set() | 
|  | hashval = update_dict[k] | 
|  | p = sstate_dir + '/' + hashval[:2] + '/*' + hashval + '*.tgz' | 
|  | files |= set(glob.glob(p)) | 
|  | p = sstate_dir + '/*/' + hashval[:2] + '/*' + hashval + '*.tgz' | 
|  | files |= set(glob.glob(p)) | 
|  | files = list(files) | 
|  | if len(files) == 1: | 
|  | sstate_objects.extend(files) | 
|  | elif len(files) > 1: | 
|  | logger.error("More than one matching sstate object found for %s" % hashval) | 
|  |  | 
|  | return sstate_objects | 
|  |  | 
|  | def mkdir(d): | 
|  | try: | 
|  | os.makedirs(d) | 
|  | except OSError as e: | 
|  | if e.errno != errno.EEXIST: | 
|  | raise e | 
|  |  | 
|  | def install_sstate_objects(sstate_objects, src_sdk, dest_sdk): | 
|  | """Install sstate objects into destination SDK""" | 
|  | sstate_dir = os.path.join(dest_sdk, 'sstate-cache') | 
|  | if not os.path.exists(sstate_dir): | 
|  | logger.error("Missing sstate-cache directory in %s, it might not be an extensible SDK." % dest_sdk) | 
|  | raise | 
|  | for sb in sstate_objects: | 
|  | dst = sb.replace(src_sdk, dest_sdk) | 
|  | destdir = os.path.dirname(dst) | 
|  | mkdir(destdir) | 
|  | logger.debug("Copying %s to %s" % (sb, dst)) | 
|  | shutil.copy(sb, dst) | 
|  |  | 
|  | def check_manifest(fn, basepath): | 
|  | import bb.utils | 
|  | changedfiles = [] | 
|  | with open(fn, 'r') as f: | 
|  | for line in f: | 
|  | splitline = line.split() | 
|  | if len(splitline) > 1: | 
|  | chksum = splitline[0] | 
|  | fpath = splitline[1] | 
|  | curr_chksum = bb.utils.sha256_file(os.path.join(basepath, fpath)) | 
|  | if chksum != curr_chksum: | 
|  | logger.debug('File %s changed: old csum = %s, new = %s' % (os.path.join(basepath, fpath), curr_chksum, chksum)) | 
|  | changedfiles.append(fpath) | 
|  | return changedfiles | 
|  |  | 
|  | def sdk_update(args, config, basepath, workspace): | 
|  | # Fetch locked-sigs.inc file from remote/local destination | 
|  | updateserver = args.updateserver | 
|  | if not updateserver: | 
|  | updateserver = config.get('SDK', 'updateserver', '') | 
|  | logger.debug("updateserver: %s" % updateserver) | 
|  |  | 
|  | # Make sure we are using sdk-update from within SDK | 
|  | logger.debug("basepath = %s" % basepath) | 
|  | old_locked_sig_file_path = os.path.join(basepath, 'conf/locked-sigs.inc') | 
|  | if not os.path.exists(old_locked_sig_file_path): | 
|  | logger.error("Not using devtool's sdk-update command from within an extensible SDK. Please specify correct basepath via --basepath option") | 
|  | return -1 | 
|  | else: | 
|  | logger.debug("Found conf/locked-sigs.inc in %s" % basepath) | 
|  |  | 
|  | if ':' in updateserver: | 
|  | is_remote = True | 
|  | else: | 
|  | is_remote = False | 
|  |  | 
|  | layers_dir = os.path.join(basepath, 'layers') | 
|  | conf_dir = os.path.join(basepath, 'conf') | 
|  |  | 
|  | # Grab variable values | 
|  | tinfoil = setup_tinfoil(config_only=True, basepath=basepath) | 
|  | try: | 
|  | stamps_dir = tinfoil.config_data.getVar('STAMPS_DIR', True) | 
|  | sstate_mirrors = tinfoil.config_data.getVar('SSTATE_MIRRORS', True) | 
|  | site_conf_version = tinfoil.config_data.getVar('SITE_CONF_VERSION', True) | 
|  | finally: | 
|  | tinfoil.shutdown() | 
|  |  | 
|  | if not is_remote: | 
|  | # devtool sdk-update /local/path/to/latest/sdk | 
|  | new_locked_sig_file_path = os.path.join(updateserver, 'conf/locked-sigs.inc') | 
|  | if not os.path.exists(new_locked_sig_file_path): | 
|  | logger.error("%s doesn't exist or is not an extensible SDK" % updateserver) | 
|  | return -1 | 
|  | else: | 
|  | logger.debug("Found conf/locked-sigs.inc in %s" % updateserver) | 
|  | update_dict = generate_update_dict(new_locked_sig_file_path, old_locked_sig_file_path) | 
|  | logger.debug("update_dict = %s" % update_dict) | 
|  | newsdk_path = updateserver | 
|  | sstate_dir = os.path.join(newsdk_path, 'sstate-cache') | 
|  | if not os.path.exists(sstate_dir): | 
|  | logger.error("sstate-cache directory not found under %s" % newsdk_path) | 
|  | return 1 | 
|  | sstate_objects = get_sstate_objects(update_dict, sstate_dir) | 
|  | logger.debug("sstate_objects = %s" % sstate_objects) | 
|  | if len(sstate_objects) == 0: | 
|  | logger.info("No need to update.") | 
|  | return 0 | 
|  | logger.info("Installing sstate objects into %s", basepath) | 
|  | install_sstate_objects(sstate_objects, updateserver.rstrip('/'), basepath) | 
|  | logger.info("Updating configuration files") | 
|  | new_conf_dir = os.path.join(updateserver, 'conf') | 
|  | shutil.rmtree(conf_dir) | 
|  | shutil.copytree(new_conf_dir, conf_dir) | 
|  | logger.info("Updating layers") | 
|  | new_layers_dir = os.path.join(updateserver, 'layers') | 
|  | shutil.rmtree(layers_dir) | 
|  | ret = subprocess.call("cp -a %s %s" % (new_layers_dir, layers_dir), shell=True) | 
|  | if ret != 0: | 
|  | logger.error("Copying %s to %s failed" % (new_layers_dir, layers_dir)) | 
|  | return ret | 
|  | else: | 
|  | # devtool sdk-update http://myhost/sdk | 
|  | tmpsdk_dir = tempfile.mkdtemp() | 
|  | try: | 
|  | os.makedirs(os.path.join(tmpsdk_dir, 'conf')) | 
|  | new_locked_sig_file_path = os.path.join(tmpsdk_dir, 'conf', 'locked-sigs.inc') | 
|  | # Fetch manifest from server | 
|  | tmpmanifest = os.path.join(tmpsdk_dir, 'conf', 'sdk-conf-manifest') | 
|  | ret = subprocess.call("wget -q -O %s %s/conf/sdk-conf-manifest" % (tmpmanifest, updateserver), shell=True) | 
|  | changedfiles = check_manifest(tmpmanifest, basepath) | 
|  | if not changedfiles: | 
|  | logger.info("Already up-to-date") | 
|  | return 0 | 
|  | # Update metadata | 
|  | logger.debug("Updating metadata via git ...") | 
|  | #Check for the status before doing a fetch and reset | 
|  | if os.path.exists(os.path.join(basepath, 'layers/.git')): | 
|  | out = subprocess.check_output("git status --porcelain", shell=True, cwd=layers_dir) | 
|  | if not out: | 
|  | ret = subprocess.call("git fetch --all; git reset --hard", shell=True, cwd=layers_dir) | 
|  | else: | 
|  | logger.error("Failed to update metadata as there have been changes made to it. Aborting."); | 
|  | logger.error("Changed files:\n%s" % out); | 
|  | return -1 | 
|  | else: | 
|  | ret = -1 | 
|  | if ret != 0: | 
|  | ret = subprocess.call("git clone %s/layers/.git" % updateserver, shell=True, cwd=tmpsdk_dir) | 
|  | if ret != 0: | 
|  | logger.error("Updating metadata via git failed") | 
|  | return ret | 
|  | logger.debug("Updating conf files ...") | 
|  | for changedfile in changedfiles: | 
|  | ret = subprocess.call("wget -q -O %s %s/%s" % (changedfile, updateserver, changedfile), shell=True, cwd=tmpsdk_dir) | 
|  | if ret != 0: | 
|  | logger.error("Updating %s failed" % changedfile) | 
|  | return ret | 
|  |  | 
|  | # Check if UNINATIVE_CHECKSUM changed | 
|  | uninative = False | 
|  | if 'conf/local.conf' in changedfiles: | 
|  | def read_uninative_checksums(fn): | 
|  | chksumitems = [] | 
|  | with open(fn, 'r') as f: | 
|  | for line in f: | 
|  | if line.startswith('UNINATIVE_CHECKSUM'): | 
|  | splitline = re.split(r'[\[\]"\']', line) | 
|  | if len(splitline) > 3: | 
|  | chksumitems.append((splitline[1], splitline[3])) | 
|  | return chksumitems | 
|  |  | 
|  | oldsums = read_uninative_checksums(os.path.join(basepath, 'conf/local.conf')) | 
|  | newsums = read_uninative_checksums(os.path.join(tmpsdk_dir, 'conf/local.conf')) | 
|  | if oldsums != newsums: | 
|  | uninative = True | 
|  | for buildarch, chksum in newsums: | 
|  | uninative_file = os.path.join('downloads', 'uninative', chksum, '%s-nativesdk-libc.tar.bz2' % buildarch) | 
|  | mkdir(os.path.join(tmpsdk_dir, os.path.dirname(uninative_file))) | 
|  | ret = subprocess.call("wget -q -O %s %s/%s" % (uninative_file, updateserver, uninative_file), shell=True, cwd=tmpsdk_dir) | 
|  |  | 
|  | # Ok, all is well at this point - move everything over | 
|  | tmplayers_dir = os.path.join(tmpsdk_dir, 'layers') | 
|  | if os.path.exists(tmplayers_dir): | 
|  | shutil.rmtree(layers_dir) | 
|  | shutil.move(tmplayers_dir, layers_dir) | 
|  | for changedfile in changedfiles: | 
|  | destfile = os.path.join(basepath, changedfile) | 
|  | os.remove(destfile) | 
|  | shutil.move(os.path.join(tmpsdk_dir, changedfile), destfile) | 
|  | os.remove(os.path.join(conf_dir, 'sdk-conf-manifest')) | 
|  | shutil.move(tmpmanifest, conf_dir) | 
|  | if uninative: | 
|  | shutil.rmtree(os.path.join(basepath, 'downloads', 'uninative')) | 
|  | shutil.move(os.path.join(tmpsdk_dir, 'downloads', 'uninative'), os.path.join(basepath, 'downloads')) | 
|  |  | 
|  | if not sstate_mirrors: | 
|  | with open(os.path.join(conf_dir, 'site.conf'), 'a') as f: | 
|  | f.write('SCONF_VERSION = "%s"\n' % site_conf_version) | 
|  | f.write('SSTATE_MIRRORS_append = " file://.* %s/sstate-cache/PATH \\n "\n' % updateserver) | 
|  | finally: | 
|  | shutil.rmtree(tmpsdk_dir) | 
|  |  | 
|  | if not args.skip_prepare: | 
|  | # Find all potentially updateable tasks | 
|  | sdk_update_targets = [] | 
|  | tasks = ['do_populate_sysroot', 'do_packagedata'] | 
|  | for root, _, files in os.walk(stamps_dir): | 
|  | for fn in files: | 
|  | if not '.sigdata.' in fn: | 
|  | for task in tasks: | 
|  | if '.%s.' % task in fn or '.%s_setscene.' % task in fn: | 
|  | sdk_update_targets.append('%s:%s' % (os.path.basename(root), task)) | 
|  | # Run bitbake command for the whole SDK | 
|  | logger.info("Preparing build system... (This may take some time.)") | 
|  | try: | 
|  | exec_build_env_command(config.init_path, basepath, 'bitbake --setscene-only %s' % ' '.join(sdk_update_targets), stderr=subprocess.STDOUT) | 
|  | output, _ = exec_build_env_command(config.init_path, basepath, 'bitbake -n %s' % ' '.join(sdk_update_targets), stderr=subprocess.STDOUT) | 
|  | runlines = [] | 
|  | for line in output.splitlines(): | 
|  | if 'Running task ' in line: | 
|  | runlines.append(line) | 
|  | if runlines: | 
|  | logger.error('Unexecuted tasks found in preparation log:\n  %s' % '\n  '.join(runlines)) | 
|  | return -1 | 
|  | except bb.process.ExecutionError as e: | 
|  | logger.error('Preparation failed:\n%s' % e.stdout) | 
|  | return -1 | 
|  | return 0 | 
|  |  | 
|  | def sdk_install(args, config, basepath, workspace): | 
|  | """Entry point for the devtool sdk-install command""" | 
|  |  | 
|  | import oe.recipeutils | 
|  | import bb.process | 
|  |  | 
|  | for recipe in args.recipename: | 
|  | if recipe in workspace: | 
|  | raise DevtoolError('recipe %s is a recipe in your workspace' % recipe) | 
|  |  | 
|  | tasks = ['do_populate_sysroot', 'do_packagedata'] | 
|  | stampprefixes = {} | 
|  | def checkstamp(recipe): | 
|  | stampprefix = stampprefixes[recipe] | 
|  | stamps = glob.glob(stampprefix + '*') | 
|  | for stamp in stamps: | 
|  | if '.sigdata.' not in stamp and stamp.startswith((stampprefix + '.', stampprefix + '_setscene.')): | 
|  | return True | 
|  | else: | 
|  | return False | 
|  |  | 
|  | install_recipes = [] | 
|  | tinfoil = setup_tinfoil(config_only=False, basepath=basepath) | 
|  | try: | 
|  | for recipe in args.recipename: | 
|  | rd = parse_recipe(config, tinfoil, recipe, True) | 
|  | if not rd: | 
|  | return 1 | 
|  | stampprefixes[recipe] = '%s.%s' % (rd.getVar('STAMP', True), tasks[0]) | 
|  | if checkstamp(recipe): | 
|  | logger.info('%s is already installed' % recipe) | 
|  | else: | 
|  | install_recipes.append(recipe) | 
|  | finally: | 
|  | tinfoil.shutdown() | 
|  |  | 
|  | if install_recipes: | 
|  | logger.info('Installing %s...' % ', '.join(install_recipes)) | 
|  | install_tasks = [] | 
|  | for recipe in install_recipes: | 
|  | for task in tasks: | 
|  | if recipe.endswith('-native') and 'package' in task: | 
|  | continue | 
|  | install_tasks.append('%s:%s' % (recipe, task)) | 
|  | options = '' | 
|  | if not args.allow_build: | 
|  | options += ' --setscene-only' | 
|  | try: | 
|  | exec_build_env_command(config.init_path, basepath, 'bitbake %s %s' % (options, ' '.join(install_tasks)), watch=True) | 
|  | except bb.process.ExecutionError as e: | 
|  | raise DevtoolError('Failed to install %s:\n%s' % (recipe, str(e))) | 
|  | failed = False | 
|  | for recipe in install_recipes: | 
|  | if checkstamp(recipe): | 
|  | logger.info('Successfully installed %s' % recipe) | 
|  | else: | 
|  | raise DevtoolError('Failed to install %s - unavailable' % recipe) | 
|  | failed = True | 
|  | if failed: | 
|  | return 2 | 
|  |  | 
|  | def register_commands(subparsers, context): | 
|  | """Register devtool subcommands from the sdk plugin""" | 
|  | if context.fixed_setup: | 
|  | parser_sdk = subparsers.add_parser('sdk-update', | 
|  | help='Update SDK components', | 
|  | description='Updates installed SDK components from a remote server', | 
|  | group='sdk') | 
|  | updateserver = context.config.get('SDK', 'updateserver', '') | 
|  | if updateserver: | 
|  | parser_sdk.add_argument('updateserver', help='The update server to fetch latest SDK components from (default %s)' % updateserver, nargs='?') | 
|  | else: | 
|  | parser_sdk.add_argument('updateserver', help='The update server to fetch latest SDK components from') | 
|  | parser_sdk.add_argument('--skip-prepare', action="store_true", help='Skip re-preparing the build system after updating (for debugging only)') | 
|  | parser_sdk.set_defaults(func=sdk_update) | 
|  |  | 
|  | parser_sdk_install = subparsers.add_parser('sdk-install', | 
|  | help='Install additional SDK components', | 
|  | description='Installs additional recipe development files into the SDK. (You can use "devtool search" to find available recipes.)', | 
|  | group='sdk') | 
|  | parser_sdk_install.add_argument('recipename', help='Name of the recipe to install the development artifacts for', nargs='+') | 
|  | parser_sdk_install.add_argument('-s', '--allow-build', help='Allow building requested item(s) from source', action='store_true') | 
|  | parser_sdk_install.set_defaults(func=sdk_install) |