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 and s390x, as
8# it is based on the VM image used by the BPF CI, which is
9# available only for these architectures.
10ARCH="$(uname -m)"
11case "${ARCH}" in
12s390x)
13	QEMU_BINARY=qemu-system-s390x
14	QEMU_CONSOLE="ttyS1"
15	QEMU_FLAGS=(-smp 2)
16	BZIMAGE="arch/s390/boot/compressed/vmlinux"
17	;;
18x86_64)
19	QEMU_BINARY=qemu-system-x86_64
20	QEMU_CONSOLE="ttyS0,115200"
21	QEMU_FLAGS=(-cpu host -smp 8)
22	BZIMAGE="arch/x86/boot/bzImage"
23	;;
24*)
25	echo "Unsupported architecture"
26	exit 1
27	;;
28esac
29DEFAULT_COMMAND="./test_progs"
30MOUNT_DIR="mnt"
31ROOTFS_IMAGE="root.img"
32OUTPUT_DIR="$HOME/.bpf_selftests"
33KCONFIG_REL_PATHS=("tools/testing/selftests/bpf/config" "tools/testing/selftests/bpf/config.${ARCH}")
34INDEX_URL="https://raw.githubusercontent.com/libbpf/ci/master/INDEX"
35NUM_COMPILE_JOBS="$(nproc)"
36LOG_FILE_BASE="$(date +"bpf_selftests.%Y-%m-%d_%H-%M-%S")"
37LOG_FILE="${LOG_FILE_BASE}.log"
38EXIT_STATUS_FILE="${LOG_FILE_BASE}.exit_status"
39
40usage()
41{
42	cat <<EOF
43Usage: $0 [-i] [-s] [-d <output_dir>] -- [<command>]
44
45<command> is the command you would normally run when you are in
46tools/testing/selftests/bpf. e.g:
47
48	$0 -- ./test_progs -t test_lsm
49
50If no command is specified and a debug shell (-s) is not requested,
51"${DEFAULT_COMMAND}" will be run by default.
52
53If you build your kernel using KBUILD_OUTPUT= or O= options, these
54can be passed as environment variables to the script:
55
56  O=<kernel_build_path> $0 -- ./test_progs -t test_lsm
57
58or
59
60  KBUILD_OUTPUT=<kernel_build_path> $0 -- ./test_progs -t test_lsm
61
62Options:
63
64	-i)		Update the rootfs image with a newer version.
65	-d)		Update the output directory (default: ${OUTPUT_DIR})
66	-j)		Number of jobs for compilation, similar to -j in make
67			(default: ${NUM_COMPILE_JOBS})
68	-s)		Instead of powering off the VM, start an interactive
69			shell. If <command> is specified, the shell runs after
70			the command finishes executing
71EOF
72}
73
74unset URLS
75populate_url_map()
76{
77	if ! declare -p URLS &> /dev/null; then
78		# URLS contain the mapping from file names to URLs where
79		# those files can be downloaded from.
80		declare -gA URLS
81		while IFS=$'\t' read -r name url; do
82			URLS["$name"]="$url"
83		done < <(curl -Lsf ${INDEX_URL})
84	fi
85}
86
87download()
88{
89	local file="$1"
90
91	if [[ ! -v URLS[$file] ]]; then
92		echo "$file not found" >&2
93		return 1
94	fi
95
96	echo "Downloading $file..." >&2
97	curl -Lsf "${URLS[$file]}" "${@:2}"
98}
99
100newest_rootfs_version()
101{
102	{
103	for file in "${!URLS[@]}"; do
104		if [[ $file =~ ^"${ARCH}"/libbpf-vmtest-rootfs-(.*)\.tar\.zst$ ]]; then
105			echo "${BASH_REMATCH[1]}"
106		fi
107	done
108	} | sort -rV | head -1
109}
110
111download_rootfs()
112{
113	local rootfsversion="$1"
114	local dir="$2"
115
116	if ! which zstd &> /dev/null; then
117		echo 'Could not find "zstd" on the system, please install zstd'
118		exit 1
119	fi
120
121	download "${ARCH}/libbpf-vmtest-rootfs-$rootfsversion.tar.zst" |
122		zstd -d | sudo tar -C "$dir" -x
123}
124
125recompile_kernel()
126{
127	local kernel_checkout="$1"
128	local make_command="$2"
129
130	cd "${kernel_checkout}"
131
132	${make_command} olddefconfig
133	${make_command}
134}
135
136mount_image()
137{
138	local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
139	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
140
141	sudo mount -o loop "${rootfs_img}" "${mount_dir}"
142}
143
144unmount_image()
145{
146	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
147
148	sudo umount "${mount_dir}" &> /dev/null
149}
150
151update_selftests()
152{
153	local kernel_checkout="$1"
154	local selftests_dir="${kernel_checkout}/tools/testing/selftests/bpf"
155
156	cd "${selftests_dir}"
157	${make_command}
158
159	# Mount the image and copy the selftests to the image.
160	mount_image
161	sudo rm -rf "${mount_dir}/root/bpf"
162	sudo cp -r "${selftests_dir}" "${mount_dir}/root"
163	unmount_image
164}
165
166update_init_script()
167{
168	local init_script_dir="${OUTPUT_DIR}/${MOUNT_DIR}/etc/rcS.d"
169	local init_script="${init_script_dir}/S50-startup"
170	local command="$1"
171	local exit_command="$2"
172
173	mount_image
174
175	if [[ ! -d "${init_script_dir}" ]]; then
176		cat <<EOF
177Could not find ${init_script_dir} in the mounted image.
178This likely indicates a bad rootfs image, Please download
179a new image by passing "-i" to the script
180EOF
181		exit 1
182
183	fi
184
185	sudo bash -c "echo '#!/bin/bash' > ${init_script}"
186
187	if [[ "${command}" != "" ]]; then
188		sudo bash -c "cat >>${init_script}" <<EOF
189# Have a default value in the exit status file
190# incase the VM is forcefully stopped.
191echo "130" > "/root/${EXIT_STATUS_FILE}"
192
193{
194	cd /root/bpf
195	echo ${command}
196	stdbuf -oL -eL ${command}
197	echo "\$?" > "/root/${EXIT_STATUS_FILE}"
198} 2>&1 | tee "/root/${LOG_FILE}"
199# Ensure that the logs are written to disk
200sync
201EOF
202	fi
203
204	sudo bash -c "echo ${exit_command} >> ${init_script}"
205	sudo chmod a+x "${init_script}"
206	unmount_image
207}
208
209create_vm_image()
210{
211	local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
212	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
213
214	rm -rf "${rootfs_img}"
215	touch "${rootfs_img}"
216	chattr +C "${rootfs_img}" >/dev/null 2>&1 || true
217
218	truncate -s 2G "${rootfs_img}"
219	mkfs.ext4 -q "${rootfs_img}"
220
221	mount_image
222	download_rootfs "$(newest_rootfs_version)" "${mount_dir}"
223	unmount_image
224}
225
226run_vm()
227{
228	local kernel_bzimage="$1"
229	local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
230
231	if ! which "${QEMU_BINARY}" &> /dev/null; then
232		cat <<EOF
233Could not find ${QEMU_BINARY}
234Please install qemu or set the QEMU_BINARY environment variable.
235EOF
236		exit 1
237	fi
238
239	${QEMU_BINARY} \
240		-nodefaults \
241		-display none \
242		-serial mon:stdio \
243		"${QEMU_FLAGS[@]}" \
244		-enable-kvm \
245		-m 4G \
246		-drive file="${rootfs_img}",format=raw,index=1,media=disk,if=virtio,cache=none \
247		-kernel "${kernel_bzimage}" \
248		-append "root=/dev/vda rw console=${QEMU_CONSOLE}"
249}
250
251copy_logs()
252{
253	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
254	local log_file="${mount_dir}/root/${LOG_FILE}"
255	local exit_status_file="${mount_dir}/root/${EXIT_STATUS_FILE}"
256
257	mount_image
258	sudo cp ${log_file} "${OUTPUT_DIR}"
259	sudo cp ${exit_status_file} "${OUTPUT_DIR}"
260	sudo rm -f ${log_file}
261	unmount_image
262}
263
264is_rel_path()
265{
266	local path="$1"
267
268	[[ ${path:0:1} != "/" ]]
269}
270
271do_update_kconfig()
272{
273	local kernel_checkout="$1"
274	local kconfig_file="$2"
275
276	rm -f "$kconfig_file" 2> /dev/null
277
278	for config in "${KCONFIG_REL_PATHS[@]}"; do
279		local kconfig_src="${kernel_checkout}/${config}"
280		cat "$kconfig_src" >> "$kconfig_file"
281	done
282}
283
284update_kconfig()
285{
286	local kernel_checkout="$1"
287	local kconfig_file="$2"
288
289	if [[ -f "${kconfig_file}" ]]; then
290		local local_modified="$(stat -c %Y "${kconfig_file}")"
291
292		for config in "${KCONFIG_REL_PATHS[@]}"; do
293			local kconfig_src="${kernel_checkout}/${config}"
294			local src_modified="$(stat -c %Y "${kconfig_src}")"
295			# Only update the config if it has been updated after the
296			# previously cached config was created. This avoids
297			# unnecessarily compiling the kernel and selftests.
298			if [[ "${src_modified}" -gt "${local_modified}" ]]; then
299				do_update_kconfig "$kernel_checkout" "$kconfig_file"
300				# Once we have found one outdated configuration
301				# there is no need to check other ones.
302				break
303			fi
304		done
305	else
306		do_update_kconfig "$kernel_checkout" "$kconfig_file"
307	fi
308}
309
310main()
311{
312	local script_dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
313	local kernel_checkout=$(realpath "${script_dir}"/../../../../)
314	# By default the script searches for the kernel in the checkout directory but
315	# it also obeys environment variables O= and KBUILD_OUTPUT=
316	local kernel_bzimage="${kernel_checkout}/${BZIMAGE}"
317	local command="${DEFAULT_COMMAND}"
318	local update_image="no"
319	local exit_command="poweroff -f"
320	local debug_shell="no"
321
322	while getopts 'hskid:j:' opt; do
323		case ${opt} in
324		i)
325			update_image="yes"
326			;;
327		d)
328			OUTPUT_DIR="$OPTARG"
329			;;
330		j)
331			NUM_COMPILE_JOBS="$OPTARG"
332			;;
333		s)
334			command=""
335			debug_shell="yes"
336			exit_command="bash"
337			;;
338		h)
339			usage
340			exit 0
341			;;
342		\? )
343			echo "Invalid Option: -$OPTARG"
344			usage
345			exit 1
346			;;
347		: )
348			echo "Invalid Option: -$OPTARG requires an argument"
349			usage
350			exit 1
351			;;
352		esac
353	done
354	shift $((OPTIND -1))
355
356	if [[ $# -eq 0  && "${debug_shell}" == "no" ]]; then
357		echo "No command specified, will run ${DEFAULT_COMMAND} in the vm"
358	else
359		command="$@"
360	fi
361
362	local kconfig_file="${OUTPUT_DIR}/latest.config"
363	local make_command="make -j ${NUM_COMPILE_JOBS} KCONFIG_CONFIG=${kconfig_file}"
364
365	# Figure out where the kernel is being built.
366	# O takes precedence over KBUILD_OUTPUT.
367	if [[ "${O:=""}" != "" ]]; then
368		if is_rel_path "${O}"; then
369			O="$(realpath "${PWD}/${O}")"
370		fi
371		kernel_bzimage="${O}/${BZIMAGE}"
372		make_command="${make_command} O=${O}"
373	elif [[ "${KBUILD_OUTPUT:=""}" != "" ]]; then
374		if is_rel_path "${KBUILD_OUTPUT}"; then
375			KBUILD_OUTPUT="$(realpath "${PWD}/${KBUILD_OUTPUT}")"
376		fi
377		kernel_bzimage="${KBUILD_OUTPUT}/${BZIMAGE}"
378		make_command="${make_command} KBUILD_OUTPUT=${KBUILD_OUTPUT}"
379	fi
380
381	populate_url_map
382
383	local rootfs_img="${OUTPUT_DIR}/${ROOTFS_IMAGE}"
384	local mount_dir="${OUTPUT_DIR}/${MOUNT_DIR}"
385
386	echo "Output directory: ${OUTPUT_DIR}"
387
388	mkdir -p "${OUTPUT_DIR}"
389	mkdir -p "${mount_dir}"
390	update_kconfig "${kernel_checkout}" "${kconfig_file}"
391
392	recompile_kernel "${kernel_checkout}" "${make_command}"
393
394	if [[ "${update_image}" == "no" && ! -f "${rootfs_img}" ]]; then
395		echo "rootfs image not found in ${rootfs_img}"
396		update_image="yes"
397	fi
398
399	if [[ "${update_image}" == "yes" ]]; then
400		create_vm_image
401	fi
402
403	update_selftests "${kernel_checkout}" "${make_command}"
404	update_init_script "${command}" "${exit_command}"
405	run_vm "${kernel_bzimage}"
406	if [[ "${command}" != "" ]]; then
407		copy_logs
408		echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}"
409	fi
410}
411
412catch()
413{
414	local exit_code=$1
415	local exit_status_file="${OUTPUT_DIR}/${EXIT_STATUS_FILE}"
416	# This is just a cleanup and the directory may
417	# have already been unmounted. So, don't let this
418	# clobber the error code we intend to return.
419	unmount_image || true
420	if [[ -f "${exit_status_file}" ]]; then
421		exit_code="$(cat ${exit_status_file})"
422	fi
423	exit ${exit_code}
424}
425
426trap 'catch "$?"' EXIT
427
428main "$@"
429