1#!/bin/bash
2# SPDX-License-Identifier: GPL-2.0
3
4set -u
5set -e
6
7# This script currently only works for x86_64
8ARCH="$(uname -m)"
9case "${ARCH}" in
10x86_64)
11	QEMU_BINARY=qemu-system-x86_64
12	BZIMAGE="arch/x86/boot/bzImage"
13	;;
14*)
15	echo "Unsupported architecture"
16	exit 1
17	;;
18esac
19DEFAULT_COMMAND="./hid_bpf"
20SCRIPT_DIR="$(dirname $(realpath $0))"
21OUTPUT_DIR="$SCRIPT_DIR/results"
22KCONFIG_REL_PATHS=("${SCRIPT_DIR}/config" "${SCRIPT_DIR}/config.common" "${SCRIPT_DIR}/config.${ARCH}")
23B2C_URL="https://gitlab.freedesktop.org/mupuf/boot2container/-/raw/master/vm2c.py"
24NUM_COMPILE_JOBS="$(nproc)"
25LOG_FILE_BASE="$(date +"hid_selftests.%Y-%m-%d_%H-%M-%S")"
26LOG_FILE="${LOG_FILE_BASE}.log"
27EXIT_STATUS_FILE="${LOG_FILE_BASE}.exit_status"
28CONTAINER_IMAGE="registry.fedoraproject.org/fedora:36"
29
30usage()
31{
32	cat <<EOF
33Usage: $0 [-i] [-s] [-d <output_dir>] -- [<command>]
34
35<command> is the command you would normally run when you are in
36tools/testing/selftests/bpf. e.g:
37
38	$0 -- ./hid_bpf
39
40If no command is specified and a debug shell (-s) is not requested,
41"${DEFAULT_COMMAND}" will be run by default.
42
43If you build your kernel using KBUILD_OUTPUT= or O= options, these
44can be passed as environment variables to the script:
45
46  O=<kernel_build_path> $0 -- ./hid_bpf
47
48or
49
50  KBUILD_OUTPUT=<kernel_build_path> $0 -- ./hid_bpf
51
52Options:
53
54	-u)		Update the boot2container script to a newer version.
55	-d)		Update the output directory (default: ${OUTPUT_DIR})
56	-j)		Number of jobs for compilation, similar to -j in make
57			(default: ${NUM_COMPILE_JOBS})
58	-s)		Instead of powering off the VM, start an interactive
59			shell. If <command> is specified, the shell runs after
60			the command finishes executing
61EOF
62}
63
64download()
65{
66	local file="$1"
67
68	echo "Downloading $file..." >&2
69	curl -Lsf "$file" -o "${@:2}"
70}
71
72recompile_kernel()
73{
74	local kernel_checkout="$1"
75	local make_command="$2"
76
77	cd "${kernel_checkout}"
78
79	${make_command} olddefconfig
80	${make_command}
81}
82
83update_selftests()
84{
85	local kernel_checkout="$1"
86	local selftests_dir="${kernel_checkout}/tools/testing/selftests/hid"
87
88	cd "${selftests_dir}"
89	${make_command}
90}
91
92run_vm()
93{
94	local b2c="$1"
95	local kernel_bzimage="$2"
96	local command="$3"
97	local post_command=""
98
99	if ! which "${QEMU_BINARY}" &> /dev/null; then
100		cat <<EOF
101Could not find ${QEMU_BINARY}
102Please install qemu or set the QEMU_BINARY environment variable.
103EOF
104		exit 1
105	fi
106
107	# alpine (used in post-container requires the PATH to have /bin
108	export PATH=$PATH:/bin
109
110	if [[ "${debug_shell}" != "yes" ]]
111	then
112		touch ${OUTPUT_DIR}/${LOG_FILE}
113		command="mount bpffs -t bpf /sys/fs/bpf/; set -o pipefail ; ${command} 2>&1 | tee ${OUTPUT_DIR}/${LOG_FILE}"
114		post_command="cat ${OUTPUT_DIR}/${LOG_FILE}"
115	else
116		command="mount bpffs -t bpf /sys/fs/bpf/; ${command}"
117	fi
118
119	set +e
120	$b2c --command "${command}" \
121	     --kernel ${kernel_bzimage} \
122	     --workdir ${OUTPUT_DIR} \
123	     --image ${CONTAINER_IMAGE}
124
125	echo $? > ${OUTPUT_DIR}/${EXIT_STATUS_FILE}
126
127	set -e
128
129	${post_command}
130}
131
132is_rel_path()
133{
134	local path="$1"
135
136	[[ ${path:0:1} != "/" ]]
137}
138
139do_update_kconfig()
140{
141	local kernel_checkout="$1"
142	local kconfig_file="$2"
143
144	rm -f "$kconfig_file" 2> /dev/null
145
146	for config in "${KCONFIG_REL_PATHS[@]}"; do
147		local kconfig_src="${config}"
148		cat "$kconfig_src" >> "$kconfig_file"
149	done
150}
151
152update_kconfig()
153{
154	local kernel_checkout="$1"
155	local kconfig_file="$2"
156
157	if [[ -f "${kconfig_file}" ]]; then
158		local local_modified="$(stat -c %Y "${kconfig_file}")"
159
160		for config in "${KCONFIG_REL_PATHS[@]}"; do
161			local kconfig_src="${config}"
162			local src_modified="$(stat -c %Y "${kconfig_src}")"
163			# Only update the config if it has been updated after the
164			# previously cached config was created. This avoids
165			# unnecessarily compiling the kernel and selftests.
166			if [[ "${src_modified}" -gt "${local_modified}" ]]; then
167				do_update_kconfig "$kernel_checkout" "$kconfig_file"
168				# Once we have found one outdated configuration
169				# there is no need to check other ones.
170				break
171			fi
172		done
173	else
174		do_update_kconfig "$kernel_checkout" "$kconfig_file"
175	fi
176}
177
178main()
179{
180	local script_dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
181	local kernel_checkout=$(realpath "${script_dir}"/../../../../)
182	# By default the script searches for the kernel in the checkout directory but
183	# it also obeys environment variables O= and KBUILD_OUTPUT=
184	local kernel_bzimage="${kernel_checkout}/${BZIMAGE}"
185	local command="${DEFAULT_COMMAND}"
186	local update_b2c="no"
187	local debug_shell="no"
188
189	while getopts ':hsud:j:' opt; do
190		case ${opt} in
191		u)
192			update_b2c="yes"
193			;;
194		d)
195			OUTPUT_DIR="$OPTARG"
196			;;
197		j)
198			NUM_COMPILE_JOBS="$OPTARG"
199			;;
200		s)
201			command="/bin/sh"
202			debug_shell="yes"
203			;;
204		h)
205			usage
206			exit 0
207			;;
208		\? )
209			echo "Invalid Option: -$OPTARG"
210			usage
211			exit 1
212			;;
213		: )
214			echo "Invalid Option: -$OPTARG requires an argument"
215			usage
216			exit 1
217			;;
218		esac
219	done
220	shift $((OPTIND -1))
221
222	# trap 'catch "$?"' EXIT
223
224	if [[ "${debug_shell}" == "no" ]]; then
225		if [[ $# -eq 0 ]]; then
226			echo "No command specified, will run ${DEFAULT_COMMAND} in the vm"
227		else
228			command="$@"
229
230			if [[ "${command}" == "/bin/bash" || "${command}" == "bash" ]]
231			then
232				debug_shell="yes"
233			fi
234		fi
235	fi
236
237	local kconfig_file="${OUTPUT_DIR}/latest.config"
238	local make_command="make -j ${NUM_COMPILE_JOBS} KCONFIG_CONFIG=${kconfig_file}"
239
240	# Figure out where the kernel is being built.
241	# O takes precedence over KBUILD_OUTPUT.
242	if [[ "${O:=""}" != "" ]]; then
243		if is_rel_path "${O}"; then
244			O="$(realpath "${PWD}/${O}")"
245		fi
246		kernel_bzimage="${O}/${BZIMAGE}"
247		make_command="${make_command} O=${O}"
248	elif [[ "${KBUILD_OUTPUT:=""}" != "" ]]; then
249		if is_rel_path "${KBUILD_OUTPUT}"; then
250			KBUILD_OUTPUT="$(realpath "${PWD}/${KBUILD_OUTPUT}")"
251		fi
252		kernel_bzimage="${KBUILD_OUTPUT}/${BZIMAGE}"
253		make_command="${make_command} KBUILD_OUTPUT=${KBUILD_OUTPUT}"
254	fi
255
256	local b2c="${OUTPUT_DIR}/vm2c.py"
257
258	echo "Output directory: ${OUTPUT_DIR}"
259
260	mkdir -p "${OUTPUT_DIR}"
261	update_kconfig "${kernel_checkout}" "${kconfig_file}"
262
263	recompile_kernel "${kernel_checkout}" "${make_command}"
264
265	if [[ "${update_b2c}" == "no" && ! -f "${b2c}" ]]; then
266		echo "vm2c script not found in ${b2c}"
267		update_b2c="yes"
268	fi
269
270	if [[ "${update_b2c}" == "yes" ]]; then
271		download $B2C_URL $b2c
272		chmod +x $b2c
273	fi
274
275	update_selftests "${kernel_checkout}" "${make_command}"
276	run_vm $b2c "${kernel_bzimage}" "${command}"
277	if [[ "${debug_shell}" != "yes" ]]; then
278		echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}"
279	fi
280
281	exit $(cat ${OUTPUT_DIR}/${EXIT_STATUS_FILE})
282}
283
284main "$@"
285