Merge tag 'u-boot-atmel-fixes-2021.01-b' of https://gitlab.denx.de/u-boot/custodians...
[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 shutil
9 import struct
10 import sys
11 import tempfile
12
13 from patman import command
14 from patman import tout
15
16 # Output directly (generally this is temporary)
17 outdir = None
18
19 # True to keep the output directory around after exiting
20 preserve_outdir = False
21
22 # Path to the Chrome OS chroot, if we know it
23 chroot_path = None
24
25 # Search paths to use for Filename(), used to find files
26 search_paths = []
27
28 tool_search_paths = []
29
30 # Tools and the packages that contain them, on debian
31 packages = {
32     'lz4': 'liblz4-tool',
33     }
34
35 # List of paths to use when looking for an input file
36 indir = []
37
38 def PrepareOutputDir(dirname, preserve=False):
39     """Select an output directory, ensuring it exists.
40
41     This either creates a temporary directory or checks that the one supplied
42     by the user is valid. For a temporary directory, it makes a note to
43     remove it later if required.
44
45     Args:
46         dirname: a string, name of the output directory to use to store
47                 intermediate and output files. If is None - create a temporary
48                 directory.
49         preserve: a Boolean. If outdir above is None and preserve is False, the
50                 created temporary directory will be destroyed on exit.
51
52     Raises:
53         OSError: If it cannot create the output directory.
54     """
55     global outdir, preserve_outdir
56
57     preserve_outdir = dirname or preserve
58     if dirname:
59         outdir = dirname
60         if not os.path.isdir(outdir):
61             try:
62                 os.makedirs(outdir)
63             except OSError as err:
64                 raise CmdError("Cannot make output directory '%s': '%s'" %
65                                 (outdir, err.strerror))
66         tout.Debug("Using output directory '%s'" % outdir)
67     else:
68         outdir = tempfile.mkdtemp(prefix='binman.')
69         tout.Debug("Using temporary directory '%s'" % outdir)
70
71 def _RemoveOutputDir():
72     global outdir
73
74     shutil.rmtree(outdir)
75     tout.Debug("Deleted temporary directory '%s'" % outdir)
76     outdir = None
77
78 def FinaliseOutputDir():
79     global outdir, preserve_outdir
80
81     """Tidy up: delete output directory if temporary and not preserved."""
82     if outdir and not preserve_outdir:
83         _RemoveOutputDir()
84         outdir = None
85
86 def GetOutputFilename(fname):
87     """Return a filename within the output directory.
88
89     Args:
90         fname: Filename to use for new file
91
92     Returns:
93         The full path of the filename, within the output directory
94     """
95     return os.path.join(outdir, fname)
96
97 def _FinaliseForTest():
98     """Remove the output directory (for use by tests)"""
99     global outdir
100
101     if outdir:
102         _RemoveOutputDir()
103         outdir = None
104
105 def SetInputDirs(dirname):
106     """Add a list of input directories, where input files are kept.
107
108     Args:
109         dirname: a list of paths to input directories to use for obtaining
110                 files needed by binman to place in the image.
111     """
112     global indir
113
114     indir = dirname
115     tout.Debug("Using input directories %s" % indir)
116
117 def GetInputFilename(fname, allow_missing=False):
118     """Return a filename for use as input.
119
120     Args:
121         fname: Filename to use for new file
122         allow_missing: True if the filename can be missing
123
124     Returns:
125         The full path of the filename, within the input directory, or
126         None on error
127     """
128     if not indir or fname[:1] == '/':
129         return fname
130     for dirname in indir:
131         pathname = os.path.join(dirname, fname)
132         if os.path.exists(pathname):
133             return pathname
134
135     if allow_missing:
136         return None
137     raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
138                      (fname, ','.join(indir), os.getcwd()))
139
140 def GetInputFilenameGlob(pattern):
141     """Return a list of filenames for use as input.
142
143     Args:
144         pattern: Filename pattern to search for
145
146     Returns:
147         A list of matching files in all input directories
148     """
149     if not indir:
150         return glob.glob(fname)
151     files = []
152     for dirname in indir:
153         pathname = os.path.join(dirname, pattern)
154         files += glob.glob(pathname)
155     return sorted(files)
156
157 def Align(pos, align):
158     if align:
159         mask = align - 1
160         pos = (pos + mask) & ~mask
161     return pos
162
163 def NotPowerOfTwo(num):
164     return num and (num & (num - 1))
165
166 def SetToolPaths(toolpaths):
167     """Set the path to search for tools
168
169     Args:
170         toolpaths: List of paths to search for tools executed by Run()
171     """
172     global tool_search_paths
173
174     tool_search_paths = toolpaths
175
176 def PathHasFile(path_spec, fname):
177     """Check if a given filename is in the PATH
178
179     Args:
180         path_spec: Value of PATH variable to check
181         fname: Filename to check
182
183     Returns:
184         True if found, False if not
185     """
186     for dir in path_spec.split(':'):
187         if os.path.exists(os.path.join(dir, fname)):
188             return True
189     return False
190
191 def GetHostCompileTool(name):
192     """Get the host-specific version for a compile tool
193
194     This checks the environment variables that specify which version of
195     the tool should be used (e.g. ${HOSTCC}).
196
197     The following table lists the host-specific versions of the tools
198     this function resolves to:
199
200         Compile Tool  | Host version
201         --------------+----------------
202         as            |  ${HOSTAS}
203         ld            |  ${HOSTLD}
204         cc            |  ${HOSTCC}
205         cpp           |  ${HOSTCPP}
206         c++           |  ${HOSTCXX}
207         ar            |  ${HOSTAR}
208         nm            |  ${HOSTNM}
209         ldr           |  ${HOSTLDR}
210         strip         |  ${HOSTSTRIP}
211         objcopy       |  ${HOSTOBJCOPY}
212         objdump       |  ${HOSTOBJDUMP}
213         dtc           |  ${HOSTDTC}
214
215     Args:
216         name: Command name to run
217
218     Returns:
219         host_name: Exact command name to run instead
220         extra_args: List of extra arguments to pass
221     """
222     host_name = None
223     extra_args = []
224     if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
225                 'objcopy', 'objdump', 'dtc'):
226         host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
227     elif name == 'c++':
228         host_name, *host_args = env.get('HOSTCXX', '').split(' ')
229
230     if host_name:
231         return host_name, extra_args
232     return name, []
233
234 def GetTargetCompileTool(name, cross_compile=None):
235     """Get the target-specific version for a compile tool
236
237     This first checks the environment variables that specify which
238     version of the tool should be used (e.g. ${CC}). If those aren't
239     specified, it checks the CROSS_COMPILE variable as a prefix for the
240     tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
241
242     The following table lists the target-specific versions of the tools
243     this function resolves to:
244
245         Compile Tool  | First choice   | Second choice
246         --------------+----------------+----------------------------
247         as            |  ${AS}         | ${CROSS_COMPILE}as
248         ld            |  ${LD}         | ${CROSS_COMPILE}ld.bfd
249                       |                |   or ${CROSS_COMPILE}ld
250         cc            |  ${CC}         | ${CROSS_COMPILE}gcc
251         cpp           |  ${CPP}        | ${CROSS_COMPILE}gcc -E
252         c++           |  ${CXX}        | ${CROSS_COMPILE}g++
253         ar            |  ${AR}         | ${CROSS_COMPILE}ar
254         nm            |  ${NM}         | ${CROSS_COMPILE}nm
255         ldr           |  ${LDR}        | ${CROSS_COMPILE}ldr
256         strip         |  ${STRIP}      | ${CROSS_COMPILE}strip
257         objcopy       |  ${OBJCOPY}    | ${CROSS_COMPILE}objcopy
258         objdump       |  ${OBJDUMP}    | ${CROSS_COMPILE}objdump
259         dtc           |  ${DTC}        | (no CROSS_COMPILE version)
260
261     Args:
262         name: Command name to run
263
264     Returns:
265         target_name: Exact command name to run instead
266         extra_args: List of extra arguments to pass
267     """
268     env = dict(os.environ)
269
270     target_name = None
271     extra_args = []
272     if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
273                 'objcopy', 'objdump', 'dtc'):
274         target_name, *extra_args = env.get(name.upper(), '').split(' ')
275     elif name == 'c++':
276         target_name, *extra_args = env.get('CXX', '').split(' ')
277
278     if target_name:
279         return target_name, extra_args
280
281     if cross_compile is None:
282         cross_compile = env.get('CROSS_COMPILE', '')
283     if not cross_compile:
284         return name, []
285
286     if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
287         target_name = cross_compile + name
288     elif name == 'ld':
289         try:
290             if Run(cross_compile + 'ld.bfd', '-v'):
291                 target_name = cross_compile + 'ld.bfd'
292         except:
293             target_name = cross_compile + 'ld'
294     elif name == 'cc':
295         target_name = cross_compile + 'gcc'
296     elif name == 'cpp':
297         target_name = cross_compile + 'gcc'
298         extra_args = ['-E']
299     elif name == 'c++':
300         target_name = cross_compile + 'g++'
301     else:
302         target_name = name
303     return target_name, extra_args
304
305 def Run(name, *args, **kwargs):
306     """Run a tool with some arguments
307
308     This runs a 'tool', which is a program used by binman to process files and
309     perhaps produce some output. Tools can be located on the PATH or in a
310     search path.
311
312     Args:
313         name: Command name to run
314         args: Arguments to the tool
315         for_host: True to resolve the command to the version for the host
316         for_target: False to run the command as-is, without resolving it
317                    to the version for the compile target
318
319     Returns:
320         CommandResult object
321     """
322     try:
323         binary = kwargs.get('binary')
324         for_host = kwargs.get('for_host', False)
325         for_target = kwargs.get('for_target', not for_host)
326         env = None
327         if tool_search_paths:
328             env = dict(os.environ)
329             env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
330         if for_target:
331             name, extra_args = GetTargetCompileTool(name)
332             args = tuple(extra_args) + args
333         elif for_host:
334             name, extra_args = GetHostCompileTool(name)
335             args = tuple(extra_args) + args
336         name = os.path.expanduser(name)  # Expand paths containing ~
337         all_args = (name,) + args
338         result = command.RunPipe([all_args], capture=True, capture_stderr=True,
339                                  env=env, raise_on_error=False, binary=binary)
340         if result.return_code:
341             raise Exception("Error %d running '%s': %s" %
342                (result.return_code,' '.join(all_args),
343                 result.stderr))
344         return result.stdout
345     except:
346         if env and not PathHasFile(env['PATH'], name):
347             msg = "Please install tool '%s'" % name
348             package = packages.get(name)
349             if package:
350                  msg += " (e.g. from package '%s')" % package
351             raise ValueError(msg)
352         raise
353
354 def Filename(fname):
355     """Resolve a file path to an absolute path.
356
357     If fname starts with ##/ and chroot is available, ##/ gets replaced with
358     the chroot path. If chroot is not available, this file name can not be
359     resolved, `None' is returned.
360
361     If fname is not prepended with the above prefix, and is not an existing
362     file, the actual file name is retrieved from the passed in string and the
363     search_paths directories (if any) are searched to for the file. If found -
364     the path to the found file is returned, `None' is returned otherwise.
365
366     Args:
367       fname: a string,  the path to resolve.
368
369     Returns:
370       Absolute path to the file or None if not found.
371     """
372     if fname.startswith('##/'):
373       if chroot_path:
374         fname = os.path.join(chroot_path, fname[3:])
375       else:
376         return None
377
378     # Search for a pathname that exists, and return it if found
379     if fname and not os.path.exists(fname):
380         for path in search_paths:
381             pathname = os.path.join(path, os.path.basename(fname))
382             if os.path.exists(pathname):
383                 return pathname
384
385     # If not found, just return the standard, unchanged path
386     return fname
387
388 def ReadFile(fname, binary=True):
389     """Read and return the contents of a file.
390
391     Args:
392       fname: path to filename to read, where ## signifiies the chroot.
393
394     Returns:
395       data read from file, as a string.
396     """
397     with open(Filename(fname), binary and 'rb' or 'r') as fd:
398         data = fd.read()
399     #self._out.Info("Read file '%s' size %d (%#0x)" %
400                    #(fname, len(data), len(data)))
401     return data
402
403 def WriteFile(fname, data, binary=True):
404     """Write data into a file.
405
406     Args:
407         fname: path to filename to write
408         data: data to write to file, as a string
409     """
410     #self._out.Info("Write file '%s' size %d (%#0x)" %
411                    #(fname, len(data), len(data)))
412     with open(Filename(fname), binary and 'wb' or 'w') as fd:
413         fd.write(data)
414
415 def GetBytes(byte, size):
416     """Get a string of bytes of a given size
417
418     This handles the unfortunate different between Python 2 and Python 2.
419
420     Args:
421         byte: Numeric byte value to use
422         size: Size of bytes/string to return
423
424     Returns:
425         A bytes type with 'byte' repeated 'size' times
426     """
427     if sys.version_info[0] >= 3:
428         data = bytes([byte]) * size
429     else:
430         data = chr(byte) * size
431     return data
432
433 def ToUnicode(val):
434     """Make sure a value is a unicode string
435
436     This allows some amount of compatibility between Python 2 and Python3. For
437     the former, it returns a unicode object.
438
439     Args:
440         val: string or unicode object
441
442     Returns:
443         unicode version of val
444     """
445     if sys.version_info[0] >= 3:
446         return val
447     return val if isinstance(val, unicode) else val.decode('utf-8')
448
449 def FromUnicode(val):
450     """Make sure a value is a non-unicode string
451
452     This allows some amount of compatibility between Python 2 and Python3. For
453     the former, it converts a unicode object to a string.
454
455     Args:
456         val: string or unicode object
457
458     Returns:
459         non-unicode version of val
460     """
461     if sys.version_info[0] >= 3:
462         return val
463     return val if isinstance(val, str) else val.encode('utf-8')
464
465 def ToByte(ch):
466     """Convert a character to an ASCII value
467
468     This is useful because in Python 2 bytes is an alias for str, but in
469     Python 3 they are separate types. This function converts the argument to
470     an ASCII value in either case.
471
472     Args:
473         ch: A string (Python 2) or byte (Python 3) value
474
475     Returns:
476         integer ASCII value for ch
477     """
478     return ord(ch) if type(ch) == str else ch
479
480 def ToChar(byte):
481     """Convert a byte to a character
482
483     This is useful because in Python 2 bytes is an alias for str, but in
484     Python 3 they are separate types. This function converts an ASCII value to
485     a value with the appropriate type in either case.
486
487     Args:
488         byte: A byte or str value
489     """
490     return chr(byte) if type(byte) != str else byte
491
492 def ToChars(byte_list):
493     """Convert a list of bytes to a str/bytes type
494
495     Args:
496         byte_list: List of ASCII values representing the string
497
498     Returns:
499         string made by concatenating all the ASCII values
500     """
501     return ''.join([chr(byte) for byte in byte_list])
502
503 def ToBytes(string):
504     """Convert a str type into a bytes type
505
506     Args:
507         string: string to convert
508
509     Returns:
510         Python 3: A bytes type
511         Python 2: A string type
512     """
513     if sys.version_info[0] >= 3:
514         return string.encode('utf-8')
515     return string
516
517 def ToString(bval):
518     """Convert a bytes type into a str type
519
520     Args:
521         bval: bytes value to convert
522
523     Returns:
524         Python 3: A bytes type
525         Python 2: A string type
526     """
527     return bval.decode('utf-8')
528
529 def Compress(indata, algo, with_header=True):
530     """Compress some data using a given algorithm
531
532     Note that for lzma this uses an old version of the algorithm, not that
533     provided by xz.
534
535     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
536     directory to be previously set up, by calling PrepareOutputDir().
537
538     Args:
539         indata: Input data to compress
540         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
541
542     Returns:
543         Compressed data
544     """
545     if algo == 'none':
546         return indata
547     fname = GetOutputFilename('%s.comp.tmp' % algo)
548     WriteFile(fname, indata)
549     if algo == 'lz4':
550         data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
551     # cbfstool uses a very old version of lzma
552     elif algo == 'lzma':
553         outfname = GetOutputFilename('%s.comp.otmp' % algo)
554         Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
555         data = ReadFile(outfname)
556     elif algo == 'gzip':
557         data = Run('gzip', '-c', fname, binary=True)
558     else:
559         raise ValueError("Unknown algorithm '%s'" % algo)
560     if with_header:
561         hdr = struct.pack('<I', len(data))
562         data = hdr + data
563     return data
564
565 def Decompress(indata, algo, with_header=True):
566     """Decompress some data using a given algorithm
567
568     Note that for lzma this uses an old version of the algorithm, not that
569     provided by xz.
570
571     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
572     directory to be previously set up, by calling PrepareOutputDir().
573
574     Args:
575         indata: Input data to decompress
576         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
577
578     Returns:
579         Compressed data
580     """
581     if algo == 'none':
582         return indata
583     if with_header:
584         data_len = struct.unpack('<I', indata[:4])[0]
585         indata = indata[4:4 + data_len]
586     fname = GetOutputFilename('%s.decomp.tmp' % algo)
587     with open(fname, 'wb') as fd:
588         fd.write(indata)
589     if algo == 'lz4':
590         data = Run('lz4', '-dc', fname, binary=True)
591     elif algo == 'lzma':
592         outfname = GetOutputFilename('%s.decomp.otmp' % algo)
593         Run('lzma_alone', 'd', fname, outfname)
594         data = ReadFile(outfname, binary=True)
595     elif algo == 'gzip':
596         data = Run('gzip', '-cd', fname, binary=True)
597     else:
598         raise ValueError("Unknown algorithm '%s'" % algo)
599     return data
600
601 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
602
603 IFWITOOL_CMDS = {
604     CMD_CREATE: 'create',
605     CMD_DELETE: 'delete',
606     CMD_ADD: 'add',
607     CMD_REPLACE: 'replace',
608     CMD_EXTRACT: 'extract',
609     }
610
611 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
612     """Run ifwitool with the given arguments:
613
614     Args:
615         ifwi_file: IFWI file to operation on
616         cmd: Command to execute (CMD_...)
617         fname: Filename of file to add/replace/extract/create (None for
618             CMD_DELETE)
619         subpart: Name of sub-partition to operation on (None for CMD_CREATE)
620         entry_name: Name of directory entry to operate on, or None if none
621     """
622     args = ['ifwitool', ifwi_file]
623     args.append(IFWITOOL_CMDS[cmd])
624     if fname:
625         args += ['-f', fname]
626     if subpart:
627         args += ['-n', subpart]
628     if entry_name:
629         args += ['-d', '-e', entry_name]
630     Run(*args)
631
632 def ToHex(val):
633     """Convert an integer value (or None) to a string
634
635     Returns:
636         hex value, or 'None' if the value is None
637     """
638     return 'None' if val is None else '%#x' % val
639
640 def ToHexSize(val):
641     """Return the size of an object in hex
642
643     Returns:
644         hex value of size, or 'None' if the value is None
645     """
646     return 'None' if val is None else '%#x' % len(val)