Merge tag 'mips-pull-2019-10-25' of git://git.denx.de/u-boot-mips
[oweals/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 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     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):
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
200     Returns:
201         CommandResult object
202     """
203     try:
204         env = None
205         if tool_search_paths:
206             env = dict(os.environ)
207             env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
208         all_args = (name,) + args
209         result = command.RunPipe([all_args], capture=True, capture_stderr=True,
210                                  env=env, raise_on_error=False)
211         if result.return_code:
212             raise Exception("Error %d running '%s': %s" %
213                (result.return_code,' '.join(all_args),
214                 result.stderr))
215         return result.stdout
216     except:
217         if env and not PathHasFile(env['PATH'], name):
218             msg = "Please install tool '%s'" % name
219             package = packages.get(name)
220             if package:
221                  msg += " (e.g. from package '%s')" % package
222             raise ValueError(msg)
223         raise
224
225 def Filename(fname):
226     """Resolve a file path to an absolute path.
227
228     If fname starts with ##/ and chroot is available, ##/ gets replaced with
229     the chroot path. If chroot is not available, this file name can not be
230     resolved, `None' is returned.
231
232     If fname is not prepended with the above prefix, and is not an existing
233     file, the actual file name is retrieved from the passed in string and the
234     search_paths directories (if any) are searched to for the file. If found -
235     the path to the found file is returned, `None' is returned otherwise.
236
237     Args:
238       fname: a string,  the path to resolve.
239
240     Returns:
241       Absolute path to the file or None if not found.
242     """
243     if fname.startswith('##/'):
244       if chroot_path:
245         fname = os.path.join(chroot_path, fname[3:])
246       else:
247         return None
248
249     # Search for a pathname that exists, and return it if found
250     if fname and not os.path.exists(fname):
251         for path in search_paths:
252             pathname = os.path.join(path, os.path.basename(fname))
253             if os.path.exists(pathname):
254                 return pathname
255
256     # If not found, just return the standard, unchanged path
257     return fname
258
259 def ReadFile(fname, binary=True):
260     """Read and return the contents of a file.
261
262     Args:
263       fname: path to filename to read, where ## signifiies the chroot.
264
265     Returns:
266       data read from file, as a string.
267     """
268     with open(Filename(fname), binary and 'rb' or 'r') as fd:
269         data = fd.read()
270     #self._out.Info("Read file '%s' size %d (%#0x)" %
271                    #(fname, len(data), len(data)))
272     return data
273
274 def WriteFile(fname, data):
275     """Write data into a file.
276
277     Args:
278         fname: path to filename to write
279         data: data to write to file, as a string
280     """
281     #self._out.Info("Write file '%s' size %d (%#0x)" %
282                    #(fname, len(data), len(data)))
283     with open(Filename(fname), 'wb') as fd:
284         fd.write(data)
285
286 def GetBytes(byte, size):
287     """Get a string of bytes of a given size
288
289     This handles the unfortunate different between Python 2 and Python 2.
290
291     Args:
292         byte: Numeric byte value to use
293         size: Size of bytes/string to return
294
295     Returns:
296         A bytes type with 'byte' repeated 'size' times
297     """
298     if sys.version_info[0] >= 3:
299         data = bytes([byte]) * size
300     else:
301         data = chr(byte) * size
302     return data
303
304 def ToUnicode(val):
305     """Make sure a value is a unicode string
306
307     This allows some amount of compatibility between Python 2 and Python3. For
308     the former, it returns a unicode object.
309
310     Args:
311         val: string or unicode object
312
313     Returns:
314         unicode version of val
315     """
316     if sys.version_info[0] >= 3:
317         return val
318     return val if isinstance(val, unicode) else val.decode('utf-8')
319
320 def FromUnicode(val):
321     """Make sure a value is a non-unicode string
322
323     This allows some amount of compatibility between Python 2 and Python3. For
324     the former, it converts a unicode object to a string.
325
326     Args:
327         val: string or unicode object
328
329     Returns:
330         non-unicode version of val
331     """
332     if sys.version_info[0] >= 3:
333         return val
334     return val if isinstance(val, str) else val.encode('utf-8')
335
336 def ToByte(ch):
337     """Convert a character to an ASCII value
338
339     This is useful because in Python 2 bytes is an alias for str, but in
340     Python 3 they are separate types. This function converts the argument to
341     an ASCII value in either case.
342
343     Args:
344         ch: A string (Python 2) or byte (Python 3) value
345
346     Returns:
347         integer ASCII value for ch
348     """
349     return ord(ch) if type(ch) == str else ch
350
351 def ToChar(byte):
352     """Convert a byte to a character
353
354     This is useful because in Python 2 bytes is an alias for str, but in
355     Python 3 they are separate types. This function converts an ASCII value to
356     a value with the appropriate type in either case.
357
358     Args:
359         byte: A byte or str value
360     """
361     return chr(byte) if type(byte) != str else byte
362
363 def ToChars(byte_list):
364     """Convert a list of bytes to a str/bytes type
365
366     Args:
367         byte_list: List of ASCII values representing the string
368
369     Returns:
370         string made by concatenating all the ASCII values
371     """
372     return ''.join([chr(byte) for byte in byte_list])
373
374 def ToBytes(string):
375     """Convert a str type into a bytes type
376
377     Args:
378         string: string to convert value
379
380     Returns:
381         Python 3: A bytes type
382         Python 2: A string type
383     """
384     if sys.version_info[0] >= 3:
385         return string.encode('utf-8')
386     return string
387
388 def Compress(indata, algo, with_header=True):
389     """Compress some data using a given algorithm
390
391     Note that for lzma this uses an old version of the algorithm, not that
392     provided by xz.
393
394     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
395     directory to be previously set up, by calling PrepareOutputDir().
396
397     Args:
398         indata: Input data to compress
399         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
400
401     Returns:
402         Compressed data
403     """
404     if algo == 'none':
405         return indata
406     fname = GetOutputFilename('%s.comp.tmp' % algo)
407     WriteFile(fname, indata)
408     if algo == 'lz4':
409         data = Run('lz4', '--no-frame-crc', '-c', fname)
410     # cbfstool uses a very old version of lzma
411     elif algo == 'lzma':
412         outfname = GetOutputFilename('%s.comp.otmp' % algo)
413         Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
414         data = ReadFile(outfname)
415     elif algo == 'gzip':
416         data = Run('gzip', '-c', fname)
417     else:
418         raise ValueError("Unknown algorithm '%s'" % algo)
419     if with_header:
420         hdr = struct.pack('<I', len(data))
421         data = hdr + data
422     return data
423
424 def Decompress(indata, algo, with_header=True):
425     """Decompress some data using a given algorithm
426
427     Note that for lzma this uses an old version of the algorithm, not that
428     provided by xz.
429
430     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
431     directory to be previously set up, by calling PrepareOutputDir().
432
433     Args:
434         indata: Input data to decompress
435         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
436
437     Returns:
438         Compressed data
439     """
440     if algo == 'none':
441         return indata
442     if with_header:
443         data_len = struct.unpack('<I', indata[:4])[0]
444         indata = indata[4:4 + data_len]
445     fname = GetOutputFilename('%s.decomp.tmp' % algo)
446     with open(fname, 'wb') as fd:
447         fd.write(indata)
448     if algo == 'lz4':
449         data = Run('lz4', '-dc', fname)
450     elif algo == 'lzma':
451         outfname = GetOutputFilename('%s.decomp.otmp' % algo)
452         Run('lzma_alone', 'd', fname, outfname)
453         data = ReadFile(outfname)
454     elif algo == 'gzip':
455         data = Run('gzip', '-cd', fname)
456     else:
457         raise ValueError("Unknown algorithm '%s'" % algo)
458     return data
459
460 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
461
462 IFWITOOL_CMDS = {
463     CMD_CREATE: 'create',
464     CMD_DELETE: 'delete',
465     CMD_ADD: 'add',
466     CMD_REPLACE: 'replace',
467     CMD_EXTRACT: 'extract',
468     }
469
470 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
471     """Run ifwitool with the given arguments:
472
473     Args:
474         ifwi_file: IFWI file to operation on
475         cmd: Command to execute (CMD_...)
476         fname: Filename of file to add/replace/extract/create (None for
477             CMD_DELETE)
478         subpart: Name of sub-partition to operation on (None for CMD_CREATE)
479         entry_name: Name of directory entry to operate on, or None if none
480     """
481     args = ['ifwitool', ifwi_file]
482     args.append(IFWITOOL_CMDS[cmd])
483     if fname:
484         args += ['-f', fname]
485     if subpart:
486         args += ['-n', subpart]
487     if entry_name:
488         args += ['-d', '-e', entry_name]
489     Run(*args)
490
491 def ToHex(val):
492     """Convert an integer value (or None) to a string
493
494     Returns:
495         hex value, or 'None' if the value is None
496     """
497     return 'None' if val is None else '%#x' % val
498
499 def ToHexSize(val):
500     """Return the size of an object in hex
501
502     Returns:
503         hex value of size, or 'None' if the value is None
504     """
505     return 'None' if val is None else '%#x' % len(val)