blob: 3feddb292fc70a5a250ab3de30cb01eb17a77403 [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glassbe39f052016-07-25 18:59:08 -06002#
3# Copyright (c) 2016 Google, Inc
4#
Simon Glassbe39f052016-07-25 18:59:08 -06005
Simon Glass578d53e2019-07-08 13:18:51 -06006from __future__ import print_function
7
Simon Glass0362cd22018-07-17 13:25:43 -06008import command
Simon Glassac6328c2018-09-14 04:57:28 -06009import glob
Simon Glassbe39f052016-07-25 18:59:08 -060010import os
11import shutil
Simon Glass37fdd142019-07-20 12:24:06 -060012import struct
Simon Glassac0d4952019-05-14 15:53:47 -060013import sys
Simon Glassbe39f052016-07-25 18:59:08 -060014import tempfile
15
16import tout
17
Simon Glass0362cd22018-07-17 13:25:43 -060018# Output directly (generally this is temporary)
Simon Glassbe39f052016-07-25 18:59:08 -060019outdir = None
Simon Glass0362cd22018-07-17 13:25:43 -060020
21# True to keep the output directory around after exiting
Simon Glassbe39f052016-07-25 18:59:08 -060022preserve_outdir = False
23
Simon Glass0362cd22018-07-17 13:25:43 -060024# Path to the Chrome OS chroot, if we know it
25chroot_path = None
26
27# Search paths to use for Filename(), used to find files
28search_paths = []
29
Simon Glass90c3c4c2019-07-08 13:18:27 -060030tool_search_paths = []
31
Simon Glass778ab842018-09-14 04:57:25 -060032# Tools and the packages that contain them, on debian
33packages = {
34 'lz4': 'liblz4-tool',
35 }
Simon Glass0362cd22018-07-17 13:25:43 -060036
Simon Glass867b9062018-10-01 21:12:44 -060037# List of paths to use when looking for an input file
38indir = []
39
Simon Glassbe39f052016-07-25 18:59:08 -060040def 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
73def _RemoveOutputDir():
74 global outdir
75
76 shutil.rmtree(outdir)
77 tout.Debug("Deleted temporary directory '%s'" % outdir)
78 outdir = None
79
80def 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()
Simon Glass7f47e082019-07-20 12:24:07 -060086 outdir = None
Simon Glassbe39f052016-07-25 18:59:08 -060087
88def 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
99def _FinaliseForTest():
100 """Remove the output directory (for use by tests)"""
101 global outdir
102
103 if outdir:
104 _RemoveOutputDir()
Simon Glass7f47e082019-07-20 12:24:07 -0600105 outdir = None
Simon Glassbe39f052016-07-25 18:59:08 -0600106
107def 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
119def 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 """
Simon Glass4affd4b2019-08-24 07:22:54 -0600128 if not indir or fname[:1] == '/':
Simon Glassbe39f052016-07-25 18:59:08 -0600129 return fname
130 for dirname in indir:
131 pathname = os.path.join(dirname, fname)
132 if os.path.exists(pathname):
133 return pathname
134
Simon Glassf2eb0542018-07-17 13:25:45 -0600135 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
136 (fname, ','.join(indir), os.getcwd()))
Simon Glassbe39f052016-07-25 18:59:08 -0600137
Simon Glassac6328c2018-09-14 04:57:28 -0600138def 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
Simon Glassbe39f052016-07-25 18:59:08 -0600155def Align(pos, align):
156 if align:
157 mask = align - 1
158 pos = (pos + mask) & ~mask
159 return pos
160
161def NotPowerOfTwo(num):
162 return num and (num & (num - 1))
Simon Glass0362cd22018-07-17 13:25:43 -0600163
Simon Glass90c3c4c2019-07-08 13:18:27 -0600164def 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
174def PathHasFile(path_spec, fname):
Simon Glass778ab842018-09-14 04:57:25 -0600175 """Check if a given filename is in the PATH
176
177 Args:
Simon Glass90c3c4c2019-07-08 13:18:27 -0600178 path_spec: Value of PATH variable to check
Simon Glass778ab842018-09-14 04:57:25 -0600179 fname: Filename to check
180
181 Returns:
182 True if found, False if not
183 """
Simon Glass90c3c4c2019-07-08 13:18:27 -0600184 for dir in path_spec.split(':'):
Simon Glass778ab842018-09-14 04:57:25 -0600185 if os.path.exists(os.path.join(dir, fname)):
186 return True
187 return False
188
Simon Glasscc311ac2019-10-31 07:42:50 -0600189def Run(name, *args, **kwargs):
Simon Glass90c3c4c2019-07-08 13:18:27 -0600190 """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
Simon Glass90c3c4c2019-07-08 13:18:27 -0600199
200 Returns:
201 CommandResult object
202 """
Simon Glass778ab842018-09-14 04:57:25 -0600203 try:
Simon Glasscc311ac2019-10-31 07:42:50 -0600204 binary = kwargs.get('binary')
Simon Glass90c3c4c2019-07-08 13:18:27 -0600205 env = None
206 if tool_search_paths:
207 env = dict(os.environ)
208 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
Simon Glass9f5051a2019-08-24 07:22:42 -0600209 all_args = (name,) + args
210 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
Simon Glasscc311ac2019-10-31 07:42:50 -0600211 env=env, raise_on_error=False, binary=binary)
Simon Glass9f5051a2019-08-24 07:22:42 -0600212 if result.return_code:
213 raise Exception("Error %d running '%s': %s" %
214 (result.return_code,' '.join(all_args),
215 result.stderr))
216 return result.stdout
Simon Glass778ab842018-09-14 04:57:25 -0600217 except:
Simon Glass90c3c4c2019-07-08 13:18:27 -0600218 if env and not PathHasFile(env['PATH'], name):
219 msg = "Please install tool '%s'" % name
Simon Glass778ab842018-09-14 04:57:25 -0600220 package = packages.get(name)
221 if package:
222 msg += " (e.g. from package '%s')" % package
223 raise ValueError(msg)
224 raise
Simon Glass0362cd22018-07-17 13:25:43 -0600225
226def Filename(fname):
227 """Resolve a file path to an absolute path.
228
229 If fname starts with ##/ and chroot is available, ##/ gets replaced with
230 the chroot path. If chroot is not available, this file name can not be
231 resolved, `None' is returned.
232
233 If fname is not prepended with the above prefix, and is not an existing
234 file, the actual file name is retrieved from the passed in string and the
235 search_paths directories (if any) are searched to for the file. If found -
236 the path to the found file is returned, `None' is returned otherwise.
237
238 Args:
239 fname: a string, the path to resolve.
240
241 Returns:
242 Absolute path to the file or None if not found.
243 """
244 if fname.startswith('##/'):
245 if chroot_path:
246 fname = os.path.join(chroot_path, fname[3:])
247 else:
248 return None
249
250 # Search for a pathname that exists, and return it if found
251 if fname and not os.path.exists(fname):
252 for path in search_paths:
253 pathname = os.path.join(path, os.path.basename(fname))
254 if os.path.exists(pathname):
255 return pathname
256
257 # If not found, just return the standard, unchanged path
258 return fname
259
Simon Glass0696ee62019-05-17 22:00:44 -0600260def ReadFile(fname, binary=True):
Simon Glass0362cd22018-07-17 13:25:43 -0600261 """Read and return the contents of a file.
262
263 Args:
264 fname: path to filename to read, where ## signifiies the chroot.
265
266 Returns:
267 data read from file, as a string.
268 """
Simon Glass0696ee62019-05-17 22:00:44 -0600269 with open(Filename(fname), binary and 'rb' or 'r') as fd:
Simon Glass0362cd22018-07-17 13:25:43 -0600270 data = fd.read()
271 #self._out.Info("Read file '%s' size %d (%#0x)" %
272 #(fname, len(data), len(data)))
273 return data
274
275def WriteFile(fname, data):
276 """Write data into a file.
277
278 Args:
279 fname: path to filename to write
280 data: data to write to file, as a string
281 """
282 #self._out.Info("Write file '%s' size %d (%#0x)" %
283 #(fname, len(data), len(data)))
284 with open(Filename(fname), 'wb') as fd:
285 fd.write(data)
Simon Glassac0d4952019-05-14 15:53:47 -0600286
287def GetBytes(byte, size):
288 """Get a string of bytes of a given size
289
290 This handles the unfortunate different between Python 2 and Python 2.
291
292 Args:
293 byte: Numeric byte value to use
294 size: Size of bytes/string to return
295
296 Returns:
297 A bytes type with 'byte' repeated 'size' times
298 """
299 if sys.version_info[0] >= 3:
300 data = bytes([byte]) * size
301 else:
302 data = chr(byte) * size
303 return data
Simon Glass8bb7a7a2019-05-14 15:53:50 -0600304
305def ToUnicode(val):
306 """Make sure a value is a unicode string
307
308 This allows some amount of compatibility between Python 2 and Python3. For
309 the former, it returns a unicode object.
310
311 Args:
312 val: string or unicode object
313
314 Returns:
315 unicode version of val
316 """
317 if sys.version_info[0] >= 3:
318 return val
319 return val if isinstance(val, unicode) else val.decode('utf-8')
320
321def FromUnicode(val):
322 """Make sure a value is a non-unicode string
323
324 This allows some amount of compatibility between Python 2 and Python3. For
325 the former, it converts a unicode object to a string.
326
327 Args:
328 val: string or unicode object
329
330 Returns:
331 non-unicode version of val
332 """
333 if sys.version_info[0] >= 3:
334 return val
335 return val if isinstance(val, str) else val.encode('utf-8')
Simon Glassd8f593f2019-05-17 22:00:35 -0600336
337def ToByte(ch):
338 """Convert a character to an ASCII value
339
340 This is useful because in Python 2 bytes is an alias for str, but in
341 Python 3 they are separate types. This function converts the argument to
342 an ASCII value in either case.
343
344 Args:
345 ch: A string (Python 2) or byte (Python 3) value
346
347 Returns:
348 integer ASCII value for ch
349 """
350 return ord(ch) if type(ch) == str else ch
351
352def ToChar(byte):
353 """Convert a byte to a character
354
355 This is useful because in Python 2 bytes is an alias for str, but in
356 Python 3 they are separate types. This function converts an ASCII value to
357 a value with the appropriate type in either case.
358
359 Args:
360 byte: A byte or str value
361 """
362 return chr(byte) if type(byte) != str else byte
Simon Glass1cd40082019-05-17 22:00:36 -0600363
364def ToChars(byte_list):
365 """Convert a list of bytes to a str/bytes type
366
367 Args:
368 byte_list: List of ASCII values representing the string
369
370 Returns:
371 string made by concatenating all the ASCII values
372 """
373 return ''.join([chr(byte) for byte in byte_list])
374
375def ToBytes(string):
376 """Convert a str type into a bytes type
377
378 Args:
Simon Glasscc311ac2019-10-31 07:42:50 -0600379 string: string to convert
Simon Glass1cd40082019-05-17 22:00:36 -0600380
381 Returns:
382 Python 3: A bytes type
383 Python 2: A string type
384 """
385 if sys.version_info[0] >= 3:
386 return string.encode('utf-8')
387 return string
Simon Glassdfd19012019-07-08 13:18:41 -0600388
Simon Glasscc311ac2019-10-31 07:42:50 -0600389def ToString(bval):
390 """Convert a bytes type into a str type
391
392 Args:
393 bval: bytes value to convert
394
395 Returns:
396 Python 3: A bytes type
397 Python 2: A string type
398 """
399 return bval.decode('utf-8')
400
Simon Glass37fdd142019-07-20 12:24:06 -0600401def Compress(indata, algo, with_header=True):
Simon Glassdfd19012019-07-08 13:18:41 -0600402 """Compress some data using a given algorithm
403
404 Note that for lzma this uses an old version of the algorithm, not that
405 provided by xz.
406
407 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
408 directory to be previously set up, by calling PrepareOutputDir().
409
410 Args:
411 indata: Input data to compress
412 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
413
414 Returns:
415 Compressed data
416 """
417 if algo == 'none':
418 return indata
419 fname = GetOutputFilename('%s.comp.tmp' % algo)
420 WriteFile(fname, indata)
421 if algo == 'lz4':
Simon Glasscc311ac2019-10-31 07:42:50 -0600422 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
Simon Glassdfd19012019-07-08 13:18:41 -0600423 # cbfstool uses a very old version of lzma
424 elif algo == 'lzma':
425 outfname = GetOutputFilename('%s.comp.otmp' % algo)
426 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
427 data = ReadFile(outfname)
428 elif algo == 'gzip':
Simon Glasscc311ac2019-10-31 07:42:50 -0600429 data = Run('gzip', '-c', fname, binary=True)
Simon Glassdfd19012019-07-08 13:18:41 -0600430 else:
431 raise ValueError("Unknown algorithm '%s'" % algo)
Simon Glass37fdd142019-07-20 12:24:06 -0600432 if with_header:
433 hdr = struct.pack('<I', len(data))
434 data = hdr + data
Simon Glassdfd19012019-07-08 13:18:41 -0600435 return data
436
Simon Glass37fdd142019-07-20 12:24:06 -0600437def Decompress(indata, algo, with_header=True):
Simon Glassdfd19012019-07-08 13:18:41 -0600438 """Decompress some data using a given algorithm
439
440 Note that for lzma this uses an old version of the algorithm, not that
441 provided by xz.
442
443 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
444 directory to be previously set up, by calling PrepareOutputDir().
445
446 Args:
447 indata: Input data to decompress
448 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
449
450 Returns:
451 Compressed data
452 """
453 if algo == 'none':
454 return indata
Simon Glass37fdd142019-07-20 12:24:06 -0600455 if with_header:
456 data_len = struct.unpack('<I', indata[:4])[0]
457 indata = indata[4:4 + data_len]
Simon Glassdfd19012019-07-08 13:18:41 -0600458 fname = GetOutputFilename('%s.decomp.tmp' % algo)
459 with open(fname, 'wb') as fd:
460 fd.write(indata)
461 if algo == 'lz4':
Simon Glasscc311ac2019-10-31 07:42:50 -0600462 data = Run('lz4', '-dc', fname, binary=True)
Simon Glassdfd19012019-07-08 13:18:41 -0600463 elif algo == 'lzma':
464 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
465 Run('lzma_alone', 'd', fname, outfname)
Simon Glasscc311ac2019-10-31 07:42:50 -0600466 data = ReadFile(outfname, binary=True)
Simon Glassdfd19012019-07-08 13:18:41 -0600467 elif algo == 'gzip':
Simon Glasscc311ac2019-10-31 07:42:50 -0600468 data = Run('gzip', '-cd', fname, binary=True)
Simon Glassdfd19012019-07-08 13:18:41 -0600469 else:
470 raise ValueError("Unknown algorithm '%s'" % algo)
471 return data
Simon Glass578d53e2019-07-08 13:18:51 -0600472
473CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
474
475IFWITOOL_CMDS = {
476 CMD_CREATE: 'create',
477 CMD_DELETE: 'delete',
478 CMD_ADD: 'add',
479 CMD_REPLACE: 'replace',
480 CMD_EXTRACT: 'extract',
481 }
482
483def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
484 """Run ifwitool with the given arguments:
485
486 Args:
487 ifwi_file: IFWI file to operation on
488 cmd: Command to execute (CMD_...)
489 fname: Filename of file to add/replace/extract/create (None for
490 CMD_DELETE)
491 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
492 entry_name: Name of directory entry to operate on, or None if none
493 """
494 args = ['ifwitool', ifwi_file]
495 args.append(IFWITOOL_CMDS[cmd])
496 if fname:
497 args += ['-f', fname]
498 if subpart:
499 args += ['-n', subpart]
500 if entry_name:
501 args += ['-d', '-e', entry_name]
502 Run(*args)
Simon Glassb6dff4c2019-07-20 12:23:36 -0600503
504def ToHex(val):
505 """Convert an integer value (or None) to a string
506
507 Returns:
508 hex value, or 'None' if the value is None
509 """
510 return 'None' if val is None else '%#x' % val
511
512def ToHexSize(val):
513 """Return the size of an object in hex
514
515 Returns:
516 hex value of size, or 'None' if the value is None
517 """
518 return 'None' if val is None else '%#x' % len(val)