1#!/usr/bin/env python3 2 3# OpenEmbedded Development tool 4# 5# Copyright (C) 2014-2015 Intel Corporation 6# 7# SPDX-License-Identifier: GPL-2.0-only 8# 9 10import sys 11import os 12import argparse 13import glob 14import re 15import configparser 16import subprocess 17import logging 18 19basepath = '' 20workspace = {} 21config = None 22context = None 23 24 25scripts_path = os.path.dirname(os.path.realpath(__file__)) 26lib_path = scripts_path + '/lib' 27sys.path = sys.path + [lib_path] 28from devtool import DevtoolError, setup_tinfoil 29import scriptutils 30import argparse_oe 31logger = scriptutils.logger_create('devtool') 32 33plugins = [] 34 35 36class ConfigHandler(object): 37 config_file = '' 38 config_obj = None 39 init_path = '' 40 workspace_path = '' 41 42 def __init__(self, filename): 43 self.config_file = filename 44 self.config_obj = configparser.ConfigParser() 45 46 def get(self, section, option, default=None): 47 try: 48 ret = self.config_obj.get(section, option) 49 except (configparser.NoOptionError, configparser.NoSectionError): 50 if default != None: 51 ret = default 52 else: 53 raise 54 return ret 55 56 def read(self): 57 if os.path.exists(self.config_file): 58 self.config_obj.read(self.config_file) 59 60 if self.config_obj.has_option('General', 'init_path'): 61 pth = self.get('General', 'init_path') 62 self.init_path = os.path.join(basepath, pth) 63 if not os.path.exists(self.init_path): 64 logger.error('init_path %s specified in config file cannot be found' % pth) 65 return False 66 else: 67 self.config_obj.add_section('General') 68 69 self.workspace_path = self.get('General', 'workspace_path', os.path.join(basepath, 'workspace')) 70 return True 71 72 73 def write(self): 74 logger.debug('writing to config file %s' % self.config_file) 75 self.config_obj.set('General', 'workspace_path', self.workspace_path) 76 with open(self.config_file, 'w') as f: 77 self.config_obj.write(f) 78 79 def set(self, section, option, value): 80 if not self.config_obj.has_section(section): 81 self.config_obj.add_section(section) 82 self.config_obj.set(section, option, value) 83 84class Context: 85 def __init__(self, **kwargs): 86 self.__dict__.update(kwargs) 87 88 89def read_workspace(): 90 global workspace 91 workspace = {} 92 if not os.path.exists(os.path.join(config.workspace_path, 'conf', 'layer.conf')): 93 if context.fixed_setup: 94 logger.error("workspace layer not set up") 95 sys.exit(1) 96 else: 97 logger.info('Creating workspace layer in %s' % config.workspace_path) 98 _create_workspace(config.workspace_path, config, basepath) 99 if not context.fixed_setup: 100 _enable_workspace_layer(config.workspace_path, config, basepath) 101 102 logger.debug('Reading workspace in %s' % config.workspace_path) 103 externalsrc_re = re.compile(r'^EXTERNALSRC(:pn-([^ =]+))? *= *"([^"]*)"$') 104 for fn in glob.glob(os.path.join(config.workspace_path, 'appends', '*.bbappend')): 105 with open(fn, 'r') as f: 106 pnvalues = {} 107 pn = None 108 for line in f: 109 res = externalsrc_re.match(line.rstrip()) 110 if res: 111 recipepn = os.path.splitext(os.path.basename(fn))[0].split('_')[0] 112 pn = res.group(2) or recipepn 113 # Find the recipe file within the workspace, if any 114 bbfile = os.path.basename(fn).replace('.bbappend', '.bb').replace('%', '*') 115 recipefile = glob.glob(os.path.join(config.workspace_path, 116 'recipes', 117 recipepn, 118 bbfile)) 119 if recipefile: 120 recipefile = recipefile[0] 121 pnvalues['srctree'] = res.group(3) 122 pnvalues['bbappend'] = fn 123 pnvalues['recipefile'] = recipefile 124 elif line.startswith('# srctreebase: '): 125 pnvalues['srctreebase'] = line.split(':', 1)[1].strip() 126 if pnvalues: 127 if not pn: 128 raise DevtoolError("Found *.bbappend in %s, but could not determine EXTERNALSRC:pn-*. " 129 "Maybe still using old syntax?" % config.workspace_path) 130 if not pnvalues.get('srctreebase', None): 131 pnvalues['srctreebase'] = pnvalues['srctree'] 132 logger.debug('Found recipe %s' % pnvalues) 133 workspace[pn] = pnvalues 134 135def create_workspace(args, config, basepath, workspace): 136 if args.layerpath: 137 workspacedir = os.path.abspath(args.layerpath) 138 else: 139 workspacedir = os.path.abspath(os.path.join(basepath, 'workspace')) 140 _create_workspace(workspacedir, config, basepath) 141 if not args.create_only: 142 _enable_workspace_layer(workspacedir, config, basepath) 143 144def _create_workspace(workspacedir, config, basepath): 145 import bb 146 147 confdir = os.path.join(workspacedir, 'conf') 148 if os.path.exists(os.path.join(confdir, 'layer.conf')): 149 logger.info('Specified workspace already set up, leaving as-is') 150 else: 151 # Add a config file 152 bb.utils.mkdirhier(confdir) 153 with open(os.path.join(confdir, 'layer.conf'), 'w') as f: 154 f.write('# ### workspace layer auto-generated by devtool ###\n') 155 f.write('BBPATH =. "$' + '{LAYERDIR}:"\n') 156 f.write('BBFILES += "$' + '{LAYERDIR}/recipes/*/*.bb \\\n') 157 f.write(' $' + '{LAYERDIR}/appends/*.bbappend"\n') 158 f.write('BBFILE_COLLECTIONS += "workspacelayer"\n') 159 f.write('BBFILE_PATTERN_workspacelayer = "^$' + '{LAYERDIR}/"\n') 160 f.write('BBFILE_PATTERN_IGNORE_EMPTY_workspacelayer = "1"\n') 161 f.write('BBFILE_PRIORITY_workspacelayer = "99"\n') 162 f.write('LAYERSERIES_COMPAT_workspacelayer = "${LAYERSERIES_COMPAT_core}"\n') 163 # Add a README file 164 with open(os.path.join(workspacedir, 'README'), 'w') as f: 165 f.write('This layer was created by the OpenEmbedded devtool utility in order to\n') 166 f.write('contain recipes and bbappends that are currently being worked on. The idea\n') 167 f.write('is that the contents is temporary - once you have finished working on a\n') 168 f.write('recipe you use the appropriate method to move the files you have been\n') 169 f.write('working on to a proper layer. In most instances you should use the\n') 170 f.write('devtool utility to manage files within it rather than modifying files\n') 171 f.write('directly (although recipes added with "devtool add" will often need\n') 172 f.write('direct modification.)\n') 173 f.write('\nIf you no longer need to use devtool or the workspace layer\'s contents\n') 174 f.write('you can remove the path to this workspace layer from your conf/bblayers.conf\n') 175 f.write('file (and then delete the layer, if you wish).\n') 176 f.write('\nNote that by default, if devtool fetches and unpacks source code, it\n') 177 f.write('will place it in a subdirectory of a "sources" subdirectory of the\n') 178 f.write('layer. If you prefer it to be elsewhere you can specify the source\n') 179 f.write('tree path on the command line.\n') 180 181def _enable_workspace_layer(workspacedir, config, basepath): 182 """Ensure the workspace layer is in bblayers.conf""" 183 import bb 184 bblayers_conf = os.path.join(basepath, 'conf', 'bblayers.conf') 185 if not os.path.exists(bblayers_conf): 186 logger.error('Unable to find bblayers.conf') 187 return 188 if os.path.abspath(workspacedir) != os.path.abspath(config.workspace_path): 189 removedir = config.workspace_path 190 else: 191 removedir = None 192 _, added = bb.utils.edit_bblayers_conf(bblayers_conf, workspacedir, removedir) 193 if added: 194 logger.info('Enabling workspace layer in bblayers.conf') 195 if config.workspace_path != workspacedir: 196 # Update our config to point to the new location 197 config.workspace_path = workspacedir 198 config.write() 199 200 201def main(): 202 global basepath 203 global config 204 global context 205 206 if sys.getfilesystemencoding() != "utf-8": 207 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.") 208 209 context = Context(fixed_setup=False) 210 211 # Default basepath 212 basepath = os.path.dirname(os.path.abspath(__file__)) 213 214 parser = argparse_oe.ArgumentParser(description="OpenEmbedded development tool", 215 add_help=False, 216 epilog="Use %(prog)s <subcommand> --help to get help on a specific command") 217 parser.add_argument('--basepath', help='Base directory of SDK / build directory') 218 parser.add_argument('--bbpath', help='Explicitly specify the BBPATH, rather than getting it from the metadata') 219 parser.add_argument('-d', '--debug', help='Enable debug output', action='store_true') 220 parser.add_argument('-q', '--quiet', help='Print only errors', action='store_true') 221 parser.add_argument('--color', choices=['auto', 'always', 'never'], default='auto', help='Colorize output (where %(metavar)s is %(choices)s)', metavar='COLOR') 222 223 global_args, unparsed_args = parser.parse_known_args() 224 225 # Help is added here rather than via add_help=True, as we don't want it to 226 # be handled by parse_known_args() 227 parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, 228 help='show this help message and exit') 229 230 if global_args.debug: 231 logger.setLevel(logging.DEBUG) 232 elif global_args.quiet: 233 logger.setLevel(logging.ERROR) 234 235 if global_args.basepath: 236 # Override 237 basepath = global_args.basepath 238 if os.path.exists(os.path.join(basepath, '.devtoolbase')): 239 context.fixed_setup = True 240 else: 241 pth = basepath 242 while pth != '' and pth != os.sep: 243 if os.path.exists(os.path.join(pth, '.devtoolbase')): 244 context.fixed_setup = True 245 basepath = pth 246 break 247 pth = os.path.dirname(pth) 248 249 if not context.fixed_setup: 250 basepath = os.environ.get('BUILDDIR') 251 if not basepath: 252 logger.error("This script can only be run after initialising the build environment (e.g. by using oe-init-build-env)") 253 sys.exit(1) 254 255 logger.debug('Using basepath %s' % basepath) 256 257 config = ConfigHandler(os.path.join(basepath, 'conf', 'devtool.conf')) 258 if not config.read(): 259 return -1 260 context.config = config 261 262 bitbake_subdir = config.get('General', 'bitbake_subdir', '') 263 if bitbake_subdir: 264 # Normally set for use within the SDK 265 logger.debug('Using bitbake subdir %s' % bitbake_subdir) 266 sys.path.insert(0, os.path.join(basepath, bitbake_subdir, 'lib')) 267 core_meta_subdir = config.get('General', 'core_meta_subdir') 268 sys.path.insert(0, os.path.join(basepath, core_meta_subdir, 'lib')) 269 else: 270 # Standard location 271 import scriptpath 272 bitbakepath = scriptpath.add_bitbake_lib_path() 273 if not bitbakepath: 274 logger.error("Unable to find bitbake by searching parent directory of this script or PATH") 275 sys.exit(1) 276 logger.debug('Using standard bitbake path %s' % bitbakepath) 277 scriptpath.add_oe_lib_path() 278 279 scriptutils.logger_setup_color(logger, global_args.color) 280 281 if global_args.bbpath is None: 282 try: 283 tinfoil = setup_tinfoil(config_only=True, basepath=basepath) 284 try: 285 global_args.bbpath = tinfoil.config_data.getVar('BBPATH') 286 finally: 287 tinfoil.shutdown() 288 except bb.BBHandledException: 289 return 2 290 291 # Search BBPATH first to allow layers to override plugins in scripts_path 292 for path in global_args.bbpath.split(':') + [scripts_path]: 293 pluginpath = os.path.join(path, 'lib', 'devtool') 294 scriptutils.load_plugins(logger, plugins, pluginpath) 295 296 subparsers = parser.add_subparsers(dest="subparser_name", title='subcommands', metavar='<subcommand>') 297 subparsers.required = True 298 299 subparsers.add_subparser_group('sdk', 'SDK maintenance', -2) 300 subparsers.add_subparser_group('advanced', 'Advanced', -1) 301 subparsers.add_subparser_group('starting', 'Beginning work on a recipe', 100) 302 subparsers.add_subparser_group('info', 'Getting information') 303 subparsers.add_subparser_group('working', 'Working on a recipe in the workspace') 304 subparsers.add_subparser_group('testbuild', 'Testing changes on target') 305 306 if not context.fixed_setup: 307 parser_create_workspace = subparsers.add_parser('create-workspace', 308 help='Set up workspace in an alternative location', 309 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.', 310 group='advanced') 311 parser_create_workspace.add_argument('layerpath', nargs='?', help='Path in which the workspace layer should be created') 312 parser_create_workspace.add_argument('--create-only', action="store_true", help='Only create the workspace layer, do not alter configuration') 313 parser_create_workspace.set_defaults(func=create_workspace, no_workspace=True) 314 315 for plugin in plugins: 316 if hasattr(plugin, 'register_commands'): 317 plugin.register_commands(subparsers, context) 318 319 args = parser.parse_args(unparsed_args, namespace=global_args) 320 321 try: 322 if not getattr(args, 'no_workspace', False): 323 read_workspace() 324 325 ret = args.func(args, config, basepath, workspace) 326 except DevtoolError as err: 327 if str(err): 328 logger.error(str(err)) 329 ret = err.exitcode 330 except argparse_oe.ArgumentUsageError as ae: 331 parser.error_subcommand(ae.message, ae.subcommand) 332 333 return ret 334 335 336if __name__ == "__main__": 337 try: 338 ret = main() 339 except Exception: 340 ret = 1 341 import traceback 342 traceback.print_exc() 343 sys.exit(ret) 344