16ebf5866SFelix Guo# SPDX-License-Identifier: GPL-2.0
26ebf5866SFelix Guo#
36ebf5866SFelix Guo# Runs UML kernel, collects output, and handles errors.
46ebf5866SFelix Guo#
56ebf5866SFelix Guo# Copyright (C) 2019, Google LLC.
66ebf5866SFelix Guo# Author: Felix Guo <felixguoxiuping@gmail.com>
76ebf5866SFelix Guo# Author: Brendan Higgins <brendanhiggins@google.com>
86ebf5866SFelix Guo
923552807SMichał Winiarskiimport importlib.abc
1087c9c163SBrendan Higginsimport importlib.util
116ebf5866SFelix Guoimport logging
126ebf5866SFelix Guoimport subprocess
136ebf5866SFelix Guoimport os
143f0a50f3SDaniel Latypovimport shlex
15fcdb0bc0SAndy Shevchenkoimport shutil
16021ed9f5SHeidi Fahimimport signal
177d7c48dfSDaniel Latypovimport threading
187d7c48dfSDaniel Latypovfrom typing import Iterator, List, Optional, Tuple
19021ed9f5SHeidi Fahim
206ebf5866SFelix Guoimport kunit_config
21e756dbebSDaniel Latypovfrom kunit_printer import stdout
2287c9c163SBrendan Higginsimport qemu_config
236ebf5866SFelix Guo
246ebf5866SFelix GuoKCONFIG_PATH = '.config'
25fcdb0bc0SAndy ShevchenkoKUNITCONFIG_PATH = '.kunitconfig'
264c2911f1SDaniel LatypovOLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
27d9d6b822SDavid GowDEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
28021ed9f5SHeidi FahimBROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
29128dc4bcSAndy ShevchenkoOUTFILE_PATH = 'test.log'
3087c9c163SBrendan HigginsABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
3187c9c163SBrendan HigginsQEMU_CONFIGS_DIR = os.path.join(ABS_TOOL_PATH, 'qemu_configs')
326ebf5866SFelix Guo
336ebf5866SFelix Guoclass ConfigError(Exception):
346ebf5866SFelix Guo	"""Represents an error trying to configure the Linux kernel."""
356ebf5866SFelix Guo
366ebf5866SFelix Guo
376ebf5866SFelix Guoclass BuildError(Exception):
386ebf5866SFelix Guo	"""Represents an error trying to build the Linux kernel."""
396ebf5866SFelix Guo
406ebf5866SFelix Guo
410453f984SDaniel Latypovclass LinuxSourceTreeOperations:
426ebf5866SFelix Guo	"""An abstraction over command line operations performed on a source tree."""
436ebf5866SFelix Guo
4487c9c163SBrendan Higgins	def __init__(self, linux_arch: str, cross_compile: Optional[str]):
4587c9c163SBrendan Higgins		self._linux_arch = linux_arch
4687c9c163SBrendan Higgins		self._cross_compile = cross_compile
4787c9c163SBrendan Higgins
4809641f7cSDaniel Latypov	def make_mrproper(self) -> None:
496ebf5866SFelix Guo		try:
505a9fcad7SWill Chen			subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT)
516ebf5866SFelix Guo		except OSError as e:
521abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
536ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
541abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
556ebf5866SFelix Guo
560453f984SDaniel Latypov	def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
5787c9c163SBrendan Higgins		pass
5887c9c163SBrendan Higgins
59aa1c0555SDaniel Latypov	def make_allyesconfig(self, build_dir: str, make_options) -> None:
6087c9c163SBrendan Higgins		raise ConfigError('Only the "um" arch is supported for alltests')
6187c9c163SBrendan Higgins
62aa1c0555SDaniel Latypov	def make_olddefconfig(self, build_dir: str, make_options) -> None:
63aa1c0555SDaniel Latypov		command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, 'olddefconfig']
6487c9c163SBrendan Higgins		if self._cross_compile:
6587c9c163SBrendan Higgins			command += ['CROSS_COMPILE=' + self._cross_compile]
660476e69fSGreg Thelen		if make_options:
670476e69fSGreg Thelen			command.extend(make_options)
6887c9c163SBrendan Higgins		print('Populating config with:\n$', ' '.join(command))
696ebf5866SFelix Guo		try:
705a9fcad7SWill Chen			subprocess.check_output(command, stderr=subprocess.STDOUT)
716ebf5866SFelix Guo		except OSError as e:
721abdd39fSDaniel Latypov			raise ConfigError('Could not call make command: ' + str(e))
736ebf5866SFelix Guo		except subprocess.CalledProcessError as e:
741abdd39fSDaniel Latypov			raise ConfigError(e.output.decode())
756ebf5866SFelix Guo
76aa1c0555SDaniel Latypov	def make(self, jobs, build_dir: str, make_options) -> None:
77aa1c0555SDaniel Latypov		command = ['make', 'ARCH=' + self._linux_arch, 'O=' + build_dir, '--jobs=' + str(jobs)]
7887c9c163SBrendan Higgins		if make_options:
7987c9c163SBrendan Higgins			command.extend(make_options)
8087c9c163SBrendan Higgins		if self._cross_compile:
8187c9c163SBrendan Higgins			command += ['CROSS_COMPILE=' + self._cross_compile]
8287c9c163SBrendan Higgins		print('Building with:\n$', ' '.join(command))
8387c9c163SBrendan Higgins		try:
8487c9c163SBrendan Higgins			proc = subprocess.Popen(command,
8587c9c163SBrendan Higgins						stderr=subprocess.PIPE,
8687c9c163SBrendan Higgins						stdout=subprocess.DEVNULL)
8787c9c163SBrendan Higgins		except OSError as e:
8887c9c163SBrendan Higgins			raise BuildError('Could not call execute make: ' + str(e))
8987c9c163SBrendan Higgins		except subprocess.CalledProcessError as e:
9087c9c163SBrendan Higgins			raise BuildError(e.output)
9187c9c163SBrendan Higgins		_, stderr = proc.communicate()
9287c9c163SBrendan Higgins		if proc.returncode != 0:
9387c9c163SBrendan Higgins			raise BuildError(stderr.decode())
9487c9c163SBrendan Higgins		if stderr:  # likely only due to build warnings
9587c9c163SBrendan Higgins			print(stderr.decode())
9687c9c163SBrendan Higgins
977d7c48dfSDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
987d7c48dfSDaniel Latypov		raise RuntimeError('not implemented!')
9987c9c163SBrendan Higgins
10087c9c163SBrendan Higgins
10187c9c163SBrendan Higginsclass LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
10287c9c163SBrendan Higgins
10387c9c163SBrendan Higgins	def __init__(self, qemu_arch_params: qemu_config.QemuArchParams, cross_compile: Optional[str]):
10487c9c163SBrendan Higgins		super().__init__(linux_arch=qemu_arch_params.linux_arch,
10587c9c163SBrendan Higgins				 cross_compile=cross_compile)
10687c9c163SBrendan Higgins		self._kconfig = qemu_arch_params.kconfig
10787c9c163SBrendan Higgins		self._qemu_arch = qemu_arch_params.qemu_arch
10887c9c163SBrendan Higgins		self._kernel_path = qemu_arch_params.kernel_path
10987c9c163SBrendan Higgins		self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
11087c9c163SBrendan Higgins		self._extra_qemu_params = qemu_arch_params.extra_qemu_params
11187c9c163SBrendan Higgins
11287c9c163SBrendan Higgins	def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
11398978490SDaniel Latypov		kconfig = kunit_config.parse_from_string(self._kconfig)
11487c9c163SBrendan Higgins		base_kunitconfig.merge_in_entries(kconfig)
11587c9c163SBrendan Higgins
1167d7c48dfSDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
11787c9c163SBrendan Higgins		kernel_path = os.path.join(build_dir, self._kernel_path)
11887c9c163SBrendan Higgins		qemu_command = ['qemu-system-' + self._qemu_arch,
11987c9c163SBrendan Higgins				'-nodefaults',
12087c9c163SBrendan Higgins				'-m', '1024',
12187c9c163SBrendan Higgins				'-kernel', kernel_path,
1223f0a50f3SDaniel Latypov				'-append', ' '.join(params + [self._kernel_command_line]),
12387c9c163SBrendan Higgins				'-no-reboot',
12487c9c163SBrendan Higgins				'-nographic',
1253f0a50f3SDaniel Latypov				'-serial', 'stdio'] + self._extra_qemu_params
1263f0a50f3SDaniel Latypov		# Note: shlex.join() does what we want, but requires python 3.8+.
1273f0a50f3SDaniel Latypov		print('Running tests with:\n$', ' '.join(shlex.quote(arg) for arg in qemu_command))
1283f0a50f3SDaniel Latypov		return subprocess.Popen(qemu_command,
12987c9c163SBrendan Higgins					stdin=subprocess.PIPE,
1307d7c48dfSDaniel Latypov					stdout=subprocess.PIPE,
13187c9c163SBrendan Higgins					stderr=subprocess.STDOUT,
1323f0a50f3SDaniel Latypov					text=True, errors='backslashreplace')
13387c9c163SBrendan Higgins
13487c9c163SBrendan Higginsclass LinuxSourceTreeOperationsUml(LinuxSourceTreeOperations):
13587c9c163SBrendan Higgins	"""An abstraction over command line operations performed on a source tree."""
13687c9c163SBrendan Higgins
13787c9c163SBrendan Higgins	def __init__(self, cross_compile=None):
13887c9c163SBrendan Higgins		super().__init__(linux_arch='um', cross_compile=cross_compile)
13987c9c163SBrendan Higgins
140aa1c0555SDaniel Latypov	def make_allyesconfig(self, build_dir: str, make_options) -> None:
141e756dbebSDaniel Latypov		stdout.print_with_timestamp(
142021ed9f5SHeidi Fahim			'Enabling all CONFIGs for UML...')
143aa1c0555SDaniel Latypov		command = ['make', 'ARCH=um', 'O=' + build_dir, 'allyesconfig']
14467e2fae3SBrendan Higgins		if make_options:
14567e2fae3SBrendan Higgins			command.extend(make_options)
146021ed9f5SHeidi Fahim		process = subprocess.Popen(
14767e2fae3SBrendan Higgins			command,
148021ed9f5SHeidi Fahim			stdout=subprocess.DEVNULL,
149021ed9f5SHeidi Fahim			stderr=subprocess.STDOUT)
150021ed9f5SHeidi Fahim		process.wait()
151e756dbebSDaniel Latypov		stdout.print_with_timestamp(
152021ed9f5SHeidi Fahim			'Disabling broken configs to run KUnit tests...')
153a54ea2e0SDaniel Latypov
154a54ea2e0SDaniel Latypov		with open(get_kconfig_path(build_dir), 'a') as config:
155a54ea2e0SDaniel Latypov			with open(BROKEN_ALLCONFIG_PATH, 'r') as disable:
156a54ea2e0SDaniel Latypov				config.write(disable.read())
157e756dbebSDaniel Latypov		stdout.print_with_timestamp(
158021ed9f5SHeidi Fahim			'Starting Kernel with all configs takes a few minutes...')
159021ed9f5SHeidi Fahim
1607d7c48dfSDaniel Latypov	def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
1616ebf5866SFelix Guo		"""Runs the Linux UML binary. Must be named 'linux'."""
162aa1c0555SDaniel Latypov		linux_bin = os.path.join(build_dir, 'linux')
1639241bc81SDaniel Latypov		params.extend(['mem=1G', 'console=tty', 'kunit_shutdown=halt'])
1647d7c48dfSDaniel Latypov		return subprocess.Popen([linux_bin] + params,
16587c9c163SBrendan Higgins					   stdin=subprocess.PIPE,
1667d7c48dfSDaniel Latypov					   stdout=subprocess.PIPE,
16787c9c163SBrendan Higgins					   stderr=subprocess.STDOUT,
1682ab5d5e6SDaniel Latypov					   text=True, errors='backslashreplace')
1696ebf5866SFelix Guo
170aa1c0555SDaniel Latypovdef get_kconfig_path(build_dir: str) -> str:
171aa1c0555SDaniel Latypov	return os.path.join(build_dir, KCONFIG_PATH)
1726ebf5866SFelix Guo
173aa1c0555SDaniel Latypovdef get_kunitconfig_path(build_dir: str) -> str:
174aa1c0555SDaniel Latypov	return os.path.join(build_dir, KUNITCONFIG_PATH)
175fcdb0bc0SAndy Shevchenko
176aa1c0555SDaniel Latypovdef get_old_kunitconfig_path(build_dir: str) -> str:
177aa1c0555SDaniel Latypov	return os.path.join(build_dir, OLD_KUNITCONFIG_PATH)
1784c2911f1SDaniel Latypov
179aa1c0555SDaniel Latypovdef get_outfile_path(build_dir: str) -> str:
180aa1c0555SDaniel Latypov	return os.path.join(build_dir, OUTFILE_PATH)
181128dc4bcSAndy Shevchenko
1828c278d97SDaniel Latypovdef _default_qemu_config_path(arch: str) -> str:
18387c9c163SBrendan Higgins	config_path = os.path.join(QEMU_CONFIGS_DIR, arch + '.py')
1840453f984SDaniel Latypov	if os.path.isfile(config_path):
1858c278d97SDaniel Latypov		return config_path
186fe678fedSDaniel Latypov
187fe678fedSDaniel Latypov	options = [f[:-3] for f in os.listdir(QEMU_CONFIGS_DIR) if f.endswith('.py')]
188fe678fedSDaniel Latypov	raise ConfigError(arch + ' is not a valid arch, options are ' + str(sorted(options)))
18987c9c163SBrendan Higgins
1908c278d97SDaniel Latypovdef _get_qemu_ops(config_path: str,
191*a9333bd3SDaniel Latypov		  extra_qemu_args: Optional[List[str]],
1928c278d97SDaniel Latypov		  cross_compile: Optional[str]) -> Tuple[str, LinuxSourceTreeOperations]:
19387c9c163SBrendan Higgins	# The module name/path has very little to do with where the actual file
19487c9c163SBrendan Higgins	# exists (I learned this through experimentation and could not find it
19587c9c163SBrendan Higgins	# anywhere in the Python documentation).
19687c9c163SBrendan Higgins	#
19787c9c163SBrendan Higgins	# Bascially, we completely ignore the actual file location of the config
19887c9c163SBrendan Higgins	# we are loading and just tell Python that the module lives in the
19987c9c163SBrendan Higgins	# QEMU_CONFIGS_DIR for import purposes regardless of where it actually
20087c9c163SBrendan Higgins	# exists as a file.
20187c9c163SBrendan Higgins	module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
20287c9c163SBrendan Higgins	spec = importlib.util.spec_from_file_location(module_path, config_path)
20385310a62SDaniel Latypov	assert spec is not None
20487c9c163SBrendan Higgins	config = importlib.util.module_from_spec(spec)
20552a5d80aSDaniel Latypov	# See https://github.com/python/typeshed/pull/2626 for context.
20652a5d80aSDaniel Latypov	assert isinstance(spec.loader, importlib.abc.Loader)
20752a5d80aSDaniel Latypov	spec.loader.exec_module(config)
20852a5d80aSDaniel Latypov
20952a5d80aSDaniel Latypov	if not hasattr(config, 'QEMU_ARCH'):
21052a5d80aSDaniel Latypov		raise ValueError('qemu_config module missing "QEMU_ARCH": ' + config_path)
21152a5d80aSDaniel Latypov	params: qemu_config.QemuArchParams = config.QEMU_ARCH  # type: ignore
212*a9333bd3SDaniel Latypov	if extra_qemu_args:
213*a9333bd3SDaniel Latypov		params.extra_qemu_params.extend(extra_qemu_args)
21452a5d80aSDaniel Latypov	return params.linux_arch, LinuxSourceTreeOperationsQemu(
21552a5d80aSDaniel Latypov			params, cross_compile=cross_compile)
21687c9c163SBrendan Higgins
2170453f984SDaniel Latypovclass LinuxSourceTree:
2186ebf5866SFelix Guo	"""Represents a Linux kernel source tree with KUnit tests."""
2196ebf5866SFelix Guo
22087c9c163SBrendan Higgins	def __init__(
22187c9c163SBrendan Higgins	      self,
22287c9c163SBrendan Higgins	      build_dir: str,
22387c9c163SBrendan Higgins	      kunitconfig_path='',
2249f57cc76SDaniel Latypov	      kconfig_add: Optional[List[str]]=None,
22587c9c163SBrendan Higgins	      arch=None,
22687c9c163SBrendan Higgins	      cross_compile=None,
227*a9333bd3SDaniel Latypov	      qemu_config_path=None,
228*a9333bd3SDaniel Latypov	      extra_qemu_args=None) -> None:
229021ed9f5SHeidi Fahim		signal.signal(signal.SIGINT, self.signal_handler)
23087c9c163SBrendan Higgins		if qemu_config_path:
231*a9333bd3SDaniel Latypov			self._arch, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
23287c9c163SBrendan Higgins		else:
23387c9c163SBrendan Higgins			self._arch = 'um' if arch is None else arch
2348c278d97SDaniel Latypov			if self._arch == 'um':
2358c278d97SDaniel Latypov				self._ops = LinuxSourceTreeOperationsUml(cross_compile=cross_compile)
2368c278d97SDaniel Latypov			else:
2378c278d97SDaniel Latypov				qemu_config_path = _default_qemu_config_path(self._arch)
238*a9333bd3SDaniel Latypov				_, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)
2392b8fdbbfSDaniel Latypov
240243180f5SDaniel Latypov		if kunitconfig_path:
2419854781dSDaniel Latypov			if os.path.isdir(kunitconfig_path):
2429854781dSDaniel Latypov				kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH)
243243180f5SDaniel Latypov			if not os.path.exists(kunitconfig_path):
244243180f5SDaniel Latypov				raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist')
245243180f5SDaniel Latypov		else:
2462b8fdbbfSDaniel Latypov			kunitconfig_path = get_kunitconfig_path(build_dir)
2472b8fdbbfSDaniel Latypov			if not os.path.exists(kunitconfig_path):
248243180f5SDaniel Latypov				shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
2492b8fdbbfSDaniel Latypov
25098978490SDaniel Latypov		self._kconfig = kunit_config.parse_file(kunitconfig_path)
2519f57cc76SDaniel Latypov		if kconfig_add:
2529f57cc76SDaniel Latypov			kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
2539f57cc76SDaniel Latypov			self._kconfig.merge_in_entries(kconfig)
2549f57cc76SDaniel Latypov
255885210d3SDaniel Latypov	def arch(self) -> str:
256885210d3SDaniel Latypov		return self._arch
2572b8fdbbfSDaniel Latypov
25809641f7cSDaniel Latypov	def clean(self) -> bool:
2596ebf5866SFelix Guo		try:
2606ebf5866SFelix Guo			self._ops.make_mrproper()
2616ebf5866SFelix Guo		except ConfigError as e:
2626ebf5866SFelix Guo			logging.error(e)
2636ebf5866SFelix Guo			return False
2646ebf5866SFelix Guo		return True
2656ebf5866SFelix Guo
266aa1c0555SDaniel Latypov	def validate_config(self, build_dir: str) -> bool:
267dde54b94SHeidi Fahim		kconfig_path = get_kconfig_path(build_dir)
26898978490SDaniel Latypov		validated_kconfig = kunit_config.parse_file(kconfig_path)
269c44895b6SDaniel Latypov		if self._kconfig.is_subset_of(validated_kconfig):
270c44895b6SDaniel Latypov			return True
271dde54b94SHeidi Fahim		invalid = self._kconfig.entries() - validated_kconfig.entries()
272c44895b6SDaniel Latypov		message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
273c44895b6SDaniel Latypov			  'This is probably due to unsatisfied dependencies.\n' \
274c44895b6SDaniel Latypov			  'Missing: ' + ', '.join([str(e) for e in invalid])
275c44895b6SDaniel Latypov		if self._arch == 'um':
276c44895b6SDaniel Latypov			message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
277c44895b6SDaniel Latypov				   'on a different architecture with something like "--arch=x86_64".'
278dde54b94SHeidi Fahim		logging.error(message)
279dde54b94SHeidi Fahim		return False
280dde54b94SHeidi Fahim
281aa1c0555SDaniel Latypov	def build_config(self, build_dir: str, make_options) -> bool:
2826ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
2836ebf5866SFelix Guo		if build_dir and not os.path.exists(build_dir):
2846ebf5866SFelix Guo			os.mkdir(build_dir)
2856ebf5866SFelix Guo		try:
28687c9c163SBrendan Higgins			self._ops.make_arch_qemuconfig(self._kconfig)
28787c9c163SBrendan Higgins			self._kconfig.write_to_file(kconfig_path)
2880476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
2896ebf5866SFelix Guo		except ConfigError as e:
2906ebf5866SFelix Guo			logging.error(e)
2916ebf5866SFelix Guo			return False
2924c2911f1SDaniel Latypov		if not self.validate_config(build_dir):
2934c2911f1SDaniel Latypov			return False
2944c2911f1SDaniel Latypov
2954c2911f1SDaniel Latypov		old_path = get_old_kunitconfig_path(build_dir)
2964c2911f1SDaniel Latypov		if os.path.exists(old_path):
2974c2911f1SDaniel Latypov			os.remove(old_path)  # write_to_file appends to the file
2984c2911f1SDaniel Latypov		self._kconfig.write_to_file(old_path)
2994c2911f1SDaniel Latypov		return True
3004c2911f1SDaniel Latypov
3014c2911f1SDaniel Latypov	def _kunitconfig_changed(self, build_dir: str) -> bool:
3024c2911f1SDaniel Latypov		old_path = get_old_kunitconfig_path(build_dir)
3034c2911f1SDaniel Latypov		if not os.path.exists(old_path):
3044c2911f1SDaniel Latypov			return True
3054c2911f1SDaniel Latypov
3064c2911f1SDaniel Latypov		old_kconfig = kunit_config.parse_file(old_path)
3074c2911f1SDaniel Latypov		return old_kconfig.entries() != self._kconfig.entries()
3086ebf5866SFelix Guo
309aa1c0555SDaniel Latypov	def build_reconfig(self, build_dir: str, make_options) -> bool:
31014ee5cfdSSeongJae Park		"""Creates a new .config if it is not a subset of the .kunitconfig."""
3116ebf5866SFelix Guo		kconfig_path = get_kconfig_path(build_dir)
3124c2911f1SDaniel Latypov		if not os.path.exists(kconfig_path):
3134c2911f1SDaniel Latypov			print('Generating .config ...')
3144c2911f1SDaniel Latypov			return self.build_config(build_dir, make_options)
3154c2911f1SDaniel Latypov
31698978490SDaniel Latypov		existing_kconfig = kunit_config.parse_file(kconfig_path)
31787c9c163SBrendan Higgins		self._ops.make_arch_qemuconfig(self._kconfig)
3184c2911f1SDaniel Latypov		if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
3194c2911f1SDaniel Latypov			return True
3206ebf5866SFelix Guo		print('Regenerating .config ...')
3216ebf5866SFelix Guo		os.remove(kconfig_path)
3220476e69fSGreg Thelen		return self.build_config(build_dir, make_options)
3236ebf5866SFelix Guo
324aa1c0555SDaniel Latypov	def build_kernel(self, alltests, jobs, build_dir: str, make_options) -> bool:
3256ebf5866SFelix Guo		try:
32667e2fae3SBrendan Higgins			if alltests:
32767e2fae3SBrendan Higgins				self._ops.make_allyesconfig(build_dir, make_options)
3280476e69fSGreg Thelen			self._ops.make_olddefconfig(build_dir, make_options)
3290476e69fSGreg Thelen			self._ops.make(jobs, build_dir, make_options)
3306ebf5866SFelix Guo		except (ConfigError, BuildError) as e:
3316ebf5866SFelix Guo			logging.error(e)
3326ebf5866SFelix Guo			return False
333dde54b94SHeidi Fahim		return self.validate_config(build_dir)
3346ebf5866SFelix Guo
3357af29141SDaniel Latypov	def run_kernel(self, args=None, build_dir='', filter_glob='', timeout=None) -> Iterator[str]:
3367af29141SDaniel Latypov		if not args:
3377af29141SDaniel Latypov			args = []
338d992880bSDaniel Latypov		if filter_glob:
339d992880bSDaniel Latypov			args.append('kunit.filter_glob='+filter_glob)
3407d7c48dfSDaniel Latypov
3417d7c48dfSDaniel Latypov		process = self._ops.start(args, build_dir)
3427d7c48dfSDaniel Latypov		assert process.stdout is not None  # tell mypy it's set
3437d7c48dfSDaniel Latypov
3447d7c48dfSDaniel Latypov		# Enforce the timeout in a background thread.
3457d7c48dfSDaniel Latypov		def _wait_proc():
3467d7c48dfSDaniel Latypov			try:
3477d7c48dfSDaniel Latypov				process.wait(timeout=timeout)
3487d7c48dfSDaniel Latypov			except Exception as e:
3497d7c48dfSDaniel Latypov				print(e)
3507d7c48dfSDaniel Latypov				process.terminate()
3517d7c48dfSDaniel Latypov				process.wait()
3527d7c48dfSDaniel Latypov		waiter = threading.Thread(target=_wait_proc)
3537d7c48dfSDaniel Latypov		waiter.start()
3547d7c48dfSDaniel Latypov
3557d7c48dfSDaniel Latypov		output = open(get_outfile_path(build_dir), 'w')
3567d7c48dfSDaniel Latypov		try:
3577d7c48dfSDaniel Latypov			# Tee the output to the file and to our caller in real time.
3587d7c48dfSDaniel Latypov			for line in process.stdout:
3597d7c48dfSDaniel Latypov				output.write(line)
360021ed9f5SHeidi Fahim				yield line
3617d7c48dfSDaniel Latypov		# This runs even if our caller doesn't consume every line.
3627d7c48dfSDaniel Latypov		finally:
3637d7c48dfSDaniel Latypov			# Flush any leftover output to the file
3647d7c48dfSDaniel Latypov			output.write(process.stdout.read())
3657d7c48dfSDaniel Latypov			output.close()
3667d7c48dfSDaniel Latypov			process.stdout.close()
3677d7c48dfSDaniel Latypov
3687d7c48dfSDaniel Latypov			waiter.join()
3697d7c48dfSDaniel Latypov			subprocess.call(['stty', 'sane'])
370021ed9f5SHeidi Fahim
3710453f984SDaniel Latypov	def signal_handler(self, unused_sig, unused_frame) -> None:
372021ed9f5SHeidi Fahim		logging.error('Build interruption occurred. Cleaning console.')
373021ed9f5SHeidi Fahim		subprocess.call(['stty', 'sane'])
374