]>
git.ipfire.org Git - thirdparty/u-boot.git/blob - tools/binman/bintool.py
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 patman
import command
22 from patman
import terminal
23 from patman
import tools
24 from patman
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)
46 DOWNLOAD_DESTDIR
= os
.path
.join(os
.getenv('HOME'), 'bin')
49 """Tool which operates on binaries to help produce entry contents
51 This is the base class for all bintools
53 # List of bintools to regard as missing
56 def __init__(self
, name
, desc
, version_regex
=None, version_args
='-V'):
59 self
.version_regex
= version_regex
60 self
.version_args
= version_args
63 def find_bintool_class(btype
):
64 """Look up the bintool class for bintool
67 byte: Bintool to use, e.g. 'mkimage'
70 The bintool class object if found, else a tuple:
71 module name that could not be found
74 # Convert something like 'u-boot' to 'u_boot' since we are only
75 # interested in the type.
76 module_name
= btype
.replace('-', '_')
77 module
= modules
.get(module_name
)
78 class_name
= f
'Bintool{module_name}'
80 # Import the module if we have not already done so
83 module
= importlib
.import_module('binman.btool.' + module_name
)
84 except ImportError as exc
:
86 # Deal with classes which must be renamed due to conflicts
87 # with Python libraries
88 module
= importlib
.import_module('binman.btool.btool_' +
91 return module_name
, exc
92 modules
[module_name
] = module
94 # Look up the expected class name
95 return getattr(module
, class_name
)
99 """Create a new bintool object
102 name (str): Bintool to create, e.g. 'mkimage'
105 A new object of the correct type (a subclass of Binutil)
107 cls
= Bintool
.find_bintool_class(name
)
108 if isinstance(cls
, tuple):
109 raise ValueError("Cannot import bintool module '%s': %s" % cls
)
111 # Call its constructor to get the object we want.
116 """Show a line of information about a bintool"""
117 if self
.is_present():
118 version
= self
.version()
121 print(FORMAT
% (self
.name
, version
, self
.desc
,
122 self
.get_path() or '(not found)'))
125 def set_missing_list(cls
, missing_list
):
126 cls
.missing_list
= missing_list
or []
129 def get_tool_list(include_testing
=False):
130 """Get a list of the known tools
133 list of str: names of all tools known to binman
135 files
= glob
.glob(os
.path
.join(BINMAN_DIR
, 'btool/*'))
136 names
= [os
.path
.splitext(os
.path
.basename(fname
))[0]
138 names
= [name
for name
in names
if name
[0] != '_']
139 names
= [name
[6:] if name
.startswith('btool_') else name
142 names
.append('_testing')
147 """List all the bintools known to binman"""
148 names
= Bintool
.get_tool_list()
149 print(FORMAT
% ('Name', 'Version', 'Description', 'Path'))
150 print(FORMAT
% ('-' * 15,'-' * 11, '-' * 25, '-' * 30))
152 btool
= Bintool
.create(name
)
155 def is_present(self
):
156 """Check if a bintool is available on the system
159 bool: True if available, False if not
161 if self
.name
in self
.missing_list
:
163 return bool(self
.get_path())
166 """Get the path of a bintool
169 str: Path to the tool, if available, else None
171 return tools
.tool_find(self
.name
)
173 def fetch_tool(self
, method
, col
, skip_present
):
174 """Fetch a single tool
177 method (FETCH_...): Method to use
178 col (terminal.Color): Color terminal object
179 skip_present (boo;): Skip fetching if it is already present
182 int: Result of fetch either FETCHED, FAIL, PRESENT
187 res
= self
.fetch(meth
)
188 except urllib
.error
.URLError
as uerr
:
189 message
= uerr
.reason
190 print(col
.build(col
.RED
, f
'- {message}'))
192 except ValueError as exc
:
193 print(f
'Exception: {exc}')
196 if skip_present
and self
.is_present():
198 print(col
.build(col
.YELLOW
, 'Fetch: %s' % self
.name
))
199 if method
== FETCH_ANY
:
200 for try_method
in range(1, FETCH_COUNT
):
201 print(f
'- trying method: {FETCH_NAMES[try_method]}')
202 result
= try_fetch(try_method
)
206 result
= try_fetch(method
)
209 if result
is not True:
210 fname
, tmpdir
= result
211 dest
= os
.path
.join(DOWNLOAD_DESTDIR
, self
.name
)
212 print(f
"- writing to '{dest}'")
213 shutil
.move(fname
, dest
)
215 shutil
.rmtree(tmpdir
)
219 def fetch_tools(method
, names_to_fetch
):
220 """Fetch bintools from a suitable place
222 This fetches or builds the requested bintools so that they can be used
226 names_to_fetch (list of str): names of bintools to fetch
229 True on success, False on failure
231 def show_status(color
, prompt
, names
):
233 color
, f
'{prompt}:%s{len(names):2}: %s' %
234 (' ' * (16 - len(prompt
)), ' '.join(names
))))
236 col
= terminal
.Color()
238 name_list
= names_to_fetch
239 if len(names_to_fetch
) == 1 and names_to_fetch
[0] in ['all', 'missing']:
240 name_list
= Bintool
.get_tool_list()
241 if names_to_fetch
[0] == 'missing':
243 print(col
.build(col
.YELLOW
,
244 'Fetching tools: %s' % ' '.join(name_list
)))
245 status
= collections
.defaultdict(list)
246 for name
in name_list
:
247 btool
= Bintool
.create(name
)
248 result
= btool
.fetch_tool(method
, col
, skip_present
)
249 status
[result
].append(name
)
251 if method
== FETCH_ANY
:
252 print('- failed to fetch with all methods')
254 print(f
"- method '{FETCH_NAMES[method]}' is not supported")
256 if len(name_list
) > 1:
258 show_status(col
.GREEN
, 'Already present', status
[PRESENT
])
259 show_status(col
.GREEN
, 'Tools fetched', status
[FETCHED
])
261 show_status(col
.RED
, 'Failures', status
[FAIL
])
262 return not status
[FAIL
]
264 def run_cmd_result(self
, *args
, binary
=False, raise_on_error
=True):
265 """Run the bintool using command-line arguments
268 args (list of str): Arguments to provide, in addition to the bintool
270 binary (bool): True to return output as bytes instead of str
271 raise_on_error (bool): True to raise a ValueError exception if the
272 tool returns a non-zero return code
275 CommandResult: Resulting output from the bintool, or None if the
278 if self
.name
in self
.missing_list
:
280 name
= os
.path
.expanduser(self
.name
) # Expand paths containing ~
281 all_args
= (name
,) + args
282 env
= tools
.get_env_with_path()
283 tout
.detail(f
"bintool: {' '.join(all_args)}")
284 result
= command
.run_pipe(
285 [all_args
], capture
=True, capture_stderr
=True, env
=env
,
286 raise_on_error
=False, binary
=binary
)
288 if result
.return_code
:
289 # Return None if the tool was not found. In this case there is no
290 # output from the tool and it does not appear on the path. We still
291 # try to run it (as above) since RunPipe() allows faking the tool's
293 if not any([result
.stdout
, result
.stderr
, tools
.tool_find(name
)]):
294 tout
.info(f
"bintool '{name}' not found")
297 tout
.info(f
"bintool '{name}' failed")
298 raise ValueError("Error %d running '%s': %s" %
299 (result
.return_code
, ' '.join(all_args
),
300 result
.stderr
or result
.stdout
))
302 tout
.debug(result
.stdout
)
304 tout
.debug(result
.stderr
)
307 def run_cmd(self
, *args
, binary
=False):
308 """Run the bintool using command-line arguments
311 args (list of str): Arguments to provide, in addition to the bintool
313 binary (bool): True to return output as bytes instead of str
316 str or bytes: Resulting stdout from the bintool
318 result
= self
.run_cmd_result(*args
, binary
=binary
)
323 def build_from_git(cls
, git_repo
, make_target
, bintool_path
, flags
=None):
324 """Build a bintool from a git repo
326 This clones the repo in a temporary directory, builds it with 'make',
327 then returns the filename of the resulting executable bintool
330 git_repo (str): URL of git repo
331 make_target (str): Target to pass to 'make' to build the tool
332 bintool_path (str): Relative path of the tool in the repo, after
334 flags (list of str): Flags or variables to pass to make, or None
338 str: Filename of fetched file to copy to a suitable directory
339 str: Name of temp directory to remove, or None
342 tmpdir
= tempfile
.mkdtemp(prefix
='binmanf.')
343 print(f
"- clone git repo '{git_repo}' to '{tmpdir}'")
344 tools
.run('git', 'clone', '--depth', '1', git_repo
, tmpdir
)
345 print(f
"- build target '{make_target}'")
346 cmd
= ['make', '-C', tmpdir
, '-j', f
'{multiprocessing.cpu_count()}',
351 fname
= os
.path
.join(tmpdir
, bintool_path
)
352 if not os
.path
.exists(fname
):
353 print(f
"- File '{fname}' was not produced")
358 def fetch_from_url(cls
, url
):
359 """Fetch a bintool from a URL
362 url (str): URL to fetch from
366 str: Filename of fetched file to copy to a suitable directory
367 str: Name of temp directory to remove, or None
369 fname
, tmpdir
= tools
.download(url
)
370 tools
.run('chmod', 'a+x', fname
)
374 def fetch_from_drive(cls
, drive_id
):
375 """Fetch a bintool from Google drive
378 drive_id (str): ID of file to fetch. For a URL of the form
379 'https://drive.google.com/file/d/xxx/view?usp=sharing' the value
380 passed here should be 'xxx'
384 str: Filename of fetched file to copy to a suitable directory
385 str: Name of temp directory to remove, or None
387 url
= f
'https://drive.google.com/uc?export=download&id={drive_id}'
388 return cls
.fetch_from_url(url
)
391 def apt_install(cls
, package
):
392 """Install a bintool using the 'aot' tool
394 This requires use of servo so may request a password
397 package (str): Name of package to install
400 True, assuming it completes without error
402 args
= ['sudo', 'apt', 'install', '-y', package
]
403 print('- %s' % ' '.join(args
))
408 def WriteDocs(modules
, test_missing
=None):
409 """Write out documentation about the various bintools to stdout
412 modules: List of modules to include
413 test_missing: Used for testing. This is a module to report
416 print('''.. SPDX-License-Identifier: GPL-2.0+
418 Binman bintool Documentation
419 ============================
421 This file describes the bintools (binary tools) supported by binman. Bintools
422 are binman's name for external executables that it runs to generate or process
423 binaries. It is fairly easy to create new bintools. Just add a new file to the
424 'btool' directory. You can use existing bintools as examples.
428 modules
= sorted(modules
)
431 module
= Bintool
.find_bintool_class(name
)
432 docs
= getattr(module
, '__doc__')
433 if test_missing
== name
:
436 lines
= docs
.splitlines()
437 first_line
= lines
[0]
438 rest
= [line
[4:] for line
in lines
[1:]]
439 hdr
= 'Bintool: %s: %s' % (name
, first_line
)
441 print('-' * len(hdr
))
442 print('\n'.join(rest
))
449 raise ValueError('Documentation is missing for modules: %s' %
452 # pylint: disable=W0613
453 def fetch(self
, method
):
454 """Fetch handler for a bintool
456 This should be implemented by the base class
459 method (FETCH_...): Method to use
463 str: Filename of fetched file to copy to a suitable directory
464 str: Name of temp directory to remove, or None
465 or True if the file was fetched and already installed
466 or None if no fetch() implementation is available
469 Valuerror: Fetching could not be completed
471 print(f
"No method to fetch bintool '{self.name}'")
475 """Version handler for a bintool
478 str: Version string for this bintool
480 if self
.version_regex
is None:
485 result
= self
.run_cmd_result(self
.version_args
)
486 out
= result
.stdout
.strip()
488 out
= result
.stderr
.strip()
492 m_version
= re
.search(self
.version_regex
, out
)
493 return m_version
.group(1) if m_version
else out
496 class BintoolPacker(Bintool
):
497 """Tool which compression / decompression entry contents
499 This is a bintools base class for compression / decompression packer
502 name: Name of packer tool
503 compression: Compression type (COMPRESS_...), value of 'name' property
505 compress_args: List of positional args provided to tool for compress,
506 ['--compress'] if none
507 decompress_args: List of positional args provided to tool for
508 decompress, ['--decompress'] if none
509 fetch_package: Name of the tool installed using the apt, value of 'name'
511 version_regex: Regular expressions to extract the version from tool
512 version output, '(v[0-9.]+)' if none
514 def __init__(self
, name
, compression
=None, compress_args
=None,
515 decompress_args
=None, fetch_package
=None,
516 version_regex
=r
'(v[0-9.]+)', version_args
='-V'):
517 desc
= '%s compression' % (compression
if compression
else name
)
518 super().__init
__(name
, desc
, version_regex
, version_args
)
519 if compress_args
is None:
520 compress_args
= ['--compress']
521 self
.compress_args
= compress_args
522 if decompress_args
is None:
523 decompress_args
= ['--decompress']
524 self
.decompress_args
= decompress_args
525 if fetch_package
is None:
527 self
.fetch_package
= fetch_package
529 def compress(self
, indata
):
533 indata (bytes): Data to compress
536 bytes: Compressed data
538 with tempfile
.NamedTemporaryFile(prefix
='comp.tmp',
539 dir=tools
.get_output_dir()) as tmp
:
540 tools
.write_file(tmp
.name
, indata
)
541 args
= self
.compress_args
+ ['--stdout', tmp
.name
]
542 return self
.run_cmd(*args
, binary
=True)
544 def decompress(self
, indata
):
548 indata (bytes): Data to decompress
551 bytes: Decompressed data
553 with tempfile
.NamedTemporaryFile(prefix
='decomp.tmp',
554 dir=tools
.get_output_dir()) as inf
:
555 tools
.write_file(inf
.name
, indata
)
556 args
= self
.decompress_args
+ ['--stdout', inf
.name
]
557 return self
.run_cmd(*args
, binary
=True)
559 def fetch(self
, method
):
562 This installs the gzip package using the apt utility.
565 method (FETCH_...): Method to use
568 True if the file was fetched and now installed, None if a method
569 other than FETCH_BIN was requested
572 Valuerror: Fetching could not be completed
574 if method
!= FETCH_BIN
:
576 return self
.apt_install(self
.fetch_package
)