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