1# SPDX-License-Identifier: GPL-2.0+
2#
3# Copyright (c) 2016 Google, Inc
4#
5
6import glob
7import os
8import shlex
9import shutil
10import sys
11import tempfile
12import urllib.request
13
14from u_boot_pylib import command
15from u_boot_pylib import tout
16
17# Output directly (generally this is temporary)
18outdir = None
19
20# True to keep the output directory around after exiting
21preserve_outdir = False
22
23# Path to the Chrome OS chroot, if we know it
24chroot_path = None
25
26# Search paths to use for filename(), used to find files
27search_paths = []
28
29tool_search_paths = []
30
31# Tools and the packages that contain them, on debian
32packages = {
33    'lz4': 'liblz4-tool',
34    }
35
36# List of paths to use when looking for an input file
37indir = []
38
39def prepare_output_dir(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 ValueError(
66                    f"Cannot make output directory '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
72def _remove_output_dir():
73    global outdir
74
75    shutil.rmtree(outdir)
76    tout.debug("Deleted temporary directory '%s'" % outdir)
77    outdir = None
78
79def finalise_output_dir():
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        _remove_output_dir()
85        outdir = None
86
87def get_output_filename(fname):
88    """Return a filename within the output directory.
89
90    Args:
91        fname: Filename to use for new file
92
93    Returns:
94        The full path of the filename, within the output directory
95    """
96    return os.path.join(outdir, fname)
97
98def get_output_dir():
99    """Return the current output directory
100
101    Returns:
102        str: The output directory
103    """
104    return outdir
105
106def _finalise_for_test():
107    """Remove the output directory (for use by tests)"""
108    global outdir
109
110    if outdir:
111        _remove_output_dir()
112        outdir = None
113
114def set_input_dirs(dirname):
115    """Add a list of input directories, where input files are kept.
116
117    Args:
118        dirname: a list of paths to input directories to use for obtaining
119                files needed by binman to place in the image.
120    """
121    global indir
122
123    indir = dirname
124    tout.debug("Using input directories %s" % indir)
125
126def get_input_filename(fname, allow_missing=False):
127    """Return a filename for use as input.
128
129    Args:
130        fname: Filename to use for new file
131        allow_missing: True if the filename can be missing
132
133    Returns:
134        fname, if indir is None;
135        full path of the filename, within the input directory;
136        None, if file is missing and allow_missing is True
137
138    Raises:
139        ValueError if file is missing and allow_missing is False
140    """
141    if not indir or fname[:1] == '/':
142        return fname
143    for dirname in indir:
144        pathname = os.path.join(dirname, fname)
145        if os.path.exists(pathname):
146            return pathname
147
148    if allow_missing:
149        return None
150    raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
151                     (fname, ','.join(indir), os.getcwd()))
152
153def get_input_filename_glob(pattern):
154    """Return a list of filenames for use as input.
155
156    Args:
157        pattern: Filename pattern to search for
158
159    Returns:
160        A list of matching files in all input directories
161    """
162    if not indir:
163        return glob.glob(pattern)
164    files = []
165    for dirname in indir:
166        pathname = os.path.join(dirname, pattern)
167        files += glob.glob(pathname)
168    return sorted(files)
169
170def align(pos, align):
171    if align:
172        mask = align - 1
173        pos = (pos + mask) & ~mask
174    return pos
175
176def not_power_of_two(num):
177    return num and (num & (num - 1))
178
179def set_tool_paths(toolpaths):
180    """Set the path to search for tools
181
182    Args:
183        toolpaths: List of paths to search for tools executed by run()
184    """
185    global tool_search_paths
186
187    tool_search_paths = toolpaths
188
189def path_has_file(path_spec, fname):
190    """Check if a given filename is in the PATH
191
192    Args:
193        path_spec: Value of PATH variable to check
194        fname: Filename to check
195
196    Returns:
197        True if found, False if not
198    """
199    for dir in path_spec.split(':'):
200        if os.path.exists(os.path.join(dir, fname)):
201            return True
202    return False
203
204def get_host_compile_tool(env, name):
205    """Get the host-specific version for a compile tool
206
207    This checks the environment variables that specify which version of
208    the tool should be used (e.g. ${HOSTCC}).
209
210    The following table lists the host-specific versions of the tools
211    this function resolves to:
212
213        Compile Tool  | Host version
214        --------------+----------------
215        as            |  ${HOSTAS}
216        ld            |  ${HOSTLD}
217        cc            |  ${HOSTCC}
218        cpp           |  ${HOSTCPP}
219        c++           |  ${HOSTCXX}
220        ar            |  ${HOSTAR}
221        nm            |  ${HOSTNM}
222        ldr           |  ${HOSTLDR}
223        strip         |  ${HOSTSTRIP}
224        objcopy       |  ${HOSTOBJCOPY}
225        objdump       |  ${HOSTOBJDUMP}
226        dtc           |  ${HOSTDTC}
227
228    Args:
229        name: Command name to run
230
231    Returns:
232        host_name: Exact command name to run instead
233        extra_args: List of extra arguments to pass
234    """
235    host_name = None
236    extra_args = []
237    if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
238                'objcopy', 'objdump', 'dtc'):
239        host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
240    elif name == 'c++':
241        host_name, *host_args = env.get('HOSTCXX', '').split(' ')
242
243    if host_name:
244        return host_name, extra_args
245    return name, []
246
247def get_target_compile_tool(name, cross_compile=None):
248    """Get the target-specific version for a compile tool
249
250    This first checks the environment variables that specify which
251    version of the tool should be used (e.g. ${CC}). If those aren't
252    specified, it checks the CROSS_COMPILE variable as a prefix for the
253    tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
254
255    The following table lists the target-specific versions of the tools
256    this function resolves to:
257
258        Compile Tool  | First choice   | Second choice
259        --------------+----------------+----------------------------
260        as            |  ${AS}         | ${CROSS_COMPILE}as
261        ld            |  ${LD}         | ${CROSS_COMPILE}ld.bfd
262                      |                |   or ${CROSS_COMPILE}ld
263        cc            |  ${CC}         | ${CROSS_COMPILE}gcc
264        cpp           |  ${CPP}        | ${CROSS_COMPILE}gcc -E
265        c++           |  ${CXX}        | ${CROSS_COMPILE}g++
266        ar            |  ${AR}         | ${CROSS_COMPILE}ar
267        nm            |  ${NM}         | ${CROSS_COMPILE}nm
268        ldr           |  ${LDR}        | ${CROSS_COMPILE}ldr
269        strip         |  ${STRIP}      | ${CROSS_COMPILE}strip
270        objcopy       |  ${OBJCOPY}    | ${CROSS_COMPILE}objcopy
271        objdump       |  ${OBJDUMP}    | ${CROSS_COMPILE}objdump
272        dtc           |  ${DTC}        | (no CROSS_COMPILE version)
273
274    Args:
275        name: Command name to run
276
277    Returns:
278        target_name: Exact command name to run instead
279        extra_args: List of extra arguments to pass
280    """
281    env = dict(os.environ)
282
283    target_name = None
284    extra_args = []
285    if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
286                'objcopy', 'objdump', 'dtc'):
287        target_name, *extra_args = env.get(name.upper(), '').split(' ')
288    elif name == 'c++':
289        target_name, *extra_args = env.get('CXX', '').split(' ')
290
291    if target_name:
292        return target_name, extra_args
293
294    if cross_compile is None:
295        cross_compile = env.get('CROSS_COMPILE', '')
296
297    if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
298        target_name = cross_compile + name
299    elif name == 'ld':
300        try:
301            if run(cross_compile + 'ld.bfd', '-v'):
302                target_name = cross_compile + 'ld.bfd'
303        except:
304            target_name = cross_compile + 'ld'
305    elif name == 'cc':
306        target_name = cross_compile + 'gcc'
307    elif name == 'cpp':
308        target_name = cross_compile + 'gcc'
309        extra_args = ['-E']
310    elif name == 'c++':
311        target_name = cross_compile + 'g++'
312    else:
313        target_name = name
314    return target_name, extra_args
315
316def get_env_with_path():
317    """Get an updated environment with the PATH variable set correctly
318
319    If there are any search paths set, these need to come first in the PATH so
320    that these override any other version of the tools.
321
322    Returns:
323        dict: New environment with PATH updated, or None if there are not search
324            paths
325    """
326    if tool_search_paths:
327        env = dict(os.environ)
328        env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
329        return env
330
331def run_result(name, *args, **kwargs):
332    """Run a tool with some arguments
333
334    This runs a 'tool', which is a program used by binman to process files and
335    perhaps produce some output. Tools can be located on the PATH or in a
336    search path.
337
338    Args:
339        name: Command name to run
340        args: Arguments to the tool
341        for_host: True to resolve the command to the version for the host
342        for_target: False to run the command as-is, without resolving it
343                   to the version for the compile target
344        raise_on_error: Raise an error if the command fails (True by default)
345
346    Returns:
347        CommandResult object
348    """
349    try:
350        binary = kwargs.get('binary')
351        for_host = kwargs.get('for_host', False)
352        for_target = kwargs.get('for_target', not for_host)
353        raise_on_error = kwargs.get('raise_on_error', True)
354        env = get_env_with_path()
355        if for_target:
356            name, extra_args = get_target_compile_tool(name)
357            args = tuple(extra_args) + args
358        elif for_host:
359            name, extra_args = get_host_compile_tool(env, name)
360            args = tuple(extra_args) + args
361        name = os.path.expanduser(name)  # Expand paths containing ~
362        all_args = (name,) + args
363        result = command.run_pipe([all_args], capture=True, capture_stderr=True,
364                                 env=env, raise_on_error=False, binary=binary)
365        if result.return_code:
366            if raise_on_error:
367                raise ValueError("Error %d running '%s': %s" %
368                                 (result.return_code,' '.join(all_args),
369                                  result.stderr or result.stdout))
370        return result
371    except ValueError:
372        if env and not path_has_file(env['PATH'], name):
373            msg = "Please install tool '%s'" % name
374            package = packages.get(name)
375            if package:
376                 msg += " (e.g. from package '%s')" % package
377            raise ValueError(msg)
378        raise
379
380def tool_find(name):
381    """Search the current path for a tool
382
383    This uses both PATH and any value from set_tool_paths() to search for a tool
384
385    Args:
386        name (str): Name of tool to locate
387
388    Returns:
389        str: Full path to tool if found, else None
390    """
391    name = os.path.expanduser(name)  # Expand paths containing ~
392    paths = []
393    pathvar = os.environ.get('PATH')
394    if pathvar:
395        paths = pathvar.split(':')
396    if tool_search_paths:
397        paths += tool_search_paths
398    for path in paths:
399        fname = os.path.join(path, name)
400        if os.path.isfile(fname) and os.access(fname, os.X_OK):
401            return fname
402
403def run(name, *args, **kwargs):
404    """Run a tool with some arguments
405
406    This runs a 'tool', which is a program used by binman to process files and
407    perhaps produce some output. Tools can be located on the PATH or in a
408    search path.
409
410    Args:
411        name: Command name to run
412        args: Arguments to the tool
413        for_host: True to resolve the command to the version for the host
414        for_target: False to run the command as-is, without resolving it
415                   to the version for the compile target
416
417    Returns:
418        CommandResult object
419    """
420    result = run_result(name, *args, **kwargs)
421    if result is not None:
422        return result.stdout
423
424def filename(fname):
425    """Resolve a file path to an absolute path.
426
427    If fname starts with ##/ and chroot is available, ##/ gets replaced with
428    the chroot path. If chroot is not available, this file name can not be
429    resolved, `None' is returned.
430
431    If fname is not prepended with the above prefix, and is not an existing
432    file, the actual file name is retrieved from the passed in string and the
433    search_paths directories (if any) are searched to for the file. If found -
434    the path to the found file is returned, `None' is returned otherwise.
435
436    Args:
437      fname: a string,  the path to resolve.
438
439    Returns:
440      Absolute path to the file or None if not found.
441    """
442    if fname.startswith('##/'):
443      if chroot_path:
444        fname = os.path.join(chroot_path, fname[3:])
445      else:
446        return None
447
448    # Search for a pathname that exists, and return it if found
449    if fname and not os.path.exists(fname):
450        for path in search_paths:
451            pathname = os.path.join(path, os.path.basename(fname))
452            if os.path.exists(pathname):
453                return pathname
454
455    # If not found, just return the standard, unchanged path
456    return fname
457
458def read_file(fname, binary=True):
459    """Read and return the contents of a file.
460
461    Args:
462      fname: path to filename to read, where ## signifiies the chroot.
463
464    Returns:
465      data read from file, as a string.
466    """
467    with open(filename(fname), binary and 'rb' or 'r') as fd:
468        data = fd.read()
469    #self._out.Info("Read file '%s' size %d (%#0x)" %
470                   #(fname, len(data), len(data)))
471    return data
472
473def write_file(fname, data, binary=True):
474    """Write data into a file.
475
476    Args:
477        fname: path to filename to write
478        data: data to write to file, as a string
479    """
480    #self._out.Info("Write file '%s' size %d (%#0x)" %
481                   #(fname, len(data), len(data)))
482    with open(filename(fname), binary and 'wb' or 'w') as fd:
483        fd.write(data)
484
485def get_bytes(byte, size):
486    """Get a string of bytes of a given size
487
488    Args:
489        byte: Numeric byte value to use
490        size: Size of bytes/string to return
491
492    Returns:
493        A bytes type with 'byte' repeated 'size' times
494    """
495    return bytes([byte]) * size
496
497def to_bytes(string):
498    """Convert a str type into a bytes type
499
500    Args:
501        string: string to convert
502
503    Returns:
504        A bytes type
505    """
506    return string.encode('utf-8')
507
508def to_string(bval):
509    """Convert a bytes type into a str type
510
511    Args:
512        bval: bytes value to convert
513
514    Returns:
515        Python 3: A bytes type
516        Python 2: A string type
517    """
518    return bval.decode('utf-8')
519
520def to_hex(val):
521    """Convert an integer value (or None) to a string
522
523    Returns:
524        hex value, or 'None' if the value is None
525    """
526    return 'None' if val is None else '%#x' % val
527
528def to_hex_size(val):
529    """Return the size of an object in hex
530
531    Returns:
532        hex value of size, or 'None' if the value is None
533    """
534    return 'None' if val is None else '%#x' % len(val)
535
536def print_full_help(fname):
537    """Print the full help message for a tool using an appropriate pager.
538
539    Args:
540        fname: Path to a file containing the full help message
541    """
542    pager = shlex.split(os.getenv('PAGER', ''))
543    if not pager:
544        lesspath = shutil.which('less')
545        pager = [lesspath] if lesspath else None
546    if not pager:
547        pager = ['more']
548    command.run(*pager, fname)
549
550def download(url, tmpdir_pattern='.patman'):
551    """Download a file to a temporary directory
552
553    Args:
554        url (str): URL to download
555        tmpdir_pattern (str): pattern to use for the temporary directory
556
557    Returns:
558        Tuple:
559            Full path to the downloaded archive file in that directory,
560                or None if there was an error while downloading
561            Temporary directory name
562    """
563    print('- downloading: %s' % url)
564    leaf = url.split('/')[-1]
565    tmpdir = tempfile.mkdtemp(tmpdir_pattern)
566    response = urllib.request.urlopen(url)
567    fname = os.path.join(tmpdir, leaf)
568    fd = open(fname, 'wb')
569    meta = response.info()
570    size = int(meta.get('Content-Length'))
571    done = 0
572    block_size = 1 << 16
573    status = ''
574
575    # Read the file in chunks and show progress as we go
576    while True:
577        buffer = response.read(block_size)
578        if not buffer:
579            print(chr(8) * (len(status) + 1), '\r', end=' ')
580            break
581
582        done += len(buffer)
583        fd.write(buffer)
584        status = r'%10d MiB  [%3d%%]' % (done // 1024 // 1024,
585                                            done * 100 // size)
586        status = status + chr(8) * (len(status) + 1)
587        print(status, end=' ')
588        sys.stdout.flush()
589    print('\r', end='')
590    sys.stdout.flush()
591    fd.close()
592    if done != size:
593        print('Error, failed to download')
594        os.remove(fname)
595        fname = None
596    return fname, tmpdir
597