blob: 1f199b9f234c5f8e494dd9cf4902e20d3941b41d [file] [log] [blame]
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001#
2# Copyright (c) 2013, Intel Corporation.
Brad Bishopd7bf8c12018-02-25 22:55:05 -05003#
Brad Bishopc342db32019-05-15 21:57:59 -04004# SPDX-License-Identifier: GPL-2.0-only
Brad Bishopd7bf8c12018-02-25 22:55:05 -05005#
6# DESCRIPTION
7# This module provides a place to collect various wic-related utils
8# for the OpenEmbedded Image Tools.
9#
10# AUTHORS
11# Tom Zanussi <tom.zanussi (at] linux.intel.com>
12#
13"""Miscellaneous functions."""
14
15import logging
16import os
17import re
18import subprocess
19
20from collections import defaultdict
21from distutils import spawn
22
23from wic import WicError
24
25logger = logging.getLogger('wic')
26
27# executable -> recipe pairs for exec_native_cmd
28NATIVE_RECIPES = {"bmaptool": "bmap-tools",
29 "grub-mkimage": "grub-efi",
30 "isohybrid": "syslinux",
31 "mcopy": "mtools",
32 "mdel" : "mtools",
33 "mdeltree" : "mtools",
34 "mdir" : "mtools",
35 "mkdosfs": "dosfstools",
36 "mkisofs": "cdrtools",
37 "mkfs.btrfs": "btrfs-tools",
38 "mkfs.ext2": "e2fsprogs",
39 "mkfs.ext3": "e2fsprogs",
40 "mkfs.ext4": "e2fsprogs",
41 "mkfs.vfat": "dosfstools",
42 "mksquashfs": "squashfs-tools",
43 "mkswap": "util-linux",
44 "mmd": "mtools",
45 "parted": "parted",
46 "sfdisk": "util-linux",
47 "sgdisk": "gptfdisk",
48 "syslinux": "syslinux"
49 }
50
51def runtool(cmdln_or_args):
52 """ wrapper for most of the subprocess calls
53 input:
54 cmdln_or_args: can be both args and cmdln str (shell=True)
55 return:
56 rc, output
57 """
58 if isinstance(cmdln_or_args, list):
59 cmd = cmdln_or_args[0]
60 shell = False
61 else:
62 import shlex
63 cmd = shlex.split(cmdln_or_args)[0]
64 shell = True
65
66 sout = subprocess.PIPE
67 serr = subprocess.STDOUT
68
69 try:
70 process = subprocess.Popen(cmdln_or_args, stdout=sout,
71 stderr=serr, shell=shell)
72 sout, serr = process.communicate()
73 # combine stdout and stderr, filter None out and decode
74 out = ''.join([out.decode('utf-8') for out in [sout, serr] if out])
75 except OSError as err:
76 if err.errno == 2:
77 # [Errno 2] No such file or directory
78 raise WicError('Cannot run command: %s, lost dependency?' % cmd)
79 else:
80 raise # relay
81
82 return process.returncode, out
83
84def _exec_cmd(cmd_and_args, as_shell=False):
85 """
86 Execute command, catching stderr, stdout
87
88 Need to execute as_shell if the command uses wildcards
89 """
90 logger.debug("_exec_cmd: %s", cmd_and_args)
91 args = cmd_and_args.split()
92 logger.debug(args)
93
94 if as_shell:
95 ret, out = runtool(cmd_and_args)
96 else:
97 ret, out = runtool(args)
98 out = out.strip()
99 if ret != 0:
100 raise WicError("_exec_cmd: %s returned '%s' instead of 0\noutput: %s" % \
101 (cmd_and_args, ret, out))
102
103 logger.debug("_exec_cmd: output for %s (rc = %d): %s",
104 cmd_and_args, ret, out)
105
106 return ret, out
107
108
109def exec_cmd(cmd_and_args, as_shell=False):
110 """
111 Execute command, return output
112 """
113 return _exec_cmd(cmd_and_args, as_shell)[1]
114
115
116def exec_native_cmd(cmd_and_args, native_sysroot, pseudo=""):
117 """
118 Execute native command, catching stderr, stdout
119
120 Need to execute as_shell if the command uses wildcards
121
122 Always need to execute native commands as_shell
123 """
124 # The reason -1 is used is because there may be "export" commands.
125 args = cmd_and_args.split(';')[-1].split()
126 logger.debug(args)
127
128 if pseudo:
129 cmd_and_args = pseudo + cmd_and_args
130
131 native_paths = "%s/sbin:%s/usr/sbin:%s/usr/bin" % \
132 (native_sysroot, native_sysroot, native_sysroot)
133
134 native_cmd_and_args = "export PATH=%s:$PATH;%s" % \
135 (native_paths, cmd_and_args)
136 logger.debug("exec_native_cmd: %s", native_cmd_and_args)
137
138 # If the command isn't in the native sysroot say we failed.
139 if spawn.find_executable(args[0], native_paths):
140 ret, out = _exec_cmd(native_cmd_and_args, True)
141 else:
142 ret = 127
143 out = "can't find native executable %s in %s" % (args[0], native_paths)
144
145 prog = args[0]
146 # shell command-not-found
147 if ret == 127 \
148 or (pseudo and ret == 1 and out == "Can't find '%s' in $PATH." % prog):
149 msg = "A native program %s required to build the image "\
150 "was not found (see details above).\n\n" % prog
151 recipe = NATIVE_RECIPES.get(prog)
152 if recipe:
153 msg += "Please make sure wic-tools have %s-native in its DEPENDS, "\
154 "build it with 'bitbake wic-tools' and try again.\n" % recipe
155 else:
156 msg += "Wic failed to find a recipe to build native %s. Please "\
157 "file a bug against wic.\n" % prog
158 raise WicError(msg)
159
160 return ret, out
161
162BOOTDD_EXTRA_SPACE = 16384
163
164class BitbakeVars(defaultdict):
165 """
166 Container for Bitbake variables.
167 """
168 def __init__(self):
169 defaultdict.__init__(self, dict)
170
171 # default_image and vars_dir attributes should be set from outside
172 self.default_image = None
173 self.vars_dir = None
174
175 def _parse_line(self, line, image, matcher=re.compile(r"^([a-zA-Z0-9\-_+./~]+)=(.*)")):
176 """
177 Parse one line from bitbake -e output or from .env file.
178 Put result key-value pair into the storage.
179 """
180 if "=" not in line:
181 return
182 match = matcher.match(line)
183 if not match:
184 return
185 key, val = match.groups()
186 self[image][key] = val.strip('"')
187
188 def get_var(self, var, image=None, cache=True):
189 """
190 Get bitbake variable from 'bitbake -e' output or from .env file.
191 This is a lazy method, i.e. it runs bitbake or parses file only when
192 only when variable is requested. It also caches results.
193 """
194 if not image:
195 image = self.default_image
196
197 if image not in self:
198 if image and self.vars_dir:
199 fname = os.path.join(self.vars_dir, image + '.env')
200 if os.path.isfile(fname):
201 # parse .env file
202 with open(fname) as varsfile:
203 for line in varsfile:
204 self._parse_line(line, image)
205 else:
206 print("Couldn't get bitbake variable from %s." % fname)
207 print("File %s doesn't exist." % fname)
208 return
209 else:
210 # Get bitbake -e output
211 cmd = "bitbake -e"
212 if image:
213 cmd += " %s" % image
214
215 log_level = logger.getEffectiveLevel()
216 logger.setLevel(logging.INFO)
217 ret, lines = _exec_cmd(cmd)
218 logger.setLevel(log_level)
219
220 if ret:
221 logger.error("Couldn't get '%s' output.", cmd)
222 logger.error("Bitbake failed with error:\n%s\n", lines)
223 return
224
225 # Parse bitbake -e output
226 for line in lines.split('\n'):
227 self._parse_line(line, image)
228
229 # Make first image a default set of variables
230 if cache:
231 images = [key for key in self if key]
232 if len(images) == 1:
233 self[None] = self[image]
234
235 result = self[image].get(var)
236 if not cache:
237 self.pop(image, None)
238
239 return result
240
241# Create BB_VARS singleton
242BB_VARS = BitbakeVars()
243
244def get_bitbake_var(var, image=None, cache=True):
245 """
246 Provide old get_bitbake_var API by wrapping
247 get_var method of BB_VARS singleton.
248 """
249 return BB_VARS.get_var(var, image, cache)