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