Merge tag 'video-for-2019.10-rc1' of https://gitlab.denx.de/u-boot/custodians/u-boot...
[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 from __future__ import print_function
7
8 import command
9 import glob
10 import os
11 import shutil
12 import struct
13 import sys
14 import tempfile
15
16 import tout
17
18 # Output directly (generally this is temporary)
19 outdir = None
20
21 # True to keep the output directory around after exiting
22 preserve_outdir = False
23
24 # Path to the Chrome OS chroot, if we know it
25 chroot_path = None
26
27 # Search paths to use for Filename(), used to find files
28 search_paths = []
29
30 tool_search_paths = []
31
32 # Tools and the packages that contain them, on debian
33 packages = {
34     'lz4': 'liblz4-tool',
35     }
36
37 # List of paths to use when looking for an input file
38 indir = []
39
40 def PrepareOutputDir(dirname, preserve=False):
41     """Select an output directory, ensuring it exists.
42
43     This either creates a temporary directory or checks that the one supplied
44     by the user is valid. For a temporary directory, it makes a note to
45     remove it later if required.
46
47     Args:
48         dirname: a string, name of the output directory to use to store
49                 intermediate and output files. If is None - create a temporary
50                 directory.
51         preserve: a Boolean. If outdir above is None and preserve is False, the
52                 created temporary directory will be destroyed on exit.
53
54     Raises:
55         OSError: If it cannot create the output directory.
56     """
57     global outdir, preserve_outdir
58
59     preserve_outdir = dirname or preserve
60     if dirname:
61         outdir = dirname
62         if not os.path.isdir(outdir):
63             try:
64                 os.makedirs(outdir)
65             except OSError as err:
66                 raise CmdError("Cannot make output directory '%s': '%s'" %
67                                 (outdir, err.strerror))
68         tout.Debug("Using output directory '%s'" % outdir)
69     else:
70         outdir = tempfile.mkdtemp(prefix='binman.')
71         tout.Debug("Using temporary directory '%s'" % outdir)
72
73 def _RemoveOutputDir():
74     global outdir
75
76     shutil.rmtree(outdir)
77     tout.Debug("Deleted temporary directory '%s'" % outdir)
78     outdir = None
79
80 def FinaliseOutputDir():
81     global outdir, preserve_outdir
82
83     """Tidy up: delete output directory if temporary and not preserved."""
84     if outdir and not preserve_outdir:
85         _RemoveOutputDir()
86         outdir = None
87
88 def GetOutputFilename(fname):
89     """Return a filename within the output directory.
90
91     Args:
92         fname: Filename to use for new file
93
94     Returns:
95         The full path of the filename, within the output directory
96     """
97     return os.path.join(outdir, fname)
98
99 def _FinaliseForTest():
100     """Remove the output directory (for use by tests)"""
101     global outdir
102
103     if outdir:
104         _RemoveOutputDir()
105         outdir = None
106
107 def SetInputDirs(dirname):
108     """Add a list of input directories, where input files are kept.
109
110     Args:
111         dirname: a list of paths to input directories to use for obtaining
112                 files needed by binman to place in the image.
113     """
114     global indir
115
116     indir = dirname
117     tout.Debug("Using input directories %s" % indir)
118
119 def GetInputFilename(fname):
120     """Return a filename for use as input.
121
122     Args:
123         fname: Filename to use for new file
124
125     Returns:
126         The full path of the filename, within the input directory
127     """
128     if not indir:
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     raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
136                      (fname, ','.join(indir), os.getcwd()))
137
138 def GetInputFilenameGlob(pattern):
139     """Return a list of filenames for use as input.
140
141     Args:
142         pattern: Filename pattern to search for
143
144     Returns:
145         A list of matching files in all input directories
146     """
147     if not indir:
148         return glob.glob(fname)
149     files = []
150     for dirname in indir:
151         pathname = os.path.join(dirname, pattern)
152         files += glob.glob(pathname)
153     return sorted(files)
154
155 def Align(pos, align):
156     if align:
157         mask = align - 1
158         pos = (pos + mask) & ~mask
159     return pos
160
161 def NotPowerOfTwo(num):
162     return num and (num & (num - 1))
163
164 def SetToolPaths(toolpaths):
165     """Set the path to search for tools
166
167     Args:
168         toolpaths: List of paths to search for tools executed by Run()
169     """
170     global tool_search_paths
171
172     tool_search_paths = toolpaths
173
174 def PathHasFile(path_spec, fname):
175     """Check if a given filename is in the PATH
176
177     Args:
178         path_spec: Value of PATH variable to check
179         fname: Filename to check
180
181     Returns:
182         True if found, False if not
183     """
184     for dir in path_spec.split(':'):
185         if os.path.exists(os.path.join(dir, fname)):
186             return True
187     return False
188
189 def Run(name, *args, **kwargs):
190     """Run a tool with some arguments
191
192     This runs a 'tool', which is a program used by binman to process files and
193     perhaps produce some output. Tools can be located on the PATH or in a
194     search path.
195
196     Args:
197         name: Command name to run
198         args: Arguments to the tool
199         kwargs: Options to pass to command.run()
200
201     Returns:
202         CommandResult object
203     """
204     try:
205         env = None
206         if tool_search_paths:
207             env = dict(os.environ)
208             env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
209         return command.Run(name, *args, capture=True,
210                            capture_stderr=True, env=env, **kwargs)
211     except:
212         if env and not PathHasFile(env['PATH'], name):
213             msg = "Please install tool '%s'" % name
214             package = packages.get(name)
215             if package:
216                  msg += " (e.g. from package '%s')" % package
217             raise ValueError(msg)
218         raise
219
220 def Filename(fname):
221     """Resolve a file path to an absolute path.
222
223     If fname starts with ##/ and chroot is available, ##/ gets replaced with
224     the chroot path. If chroot is not available, this file name can not be
225     resolved, `None' is returned.
226
227     If fname is not prepended with the above prefix, and is not an existing
228     file, the actual file name is retrieved from the passed in string and the
229     search_paths directories (if any) are searched to for the file. If found -
230     the path to the found file is returned, `None' is returned otherwise.
231
232     Args:
233       fname: a string,  the path to resolve.
234
235     Returns:
236       Absolute path to the file or None if not found.
237     """
238     if fname.startswith('##/'):
239       if chroot_path:
240         fname = os.path.join(chroot_path, fname[3:])
241       else:
242         return None
243
244     # Search for a pathname that exists, and return it if found
245     if fname and not os.path.exists(fname):
246         for path in search_paths:
247             pathname = os.path.join(path, os.path.basename(fname))
248             if os.path.exists(pathname):
249                 return pathname
250
251     # If not found, just return the standard, unchanged path
252     return fname
253
254 def ReadFile(fname, binary=True):
255     """Read and return the contents of a file.
256
257     Args:
258       fname: path to filename to read, where ## signifiies the chroot.
259
260     Returns:
261       data read from file, as a string.
262     """
263     with open(Filename(fname), binary and 'rb' or 'r') as fd:
264         data = fd.read()
265     #self._out.Info("Read file '%s' size %d (%#0x)" %
266                    #(fname, len(data), len(data)))
267     return data
268
269 def WriteFile(fname, data):
270     """Write data into a file.
271
272     Args:
273         fname: path to filename to write
274         data: data to write to file, as a string
275     """
276     #self._out.Info("Write file '%s' size %d (%#0x)" %
277                    #(fname, len(data), len(data)))
278     with open(Filename(fname), 'wb') as fd:
279         fd.write(data)
280
281 def GetBytes(byte, size):
282     """Get a string of bytes of a given size
283
284     This handles the unfortunate different between Python 2 and Python 2.
285
286     Args:
287         byte: Numeric byte value to use
288         size: Size of bytes/string to return
289
290     Returns:
291         A bytes type with 'byte' repeated 'size' times
292     """
293     if sys.version_info[0] >= 3:
294         data = bytes([byte]) * size
295     else:
296         data = chr(byte) * size
297     return data
298
299 def ToUnicode(val):
300     """Make sure a value is a unicode string
301
302     This allows some amount of compatibility between Python 2 and Python3. For
303     the former, it returns a unicode object.
304
305     Args:
306         val: string or unicode object
307
308     Returns:
309         unicode version of val
310     """
311     if sys.version_info[0] >= 3:
312         return val
313     return val if isinstance(val, unicode) else val.decode('utf-8')
314
315 def FromUnicode(val):
316     """Make sure a value is a non-unicode string
317
318     This allows some amount of compatibility between Python 2 and Python3. For
319     the former, it converts a unicode object to a string.
320
321     Args:
322         val: string or unicode object
323
324     Returns:
325         non-unicode version of val
326     """
327     if sys.version_info[0] >= 3:
328         return val
329     return val if isinstance(val, str) else val.encode('utf-8')
330
331 def ToByte(ch):
332     """Convert a character to an ASCII value
333
334     This is useful because in Python 2 bytes is an alias for str, but in
335     Python 3 they are separate types. This function converts the argument to
336     an ASCII value in either case.
337
338     Args:
339         ch: A string (Python 2) or byte (Python 3) value
340
341     Returns:
342         integer ASCII value for ch
343     """
344     return ord(ch) if type(ch) == str else ch
345
346 def ToChar(byte):
347     """Convert a byte to a character
348
349     This is useful because in Python 2 bytes is an alias for str, but in
350     Python 3 they are separate types. This function converts an ASCII value to
351     a value with the appropriate type in either case.
352
353     Args:
354         byte: A byte or str value
355     """
356     return chr(byte) if type(byte) != str else byte
357
358 def ToChars(byte_list):
359     """Convert a list of bytes to a str/bytes type
360
361     Args:
362         byte_list: List of ASCII values representing the string
363
364     Returns:
365         string made by concatenating all the ASCII values
366     """
367     return ''.join([chr(byte) for byte in byte_list])
368
369 def ToBytes(string):
370     """Convert a str type into a bytes type
371
372     Args:
373         string: string to convert value
374
375     Returns:
376         Python 3: A bytes type
377         Python 2: A string type
378     """
379     if sys.version_info[0] >= 3:
380         return string.encode('utf-8')
381     return string
382
383 def Compress(indata, algo, with_header=True):
384     """Compress some data using a given algorithm
385
386     Note that for lzma this uses an old version of the algorithm, not that
387     provided by xz.
388
389     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
390     directory to be previously set up, by calling PrepareOutputDir().
391
392     Args:
393         indata: Input data to compress
394         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
395
396     Returns:
397         Compressed data
398     """
399     if algo == 'none':
400         return indata
401     fname = GetOutputFilename('%s.comp.tmp' % algo)
402     WriteFile(fname, indata)
403     if algo == 'lz4':
404         data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
405     # cbfstool uses a very old version of lzma
406     elif algo == 'lzma':
407         outfname = GetOutputFilename('%s.comp.otmp' % algo)
408         Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
409         data = ReadFile(outfname)
410     elif algo == 'gzip':
411         data = Run('gzip', '-c', fname, binary=True)
412     else:
413         raise ValueError("Unknown algorithm '%s'" % algo)
414     if with_header:
415         hdr = struct.pack('<I', len(data))
416         data = hdr + data
417     return data
418
419 def Decompress(indata, algo, with_header=True):
420     """Decompress some data using a given algorithm
421
422     Note that for lzma this uses an old version of the algorithm, not that
423     provided by xz.
424
425     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
426     directory to be previously set up, by calling PrepareOutputDir().
427
428     Args:
429         indata: Input data to decompress
430         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
431
432     Returns:
433         Compressed data
434     """
435     if algo == 'none':
436         return indata
437     if with_header:
438         data_len = struct.unpack('<I', indata[:4])[0]
439         indata = indata[4:4 + data_len]
440     fname = GetOutputFilename('%s.decomp.tmp' % algo)
441     with open(fname, 'wb') as fd:
442         fd.write(indata)
443     if algo == 'lz4':
444         data = Run('lz4', '-dc', fname, binary=True)
445     elif algo == 'lzma':
446         outfname = GetOutputFilename('%s.decomp.otmp' % algo)
447         Run('lzma_alone', 'd', fname, outfname)
448         data = ReadFile(outfname)
449     elif algo == 'gzip':
450         data = Run('gzip', '-cd', fname, binary=True)
451     else:
452         raise ValueError("Unknown algorithm '%s'" % algo)
453     return data
454
455 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
456
457 IFWITOOL_CMDS = {
458     CMD_CREATE: 'create',
459     CMD_DELETE: 'delete',
460     CMD_ADD: 'add',
461     CMD_REPLACE: 'replace',
462     CMD_EXTRACT: 'extract',
463     }
464
465 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
466     """Run ifwitool with the given arguments:
467
468     Args:
469         ifwi_file: IFWI file to operation on
470         cmd: Command to execute (CMD_...)
471         fname: Filename of file to add/replace/extract/create (None for
472             CMD_DELETE)
473         subpart: Name of sub-partition to operation on (None for CMD_CREATE)
474         entry_name: Name of directory entry to operate on, or None if none
475     """
476     args = ['ifwitool', ifwi_file]
477     args.append(IFWITOOL_CMDS[cmd])
478     if fname:
479         args += ['-f', fname]
480     if subpart:
481         args += ['-n', subpart]
482     if entry_name:
483         args += ['-d', '-e', entry_name]
484     Run(*args)
485
486 def ToHex(val):
487     """Convert an integer value (or None) to a string
488
489     Returns:
490         hex value, or 'None' if the value is None
491     """
492     return 'None' if val is None else '%#x' % val
493
494 def ToHexSize(val):
495     """Return the size of an object in hex
496
497     Returns:
498         hex value of size, or 'None' if the value is None
499     """
500     return 'None' if val is None else '%#x' % len(val)