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