blob: 8a4f41bc372ecebcf4e38d96e28f6441dd6df144 [file] [log] [blame]
lh9ed821d2023-04-07 01:36:19 -07001#!/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)