bbb157da873e68eb8ab839a6e9bbd51a8f5d759a
[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         all_args = (name,) + args
337         result = command.RunPipe([all_args], capture=True, capture_stderr=True,
338                                  env=env, raise_on_error=False, binary=binary)
339         if result.return_code:
340             raise Exception("Error %d running '%s': %s" %
341                (result.return_code,' '.join(all_args),
342                 result.stderr))
343         return result.stdout
344     except:
345         if env and not PathHasFile(env['PATH'], name):
346             msg = "Please install tool '%s'" % name
347             package = packages.get(name)
348             if package:
349                  msg += " (e.g. from package '%s')" % package
350             raise ValueError(msg)
351         raise
352
353 def Filename(fname):
354     """Resolve a file path to an absolute path.
355
356     If fname starts with ##/ and chroot is available, ##/ gets replaced with
357     the chroot path. If chroot is not available, this file name can not be
358     resolved, `None' is returned.
359
360     If fname is not prepended with the above prefix, and is not an existing
361     file, the actual file name is retrieved from the passed in string and the
362     search_paths directories (if any) are searched to for the file. If found -
363     the path to the found file is returned, `None' is returned otherwise.
364
365     Args:
366       fname: a string,  the path to resolve.
367
368     Returns:
369       Absolute path to the file or None if not found.
370     """
371     if fname.startswith('##/'):
372       if chroot_path:
373         fname = os.path.join(chroot_path, fname[3:])
374       else:
375         return None
376
377     # Search for a pathname that exists, and return it if found
378     if fname and not os.path.exists(fname):
379         for path in search_paths:
380             pathname = os.path.join(path, os.path.basename(fname))
381             if os.path.exists(pathname):
382                 return pathname
383
384     # If not found, just return the standard, unchanged path
385     return fname
386
387 def ReadFile(fname, binary=True):
388     """Read and return the contents of a file.
389
390     Args:
391       fname: path to filename to read, where ## signifiies the chroot.
392
393     Returns:
394       data read from file, as a string.
395     """
396     with open(Filename(fname), binary and 'rb' or 'r') as fd:
397         data = fd.read()
398     #self._out.Info("Read file '%s' size %d (%#0x)" %
399                    #(fname, len(data), len(data)))
400     return data
401
402 def WriteFile(fname, data, binary=True):
403     """Write data into a file.
404
405     Args:
406         fname: path to filename to write
407         data: data to write to file, as a string
408     """
409     #self._out.Info("Write file '%s' size %d (%#0x)" %
410                    #(fname, len(data), len(data)))
411     with open(Filename(fname), binary and 'wb' or 'w') as fd:
412         fd.write(data)
413
414 def GetBytes(byte, size):
415     """Get a string of bytes of a given size
416
417     This handles the unfortunate different between Python 2 and Python 2.
418
419     Args:
420         byte: Numeric byte value to use
421         size: Size of bytes/string to return
422
423     Returns:
424         A bytes type with 'byte' repeated 'size' times
425     """
426     if sys.version_info[0] >= 3:
427         data = bytes([byte]) * size
428     else:
429         data = chr(byte) * size
430     return data
431
432 def ToUnicode(val):
433     """Make sure a value is a unicode string
434
435     This allows some amount of compatibility between Python 2 and Python3. For
436     the former, it returns a unicode object.
437
438     Args:
439         val: string or unicode object
440
441     Returns:
442         unicode version of val
443     """
444     if sys.version_info[0] >= 3:
445         return val
446     return val if isinstance(val, unicode) else val.decode('utf-8')
447
448 def FromUnicode(val):
449     """Make sure a value is a non-unicode string
450
451     This allows some amount of compatibility between Python 2 and Python3. For
452     the former, it converts a unicode object to a string.
453
454     Args:
455         val: string or unicode object
456
457     Returns:
458         non-unicode version of val
459     """
460     if sys.version_info[0] >= 3:
461         return val
462     return val if isinstance(val, str) else val.encode('utf-8')
463
464 def ToByte(ch):
465     """Convert a character to an ASCII value
466
467     This is useful because in Python 2 bytes is an alias for str, but in
468     Python 3 they are separate types. This function converts the argument to
469     an ASCII value in either case.
470
471     Args:
472         ch: A string (Python 2) or byte (Python 3) value
473
474     Returns:
475         integer ASCII value for ch
476     """
477     return ord(ch) if type(ch) == str else ch
478
479 def ToChar(byte):
480     """Convert a byte to a character
481
482     This is useful because in Python 2 bytes is an alias for str, but in
483     Python 3 they are separate types. This function converts an ASCII value to
484     a value with the appropriate type in either case.
485
486     Args:
487         byte: A byte or str value
488     """
489     return chr(byte) if type(byte) != str else byte
490
491 def ToChars(byte_list):
492     """Convert a list of bytes to a str/bytes type
493
494     Args:
495         byte_list: List of ASCII values representing the string
496
497     Returns:
498         string made by concatenating all the ASCII values
499     """
500     return ''.join([chr(byte) for byte in byte_list])
501
502 def ToBytes(string):
503     """Convert a str type into a bytes type
504
505     Args:
506         string: string to convert
507
508     Returns:
509         Python 3: A bytes type
510         Python 2: A string type
511     """
512     if sys.version_info[0] >= 3:
513         return string.encode('utf-8')
514     return string
515
516 def ToString(bval):
517     """Convert a bytes type into a str type
518
519     Args:
520         bval: bytes value to convert
521
522     Returns:
523         Python 3: A bytes type
524         Python 2: A string type
525     """
526     return bval.decode('utf-8')
527
528 def Compress(indata, algo, with_header=True):
529     """Compress some data using a given algorithm
530
531     Note that for lzma this uses an old version of the algorithm, not that
532     provided by xz.
533
534     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
535     directory to be previously set up, by calling PrepareOutputDir().
536
537     Args:
538         indata: Input data to compress
539         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
540
541     Returns:
542         Compressed data
543     """
544     if algo == 'none':
545         return indata
546     fname = GetOutputFilename('%s.comp.tmp' % algo)
547     WriteFile(fname, indata)
548     if algo == 'lz4':
549         data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
550     # cbfstool uses a very old version of lzma
551     elif algo == 'lzma':
552         outfname = GetOutputFilename('%s.comp.otmp' % algo)
553         Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
554         data = ReadFile(outfname)
555     elif algo == 'gzip':
556         data = Run('gzip', '-c', fname, binary=True)
557     else:
558         raise ValueError("Unknown algorithm '%s'" % algo)
559     if with_header:
560         hdr = struct.pack('<I', len(data))
561         data = hdr + data
562     return data
563
564 def Decompress(indata, algo, with_header=True):
565     """Decompress some data using a given algorithm
566
567     Note that for lzma this uses an old version of the algorithm, not that
568     provided by xz.
569
570     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
571     directory to be previously set up, by calling PrepareOutputDir().
572
573     Args:
574         indata: Input data to decompress
575         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
576
577     Returns:
578         Compressed data
579     """
580     if algo == 'none':
581         return indata
582     if with_header:
583         data_len = struct.unpack('<I', indata[:4])[0]
584         indata = indata[4:4 + data_len]
585     fname = GetOutputFilename('%s.decomp.tmp' % algo)
586     with open(fname, 'wb') as fd:
587         fd.write(indata)
588     if algo == 'lz4':
589         data = Run('lz4', '-dc', fname, binary=True)
590     elif algo == 'lzma':
591         outfname = GetOutputFilename('%s.decomp.otmp' % algo)
592         Run('lzma_alone', 'd', fname, outfname)
593         data = ReadFile(outfname, binary=True)
594     elif algo == 'gzip':
595         data = Run('gzip', '-cd', fname, binary=True)
596     else:
597         raise ValueError("Unknown algorithm '%s'" % algo)
598     return data
599
600 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
601
602 IFWITOOL_CMDS = {
603     CMD_CREATE: 'create',
604     CMD_DELETE: 'delete',
605     CMD_ADD: 'add',
606     CMD_REPLACE: 'replace',
607     CMD_EXTRACT: 'extract',
608     }
609
610 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
611     """Run ifwitool with the given arguments:
612
613     Args:
614         ifwi_file: IFWI file to operation on
615         cmd: Command to execute (CMD_...)
616         fname: Filename of file to add/replace/extract/create (None for
617             CMD_DELETE)
618         subpart: Name of sub-partition to operation on (None for CMD_CREATE)
619         entry_name: Name of directory entry to operate on, or None if none
620     """
621     args = ['ifwitool', ifwi_file]
622     args.append(IFWITOOL_CMDS[cmd])
623     if fname:
624         args += ['-f', fname]
625     if subpart:
626         args += ['-n', subpart]
627     if entry_name:
628         args += ['-d', '-e', entry_name]
629     Run(*args)
630
631 def ToHex(val):
632     """Convert an integer value (or None) to a string
633
634     Returns:
635         hex value, or 'None' if the value is None
636     """
637     return 'None' if val is None else '%#x' % val
638
639 def ToHexSize(val):
640     """Return the size of an object in hex
641
642     Returns:
643         hex value of size, or 'None' if the value is None
644     """
645     return 'None' if val is None else '%#x' % len(val)