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