blob: d41115a22c4615d330de394c917fd63076bc98ad [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass1f1864b2016-07-25 18:59:08 -06002#
3# Copyright (c) 2016 Google, Inc
4#
Simon Glass1f1864b2016-07-25 18:59:08 -06005
Simon Glass0a98b282018-09-14 04:57:28 -06006import glob
Simon Glass1f1864b2016-07-25 18:59:08 -06007import os
8import shutil
Simon Glasseb0f4a42019-07-20 12:24:06 -06009import struct
Simon Glasse6d85ff2019-05-14 15:53:47 -060010import sys
Simon Glass1f1864b2016-07-25 18:59:08 -060011import tempfile
12
Simon Glassbf776672020-04-17 18:09:04 -060013from patman import command
14from patman import tout
Simon Glass1f1864b2016-07-25 18:59:08 -060015
Simon Glassaeffc5e2018-07-17 13:25:43 -060016# Output directly (generally this is temporary)
Simon Glass1f1864b2016-07-25 18:59:08 -060017outdir = None
Simon Glassaeffc5e2018-07-17 13:25:43 -060018
19# True to keep the output directory around after exiting
Simon Glass1f1864b2016-07-25 18:59:08 -060020preserve_outdir = False
21
Simon Glassaeffc5e2018-07-17 13:25:43 -060022# Path to the Chrome OS chroot, if we know it
23chroot_path = None
24
25# Search paths to use for Filename(), used to find files
26search_paths = []
27
Simon Glassc22b8cf2019-07-08 13:18:27 -060028tool_search_paths = []
29
Simon Glass04187a82018-09-14 04:57:25 -060030# Tools and the packages that contain them, on debian
31packages = {
32 'lz4': 'liblz4-tool',
33 }
Simon Glassaeffc5e2018-07-17 13:25:43 -060034
Simon Glass1fda1822018-10-01 21:12:44 -060035# List of paths to use when looking for an input file
36indir = []
37
Simon Glass1f1864b2016-07-25 18:59:08 -060038def 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
71def _RemoveOutputDir():
72 global outdir
73
74 shutil.rmtree(outdir)
75 tout.Debug("Deleted temporary directory '%s'" % outdir)
76 outdir = None
77
78def 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()
Simon Glass31353302019-07-20 12:24:07 -060084 outdir = None
Simon Glass1f1864b2016-07-25 18:59:08 -060085
86def 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
97def _FinaliseForTest():
98 """Remove the output directory (for use by tests)"""
99 global outdir
100
101 if outdir:
102 _RemoveOutputDir()
Simon Glass31353302019-07-20 12:24:07 -0600103 outdir = None
Simon Glass1f1864b2016-07-25 18:59:08 -0600104
105def SetInputDirs(dirname):
106 """Add a list of input directories, where input files are kept.
107
108 Args:
109 dirname: a list of paths to input directories to use for obtaining
110 files needed by binman to place in the image.
111 """
112 global indir
113
114 indir = dirname
115 tout.Debug("Using input directories %s" % indir)
116
Simon Glass4f9f1052020-07-09 18:39:38 -0600117def GetInputFilename(fname, allow_missing=False):
Simon Glass1f1864b2016-07-25 18:59:08 -0600118 """Return a filename for use as input.
119
120 Args:
121 fname: Filename to use for new file
Simon Glass4f9f1052020-07-09 18:39:38 -0600122 allow_missing: True if the filename can be missing
Simon Glass1f1864b2016-07-25 18:59:08 -0600123
124 Returns:
Simon Glass4f9f1052020-07-09 18:39:38 -0600125 The full path of the filename, within the input directory, or
126 None on error
Simon Glass1f1864b2016-07-25 18:59:08 -0600127 """
Simon Glassf514d8f2019-08-24 07:22:54 -0600128 if not indir or fname[:1] == '/':
Simon Glass1f1864b2016-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 Glass4f9f1052020-07-09 18:39:38 -0600135 if allow_missing:
136 return None
Simon Glass4f5dea42018-07-17 13:25:45 -0600137 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
138 (fname, ','.join(indir), os.getcwd()))
Simon Glass1f1864b2016-07-25 18:59:08 -0600139
Simon Glass0a98b282018-09-14 04:57:28 -0600140def GetInputFilenameGlob(pattern):
141 """Return a list of filenames for use as input.
142
143 Args:
144 pattern: Filename pattern to search for
145
146 Returns:
147 A list of matching files in all input directories
148 """
149 if not indir:
150 return glob.glob(fname)
151 files = []
152 for dirname in indir:
153 pathname = os.path.join(dirname, pattern)
154 files += glob.glob(pathname)
155 return sorted(files)
156
Simon Glass1f1864b2016-07-25 18:59:08 -0600157def Align(pos, align):
158 if align:
159 mask = align - 1
160 pos = (pos + mask) & ~mask
161 return pos
162
163def NotPowerOfTwo(num):
164 return num and (num & (num - 1))
Simon Glassaeffc5e2018-07-17 13:25:43 -0600165
Simon Glassc22b8cf2019-07-08 13:18:27 -0600166def SetToolPaths(toolpaths):
167 """Set the path to search for tools
168
169 Args:
170 toolpaths: List of paths to search for tools executed by Run()
171 """
172 global tool_search_paths
173
174 tool_search_paths = toolpaths
175
176def PathHasFile(path_spec, fname):
Simon Glass04187a82018-09-14 04:57:25 -0600177 """Check if a given filename is in the PATH
178
179 Args:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600180 path_spec: Value of PATH variable to check
Simon Glass04187a82018-09-14 04:57:25 -0600181 fname: Filename to check
182
183 Returns:
184 True if found, False if not
185 """
Simon Glassc22b8cf2019-07-08 13:18:27 -0600186 for dir in path_spec.split(':'):
Simon Glass04187a82018-09-14 04:57:25 -0600187 if os.path.exists(os.path.join(dir, fname)):
188 return True
189 return False
190
Simon Glass3b3e3c02019-10-31 07:42:50 -0600191def Run(name, *args, **kwargs):
Simon Glassc22b8cf2019-07-08 13:18:27 -0600192 """Run a tool with some arguments
193
194 This runs a 'tool', which is a program used by binman to process files and
195 perhaps produce some output. Tools can be located on the PATH or in a
196 search path.
197
198 Args:
199 name: Command name to run
200 args: Arguments to the tool
Simon Glassc22b8cf2019-07-08 13:18:27 -0600201
202 Returns:
203 CommandResult object
204 """
Simon Glass04187a82018-09-14 04:57:25 -0600205 try:
Simon Glass3b3e3c02019-10-31 07:42:50 -0600206 binary = kwargs.get('binary')
Simon Glassc22b8cf2019-07-08 13:18:27 -0600207 env = None
208 if tool_search_paths:
209 env = dict(os.environ)
210 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
Simon Glass6eace392019-08-24 07:22:42 -0600211 all_args = (name,) + args
212 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
Simon Glass3b3e3c02019-10-31 07:42:50 -0600213 env=env, raise_on_error=False, binary=binary)
Simon Glass6eace392019-08-24 07:22:42 -0600214 if result.return_code:
215 raise Exception("Error %d running '%s': %s" %
216 (result.return_code,' '.join(all_args),
217 result.stderr))
218 return result.stdout
Simon Glass04187a82018-09-14 04:57:25 -0600219 except:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600220 if env and not PathHasFile(env['PATH'], name):
221 msg = "Please install tool '%s'" % name
Simon Glass04187a82018-09-14 04:57:25 -0600222 package = packages.get(name)
223 if package:
224 msg += " (e.g. from package '%s')" % package
225 raise ValueError(msg)
226 raise
Simon Glassaeffc5e2018-07-17 13:25:43 -0600227
228def Filename(fname):
229 """Resolve a file path to an absolute path.
230
231 If fname starts with ##/ and chroot is available, ##/ gets replaced with
232 the chroot path. If chroot is not available, this file name can not be
233 resolved, `None' is returned.
234
235 If fname is not prepended with the above prefix, and is not an existing
236 file, the actual file name is retrieved from the passed in string and the
237 search_paths directories (if any) are searched to for the file. If found -
238 the path to the found file is returned, `None' is returned otherwise.
239
240 Args:
241 fname: a string, the path to resolve.
242
243 Returns:
244 Absolute path to the file or None if not found.
245 """
246 if fname.startswith('##/'):
247 if chroot_path:
248 fname = os.path.join(chroot_path, fname[3:])
249 else:
250 return None
251
252 # Search for a pathname that exists, and return it if found
253 if fname and not os.path.exists(fname):
254 for path in search_paths:
255 pathname = os.path.join(path, os.path.basename(fname))
256 if os.path.exists(pathname):
257 return pathname
258
259 # If not found, just return the standard, unchanged path
260 return fname
261
Simon Glass3c47e412019-05-17 22:00:44 -0600262def ReadFile(fname, binary=True):
Simon Glassaeffc5e2018-07-17 13:25:43 -0600263 """Read and return the contents of a file.
264
265 Args:
266 fname: path to filename to read, where ## signifiies the chroot.
267
268 Returns:
269 data read from file, as a string.
270 """
Simon Glass3c47e412019-05-17 22:00:44 -0600271 with open(Filename(fname), binary and 'rb' or 'r') as fd:
Simon Glassaeffc5e2018-07-17 13:25:43 -0600272 data = fd.read()
273 #self._out.Info("Read file '%s' size %d (%#0x)" %
274 #(fname, len(data), len(data)))
275 return data
276
Simon Glassfd709862020-07-05 21:41:50 -0600277def WriteFile(fname, data, binary=True):
Simon Glassaeffc5e2018-07-17 13:25:43 -0600278 """Write data into a file.
279
280 Args:
281 fname: path to filename to write
282 data: data to write to file, as a string
283 """
284 #self._out.Info("Write file '%s' size %d (%#0x)" %
285 #(fname, len(data), len(data)))
Simon Glassfd709862020-07-05 21:41:50 -0600286 with open(Filename(fname), binary and 'wb' or 'w') as fd:
Simon Glassaeffc5e2018-07-17 13:25:43 -0600287 fd.write(data)
Simon Glasse6d85ff2019-05-14 15:53:47 -0600288
289def GetBytes(byte, size):
290 """Get a string of bytes of a given size
291
292 This handles the unfortunate different between Python 2 and Python 2.
293
294 Args:
295 byte: Numeric byte value to use
296 size: Size of bytes/string to return
297
298 Returns:
299 A bytes type with 'byte' repeated 'size' times
300 """
301 if sys.version_info[0] >= 3:
302 data = bytes([byte]) * size
303 else:
304 data = chr(byte) * size
305 return data
Simon Glass513eace2019-05-14 15:53:50 -0600306
307def ToUnicode(val):
308 """Make sure a value is a unicode string
309
310 This allows some amount of compatibility between Python 2 and Python3. For
311 the former, it returns a unicode object.
312
313 Args:
314 val: string or unicode object
315
316 Returns:
317 unicode version of val
318 """
319 if sys.version_info[0] >= 3:
320 return val
321 return val if isinstance(val, unicode) else val.decode('utf-8')
322
323def FromUnicode(val):
324 """Make sure a value is a non-unicode string
325
326 This allows some amount of compatibility between Python 2 and Python3. For
327 the former, it converts a unicode object to a string.
328
329 Args:
330 val: string or unicode object
331
332 Returns:
333 non-unicode version of val
334 """
335 if sys.version_info[0] >= 3:
336 return val
337 return val if isinstance(val, str) else val.encode('utf-8')
Simon Glass2b6ed5e2019-05-17 22:00:35 -0600338
339def ToByte(ch):
340 """Convert a character to an ASCII value
341
342 This is useful because in Python 2 bytes is an alias for str, but in
343 Python 3 they are separate types. This function converts the argument to
344 an ASCII value in either case.
345
346 Args:
347 ch: A string (Python 2) or byte (Python 3) value
348
349 Returns:
350 integer ASCII value for ch
351 """
352 return ord(ch) if type(ch) == str else ch
353
354def ToChar(byte):
355 """Convert a byte to a character
356
357 This is useful because in Python 2 bytes is an alias for str, but in
358 Python 3 they are separate types. This function converts an ASCII value to
359 a value with the appropriate type in either case.
360
361 Args:
362 byte: A byte or str value
363 """
364 return chr(byte) if type(byte) != str else byte
Simon Glassf6b64812019-05-17 22:00:36 -0600365
366def ToChars(byte_list):
367 """Convert a list of bytes to a str/bytes type
368
369 Args:
370 byte_list: List of ASCII values representing the string
371
372 Returns:
373 string made by concatenating all the ASCII values
374 """
375 return ''.join([chr(byte) for byte in byte_list])
376
377def ToBytes(string):
378 """Convert a str type into a bytes type
379
380 Args:
Simon Glass3b3e3c02019-10-31 07:42:50 -0600381 string: string to convert
Simon Glassf6b64812019-05-17 22:00:36 -0600382
383 Returns:
384 Python 3: A bytes type
385 Python 2: A string type
386 """
387 if sys.version_info[0] >= 3:
388 return string.encode('utf-8')
389 return string
Simon Glass07d9e702019-07-08 13:18:41 -0600390
Simon Glass3b3e3c02019-10-31 07:42:50 -0600391def ToString(bval):
392 """Convert a bytes type into a str type
393
394 Args:
395 bval: bytes value to convert
396
397 Returns:
398 Python 3: A bytes type
399 Python 2: A string type
400 """
401 return bval.decode('utf-8')
402
Simon Glasseb0f4a42019-07-20 12:24:06 -0600403def Compress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600404 """Compress some data using a given algorithm
405
406 Note that for lzma this uses an old version of the algorithm, not that
407 provided by xz.
408
409 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
410 directory to be previously set up, by calling PrepareOutputDir().
411
412 Args:
413 indata: Input data to compress
414 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
415
416 Returns:
417 Compressed data
418 """
419 if algo == 'none':
420 return indata
421 fname = GetOutputFilename('%s.comp.tmp' % algo)
422 WriteFile(fname, indata)
423 if algo == 'lz4':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600424 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600425 # cbfstool uses a very old version of lzma
426 elif algo == 'lzma':
427 outfname = GetOutputFilename('%s.comp.otmp' % algo)
428 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
429 data = ReadFile(outfname)
430 elif algo == 'gzip':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600431 data = Run('gzip', '-c', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600432 else:
433 raise ValueError("Unknown algorithm '%s'" % algo)
Simon Glasseb0f4a42019-07-20 12:24:06 -0600434 if with_header:
435 hdr = struct.pack('<I', len(data))
436 data = hdr + data
Simon Glass07d9e702019-07-08 13:18:41 -0600437 return data
438
Simon Glasseb0f4a42019-07-20 12:24:06 -0600439def Decompress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600440 """Decompress some data using a given algorithm
441
442 Note that for lzma this uses an old version of the algorithm, not that
443 provided by xz.
444
445 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
446 directory to be previously set up, by calling PrepareOutputDir().
447
448 Args:
449 indata: Input data to decompress
450 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
451
452 Returns:
453 Compressed data
454 """
455 if algo == 'none':
456 return indata
Simon Glasseb0f4a42019-07-20 12:24:06 -0600457 if with_header:
458 data_len = struct.unpack('<I', indata[:4])[0]
459 indata = indata[4:4 + data_len]
Simon Glass07d9e702019-07-08 13:18:41 -0600460 fname = GetOutputFilename('%s.decomp.tmp' % algo)
461 with open(fname, 'wb') as fd:
462 fd.write(indata)
463 if algo == 'lz4':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600464 data = Run('lz4', '-dc', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600465 elif algo == 'lzma':
466 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
467 Run('lzma_alone', 'd', fname, outfname)
Simon Glass3b3e3c02019-10-31 07:42:50 -0600468 data = ReadFile(outfname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600469 elif algo == 'gzip':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600470 data = Run('gzip', '-cd', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600471 else:
472 raise ValueError("Unknown algorithm '%s'" % algo)
473 return data
Simon Glass1cfdfc02019-07-08 13:18:51 -0600474
475CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
476
477IFWITOOL_CMDS = {
478 CMD_CREATE: 'create',
479 CMD_DELETE: 'delete',
480 CMD_ADD: 'add',
481 CMD_REPLACE: 'replace',
482 CMD_EXTRACT: 'extract',
483 }
484
485def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
486 """Run ifwitool with the given arguments:
487
488 Args:
489 ifwi_file: IFWI file to operation on
490 cmd: Command to execute (CMD_...)
491 fname: Filename of file to add/replace/extract/create (None for
492 CMD_DELETE)
493 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
494 entry_name: Name of directory entry to operate on, or None if none
495 """
496 args = ['ifwitool', ifwi_file]
497 args.append(IFWITOOL_CMDS[cmd])
498 if fname:
499 args += ['-f', fname]
500 if subpart:
501 args += ['-n', subpart]
502 if entry_name:
503 args += ['-d', '-e', entry_name]
504 Run(*args)
Simon Glass9f297b02019-07-20 12:23:36 -0600505
506def ToHex(val):
507 """Convert an integer value (or None) to a string
508
509 Returns:
510 hex value, or 'None' if the value is None
511 """
512 return 'None' if val is None else '%#x' % val
513
514def ToHexSize(val):
515 """Return the size of an object in hex
516
517 Returns:
518 hex value of size, or 'None' if the value is None
519 """
520 return 'None' if val is None else '%#x' % len(val)