blob: 34102dafa2a84b716648a5f66369fb074550375f [file] [log] [blame]
Simon Glass861fbbf2022-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
177 print(col.Color(col.RED, f'- {message}'))
178
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
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)
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):
219 print(col.Color(
220 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
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)
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()
270 tout.Detail(f"bintool: {' '.join(all_args)}")
271 result = command.RunPipe(
272 [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)]):
281 tout.Info(f"bintool '{name}' not found")
282 return None
283 if raise_on_error:
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))
288 if result.stdout:
289 tout.Debug(result.stdout)
290 if result.stderr:
291 tout.Debug(result.stderr)
292 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}'")
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()}',
333 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 """
352 fname, tmpdir = tools.Download(url)
353 tools.Run('chmod', 'a+x', fname)
354 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))
387 tools.Run(*args)
388 return True
389
390 # pylint: disable=W0613
391 def fetch(self, method):
392 """Fetch handler for a bintool
393
394 This should be implemented by the base class
395
396 Args:
397 method (FETCH_...): Method to use
398
399 Returns:
400 tuple:
401 str: Filename of fetched file to copy to a suitable directory
402 str: Name of temp directory to remove, or None
403 or True if the file was fetched and already installed
404 or None if no fetch() implementation is available
405
406 Raises:
407 Valuerror: Fetching could not be completed
408 """
409 print(f"No method to fetch bintool '{self.name}'")
410 return False
411
412 # pylint: disable=R0201
413 def version(self):
414 """Version handler for a bintool
415
416 This should be implemented by the base class
417
418 Returns:
419 str: Version string for this bintool
420 """
421 return 'unknown'