blob: 81872db377f92c8052bb9916b587bea057fb7a63 [file] [log] [blame]
Simon Glass861fbbf2022-01-09 20:13:49 -07001# SPDX-License-Identifier: GPL-2.0+
2# Copyright 2022 Google LLC
Stefan Herbrechtsmeier23cb8212022-08-19 16:25:33 +02003# Copyright (C) 2022 Weidmüller Interface GmbH & Co. KG
4# Stefan Herbrechtsmeier <stefan.herbrechtsmeier@weidmueller.com>
Simon Glass861fbbf2022-01-09 20:13:49 -07005#
6"""Base class for all bintools
7
8This defines the common functionality for all bintools, including running
9the tool, checking its version and fetching it if needed.
10"""
11
12import collections
13import glob
14import importlib
15import multiprocessing
16import os
17import shutil
18import tempfile
19import urllib.error
20
Simon Glass131444f2023-02-23 18:18:04 -070021from u_boot_pylib import command
22from u_boot_pylib import terminal
23from u_boot_pylib import tools
24from u_boot_pylib import tout
Simon Glass861fbbf2022-01-09 20:13:49 -070025
26BINMAN_DIR = os.path.dirname(os.path.realpath(__file__))
27
28# Format string for listing bintools, see also the header in list_all()
29FORMAT = '%-16.16s %-12.12s %-26.26s %s'
30
31# List of known modules, to avoid importing the module multiple times
32modules = {}
33
34# Possible ways of fetching a tool (FETCH_COUNT is number of ways)
35FETCH_ANY, FETCH_BIN, FETCH_BUILD, FETCH_COUNT = range(4)
36
37FETCH_NAMES = {
38 FETCH_ANY: 'any method',
39 FETCH_BIN: 'binary download',
40 FETCH_BUILD: 'build from source'
41 }
42
43# Status of tool fetching
44FETCHED, FAIL, PRESENT, STATUS_COUNT = range(4)
45
Simon Glass861fbbf2022-01-09 20:13:49 -070046class Bintool:
47 """Tool which operates on binaries to help produce entry contents
48
49 This is the base class for all bintools
50 """
51 # List of bintools to regard as missing
52 missing_list = []
53
Simon Glass9a1c7262023-02-22 12:14:49 -070054 # Directory to store tools. Note that this set up by set_tool_dir() which
55 # must be called before this class is used.
56 tooldir = ''
Simon Glassafd71752023-02-22 12:14:47 -070057
Quentin Schulz3264c352022-09-01 17:51:40 +020058 def __init__(self, name, desc, version_regex=None, version_args='-V'):
Simon Glass861fbbf2022-01-09 20:13:49 -070059 self.name = name
60 self.desc = desc
Quentin Schulzbbfc1372022-09-01 17:51:37 +020061 self.version_regex = version_regex
Quentin Schulz3264c352022-09-01 17:51:40 +020062 self.version_args = version_args
Simon Glass861fbbf2022-01-09 20:13:49 -070063
64 @staticmethod
65 def find_bintool_class(btype):
66 """Look up the bintool class for bintool
67
68 Args:
69 byte: Bintool to use, e.g. 'mkimage'
70
71 Returns:
72 The bintool class object if found, else a tuple:
73 module name that could not be found
74 exception received
75 """
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)
Stefan Herbrechtsmeier2b8cda62022-08-19 16:25:35 +020080 class_name = f'Bintool{module_name}'
Simon Glass861fbbf2022-01-09 20:13:49 -070081
82 # Import the module if we have not already done so
83 if not module:
84 try:
85 module = importlib.import_module('binman.btool.' + module_name)
86 except ImportError as exc:
Stefan Herbrechtsmeier2b8cda62022-08-19 16:25:35 +020087 try:
88 # Deal with classes which must be renamed due to conflicts
89 # with Python libraries
Stefan Herbrechtsmeier2b8cda62022-08-19 16:25:35 +020090 module = importlib.import_module('binman.btool.btool_' +
91 module_name)
92 except ImportError:
93 return module_name, exc
Simon Glass861fbbf2022-01-09 20:13:49 -070094 modules[module_name] = module
95
96 # Look up the expected class name
Stefan Herbrechtsmeier2b8cda62022-08-19 16:25:35 +020097 return getattr(module, class_name)
Simon Glass861fbbf2022-01-09 20:13:49 -070098
99 @staticmethod
100 def create(name):
101 """Create a new bintool object
102
103 Args:
104 name (str): Bintool to create, e.g. 'mkimage'
105
106 Returns:
107 A new object of the correct type (a subclass of Binutil)
108 """
109 cls = Bintool.find_bintool_class(name)
110 if isinstance(cls, tuple):
111 raise ValueError("Cannot import bintool module '%s': %s" % cls)
112
113 # Call its constructor to get the object we want.
114 obj = cls(name)
115 return obj
116
Simon Glass9a1c7262023-02-22 12:14:49 -0700117 @classmethod
118 def set_tool_dir(cls, pathname):
119 """Set the path to use to store and find tools"""
120 cls.tooldir = pathname
121
Simon Glass861fbbf2022-01-09 20:13:49 -0700122 def show(self):
123 """Show a line of information about a bintool"""
124 if self.is_present():
125 version = self.version()
126 else:
127 version = '-'
128 print(FORMAT % (self.name, version, self.desc,
129 self.get_path() or '(not found)'))
130
131 @classmethod
132 def set_missing_list(cls, missing_list):
133 cls.missing_list = missing_list or []
134
135 @staticmethod
136 def get_tool_list(include_testing=False):
137 """Get a list of the known tools
138
139 Returns:
140 list of str: names of all tools known to binman
141 """
142 files = glob.glob(os.path.join(BINMAN_DIR, 'btool/*'))
143 names = [os.path.splitext(os.path.basename(fname))[0]
144 for fname in files]
145 names = [name for name in names if name[0] != '_']
Quentin Schulzc33383c2022-11-07 13:54:54 +0100146 names = [name[6:] if name.startswith('btool_') else name
147 for name in names]
Simon Glass861fbbf2022-01-09 20:13:49 -0700148 if include_testing:
149 names.append('_testing')
150 return sorted(names)
151
152 @staticmethod
153 def list_all():
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))
158 for name in names:
159 btool = Bintool.create(name)
160 btool.show()
161
162 def is_present(self):
163 """Check if a bintool is available on the system
164
165 Returns:
166 bool: True if available, False if not
167 """
168 if self.name in self.missing_list:
169 return False
170 return bool(self.get_path())
171
172 def get_path(self):
173 """Get the path of a bintool
174
175 Returns:
176 str: Path to the tool, if available, else None
177 """
178 return tools.tool_find(self.name)
179
180 def fetch_tool(self, method, col, skip_present):
181 """Fetch a single tool
182
183 Args:
184 method (FETCH_...): Method to use
185 col (terminal.Color): Color terminal object
186 skip_present (boo;): Skip fetching if it is already present
187
188 Returns:
189 int: Result of fetch either FETCHED, FAIL, PRESENT
190 """
191 def try_fetch(meth):
192 res = None
193 try:
194 res = self.fetch(meth)
195 except urllib.error.URLError as uerr:
196 message = uerr.reason
Simon Glassf45d3742022-01-29 14:14:17 -0700197 print(col.build(col.RED, f'- {message}'))
Simon Glass861fbbf2022-01-09 20:13:49 -0700198
199 except ValueError as exc:
200 print(f'Exception: {exc}')
201 return res
202
203 if skip_present and self.is_present():
204 return PRESENT
Simon Glassf45d3742022-01-29 14:14:17 -0700205 print(col.build(col.YELLOW, 'Fetch: %s' % self.name))
Simon Glass861fbbf2022-01-09 20:13:49 -0700206 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)
210 if result:
211 break
212 else:
213 result = try_fetch(method)
214 if not result:
215 return FAIL
216 if result is not True:
217 fname, tmpdir = result
Simon Glassafd71752023-02-22 12:14:47 -0700218 dest = os.path.join(self.tooldir, self.name)
Simon Glass9a1c7262023-02-22 12:14:49 -0700219 os.makedirs(self.tooldir, exist_ok=True)
Simon Glass861fbbf2022-01-09 20:13:49 -0700220 print(f"- writing to '{dest}'")
221 shutil.move(fname, dest)
222 if tmpdir:
223 shutil.rmtree(tmpdir)
224 return FETCHED
225
226 @staticmethod
227 def fetch_tools(method, names_to_fetch):
228 """Fetch bintools from a suitable place
229
230 This fetches or builds the requested bintools so that they can be used
231 by binman
232
233 Args:
234 names_to_fetch (list of str): names of bintools to fetch
235
236 Returns:
237 True on success, False on failure
238 """
239 def show_status(color, prompt, names):
Simon Glassf45d3742022-01-29 14:14:17 -0700240 print(col.build(
Simon Glass861fbbf2022-01-09 20:13:49 -0700241 color, f'{prompt}:%s{len(names):2}: %s' %
242 (' ' * (16 - len(prompt)), ' '.join(names))))
243
244 col = terminal.Color()
245 skip_present = False
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':
250 skip_present = True
Simon Glassf45d3742022-01-29 14:14:17 -0700251 print(col.build(col.YELLOW,
Simon Glass861fbbf2022-01-09 20:13:49 -0700252 '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)
258 if result == FAIL:
259 if method == FETCH_ANY:
260 print('- failed to fetch with all methods')
261 else:
262 print(f"- method '{FETCH_NAMES[method]}' is not supported")
263
264 if len(name_list) > 1:
265 if skip_present:
266 show_status(col.GREEN, 'Already present', status[PRESENT])
267 show_status(col.GREEN, 'Tools fetched', status[FETCHED])
268 if status[FAIL]:
269 show_status(col.RED, 'Failures', status[FAIL])
270 return not status[FAIL]
271
272 def run_cmd_result(self, *args, binary=False, raise_on_error=True):
273 """Run the bintool using command-line arguments
274
275 Args:
276 args (list of str): Arguments to provide, in addition to the bintool
277 name
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
281
282 Returns:
283 CommandResult: Resulting output from the bintool, or None if the
284 tool is not present
285 """
286 if self.name in self.missing_list:
287 return None
288 name = os.path.expanduser(self.name) # Expand paths containing ~
289 all_args = (name,) + args
290 env = tools.get_env_with_path()
Simon Glassab7396b2023-07-18 07:24:09 -0600291 tout.debug(f"bintool: {' '.join(all_args)}")
Simon Glass840be732022-01-29 14:14:05 -0700292 result = command.run_pipe(
Simon Glass861fbbf2022-01-09 20:13:49 -0700293 [all_args], capture=True, capture_stderr=True, env=env,
294 raise_on_error=False, binary=binary)
295
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
300 # output
301 if not any([result.stdout, result.stderr, tools.tool_find(name)]):
Simon Glass011f1b32022-01-29 14:14:15 -0700302 tout.info(f"bintool '{name}' not found")
Simon Glass861fbbf2022-01-09 20:13:49 -0700303 return None
304 if raise_on_error:
Simon Glass011f1b32022-01-29 14:14:15 -0700305 tout.info(f"bintool '{name}' failed")
Simon Glass861fbbf2022-01-09 20:13:49 -0700306 raise ValueError("Error %d running '%s': %s" %
307 (result.return_code, ' '.join(all_args),
308 result.stderr or result.stdout))
309 if result.stdout:
Simon Glass011f1b32022-01-29 14:14:15 -0700310 tout.debug(result.stdout)
Simon Glass861fbbf2022-01-09 20:13:49 -0700311 if result.stderr:
Simon Glass011f1b32022-01-29 14:14:15 -0700312 tout.debug(result.stderr)
Simon Glass861fbbf2022-01-09 20:13:49 -0700313 return result
314
315 def run_cmd(self, *args, binary=False):
316 """Run the bintool using command-line arguments
317
318 Args:
319 args (list of str): Arguments to provide, in addition to the bintool
320 name
321 binary (bool): True to return output as bytes instead of str
322
323 Returns:
324 str or bytes: Resulting stdout from the bintool
325 """
326 result = self.run_cmd_result(*args, binary=binary)
327 if result:
328 return result.stdout
329
330 @classmethod
Leonard Anderweitc58ed5c2025-02-26 22:04:59 +0100331 def build_from_git(cls, git_repo, make_targets, bintool_path,
Leonard Anderweitb1eec552025-02-26 22:05:00 +0100332 flags=None, git_branch=None, make_path=None):
Simon Glass861fbbf2022-01-09 20:13:49 -0700333 """Build a bintool from a git repo
334
335 This clones the repo in a temporary directory, builds it with 'make',
336 then returns the filename of the resulting executable bintool
337
338 Args:
339 git_repo (str): URL of git repo
Sughosh Ganu6526e092023-08-22 23:09:54 +0530340 make_targets (list of str): List of targets to pass to 'make' to build
341 the tool
Simon Glass861fbbf2022-01-09 20:13:49 -0700342 bintool_path (str): Relative path of the tool in the repo, after
343 build is complete
Simon Glass49145472022-09-17 09:01:19 -0600344 flags (list of str): Flags or variables to pass to make, or None
Leonard Anderweitc58ed5c2025-02-26 22:04:59 +0100345 git_branch (str): Branch of git repo, or None to use the default
Leonard Anderweitb1eec552025-02-26 22:05:00 +0100346 make_path (str): Relative path inside git repo containing the
347 Makefile, or None
Simon Glass861fbbf2022-01-09 20:13:49 -0700348
349 Returns:
350 tuple:
351 str: Filename of fetched file to copy to a suitable directory
352 str: Name of temp directory to remove, or None
353 or None on error
354 """
355 tmpdir = tempfile.mkdtemp(prefix='binmanf.')
356 print(f"- clone git repo '{git_repo}' to '{tmpdir}'")
Leonard Anderweitc58ed5c2025-02-26 22:04:59 +0100357 if git_branch:
358 tools.run('git', 'clone', '--depth', '1', '--branch', git_branch,
359 git_repo, tmpdir)
360 else:
361 tools.run('git', 'clone', '--depth', '1', git_repo, tmpdir)
Sughosh Ganu6526e092023-08-22 23:09:54 +0530362 for target in make_targets:
363 print(f"- build target '{target}'")
Leonard Anderweitb1eec552025-02-26 22:05:00 +0100364 makedir = tmpdir
365 if make_path:
366 makedir = os.path.join(tmpdir, make_path)
367 cmd = ['make', '-C', makedir, '-j', f'{multiprocessing.cpu_count()}',
Sughosh Ganu6526e092023-08-22 23:09:54 +0530368 target]
369 if flags:
370 cmd += flags
371 tools.run(*cmd)
372
Simon Glass861fbbf2022-01-09 20:13:49 -0700373 fname = os.path.join(tmpdir, bintool_path)
374 if not os.path.exists(fname):
375 print(f"- File '{fname}' was not produced")
376 return None
377 return fname, tmpdir
378
379 @classmethod
380 def fetch_from_url(cls, url):
381 """Fetch a bintool from a URL
382
383 Args:
384 url (str): URL to fetch from
385
386 Returns:
387 tuple:
388 str: Filename of fetched file to copy to a suitable directory
389 str: Name of temp directory to remove, or None
390 """
Simon Glass80025522022-01-29 14:14:04 -0700391 fname, tmpdir = tools.download(url)
392 tools.run('chmod', 'a+x', fname)
Simon Glass861fbbf2022-01-09 20:13:49 -0700393 return fname, tmpdir
394
395 @classmethod
396 def fetch_from_drive(cls, drive_id):
397 """Fetch a bintool from Google drive
398
399 Args:
400 drive_id (str): ID of file to fetch. For a URL of the form
401 'https://drive.google.com/file/d/xxx/view?usp=sharing' the value
402 passed here should be 'xxx'
403
404 Returns:
405 tuple:
406 str: Filename of fetched file to copy to a suitable directory
407 str: Name of temp directory to remove, or None
408 """
409 url = f'https://drive.google.com/uc?export=download&id={drive_id}'
410 return cls.fetch_from_url(url)
411
412 @classmethod
413 def apt_install(cls, package):
Simon Glass1e040142023-02-22 12:14:45 -0700414 """Install a bintool using the 'apt' tool
Simon Glass861fbbf2022-01-09 20:13:49 -0700415
416 This requires use of servo so may request a password
417
418 Args:
419 package (str): Name of package to install
420
421 Returns:
422 True, assuming it completes without error
423 """
424 args = ['sudo', 'apt', 'install', '-y', package]
425 print('- %s' % ' '.join(args))
Simon Glass80025522022-01-29 14:14:04 -0700426 tools.run(*args)
Simon Glass861fbbf2022-01-09 20:13:49 -0700427 return True
428
Simon Glass620c4462022-01-09 20:14:11 -0700429 @staticmethod
430 def WriteDocs(modules, test_missing=None):
431 """Write out documentation about the various bintools to stdout
432
433 Args:
434 modules: List of modules to include
435 test_missing: Used for testing. This is a module to report
436 as missing
437 """
438 print('''.. SPDX-License-Identifier: GPL-2.0+
439
440Binman bintool Documentation
441============================
442
443This file describes the bintools (binary tools) supported by binman. Bintools
444are binman's name for external executables that it runs to generate or process
445binaries. It is fairly easy to create new bintools. Just add a new file to the
446'btool' directory. You can use existing bintools as examples.
447
448
449''')
450 modules = sorted(modules)
451 missing = []
452 for name in modules:
453 module = Bintool.find_bintool_class(name)
454 docs = getattr(module, '__doc__')
455 if test_missing == name:
456 docs = None
457 if docs:
458 lines = docs.splitlines()
459 first_line = lines[0]
460 rest = [line[4:] for line in lines[1:]]
461 hdr = 'Bintool: %s: %s' % (name, first_line)
462 print(hdr)
463 print('-' * len(hdr))
464 print('\n'.join(rest))
465 print()
466 print()
467 else:
468 missing.append(name)
469
470 if missing:
471 raise ValueError('Documentation is missing for modules: %s' %
472 ', '.join(missing))
473
Simon Glass861fbbf2022-01-09 20:13:49 -0700474 # pylint: disable=W0613
475 def fetch(self, method):
476 """Fetch handler for a bintool
477
478 This should be implemented by the base class
479
480 Args:
481 method (FETCH_...): Method to use
482
483 Returns:
484 tuple:
485 str: Filename of fetched file to copy to a suitable directory
486 str: Name of temp directory to remove, or None
487 or True if the file was fetched and already installed
488 or None if no fetch() implementation is available
489
490 Raises:
491 Valuerror: Fetching could not be completed
492 """
493 print(f"No method to fetch bintool '{self.name}'")
494 return False
495
Simon Glass861fbbf2022-01-09 20:13:49 -0700496 def version(self):
497 """Version handler for a bintool
498
Simon Glass861fbbf2022-01-09 20:13:49 -0700499 Returns:
500 str: Version string for this bintool
501 """
Quentin Schulzbbfc1372022-09-01 17:51:37 +0200502 if self.version_regex is None:
503 return 'unknown'
504
505 import re
506
Quentin Schulz3264c352022-09-01 17:51:40 +0200507 result = self.run_cmd_result(self.version_args)
Quentin Schulzbbfc1372022-09-01 17:51:37 +0200508 out = result.stdout.strip()
509 if not out:
510 out = result.stderr.strip()
511 if not out:
512 return 'unknown'
513
514 m_version = re.search(self.version_regex, out)
515 return m_version.group(1) if m_version else out
516
Stefan Herbrechtsmeier23cb8212022-08-19 16:25:33 +0200517
518class BintoolPacker(Bintool):
519 """Tool which compression / decompression entry contents
520
521 This is a bintools base class for compression / decompression packer
522
523 Properties:
524 name: Name of packer tool
525 compression: Compression type (COMPRESS_...), value of 'name' property
526 if none
527 compress_args: List of positional args provided to tool for compress,
528 ['--compress'] if none
529 decompress_args: List of positional args provided to tool for
530 decompress, ['--decompress'] if none
531 fetch_package: Name of the tool installed using the apt, value of 'name'
532 property if none
533 version_regex: Regular expressions to extract the version from tool
534 version output, '(v[0-9.]+)' if none
535 """
536 def __init__(self, name, compression=None, compress_args=None,
537 decompress_args=None, fetch_package=None,
Quentin Schulz3264c352022-09-01 17:51:40 +0200538 version_regex=r'(v[0-9.]+)', version_args='-V'):
Stefan Herbrechtsmeier23cb8212022-08-19 16:25:33 +0200539 desc = '%s compression' % (compression if compression else name)
Quentin Schulz3264c352022-09-01 17:51:40 +0200540 super().__init__(name, desc, version_regex, version_args)
Stefan Herbrechtsmeier23cb8212022-08-19 16:25:33 +0200541 if compress_args is None:
542 compress_args = ['--compress']
543 self.compress_args = compress_args
544 if decompress_args is None:
545 decompress_args = ['--decompress']
546 self.decompress_args = decompress_args
547 if fetch_package is None:
548 fetch_package = name
549 self.fetch_package = fetch_package
Stefan Herbrechtsmeier23cb8212022-08-19 16:25:33 +0200550
551 def compress(self, indata):
552 """Compress data
553
554 Args:
555 indata (bytes): Data to compress
556
557 Returns:
558 bytes: Compressed data
559 """
560 with tempfile.NamedTemporaryFile(prefix='comp.tmp',
561 dir=tools.get_output_dir()) as tmp:
562 tools.write_file(tmp.name, indata)
563 args = self.compress_args + ['--stdout', tmp.name]
564 return self.run_cmd(*args, binary=True)
565
566 def decompress(self, indata):
567 """Decompress data
568
569 Args:
570 indata (bytes): Data to decompress
571
572 Returns:
573 bytes: Decompressed data
574 """
575 with tempfile.NamedTemporaryFile(prefix='decomp.tmp',
576 dir=tools.get_output_dir()) as inf:
577 tools.write_file(inf.name, indata)
578 args = self.decompress_args + ['--stdout', inf.name]
579 return self.run_cmd(*args, binary=True)
580
581 def fetch(self, method):
582 """Fetch handler
583
584 This installs the gzip package using the apt utility.
585
586 Args:
587 method (FETCH_...): Method to use
588
589 Returns:
590 True if the file was fetched and now installed, None if a method
591 other than FETCH_BIN was requested
592
593 Raises:
594 Valuerror: Fetching could not be completed
595 """
596 if method != FETCH_BIN:
597 return None
598 return self.apt_install(self.fetch_package)