#!/usr/bin/env bash # Build a bootc OCI image from an image definition under images/. # # Usage: build.sh [arch] # # The per-image build.conf selects the build mode: # # upstream Clone the upstream Fedora manifest repo at a ref mapped from # , overlay this image's manifests and support files, then # compose the OCI image with `rpm-ostree compose image`. # # standalone Compose from this image's own manifest.yaml, wrapping it with the # releasever/ref from build.conf. set -euo pipefail if [[ $# -lt 2 ]]; then echo "Usage: $0 [arch]" >&2 exit 1 fi IMAGE="$1" DISTRO="$2" ARCH="${3:-$(uname -m)}" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IMAGE_DIR="${REPO_ROOT}/images/${IMAGE}" CONF="${IMAGE_DIR}/build.conf" [[ -d "${IMAGE_DIR}" ]] || { echo "Unknown image: ${IMAGE}" >&2; exit 1; } [[ -f "${CONF}" ]] || { echo "Missing build config: ${CONF}" >&2; exit 1; } # Defaults, overridden by build.conf. BUILD_MODE="" UPSTREAM_REPO="" TOKEN2=0 RUN_TEST_SH=0 RELEASEVER="" REF="" declare -A UPSTREAM_REFS=() # shellcheck disable=SC1090 source "${CONF}" BUILD_DIR="${REPO_ROOT}/build/${IMAGE}-${DISTRO}-${ARCH}" OCI_ARCHIVE="${BUILD_DIR}/${IMAGE}-${DISTRO}-${ARCH}.ociarchive" CACHE_DIR="${REPO_ROOT}/cache" [[ -n "${BUILD_MODE}" ]] || { echo "BUILD_MODE not set in ${CONF}" >&2; exit 1; } # Build IDs must be unique per build (release tags are derived from them), so # include the CI run number when available, otherwise a timestamp. if [[ -n "${GITHUB_RUN_NUMBER:-}" ]]; then buildid="$(date '+%Y%m%d').${GITHUB_RUN_NUMBER}" else buildid="$(date '+%Y%m%d.%H%M%S')" fi echo "${buildid}" > "${REPO_ROOT}/.buildid" rm -rf "${BUILD_DIR}" mkdir -p "${BUILD_DIR}" "${CACHE_DIR}" if [[ "${RUN_TEST_SH}" == "1" && -x "${IMAGE_DIR}/test.sh" ]]; then echo "Running ${IMAGE_DIR}/test.sh" ( cd "${IMAGE_DIR}" && ./test.sh ) fi # Download the latest token2-fido-bridge RPM from GitHub and turn it into a # local yum repo so the manifest can install it. setup_token2_repo() { local dest="$1" local api url api="https://api.github.com/repos/token2/token2-fido-bridge/releases/latest" url="$(curl -fsSL "${api}" \ | jq -r '.assets[] | select(.name | endswith(".rpm")) | .browser_download_url' \ | head -n1)" if [[ -z "${url}" || "${url}" == "null" ]]; then echo "ERROR: no token2-fido-bridge .rpm found in the latest GitHub release" >&2 exit 1 fi mkdir -p "${dest}/token2-repo" curl -fsSL "${url}" -o "${dest}/token2-repo/$(basename "${url}")" createrepo_c "${dest}/token2-repo" cat > "${dest}/token2-fido-bridge.repo" < "${f}.tmp" # Drop top-level keys left with no list items or nested keys (e.g. if every # entry under packages-x86_64: was removed). Comments and blank lines do not # count as content, so a key followed only by comments is still dropped. awk ' { lines[NR] = $0 } END { for (i = 1; i <= NR; i++) { if (lines[i] ~ /^[A-Za-z0-9_.-]+:$/) { keep = 0 for (j = i + 1; j <= NR; j++) { if (lines[j] ~ /^[A-Za-z0-9_.-]+:/) break if (lines[j] ~ /^[[:space:]]+-[[:space:]]/) { keep = 1; break } if (lines[j] ~ /^[[:space:]]+[A-Za-z0-9_.-]+:/) { keep = 1; break } } if (!keep) continue } print lines[i] } } ' "${f}.tmp" > "${f}.tmp2" mv "${f}.tmp2" "${f}" rm -f "${f}.tmp" done } # Generate an exclude-packages snippet from the same blocklist. This blocks the # removed packages from being pulled back in as recommends (e.g. linux-firmware # recommends the split firmware packages). generate_exclude_yaml() { local blocklist="$1" local out="$2" { echo "# Generated by build.sh from $(basename "${blocklist}") - do not edit." echo "exclude-packages:" grep -vE '^[[:space:]]*(#|$)' "${blocklist}" | sed 's/^/ - /' } > "${out}" } # Seed the Kanidm unixd service account token from the KANIDM_UNIXD_TOKEN # environment variable (a CI secret). The token is added to the image, so treat # the image as sensitive: anyone who can pull it can read the token. seed_kanidm_token() { local dir="$1" local out="${dir}/kanidm-token.yaml" if [[ -n "${KANIDM_UNIXD_TOKEN:-}" ]]; then printf '%s\n' "${KANIDM_UNIXD_TOKEN}" > "${dir}/kanidm-unixd-token" chmod 0600 "${dir}/kanidm-unixd-token" # kanidm-unixd is DynamicUser=yes, so it cannot read the root-only token # file directly; hand it over as a systemd credential instead. cat > "${dir}/kanidm-unixd-token.conf" <<'EOF' [Service] LoadCredential=unixd_token:/etc/kanidm/unixd_token Environment=KANIDM_SERVICE_ACCOUNT_TOKEN_PATH=%d/unixd_token EOF cat > "${out}" <<'EOF' # Generated by build.sh from the KANIDM_UNIXD_TOKEN secret. add-files: - ["kanidm-unixd-token", "/etc/kanidm/unixd_token"] - ["kanidm-unixd-token.conf", "/usr/lib/systemd/system/kanidm-unixd.service.d/10-token.conf"] EOF else cat > "${out}" <<'EOF' # Generated by build.sh: no Kanidm token seeded (KANIDM_UNIXD_TOKEN unset). add-files: [] EOF fi } case "${BUILD_MODE}" in upstream) [[ -n "${UPSTREAM_REPO}" ]] || { echo "UPSTREAM_REPO not set in ${CONF}" >&2; exit 1; } upstream_ref="${UPSTREAM_REFS[${DISTRO}]:-}" [[ -n "${upstream_ref}" ]] || { echo "No upstream ref configured for distro '${DISTRO}'" >&2; exit 1; } git clone --depth 1 --branch "${upstream_ref}" "${UPSTREAM_REPO}" "${BUILD_DIR}/upstream" # Overlay our manifests/support files into the upstream tree so that # relative includes and *.repo discovery keep working. cp "${IMAGE_DIR}/manifest.yaml" "${BUILD_DIR}/upstream/" cp "${IMAGE_DIR}/custom.yaml" "${BUILD_DIR}/upstream/" if compgen -G "${IMAGE_DIR}/repos/*.repo" >/dev/null; then cp "${IMAGE_DIR}"/repos/*.repo "${BUILD_DIR}/upstream/" fi if compgen -G "${IMAGE_DIR}/repos/${DISTRO}/*.repo" >/dev/null; then cp "${IMAGE_DIR}"/repos/"${DISTRO}"/*.repo "${BUILD_DIR}/upstream/" fi if [[ -d "${IMAGE_DIR}/files" ]]; then cp -a "${IMAGE_DIR}/files/." "${BUILD_DIR}/upstream/" fi # Shared overlay (files/repos used by multiple images). if [[ -d "${REPO_ROOT}/images/shared/files" ]]; then cp -a "${REPO_ROOT}/images/shared/files/." "${BUILD_DIR}/upstream/" fi if compgen -G "${REPO_ROOT}/images/shared/repos/*.repo" >/dev/null; then cp "${REPO_ROOT}"/images/shared/repos/*.repo "${BUILD_DIR}/upstream/" fi if compgen -G "${REPO_ROOT}/images/shared/repos/${DISTRO}/*.repo" >/dev/null; then cp "${REPO_ROOT}"/images/shared/repos/"${DISTRO}"/*.repo "${BUILD_DIR}/upstream/" fi if [[ "${TOKEN2}" == "1" ]]; then setup_token2_repo "${BUILD_DIR}/upstream" fi if [[ -f "${IMAGE_DIR}/upstream-exclude.txt" ]]; then trim_upstream_packages "${BUILD_DIR}/upstream" "${IMAGE_DIR}/upstream-exclude.txt" generate_exclude_yaml "${IMAGE_DIR}/upstream-exclude.txt" \ "${BUILD_DIR}/upstream/hardware-exclude.yaml" fi seed_kanidm_token "${BUILD_DIR}/upstream" MANIFEST="${BUILD_DIR}/upstream/manifest.yaml" ;; standalone) [[ -n "${RELEASEVER}" ]] || { echo "RELEASEVER not set in ${CONF}" >&2; exit 1; } [[ -n "${REF}" ]] || { echo "REF not set in ${CONF}" >&2; exit 1; } # Build from a copy so we never mutate the checked-in image definition. mkdir -p "${BUILD_DIR}/images" cp -a "${IMAGE_DIR}" "${BUILD_DIR}/images/${IMAGE}" local_dir="${BUILD_DIR}/images/${IMAGE}" if compgen -G "${local_dir}/repos/*.repo" >/dev/null; then cp "${local_dir}"/repos/*.repo "${local_dir}/" fi if compgen -G "${local_dir}/repos/${DISTRO}/*.repo" >/dev/null; then cp "${local_dir}"/repos/"${DISTRO}"/*.repo "${local_dir}/" fi if [[ -d "${REPO_ROOT}/images/shared/files" ]]; then cp -a "${REPO_ROOT}/images/shared/files/." "${local_dir}/" fi if [[ "${TOKEN2}" == "1" ]]; then setup_token2_repo "${local_dir}" fi cat > "${local_dir}/.build-manifest.yaml" <&2 exit 1 ;; esac echo "Composing ${IMAGE} ${DISTRO} (${ARCH}) build ${buildid} ..." RPM_OSTREE=(rpm-ostree) if [[ "${EUID}" -ne 0 ]]; then RPM_OSTREE=(sudo -E rpm-ostree) fi "${RPM_OSTREE[@]}" compose image \ --cachedir="${CACHE_DIR}" \ --initialize \ --max-layers=256 \ "${MANIFEST}" \ "${OCI_ARCHIVE}" # The finalize.d hook records the installed package list next to the treefile. manifest_dir="$(dirname "${MANIFEST}")" if [[ -f "${manifest_dir}/packages-current.txt" ]]; then cp "${manifest_dir}/packages-current.txt" "${BUILD_DIR}/packages-current.txt" echo "Package list: ${BUILD_DIR}/packages-current.txt" fi echo "Built: ${OCI_ARCHIVE}"