]> Git Repo - J-u-boot.git/blob - tools/patman/tools.py
binman: Put compressed data into separate files
[J-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 GetOutputDir():
98     """Return the current output directory
99
100     Returns:
101         str: The output directory
102     """
103     return outdir
104
105 def _FinaliseForTest():
106     """Remove the output directory (for use by tests)"""
107     global outdir
108
109     if outdir:
110         _RemoveOutputDir()
111         outdir = None
112
113 def SetInputDirs(dirname):
114     """Add a list of input directories, where input files are kept.
115
116     Args:
117         dirname: a list of paths to input directories to use for obtaining
118                 files needed by binman to place in the image.
119     """
120     global indir
121
122     indir = dirname
123     tout.Debug("Using input directories %s" % indir)
124
125 def GetInputFilename(fname, allow_missing=False):
126     """Return a filename for use as input.
127
128     Args:
129         fname: Filename to use for new file
130         allow_missing: True if the filename can be missing
131
132     Returns:
133         fname, if indir is None;
134         full path of the filename, within the input directory;
135         None, if file is missing and allow_missing is True
136
137     Raises:
138         ValueError if file is missing and allow_missing is False
139     """
140     if not indir or fname[:1] == '/':
141         return fname
142     for dirname in indir:
143         pathname = os.path.join(dirname, fname)
144         if os.path.exists(pathname):
145             return pathname
146
147     if allow_missing:
148         return None
149     raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
150                      (fname, ','.join(indir), os.getcwd()))
151
152 def GetInputFilenameGlob(pattern):
153     """Return a list of filenames for use as input.
154
155     Args:
156         pattern: Filename pattern to search for
157
158     Returns:
159         A list of matching files in all input directories
160     """
161     if not indir:
162         return glob.glob(fname)
163     files = []
164     for dirname in indir:
165         pathname = os.path.join(dirname, pattern)
166         files += glob.glob(pathname)
167     return sorted(files)
168
169 def Align(pos, align):
170     if align:
171         mask = align - 1
172         pos = (pos + mask) & ~mask
173     return pos
174
175 def NotPowerOfTwo(num):
176     return num and (num & (num - 1))
177
178 def SetToolPaths(toolpaths):
179     """Set the path to search for tools
180
181     Args:
182         toolpaths: List of paths to search for tools executed by Run()
183     """
184     global tool_search_paths
185
186     tool_search_paths = toolpaths
187
188 def PathHasFile(path_spec, fname):
189     """Check if a given filename is in the PATH
190
191     Args:
192         path_spec: Value of PATH variable to check
193         fname: Filename to check
194
195     Returns:
196         True if found, False if not
197     """
198     for dir in path_spec.split(':'):
199         if os.path.exists(os.path.join(dir, fname)):
200             return True
201     return False
202
203 def GetHostCompileTool(name):
204     """Get the host-specific version for a compile tool
205
206     This checks the environment variables that specify which version of
207     the tool should be used (e.g. ${HOSTCC}).
208
209     The following table lists the host-specific versions of the tools
210     this function resolves to:
211
212         Compile Tool  | Host version
213         --------------+----------------
214         as            |  ${HOSTAS}
215         ld            |  ${HOSTLD}
216         cc            |  ${HOSTCC}
217         cpp           |  ${HOSTCPP}
218         c++           |  ${HOSTCXX}
219         ar            |  ${HOSTAR}
220         nm            |  ${HOSTNM}
221         ldr           |  ${HOSTLDR}
222         strip         |  ${HOSTSTRIP}
223         objcopy       |  ${HOSTOBJCOPY}
224         objdump       |  ${HOSTOBJDUMP}
225         dtc           |  ${HOSTDTC}
226
227     Args:
228         name: Command name to run
229
230     Returns:
231         host_name: Exact command name to run instead
232         extra_args: List of extra arguments to pass
233     """
234     host_name = None
235     extra_args = []
236     if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
237                 'objcopy', 'objdump', 'dtc'):
238         host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
239     elif name == 'c++':
240         host_name, *host_args = env.get('HOSTCXX', '').split(' ')
241
242     if host_name:
243         return host_name, extra_args
244     return name, []
245
246 def GetTargetCompileTool(name, cross_compile=None):
247     """Get the target-specific version for a compile tool
248
249     This first checks the environment variables that specify which
250     version of the tool should be used (e.g. ${CC}). If those aren't
251     specified, it checks the CROSS_COMPILE variable as a prefix for the
252     tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
253
254     The following table lists the target-specific versions of the tools
255     this function resolves to:
256
257         Compile Tool  | First choice   | Second choice
258         --------------+----------------+----------------------------
259         as            |  ${AS}         | ${CROSS_COMPILE}as
260         ld            |  ${LD}         | ${CROSS_COMPILE}ld.bfd
261                       |                |   or ${CROSS_COMPILE}ld
262         cc            |  ${CC}         | ${CROSS_COMPILE}gcc
263         cpp           |  ${CPP}        | ${CROSS_COMPILE}gcc -E
264         c++           |  ${CXX}        | ${CROSS_COMPILE}g++
265         ar            |  ${AR}         | ${CROSS_COMPILE}ar
266         nm            |  ${NM}         | ${CROSS_COMPILE}nm
267         ldr           |  ${LDR}        | ${CROSS_COMPILE}ldr
268         strip         |  ${STRIP}      | ${CROSS_COMPILE}strip
269         objcopy       |  ${OBJCOPY}    | ${CROSS_COMPILE}objcopy
270         objdump       |  ${OBJDUMP}    | ${CROSS_COMPILE}objdump
271         dtc           |  ${DTC}        | (no CROSS_COMPILE version)
272
273     Args:
274         name: Command name to run
275
276     Returns:
277         target_name: Exact command name to run instead
278         extra_args: List of extra arguments to pass
279     """
280     env = dict(os.environ)
281
282     target_name = None
283     extra_args = []
284     if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
285                 'objcopy', 'objdump', 'dtc'):
286         target_name, *extra_args = env.get(name.upper(), '').split(' ')
287     elif name == 'c++':
288         target_name, *extra_args = env.get('CXX', '').split(' ')
289
290     if target_name:
291         return target_name, extra_args
292
293     if cross_compile is None:
294         cross_compile = env.get('CROSS_COMPILE', '')
295
296     if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
297         target_name = cross_compile + name
298     elif name == 'ld':
299         try:
300             if Run(cross_compile + 'ld.bfd', '-v'):
301                 target_name = cross_compile + 'ld.bfd'
302         except:
303             target_name = cross_compile + 'ld'
304     elif name == 'cc':
305         target_name = cross_compile + 'gcc'
306     elif name == 'cpp':
307         target_name = cross_compile + 'gcc'
308         extra_args = ['-E']
309     elif name == 'c++':
310         target_name = cross_compile + 'g++'
311     else:
312         target_name = name
313     return target_name, extra_args
314
315 def Run(name, *args, **kwargs):
316     """Run a tool with some arguments
317
318     This runs a 'tool', which is a program used by binman to process files and
319     perhaps produce some output. Tools can be located on the PATH or in a
320     search path.
321
322     Args:
323         name: Command name to run
324         args: Arguments to the tool
325         for_host: True to resolve the command to the version for the host
326         for_target: False to run the command as-is, without resolving it
327                    to the version for the compile target
328
329     Returns:
330         CommandResult object
331     """
332     try:
333         binary = kwargs.get('binary')
334         for_host = kwargs.get('for_host', False)
335         for_target = kwargs.get('for_target', not for_host)
336         env = None
337         if tool_search_paths:
338             env = dict(os.environ)
339             env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
340         if for_target:
341             name, extra_args = GetTargetCompileTool(name)
342             args = tuple(extra_args) + args
343         elif for_host:
344             name, extra_args = GetHostCompileTool(name)
345             args = tuple(extra_args) + args
346         name = os.path.expanduser(name)  # Expand paths containing ~
347         all_args = (name,) + args
348         result = command.RunPipe([all_args], capture=True, capture_stderr=True,
349                                  env=env, raise_on_error=False, binary=binary)
350         if result.return_code:
351             raise Exception("Error %d running '%s': %s" %
352                (result.return_code,' '.join(all_args),
353                 result.stderr))
354         return result.stdout
355     except:
356         if env and not PathHasFile(env['PATH'], name):
357             msg = "Please install tool '%s'" % name
358             package = packages.get(name)
359             if package:
360                  msg += " (e.g. from package '%s')" % package
361             raise ValueError(msg)
362         raise
363
364 def Filename(fname):
365     """Resolve a file path to an absolute path.
366
367     If fname starts with ##/ and chroot is available, ##/ gets replaced with
368     the chroot path. If chroot is not available, this file name can not be
369     resolved, `None' is returned.
370
371     If fname is not prepended with the above prefix, and is not an existing
372     file, the actual file name is retrieved from the passed in string and the
373     search_paths directories (if any) are searched to for the file. If found -
374     the path to the found file is returned, `None' is returned otherwise.
375
376     Args:
377       fname: a string,  the path to resolve.
378
379     Returns:
380       Absolute path to the file or None if not found.
381     """
382     if fname.startswith('##/'):
383       if chroot_path:
384         fname = os.path.join(chroot_path, fname[3:])
385       else:
386         return None
387
388     # Search for a pathname that exists, and return it if found
389     if fname and not os.path.exists(fname):
390         for path in search_paths:
391             pathname = os.path.join(path, os.path.basename(fname))
392             if os.path.exists(pathname):
393                 return pathname
394
395     # If not found, just return the standard, unchanged path
396     return fname
397
398 def ReadFile(fname, binary=True):
399     """Read and return the contents of a file.
400
401     Args:
402       fname: path to filename to read, where ## signifiies the chroot.
403
404     Returns:
405       data read from file, as a string.
406     """
407     with open(Filename(fname), binary and 'rb' or 'r') as fd:
408         data = fd.read()
409     #self._out.Info("Read file '%s' size %d (%#0x)" %
410                    #(fname, len(data), len(data)))
411     return data
412
413 def WriteFile(fname, data, binary=True):
414     """Write data into a file.
415
416     Args:
417         fname: path to filename to write
418         data: data to write to file, as a string
419     """
420     #self._out.Info("Write file '%s' size %d (%#0x)" %
421                    #(fname, len(data), len(data)))
422     with open(Filename(fname), binary and 'wb' or 'w') as fd:
423         fd.write(data)
424
425 def GetBytes(byte, size):
426     """Get a string of bytes of a given size
427
428     Args:
429         byte: Numeric byte value to use
430         size: Size of bytes/string to return
431
432     Returns:
433         A bytes type with 'byte' repeated 'size' times
434     """
435     return bytes([byte]) * size
436
437 def ToBytes(string):
438     """Convert a str type into a bytes type
439
440     Args:
441         string: string to convert
442
443     Returns:
444         A bytes type
445     """
446     return string.encode('utf-8')
447
448 def ToString(bval):
449     """Convert a bytes type into a str type
450
451     Args:
452         bval: bytes value to convert
453
454     Returns:
455         Python 3: A bytes type
456         Python 2: A string type
457     """
458     return bval.decode('utf-8')
459
460 def Compress(indata, algo, with_header=True):
461     """Compress some data using a given algorithm
462
463     Note that for lzma this uses an old version of the algorithm, not that
464     provided by xz.
465
466     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
467     directory to be previously set up, by calling PrepareOutputDir().
468
469     Care is taken to use unique temporary files so that this function can be
470     called from multiple threads.
471
472     Args:
473         indata: Input data to compress
474         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
475
476     Returns:
477         Compressed data
478     """
479     if algo == 'none':
480         return indata
481     fname = tempfile.NamedTemporaryFile(prefix='%s.comp.tmp' % algo,
482                                         dir=outdir).name
483     WriteFile(fname, indata)
484     if algo == 'lz4':
485         data = Run('lz4', '--no-frame-crc', '-B4', '-5', '-c', fname,
486                    binary=True)
487     # cbfstool uses a very old version of lzma
488     elif algo == 'lzma':
489         outfname = tempfile.NamedTemporaryFile(prefix='%s.comp.otmp' % algo,
490                                                dir=outdir).name
491         Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
492         data = ReadFile(outfname)
493     elif algo == 'gzip':
494         data = Run('gzip', '-c', fname, binary=True)
495     else:
496         raise ValueError("Unknown algorithm '%s'" % algo)
497     if with_header:
498         hdr = struct.pack('<I', len(data))
499         data = hdr + data
500     return data
501
502 def Decompress(indata, algo, with_header=True):
503     """Decompress some data using a given algorithm
504
505     Note that for lzma this uses an old version of the algorithm, not that
506     provided by xz.
507
508     This requires 'lz4' and 'lzma_alone' tools. It also requires an output
509     directory to be previously set up, by calling PrepareOutputDir().
510
511     Args:
512         indata: Input data to decompress
513         algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
514
515     Returns:
516         Compressed data
517     """
518     if algo == 'none':
519         return indata
520     if with_header:
521         data_len = struct.unpack('<I', indata[:4])[0]
522         indata = indata[4:4 + data_len]
523     fname = GetOutputFilename('%s.decomp.tmp' % algo)
524     with open(fname, 'wb') as fd:
525         fd.write(indata)
526     if algo == 'lz4':
527         data = Run('lz4', '-dc', fname, binary=True)
528     elif algo == 'lzma':
529         outfname = GetOutputFilename('%s.decomp.otmp' % algo)
530         Run('lzma_alone', 'd', fname, outfname)
531         data = ReadFile(outfname, binary=True)
532     elif algo == 'gzip':
533         data = Run('gzip', '-cd', fname, binary=True)
534     else:
535         raise ValueError("Unknown algorithm '%s'" % algo)
536     return data
537
538 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
539
540 IFWITOOL_CMDS = {
541     CMD_CREATE: 'create',
542     CMD_DELETE: 'delete',
543     CMD_ADD: 'add',
544     CMD_REPLACE: 'replace',
545     CMD_EXTRACT: 'extract',
546     }
547
548 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
549     """Run ifwitool with the given arguments:
550
551     Args:
552         ifwi_file: IFWI file to operation on
553         cmd: Command to execute (CMD_...)
554         fname: Filename of file to add/replace/extract/create (None for
555             CMD_DELETE)
556         subpart: Name of sub-partition to operation on (None for CMD_CREATE)
557         entry_name: Name of directory entry to operate on, or None if none
558     """
559     args = ['ifwitool', ifwi_file]
560     args.append(IFWITOOL_CMDS[cmd])
561     if fname:
562         args += ['-f', fname]
563     if subpart:
564         args += ['-n', subpart]
565     if entry_name:
566         args += ['-d', '-e', entry_name]
567     Run(*args)
568
569 def ToHex(val):
570     """Convert an integer value (or None) to a string
571
572     Returns:
573         hex value, or 'None' if the value is None
574     """
575     return 'None' if val is None else '%#x' % val
576
577 def ToHexSize(val):
578     """Return the size of an object in hex
579
580     Returns:
581         hex value of size, or 'None' if the value is None
582     """
583     return 'None' if val is None else '%#x' % len(val)
This page took 0.061139 seconds and 4 git commands to generate.