From c32e1081c81ba27f0d5a21a1885601f04d329d21 Mon Sep 17 00:00:00 2001 From: Bruce Ashfield Date: Thu, 1 Jan 2026 17:08:35 +0000 Subject: container-cross-install: add bbclass for bundling containers into images This class enables bundling pre-built OCI containers into Yocto images at build time. It uses vdkr/vpdmn to process containers via QEMU, producing properly formatted storage that can be merged into the target rootfs. Two mechanisms for bundling containers: 1. BUNDLED_CONTAINERS variable (direct specification): - Set in local.conf or image recipe - Format: "name:runtime[:autostart][:external]" - Dependencies auto-generated at parse time - Example: BUNDLED_CONTAINERS = "container-base:docker:autostart" 2. container-bundle packages (package-based): - Recipes inherit container-bundle.bbclass to create packages - Installing packages via IMAGE_INSTALL triggers processing - merge_installed_bundles() scans installed OCI directories - Runs vrunner once in batch-import mode for efficiency Automatic dependency generation for BUNDLED_CONTAINERS: - Parses entries at recipe parse time via python __anonymous() - Derives recipe name from OCI dir names (strips -latest-oci/-oci suffix) - Generates do_rootfs[depends] on recipe:do_image_complete - Use :external tag to skip dependency for third-party blobs - Single "bitbake container-image-host" builds containers + image Usage in image recipe: inherit container-cross-install Features: - Automatic detection of target architecture - Support for both Docker and Podman runtimes - Batch import mode for efficiency with multiple containers - Autostart support via systemd services (Docker) or Quadlet (Podman) - Integration with vrunner for QEMU-based processing - Backwards compatible with legacy -latest-oci naming convention The class processes containers during do_rootfs and merges the resulting storage into /var/lib/docker or /var/lib/containers. Signed-off-by: Bruce Ashfield --- classes/container-cross-install.bbclass | 911 ++++++++++++++++++++++++++++++++ 1 file changed, 911 insertions(+) create mode 100644 classes/container-cross-install.bbclass (limited to 'classes/container-cross-install.bbclass') diff --git a/classes/container-cross-install.bbclass b/classes/container-cross-install.bbclass new file mode 100644 index 00000000..d6a38dad --- /dev/null +++ b/classes/container-cross-install.bbclass @@ -0,0 +1,911 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield +# +# SPDX-License-Identifier: MIT +# +# container-cross-install.bbclass +# =========================================================================== +# Cross-architecture container deployment class +# =========================================================================== +# +# This class enables bundling containers into target images during build time. +# It uses QEMU with a pre-built initramfs to process containers built for +# different architectures (cross-compilation safe). +# +# Usage (in image recipe, e.g., core-image-minimal.bbappend): +# inherit container-cross-install +# +# Configuration (in local.conf or image recipe): +# BUNDLED_CONTAINERS = "container-base:docker myapp:podman:autostart" +# +# Container format: name:runtime[:autostart][:external] +# - name: Container recipe name OR OCI directory name in DEPLOY_DIR_IMAGE +# - runtime: docker or podman +# - autostart: Optional. Creates systemd service to start on boot: +# * autostart - alias for unless-stopped (recommended) +# * always - always restart container +# * unless-stopped - restart unless manually stopped +# * on-failure - restart only on non-zero exit code +# - external: Optional tag for third-party containers (no dependency generated) +# +# Automatic dependency generation: +# - Dependencies on container recipes are auto-generated at parse time +# - If name ends in -oci, recipe name is derived (strip -latest-oci or -oci) +# - Use :external tag to skip dependency for third-party blobs +# +# OCI directory resolution: +# - If name ends in -oci, use directly from DEPLOY_DIR_IMAGE +# - Otherwise, search: name-latest-oci -> name-oci (fallback) +# +# Examples: +# BUNDLED_CONTAINERS = "container-base:docker" # auto-dep, no autostart +# BUNDLED_CONTAINERS = "myapp:podman:autostart" # auto-dep, autostart +# BUNDLED_CONTAINERS = "vendor-blob:docker:external" # no dep, third-party +# BUNDLED_CONTAINERS = "vendor-blob:docker:autostart:external" # no dep, autostart +# # Legacy format still supported: +# BUNDLED_CONTAINERS = "container-base-latest-oci:docker" # auto-dep (derived) +# +# Generated autostart files: +# Docker: /etc/systemd/system/container-.service (enabled) +# Podman: /etc/containers/systemd/.container (Quadlet format) +# +# The class uses vdkr/vpdmn initramfs blobs built via multiconfig (mcdepends). +# These contain Docker or Podman tools respectively and are built by +# vdkr-initramfs-create and vpdmn-initramfs-create recipes. +# +# =========================================================================== +# Choosing Between BUNDLED_CONTAINERS and container-bundle Packages +# =========================================================================== +# +# There are two ways to bundle containers into a host image: +# +# 1. BUNDLED_CONTAINERS variable (this class, simpler) +# Set in local.conf or image recipe - no extra recipe needed +# +# 2. container-bundle packages +# Create a bundle recipe that inherits container-bundle.bbclass, +# then add the package to IMAGE_INSTALL +# +# Decision guide: +# +# Use Case | BUNDLED_CONTAINERS | Bundle Recipe +# --------------------------------------------|--------------------|-------------- +# Simple: containers in one host image | recommended | overkill +# Reuse containers across multiple images | repetitive | recommended +# Remote containers (docker.io/library/...) | not supported | required +# Package versioning and dependencies | not supported | supported +# Distribute pre-built container set | not supported | supported +# +# For most single-image use cases, BUNDLED_CONTAINERS is simpler: +# - No bundle recipe needed +# - Dependencies auto-generated at parse time +# - vrunner batch-import runs once for all containers +# +# Use container-bundle.bbclass when you need: +# - Remote container fetching via skopeo +# - A distributable/versioned package of containers +# - To share the same bundle across multiple different host images +# +# =========================================================================== +# Integration with container-bundle.bbclass +# =========================================================================== +# +# This class also processes packages created by container-bundle.bbclass: +# 1. merge_installed_bundles() runs as ROOTFS_POSTPROCESS_COMMAND +# 2. Scans ${datadir}/container-bundles/{docker,podman}/oci/ and *.refs files +# 3. Runs vrunner --batch-import once to create storage, extracts to rootfs +# 4. Reads *.meta files for autostart service generation +# +# The runtime is determined by the subdirectory (docker/ vs podman/), +# which is set by container-bundle.bbclass based on CONTAINER_BUNDLE_RUNTIME. +# +# See also: container-bundle.bbclass + +# Dependencies on native tools +# vcontainer-native provides vrunner.sh +# Blobs come from multiconfig builds (vdkr-initramfs-create, vpdmn-initramfs-create) +DEPENDS += "qemuwrapper-cross qemu-system-native skopeo-native" +DEPENDS += "vcontainer-native coreutils-native" + +# Determine multiconfig name for blob building based on target architecture +def get_vruntime_multiconfig(d): + arch = d.getVar('TARGET_ARCH') + if arch == 'aarch64': + return 'vruntime-aarch64' + elif arch in ['x86_64', 'i686', 'i586']: + return 'vruntime-x86-64' + else: + return None + +# Get the MACHINE name used in the multiconfig (for deploy path) +def get_vruntime_machine(d): + arch = d.getVar('TARGET_ARCH') + if arch == 'aarch64': + return 'qemuarm64' + elif arch in ['x86_64', 'i686', 'i586']: + return 'qemux86-64' + else: + return None + +VRUNTIME_MULTICONFIG = "${@get_vruntime_multiconfig(d)}" +VRUNTIME_MACHINE = "${@get_vruntime_machine(d)}" + +# Use mcdepends to automatically build vdkr/vpdmn blobs via multiconfig +# This ensures blobs are built as part of the normal Yocto build flow +# Requires BBMULTICONFIG = "vruntime-aarch64 vruntime-x86-64" in local.conf +do_rootfs[mcdepends] = "mc::${VRUNTIME_MULTICONFIG}:vdkr-initramfs-create:do_deploy mc::${VRUNTIME_MULTICONFIG}:vpdmn-initramfs-create:do_deploy" + +# Generate dependencies for BUNDLED_CONTAINERS at parse time +# Format: name:runtime[:autostart][:external] +# - If :external present, no dependency generated (third-party blob) +# - If name ends in -oci, derive recipe name and generate dependency +# - Otherwise, generate dependency on name:do_image_complete +python __anonymous() { + bundled = (d.getVar('BUNDLED_CONTAINERS') or "").split() + if not bundled: + return + + deps = "" + for entry in bundled: + parts = entry.split(':') + container_name = parts[0] + + # Check for :external tag (can be in position 3 or 4) + is_external = 'external' in parts + + # Skip dependency for external containers + if is_external: + continue + + # Derive recipe name from OCI dir name if needed + recipe_name = container_name + if container_name.endswith('-latest-oci'): + recipe_name = container_name[:-11] # strip -latest-oci + elif container_name.endswith('-oci'): + recipe_name = container_name[:-4] # strip -oci + + # Generate dependency + deps += f" {recipe_name}:do_image_complete" + + if deps: + d.appendVarFlag('do_rootfs', 'depends', deps) +} + +# Path to vrunner.sh from vcontainer-native +VRUNNER_PATH = "${STAGING_BINDIR_NATIVE}/vrunner.sh" + +# Blobs come from multiconfig's DEPLOY_DIR (built by mcdepends on vdkr-initramfs-create) +# Multiconfig uses separate TMPDIR, so deploy path is: +# ${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/ +# Blobs are in runtime/arch subdirectories: ${BLOB_DIR}/${ARCH}/ (e.g., x86_64/, aarch64/) +VDKR_BLOB_DIR = "${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/vdkr" +VPDMN_BLOB_DIR = "${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/vpdmn" + +bundle_containers[network] = "1" +do_testsdkext[nostamp] = "1" + +# Map TARGET_ARCH to QEMU architecture names +def get_qemu_arch(d): + """Map Yocto TARGET_ARCH to QEMU architecture name""" + arch = d.getVar('TARGET_ARCH') + arch_map = { + 'aarch64': 'aarch64', + 'arm': 'arm', + 'x86_64': 'x86_64', + 'i686': 'i386', + 'i586': 'i386', + } + return arch_map.get(arch, arch) + +QEMU_ARCH = "${@get_qemu_arch(d)}" + +# Map TARGET_ARCH to kernel image name +def get_kernel_name(d): + """Map Yocto TARGET_ARCH to kernel image filename""" + arch = d.getVar('TARGET_ARCH') + kernel_map = { + 'aarch64': 'Image', + 'arm': 'zImage', + 'x86_64': 'bzImage', + 'i686': 'bzImage', + 'i586': 'bzImage', + } + return kernel_map.get(arch, 'Image') + +KERNEL_IMAGETYPE_QEMU = "${@get_kernel_name(d)}" + +# Map TARGET_ARCH to blob directory name (aarch64, x86_64) +def get_blob_arch(d): + """Map Yocto TARGET_ARCH to blob directory name""" + arch = d.getVar('TARGET_ARCH') + blob_map = { + 'aarch64': 'aarch64', + 'arm': 'aarch64', # Use aarch64 blobs for 32-bit ARM too + 'x86_64': 'x86_64', + 'i686': 'x86_64', + 'i586': 'x86_64', + } + return blob_map.get(arch, 'aarch64') + +BLOB_ARCH = "${@get_blob_arch(d)}" + +# ============================================================================ +# Merge container bundles installed via IMAGE_INSTALL +# This function processes packages created by container-bundle.bbclass +# ============================================================================ +merge_installed_bundles() { + # Disable errexit - we handle errors explicitly + set +e + + BUNDLES_DIR="${IMAGE_ROOTFS}${datadir}/container-bundles" + + if [ ! -d "${BUNDLES_DIR}" ]; then + bbnote "No container bundles found in ${BUNDLES_DIR}" + return 0 + fi + + bbnote "Processing installed container bundles from ${BUNDLES_DIR}" + + # Collect all OCI directories from bundle packages + # Bundle packages now contain OCI directories, not storage tars + # We run vrunner ONCE with all containers to create a single storage tar + + local docker_containers="" + local podman_containers="" + + # Collect Docker OCI directories and refs + if [ -d "${BUNDLES_DIR}/docker/oci" ]; then + for refs_file in ${BUNDLES_DIR}/docker/*.refs; do + [ -f "$refs_file" ] || continue + while IFS=: read -r oci_name image_ref; do + [ -z "$oci_name" ] && continue + local oci_path="${BUNDLES_DIR}/docker/oci/${oci_name}" + if [ -d "$oci_path" ]; then + # Format for vrunner batch-import: path:image:tag + docker_containers="${docker_containers} ${oci_path}:${image_ref}" + bbnote "Docker container: ${oci_path} -> ${image_ref}" + fi + done < "$refs_file" + done + fi + + # Collect Podman OCI directories and refs + if [ -d "${BUNDLES_DIR}/podman/oci" ]; then + for refs_file in ${BUNDLES_DIR}/podman/*.refs; do + [ -f "$refs_file" ] || continue + while IFS=: read -r oci_name image_ref; do + [ -z "$oci_name" ] && continue + local oci_path="${BUNDLES_DIR}/podman/oci/${oci_name}" + if [ -d "$oci_path" ]; then + podman_containers="${podman_containers} ${oci_path}:${image_ref}" + bbnote "Podman container: ${oci_path} -> ${image_ref}" + fi + done < "$refs_file" + done + fi + + # Import all Docker containers via vrunner (single invocation) + if [ -n "${docker_containers}" ]; then + bbnote "Importing Docker containers via vrunner: ${docker_containers}" + + local docker_storage="${WORKDIR}/docker-storage-$$.tar" + + ${VRUNNER_PATH} \ + --runtime docker \ + --arch ${BLOB_ARCH} \ + --blob-dir ${VDKR_BLOB_DIR} \ + --batch-import \ + --output "${docker_storage}" \ + --verbose \ + -- ${docker_containers} + + if [ $? -ne 0 ]; then + bbfatal "Docker container import failed" + fi + + if [ -f "${docker_storage}" ]; then + mkdir -p "${IMAGE_ROOTFS}/var/lib" + bbnote "Extracting Docker storage to rootfs..." + if ! tar -xf "${docker_storage}" -C "${IMAGE_ROOTFS}/var/lib" --no-same-owner; then + bbwarn "Docker storage extraction failed, trying with verbose..." + tar -xvf "${docker_storage}" -C "${IMAGE_ROOTFS}/var/lib" --no-same-owner 2>&1 | head -50 + fi + rm -f "${docker_storage}" + bbnote "Docker storage extraction complete" + fi + fi + + # Import all Podman containers via vrunner (single invocation) + if [ -n "${podman_containers}" ]; then + bbnote "Importing Podman containers via vrunner: ${podman_containers}" + + local podman_storage="${WORKDIR}/podman-storage-$$.tar" + + ${VRUNNER_PATH} \ + --runtime podman \ + --arch ${BLOB_ARCH} \ + --blob-dir ${VPDMN_BLOB_DIR} \ + --batch-import \ + --output "${podman_storage}" \ + --verbose \ + -- ${podman_containers} + + if [ $? -ne 0 ]; then + bbfatal "Podman container import failed" + fi + + if [ -f "${podman_storage}" ]; then + mkdir -p "${IMAGE_ROOTFS}/var/lib/containers/storage" + bbnote "Extracting Podman storage to rootfs..." + if ! tar -xf "${podman_storage}" -C "${IMAGE_ROOTFS}/var/lib/containers/storage" --no-same-owner; then + bbwarn "Podman storage extraction failed, trying with verbose..." + tar -xvf "${podman_storage}" -C "${IMAGE_ROOTFS}/var/lib/containers/storage" --no-same-owner 2>&1 | head -50 + fi + rm -f "${podman_storage}" + bbnote "Podman storage extraction complete" + fi + fi + + # Process autostart metadata from bundle packages + for meta in ${BUNDLES_DIR}/*.meta; do + [ -f "$meta" ] || continue + bbnote "Processing autostart metadata: $(basename $meta)" + + while IFS= read -r bundle || [ -n "$bundle" ]; do + [ -z "$bundle" ] && continue + + # Parse: source:runtime[:autostart-policy] + # source may contain colons (e.g., docker.io/library/busybox:1.36) + # Parse from the end: extract autostart first, then runtime, rest is source + local autostart_policy="" + local runtime_type="" + local source="" + + # Check if last field is an autostart policy + local last_field=$(echo "$bundle" | rev | cut -d: -f1 | rev) + case "$last_field" in + autostart|always|unless-stopped|on-failure|no) + autostart_policy="$last_field" + # Get runtime (second to last) and source (rest) + local without_autostart=$(echo "$bundle" | sed "s/:${autostart_policy}$//") + runtime_type=$(echo "$without_autostart" | rev | cut -d: -f1 | rev) + source=$(echo "$without_autostart" | sed "s/:${runtime_type}$//") + ;; + docker|podman) + # No autostart, last field is runtime + runtime_type="$last_field" + source=$(echo "$bundle" | sed "s/:${runtime_type}$//") + ;; + *) + # Unexpected format, try simple parsing + source=$(echo "$bundle" | cut -d: -f1) + runtime_type=$(echo "$bundle" | cut -d: -f2) + autostart_policy=$(echo "$bundle" | cut -d: -f3) + ;; + esac + + # Skip if no autostart requested + [ -z "$autostart_policy" ] && continue + + # Normalize autostart policy + local restart_policy + case "$autostart_policy" in + autostart|unless-stopped) + restart_policy="unless-stopped" + ;; + always|on-failure|no) + restart_policy="$autostart_policy" + ;; + *) + bbwarn "Unknown restart policy '$autostart_policy' for $source, using 'unless-stopped'" + restart_policy="unless-stopped" + ;; + esac + + # Extract image name from source + local image_name + local image_tag="latest" + if echo "$source" | grep -qE '[/.]'; then + # Remote container URL + image_name=$(echo "$source" | sed 's|.*/||' | sed 's/:.*$//') + image_tag=$(echo "$source" | grep -oE ':[^:]+$' | sed 's/^://' || echo "latest") + else + # Local container name + image_name="$source" + image_tag="latest" + fi + + local service_name="container-$(echo "$image_name" | sed 's/[^a-zA-Z0-9_-]/-/g' | tr '[:upper:]' '[:lower:]')" + + bbnote "Creating autostart service for $source ($runtime_type, restart=$restart_policy)" + + if [ "$runtime_type" = "docker" ]; then + generate_docker_service_from_bundle "$service_name" "$image_name" "$image_tag" "$restart_policy" + elif [ "$runtime_type" = "podman" ]; then + generate_podman_service_from_bundle "$service_name" "$image_name" "$image_tag" "$restart_policy" + fi + done < "$meta" + done + + # Clean up bundle files from final image (they're just intermediate artifacts) + rm -rf "${BUNDLES_DIR}" + bbnote "Cleaned up container bundle files" + + return 0 +} + +# Generate Docker systemd service (for bundle packages) +generate_docker_service_from_bundle() { + local service_name="$1" + local image_name="$2" + local image_tag="$3" + local restart_policy="$4" + + local service_dir="${IMAGE_ROOTFS}/lib/systemd/system" + local service_file="${service_dir}/${service_name}.service" + + mkdir -p "$service_dir" + + cat > "$service_file" << EOF +[Unit] +Description=Docker Container ${image_name}:${image_tag} +After=docker.service +Requires=docker.service + +[Service] +Type=simple +Restart=${restart_policy} +RestartSec=5s +TimeoutStartSec=0 +ExecStartPre=-/usr/bin/docker rm -f ${image_name} +ExecStart=/usr/bin/docker run --rm --name ${image_name} ${image_name}:${image_tag} +ExecStop=/usr/bin/docker stop ${image_name} + +[Install] +WantedBy=multi-user.target +EOF + + local wants_dir="${IMAGE_ROOTFS}/etc/systemd/system/multi-user.target.wants" + mkdir -p "$wants_dir" + ln -sf "/lib/systemd/system/${service_name}.service" "${wants_dir}/${service_name}.service" + + bbnote "Created and enabled ${service_name}.service for Docker container ${image_name}:${image_tag}" +} + +# Generate Podman Quadlet container file (for bundle packages) +generate_podman_service_from_bundle() { + local service_name="$1" + local image_name="$2" + local image_tag="$3" + local restart_policy="$4" + + local quadlet_dir="${IMAGE_ROOTFS}/etc/containers/systemd" + local container_file="${quadlet_dir}/${service_name}.container" + + mkdir -p "$quadlet_dir" + + cat > "$container_file" << EOF +# Quadlet container file for ${image_name}:${image_tag} +# Generated by container-cross-install + +[Unit] +Description=Podman Container ${image_name}:${image_tag} + +[Container] +Image=${image_name}:${image_tag} +ContainerName=${image_name} + +[Service] +Restart=${restart_policy} +RestartSec=5s + +[Install] +WantedBy=multi-user.target +EOF + + bbnote "Created Quadlet file ${service_name}.container for Podman container ${image_name}:${image_tag}" +} + +bundle_containers() { + set +e + + # ======================================================================== + # Helper functions for autostart support + # These must be defined INSIDE bundle_containers() to be available in + # bitbake's ROOTFS_POSTPROCESS_COMMAND execution context + # ======================================================================== + + # Extract container image name and tag from OCI directory name + # Sets: CONTAINER_IMAGE_NAME, CONTAINER_IMAGE_TAG + # Input: /path/to/container-base-latest-oci + # Note: Use _cci_ prefix to avoid conflicts with bitbake's environment variables + extract_container_info() { + _cci_oci_path="$1" + _cci_dir_name=$(basename "$_cci_oci_path" | sed 's/-oci$//') + + CONTAINER_IMAGE_NAME="" + CONTAINER_IMAGE_TAG="latest" + + # Three-part pattern: part1-part2-part3 (e.g., container-base-latest) + if echo "$_cci_dir_name" | grep -qE '^[^-]+-[^-]+-[^-]+$'; then + _cci_part1=$(echo "$_cci_dir_name" | cut -d- -f1) + _cci_part2=$(echo "$_cci_dir_name" | cut -d- -f2) + _cci_part3=$(echo "$_cci_dir_name" | cut -d- -f3) + CONTAINER_IMAGE_NAME="${_cci_part1}-${_cci_part2}" + CONTAINER_IMAGE_TAG="$_cci_part3" + # Two-part pattern: name-tag (e.g., myapp-1.0) + elif echo "$_cci_dir_name" | grep -qE '^[^-]+-[^-]+$'; then + CONTAINER_IMAGE_NAME=$(echo "$_cci_dir_name" | cut -d- -f1) + CONTAINER_IMAGE_TAG=$(echo "$_cci_dir_name" | cut -d- -f2) + # Single name (e.g., myapp) + else + CONTAINER_IMAGE_NAME="$_cci_dir_name" + CONTAINER_IMAGE_TAG="latest" + fi + } + + # Resolve OCI directory - support both recipe names and OCI dir names + # Input: container name (e.g., "container-base" or "container-base-latest-oci") + # Output: full path to OCI directory, or empty string if not found + resolve_oci_dir() { + local name="$1" + # If already ends in -oci, use as-is + if echo "$name" | grep -q '\-oci$'; then + echo "${DEPLOY_DIR_IMAGE}/${name}" + return + fi + # Fallback search (same as container-bundle.bbclass) + if [ -d "${DEPLOY_DIR_IMAGE}/${name}-latest-oci" ]; then + echo "${DEPLOY_DIR_IMAGE}/${name}-latest-oci" + elif [ -d "${DEPLOY_DIR_IMAGE}/${name}-oci" ]; then + echo "${DEPLOY_DIR_IMAGE}/${name}-oci" + else + echo "" + fi + } + + # Convert container name to valid systemd service name + # Input: my-app/special:latest + # Output: my-app-special-latest + sanitize_service_name() { + local name="$1" + echo "$name" | sed 's/[^a-zA-Z0-9_-]/-/g' | tr '[:upper:]' '[:lower:]' + } + + # Generate Docker systemd service file + # Args: service_name image_name image_tag restart_policy + generate_docker_service() { + local service_name="$1" + local image_name="$2" + local image_tag="$3" + local restart_policy="$4" + + # Use standard paths - systemd_system_unitdir is /lib/systemd/system + local service_dir="${IMAGE_ROOTFS}/lib/systemd/system" + local service_file="${service_dir}/${service_name}.service" + + mkdir -p "$service_dir" + + cat > "$service_file" << EOF +[Unit] +Description=Docker Container ${image_name}:${image_tag} +After=docker.service +Requires=docker.service + +[Service] +Type=simple +Restart=${restart_policy} +RestartSec=5s +TimeoutStartSec=0 +ExecStartPre=-/usr/bin/docker rm -f ${image_name} +ExecStart=/usr/bin/docker run --rm --name ${image_name} ${image_name}:${image_tag} +ExecStop=/usr/bin/docker stop ${image_name} + +[Install] +WantedBy=multi-user.target +EOF + + # Enable the service via symlink + local wants_dir="${IMAGE_ROOTFS}/etc/systemd/system/multi-user.target.wants" + mkdir -p "$wants_dir" + ln -sf "/lib/systemd/system/${service_name}.service" "${wants_dir}/${service_name}.service" + + bbnote "Created and enabled ${service_name}.service for Docker container ${image_name}:${image_tag}" + } + + # Generate Podman Quadlet container file + # Args: service_name image_name image_tag restart_policy + generate_podman_service() { + local service_name="$1" + local image_name="$2" + local image_tag="$3" + local restart_policy="$4" + + # Use Quadlet format for modern Podman + local quadlet_dir="${IMAGE_ROOTFS}/etc/containers/systemd" + local container_file="${quadlet_dir}/${service_name}.container" + + mkdir -p "$quadlet_dir" + + cat > "$container_file" << EOF +# Quadlet container file for ${image_name}:${image_tag} +# Generated by container-cross-install + +[Unit] +Description=Podman Container ${image_name}:${image_tag} + +[Container] +Image=${image_name}:${image_tag} +ContainerName=${image_name} + +[Service] +Restart=${restart_policy} +RestartSec=5s + +[Install] +WantedBy=multi-user.target +EOF + + bbnote "Created Quadlet file ${service_name}.container for Podman container ${image_name}:${image_tag}" + } + + # Install autostart services for containers with autostart policy + install_autostart_services() { + bbnote "Processing container autostart services..." + + if [ -z "${BUNDLED_CONTAINERS}" ]; then + return 0 + fi + + for bc in ${BUNDLED_CONTAINERS}; do + # Parse extended format: container:runtime[:autostart-policy] + local container_name="$(echo $bc | cut -d: -f1)" + local runtime_type="$(echo $bc | cut -d: -f2)" + local autostart_policy="$(echo $bc | cut -d: -f3)" + + # Default runtime to docker if not specified + if [ "$container_name" = "$runtime_type" ]; then + runtime_type="docker" + autostart_policy="" + fi + + # Skip if no autostart requested + if [ -z "$autostart_policy" ]; then + bbnote "Container $container_name: no autostart configured" + continue + fi + + # Normalize autostart policy + local restart_policy + case "$autostart_policy" in + autostart|unless-stopped) + restart_policy="unless-stopped" + ;; + always|on-failure|no) + restart_policy="$autostart_policy" + ;; + *) + bbwarn "Unknown restart policy '$autostart_policy' for $container_name, using 'unless-stopped'" + restart_policy="unless-stopped" + ;; + esac + + # Extract image name and tag from OCI directory + extract_container_info "${DEPLOY_DIR_IMAGE}/$container_name" + + # Generate service name + local service_name="container-$(sanitize_service_name "${CONTAINER_IMAGE_NAME}")" + + bbnote "Creating autostart service for $container_name ($runtime_type, restart=$restart_policy)" + + if [ "$runtime_type" = "docker" ]; then + generate_docker_service "$service_name" "${CONTAINER_IMAGE_NAME}" "${CONTAINER_IMAGE_TAG}" "$restart_policy" + elif [ "$runtime_type" = "podman" ]; then + generate_podman_service "$service_name" "${CONTAINER_IMAGE_NAME}" "${CONTAINER_IMAGE_TAG}" "$restart_policy" + else + bbwarn "Unknown runtime '$runtime_type' for autostart, skipping service generation" + fi + done + } + + # ======================================================================== + # End helper functions + # ======================================================================== + + if [ -z "${BUNDLED_CONTAINERS}" ]; then + bbnote "No bundled containers specified" + return 0 + fi + + bbnote "Processing bundled containers: ${BUNDLED_CONTAINERS}" + bbnote "Target architecture: ${QEMU_ARCH} (blob arch: ${BLOB_ARCH})" + + # Locate vrunner from vcontainer-native + VRUNNER="${VRUNNER_PATH}" + bbnote "vrunner: ${VRUNNER}" + + # Verify vrunner exists + if [ ! -f "${VRUNNER}" ]; then + bbfatal "vrunner not found at ${VRUNNER}. Ensure vcontainer-native is built." + fi + + # ======================================================================== + # Collect containers by runtime for batch processing + # Format: path:image:tag (as expected by vrunner --batch-import) + # ======================================================================== + DOCKER_CONTAINERS="" + PODMAN_CONTAINERS="" + + for bc in ${BUNDLED_CONTAINERS}; do + # Strip :external tag if present (used for third-party blobs) + # The :external tag only affects dependency generation (in __anonymous) + bc_clean="$bc" + if echo "$bc" | grep -q ':external'; then + bc_clean=$(echo "$bc" | sed 's/:external//') + fi + + container_name="$(echo $bc_clean | cut -d: -f1)" + runtime_type="$(echo $bc_clean | cut -d: -f2)" + + # Default runtime to docker if not specified + if [ "$container_name" = "$runtime_type" ]; then + runtime_type="docker" + fi + + bbnote "Collecting container: $container_name (runtime: $runtime_type)" + + # Resolve OCI directory (supports both recipe names and OCI dir names) + oci_dir=$(resolve_oci_dir "$container_name") + if [ -z "$oci_dir" ] || [ ! -e "$oci_dir" ]; then + bbfatal "============================================================ +MISSING CONTAINER: $container_name +============================================================ +OCI directory not found for '$container_name' + +Searched for: + ${DEPLOY_DIR_IMAGE}/${container_name}-latest-oci + ${DEPLOY_DIR_IMAGE}/${container_name}-oci + ${DEPLOY_DIR_IMAGE}/${container_name} + +To fix, build the container for this machine: + MACHINE=${MACHINE} bitbake ${container_name} + +Or remove it from BUNDLED_CONTAINERS if not needed. +============================================================" + fi + + # Extract image name and tag from OCI directory name + extract_container_info "$oci_dir" + BATCH_ENTRY="${oci_dir}:${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}" + + if [ "$runtime_type" = "docker" ]; then + DOCKER_CONTAINERS="${DOCKER_CONTAINERS} ${BATCH_ENTRY}" + elif [ "$runtime_type" = "podman" ]; then + PODMAN_CONTAINERS="${PODMAN_CONTAINERS} ${BATCH_ENTRY}" + else + bbwarn "Unknown runtime type: $runtime_type for $container_name, skipping" + fi + done + + # ======================================================================== + # Process Docker containers (batch import) + # ======================================================================== + if [ -n "${DOCKER_CONTAINERS}" ]; then + bbnote "Processing Docker containers: ${DOCKER_CONTAINERS}" + + DOCKER_STORAGE_TAR="${WORKDIR}/docker-storage-$$.tar" + DOCKER_OUTPUT_DIR="${IMAGE_ROOTFS}/var/lib/docker" + + # Verify Docker blobs exist + if [ ! -d "${VDKR_BLOB_DIR}" ]; then + bbfatal "Docker blob directory not found at ${VDKR_BLOB_DIR}. Build with: bitbake vdkr-initramfs-create" + fi + + # Check for existing Docker storage in rootfs (additive support) + EXISTING_DOCKER_STORAGE="" + if [ -d "${DOCKER_OUTPUT_DIR}" ] && [ -n "$(ls -A ${DOCKER_OUTPUT_DIR} 2>/dev/null)" ]; then + bbnote "Found existing Docker storage, will merge additively" + EXISTING_DOCKER_STORAGE="${WORKDIR}/existing-docker-$$.tar" + tar -cf "${EXISTING_DOCKER_STORAGE}" -C "${IMAGE_ROOTFS}/var/lib" docker + fi + + # Build vrunner batch-import command + VRUNNER_CMD="${VRUNNER} \ + --runtime docker \ + --arch ${BLOB_ARCH} \ + --blob-dir ${VDKR_BLOB_DIR} \ + --batch-import \ + --output ${DOCKER_STORAGE_TAR} \ + --verbose" + + if [ -n "${EXISTING_DOCKER_STORAGE}" ]; then + VRUNNER_CMD="${VRUNNER_CMD} --input-storage ${EXISTING_DOCKER_STORAGE}" + fi + + VRUNNER_CMD="${VRUNNER_CMD} -- ${DOCKER_CONTAINERS}" + + bbnote "Running batch import for Docker containers..." + TMPDIR="${WORKDIR}" eval ${VRUNNER_CMD} + + if [ $? -ne 0 ]; then + bbfatal "Docker batch import failed" + fi + + # Simple tar extraction - no merger needed! + # The storage tar has correct structure with docker/ at root + if [ -f "${DOCKER_STORAGE_TAR}" ]; then + bbnote "Extracting Docker storage to rootfs..." + mkdir -p "${DOCKER_OUTPUT_DIR}" + # Extract with --strip-components=1 to remove the 'docker' prefix + # since we're extracting directly into /var/lib/docker + tar -xf "${DOCKER_STORAGE_TAR}" -C "${IMAGE_ROOTFS}/var/lib" --no-same-owner + bbnote "Docker storage extracted successfully" + fi + + rm -f "${EXISTING_DOCKER_STORAGE}" + fi + + # ======================================================================== + # Process Podman containers (batch import) + # ======================================================================== + if [ -n "${PODMAN_CONTAINERS}" ]; then + bbnote "Processing Podman containers: ${PODMAN_CONTAINERS}" + + PODMAN_STORAGE_TAR="${WORKDIR}/podman-storage-$$.tar" + PODMAN_OUTPUT_DIR="${IMAGE_ROOTFS}/var/lib/containers/storage" + + # Verify Podman blobs exist + if [ ! -d "${VPDMN_BLOB_DIR}" ]; then + bbfatal "Podman blob directory not found at ${VPDMN_BLOB_DIR}. Build with: bitbake vpdmn-initramfs-create" + fi + + # Check for existing Podman storage in rootfs (additive support) + EXISTING_PODMAN_STORAGE="" + if [ -d "${PODMAN_OUTPUT_DIR}" ] && [ -n "$(ls -A ${PODMAN_OUTPUT_DIR} 2>/dev/null)" ]; then + bbnote "Found existing Podman storage, will merge additively" + EXISTING_PODMAN_STORAGE="${WORKDIR}/existing-podman-$$.tar" + tar -cf "${EXISTING_PODMAN_STORAGE}" -C "${IMAGE_ROOTFS}/var/lib/containers" storage + fi + + # Build vrunner batch-import command + VRUNNER_CMD="${VRUNNER} \ + --runtime podman \ + --arch ${BLOB_ARCH} \ + --blob-dir ${VPDMN_BLOB_DIR} \ + --batch-import \ + --output ${PODMAN_STORAGE_TAR} \ + --verbose" + + if [ -n "${EXISTING_PODMAN_STORAGE}" ]; then + VRUNNER_CMD="${VRUNNER_CMD} --input-storage ${EXISTING_PODMAN_STORAGE}" + fi + + VRUNNER_CMD="${VRUNNER_CMD} -- ${PODMAN_CONTAINERS}" + + bbnote "Running batch import for Podman containers..." + TMPDIR="${WORKDIR}" eval ${VRUNNER_CMD} + + if [ $? -ne 0 ]; then + bbfatal "Podman batch import failed" + fi + + # Simple tar extraction - no merger needed! + if [ -f "${PODMAN_STORAGE_TAR}" ]; then + bbnote "Extracting Podman storage to rootfs..." + mkdir -p "${PODMAN_OUTPUT_DIR}" + tar -xf "${PODMAN_STORAGE_TAR}" -C "${PODMAN_OUTPUT_DIR}" --no-same-owner + bbnote "Podman storage extracted successfully" + fi + + rm -f "${EXISTING_PODMAN_STORAGE}" + fi + + # ======================================================================== + # Install autostart services + # ======================================================================== + install_autostart_services + + bbnote "Done processing all bundled containers" +} + +# First merge any bundles installed via IMAGE_INSTALL, then process BUNDLED_CONTAINERS +ROOTFS_POSTPROCESS_COMMAND += "merge_installed_bundles; bundle_containers;" -- cgit v1.2.3-54-g00ecf