2 # Copyright (c) 2020 Project CHIP Authors
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 """Utitilies to flash or erase a device."""
26 # Here are the options that can be use to configure a `Flasher`
27 # object (as dictionary keys) and/or passed as command line options.
30 # Configuration options define properties used in flashing operations.
31 # (The outer level of an options definition corresponds to option groups
32 # in the command-line help message.)
34 # Script configuration options.
36 'help': 'Report more verbosely',
44 # 1 - action to be taken
45 # 2 - results of action, even if successful
50 # Action control options specify operations that Flasher.action() or
51 # the function interface flash_command() will perform.
53 # Action control options.
55 'help': 'Erase device',
58 'action': 'store_true'
62 'help': 'Flash an image',
68 'verify_application': {
69 'help': 'Verify the image after flashing',
72 'action': 'store_true'
75 # 'reset' is a three-way switch; if None, action() will reset the
76 # device if and only if an application image is flashed. So, we add
77 # an explicit option to set it false.
79 'help': 'Reset device after flashing',
80 'default': None, # None = Reset iff application was flashed.
82 'action': 'store_true'
86 'help': 'Do not reset device after flashing',
87 'default': None, # None = Reset iff application was flashed.
90 'action': 'store_false'
95 # Internal; these properties do not have command line options
96 # (because they don't have an `argparse` key).
98 # Script configuration options.
100 'help': 'Short name of the current platform',
104 'help': 'Invoking Python module, for generating scripts',
112 """Manage flashing."""
114 def __init__(self, **options):
115 # An integer giving the current Flasher status.
116 # 0 if OK, and normally an errno value if positive.
119 # Namespace of option values.
120 self.option = argparse.Namespace(**options)
122 # Namespace of option metadata. This contains the option specification
123 # information one level down from `define_options()`, i.e. without the
124 # group; the keys are mostly the same as those of `self.option`.
125 # (Exceptions include options with no metadata and only defined when
126 # constructing the Flasher, and options where different command line
127 # options (`info` keys) affect a single attribute (e.g. `reset` and
128 # `skip-reset` have distinct `info` entries but one option).
129 self.info = argparse.Namespace()
131 # `argv[0]` from the most recent call to parse_argv(); that is,
132 # the path used to invoke the script. This is used to find files
133 # relative to the script.
136 # Argument parser for `parse_argv()`. Normally defines command-line
137 # options for most of the `self.option` keys.
138 self.parser = argparse.ArgumentParser(
139 description='Flash {} device'.format(self.option.platform or 'a'))
141 # Argument parser groups.
144 # Construct the global options for all Flasher()s.
145 self.define_options(OPTIONS)
147 def define_options(self, options):
148 """Define options, including setting defaults and argument parsing."""
149 for group, group_options in options.items():
150 if group not in self.group:
151 self.group[group] = self.parser.add_argument_group(group)
152 for key, info in group_options.items():
153 setattr(self.info, key, info)
154 if 'argparse' not in info:
156 argument = info['argparse']
157 attribute = argument.get('dest', key)
159 if attribute not in self.option:
160 setattr(self.option, attribute, info['default'])
161 # Add command line argument.
164 names.append('--' + key.replace('_', '-'))
166 names += info['alias']
167 self.group[group].add_argument(
170 default=getattr(self.option, attribute),
175 """Return the current error code."""
179 """Perform actions on the device according to self.option."""
180 raise NotImplementedError()
182 def log(self, level, *args):
183 """Optionally log a message to stderr."""
184 if self.option.verbose >= level:
185 print(*args, file=sys.stderr)
195 capture_output=False):
196 """Run an external tool."""
201 option_map = vars(self.option)
203 option_map.update(options)
204 arguments = self.format_command(arguments, opt=option_map)
205 if not getattr(self.option, tool, None):
206 setattr(self.option, tool, self.locate_tool(tool))
207 tool_info = getattr(self.info, tool)
208 command_template = tool_info.get('command', ['{' + tool + '}', ()])
209 command = self.format_command(command_template, arguments, option_map)
210 self.log(3, 'Execute:', *command)
215 result = subprocess.run(
218 encoding=locale.getpreferredencoding(),
222 self.err = subprocess.call(command)
223 except FileNotFoundError as exception:
224 self.err = exception.errno
225 if self.err == errno.ENOENT:
226 # This likely means that the program was not found.
227 # But if it seems OK, rethrow the exception.
228 if self.verify_tool(tool):
232 self.log(fail_level, fail_message or ('FAILED: ' + name))
234 self.log(2, pass_message or (name + ' complete'))
237 def locate_tool(self, tool):
238 """Called to find an undefined tool. (Override in platform.)"""
241 def verify_tool(self, tool):
242 """Run a command to verify that an external tool is available.
244 Prints a configurable error and returns False if not.
246 tool_info = getattr(self.info, tool)
247 command_template = tool_info.get('verify')
248 if not command_template:
250 command = self.format_command(command_template, opt=vars(self.option))
252 self.err = subprocess.call(command)
253 except OSError as ex:
256 note = tool_info.get('error', 'Unable to execute {tool}.')
257 note = textwrap.dedent(note).format(tool=tool, **vars(self.option))
258 # textwrap.fill only handles single paragraphs:
259 note = '\n\n'.join((textwrap.fill(p) for p in note.split('\n\n')))
260 print(note, file=sys.stderr)
264 def format_command(self, template, args=None, opt=None):
265 """Construct a tool command line.
267 This provides a few conveniences over a simple list of fixed strings,
268 that in most cases eliminates any need for custom code to build a tool
269 command line. In this description, φ(τ) is the result of formatting a
272 template ::= list | () | str | dict
274 Typically the caller provides a list, and `format_command()` returns a
275 formatted list. The results of formatting sub-elements get interpolated
281 An empty tuple returns the supplied `args`. Typically this would be
282 used for things like subcommands or file names at the end of a command.
286 Formatting a string uses the Python string formatter with the `opt`
287 map as arguments. Typically used to interpolate an option value into
288 the command line, e.g. ['--flag', '{flag}'] or ['--flag={flag}'].
291 ↦ [σ.format_map(opt)]
293 A dictionary element provides a convenience feature. For any dictionary
294 template, if it contains an optional 'expand' key that tests true, the
295 result is recursively passed to format_command(); otherwise it is taken
298 The simplest case is an option propagated to the tool command line,
299 as a single option if the value is exactly boolean True or as an
300 option-argument pair if otherwise set.
302 optional ::= {'optional': name}
303 ↦ ['--name'] if opt[name] is True
304 ['--name', opt[name]] if opt[name] tests true
307 A dictionary with an 'option' can insert command line arguments based
308 on the value of an option. The 'result' is optional defaults to the
309 option value itself, and 'else' defaults to nothing.
311 option ::= {'option': name, 'result': ρ, 'else': δ}
315 A dictionary with a 'match' key returns a result comparing the value of
316 an option against a 'test' list of tuples. The 'else' is optional and
319 match ::= {'match': name, 'test': [(σᵢ, ρᵢ), …], 'else': ρ}
320 ↦ ρᵢ if opt[name]==σᵢ
323 if isinstance(template, str):
324 result = [template.format_map(opt)]
325 elif isinstance(template, list):
328 result += self.format_command(i, args, opt)
331 elif isinstance(template, dict):
332 if 'optional' in template:
333 name = template['optional']
334 value = opt.get(name)
336 result = ['--' + name]
338 result = ['--' + name, value]
341 elif 'option' in template:
342 name = template['option']
343 value = opt.get(name)
345 result = template.get('result', value)
347 result = template.get('else')
348 elif 'match' in template:
349 value = template['match']
350 for compare, result in template['test']:
354 result = template.get('else')
355 if result and template.get('expand'):
356 result = self.format_command(result, args, opt)
359 elif not isinstance(result, list):
362 raise ValueError('Unknown: {}'.format(template))
365 def find_file(self, filename, dirs=None):
366 """Resolve a file name; also checks the script directory."""
367 if os.path.isabs(filename) or os.path.exists(filename):
371 dirs.append(os.path.dirname(self.argv0))
372 for directory in dirs:
373 name = os.path.join(directory, filename)
374 if os.path.exists(name):
376 raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT),
379 def optional_file(self, filename, dirs=None):
380 """Resolve a file name, if present."""
383 return self.find_file(filename, dirs)
385 def parse_argv(self, argv):
386 """Handle command line options."""
388 self.parser.parse_args(argv[1:], namespace=self.option)
389 self._postprocess_argv()
392 def _postprocess_argv(self):
393 """Called after parse_argv() for platform-specific processing."""
395 def flash_command(self, argv):
396 """Perform device actions according to the command line."""
397 return self.parse_argv(argv).actions().status()
399 def _platform_wrapper_args(self, args):
400 """Called from make_wrapper() to optionally manipulate arguments."""
402 def make_wrapper(self, argv):
403 """Generate script to flash a device.
405 The generated script is a minimal wrapper around `flash_command()`,
406 containing any option values that differ from the class defaults.
409 # Note: this modifies the argument parser, so the same Flasher instance
410 # should not be used for both parse_argv() and make_wrapper().
411 self.parser.description = 'Generate a flashing script.'
412 self.parser.add_argument(
416 help='flashing script name')
418 args = self.parser.parse_args(argv[1:])
420 # Give platform-specific code a chance to manipulate the arguments
421 # for the wrapper script.
422 self._platform_wrapper_args(args)
424 # Find any option values that differ from the class defaults.
425 # These will be inserted into the wrapper script.
427 for key, value in vars(args).items():
428 if key in self.option and value != getattr(self.option, key):
429 defaults.append(' {}: {},'.format(repr(key), repr(value)))
440 if __name__ == '__main__':
441 sys.exit({module}.Flasher(**DEFAULTS).flash_command(sys.argv))
444 script = ('#!/usr/bin/env python3' + textwrap.dedent(script).format(
445 module=self.option.module, defaults='\n'.join(defaults)))
448 with open(args.output, 'w') as script_file:
449 script_file.write(script)
450 os.chmod(args.output, (stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR
451 | stat.S_IXGRP | stat.S_IRGRP
452 | stat.S_IXOTH | stat.S_IROTH))
453 except OSError as exception:
454 print(exception, sys.stderr)