summaryrefslogtreecommitdiffstats
path: root/scripts/lib/wic/misc.py
blob: 1f199b9f234c5f8e494dd9cf4902e20d3941b41d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#
# Copyright (c) 2013, Intel Corporation.
#
# SPDX-License-Identifier: GPL-2.0-only
#
# DESCRIPTION
# This module provides a place to collect various wic-related utils
# for the OpenEmbedded Image Tools.
#
# AUTHORS
# Tom Zanussi <tom.zanussi (at] linux.intel.com>
#
"""Miscellaneous functions."""

import logging
import os
import re
import subprocess

from collections import defaultdict
from distutils import spawn

from wic import WicError

logger = logging.getLogger('wic')

# executable -> recipe pairs for exec_native_cmd
NATIVE_RECIPES = {"bmaptool": "bmap-tools",
                  "grub-mkimage": "grub-efi",
                  "isohybrid": "syslinux",
                  "mcopy": "mtools",
                  "mdel" : "mtools",
                  "mdeltree" : "mtools",
                  "mdir" : "mtools",
                  "mkdosfs": "dosfstools",
                  "mkisofs": "cdrtools",
                  "mkfs.btrfs": "btrfs-tools",
                  "mkfs.ext2": "e2fsprogs",
                  "mkfs.ext3": "e2fsprogs",
                  "mkfs.ext4": "e2fsprogs",
                  "mkfs.vfat": "dosfstools",
                  "mksquashfs": "squashfs-tools",
                  "mkswap": "util-linux",
                  "mmd": "mtools",
                  "parted": "parted",
                  "sfdisk": "util-linux",
                  "sgdisk": "gptfdisk",
                  "syslinux": "syslinux"
                 }

def runtool(cmdln_or_args):
    """ wrapper for most of the subprocess calls
    input:
        cmdln_or_args: can be both args and cmdln str (shell=True)
    return:
        rc, output
    """
    if isinstance(cmdln_or_args, list):
        cmd = cmdln_or_args[0]
        shell = False
    else:
        import shlex
        cmd = shlex.split(cmdln_or_args)[0]
        shell = True

    sout = subprocess.PIPE
    serr = subprocess.STDOUT

    try:
        process = subprocess.Popen(cmdln_or_args, stdout=sout,
                                   stderr=serr, shell=shell)
        sout, serr = process.communicate()
        # combine stdout and stderr, filter None out and decode
        out = ''.join([out.decode('utf-8') for out in [sout, serr] if out])
    except OSError as err:
        if err.errno == 2:
            # [Errno 2] No such file or directory
            raise WicError('Cannot run command: %s, lost dependency?' % cmd)
        else:
            raise # relay

    return process.returncode, out

def _exec_cmd(cmd_and_args, as_shell=False):
    """
    Execute command, catching stderr, stdout

    Need to execute as_shell if the command uses wildcards
    """
    logger.debug("_exec_cmd: %s", cmd_and_args)
    args = cmd_and_args.split()
    logger.debug(args)

    if as_shell:
        ret, out = runtool(cmd_and_args)
    else:
        ret, out = runtool(args)
    out = out.strip()
    if ret != 0:
        raise WicError("_exec_cmd: %s returned '%s' instead of 0\noutput: %s" % \
                       (cmd_and_args, ret, out))

    logger.debug("_exec_cmd: output for %s (rc = %d): %s",
                 cmd_and_args, ret, out)

    return ret, out


def exec_cmd(cmd_and_args, as_shell=False):
    """
    Execute command, return output
    """
    return _exec_cmd(cmd_and_args, as_shell)[1]


def exec_native_cmd(cmd_and_args, native_sysroot, pseudo=""):
    """
    Execute native command, catching stderr, stdout

    Need to execute as_shell if the command uses wildcards

    Always need to execute native commands as_shell
    """
    # The reason -1 is used is because there may be "export" commands.
    args = cmd_and_args.split(';')[-1].split()
    logger.debug(args)

    if pseudo:
        cmd_and_args = pseudo + cmd_and_args

    native_paths = "%s/sbin:%s/usr/sbin:%s/usr/bin" % \
                   (native_sysroot, native_sysroot, native_sysroot)

    native_cmd_and_args = "export PATH=%s:$PATH;%s" % \
                   (native_paths, cmd_and_args)
    logger.debug("exec_native_cmd: %s", native_cmd_and_args)

    # If the command isn't in the native sysroot say we failed.
    if spawn.find_executable(args[0], native_paths):
        ret, out = _exec_cmd(native_cmd_and_args, True)
    else:
        ret = 127
        out = "can't find native executable %s in %s" % (args[0], native_paths)

    prog = args[0]
    # shell command-not-found
    if ret == 127 \
       or (pseudo and ret == 1 and out == "Can't find '%s' in $PATH." % prog):
        msg = "A native program %s required to build the image "\
              "was not found (see details above).\n\n" % prog
        recipe = NATIVE_RECIPES.get(prog)
        if recipe:
            msg += "Please make sure wic-tools have %s-native in its DEPENDS, "\
                   "build it with 'bitbake wic-tools' and try again.\n" % recipe
        else:
            msg += "Wic failed to find a recipe to build native %s. Please "\
                   "file a bug against wic.\n" % prog
        raise WicError(msg)

    return ret, out

BOOTDD_EXTRA_SPACE = 16384

class BitbakeVars(defaultdict):
    """
    Container for Bitbake variables.
    """
    def __init__(self):
        defaultdict.__init__(self, dict)

        # default_image and vars_dir attributes should be set from outside
        self.default_image = None
        self.vars_dir = None

    def _parse_line(self, line, image, matcher=re.compile(r"^([a-zA-Z0-9\-_+./~]+)=(.*)")):
        """
        Parse one line from bitbake -e output or from .env file.
        Put result key-value pair into the storage.
        """
        if "=" not in line:
            return
        match = matcher.match(line)
        if not match:
            return
        key, val = match.groups()
        self[image][key] = val.strip('"')

    def get_var(self, var, image=None, cache=True):
        """
        Get bitbake variable from 'bitbake -e' output or from .env file.
        This is a lazy method, i.e. it runs bitbake or parses file only when
        only when variable is requested. It also caches results.
        """
        if not image:
            image = self.default_image

        if image not in self:
            if image and self.vars_dir:
                fname = os.path.join(self.vars_dir, image + '.env')
                if os.path.isfile(fname):
                    # parse .env file
                    with open(fname) as varsfile:
                        for line in varsfile:
                            self._parse_line(line, image)
                else:
                    print("Couldn't get bitbake variable from %s." % fname)
                    print("File %s doesn't exist." % fname)
                    return
            else:
                # Get bitbake -e output
                cmd = "bitbake -e"
                if image:
                    cmd += " %s" % image

                log_level = logger.getEffectiveLevel()
                logger.setLevel(logging.INFO)
                ret, lines = _exec_cmd(cmd)
                logger.setLevel(log_level)

                if ret:
                    logger.error("Couldn't get '%s' output.", cmd)
                    logger.error("Bitbake failed with error:\n%s\n", lines)
                    return

                # Parse bitbake -e output
                for line in lines.split('\n'):
                    self._parse_line(line, image)

            # Make first image a default set of variables
            if cache:
                images = [key for key in self if key]
                if len(images) == 1:
                    self[None] = self[image]

        result = self[image].get(var)
        if not cache:
            self.pop(image, None)

        return result

# Create BB_VARS singleton
BB_VARS = BitbakeVars()

def get_bitbake_var(var, image=None, cache=True):
    """
    Provide old get_bitbake_var API by wrapping
    get_var method of BB_VARS singleton.
    """
    return BB_VARS.get_var(var, image, cache)