1#!/bin/bash
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18set -e
19set -u
20
21SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
22
23usage() {
24  echo "usage: $0 [-h] -i input.raw -o output.iso"
25  exit 1
26}
27
28input=
29output=
30
31while getopts ":hi:o:" opt; do
32  case "${opt}" in
33    h)
34      usage
35      ;;
36    i)
37      input="${OPTARG}"
38      ;;
39    o)
40      output="${OPTARG}"
41      ;;
42    \?)
43      echo "Invalid option: ${OPTARG}" >&2
44      usage
45      ;;
46    :)
47      echo "Invalid option: ${OPTARG} requires an argument" >&2
48      usage
49      ;;
50  esac
51done
52
53if [[ -z "${input}" ]]; then
54  echo "Must specify input file!"
55  usage
56fi
57
58if [[ -z "${output}" ]]; then
59  echo "Must specify output file!"
60  usage
61fi
62
63grub_cmdline="ro net.ifnames=0 console=ttyAMA0 loglevel=4"
64grub_rootfs="LABEL=install"
65
66# Validate format of the input disk
67/sbin/sgdisk -p "${input}" | grep -q "Disk identifier (GUID)" || \
68  ( echo "${input} is not a GUID partitioned disk!" && exit 2 )
69partitions="$(/sbin/sgdisk -p "${input}" | \
70                grep -m1 -A2 "Number  Start (sector)" | tail -n2)"
71( IFS=$'\n'
72for line in $partitions; do
73  IFS=' ' read -r -a partition <<< "$line"
74  if [[ "${partition[0]}" = "1" && "${partition[5]}" != "EF00" ]]; then
75    echo "${input} partition 1 is not an ESP!" && exit 3
76  fi
77  if [[ "${partition[0]}" = "2" && "${partition[6]}" != "rootfs" ]]; then
78    echo "${input} partition 2 is not rootfs!" && exit 4
79  fi
80done )
81
82failure() {
83  echo "ISO generation process failed." >&2
84  rm -f "${output}"
85}
86trap failure ERR
87
88mount=$(mktemp -d)
89mount_remove() {
90  rmdir "${mount}"
91}
92trap mount_remove EXIT
93
94workdir=$(mktemp -d)
95workdir_remove() {
96  rm -rf "${workdir}"
97  mount_remove
98}
99trap workdir_remove EXIT
100
101# Build a grub.cfg for CD booting
102cat >"${workdir}"/grub.cfg <<EOF
103set timeout=0
104menuentry "Linux" {
105  linux /vmlinuz ${grub_cmdline} root=${grub_rootfs}
106  initrd /initrd.img
107}
108EOF
109
110# Build harddisk install script
111cat >"${workdir}"/install.sh << EOF
112#!/bin/sh
113set -e
114set -u
115SCRIPT_DIR=\$(CDPATH= cd -- "\$(dirname -- "\${0}")" && pwd -P)
116if [ "\${1#*nvme}" != "\${1}" ]; then
117  partition=p
118else
119  partition=
120fi
121sgdisk --load-backup="\${SCRIPT_DIR}"/gpt.img \${1}
122sgdisk --delete=2 \${1}
123sgdisk --new=2:129M:0 --typecode=2:8305 --change-name=2:rootfs --attributes=2:set:2 \${1}
124partx -v --update \${1}
125dd if="\${SCRIPT_DIR}"/esp.img of=\${1}\${partition}1 bs=16M
126mkfs.ext4 -L ROOT -U \$(cat \${SCRIPT_DIR}/rootfs_uuid) \${1}\${partition}2
127mount \${1}\${partition}2 /media
128tar -C /media -Spxf \${SCRIPT_DIR}/rootfs.tar.xz
129umount /media
130EOF
131chmod a+x "${workdir}"/install.sh
132
133cat >"${workdir}"/override-getty.conf <<EOF
134[Service]
135ExecStart=
136ExecStart=-/sbin/agetty -a root --noclear tty1 \$TERM
137EOF
138
139cat >"${workdir}"/override-serial-getty.conf <<EOF
140[Service]
141ExecStart=
142ExecStart=-/sbin/agetty -a root --keep-baud 115200,57600,38400,9600 ttyAMA0 \$TERM
143EOF
144
145# Back up the GPT so we can restore it when installing
146/sbin/sgdisk --backup="${workdir}"/gpt.img "${input}" >/dev/null
147
148efi_partition_num=1
149efi_partition_start=$(partx -g -o START -s -n ${efi_partition_num} "${input}" | xargs)
150efi_partition_end=$(partx -g -o END -s -n ${efi_partition_num} "${input}" | xargs)
151efi_partition_offset=$((${efi_partition_start} * 512))
152efi_partition_num_sectors=$((${efi_partition_end} - ${efi_partition_start} + 1))
153
154# Back up the ESP so we can restore it when installing
155touch "${workdir}"/esp.img
156dd if="${input}" of="${workdir}"/esp.img bs=512 skip=${efi_partition_start} count=${efi_partition_num_sectors} status=none >/dev/null
157
158# Determine the architecture of the disk from the portable GRUB image path
159sudo mount -o loop,offset=${efi_partition_offset} "${input}" "${mount}"
160unmount() {
161  sudo umount "${mount}"
162}
163trap unmount EXIT
164grub_blob=$(cd "${mount}" && echo EFI/Boot/*)
165case "${grub_blob}" in
166  EFI/Boot/BOOTAA64.EFI)
167    grub_arch=arm64-efi
168    grub_cd=gcdaa64.efi
169    ;;
170  EFI/Boot/BOOTIA64.EFI)
171    grub_arch=x86_64-efi
172    grub_cd=gcdx64.efi
173    ;;
174  *)
175    echo "Unknown GRUB architecture for ${grub_blob}!"
176    exit 5
177    ;;
178esac
179sudo umount "${mount}"
180
181root_partition_num=2
182root_partition_start=$(partx -g -o START -s -n ${root_partition_num} "${input}" | xargs)
183root_partition_end=$(partx -g -o END -s -n ${root_partition_num} "${input}" | xargs)
184root_partition_offset=$((${root_partition_start} * 512))
185
186# Mount original rootfs and remove previous patching, then tar
187rootfs_uuid=$(/sbin/blkid --probe -s UUID -o value -O ${root_partition_offset} "${input}")
188sudo mount -o loop,offset=${root_partition_offset} "${input}" "${mount}"
189trap unmount EXIT
190sudo rm -f "${mount}"/root/esp.img "${mount}"/root/gpt.img
191sudo rm -f "${mount}"/root/rootfs.tar.xz
192sudo rm -f "${mount}"/root/rootfs_uuid
193sudo rm -f "${mount}"/boot/grub/eltorito.img
194sudo rm -f "${mount}"/boot/grub/${grub_arch}/grub.cfg
195sudo rm -rf "${mount}"/tmp/*
196sudo rm -rf "${mount}"/var/tmp/*
197( cd "${mount}" && sudo tar -SJcpf "${workdir}"/rootfs.tar.xz * )
198
199# Prepare a new ESP for the ISO's El Torito image
200mkdir -p "${workdir}/EFI/Boot"
201cp "${mount}/usr/lib/grub/${grub_arch}/monolithic/${grub_cd}" \
202  "${workdir}/${grub_blob}"
203truncate -s 4M "${workdir}"/eltorito.img
204/sbin/mkfs.msdos -n SYSTEM -F 12 -M 0xf8 -h 0 -s 4 -g 64/32 -S 512 \
205  "${workdir}"/eltorito.img >/dev/null
206mmd -i "${workdir}"/eltorito.img EFI EFI/Boot
207mcopy -o -i "${workdir}"/eltorito.img -s "${workdir}/EFI" ::
208
209# Build ISO from rootfs
210sudo cp "${workdir}"/esp.img "${workdir}"/gpt.img "${mount}"/root
211sudo cp "${workdir}"/rootfs.tar.xz "${workdir}"/install.sh "${mount}"/root
212echo -n "${rootfs_uuid}" | sudo tee "${mount}"/root/rootfs_uuid >/dev/null
213sudo cp "${workdir}"/eltorito.img "${mount}"/boot/grub
214sudo cp "${workdir}"/grub.cfg "${mount}"/boot/grub/${grub_arch}/grub.cfg
215sudo mkdir -p "${mount}"/etc/systemd/system/getty@tty1.service.d
216sudo cp "${workdir}"/override-getty.conf "${mount}"/etc/systemd/system/getty@tty1.service.d/override.conf
217sudo mkdir -p "${mount}"/etc/systemd/system/serial-getty@ttyAMA0.service.d
218sudo cp "${workdir}"/override-serial-getty.conf "${mount}"/etc/systemd/system/serial-getty@ttyAMA0.service.d/override.conf
219sudo chown root:root \
220  "${mount}"/root/esp.img "${mount}"/root/gpt.img \
221  "${mount}"/boot/grub/eltorito.img \
222  "${mount}"/boot/grub/${grub_arch}/grub.cfg
223sudo mv "${mount}"/usr "${mount}"/usr_o
224sudo mkzftree "${mount}"/usr_o "${mount}"/usr
225sudo rm -rf "${mount}"/usr_o
226sudo mv "${mount}"/root "${mount}"/root_o
227sudo mkzftree "${mount}"/root_o "${mount}"/root
228sudo rm -rf "${mount}"/root_o
229sudo mv "${mount}"/var "${mount}"/var_o
230sudo mkzftree "${mount}"/var_o "${mount}"/var
231sudo rm -rf "${mount}"/var_o
232rm -f "${output}"
233touch "${output}"
234sudo xorriso \
235  -as mkisofs -r -checksum_algorithm_iso sha256,sha512 -V install "${mount}" \
236  -o "${output}" -e boot/grub/eltorito.img -no-emul-boot \
237  -append_partition 2 0xef "${workdir}"/eltorito.img \
238  -z \
239  -partition_cyl_align all
240
241echo "Output ISO generated at '${output}'."
242