1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright 2022 Google LLC
4 """Base class for all bintools
6 This defines the common functionality for all bintools, including running
7 the tool, checking its version and fetching it if needed.
13 import multiprocessing
19 from patman import command
20 from patman import terminal
21 from patman import tools
22 from patman import tout
24 BINMAN_DIR = os.path.dirname(os.path.realpath(__file__))
26 # Format string for listing bintools, see also the header in list_all()
27 FORMAT = '%-16.16s %-12.12s %-26.26s %s'
29 # List of known modules, to avoid importing the module multiple times
32 # Possible ways of fetching a tool (FETCH_COUNT is number of ways)
33 FETCH_ANY, FETCH_BIN, FETCH_BUILD, FETCH_COUNT = range(4)
36 FETCH_ANY: 'any method',
37 FETCH_BIN: 'binary download',
38 FETCH_BUILD: 'build from source'
41 # Status of tool fetching
42 FETCHED, FAIL, PRESENT, STATUS_COUNT = range(4)
44 DOWNLOAD_DESTDIR = os.path.join(os.getenv('HOME'), 'bin')
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 def __init__(self, name, desc):
59 def find_bintool_class(btype):
60 """Look up the bintool class for bintool
63 byte: Bintool to use, e.g. 'mkimage'
66 The bintool class object if found, else a tuple:
67 module name that could not be found
70 # Convert something like 'u-boot' to 'u_boot' since we are only
71 # interested in the type.
72 module_name = btype.replace('-', '_')
73 module = modules.get(module_name)
75 # Import the module if we have not already done so
78 module = importlib.import_module('binman.btool.' + module_name)
79 except ImportError as exc:
80 return module_name, exc
81 modules[module_name] = module
83 # Look up the expected class name
84 return getattr(module, 'Bintool%s' % module_name)
88 """Create a new bintool object
91 name (str): Bintool to create, e.g. 'mkimage'
94 A new object of the correct type (a subclass of Binutil)
96 cls = Bintool.find_bintool_class(name)
97 if isinstance(cls, tuple):
98 raise ValueError("Cannot import bintool module '%s': %s" % cls)
100 # Call its constructor to get the object we want.
105 """Show a line of information about a bintool"""
106 if self.is_present():
107 version = self.version()
110 print(FORMAT % (self.name, version, self.desc,
111 self.get_path() or '(not found)'))
114 def set_missing_list(cls, missing_list):
115 cls.missing_list = missing_list or []
118 def get_tool_list(include_testing=False):
119 """Get a list of the known tools
122 list of str: names of all tools known to binman
124 files = glob.glob(os.path.join(BINMAN_DIR, 'btool/*'))
125 names = [os.path.splitext(os.path.basename(fname))[0]
127 names = [name for name in names if name[0] != '_']
129 names.append('_testing')
134 """List all the bintools known to binman"""
135 names = Bintool.get_tool_list()
136 print(FORMAT % ('Name', 'Version', 'Description', 'Path'))
137 print(FORMAT % ('-' * 15,'-' * 11, '-' * 25, '-' * 30))
139 btool = Bintool.create(name)
142 def is_present(self):
143 """Check if a bintool is available on the system
146 bool: True if available, False if not
148 if self.name in self.missing_list:
150 return bool(self.get_path())
153 """Get the path of a bintool
156 str: Path to the tool, if available, else None
158 return tools.tool_find(self.name)
160 def fetch_tool(self, method, col, skip_present):
161 """Fetch a single tool
164 method (FETCH_...): Method to use
165 col (terminal.Color): Color terminal object
166 skip_present (boo;): Skip fetching if it is already present
169 int: Result of fetch either FETCHED, FAIL, PRESENT
174 res = self.fetch(meth)
175 except urllib.error.URLError as uerr:
176 message = uerr.reason
177 print(col.Color(col.RED, f'- {message}'))
179 except ValueError as exc:
180 print(f'Exception: {exc}')
183 if skip_present and self.is_present():
185 print(col.Color(col.YELLOW, 'Fetch: %s' % self.name))
186 if method == FETCH_ANY:
187 for try_method in range(1, FETCH_COUNT):
188 print(f'- trying method: {FETCH_NAMES[try_method]}')
189 result = try_fetch(try_method)
193 result = try_fetch(method)
196 if result is not True:
197 fname, tmpdir = result
198 dest = os.path.join(DOWNLOAD_DESTDIR, self.name)
199 print(f"- writing to '{dest}'")
200 shutil.move(fname, dest)
202 shutil.rmtree(tmpdir)
206 def fetch_tools(method, names_to_fetch):
207 """Fetch bintools from a suitable place
209 This fetches or builds the requested bintools so that they can be used
213 names_to_fetch (list of str): names of bintools to fetch
216 True on success, False on failure
218 def show_status(color, prompt, names):
220 color, f'{prompt}:%s{len(names):2}: %s' %
221 (' ' * (16 - len(prompt)), ' '.join(names))))
223 col = terminal.Color()
225 name_list = names_to_fetch
226 if len(names_to_fetch) == 1 and names_to_fetch[0] in ['all', 'missing']:
227 name_list = Bintool.get_tool_list()
228 if names_to_fetch[0] == 'missing':
230 print(col.Color(col.YELLOW,
231 'Fetching tools: %s' % ' '.join(name_list)))
232 status = collections.defaultdict(list)
233 for name in name_list:
234 btool = Bintool.create(name)
235 result = btool.fetch_tool(method, col, skip_present)
236 status[result].append(name)
238 if method == FETCH_ANY:
239 print('- failed to fetch with all methods')
241 print(f"- method '{FETCH_NAMES[method]}' is not supported")
243 if len(name_list) > 1:
245 show_status(col.GREEN, 'Already present', status[PRESENT])
246 show_status(col.GREEN, 'Tools fetched', status[FETCHED])
248 show_status(col.RED, 'Failures', status[FAIL])
249 return not status[FAIL]
251 def run_cmd_result(self, *args, binary=False, raise_on_error=True):
252 """Run the bintool using command-line arguments
255 args (list of str): Arguments to provide, in addition to the bintool
257 binary (bool): True to return output as bytes instead of str
258 raise_on_error (bool): True to raise a ValueError exception if the
259 tool returns a non-zero return code
262 CommandResult: Resulting output from the bintool, or None if the
265 if self.name in self.missing_list:
267 name = os.path.expanduser(self.name) # Expand paths containing ~
268 all_args = (name,) + args
269 env = tools.get_env_with_path()
270 tout.Detail(f"bintool: {' '.join(all_args)}")
271 result = command.run_pipe(
272 [all_args], capture=True, capture_stderr=True, env=env,
273 raise_on_error=False, binary=binary)
275 if result.return_code:
276 # Return None if the tool was not found. In this case there is no
277 # output from the tool and it does not appear on the path. We still
278 # try to run it (as above) since RunPipe() allows faking the tool's
280 if not any([result.stdout, result.stderr, tools.tool_find(name)]):
281 tout.Info(f"bintool '{name}' not found")
284 tout.Info(f"bintool '{name}' failed")
285 raise ValueError("Error %d running '%s': %s" %
286 (result.return_code, ' '.join(all_args),
287 result.stderr or result.stdout))
289 tout.Debug(result.stdout)
291 tout.Debug(result.stderr)
294 def run_cmd(self, *args, binary=False):
295 """Run the bintool using command-line arguments
298 args (list of str): Arguments to provide, in addition to the bintool
300 binary (bool): True to return output as bytes instead of str
303 str or bytes: Resulting stdout from the bintool
305 result = self.run_cmd_result(*args, binary=binary)
310 def build_from_git(cls, git_repo, make_target, bintool_path):
311 """Build a bintool from a git repo
313 This clones the repo in a temporary directory, builds it with 'make',
314 then returns the filename of the resulting executable bintool
317 git_repo (str): URL of git repo
318 make_target (str): Target to pass to 'make' to build the tool
319 bintool_path (str): Relative path of the tool in the repo, after
324 str: Filename of fetched file to copy to a suitable directory
325 str: Name of temp directory to remove, or None
328 tmpdir = tempfile.mkdtemp(prefix='binmanf.')
329 print(f"- clone git repo '{git_repo}' to '{tmpdir}'")
330 tools.run('git', 'clone', '--depth', '1', git_repo, tmpdir)
331 print(f"- build target '{make_target}'")
332 tools.run('make', '-C', tmpdir, '-j', f'{multiprocessing.cpu_count()}',
334 fname = os.path.join(tmpdir, bintool_path)
335 if not os.path.exists(fname):
336 print(f"- File '{fname}' was not produced")
341 def fetch_from_url(cls, url):
342 """Fetch a bintool from a URL
345 url (str): URL to fetch from
349 str: Filename of fetched file to copy to a suitable directory
350 str: Name of temp directory to remove, or None
352 fname, tmpdir = tools.download(url)
353 tools.run('chmod', 'a+x', fname)
357 def fetch_from_drive(cls, drive_id):
358 """Fetch a bintool from Google drive
361 drive_id (str): ID of file to fetch. For a URL of the form
362 'https://drive.google.com/file/d/xxx/view?usp=sharing' the value
363 passed here should be 'xxx'
367 str: Filename of fetched file to copy to a suitable directory
368 str: Name of temp directory to remove, or None
370 url = f'https://drive.google.com/uc?export=download&id={drive_id}'
371 return cls.fetch_from_url(url)
374 def apt_install(cls, package):
375 """Install a bintool using the 'aot' tool
377 This requires use of servo so may request a password
380 package (str): Name of package to install
383 True, assuming it completes without error
385 args = ['sudo', 'apt', 'install', '-y', package]
386 print('- %s' % ' '.join(args))
391 def WriteDocs(modules, test_missing=None):
392 """Write out documentation about the various bintools to stdout
395 modules: List of modules to include
396 test_missing: Used for testing. This is a module to report
399 print('''.. SPDX-License-Identifier: GPL-2.0+
401 Binman bintool Documentation
402 ============================
404 This file describes the bintools (binary tools) supported by binman. Bintools
405 are binman's name for external executables that it runs to generate or process
406 binaries. It is fairly easy to create new bintools. Just add a new file to the
407 'btool' directory. You can use existing bintools as examples.
411 modules = sorted(modules)
414 module = Bintool.find_bintool_class(name)
415 docs = getattr(module, '__doc__')
416 if test_missing == name:
419 lines = docs.splitlines()
420 first_line = lines[0]
421 rest = [line[4:] for line in lines[1:]]
422 hdr = 'Bintool: %s: %s' % (name, first_line)
424 print('-' * len(hdr))
425 print('\n'.join(rest))
432 raise ValueError('Documentation is missing for modules: %s' %
435 # pylint: disable=W0613
436 def fetch(self, method):
437 """Fetch handler for a bintool
439 This should be implemented by the base class
442 method (FETCH_...): Method to use
446 str: Filename of fetched file to copy to a suitable directory
447 str: Name of temp directory to remove, or None
448 or True if the file was fetched and already installed
449 or None if no fetch() implementation is available
452 Valuerror: Fetching could not be completed
454 print(f"No method to fetch bintool '{self.name}'")
457 # pylint: disable=R0201
459 """Version handler for a bintool
461 This should be implemented by the base class
464 str: Version string for this bintool