
In a previous commit [1], the script was changed to use 7z
for extracting contents from the input ISO. This was done
because the main alternative was using 'mount', which
requires sudo privileges.
Unfortunately, 7z does not handle ownership and permissions [2],
and keeping them unchanged is required.
This commit changes the script back to using 'mount'
for extracting input ISO contents. This also means running it
now requires sudo privileges.
Ref:
[1] 4f69113d93
[2] https://linux.die.net/man/1/7z
Test Plan:
pass - Generate a pre-patched ISO and check that the permissions
on the output iso are the same as the ones in the input.
Closes-Bug: 2098385
Change-Id: I1e5e1f8ecdb94cafb577dcfe3651a1717abe19c7
Signed-off-by: Leonardo Fagundes Luz Serrano <Leonardo.FagundesLuzSerrano@windriver.com>
358 lines
10 KiB
Bash
Executable File
358 lines
10 KiB
Bash
Executable File
#!/bin/bash -e
|
|
#
|
|
# Copyright (c) 2025 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 <input filename.iso> -o <output filename.iso> [ -p ] <patch> ...
|
|
-i <file>: Specify input ISO file
|
|
-o <file>: Specify output ISO file
|
|
-p <file>: 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.
|
|
- Requires sudo privileges for mounting the input ISO
|
|
|
|
"
|
|
|
|
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=(
|
|
mkisofs__xorrisofs # genisoimage / xorriso
|
|
isohybrid # syslinux-utils
|
|
implantisomd5 # isomd5sum
|
|
ostree # ostree
|
|
rsync # rsync
|
|
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
|
|
|
|
# Create temporary mount directory
|
|
MNTDIR=$(mktemp -d -p "$PWD" patchiso_mount_XXXXXX)
|
|
if [ -z "${MNTDIR}" ] || [ ! -d "${MNTDIR}" ]; then
|
|
echo "ERROR: Failed to create temporary mount directory"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Mounting input ISO..."
|
|
if ! mount -o loop ${INPUT_ISO} ${MNTDIR} ; then
|
|
echo "ERROR: Failed to mount input ISO. Are you root?"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Extracting Input ISO contents (except ostree repo)..."
|
|
if ! rsync -a --exclude 'ostree_repo' ${MNTDIR}/ ${BUILDDIR}/ ; then
|
|
echo "ERROR: Failed to rsync content from mount dir to build dir"
|
|
umount ${MNTDIR}
|
|
exit 1
|
|
fi
|
|
|
|
umount ${MNTDIR}
|
|
|
|
# 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:"
|
|
ls -lah "${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}")"
|