#!/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}")"