1#!/bin/bash
2
3# This is a script to build a Debian image that can run in a VM created via AVF.
4# TODOs:
5# - Add Android-specific packages via a new class
6# - Use a stable release from debian-cloud-images
7
8show_help() {
9	echo "Usage: sudo $0 [OPTION]... [FILE]"
10	echo "Builds a debian image and save it to FILE. [sudo is required]"
11	echo "Options:"
12	echo "-h         Print usage and this help message and exit."
13	echo "-a ARCH    Architecture of the image [default is host arch: $(uname -m)]"
14	echo "-r         Release mode build"
15	echo "-w         Save temp work directory [for debugging]"
16}
17
18check_sudo() {
19	if [ "$EUID" -ne 0 ]; then
20		echo "Please run as root." ; exit 1
21	fi
22}
23
24parse_options() {
25	while getopts "a:hrw" option; do
26		case ${option} in
27			h)
28				show_help ; exit
29				;;
30			a)
31				arch="$OPTARG"
32				;;
33			r)
34				mode=release
35				;;
36			w)
37				save_workdir=1
38				;;
39			*)
40				echo "Invalid option: $OPTARG" ; exit 1
41				;;
42		esac
43	done
44	case "$arch" in
45		aarch64)
46			debian_arch="arm64"
47			;;
48		x86_64)
49			debian_arch="amd64"
50			;;
51		*)
52			echo "Invalid architecture: $arch" ; exit 1
53			;;
54	esac
55	if [[ "${*:$OPTIND:1}" ]]; then
56		built_image="${*:$OPTIND:1}"
57	fi
58}
59
60prepare_build_id() {
61	local filename=build_id
62	if [ -z "${KOKORO_BUILD_NUMBER}" ]; then
63		echo eng-$(hostname)-$(date --utc) > ${filename}
64	else
65		echo ${KOKORO_BUILD_NUMBER} > ${filename}
66	fi
67	echo ${filename}
68}
69
70install_prerequisites() {
71	apt update
72	packages=(
73		apt-utils
74		automake
75		binfmt-support
76		build-essential
77		ca-certificates
78		cmake
79		curl
80		debsums
81		dosfstools
82		fai-server
83		fai-setup-storage
84		fdisk
85		git
86		libjson-c-dev
87		libtool
88		libwebsockets-dev
89		make
90		protobuf-compiler
91		python3
92		python3-libcloud
93		python3-marshmallow
94		python3-pytest
95		python3-yaml
96		qemu-user-static
97		qemu-utils
98		sudo
99		udev
100	)
101	if [[ "$arch" == "aarch64" ]]; then
102		packages+=(
103			gcc-aarch64-linux-gnu
104			libc6-dev-arm64-cross
105			qemu-system-arm
106		)
107	else
108		packages+=(
109			qemu-system
110		)
111	fi
112
113	# TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
114	if [[ "$arch" == "x86_64" ]]; then
115		packages+=(
116			libguestfs-tools
117			linux-image-generic
118		)
119	fi
120	DEBIAN_FRONTEND=noninteractive \
121	apt install --no-install-recommends --assume-yes "${packages[@]}"
122
123	if [ ! -f $"HOME"/.cargo/bin/cargo ]; then
124		curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
125	fi
126
127	source "$HOME"/.cargo/env
128	rustup target add "${arch}"-unknown-linux-gnu
129	cargo install cargo-license
130}
131
132download_debian_cloud_image() {
133	local ver=master
134	local prj=debian-cloud-images
135	local url="https://salsa.debian.org/cloud-team/${prj}/-/archive/${ver}/${prj}-${ver}.tar.gz"
136	local outdir="${debian_cloud_image}"
137
138	mkdir -p "${outdir}"
139	wget -O - "${url}" | tar xz -C "${outdir}" --strip-components=1
140}
141
142build_rust_binary_and_copy() {
143	pushd "$(dirname "$0")/../../guest/$1" > /dev/null
144	local release_flag=
145	local artifact_mode=debug
146	if [[ "$mode" == "release" ]]; then
147		release_flag="--release"
148		artifact_mode=release
149	fi
150	RUSTFLAGS="-C linker=${arch}-linux-gnu-gcc" cargo build \
151		--target "${arch}-unknown-linux-gnu" \
152		--target-dir "${workdir}/$1" ${release_flag}
153	mkdir -p "${dst}/files/usr/local/bin/$1"
154	cp "${workdir}/$1/${arch}-unknown-linux-gnu/${artifact_mode}/$1" "${dst}/files/usr/local/bin/$1/AVF"
155	chmod 777 "${dst}/files/usr/local/bin/$1/AVF"
156
157	mkdir -p "${dst}/files/usr/share/doc/$1"
158	cargo license > "${dst}/files/usr/share/doc/$1/copyright"
159	popd > /dev/null
160}
161
162build_ttyd() {
163	local ttyd_version=1.7.7
164	local url="https://github.com/tsl0922/ttyd/archive/refs/tags/${ttyd_version}.tar.gz"
165	cp -r "$(dirname "$0")/ttyd" "${workdir}/ttyd"
166
167	pushd "${workdir}" > /dev/null
168	wget "${url}" -O - | tar xz
169	cp ttyd/* ttyd-${ttyd_version}/scripts
170	pushd "$workdir/ttyd-${ttyd_version}" > /dev/null
171	bash -c "env BUILD_TARGET=${arch} ./scripts/cross-build.sh"
172	mkdir -p "${dst}/files/usr/local/bin/ttyd"
173	cp "/tmp/stage/${arch}-linux-musl/bin/ttyd" "${dst}/files/usr/local/bin/ttyd/AVF"
174	chmod 777 "${dst}/files/usr/local/bin/ttyd/AVF"
175	mkdir -p "${dst}/files/usr/share/doc/ttyd"
176	cp LICENSE "${dst}/files/usr/share/doc/ttyd/copyright"
177	popd > /dev/null
178	popd > /dev/null
179}
180
181copy_android_config() {
182	local src
183	local dst
184	src="$(dirname "$0")/fai_config"
185	dst="${config_space}"
186
187	cp -R "${src}"/* "${dst}"
188	cp "$(dirname "$0")/image.yaml" "${resources_dir}"
189
190	cp -R "$(dirname "$0")/localdebs/" "${debian_cloud_image}/"
191	build_ttyd
192	build_rust_binary_and_copy forwarder_guest
193	build_rust_binary_and_copy forwarder_guest_launcher
194	build_rust_binary_and_copy ip_addr_reporter
195	build_rust_binary_and_copy shutdown_runner
196}
197
198run_fai() {
199	local out="${built_image}"
200	make -C "${debian_cloud_image}" "image_bookworm_nocloud_${debian_arch}"
201	mv "${debian_cloud_image}/image_bookworm_nocloud_${debian_arch}.raw" "${out}"
202}
203
204extract_partitions() {
205	root_partition_num=1
206	bios_partition_num=14
207	efi_partition_num=15
208
209	loop=$(losetup -f --show --partscan $built_image)
210	dd if="${loop}p$root_partition_num" of=root_part
211	if [[ "$arch" == "x86_64" ]]; then
212		dd if="${loop}p$bios_partition_num" of=bios_part
213	fi
214	dd if="${loop}p$efi_partition_num" of=efi_part
215	losetup -d "${loop}"
216
217	sed -i "s/{root_part_guid}/$(sfdisk --part-uuid $built_image $root_partition_num)/g" vm_config.json
218	if [[ "$arch" == "x86_64" ]]; then
219		sed -i "s/{bios_part_guid}/$(sfdisk --part-uuid $built_image $bios_partition_num)/g" vm_config.json
220	fi
221	sed -i "s/{efi_part_guid}/$(sfdisk --part-uuid $built_image $efi_partition_num)/g" vm_config.json
222}
223
224clean_up() {
225	[ "$save_workdir" -eq 1 ] || rm -rf "${workdir}"
226}
227
228set -e
229trap clean_up EXIT
230
231built_image=image.raw
232workdir=$(mktemp -d)
233build_id=$(prepare_build_id)
234debian_cloud_image=${workdir}/debian_cloud_image
235debian_version=bookworm
236config_space=${debian_cloud_image}/config_space/${debian_version}
237resources_dir=${debian_cloud_image}/src/debian_cloud_images/resources
238arch="$(uname -m)"
239mode=debug
240save_workdir=0
241
242parse_options "$@"
243check_sudo
244install_prerequisites
245download_debian_cloud_image
246copy_android_config
247run_fai
248fdisk -l "${built_image}"
249images=()
250
251cp "$(dirname "$0")/vm_config.json.${arch}" vm_config.json
252
253extract_partitions
254
255if [[ "$arch" == "aarch64" ]]; then
256	images+=(
257		root_part
258		efi_part
259	)
260# TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
261elif [[ "$arch" == "x86_64" ]]; then
262	rm -f vmlinuz initrd.img
263	virt-get-kernel -a "${built_image}"
264	mv vmlinuz* vmlinuz
265	mv initrd.img* initrd.img
266	images+=(
267		bios_part
268		root_part
269		efi_part
270		vmlinuz
271		initrd.img
272	)
273fi
274
275# --sparse option isn't supported in apache-commons-compress
276tar czv -f images.tar.gz ${build_id} "${images[@]}" vm_config.json
277