1# SPDX-License-Identifier: GPL-2.0 2# 3# Runs UML kernel, collects output, and handles errors. 4# 5# Copyright (C) 2019, Google LLC. 6# Author: Felix Guo <felixguoxiuping@gmail.com> 7# Author: Brendan Higgins <brendanhiggins@google.com> 8 9import importlib.abc 10import importlib.util 11import logging 12import subprocess 13import os 14import shutil 15import signal 16from typing import Iterator, Optional, Tuple 17 18from contextlib import ExitStack 19 20from collections import namedtuple 21 22import kunit_config 23import kunit_parser 24import qemu_config 25 26KCONFIG_PATH = '.config' 27KUNITCONFIG_PATH = '.kunitconfig' 28DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config' 29BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config' 30OUTFILE_PATH = 'test.log' 31ABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__)) 32QEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs') 33 34def get_file_path(build_dir, default): 35 if build_dir: 36 default = os.path.join(build_dir, default) 37 return default 38 39class ConfigError(Exception): 40 """Represents an error trying to configure the Linux kernel.""" 41 42 43class BuildError(Exception): 44 """Represents an error trying to build the Linux kernel.""" 45 46 47class LinuxSourceTreeOperations(object): 48 """An abstraction over command line operations performed on a source tree.""" 49 50 def __init__(self, linux_arch: str, cross_compile: Optional[str]): 51 self._linux_arch = linux_arch 52 self._cross_compile = cross_compile 53 54 def make_mrproper(self) -> None: 55 try: 56 subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT) 57 except OSError as e: 58 raise ConfigError('Could not call make command: ' + str(e)) 59 except subprocess.CalledProcessError as e: 60 raise ConfigError(e.output.decode()) 61 62 def make_arch_qemuconfig(self, kconfig: kunit_config.Kconfig) -> None: 63 pass 64 65 def make_allyesconfig(self, build_dir, make_options) -> None: 66 raise ConfigError('Only the "um" arch is supported for alltests') 67 68 def make_olddefconfig(self, build_dir, make_options) -> None: 69 command = ['make', 'ARCH=' + self._linux_arch, 'olddefconfig'] 70 if self._cross_compile: 71 command += ['CROSS_COMPILE=' + self._cross_compile] 72 if make_options: 73 command.extend(make_options) 74 if build_dir: 75 command += ['O=' + build_dir] 76 print('Populating config with:\n$', ' '.join(command)) 77 try: 78 subprocess.check_output(command, stderr=subprocess.STDOUT) 79 except OSError as e: 80 raise ConfigError('Could not call make command: ' + str(e)) 81 except subprocess.CalledProcessError as e: 82 raise ConfigError(e.output.decode()) 83 84 def make(self, jobs, build_dir, make_options) -> None: 85 command = ['make', 'ARCH=' + self._linux_arch, '--jobs=' + str(jobs)] 86 if make_options: 87 command.extend(make_options) 88 if self._cross_compile: 89 command += ['CROSS_COMPILE=' + self._cross_compile] 90 if build_dir: 91 command += ['O=' + build_dir] 92 print('Building with:\n$', ' '.join(command)) 93 try: 94 proc = subprocess.Popen(command, 95 stderr=subprocess.PIPE, 96 stdout=subprocess.DEVNULL) 97 except OSError as e: 98 raise BuildError('Could not call execute make: ' + str(e)) 99 except subprocess.CalledProcessError as e: 100 raise BuildError(e.output) 101 _, stderr = proc.communicate() 102 if proc.returncode != 0: 103 raise BuildError(stderr.decode()) 104 if stderr: # likely only due to build warnings 105 print(stderr.decode()) 106 107 def run(self, params, timeout, build_dir, outfile) -> None: 108 pass 109 110 111class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): 112 113 def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]): 114 super().__init__(linux_arch=qemu_arch_params.linux_arch, 115 cross_compile=cross_compile) 116 self._kconfig = qemu_arch_params.kconfig 117 self._qemu_arch = qemu_arch_params.qemu_arch 118 self._kernel_path = qemu_arch_params.kernel_path 119 self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot' 120 self._extra_qemu_params = qemu_arch_params.extra_qemu_params 121 122 def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None: 123 kconfig = kunit_config.Kconfig() 124 kconfig.parse_from_string(self._kconfig) 125 base_kunitconfig.merge_in_entries(kconfig) 126 127 def run(self, params, timeout, build_dir, outfile): 128 kernel_path = os.path.join(build_dir, self._kernel_path) 129 qemu_command = ['qemu-system-' + self._qemu_arch, 130 '-nodefaults', 131 '-m', '1024', 132 '-kernel', kernel_path, 133 '-append', '\'' + ' '.join(params + [self._kernel_command_line]) + '\'', 134 '-no-reboot', 135 '-nographic', 136 '-serial stdio'] + self._extra_qemu_params 137 print('Running tests with:\n$', ' '.join(qemu_command)) 138 with open(outfile, 'w') as output: 139 process = subprocess.Popen(' '.join(qemu_command), 140 stdin=subprocess.PIPE, 141 stdout=output, 142 stderr=subprocess.STDOUT, 143 text=True, shell=True) 144 try: 145 process.wait(timeout=timeout) 146 except Exception as e: 147 print(e) 148 process.terminate() 149 return process 150 151class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations): 152 """An abstraction over command line operations performed on a source tree.""" 153 154 def __init__(self, cross_compile=None): 155 super().__init__(linux_arch='um', cross_compile=cross_compile) 156 157 def make_allyesconfig(self, build_dir, make_options) -> None: 158 kunit_parser.print_with_timestamp( 159 'Enabling all CONFIGs for UML...') 160 command = ['make', 'ARCH=um', 'allyesconfig'] 161 if make_options: 162 command.extend(make_options) 163 if build_dir: 164 command += ['O=' + build_dir] 165 process = subprocess.Popen( 166 command, 167 stdout=subprocess.DEVNULL, 168 stderr=subprocess.STDOUT) 169 process.wait() 170 kunit_parser.print_with_timestamp( 171 'Disabling broken configs to run KUnit tests...') 172 with ExitStack() as es: 173 config = open(get_kconfig_path(build_dir), 'a') 174 disable = open(BROKEN_ALLCONFIG_PATH, 'r').read() 175 config.write(disable) 176 kunit_parser.print_with_timestamp( 177 'Starting Kernel with all configs takes a few minutes...') 178 179 def run(self, params, timeout, build_dir, outfile): 180 """Runs the Linux UML binary. Must be named 'linux'.""" 181 linux_bin = get_file_path(build_dir, 'linux') 182 outfile = get_outfile_path(build_dir) 183 with open(outfile, 'w') as output: 184 process = subprocess.Popen([linux_bin] + params, 185 stdin=subprocess.PIPE, 186 stdout=output, 187 stderr=subprocess.STDOUT, 188 text=True) 189 process.wait(timeout) 190 191def get_kconfig_path(build_dir) -> str: 192 return get_file_path(build_dir, KCONFIG_PATH) 193 194def get_kunitconfig_path(build_dir) -> str: 195 return get_file_path(build_dir, KUNITCONFIG_PATH) 196 197def get_outfile_path(build_dir) -> str: 198 return get_file_path(build_dir, OUTFILE_PATH) 199 200def get_source_tree_ops(arch: str, cross_compile: Optional[str]) -> LinuxSourceTreeOperations: 201 config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py') 202 if arch == 'um': 203 return LinuxSourceTreeOperationsUml(cross_compile=cross_compile) 204 elif os.path.isfile(config_path): 205 return get_source_tree_ops_from_qemu_config(config_path, cross_compile)[1] 206 else: 207 raise ConfigError(arch + ' is not a valid arch') 208 209def get_source_tree_ops_from_qemu_config(config_path: str, 210 cross_compile: Optional[str]) -> Tuple[ 211 str, LinuxSourceTreeOperations]: 212 # The module name/path has very little to do with where the actual file 213 # exists (I learned this through experimentation and could not find it 214 # anywhere in the Python documentation). 215 # 216 # Bascially, we completely ignore the actual file location of the config 217 # we are loading and just tell Python that the module lives in the 218 # QEMU_CONFIGS_DIR for import purposes regardless of where it actually 219 # exists as a file. 220 module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path)) 221 spec = importlib.util.spec_from_file_location(module_path, config_path) 222 config = importlib.util.module_from_spec(spec) 223 # TODO(brendanhiggins@google.com): I looked this up and apparently other 224 # Python projects have noted that pytype complains that "No attribute 225 # 'exec_module' on _importlib_modulespec._Loader". Disabling for now. 226 spec.loader.exec_module(config) # pytype: disable=attribute-error 227 return config.QEMU_ARCH.linux_arch, LinuxSourceTreeOperationsQemu( 228 config.QEMU_ARCH, cross_compile=cross_compile) 229 230class LinuxSourceTree(object): 231 """Represents a Linux kernel source tree with KUnit tests.""" 232 233 def __init__( 234 self, 235 build_dir: str, 236 load_config=True, 237 kunitconfig_path='', 238 arch=None, 239 cross_compile=None, 240 qemu_config_path=None) -> None: 241 signal.signal(signal.SIGINT, self.signal_handler) 242 if qemu_config_path: 243 self._arch, self._ops = get_source_tree_ops_from_qemu_config( 244 qemu_config_path, cross_compile) 245 else: 246 self._arch = 'um' if arch is None else arch 247 self._ops = get_source_tree_ops(self._arch, cross_compile) 248 249 if not load_config: 250 return 251 252 if kunitconfig_path: 253 if os.path.isdir(kunitconfig_path): 254 kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH) 255 if not os.path.exists(kunitconfig_path): 256 raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist') 257 else: 258 kunitconfig_path = get_kunitconfig_path(build_dir) 259 if not os.path.exists(kunitconfig_path): 260 shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path) 261 262 self._kconfig = kunit_config.Kconfig() 263 self._kconfig.read_from_file(kunitconfig_path) 264 265 def clean(self) -> bool: 266 try: 267 self._ops.make_mrproper() 268 except ConfigError as e: 269 logging.error(e) 270 return False 271 return True 272 273 def validate_config(self, build_dir) -> bool: 274 kconfig_path = get_kconfig_path(build_dir) 275 validated_kconfig = kunit_config.Kconfig() 276 validated_kconfig.read_from_file(kconfig_path) 277 if not self._kconfig.is_subset_of(validated_kconfig): 278 invalid = self._kconfig.entries() - validated_kconfig.entries() 279 message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \ 280 'but not in .config: %s' % ( 281 ', '.join([str(e) for e in invalid]) 282 ) 283 logging.error(message) 284 return False 285 return True 286 287 def build_config(self, build_dir, make_options) -> bool: 288 kconfig_path = get_kconfig_path(build_dir) 289 if build_dir and not os.path.exists(build_dir): 290 os.mkdir(build_dir) 291 try: 292 self._ops.make_arch_qemuconfig(self._kconfig) 293 self._kconfig.write_to_file(kconfig_path) 294 self._ops.make_olddefconfig(build_dir, make_options) 295 except ConfigError as e: 296 logging.error(e) 297 return False 298 return self.validate_config(build_dir) 299 300 def build_reconfig(self, build_dir, make_options) -> bool: 301 """Creates a new .config if it is not a subset of the .kunitconfig.""" 302 kconfig_path = get_kconfig_path(build_dir) 303 if os.path.exists(kconfig_path): 304 existing_kconfig = kunit_config.Kconfig() 305 existing_kconfig.read_from_file(kconfig_path) 306 self._ops.make_arch_qemuconfig(self._kconfig) 307 if not self._kconfig.is_subset_of(existing_kconfig): 308 print('Regenerating .config ...') 309 os.remove(kconfig_path) 310 return self.build_config(build_dir, make_options) 311 else: 312 return True 313 else: 314 print('Generating .config ...') 315 return self.build_config(build_dir, make_options) 316 317 def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool: 318 try: 319 if alltests: 320 self._ops.make_allyesconfig(build_dir, make_options) 321 self._ops.make_olddefconfig(build_dir, make_options) 322 self._ops.make(jobs, build_dir, make_options) 323 except (ConfigError, BuildError) as e: 324 logging.error(e) 325 return False 326 return self.validate_config(build_dir) 327 328 def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]: 329 if not args: 330 args = [] 331 args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt']) 332 if filter_glob: 333 args.append('kunit.filter_glob='+filter_glob) 334 outfile = get_outfile_path(build_dir) 335 self._ops.run(args, timeout, build_dir, outfile) 336 subprocess.call(['stty', 'sane']) 337 with open(outfile, 'r') as file: 338 for line in file: 339 yield line 340 341 def signal_handler(self, sig, frame) -> None: 342 logging.error('Build interruption occurred. Cleaning console.') 343 subprocess.call(['stty', 'sane']) 344