#!/bin/bash -e
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# Script to generate pre-patched ISOs.
#
BUILD_TOOLS_DIR="$(dirname "$0")"
# shellcheck source="./build-tools/image-utils.sh"
source "${BUILD_TOOLS_DIR}/image-utils.sh"
usage="
Script to generate pre-patched ISOs.
Inputs:
- an ISO
- one or more patches
- ostree repo (assumed to be in \${DEPLOY_DIR}/ostree_repo/
or \${STX_BUILD_HOME}/localdisk/deploy/ostree_repo/)
It generates as output an ISO with the following changes:
- Contains only the latest ostree commit from the input ostree repo
- ISO has a 'patches' folder with the patches' metadata files.
This folder is processed by kickstart during install, so that
'sw-patch query' has access to this info to list the patches
(each of them refers to one of the older commits in the ostree repo)
The intent is for the system to have record of the patches that are
already pre-installed in the system.
Usage:
$(basename "$0") -i -o [ -p ] ...
-i : Specify input ISO file
-o : Specify output ISO file
-p : Patch files. Can be called multiple times.
Attention:
- Either the DEPLOY_DIR or the STX_BUILD_HOME env variable must be defined.
It's used to find the input ostree repo.
"
function usage() {
echo "${usage}"
}
function extract_ostree_commit_from_metadata_xml() {
local XML_PATH=$1
local XPATH="//contents/ostree/commit1/commit"
# Check if xmllint is available. Otherwise, use python's xml standard lib
if (which xmllint &>/dev/null); then
xmllint --xpath "string(${XPATH})" "${XML_PATH}"
else
python3 -c "import xml.etree.ElementTree as ET ; print(ET.parse('${XML_PATH}').find('.${XPATH}').text, end='')"
fi
}
function extract_metadata() {
local patchesdir
local patchfile
local patchid
local ostree_log
local ostree_commit
patchesdir="${BUILDDIR}/patches"
patchfile="$1"
patchid="$(basename "$patchfile" .patch)"
ostree_log="$(ostree --repo="${2}" log starlingx)"
echo "Extracting ${patchfile}"
# Extract it
if ! tar xf "${patchfile}" -O metadata.tar | tar x -O > "${patchesdir}/${patchid}-metadata.xml"; then
echo "ERROR: Failed to extract metadata from ${patchfile}"
exit 1
fi
# Verify if top commit from metadata exist in ostree log
patch_ostree_commit1="$(extract_ostree_commit_from_metadata_xml "${patchesdir}/${patchid}-metadata.xml")"
if [[ "$ostree_log" != *"$patch_ostree_commit1"* ]]; then
echo "WARNING: Patch ostree commit 1 not found in input ISO."
echo "Patch ostree commit 1: ${patch_ostree_commit1}"
fi
}
declare INPUT_ISO=
declare OUTPUT_ISO=
declare BUILDDIR=
while getopts "i:o:p:" opt; do
case $opt in
i)
INPUT_ISO=$OPTARG
;;
o)
OUTPUT_ISO=$OPTARG
;;
p)
PATCH_FILES+=("$OPTARG")
;;
*)
usage
exit 1
;;
esac
done
if [ -z "$INPUT_ISO" ] || [ -z "$OUTPUT_ISO" ]; then
usage
exit 1
fi
if [ ! -f "${INPUT_ISO}" ]; then
echo "ERROR: Input file does not exist: ${INPUT_ISO}"
exit 1
fi
if [ -f "${OUTPUT_ISO}" ]; then
echo "ERROR: Output file already exists: ${OUTPUT_ISO}"
exit 1
fi
for PATCH in "${PATCH_FILES[@]}";
do
if [ ! -f "${PATCH}" ]; then
echo "ERROR: Patch file dos not exists: ${PATCH}"
exit 1
fi
if [[ ! "${PATCH}" =~ \.patch$ ]]; then
echo "ERROR: Specified file ${PATCH} does not have .patch extension"
exit 1
fi
done
shift $((OPTIND-1))
function check_requirements {
# Next to each requirement is the deb package which provides the command listed.
# Run "apt install ..."
# Declare "require reqA or reqB" as "reqA__reqB"
local -a required_utils=(
7z # p7zip-full
mkisofs__xorrisofs # genisoimage / xorriso
isohybrid # syslinux-utils
implantisomd5 # isomd5sum
ostree # ostree
xmllint__python3 # libxml2-utils / python3
)
local -i missing=0
local reqA
local reqB
for req in "${required_utils[@]}"; do
if [[ "$req" = *"__"* ]]; then
reqA="${req%__*}" # select everything before "__"
reqB="${req#*__}" # select everything after "__"
if ! (which "${reqA}" &>/dev/null) && ! (which "${reqB}" &>/dev/null); then
echo "Unable to find required utility: either ${reqA} or ${reqB}" >&2
missing=$(( missing+1 ))
fi
else
if ! (which "${req}" &>/dev/null); then
echo "Unable to find required utility: ${req}" >&2
missing=$(( missing+1 ))
fi
fi
done
if [ "${missing}" -gt 0 ]; then
echo "ERROR: One or more required utilities are missing" >&2
exit 1
fi
}
function cleanup() {
# Delete temporary build directory
if [ -n "$BUILDDIR" ] && [ -d "$BUILDDIR" ]; then
chmod -R +w "$BUILDDIR"
\rm -rf "$BUILDDIR"
fi
}
check_requirements
# Run cleanup() when finishing/interrupting execution
trap cleanup EXIT
# Define MY_REPO, which is the path to the 'root' repo. Eg.: $REPO_ROOT/cgcs_root
# Value is used to locate the following file for ISO signing:
# ${MY_REPO}/build-tools/signing/dev-private-key.pem
if [ -z "${MY_REPO}" ]; then
MY_REPO="$(dirname "${BUILD_TOOLS_DIR}")"
fi
# Define DEPLOY_DIR, which is the directory containing the input ostree repo
if [ -z "${DEPLOY_DIR}" ]; then
if [ -n "${STX_BUILD_HOME}" ]; then
DEPLOY_DIR="${STX_BUILD_HOME}/localdisk/deploy"
else
echo "ERROR: Please define either the DEPLOY_DIR or the STX_BUILD_HOME env variables."
exit 1
fi
fi
# Create temporary build directory
BUILDDIR=$(mktemp -d -p "$PWD" patchiso_build_XXXXXX)
if [ -z "${BUILDDIR}" ] || [ ! -d "${BUILDDIR}" ]; then
echo "ERROR: Failed to create temporary build directory"
exit 1
fi
echo "Extracting Input ISO contents (except ostree repo)..."
if ! 7z x "${INPUT_ISO}" -o"${BUILDDIR}" -x\!ostree_repo 1>/dev/null ; then
echo "ERROR: Failed to extract ISO contents"
exit 1
fi
# Deleting '[BOOT]' directory. It will be re-created when packing the output ISO.
if [ -d "${BUILDDIR}/[BOOT]" ]; then
rm -rf "${BUILDDIR}/[BOOT]"
fi
# Fix for permission denied if not running as root
chmod +w "${BUILDDIR}"
if [ -d "${BUILDDIR}/isolinux" ]; then
chmod -R +w "${BUILDDIR}/isolinux"
fi
# Erase current patch metadata from ISO if it exists
# This way, this script can be used on pre-patched ISOs
if [ -d "${BUILDDIR}/patches" ]; then
rm -rf "${BUILDDIR}/patches"
fi
echo "List contents extracted from Input ISO (after adjustments):"
ls -lh "${BUILDDIR}"
# Create the directory where patch metadata will be stored
mkdir -p "${BUILDDIR}/patches"
chmod -R +w "${BUILDDIR}/patches"
echo "Create a copy of the input ostree repo in the temp build directory..."
echo "Input ostree repo: ${DEPLOY_DIR}/ostree_repo/"
ostree --repo="${BUILDDIR}/ostree_repo" init --mode=archive-z2
ostree --repo="${BUILDDIR}/ostree_repo" pull-local --depth=-1 "${DEPLOY_DIR}/ostree_repo/" starlingx
ostree --repo="${BUILDDIR}/ostree_repo" summary --update
echo "Extracting patch metadata..."
for PATCH in "${PATCH_FILES[@]}";
do
extract_metadata "$PATCH" "${BUILDDIR}/ostree_repo"
done
echo "Original ostree repo history:"
echo "----------------------------------------------------------------------------------"
ostree --repo="${BUILDDIR}/ostree_repo" log starlingx
echo "----------------------------------------------------------------------------------"
echo "Clean up all commits from ostree repo except the latest one..."
function clean_ostree(){
# Create array of ostree commit IDs
mapfile -t ostree_commits < <(ostree --repo="${BUILDDIR}/ostree_repo" log starlingx | grep '^commit' | cut -d ' ' -f 2)
# Delete each commit except the latest one
for ostree_commit in "${ostree_commits[@]:1}"; do
echo "Removing commit: ${ostree_commit}"
ostree --repo="${BUILDDIR}/ostree_repo" prune --delete-commit="${ostree_commit}"
done
ostree --repo="${BUILDDIR}/ostree_repo" summary --update
}
if ! clean_ostree; then
echo "ERROR: Failed to clean ostree repo!"
exit 1
fi
echo "Output ISO ostree history:"
echo "----------------------------------------------------------------------------------"
ostree --repo="${BUILDDIR}/ostree_repo" log starlingx
echo "----------------------------------------------------------------------------------"
echo "Packing iso..."
# get the install label
ISO_LABEL=$(grep -ri instiso "${BUILDDIR}"/isolinux/isolinux.cfg | head -1 | xargs -n1 | awk -F= /instiso/'{print $2}')
if [ -z "${ISO_LABEL}" ] ; then
echo "Error: Failed to get iso install label"
fi
echo "ISO Label: ${ISO_LABEL}"
function pack_iso(){
if (which xorrisofs &>/dev/null); then
PACK_ISO_CMD="xorrisofs"
else
PACK_ISO_CMD="mkisofs"
fi
echo "ISO packaging command: ${PACK_ISO_CMD}"
# Command Reference:
# https://github.com/yoctoproject/poky/blob/master/scripts/lib/wic/plugins/source/isoimage-isohybrid.py#L419
${PACK_ISO_CMD} \
-V "${ISO_LABEL}" \
-o "${OUTPUT_ISO}" -U \
-J -joliet-long -r -iso-level 2 -b "isolinux/isolinux.bin" \
-c "isolinux/boot.cat" -no-emul-boot -boot-load-size 4 \
-boot-info-table -eltorito-alt-boot \
-eltorito-platform "0xEF" -eltorito-boot "efi.img" \
-no-emul-boot "${BUILDDIR}"
isohybrid --uefi "${OUTPUT_ISO}"
implantisomd5 "${OUTPUT_ISO}"
}
if ! pack_iso; then
if [ "${PACK_ISO_CMD}" = "mkisofs" ]; then
echo "NOTE: mkisofs has a customization in the LAT container to provide the '-eltorito-boot' flag."
echo " To execute this script outside the LAT container, install the 'xorriso' package and run again."
fi
echo "ERROR: Failed to build output ISO!"
exit 1
fi
echo "Signing the .iso with the developer private key..."
function sign_iso(){
openssl dgst -sha256 \
-sign "${MY_REPO}/build-tools/signing/dev-private-key.pem" \
-binary \
-out "${OUTPUT_ISO/%.iso/.sig}" \
"${OUTPUT_ISO}"
}
if ! sign_iso; then
echo "ERROR: Failed to sign ISO!"
exit 1
fi
echo ""
echo "Output ISO: $(realpath "${OUTPUT_ISO}")"