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