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.util 10import logging 11import subprocess 12import os 13import shutil 14import signal 15import threading 16from typing import Iterator, List, Optional, Tuple 17 18import kunit_config 19import kunit_parser 20import qemu_config 21 22KCONFIG_PATH = '.config' 23KUNITCONFIG_PATH = '.kunitconfig' 24OLD_KUNITCONFIG_PATH = 'last_used_kunitconfig' 25DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config' 26BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config' 27OUTFILE_PATH = 'test.log' 28ABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__)) 29QEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs') 30 31def get_file_path(build_dir, default): 32 if build_dir: 33 default = os.path.join(build_dir, default) 34 return default 35 36class ConfigError(Exception): 37 """Represents an error trying to configure the Linux kernel.""" 38 39 40class BuildError(Exception): 41 """Represents an error trying to build the Linux kernel.""" 42 43 44class LinuxSourceTreeOperations(object): 45 """An abstraction over command line operations performed on a source tree.""" 46 47 def __init__(self, linux_arch: str, cross_compile: Optional[str]): 48 self._linux_arch = linux_arch 49 self._cross_compile = cross_compile 50 51 def make_mrproper(self) -> None: 52 try: 53 subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT) 54 except OSError as e: 55 raise ConfigError('Could not call make command: ' + str(e)) 56 except subprocess.CalledProcessError as e: 57 raise ConfigError(e.output.decode()) 58 59 def make_arch_qemuconfig(self, kconfig: kunit_config.Kconfig) -> None: 60 pass 61 62 def make_allyesconfig(self, build_dir, make_options) -> None: 63 raise ConfigError('Only the "um" arch is supported for alltests') 64 65 def make_olddefconfig(self, build_dir, make_options) -> None: 66 command = ['make', 'ARCH=' + self._linux_arch, 'olddefconfig'] 67 if self._cross_compile: 68 command += ['CROSS_COMPILE=' + self._cross_compile] 69 if make_options: 70 command.extend(make_options) 71 if build_dir: 72 command += ['O=' + build_dir] 73 print('Populating config with:\n$', ' '.join(command)) 74 try: 75 subprocess.check_output(command, stderr=subprocess.STDOUT) 76 except OSError as e: 77 raise ConfigError('Could not call make command: ' + str(e)) 78 except subprocess.CalledProcessError as e: 79 raise ConfigError(e.output.decode()) 80 81 def make(self, jobs, build_dir, make_options) -> None: 82 command = ['make', 'ARCH=' + self._linux_arch, '--jobs=' + str(jobs)] 83 if make_options: 84 command.extend(make_options) 85 if self._cross_compile: 86 command += ['CROSS_COMPILE=' + self._cross_compile] 87 if build_dir: 88 command += ['O=' + build_dir] 89 print('Building with:\n$', ' '.join(command)) 90 try: 91 proc = subprocess.Popen(command, 92 stderr=subprocess.PIPE, 93 stdout=subprocess.DEVNULL) 94 except OSError as e: 95 raise BuildError('Could not call execute make: ' + str(e)) 96 except subprocess.CalledProcessError as e: 97 raise BuildError(e.output) 98 _, stderr = proc.communicate() 99 if proc.returncode != 0: 100 raise BuildError(stderr.decode()) 101 if stderr: # likely only due to build warnings 102 print(stderr.decode()) 103 104 def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 105 raise RuntimeError('not implemented!') 106 107 108class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): 109 110 def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]): 111 super().__init__(linux_arch=qemu_arch_params.linux_arch, 112 cross_compile=cross_compile) 113 self._kconfig = qemu_arch_params.kconfig 114 self._qemu_arch = qemu_arch_params.qemu_arch 115 self._kernel_path = qemu_arch_params.kernel_path 116 self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot' 117 self._extra_qemu_params = qemu_arch_params.extra_qemu_params 118 119 def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None: 120 kconfig = kunit_config.parse_from_string(self._kconfig) 121 base_kunitconfig.merge_in_entries(kconfig) 122 123 def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 124 kernel_path = os.path.join(build_dir, self._kernel_path) 125 qemu_command = ['qemu-system-' + self._qemu_arch, 126 '-nodefaults', 127 '-m', '1024', 128 '-kernel', kernel_path, 129 '-append', '\'' + ' '.join(params + [self._kernel_command_line]) + '\'', 130 '-no-reboot', 131 '-nographic', 132 '-serial stdio'] + self._extra_qemu_params 133 print('Running tests with:\n$', ' '.join(qemu_command)) 134 return subprocess.Popen(' '.join(qemu_command), 135 stdin=subprocess.PIPE, 136 stdout=subprocess.PIPE, 137 stderr=subprocess.STDOUT, 138 text=True, shell=True, errors='backslashreplace') 139 140class LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations): 141 """An abstraction over command line operations performed on a source tree.""" 142 143 def __init__(self, cross_compile=None): 144 super().__init__(linux_arch='um', cross_compile=cross_compile) 145 146 def make_allyesconfig(self, build_dir, make_options) -> None: 147 kunit_parser.print_with_timestamp( 148 'Enabling all CONFIGs for UML...') 149 command = ['make', 'ARCH=um', 'allyesconfig'] 150 if make_options: 151 command.extend(make_options) 152 if build_dir: 153 command += ['O=' + build_dir] 154 process = subprocess.Popen( 155 command, 156 stdout=subprocess.DEVNULL, 157 stderr=subprocess.STDOUT) 158 process.wait() 159 kunit_parser.print_with_timestamp( 160 'Disabling broken configs to run KUnit tests...') 161 162 with open(get_kconfig_path(build_dir), 'a') as config: 163 with open(BROKEN_ALLCONFIG_PATH, 'r') as disable: 164 config.write(disable.read()) 165 kunit_parser.print_with_timestamp( 166 'Starting Kernel with all configs takes a few minutes...') 167 168 def start(self, params: List[str], build_dir: str) -> subprocess.Popen: 169 """Runs the Linux UML binary. Must be named 'linux'.""" 170 linux_bin = get_file_path(build_dir, 'linux') 171 return subprocess.Popen([linux_bin] + params, 172 stdin=subprocess.PIPE, 173 stdout=subprocess.PIPE, 174 stderr=subprocess.STDOUT, 175 text=True, errors='backslashreplace') 176 177def get_kconfig_path(build_dir) -> str: 178 return get_file_path(build_dir, KCONFIG_PATH) 179 180def get_kunitconfig_path(build_dir) -> str: 181 return get_file_path(build_dir, KUNITCONFIG_PATH) 182 183def get_old_kunitconfig_path(build_dir) -> str: 184 return get_file_path(build_dir, OLD_KUNITCONFIG_PATH) 185 186def get_outfile_path(build_dir) -> str: 187 return get_file_path(build_dir, OUTFILE_PATH) 188 189def get_source_tree_ops(arch: str, cross_compile: Optional[str]) -> LinuxSourceTreeOperations: 190 config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py') 191 if arch == 'um': 192 return LinuxSourceTreeOperationsUml(cross_compile=cross_compile) 193 elif os.path.isfile(config_path): 194 return get_source_tree_ops_from_qemu_config(config_path, cross_compile)[1] 195 196 options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')] 197 raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options))) 198 199def get_source_tree_ops_from_qemu_config(config_path: str, 200 cross_compile: Optional[str]) -> Tuple[ 201 str, LinuxSourceTreeOperations]: 202 # The module name/path has very little to do with where the actual file 203 # exists (I learned this through experimentation and could not find it 204 # anywhere in the Python documentation). 205 # 206 # Bascially, we completely ignore the actual file location of the config 207 # we are loading and just tell Python that the module lives in the 208 # QEMU_CONFIGS_DIR for import purposes regardless of where it actually 209 # exists as a file. 210 module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path)) 211 spec = importlib.util.spec_from_file_location(module_path, config_path) 212 assert spec is not None 213 config = importlib.util.module_from_spec(spec) 214 # See https://github.com/python/typeshed/pull/2626 for context. 215 assert isinstance(spec.loader, importlib.abc.Loader) 216 spec.loader.exec_module(config) 217 218 if not hasattr(config, 'QEMU_ARCH'): 219 raise ValueError('qemu_config module missing "QEMU_ARCH": ' + config_path) 220 params: qemu_config.QemuArchParams = config.QEMU_ARCH # type: ignore 221 return params.linux_arch, LinuxSourceTreeOperationsQemu( 222 params, cross_compile=cross_compile) 223 224class LinuxSourceTree(object): 225 """Represents a Linux kernel source tree with KUnit tests.""" 226 227 def __init__( 228 self, 229 build_dir: str, 230 load_config=True, 231 kunitconfig_path='', 232 kconfig_add: Optional[List[str]]=None, 233 arch=None, 234 cross_compile=None, 235 qemu_config_path=None) -> None: 236 signal.signal(signal.SIGINT, self.signal_handler) 237 if qemu_config_path: 238 self._arch, self._ops = get_source_tree_ops_from_qemu_config( 239 qemu_config_path, cross_compile) 240 else: 241 self._arch = 'um' if arch is None else arch 242 self._ops = get_source_tree_ops(self._arch, cross_compile) 243 244 if not load_config: 245 return 246 247 if kunitconfig_path: 248 if os.path.isdir(kunitconfig_path): 249 kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH) 250 if not os.path.exists(kunitconfig_path): 251 raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist') 252 else: 253 kunitconfig_path = get_kunitconfig_path(build_dir) 254 if not os.path.exists(kunitconfig_path): 255 shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path) 256 257 self._kconfig = kunit_config.parse_file(kunitconfig_path) 258 if kconfig_add: 259 kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add)) 260 self._kconfig.merge_in_entries(kconfig) 261 262 263 def clean(self) -> bool: 264 try: 265 self._ops.make_mrproper() 266 except ConfigError as e: 267 logging.error(e) 268 return False 269 return True 270 271 def validate_config(self, build_dir) -> bool: 272 kconfig_path = get_kconfig_path(build_dir) 273 validated_kconfig = kunit_config.parse_file(kconfig_path) 274 if self._kconfig.is_subset_of(validated_kconfig): 275 return True 276 invalid = self._kconfig.entries() - validated_kconfig.entries() 277 message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \ 278 'This is probably due to unsatisfied dependencies.\n' \ 279 'Missing: ' + ', '.join([str(e) for e in invalid]) 280 if self._arch == 'um': 281 message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \ 282 'on a different architecture with something like "--arch=x86_64".' 283 logging.error(message) 284 return False 285 286 def build_config(self, build_dir, make_options) -> bool: 287 kconfig_path = get_kconfig_path(build_dir) 288 if build_dir and not os.path.exists(build_dir): 289 os.mkdir(build_dir) 290 try: 291 self._ops.make_arch_qemuconfig(self._kconfig) 292 self._kconfig.write_to_file(kconfig_path) 293 self._ops.make_olddefconfig(build_dir, make_options) 294 except ConfigError as e: 295 logging.error(e) 296 return False 297 if not self.validate_config(build_dir): 298 return False 299 300 old_path = get_old_kunitconfig_path(build_dir) 301 if os.path.exists(old_path): 302 os.remove(old_path) # write_to_file appends to the file 303 self._kconfig.write_to_file(old_path) 304 return True 305 306 def _kunitconfig_changed(self, build_dir: str) -> bool: 307 old_path = get_old_kunitconfig_path(build_dir) 308 if not os.path.exists(old_path): 309 return True 310 311 old_kconfig = kunit_config.parse_file(old_path) 312 return old_kconfig.entries() != self._kconfig.entries() 313 314 def build_reconfig(self, build_dir, make_options) -> bool: 315 """Creates a new .config if it is not a subset of the .kunitconfig.""" 316 kconfig_path = get_kconfig_path(build_dir) 317 if not os.path.exists(kconfig_path): 318 print('Generating .config ...') 319 return self.build_config(build_dir, make_options) 320 321 existing_kconfig = kunit_config.parse_file(kconfig_path) 322 self._ops.make_arch_qemuconfig(self._kconfig) 323 if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir): 324 return True 325 print('Regenerating .config ...') 326 os.remove(kconfig_path) 327 return self.build_config(build_dir, make_options) 328 329 def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool: 330 try: 331 if alltests: 332 self._ops.make_allyesconfig(build_dir, make_options) 333 self._ops.make_olddefconfig(build_dir, make_options) 334 self._ops.make(jobs, build_dir, make_options) 335 except (ConfigError, BuildError) as e: 336 logging.error(e) 337 return False 338 return self.validate_config(build_dir) 339 340 def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]: 341 if not args: 342 args = [] 343 args.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt']) 344 if filter_glob: 345 args.append('kunit.filter_glob='+filter_glob) 346 347 process = self._ops.start(args, build_dir) 348 assert process.stdout is not None # tell mypy it's set 349 350 # Enforce the timeout in a background thread. 351 def _wait_proc(): 352 try: 353 process.wait(timeout=timeout) 354 except Exception as e: 355 print(e) 356 process.terminate() 357 process.wait() 358 waiter = threading.Thread(target=_wait_proc) 359 waiter.start() 360 361 output = open(get_outfile_path(build_dir), 'w') 362 try: 363 # Tee the output to the file and to our caller in real time. 364 for line in process.stdout: 365 output.write(line) 366 yield line 367 # This runs even if our caller doesn't consume every line. 368 finally: 369 # Flush any leftover output to the file 370 output.write(process.stdout.read()) 371 output.close() 372 process.stdout.close() 373 374 waiter.join() 375 subprocess.call(['stty', 'sane']) 376 377 def signal_handler(self, sig, frame) -> None: 378 logging.error('Build interruption occurred. Cleaning console.') 379 subprocess.call(['stty', 'sane']) 380