| #!/usr/bin/env python3 | |
| # OpenEmbedded Development tool | |
| # | |
| # Copyright (C) 2014-2015 Intel Corporation | |
| # | |
| # SPDX-License-Identifier: GPL-2.0-only | |
| # | |
| import dataclasses | |
| import sys | |
| import os | |
| import argparse | |
| import glob | |
| import re | |
| import configparser | |
| import logging | |
| # This can be removed once our minimum is Python 3.9: https://docs.python.org/3/whatsnew/3.9.html#type-hinting-generics-in-standard-collections | |
| from typing import List | |
| scripts_path = os.path.dirname(os.path.realpath(__file__)) | |
| lib_path = scripts_path + '/lib' | |
| sys.path = sys.path + [lib_path] | |
| from devtool import DevtoolError, setup_tinfoil | |
| import scriptutils | |
| import argparse_oe | |
| logger = scriptutils.logger_create('devtool') | |
| class ConfigHandler: | |
| basepath = None | |
| config_file = '' | |
| config_obj = None | |
| init_path = '' | |
| workspace_path = '' | |
| def __init__(self, basepath, filename): | |
| self.basepath = basepath | |
| self.config_file = filename | |
| self.config_obj = configparser.ConfigParser() | |
| def get(self, section, option, default=None): | |
| try: | |
| ret = self.config_obj.get(section, option) | |
| except (configparser.NoOptionError, configparser.NoSectionError): | |
| if default is not None: | |
| ret = default | |
| else: | |
| raise | |
| return ret | |
| def read(self): | |
| if os.path.exists(self.config_file): | |
| self.config_obj.read(self.config_file) | |
| if self.config_obj.has_option('General', 'init_path'): | |
| pth = self.get('General', 'init_path') | |
| self.init_path = os.path.join(self.basepath, pth) | |
| if not os.path.exists(self.init_path): | |
| logger.error('init_path %s specified in config file cannot be found' % pth) | |
| return False | |
| else: | |
| self.config_obj.add_section('General') | |
| self.workspace_path = self.get('General', 'workspace_path', os.path.join(self.basepath, 'workspace')) | |
| return True | |
| def write(self): | |
| logger.debug('writing to config file %s' % self.config_file) | |
| self.config_obj.set('General', 'workspace_path', self.workspace_path) | |
| with open(self.config_file, 'w') as f: | |
| self.config_obj.write(f) | |
| def set(self, section, option, value): | |
| if not self.config_obj.has_section(section): | |
| self.config_obj.add_section(section) | |
| self.config_obj.set(section, option, value) | |
| @dataclasses.dataclass | |
| class Context: | |
| fixed_setup: bool | |
| config: ConfigHandler | |
| pluginpaths: List[str] | |
| def read_workspace(basepath, context): | |
| workspace = {} | |
| if not os.path.exists(os.path.join(context.config.workspace_path, 'conf', 'layer.conf')): | |
| if context.fixed_setup: | |
| logger.error("workspace layer not set up") | |
| sys.exit(1) | |
| else: | |
| logger.info('Creating workspace layer in %s' % context.config.workspace_path) | |
| _create_workspace(context.config.workspace_path, basepath) | |
| if not context.fixed_setup: | |
| _enable_workspace_layer(context.config.workspace_path, context.config, basepath) | |
| logger.debug('Reading workspace in %s' % context.config.workspace_path) | |
| externalsrc_re = re.compile(r'^EXTERNALSRC(:pn-([^ =]+))? *= *"([^"]*)"$') | |
| for fn in glob.glob(os.path.join(context.config.workspace_path, 'appends', '*.bbappend')): | |
| with open(fn, 'r') as f: | |
| pnvalues = {} | |
| pn = None | |
| for line in f: | |
| res = externalsrc_re.match(line.rstrip()) | |
| if res: | |
| recipepn = os.path.splitext(os.path.basename(fn))[0].split('_')[0] | |
| pn = res.group(2) or recipepn | |
| # Find the recipe file within the workspace, if any | |
| bbfile = os.path.basename(fn).replace('.bbappend', '.bb').replace('%', '*') | |
| recipefile = glob.glob(os.path.join(context.config.workspace_path, | |
| 'recipes', | |
| recipepn, | |
| bbfile)) | |
| if recipefile: | |
| recipefile = recipefile[0] | |
| pnvalues['srctree'] = res.group(3) | |
| pnvalues['bbappend'] = fn | |
| pnvalues['recipefile'] = recipefile | |
| elif line.startswith('# srctreebase: '): | |
| pnvalues['srctreebase'] = line.split(':', 1)[1].strip() | |
| if pnvalues: | |
| if not pn: | |
| raise DevtoolError("Found *.bbappend in %s, but could not determine EXTERNALSRC:pn-*. " | |
| "Maybe still using old syntax?" % context.config.workspace_path) | |
| if not pnvalues.get('srctreebase', None): | |
| pnvalues['srctreebase'] = pnvalues['srctree'] | |
| logger.debug('Found recipe %s' % pnvalues) | |
| workspace[pn] = pnvalues | |
| return workspace | |
| def create_workspace(args, config, basepath, _workspace): | |
| if args.layerpath: | |
| workspacedir = os.path.abspath(args.layerpath) | |
| else: | |
| workspacedir = os.path.abspath(os.path.join(basepath, 'workspace')) | |
| layerseries = None | |
| if args.layerseries: | |
| layerseries = args.layerseries | |
| _create_workspace(workspacedir, basepath, layerseries) | |
| if not args.create_only: | |
| _enable_workspace_layer(workspacedir, config, basepath) | |
| def _create_workspace(workspacedir, basepath, layerseries=None): | |
| import bb.utils | |
| confdir = os.path.join(workspacedir, 'conf') | |
| if os.path.exists(os.path.join(confdir, 'layer.conf')): | |
| logger.info('Specified workspace already set up, leaving as-is') | |
| else: | |
| if not layerseries: | |
| tinfoil = setup_tinfoil(config_only=True, basepath=basepath) | |
| try: | |
| layerseries = tinfoil.config_data.getVar('LAYERSERIES_CORENAMES') | |
| finally: | |
| tinfoil.shutdown() | |
| # Add a config file | |
| bb.utils.mkdirhier(confdir) | |
| with open(os.path.join(confdir, 'layer.conf'), 'w') as f: | |
| f.write('# ### workspace layer auto-generated by devtool ###\n') | |
| f.write('BBPATH =. "$' + '{LAYERDIR}:"\n') | |
| f.write('BBFILES += "$' + '{LAYERDIR}/recipes/*/*.bb \\\n') | |
| f.write(' $' + '{LAYERDIR}/appends/*.bbappend"\n') | |
| f.write('BBFILE_COLLECTIONS += "workspacelayer"\n') | |
| f.write('BBFILE_PATTERN_workspacelayer = "^$' + '{LAYERDIR}/"\n') | |
| f.write('BBFILE_PATTERN_IGNORE_EMPTY_workspacelayer = "1"\n') | |
| f.write('BBFILE_PRIORITY_workspacelayer = "99"\n') | |
| f.write('LAYERSERIES_COMPAT_workspacelayer = "%s"\n' % layerseries) | |
| # Add a README file | |
| with open(os.path.join(workspacedir, 'README'), 'w') as f: | |
| f.write('This layer was created by the OpenEmbedded devtool utility in order to\n') | |
| f.write('contain recipes and bbappends that are currently being worked on. The idea\n') | |
| f.write('is that the contents is temporary - once you have finished working on a\n') | |
| f.write('recipe you use the appropriate method to move the files you have been\n') | |
| f.write('working on to a proper layer. In most instances you should use the\n') | |
| f.write('devtool utility to manage files within it rather than modifying files\n') | |
| f.write('directly (although recipes added with "devtool add" will often need\n') | |
| f.write('direct modification.)\n') | |
| f.write('\nIf you no longer need to use devtool or the workspace layer\'s contents\n') | |
| f.write('you can remove the path to this workspace layer from your conf/bblayers.conf\n') | |
| f.write('file (and then delete the layer, if you wish).\n') | |
| f.write('\nNote that by default, if devtool fetches and unpacks source code, it\n') | |
| f.write('will place it in a subdirectory of a "sources" subdirectory of the\n') | |
| f.write('layer. If you prefer it to be elsewhere you can specify the source\n') | |
| f.write('tree path on the command line.\n') | |
| def _enable_workspace_layer(workspacedir, config, basepath): | |
| """Ensure the workspace layer is in bblayers.conf""" | |
| import bb.utils | |
| bblayers_conf = os.path.join(basepath, 'conf', 'bblayers.conf') | |
| if not os.path.exists(bblayers_conf): | |
| logger.error('Unable to find bblayers.conf') | |
| return | |
| if os.path.abspath(workspacedir) != os.path.abspath(config.workspace_path): | |
| removedir = config.workspace_path | |
| else: | |
| removedir = None | |
| _, added = bb.utils.edit_bblayers_conf(bblayers_conf, workspacedir, removedir) | |
| if added: | |
| logger.info('Enabling workspace layer in bblayers.conf') | |
| if config.workspace_path != workspacedir: | |
| # Update our config to point to the new location | |
| config.workspace_path = workspacedir | |
| config.write() | |
| def main(): | |
| if sys.getfilesystemencoding() != "utf-8": | |
| sys.exit("Please use a locale setting which supports utf-8.\nPython can't change the filesystem locale after loading so we need a utf-8 when python starts or things won't work.") | |
| # Default basepath | |
| basepath = os.path.dirname(os.path.abspath(__file__)) | |
| parser = argparse_oe.ArgumentParser(description="OpenEmbedded development tool", | |
| add_help=False, | |
| epilog="Use %(prog)s <subcommand> --help to get help on a specific command") | |
| parser.add_argument('--basepath', help='Base directory of SDK / build directory') | |
| parser.add_argument('--bbpath', help='Explicitly specify the BBPATH, rather than getting it from the metadata') | |
| parser.add_argument('-d', '--debug', help='Enable debug output', action='store_true') | |
| parser.add_argument('-q', '--quiet', help='Print only errors', action='store_true') | |
| parser.add_argument('--color', choices=['auto', 'always', 'never'], default='auto', help='Colorize output (where %(metavar)s is %(choices)s)', metavar='COLOR') | |
| global_args, unparsed_args = parser.parse_known_args() | |
| # Help is added here rather than via add_help=True, as we don't want it to | |
| # be handled by parse_known_args() | |
| parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, | |
| help='show this help message and exit') | |
| if global_args.debug: | |
| logger.setLevel(logging.DEBUG) | |
| elif global_args.quiet: | |
| logger.setLevel(logging.ERROR) | |
| is_fixed_setup = False | |
| if global_args.basepath: | |
| # Override | |
| basepath = global_args.basepath | |
| if os.path.exists(os.path.join(basepath, '.devtoolbase')): | |
| is_fixed_setup = True | |
| else: | |
| pth = basepath | |
| while pth != '' and pth != os.sep: | |
| if os.path.exists(os.path.join(pth, '.devtoolbase')): | |
| is_fixed_setup = True | |
| basepath = pth | |
| break | |
| pth = os.path.dirname(pth) | |
| if not is_fixed_setup: | |
| basepath = os.environ.get('BUILDDIR') | |
| if not basepath: | |
| logger.error("This script can only be run after initialising the build environment (e.g. by using oe-init-build-env)") | |
| sys.exit(1) | |
| logger.debug('Using basepath %s' % basepath) | |
| config = ConfigHandler(basepath, os.path.join(basepath, 'conf', 'devtool.conf')) | |
| if not config.read(): | |
| return -1 | |
| bitbake_subdir = config.get('General', 'bitbake_subdir', '') | |
| if bitbake_subdir: | |
| # Normally set for use within the SDK | |
| logger.debug('Using bitbake subdir %s' % bitbake_subdir) | |
| sys.path.insert(0, os.path.join(basepath, bitbake_subdir, 'lib')) | |
| core_meta_subdir = config.get('General', 'core_meta_subdir') | |
| sys.path.insert(0, os.path.join(basepath, core_meta_subdir, 'lib')) | |
| else: | |
| # Standard location | |
| import scriptpath | |
| bitbakepath = scriptpath.add_bitbake_lib_path() | |
| if not bitbakepath: | |
| logger.error("Unable to find bitbake by searching parent directory of this script or PATH") | |
| sys.exit(1) | |
| logger.debug('Using standard bitbake path %s' % bitbakepath) | |
| scriptpath.add_oe_lib_path() | |
| scriptutils.logger_setup_color(logger, global_args.color) | |
| if global_args.bbpath is None: | |
| import bb | |
| try: | |
| tinfoil = setup_tinfoil(config_only=True, basepath=basepath) | |
| try: | |
| global_args.bbpath = tinfoil.config_data.getVar('BBPATH') | |
| finally: | |
| tinfoil.shutdown() | |
| except bb.BBHandledException: | |
| return 2 | |
| # Search BBPATH first to allow layers to override plugins in scripts_path | |
| pluginpaths = [os.path.join(path, 'lib', 'devtool') for path in global_args.bbpath.split(':') + [scripts_path]] | |
| context = Context(fixed_setup=is_fixed_setup, config=config, pluginpaths=pluginpaths) | |
| plugins = [] | |
| for pluginpath in pluginpaths: | |
| scriptutils.load_plugins(logger, plugins, pluginpath) | |
| subparsers = parser.add_subparsers(dest="subparser_name", title='subcommands', metavar='<subcommand>') | |
| subparsers.required = True | |
| subparsers.add_subparser_group('sdk', 'SDK maintenance', -2) | |
| subparsers.add_subparser_group('advanced', 'Advanced', -1) | |
| subparsers.add_subparser_group('starting', 'Beginning work on a recipe', 100) | |
| subparsers.add_subparser_group('info', 'Getting information') | |
| subparsers.add_subparser_group('working', 'Working on a recipe in the workspace') | |
| subparsers.add_subparser_group('testbuild', 'Testing changes on target') | |
| if not context.fixed_setup: | |
| parser_create_workspace = subparsers.add_parser('create-workspace', | |
| help='Set up workspace in an alternative location', | |
| description='Sets up a new workspace. NOTE: other devtool subcommands will create a workspace automatically as needed, so you only need to use %(prog)s if you want to specify where the workspace should be located.', | |
| group='advanced') | |
| parser_create_workspace.add_argument('layerpath', nargs='?', help='Path in which the workspace layer should be created') | |
| parser_create_workspace.add_argument('--layerseries', help='Layer series the workspace should be set to be compatible with') | |
| parser_create_workspace.add_argument('--create-only', action="store_true", help='Only create the workspace layer, do not alter configuration') | |
| parser_create_workspace.set_defaults(func=create_workspace, no_workspace=True) | |
| for plugin in plugins: | |
| if hasattr(plugin, 'register_commands'): | |
| plugin.register_commands(subparsers, context) | |
| args = parser.parse_args(unparsed_args, namespace=global_args) | |
| try: | |
| workspace = {} | |
| if not getattr(args, 'no_workspace', False): | |
| workspace = read_workspace(basepath, context) | |
| ret = args.func(args, config, basepath, workspace) | |
| except DevtoolError as err: | |
| if str(err): | |
| logger.error(str(err)) | |
| ret = err.exitcode | |
| except argparse_oe.ArgumentUsageError as ae: | |
| parser.error_subcommand(ae.message, ae.subcommand) | |
| ret = 2 | |
| return ret | |
| if __name__ == "__main__": | |
| try: | |
| ret = main() | |
| except Exception: | |
| ret = 1 | |
| import traceback | |
| traceback.print_exc() | |
| sys.exit(ret) |