#!/bin/bash -e # # Copyright (c) 2024 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # # This script takes as input an ISO and one or more patches # and generates as output an ISO with the following changes: # # - Contains only the latest ostree commit from the input ISO # - 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. # # The intent is for the system to have record of the patches that are # already pre-installed in the system. # BUILD_TOOLS_DIR="$(dirname "$0")" # shellcheck source="./build-tools/image-utils.sh" source "${BUILD_TOOLS_DIR}/image-utils.sh" # 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 function usage() { echo "" echo "Usage: " echo " $(basename "$0") -i -o [ -p ] ..." echo " -i : Specify input ISO file" echo " -o : Specify output ISO file" echo " -p : Patch files. You can call it multiple times." echo "" } 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= 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)) declare BUILDDIR= 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 # 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 # 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 # Create the directory where patch metadata will be stored mkdir -p "${BUILDDIR}/patches" chmod -R +w "${BUILDDIR}/patches" echo "Extracting Input ISO contents..." if ! 7z x "${INPUT_ISO}" -o"${BUILDDIR}" 1>/dev/null ; then echo "ERROR: Extract ISO contents" exit 1 fi # Delete boot directory. It will be re-created when packing the output ISO if [ -d "${BUILDDIR}/[BOOT]" ]; then rm -rf "${BUILDDIR}/[BOOT]" fi 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}")"