1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright 2022 Google LLC
3 # Copyright (C) 2022 Weidmüller Interface GmbH & Co. KG
4 # Stefan Herbrechtsmeier <stefan.herbrechtsmeier@weidmueller.com>
6 """Base class for all bintools
8 This defines the common functionality for all bintools, including running
9 the tool, checking its version and fetching it if needed.
15 import multiprocessing
21 from u_boot_pylib import command
22 from u_boot_pylib import terminal
23 from u_boot_pylib import tools
24 from u_boot_pylib import tout
26 BINMAN_DIR = os.path.dirname(os.path.realpath(__file__))
28 # Format string for listing bintools, see also the header in list_all()
29 FORMAT = '%-16.16s %-12.12s %-26.26s %s'
31 # List of known modules, to avoid importing the module multiple times
34 # Possible ways of fetching a tool (FETCH_COUNT is number of ways)
35 FETCH_ANY, FETCH_BIN, FETCH_BUILD, FETCH_COUNT = range(4)
38 FETCH_ANY: 'any method',
39 FETCH_BIN: 'binary download',
40 FETCH_BUILD: 'build from source'
43 # Status of tool fetching
44 FETCHED, FAIL, PRESENT, STATUS_COUNT = range(4)
47 """Tool which operates on binaries to help produce entry contents
49 This is the base class for all bintools
51 # List of bintools to regard as missing
54 # Directory to store tools. Note that this set up by set_tool_dir() which
55 # must be called before this class is used.
58 def __init__(self, name, desc, version_regex=None, version_args='-V'):
61 self.version_regex = version_regex
62 self.version_args = version_args
65 def find_bintool_class(btype):
66 """Look up the bintool class for bintool
69 byte: Bintool to use, e.g. 'mkimage'
72 The bintool class object if found, else a tuple:
73 module name that could not be found
76 # Convert something like 'u-boot' to 'u_boot' since we are only
77 # interested in the type.
78 module_name = btype.replace('-', '_')
79 module = modules.get(module_name)
80 class_name = f'Bintool{module_name}'
82 # Import the module if we have not already done so
85 module = importlib.import_module('binman.btool.' + module_name)
86 except ImportError as exc:
88 # Deal with classes which must be renamed due to conflicts
89 # with Python libraries
90 module = importlib.import_module('binman.btool.btool_' +
93 return module_name, exc
94 modules[module_name] = module
96 # Look up the expected class name
97 return getattr(module, class_name)
101 """Create a new bintool object
104 name (str): Bintool to create, e.g. 'mkimage'
107 A new object of the correct type (a subclass of Binutil)
109 cls = Bintool.find_bintool_class(name)
110 if isinstance(cls, tuple):
111 raise ValueError("Cannot import bintool module '%s': %s" % cls)
113 # Call its constructor to get the object we want.
118 def set_tool_dir(cls, pathname):
119 """Set the path to use to store and find tools"""
120 cls.tooldir = pathname
123 """Show a line of information about a bintool"""
124 if self.is_present():
125 version = self.version()
128 print(FORMAT % (self.name, version, self.desc,
129 self.get_path() or '(not found)'))
132 def set_missing_list(cls, missing_list):
133 cls.missing_list = missing_list or []
136 def get_tool_list(include_testing=False):
137 """Get a list of the known tools
140 list of str: names of all tools known to binman
142 files = glob.glob(os.path.join(BINMAN_DIR, 'btool/*'))
143 names = [os.path.splitext(os.path.basename(fname))[0]
145 names = [name for name in names if name[0] != '_']
146 names = [name[6:] if name.startswith('btool_') else name
149 names.append('_testing')
154 """List all the bintools known to binman"""
155 names = Bintool.get_tool_list()
156 print(FORMAT % ('Name', 'Version', 'Description', 'Path'))
157 print(FORMAT % ('-' * 15,'-' * 11, '-' * 25, '-' * 30))
159 btool = Bintool.create(name)
162 def is_present(self):
163 """Check if a bintool is available on the system
166 bool: True if available, False if not
168 if self.name in self.missing_list:
170 return bool(self.get_path())
173 """Get the path of a bintool
176 str: Path to the tool, if available, else None
178 return tools.tool_find(self.name)
180 def fetch_tool(self, method, col, skip_present):
181 """Fetch a single tool
184 method (FETCH_...): Method to use
185 col (terminal.Color): Color terminal object
186 skip_present (boo;): Skip fetching if it is already present
189 int: Result of fetch either FETCHED, FAIL, PRESENT
194 res = self.fetch(meth)
195 except urllib.error.URLError as uerr:
196 message = uerr.reason
197 print(col.build(col.RED, f'- {message}'))
199 except ValueError as exc:
200 print(f'Exception: {exc}')
203 if skip_present and self.is_present():
205 print(col.build(col.YELLOW, 'Fetch: %s' % self.name))
206 if method == FETCH_ANY:
207 for try_method in range(1, FETCH_COUNT):
208 print(f'- trying method: {FETCH_NAMES[try_method]}')
209 result = try_fetch(try_method)
213 result = try_fetch(method)
216 if result is not True:
217 fname, tmpdir = result
218 dest = os.path.join(self.tooldir, self.name)
219 os.makedirs(self.tooldir, exist_ok=True)
220 print(f"- writing to '{dest}'")
221 shutil.move(fname, dest)
223 shutil.rmtree(tmpdir)
227 def fetch_tools(method, names_to_fetch):
228 """Fetch bintools from a suitable place
230 This fetches or builds the requested bintools so that they can be used
234 names_to_fetch (list of str): names of bintools to fetch
237 True on success, False on failure
239 def show_status(color, prompt, names):
241 color, f'{prompt}:%s{len(names):2}: %s' %
242 (' ' * (16 - len(prompt)), ' '.join(names))))
244 col = terminal.Color()
246 name_list = names_to_fetch
247 if len(names_to_fetch) == 1 and names_to_fetch[0] in ['all', 'missing']:
248 name_list = Bintool.get_tool_list()
249 if names_to_fetch[0] == 'missing':
251 print(col.build(col.YELLOW,
252 'Fetching tools: %s' % ' '.join(name_list)))
253 status = collections.defaultdict(list)
254 for name in name_list:
255 btool = Bintool.create(name)
256 result = btool.fetch_tool(method, col, skip_present)
257 status[result].append(name)
259 if method == FETCH_ANY:
260 print('- failed to fetch with all methods')
262 print(f"- method '{FETCH_NAMES[method]}' is not supported")
264 if len(name_list) > 1:
266 show_status(col.GREEN, 'Already present', status[PRESENT])
267 show_status(col.GREEN, 'Tools fetched', status[FETCHED])
269 show_status(col.RED, 'Failures', status[FAIL])
270 return not status[FAIL]
272 def run_cmd_result(self, *args, binary=False, raise_on_error=True):
273 """Run the bintool using command-line arguments
276 args (list of str): Arguments to provide, in addition to the bintool
278 binary (bool): True to return output as bytes instead of str
279 raise_on_error (bool): True to raise a ValueError exception if the
280 tool returns a non-zero return code
283 CommandResult: Resulting output from the bintool, or None if the
286 if self.name in self.missing_list:
288 name = os.path.expanduser(self.name) # Expand paths containing ~
289 all_args = (name,) + args
290 env = tools.get_env_with_path()
291 tout.debug(f"bintool: {' '.join(all_args)}")
292 result = command.run_pipe(
293 [all_args], capture=True, capture_stderr=True, env=env,
294 raise_on_error=False, binary=binary)
296 if result.return_code:
297 # Return None if the tool was not found. In this case there is no
298 # output from the tool and it does not appear on the path. We still
299 # try to run it (as above) since RunPipe() allows faking the tool's
301 if not any([result.stdout, result.stderr, tools.tool_find(name)]):
302 tout.info(f"bintool '{name}' not found")
305 tout.info(f"bintool '{name}' failed")
306 raise ValueError("Error %d running '%s': %s" %
307 (result.return_code, ' '.join(all_args),
308 result.stderr or result.stdout))
310 tout.debug(result.stdout)
312 tout.debug(result.stderr)
315 def run_cmd(self, *args, binary=False):
316 """Run the bintool using command-line arguments
319 args (list of str): Arguments to provide, in addition to the bintool
321 binary (bool): True to return output as bytes instead of str
324 str or bytes: Resulting stdout from the bintool
326 result = self.run_cmd_result(*args, binary=binary)
331 def build_from_git(cls, git_repo, make_target, bintool_path, flags=None):
332 """Build a bintool from a git repo
334 This clones the repo in a temporary directory, builds it with 'make',
335 then returns the filename of the resulting executable bintool
338 git_repo (str): URL of git repo
339 make_target (str): Target to pass to 'make' to build the tool
340 bintool_path (str): Relative path of the tool in the repo, after
342 flags (list of str): Flags or variables to pass to make, or None
346 str: Filename of fetched file to copy to a suitable directory
347 str: Name of temp directory to remove, or None
350 tmpdir = tempfile.mkdtemp(prefix='binmanf.')
351 print(f"- clone git repo '{git_repo}' to '{tmpdir}'")
352 tools.run('git', 'clone', '--depth', '1', git_repo, tmpdir)
353 print(f"- build target '{make_target}'")
354 cmd = ['make', '-C', tmpdir, '-j', f'{multiprocessing.cpu_count()}',
359 fname = os.path.join(tmpdir, bintool_path)
360 if not os.path.exists(fname):
361 print(f"- File '{fname}' was not produced")
366 def fetch_from_url(cls, url):
367 """Fetch a bintool from a URL
370 url (str): URL to fetch from
374 str: Filename of fetched file to copy to a suitable directory
375 str: Name of temp directory to remove, or None
377 fname, tmpdir = tools.download(url)
378 tools.run('chmod', 'a+x', fname)
382 def fetch_from_drive(cls, drive_id):
383 """Fetch a bintool from Google drive
386 drive_id (str): ID of file to fetch. For a URL of the form
387 'https://drive.google.com/file/d/xxx/view?usp=sharing' the value
388 passed here should be 'xxx'
392 str: Filename of fetched file to copy to a suitable directory
393 str: Name of temp directory to remove, or None
395 url = f'https://drive.google.com/uc?export=download&id={drive_id}'
396 return cls.fetch_from_url(url)
399 def apt_install(cls, package):
400 """Install a bintool using the 'apt' tool
402 This requires use of servo so may request a password
405 package (str): Name of package to install
408 True, assuming it completes without error
410 args = ['sudo', 'apt', 'install', '-y', package]
411 print('- %s' % ' '.join(args))
416 def WriteDocs(modules, test_missing=None):
417 """Write out documentation about the various bintools to stdout
420 modules: List of modules to include
421 test_missing: Used for testing. This is a module to report
424 print('''.. SPDX-License-Identifier: GPL-2.0+
426 Binman bintool Documentation
427 ============================
429 This file describes the bintools (binary tools) supported by binman. Bintools
430 are binman's name for external executables that it runs to generate or process
431 binaries. It is fairly easy to create new bintools. Just add a new file to the
432 'btool' directory. You can use existing bintools as examples.
436 modules = sorted(modules)
439 module = Bintool.find_bintool_class(name)
440 docs = getattr(module, '__doc__')
441 if test_missing == name:
444 lines = docs.splitlines()
445 first_line = lines[0]
446 rest = [line[4:] for line in lines[1:]]
447 hdr = 'Bintool: %s: %s' % (name, first_line)
449 print('-' * len(hdr))
450 print('\n'.join(rest))
457 raise ValueError('Documentation is missing for modules: %s' %
460 # pylint: disable=W0613
461 def fetch(self, method):
462 """Fetch handler for a bintool
464 This should be implemented by the base class
467 method (FETCH_...): Method to use
471 str: Filename of fetched file to copy to a suitable directory
472 str: Name of temp directory to remove, or None
473 or True if the file was fetched and already installed
474 or None if no fetch() implementation is available
477 Valuerror: Fetching could not be completed
479 print(f"No method to fetch bintool '{self.name}'")
483 """Version handler for a bintool
486 str: Version string for this bintool
488 if self.version_regex is None:
493 result = self.run_cmd_result(self.version_args)
494 out = result.stdout.strip()
496 out = result.stderr.strip()
500 m_version = re.search(self.version_regex, out)
501 return m_version.group(1) if m_version else out
504 class BintoolPacker(Bintool):
505 """Tool which compression / decompression entry contents
507 This is a bintools base class for compression / decompression packer
510 name: Name of packer tool
511 compression: Compression type (COMPRESS_...), value of 'name' property
513 compress_args: List of positional args provided to tool for compress,
514 ['--compress'] if none
515 decompress_args: List of positional args provided to tool for
516 decompress, ['--decompress'] if none
517 fetch_package: Name of the tool installed using the apt, value of 'name'
519 version_regex: Regular expressions to extract the version from tool
520 version output, '(v[0-9.]+)' if none
522 def __init__(self, name, compression=None, compress_args=None,
523 decompress_args=None, fetch_package=None,
524 version_regex=r'(v[0-9.]+)', version_args='-V'):
525 desc = '%s compression' % (compression if compression else name)
526 super().__init__(name, desc, version_regex, version_args)
527 if compress_args is None:
528 compress_args = ['--compress']
529 self.compress_args = compress_args
530 if decompress_args is None:
531 decompress_args = ['--decompress']
532 self.decompress_args = decompress_args
533 if fetch_package is None:
535 self.fetch_package = fetch_package
537 def compress(self, indata):
541 indata (bytes): Data to compress
544 bytes: Compressed data
546 with tempfile.NamedTemporaryFile(prefix='comp.tmp',
547 dir=tools.get_output_dir()) as tmp:
548 tools.write_file(tmp.name, indata)
549 args = self.compress_args + ['--stdout', tmp.name]
550 return self.run_cmd(*args, binary=True)
552 def decompress(self, indata):
556 indata (bytes): Data to decompress
559 bytes: Decompressed data
561 with tempfile.NamedTemporaryFile(prefix='decomp.tmp',
562 dir=tools.get_output_dir()) as inf:
563 tools.write_file(inf.name, indata)
564 args = self.decompress_args + ['--stdout', inf.name]
565 return self.run_cmd(*args, binary=True)
567 def fetch(self, method):
570 This installs the gzip package using the apt utility.
573 method (FETCH_...): Method to use
576 True if the file was fetched and now installed, None if a method
577 other than FETCH_BIN was requested
580 Valuerror: Fetching could not be completed
582 if method != FETCH_BIN:
584 return self.apt_install(self.fetch_package)