blob: 8435b29749bcb8c96ed9f5557933736962c45bfa [file] [log] [blame]
Simon Glass252de6b2022-01-09 20:13:49 -07001# SPDX-License-Identifier: GPL-2.0+
2# Copyright 2022 Google LLC
3#
4"""Base class for all bintools
5
6This defines the common functionality for all bintools, including running
7the tool, checking its version and fetching it if needed.
8"""
9
10import collections
11import glob
12import importlib
13import multiprocessing
14import os
15import shutil
16import tempfile
17import urllib.error
18
19from patman import command
20from patman import terminal
21from patman import tools
22from patman import tout
23
24BINMAN_DIR = os.path.dirname(os.path.realpath(__file__))
25
26# Format string for listing bintools, see also the header in list_all()
27FORMAT = '%-16.16s %-12.12s %-26.26s %s'
28
29# List of known modules, to avoid importing the module multiple times
30modules = {}
31
32# Possible ways of fetching a tool (FETCH_COUNT is number of ways)
33FETCH_ANY, FETCH_BIN, FETCH_BUILD, FETCH_COUNT = range(4)
34
35FETCH_NAMES = {
36 FETCH_ANY: 'any method',
37 FETCH_BIN: 'binary download',
38 FETCH_BUILD: 'build from source'
39 }
40
41# Status of tool fetching
42FETCHED, FAIL, PRESENT, STATUS_COUNT = range(4)
43
44DOWNLOAD_DESTDIR = os.path.join(os.getenv('HOME'), 'bin')
45
46class 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
54 def __init__(self, name, desc):
55 self.name = name
56 self.desc = desc
57
58 @staticmethod
59 def find_bintool_class(btype):
60 """Look up the bintool class for bintool
61
62 Args:
63 byte: Bintool to use, e.g. 'mkimage'
64
65 Returns:
66 The bintool class object if found, else a tuple:
67 module name that could not be found
68 exception received
69 """
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)
74
75 # Import the module if we have not already done so
76 if not module:
77 try:
78 module = importlib.import_module('binman.btool.' + module_name)
79 except ImportError as exc:
80 return module_name, exc
81 modules[module_name] = module
82
83 # Look up the expected class name
84 return getattr(module, 'Bintool%s' % module_name)
85
86 @staticmethod
87 def create(name):
88 """Create a new bintool object
89
90 Args:
91 name (str): Bintool to create, e.g. 'mkimage'
92
93 Returns:
94 A new object of the correct type (a subclass of Binutil)
95 """
96 cls = Bintool.find_bintool_class(name)
97 if isinstance(cls, tuple):
98 raise ValueError("Cannot import bintool module '%s': %s" % cls)
99
100 # Call its constructor to get the object we want.
101 obj = cls(name)
102 return obj
103
104 def show(self):
105 """Show a line of information about a bintool"""
106 if self.is_present():
107 version = self.version()
108 else:
109 version = '-'
110 print(FORMAT % (self.name, version, self.desc,
111 self.get_path() or '(not found)'))
112
113 @classmethod
114 def set_missing_list(cls, missing_list):
115 cls.missing_list = missing_list or []
116
117 @staticmethod
118 def get_tool_list(include_testing=False):
119 """Get a list of the known tools
120
121 Returns:
122 list of str: names of all tools known to binman
123 """
124 files = glob.glob(os.path.join(BINMAN_DIR, 'btool/*'))
125 names = [os.path.splitext(os.path.basename(fname))[0]
126 for fname in files]
127 names = [name for name in names if name[0] != '_']
128 if include_testing:
129 names.append('_testing')
130 return sorted(names)
131
132 @staticmethod
133 def list_all():
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))
138 for name in names:
139 btool = Bintool.create(name)
140 btool.show()
141
142 def is_present(self):
143 """Check if a bintool is available on the system
144
145 Returns:
146 bool: True if available, False if not
147 """
148 if self.name in self.missing_list:
149 return False
150 return bool(self.get_path())
151
152 def get_path(self):
153 """Get the path of a bintool
154
155 Returns:
156 str: Path to the tool, if available, else None
157 """
158 return tools.tool_find(self.name)
159
160 def fetch_tool(self, method, col, skip_present):
161 """Fetch a single tool
162
163 Args:
164 method (FETCH_...): Method to use
165 col (terminal.Color): Color terminal object
166 skip_present (boo;): Skip fetching if it is already present
167
168 Returns:
169 int: Result of fetch either FETCHED, FAIL, PRESENT
170 """
171 def try_fetch(meth):
172 res = None
173 try:
174 res = self.fetch(meth)
175 except urllib.error.URLError as uerr:
176 message = uerr.reason
Simon Glass252ac582022-01-29 14:14:17 -0700177 print(col.build(col.RED, f'- {message}'))
Simon Glass252de6b2022-01-09 20:13:49 -0700178
179 except ValueError as exc:
180 print(f'Exception: {exc}')
181 return res
182
183 if skip_present and self.is_present():
184 return PRESENT
Simon Glass252ac582022-01-29 14:14:17 -0700185 print(col.build(col.YELLOW, 'Fetch: %s' % self.name))
Simon Glass252de6b2022-01-09 20:13:49 -0700186 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)
190 if result:
191 break
192 else:
193 result = try_fetch(method)
194 if not result:
195 return FAIL
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)
201 if tmpdir:
202 shutil.rmtree(tmpdir)
203 return FETCHED
204
205 @staticmethod
206 def fetch_tools(method, names_to_fetch):
207 """Fetch bintools from a suitable place
208
209 This fetches or builds the requested bintools so that they can be used
210 by binman
211
212 Args:
213 names_to_fetch (list of str): names of bintools to fetch
214
215 Returns:
216 True on success, False on failure
217 """
218 def show_status(color, prompt, names):
Simon Glass252ac582022-01-29 14:14:17 -0700219 print(col.build(
Simon Glass252de6b2022-01-09 20:13:49 -0700220 color, f'{prompt}:%s{len(names):2}: %s' %
221 (' ' * (16 - len(prompt)), ' '.join(names))))
222
223 col = terminal.Color()
224 skip_present = False
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':
229 skip_present = True
Simon Glass252ac582022-01-29 14:14:17 -0700230 print(col.build(col.YELLOW,
Simon Glass252de6b2022-01-09 20:13:49 -0700231 '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)
237 if result == FAIL:
238 if method == FETCH_ANY:
239 print('- failed to fetch with all methods')
240 else:
241 print(f"- method '{FETCH_NAMES[method]}' is not supported")
242
243 if len(name_list) > 1:
244 if skip_present:
245 show_status(col.GREEN, 'Already present', status[PRESENT])
246 show_status(col.GREEN, 'Tools fetched', status[FETCHED])
247 if status[FAIL]:
248 show_status(col.RED, 'Failures', status[FAIL])
249 return not status[FAIL]
250
251 def run_cmd_result(self, *args, binary=False, raise_on_error=True):
252 """Run the bintool using command-line arguments
253
254 Args:
255 args (list of str): Arguments to provide, in addition to the bintool
256 name
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
260
261 Returns:
262 CommandResult: Resulting output from the bintool, or None if the
263 tool is not present
264 """
265 if self.name in self.missing_list:
266 return None
267 name = os.path.expanduser(self.name) # Expand paths containing ~
268 all_args = (name,) + args
269 env = tools.get_env_with_path()
Simon Glassf3385a52022-01-29 14:14:15 -0700270 tout.detail(f"bintool: {' '.join(all_args)}")
Simon Glassd9800692022-01-29 14:14:05 -0700271 result = command.run_pipe(
Simon Glass252de6b2022-01-09 20:13:49 -0700272 [all_args], capture=True, capture_stderr=True, env=env,
273 raise_on_error=False, binary=binary)
274
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
279 # output
280 if not any([result.stdout, result.stderr, tools.tool_find(name)]):
Simon Glassf3385a52022-01-29 14:14:15 -0700281 tout.info(f"bintool '{name}' not found")
Simon Glass252de6b2022-01-09 20:13:49 -0700282 return None
283 if raise_on_error:
Simon Glassf3385a52022-01-29 14:14:15 -0700284 tout.info(f"bintool '{name}' failed")
Simon Glass252de6b2022-01-09 20:13:49 -0700285 raise ValueError("Error %d running '%s': %s" %
286 (result.return_code, ' '.join(all_args),
287 result.stderr or result.stdout))
288 if result.stdout:
Simon Glassf3385a52022-01-29 14:14:15 -0700289 tout.debug(result.stdout)
Simon Glass252de6b2022-01-09 20:13:49 -0700290 if result.stderr:
Simon Glassf3385a52022-01-29 14:14:15 -0700291 tout.debug(result.stderr)
Simon Glass252de6b2022-01-09 20:13:49 -0700292 return result
293
294 def run_cmd(self, *args, binary=False):
295 """Run the bintool using command-line arguments
296
297 Args:
298 args (list of str): Arguments to provide, in addition to the bintool
299 name
300 binary (bool): True to return output as bytes instead of str
301
302 Returns:
303 str or bytes: Resulting stdout from the bintool
304 """
305 result = self.run_cmd_result(*args, binary=binary)
306 if result:
307 return result.stdout
308
309 @classmethod
310 def build_from_git(cls, git_repo, make_target, bintool_path):
311 """Build a bintool from a git repo
312
313 This clones the repo in a temporary directory, builds it with 'make',
314 then returns the filename of the resulting executable bintool
315
316 Args:
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
320 build is complete
321
322 Returns:
323 tuple:
324 str: Filename of fetched file to copy to a suitable directory
325 str: Name of temp directory to remove, or None
326 or None on error
327 """
328 tmpdir = tempfile.mkdtemp(prefix='binmanf.')
329 print(f"- clone git repo '{git_repo}' to '{tmpdir}'")
Simon Glassc1aa66e2022-01-29 14:14:04 -0700330 tools.run('git', 'clone', '--depth', '1', git_repo, tmpdir)
Simon Glass252de6b2022-01-09 20:13:49 -0700331 print(f"- build target '{make_target}'")
Simon Glassc1aa66e2022-01-29 14:14:04 -0700332 tools.run('make', '-C', tmpdir, '-j', f'{multiprocessing.cpu_count()}',
Simon Glass252de6b2022-01-09 20:13:49 -0700333 make_target)
334 fname = os.path.join(tmpdir, bintool_path)
335 if not os.path.exists(fname):
336 print(f"- File '{fname}' was not produced")
337 return None
338 return fname, tmpdir
339
340 @classmethod
341 def fetch_from_url(cls, url):
342 """Fetch a bintool from a URL
343
344 Args:
345 url (str): URL to fetch from
346
347 Returns:
348 tuple:
349 str: Filename of fetched file to copy to a suitable directory
350 str: Name of temp directory to remove, or None
351 """
Simon Glassc1aa66e2022-01-29 14:14:04 -0700352 fname, tmpdir = tools.download(url)
353 tools.run('chmod', 'a+x', fname)
Simon Glass252de6b2022-01-09 20:13:49 -0700354 return fname, tmpdir
355
356 @classmethod
357 def fetch_from_drive(cls, drive_id):
358 """Fetch a bintool from Google drive
359
360 Args:
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'
364
365 Returns:
366 tuple:
367 str: Filename of fetched file to copy to a suitable directory
368 str: Name of temp directory to remove, or None
369 """
370 url = f'https://drive.google.com/uc?export=download&id={drive_id}'
371 return cls.fetch_from_url(url)
372
373 @classmethod
374 def apt_install(cls, package):
375 """Install a bintool using the 'aot' tool
376
377 This requires use of servo so may request a password
378
379 Args:
380 package (str): Name of package to install
381
382 Returns:
383 True, assuming it completes without error
384 """
385 args = ['sudo', 'apt', 'install', '-y', package]
386 print('- %s' % ' '.join(args))
Simon Glassc1aa66e2022-01-29 14:14:04 -0700387 tools.run(*args)
Simon Glass252de6b2022-01-09 20:13:49 -0700388 return True
389
Simon Glassbc570642022-01-09 20:14:11 -0700390 @staticmethod
391 def WriteDocs(modules, test_missing=None):
392 """Write out documentation about the various bintools to stdout
393
394 Args:
395 modules: List of modules to include
396 test_missing: Used for testing. This is a module to report
397 as missing
398 """
399 print('''.. SPDX-License-Identifier: GPL-2.0+
400
401Binman bintool Documentation
402============================
403
404This file describes the bintools (binary tools) supported by binman. Bintools
405are binman's name for external executables that it runs to generate or process
406binaries. 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.
408
409
410''')
411 modules = sorted(modules)
412 missing = []
413 for name in modules:
414 module = Bintool.find_bintool_class(name)
415 docs = getattr(module, '__doc__')
416 if test_missing == name:
417 docs = None
418 if docs:
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)
423 print(hdr)
424 print('-' * len(hdr))
425 print('\n'.join(rest))
426 print()
427 print()
428 else:
429 missing.append(name)
430
431 if missing:
432 raise ValueError('Documentation is missing for modules: %s' %
433 ', '.join(missing))
434
Simon Glass252de6b2022-01-09 20:13:49 -0700435 # pylint: disable=W0613
436 def fetch(self, method):
437 """Fetch handler for a bintool
438
439 This should be implemented by the base class
440
441 Args:
442 method (FETCH_...): Method to use
443
444 Returns:
445 tuple:
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
450
451 Raises:
452 Valuerror: Fetching could not be completed
453 """
454 print(f"No method to fetch bintool '{self.name}'")
455 return False
456
457 # pylint: disable=R0201
458 def version(self):
459 """Version handler for a bintool
460
461 This should be implemented by the base class
462
463 Returns:
464 str: Version string for this bintool
465 """
466 return 'unknown'