blob: 69d03d38608dc7206d8c36851d80c88914d19fa3 [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 Glassaeffc5e2018-07-17 13:25:43 -06006import command
Simon Glass0a98b282018-09-14 04:57:28 -06007import glob
Simon Glass1f1864b2016-07-25 18:59:08 -06008import os
9import shutil
Simon Glasse6d85ff2019-05-14 15:53:47 -060010import sys
Simon Glass1f1864b2016-07-25 18:59:08 -060011import tempfile
12
13import tout
14
Simon Glassaeffc5e2018-07-17 13:25:43 -060015# Output directly (generally this is temporary)
Simon Glass1f1864b2016-07-25 18:59:08 -060016outdir = None
Simon Glassaeffc5e2018-07-17 13:25:43 -060017
18# True to keep the output directory around after exiting
Simon Glass1f1864b2016-07-25 18:59:08 -060019preserve_outdir = False
20
Simon Glassaeffc5e2018-07-17 13:25:43 -060021# Path to the Chrome OS chroot, if we know it
22chroot_path = None
23
24# Search paths to use for Filename(), used to find files
25search_paths = []
26
Simon Glassc22b8cf2019-07-08 13:18:27 -060027tool_search_paths = []
28
Simon Glass04187a82018-09-14 04:57:25 -060029# Tools and the packages that contain them, on debian
30packages = {
31 'lz4': 'liblz4-tool',
32 }
Simon Glassaeffc5e2018-07-17 13:25:43 -060033
Simon Glass1fda1822018-10-01 21:12:44 -060034# List of paths to use when looking for an input file
35indir = []
36
Simon Glass1f1864b2016-07-25 18:59:08 -060037def PrepareOutputDir(dirname, preserve=False):
38 """Select an output directory, ensuring it exists.
39
40 This either creates a temporary directory or checks that the one supplied
41 by the user is valid. For a temporary directory, it makes a note to
42 remove it later if required.
43
44 Args:
45 dirname: a string, name of the output directory to use to store
46 intermediate and output files. If is None - create a temporary
47 directory.
48 preserve: a Boolean. If outdir above is None and preserve is False, the
49 created temporary directory will be destroyed on exit.
50
51 Raises:
52 OSError: If it cannot create the output directory.
53 """
54 global outdir, preserve_outdir
55
56 preserve_outdir = dirname or preserve
57 if dirname:
58 outdir = dirname
59 if not os.path.isdir(outdir):
60 try:
61 os.makedirs(outdir)
62 except OSError as err:
63 raise CmdError("Cannot make output directory '%s': '%s'" %
64 (outdir, err.strerror))
65 tout.Debug("Using output directory '%s'" % outdir)
66 else:
67 outdir = tempfile.mkdtemp(prefix='binman.')
68 tout.Debug("Using temporary directory '%s'" % outdir)
69
70def _RemoveOutputDir():
71 global outdir
72
73 shutil.rmtree(outdir)
74 tout.Debug("Deleted temporary directory '%s'" % outdir)
75 outdir = None
76
77def FinaliseOutputDir():
78 global outdir, preserve_outdir
79
80 """Tidy up: delete output directory if temporary and not preserved."""
81 if outdir and not preserve_outdir:
82 _RemoveOutputDir()
83
84def GetOutputFilename(fname):
85 """Return a filename within the output directory.
86
87 Args:
88 fname: Filename to use for new file
89
90 Returns:
91 The full path of the filename, within the output directory
92 """
93 return os.path.join(outdir, fname)
94
95def _FinaliseForTest():
96 """Remove the output directory (for use by tests)"""
97 global outdir
98
99 if outdir:
100 _RemoveOutputDir()
101
102def SetInputDirs(dirname):
103 """Add a list of input directories, where input files are kept.
104
105 Args:
106 dirname: a list of paths to input directories to use for obtaining
107 files needed by binman to place in the image.
108 """
109 global indir
110
111 indir = dirname
112 tout.Debug("Using input directories %s" % indir)
113
114def GetInputFilename(fname):
115 """Return a filename for use as input.
116
117 Args:
118 fname: Filename to use for new file
119
120 Returns:
121 The full path of the filename, within the input directory
122 """
123 if not indir:
124 return fname
125 for dirname in indir:
126 pathname = os.path.join(dirname, fname)
127 if os.path.exists(pathname):
128 return pathname
129
Simon Glass4f5dea42018-07-17 13:25:45 -0600130 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
131 (fname, ','.join(indir), os.getcwd()))
Simon Glass1f1864b2016-07-25 18:59:08 -0600132
Simon Glass0a98b282018-09-14 04:57:28 -0600133def GetInputFilenameGlob(pattern):
134 """Return a list of filenames for use as input.
135
136 Args:
137 pattern: Filename pattern to search for
138
139 Returns:
140 A list of matching files in all input directories
141 """
142 if not indir:
143 return glob.glob(fname)
144 files = []
145 for dirname in indir:
146 pathname = os.path.join(dirname, pattern)
147 files += glob.glob(pathname)
148 return sorted(files)
149
Simon Glass1f1864b2016-07-25 18:59:08 -0600150def Align(pos, align):
151 if align:
152 mask = align - 1
153 pos = (pos + mask) & ~mask
154 return pos
155
156def NotPowerOfTwo(num):
157 return num and (num & (num - 1))
Simon Glassaeffc5e2018-07-17 13:25:43 -0600158
Simon Glassc22b8cf2019-07-08 13:18:27 -0600159def SetToolPaths(toolpaths):
160 """Set the path to search for tools
161
162 Args:
163 toolpaths: List of paths to search for tools executed by Run()
164 """
165 global tool_search_paths
166
167 tool_search_paths = toolpaths
168
169def PathHasFile(path_spec, fname):
Simon Glass04187a82018-09-14 04:57:25 -0600170 """Check if a given filename is in the PATH
171
172 Args:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600173 path_spec: Value of PATH variable to check
Simon Glass04187a82018-09-14 04:57:25 -0600174 fname: Filename to check
175
176 Returns:
177 True if found, False if not
178 """
Simon Glassc22b8cf2019-07-08 13:18:27 -0600179 for dir in path_spec.split(':'):
Simon Glass04187a82018-09-14 04:57:25 -0600180 if os.path.exists(os.path.join(dir, fname)):
181 return True
182 return False
183
Simon Glassa92939a2019-05-14 15:53:44 -0600184def Run(name, *args, **kwargs):
Simon Glassc22b8cf2019-07-08 13:18:27 -0600185 """Run a tool with some arguments
186
187 This runs a 'tool', which is a program used by binman to process files and
188 perhaps produce some output. Tools can be located on the PATH or in a
189 search path.
190
191 Args:
192 name: Command name to run
193 args: Arguments to the tool
194 kwargs: Options to pass to command.run()
195
196 Returns:
197 CommandResult object
198 """
Simon Glass04187a82018-09-14 04:57:25 -0600199 try:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600200 env = None
201 if tool_search_paths:
202 env = dict(os.environ)
203 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
204 return command.Run(name, *args, capture=True,
205 capture_stderr=True, env=env, **kwargs)
Simon Glass04187a82018-09-14 04:57:25 -0600206 except:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600207 if env and not PathHasFile(env['PATH'], name):
208 msg = "Please install tool '%s'" % name
Simon Glass04187a82018-09-14 04:57:25 -0600209 package = packages.get(name)
210 if package:
211 msg += " (e.g. from package '%s')" % package
212 raise ValueError(msg)
213 raise
Simon Glassaeffc5e2018-07-17 13:25:43 -0600214
215def Filename(fname):
216 """Resolve a file path to an absolute path.
217
218 If fname starts with ##/ and chroot is available, ##/ gets replaced with
219 the chroot path. If chroot is not available, this file name can not be
220 resolved, `None' is returned.
221
222 If fname is not prepended with the above prefix, and is not an existing
223 file, the actual file name is retrieved from the passed in string and the
224 search_paths directories (if any) are searched to for the file. If found -
225 the path to the found file is returned, `None' is returned otherwise.
226
227 Args:
228 fname: a string, the path to resolve.
229
230 Returns:
231 Absolute path to the file or None if not found.
232 """
233 if fname.startswith('##/'):
234 if chroot_path:
235 fname = os.path.join(chroot_path, fname[3:])
236 else:
237 return None
238
239 # Search for a pathname that exists, and return it if found
240 if fname and not os.path.exists(fname):
241 for path in search_paths:
242 pathname = os.path.join(path, os.path.basename(fname))
243 if os.path.exists(pathname):
244 return pathname
245
246 # If not found, just return the standard, unchanged path
247 return fname
248
Simon Glass3c47e412019-05-17 22:00:44 -0600249def ReadFile(fname, binary=True):
Simon Glassaeffc5e2018-07-17 13:25:43 -0600250 """Read and return the contents of a file.
251
252 Args:
253 fname: path to filename to read, where ## signifiies the chroot.
254
255 Returns:
256 data read from file, as a string.
257 """
Simon Glass3c47e412019-05-17 22:00:44 -0600258 with open(Filename(fname), binary and 'rb' or 'r') as fd:
Simon Glassaeffc5e2018-07-17 13:25:43 -0600259 data = fd.read()
260 #self._out.Info("Read file '%s' size %d (%#0x)" %
261 #(fname, len(data), len(data)))
262 return data
263
264def WriteFile(fname, data):
265 """Write data into a file.
266
267 Args:
268 fname: path to filename to write
269 data: data to write to file, as a string
270 """
271 #self._out.Info("Write file '%s' size %d (%#0x)" %
272 #(fname, len(data), len(data)))
273 with open(Filename(fname), 'wb') as fd:
274 fd.write(data)
Simon Glasse6d85ff2019-05-14 15:53:47 -0600275
276def GetBytes(byte, size):
277 """Get a string of bytes of a given size
278
279 This handles the unfortunate different between Python 2 and Python 2.
280
281 Args:
282 byte: Numeric byte value to use
283 size: Size of bytes/string to return
284
285 Returns:
286 A bytes type with 'byte' repeated 'size' times
287 """
288 if sys.version_info[0] >= 3:
289 data = bytes([byte]) * size
290 else:
291 data = chr(byte) * size
292 return data
Simon Glass513eace2019-05-14 15:53:50 -0600293
294def ToUnicode(val):
295 """Make sure a value is a unicode string
296
297 This allows some amount of compatibility between Python 2 and Python3. For
298 the former, it returns a unicode object.
299
300 Args:
301 val: string or unicode object
302
303 Returns:
304 unicode version of val
305 """
306 if sys.version_info[0] >= 3:
307 return val
308 return val if isinstance(val, unicode) else val.decode('utf-8')
309
310def FromUnicode(val):
311 """Make sure a value is a non-unicode string
312
313 This allows some amount of compatibility between Python 2 and Python3. For
314 the former, it converts a unicode object to a string.
315
316 Args:
317 val: string or unicode object
318
319 Returns:
320 non-unicode version of val
321 """
322 if sys.version_info[0] >= 3:
323 return val
324 return val if isinstance(val, str) else val.encode('utf-8')
Simon Glass2b6ed5e2019-05-17 22:00:35 -0600325
326def ToByte(ch):
327 """Convert a character to an ASCII value
328
329 This is useful because in Python 2 bytes is an alias for str, but in
330 Python 3 they are separate types. This function converts the argument to
331 an ASCII value in either case.
332
333 Args:
334 ch: A string (Python 2) or byte (Python 3) value
335
336 Returns:
337 integer ASCII value for ch
338 """
339 return ord(ch) if type(ch) == str else ch
340
341def ToChar(byte):
342 """Convert a byte to a character
343
344 This is useful because in Python 2 bytes is an alias for str, but in
345 Python 3 they are separate types. This function converts an ASCII value to
346 a value with the appropriate type in either case.
347
348 Args:
349 byte: A byte or str value
350 """
351 return chr(byte) if type(byte) != str else byte
Simon Glassf6b64812019-05-17 22:00:36 -0600352
353def ToChars(byte_list):
354 """Convert a list of bytes to a str/bytes type
355
356 Args:
357 byte_list: List of ASCII values representing the string
358
359 Returns:
360 string made by concatenating all the ASCII values
361 """
362 return ''.join([chr(byte) for byte in byte_list])
363
364def ToBytes(string):
365 """Convert a str type into a bytes type
366
367 Args:
368 string: string to convert value
369
370 Returns:
371 Python 3: A bytes type
372 Python 2: A string type
373 """
374 if sys.version_info[0] >= 3:
375 return string.encode('utf-8')
376 return string
Simon Glass07d9e702019-07-08 13:18:41 -0600377
378def Compress(indata, algo):
379 """Compress some data using a given algorithm
380
381 Note that for lzma this uses an old version of the algorithm, not that
382 provided by xz.
383
384 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
385 directory to be previously set up, by calling PrepareOutputDir().
386
387 Args:
388 indata: Input data to compress
389 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
390
391 Returns:
392 Compressed data
393 """
394 if algo == 'none':
395 return indata
396 fname = GetOutputFilename('%s.comp.tmp' % algo)
397 WriteFile(fname, indata)
398 if algo == 'lz4':
399 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
400 # cbfstool uses a very old version of lzma
401 elif algo == 'lzma':
402 outfname = GetOutputFilename('%s.comp.otmp' % algo)
403 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
404 data = ReadFile(outfname)
405 elif algo == 'gzip':
406 data = Run('gzip', '-c', fname, binary=True)
407 else:
408 raise ValueError("Unknown algorithm '%s'" % algo)
409 return data
410
411def Decompress(indata, algo):
412 """Decompress some data using a given algorithm
413
414 Note that for lzma this uses an old version of the algorithm, not that
415 provided by xz.
416
417 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
418 directory to be previously set up, by calling PrepareOutputDir().
419
420 Args:
421 indata: Input data to decompress
422 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
423
424 Returns:
425 Compressed data
426 """
427 if algo == 'none':
428 return indata
429 fname = GetOutputFilename('%s.decomp.tmp' % algo)
430 with open(fname, 'wb') as fd:
431 fd.write(indata)
432 if algo == 'lz4':
433 data = Run('lz4', '-dc', fname, binary=True)
434 elif algo == 'lzma':
435 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
436 Run('lzma_alone', 'd', fname, outfname)
437 data = ReadFile(outfname)
438 elif algo == 'gzip':
439 data = Run('gzip', '-cd', fname, binary=True)
440 else:
441 raise ValueError("Unknown algorithm '%s'" % algo)
442 return data