Merge tag 'v2022.04-rc5' into next
[platform/kernel/u-boot.git] / tools / patman / tools.py
1 # SPDX-License-Identifier: GPL-2.0+
2 #
3 # Copyright (c) 2016 Google, Inc
4 #
5
6 import glob
7 import os
8 import shlex
9 import shutil
10 import sys
11 import tempfile
12 import urllib.request
13
14 from patman import command
15 from patman import tout
16
17 # Output directly (generally this is temporary)
18 outdir = None
19
20 # True to keep the output directory around after exiting
21 preserve_outdir = False
22
23 # Path to the Chrome OS chroot, if we know it
24 chroot_path = None
25
26 # Search paths to use for filename(), used to find files
27 search_paths = []
28
29 tool_search_paths = []
30
31 # Tools and the packages that contain them, on debian
32 packages = {
33     'lz4': 'liblz4-tool',
34     }
35
36 # List of paths to use when looking for an input file
37 indir = []
38
39 def prepare_output_dir(dirname, preserve=False):
40     """Select an output directory, ensuring it exists.
41
42     This either creates a temporary directory or checks that the one supplied
43     by the user is valid. For a temporary directory, it makes a note to
44     remove it later if required.
45
46     Args:
47         dirname: a string, name of the output directory to use to store
48                 intermediate and output files. If is None - create a temporary
49                 directory.
50         preserve: a Boolean. If outdir above is None and preserve is False, the
51                 created temporary directory will be destroyed on exit.
52
53     Raises:
54         OSError: If it cannot create the output directory.
55     """
56     global outdir, preserve_outdir
57
58     preserve_outdir = dirname or preserve
59     if dirname:
60         outdir = dirname
61         if not os.path.isdir(outdir):
62             try:
63                 os.makedirs(outdir)
64             except OSError as err:
65                 raise ValueError(
66                     f"Cannot make output directory 'outdir': 'err.strerror'")
67         tout.debug("Using output directory '%s'" % outdir)
68     else:
69         outdir = tempfile.mkdtemp(prefix='binman.')
70         tout.debug("Using temporary directory '%s'" % outdir)
71
72 def _remove_output_dir():
73     global outdir
74
75     shutil.rmtree(outdir)
76     tout.debug("Deleted temporary directory '%s'" % outdir)
77     outdir = None
78
79 def finalise_output_dir():
80     global outdir, preserve_outdir
81
82     """Tidy up: delete output directory if temporary and not preserved."""
83     if outdir and not preserve_outdir:
84         _remove_output_dir()
85         outdir = None
86
87 def get_output_filename(fname):
88     """Return a filename within the output directory.
89
90     Args:
91         fname: Filename to use for new file
92
93     Returns:
94         The full path of the filename, within the output directory
95     """
96     return os.path.join(outdir, fname)
97
98 def get_output_dir():
99     """Return the current output directory
100
101     Returns:
102         str: The output directory
103     """
104     return outdir
105
106 def _finalise_for_test():
107     """Remove the output directory (for use by tests)"""
108     global outdir
109
110     if outdir:
111         _remove_output_dir()
112         outdir = None
113
114 def set_input_dirs(dirname):
115     """Add a list of input directories, where input files are kept.
116
117     Args:
118         dirname: a list of paths to input directories to use for obtaining
119                 files needed by binman to place in the image.
120     """
121     global indir
122
123     indir = dirname
124     tout.debug("Using input directories %s" % indir)
125
126 def get_input_filename(fname, allow_missing=False):
127     """Return a filename for use as input.
128
129     Args:
130         fname: Filename to use for new file
131         allow_missing: True if the filename can be missing
132
133     Returns:
134         fname, if indir is None;
135         full path of the filename, within the input directory;
136         None, if file is missing and allow_missing is True
137
138     Raises:
139         ValueError if file is missing and allow_missing is False
140     """
141     if not indir or fname[:1] == '/':
142         return fname
143     for dirname in indir:
144         pathname = os.path.join(dirname, fname)
145         if os.path.exists(pathname):
146             return pathname
147
148     if allow_missing:
149         return None
150     raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
151                      (fname, ','.join(indir), os.getcwd()))
152
153 def get_input_filename_glob(pattern):
154     """Return a list of filenames for use as input.
155
156     Args:
157         pattern: Filename pattern to search for
158
159     Returns:
160         A list of matching files in all input directories
161     """
162     if not indir:
163         return glob.glob(pattern)
164     files = []
165     for dirname in indir:
166         pathname = os.path.join(dirname, pattern)
167         files += glob.glob(pathname)
168     return sorted(files)
169
170 def align(pos, align):
171     if align:
172         mask = align - 1
173         pos = (pos + mask) & ~mask
174     return pos
175
176 def not_power_of_two(num):
177     return num and (num & (num - 1))
178
179 def set_tool_paths(toolpaths):
180     """Set the path to search for tools
181
182     Args:
183         toolpaths: List of paths to search for tools executed by run()
184     """
185     global tool_search_paths
186
187     tool_search_paths = toolpaths
188
189 def path_has_file(path_spec, fname):
190     """Check if a given filename is in the PATH
191
192     Args:
193         path_spec: Value of PATH variable to check
194         fname: Filename to check
195
196     Returns:
197         True if found, False if not
198     """
199     for dir in path_spec.split(':'):
200         if os.path.exists(os.path.join(dir, fname)):
201             return True
202     return False
203
204 def get_host_compile_tool(env, name):
205     """Get the host-specific version for a compile tool
206
207     This checks the environment variables that specify which version of
208     the tool should be used (e.g. ${HOSTCC}).
209
210     The following table lists the host-specific versions of the tools
211     this function resolves to:
212
213         Compile Tool  | Host version
214         --------------+----------------
215         as            |  ${HOSTAS}
216         ld            |  ${HOSTLD}
217         cc            |  ${HOSTCC}
218         cpp           |  ${HOSTCPP}
219         c++           |  ${HOSTCXX}
220         ar            |  ${HOSTAR}
221         nm            |  ${HOSTNM}
222         ldr           |  ${HOSTLDR}
223         strip         |  ${HOSTSTRIP}
224         objcopy       |  ${HOSTOBJCOPY}
225         objdump       |  ${HOSTOBJDUMP}
226         dtc           |  ${HOSTDTC}
227
228     Args:
229         name: Command name to run
230
231     Returns:
232         host_name: Exact command name to run instead
233         extra_args: List of extra arguments to pass
234     """
235     host_name = None
236     extra_args = []
237     if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
238                 'objcopy', 'objdump', 'dtc'):
239         host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
240     elif name == 'c++':
241         host_name, *host_args = env.get('HOSTCXX', '').split(' ')
242
243     if host_name:
244         return host_name, extra_args
245     return name, []
246
247 def get_target_compile_tool(name, cross_compile=None):
248     """Get the target-specific version for a compile tool
249
250     This first checks the environment variables that specify which
251     version of the tool should be used (e.g. ${CC}). If those aren't
252     specified, it checks the CROSS_COMPILE variable as a prefix for the
253     tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
254
255     The following table lists the target-specific versions of the tools
256     this function resolves to:
257
258         Compile Tool  | First choice   | Second choice
259         --------------+----------------+----------------------------
260         as            |  ${AS}         | ${CROSS_COMPILE}as
261         ld            |  ${LD}         | ${CROSS_COMPILE}ld.bfd
262                       |                |   or ${CROSS_COMPILE}ld
263         cc            |  ${CC}         | ${CROSS_COMPILE}gcc
264         cpp           |  ${CPP}        | ${CROSS_COMPILE}gcc -E
265         c++           |  ${CXX}        | ${CROSS_COMPILE}g++
266         ar            |  ${AR}         | ${CROSS_COMPILE}ar
267         nm            |  ${NM}         | ${CROSS_COMPILE}nm
268         ldr           |  ${LDR}        | ${CROSS_COMPILE}ldr
269         strip         |  ${STRIP}      | ${CROSS_COMPILE}strip
270         objcopy       |  ${OBJCOPY}    | ${CROSS_COMPILE}objcopy
271         objdump       |  ${OBJDUMP}    | ${CROSS_COMPILE}objdump
272         dtc           |  ${DTC}        | (no CROSS_COMPILE version)
273
274     Args:
275         name: Command name to run
276
277     Returns:
278         target_name: Exact command name to run instead
279         extra_args: List of extra arguments to pass
280     """
281     env = dict(os.environ)
282
283     target_name = None
284     extra_args = []
285     if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
286                 'objcopy', 'objdump', 'dtc'):
287         target_name, *extra_args = env.get(name.upper(), '').split(' ')
288     elif name == 'c++':
289         target_name, *extra_args = env.get('CXX', '').split(' ')
290
291     if target_name:
292         return target_name, extra_args
293
294     if cross_compile is None:
295         cross_compile = env.get('CROSS_COMPILE', '')
296
297     if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
298         target_name = cross_compile + name
299     elif name == 'ld':
300         try:
301             if run(cross_compile + 'ld.bfd', '-v'):
302                 target_name = cross_compile + 'ld.bfd'
303         except:
304             target_name = cross_compile + 'ld'
305     elif name == 'cc':
306         target_name = cross_compile + 'gcc'
307     elif name == 'cpp':
308         target_name = cross_compile + 'gcc'
309         extra_args = ['-E']
310     elif name == 'c++':
311         target_name = cross_compile + 'g++'
312     else:
313         target_name = name
314     return target_name, extra_args
315
316 def get_env_with_path():
317     """Get an updated environment with the PATH variable set correctly
318
319     If there are any search paths set, these need to come first in the PATH so
320     that these override any other version of the tools.
321
322     Returns:
323         dict: New environment with PATH updated, or None if there are not search
324             paths
325     """
326     if tool_search_paths:
327         env = dict(os.environ)
328         env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
329         return env
330
331 def run_result(name, *args, **kwargs):
332     """Run a tool with some arguments
333
334     This runs a 'tool', which is a program used by binman to process files and
335     perhaps produce some output. Tools can be located on the PATH or in a
336     search path.
337
338     Args:
339         name: Command name to run
340         args: Arguments to the tool
341         for_host: True to resolve the command to the version for the host
342         for_target: False to run the command as-is, without resolving it
343                    to the version for the compile target
344         raise_on_error: Raise an error if the command fails (True by default)
345
346     Returns:
347         CommandResult object
348     """
349     try:
350         binary = kwargs.get('binary')
351         for_host = kwargs.get('for_host', False)
352         for_target = kwargs.get('for_target', not for_host)
353         raise_on_error = kwargs.get('raise_on_error', True)
354         env = get_env_with_path()
355         if for_target:
356             name, extra_args = get_target_compile_tool(name)
357             args = tuple(extra_args) + args
358         elif for_host:
359             name, extra_args = get_host_compile_tool(env, name)
360             args = tuple(extra_args) + args
361         name = os.path.expanduser(name)  # Expand paths containing ~
362         all_args = (name,) + args
363         result = command.run_pipe([all_args], capture=True, capture_stderr=True,
364                                  env=env, raise_on_error=False, binary=binary)
365         if result.return_code:
366             if raise_on_error:
367                 raise ValueError("Error %d running '%s': %s" %
368                                  (result.return_code,' '.join(all_args),
369                                   result.stderr or result.stdout))
370         return result
371     except ValueError:
372         if env and not path_has_file(env['PATH'], name):
373             msg = "Please install tool '%s'" % name
374             package = packages.get(name)
375             if package:
376                  msg += " (e.g. from package '%s')" % package
377             raise ValueError(msg)
378         raise
379
380 def tool_find(name):
381     """Search the current path for a tool
382
383     This uses both PATH and any value from set_tool_paths() to search for a tool
384
385     Args:
386         name (str): Name of tool to locate
387
388     Returns:
389         str: Full path to tool if found, else None
390     """
391     name = os.path.expanduser(name)  # Expand paths containing ~
392     paths = []
393     pathvar = os.environ.get('PATH')
394     if pathvar:
395         paths = pathvar.split(':')
396     if tool_search_paths:
397         paths += tool_search_paths
398     for path in paths:
399         fname = os.path.join(path, name)
400         if os.path.isfile(fname) and os.access(fname, os.X_OK):
401             return fname
402
403 def run(name, *args, **kwargs):
404     """Run a tool with some arguments
405
406     This runs a 'tool', which is a program used by binman to process files and
407     perhaps produce some output. Tools can be located on the PATH or in a
408     search path.
409
410     Args:
411         name: Command name to run
412         args: Arguments to the tool
413         for_host: True to resolve the command to the version for the host
414         for_target: False to run the command as-is, without resolving it
415                    to the version for the compile target
416
417     Returns:
418         CommandResult object
419     """
420     result = run_result(name, *args, **kwargs)
421     if result is not None:
422         return result.stdout
423
424 def filename(fname):
425     """Resolve a file path to an absolute path.
426
427     If fname starts with ##/ and chroot is available, ##/ gets replaced with
428     the chroot path. If chroot is not available, this file name can not be
429     resolved, `None' is returned.
430
431     If fname is not prepended with the above prefix, and is not an existing
432     file, the actual file name is retrieved from the passed in string and the
433     search_paths directories (if any) are searched to for the file. If found -
434     the path to the found file is returned, `None' is returned otherwise.
435
436     Args:
437       fname: a string,  the path to resolve.
438
439     Returns:
440       Absolute path to the file or None if not found.
441     """
442     if fname.startswith('##/'):
443       if chroot_path:
444         fname = os.path.join(chroot_path, fname[3:])
445       else:
446         return None
447
448     # Search for a pathname that exists, and return it if found
449     if fname and not os.path.exists(fname):
450         for path in search_paths:
451             pathname = os.path.join(path, os.path.basename(fname))
452             if os.path.exists(pathname):
453                 return pathname
454
455     # If not found, just return the standard, unchanged path
456     return fname
457
458 def read_file(fname, binary=True):
459     """Read and return the contents of a file.
460
461     Args:
462       fname: path to filename to read, where ## signifiies the chroot.
463
464     Returns:
465       data read from file, as a string.
466     """
467     with open(filename(fname), binary and 'rb' or 'r') as fd:
468         data = fd.read()
469     #self._out.Info("Read file '%s' size %d (%#0x)" %
470                    #(fname, len(data), len(data)))
471     return data
472
473 def write_file(fname, data, binary=True):
474     """Write data into a file.
475
476     Args:
477         fname: path to filename to write
478         data: data to write to file, as a string
479     """
480     #self._out.Info("Write file '%s' size %d (%#0x)" %
481                    #(fname, len(data), len(data)))
482     with open(filename(fname), binary and 'wb' or 'w') as fd:
483         fd.write(data)
484
485 def get_bytes(byte, size):
486     """Get a string of bytes of a given size
487
488     Args:
489         byte: Numeric byte value to use
490         size: Size of bytes/string to return
491
492     Returns:
493         A bytes type with 'byte' repeated 'size' times
494     """
495     return bytes([byte]) * size
496
497 def to_bytes(string):
498     """Convert a str type into a bytes type
499
500     Args:
501         string: string to convert
502
503     Returns:
504         A bytes type
505     """
506     return string.encode('utf-8')
507
508 def to_string(bval):
509     """Convert a bytes type into a str type
510
511     Args:
512         bval: bytes value to convert
513
514     Returns:
515         Python 3: A bytes type
516         Python 2: A string type
517     """
518     return bval.decode('utf-8')
519
520 def to_hex(val):
521     """Convert an integer value (or None) to a string
522
523     Returns:
524         hex value, or 'None' if the value is None
525     """
526     return 'None' if val is None else '%#x' % val
527
528 def to_hex_size(val):
529     """Return the size of an object in hex
530
531     Returns:
532         hex value of size, or 'None' if the value is None
533     """
534     return 'None' if val is None else '%#x' % len(val)
535
536 def print_full_help(fname):
537     """Print the full help message for a tool using an appropriate pager.
538
539     Args:
540         fname: Path to a file containing the full help message
541     """
542     pager = shlex.split(os.getenv('PAGER', ''))
543     if not pager:
544         lesspath = shutil.which('less')
545         pager = [lesspath] if lesspath else None
546     if not pager:
547         pager = ['more']
548     command.run(*pager, fname)
549
550 def download(url, tmpdir_pattern='.patman'):
551     """Download a file to a temporary directory
552
553     Args:
554         url (str): URL to download
555         tmpdir_pattern (str): pattern to use for the temporary directory
556
557     Returns:
558         Tuple:
559             Full path to the downloaded archive file in that directory,
560                 or None if there was an error while downloading
561             Temporary directory name
562     """
563     print('- downloading: %s' % url)
564     leaf = url.split('/')[-1]
565     tmpdir = tempfile.mkdtemp(tmpdir_pattern)
566     response = urllib.request.urlopen(url)
567     fname = os.path.join(tmpdir, leaf)
568     fd = open(fname, 'wb')
569     meta = response.info()
570     size = int(meta.get('Content-Length'))
571     done = 0
572     block_size = 1 << 16
573     status = ''
574
575     # Read the file in chunks and show progress as we go
576     while True:
577         buffer = response.read(block_size)
578         if not buffer:
579             print(chr(8) * (len(status) + 1), '\r', end=' ')
580             break
581
582         done += len(buffer)
583         fd.write(buffer)
584         status = r'%10d MiB  [%3d%%]' % (done // 1024 // 1024,
585                                             done * 100 // size)
586         status = status + chr(8) * (len(status) + 1)
587         print(status, end=' ')
588         sys.stdout.flush()
589     print('\r', end='')
590     sys.stdout.flush()
591     fd.close()
592     if done != size:
593         print('Error, failed to download')
594         os.remove(fname)
595         fname = None
596     return fname, tmpdir