diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-01 17:08:35 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:32:52 +0000 |
| commit | c32e1081c81ba27f0d5a21a1885601f04d329d21 (patch) | |
| tree | a7e44880e69068d44e7e89d49af8ce709784a03f /classes/container-cross-install.bbclass | |
| parent | 79d03d5350c446223c847135c7115a656adc01d9 (diff) | |
| download | meta-virtualization-c32e1081c81ba27f0d5a21a1885601f04d329d21.tar.gz | |
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 <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes/container-cross-install.bbclass')
| -rw-r--r-- | classes/container-cross-install.bbclass | 911 |
1 files changed, 911 insertions, 0 deletions
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 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | # | ||
| 5 | # container-cross-install.bbclass | ||
| 6 | # =========================================================================== | ||
| 7 | # Cross-architecture container deployment class | ||
| 8 | # =========================================================================== | ||
| 9 | # | ||
| 10 | # This class enables bundling containers into target images during build time. | ||
| 11 | # It uses QEMU with a pre-built initramfs to process containers built for | ||
| 12 | # different architectures (cross-compilation safe). | ||
| 13 | # | ||
| 14 | # Usage (in image recipe, e.g., core-image-minimal.bbappend): | ||
| 15 | # inherit container-cross-install | ||
| 16 | # | ||
| 17 | # Configuration (in local.conf or image recipe): | ||
| 18 | # BUNDLED_CONTAINERS = "container-base:docker myapp:podman:autostart" | ||
| 19 | # | ||
| 20 | # Container format: name:runtime[:autostart][:external] | ||
| 21 | # - name: Container recipe name OR OCI directory name in DEPLOY_DIR_IMAGE | ||
| 22 | # - runtime: docker or podman | ||
| 23 | # - autostart: Optional. Creates systemd service to start on boot: | ||
| 24 | # * autostart - alias for unless-stopped (recommended) | ||
| 25 | # * always - always restart container | ||
| 26 | # * unless-stopped - restart unless manually stopped | ||
| 27 | # * on-failure - restart only on non-zero exit code | ||
| 28 | # - external: Optional tag for third-party containers (no dependency generated) | ||
| 29 | # | ||
| 30 | # Automatic dependency generation: | ||
| 31 | # - Dependencies on container recipes are auto-generated at parse time | ||
| 32 | # - If name ends in -oci, recipe name is derived (strip -latest-oci or -oci) | ||
| 33 | # - Use :external tag to skip dependency for third-party blobs | ||
| 34 | # | ||
| 35 | # OCI directory resolution: | ||
| 36 | # - If name ends in -oci, use directly from DEPLOY_DIR_IMAGE | ||
| 37 | # - Otherwise, search: name-latest-oci -> name-oci (fallback) | ||
| 38 | # | ||
| 39 | # Examples: | ||
| 40 | # BUNDLED_CONTAINERS = "container-base:docker" # auto-dep, no autostart | ||
| 41 | # BUNDLED_CONTAINERS = "myapp:podman:autostart" # auto-dep, autostart | ||
| 42 | # BUNDLED_CONTAINERS = "vendor-blob:docker:external" # no dep, third-party | ||
| 43 | # BUNDLED_CONTAINERS = "vendor-blob:docker:autostart:external" # no dep, autostart | ||
| 44 | # # Legacy format still supported: | ||
| 45 | # BUNDLED_CONTAINERS = "container-base-latest-oci:docker" # auto-dep (derived) | ||
| 46 | # | ||
| 47 | # Generated autostart files: | ||
| 48 | # Docker: /etc/systemd/system/container-<name>.service (enabled) | ||
| 49 | # Podman: /etc/containers/systemd/<name>.container (Quadlet format) | ||
| 50 | # | ||
| 51 | # The class uses vdkr/vpdmn initramfs blobs built via multiconfig (mcdepends). | ||
| 52 | # These contain Docker or Podman tools respectively and are built by | ||
| 53 | # vdkr-initramfs-create and vpdmn-initramfs-create recipes. | ||
| 54 | # | ||
| 55 | # =========================================================================== | ||
| 56 | # Choosing Between BUNDLED_CONTAINERS and container-bundle Packages | ||
| 57 | # =========================================================================== | ||
| 58 | # | ||
| 59 | # There are two ways to bundle containers into a host image: | ||
| 60 | # | ||
| 61 | # 1. BUNDLED_CONTAINERS variable (this class, simpler) | ||
| 62 | # Set in local.conf or image recipe - no extra recipe needed | ||
| 63 | # | ||
| 64 | # 2. container-bundle packages | ||
| 65 | # Create a bundle recipe that inherits container-bundle.bbclass, | ||
| 66 | # then add the package to IMAGE_INSTALL | ||
| 67 | # | ||
| 68 | # Decision guide: | ||
| 69 | # | ||
| 70 | # Use Case | BUNDLED_CONTAINERS | Bundle Recipe | ||
| 71 | # --------------------------------------------|--------------------|-------------- | ||
| 72 | # Simple: containers in one host image | recommended | overkill | ||
| 73 | # Reuse containers across multiple images | repetitive | recommended | ||
| 74 | # Remote containers (docker.io/library/...) | not supported | required | ||
| 75 | # Package versioning and dependencies | not supported | supported | ||
| 76 | # Distribute pre-built container set | not supported | supported | ||
| 77 | # | ||
| 78 | # For most single-image use cases, BUNDLED_CONTAINERS is simpler: | ||
| 79 | # - No bundle recipe needed | ||
| 80 | # - Dependencies auto-generated at parse time | ||
| 81 | # - vrunner batch-import runs once for all containers | ||
| 82 | # | ||
| 83 | # Use container-bundle.bbclass when you need: | ||
| 84 | # - Remote container fetching via skopeo | ||
| 85 | # - A distributable/versioned package of containers | ||
| 86 | # - To share the same bundle across multiple different host images | ||
| 87 | # | ||
| 88 | # =========================================================================== | ||
| 89 | # Integration with container-bundle.bbclass | ||
| 90 | # =========================================================================== | ||
| 91 | # | ||
| 92 | # This class also processes packages created by container-bundle.bbclass: | ||
| 93 | # 1. merge_installed_bundles() runs as ROOTFS_POSTPROCESS_COMMAND | ||
| 94 | # 2. Scans ${datadir}/container-bundles/{docker,podman}/oci/ and *.refs files | ||
| 95 | # 3. Runs vrunner --batch-import once to create storage, extracts to rootfs | ||
| 96 | # 4. Reads *.meta files for autostart service generation | ||
| 97 | # | ||
| 98 | # The runtime is determined by the subdirectory (docker/ vs podman/), | ||
| 99 | # which is set by container-bundle.bbclass based on CONTAINER_BUNDLE_RUNTIME. | ||
| 100 | # | ||
| 101 | # See also: container-bundle.bbclass | ||
| 102 | |||
| 103 | # Dependencies on native tools | ||
| 104 | # vcontainer-native provides vrunner.sh | ||
| 105 | # Blobs come from multiconfig builds (vdkr-initramfs-create, vpdmn-initramfs-create) | ||
| 106 | DEPENDS += "qemuwrapper-cross qemu-system-native skopeo-native" | ||
| 107 | DEPENDS += "vcontainer-native coreutils-native" | ||
| 108 | |||
| 109 | # Determine multiconfig name for blob building based on target architecture | ||
| 110 | def get_vruntime_multiconfig(d): | ||
| 111 | arch = d.getVar('TARGET_ARCH') | ||
| 112 | if arch == 'aarch64': | ||
| 113 | return 'vruntime-aarch64' | ||
| 114 | elif arch in ['x86_64', 'i686', 'i586']: | ||
| 115 | return 'vruntime-x86-64' | ||
| 116 | else: | ||
| 117 | return None | ||
| 118 | |||
| 119 | # Get the MACHINE name used in the multiconfig (for deploy path) | ||
| 120 | def get_vruntime_machine(d): | ||
| 121 | arch = d.getVar('TARGET_ARCH') | ||
| 122 | if arch == 'aarch64': | ||
| 123 | return 'qemuarm64' | ||
| 124 | elif arch in ['x86_64', 'i686', 'i586']: | ||
| 125 | return 'qemux86-64' | ||
| 126 | else: | ||
| 127 | return None | ||
| 128 | |||
| 129 | VRUNTIME_MULTICONFIG = "${@get_vruntime_multiconfig(d)}" | ||
| 130 | VRUNTIME_MACHINE = "${@get_vruntime_machine(d)}" | ||
| 131 | |||
| 132 | # Use mcdepends to automatically build vdkr/vpdmn blobs via multiconfig | ||
| 133 | # This ensures blobs are built as part of the normal Yocto build flow | ||
| 134 | # Requires BBMULTICONFIG = "vruntime-aarch64 vruntime-x86-64" in local.conf | ||
| 135 | do_rootfs[mcdepends] = "mc::${VRUNTIME_MULTICONFIG}:vdkr-initramfs-create:do_deploy mc::${VRUNTIME_MULTICONFIG}:vpdmn-initramfs-create:do_deploy" | ||
| 136 | |||
| 137 | # Generate dependencies for BUNDLED_CONTAINERS at parse time | ||
| 138 | # Format: name:runtime[:autostart][:external] | ||
| 139 | # - If :external present, no dependency generated (third-party blob) | ||
| 140 | # - If name ends in -oci, derive recipe name and generate dependency | ||
| 141 | # - Otherwise, generate dependency on name:do_image_complete | ||
| 142 | python __anonymous() { | ||
| 143 | bundled = (d.getVar('BUNDLED_CONTAINERS') or "").split() | ||
| 144 | if not bundled: | ||
| 145 | return | ||
| 146 | |||
| 147 | deps = "" | ||
| 148 | for entry in bundled: | ||
| 149 | parts = entry.split(':') | ||
| 150 | container_name = parts[0] | ||
| 151 | |||
| 152 | # Check for :external tag (can be in position 3 or 4) | ||
| 153 | is_external = 'external' in parts | ||
| 154 | |||
| 155 | # Skip dependency for external containers | ||
| 156 | if is_external: | ||
| 157 | continue | ||
| 158 | |||
| 159 | # Derive recipe name from OCI dir name if needed | ||
| 160 | recipe_name = container_name | ||
| 161 | if container_name.endswith('-latest-oci'): | ||
| 162 | recipe_name = container_name[:-11] # strip -latest-oci | ||
| 163 | elif container_name.endswith('-oci'): | ||
| 164 | recipe_name = container_name[:-4] # strip -oci | ||
| 165 | |||
| 166 | # Generate dependency | ||
| 167 | deps += f" {recipe_name}:do_image_complete" | ||
| 168 | |||
| 169 | if deps: | ||
| 170 | d.appendVarFlag('do_rootfs', 'depends', deps) | ||
| 171 | } | ||
| 172 | |||
| 173 | # Path to vrunner.sh from vcontainer-native | ||
| 174 | VRUNNER_PATH = "${STAGING_BINDIR_NATIVE}/vrunner.sh" | ||
| 175 | |||
| 176 | # Blobs come from multiconfig's DEPLOY_DIR (built by mcdepends on vdkr-initramfs-create) | ||
| 177 | # Multiconfig uses separate TMPDIR, so deploy path is: | ||
| 178 | # ${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/ | ||
| 179 | # Blobs are in runtime/arch subdirectories: ${BLOB_DIR}/${ARCH}/ (e.g., x86_64/, aarch64/) | ||
| 180 | VDKR_BLOB_DIR = "${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/vdkr" | ||
| 181 | VPDMN_BLOB_DIR = "${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/vpdmn" | ||
| 182 | |||
| 183 | bundle_containers[network] = "1" | ||
| 184 | do_testsdkext[nostamp] = "1" | ||
| 185 | |||
| 186 | # Map TARGET_ARCH to QEMU architecture names | ||
| 187 | def get_qemu_arch(d): | ||
| 188 | """Map Yocto TARGET_ARCH to QEMU architecture name""" | ||
| 189 | arch = d.getVar('TARGET_ARCH') | ||
| 190 | arch_map = { | ||
| 191 | 'aarch64': 'aarch64', | ||
| 192 | 'arm': 'arm', | ||
| 193 | 'x86_64': 'x86_64', | ||
| 194 | 'i686': 'i386', | ||
| 195 | 'i586': 'i386', | ||
| 196 | } | ||
| 197 | return arch_map.get(arch, arch) | ||
| 198 | |||
| 199 | QEMU_ARCH = "${@get_qemu_arch(d)}" | ||
| 200 | |||
| 201 | # Map TARGET_ARCH to kernel image name | ||
| 202 | def get_kernel_name(d): | ||
| 203 | """Map Yocto TARGET_ARCH to kernel image filename""" | ||
| 204 | arch = d.getVar('TARGET_ARCH') | ||
| 205 | kernel_map = { | ||
| 206 | 'aarch64': 'Image', | ||
| 207 | 'arm': 'zImage', | ||
| 208 | 'x86_64': 'bzImage', | ||
| 209 | 'i686': 'bzImage', | ||
| 210 | 'i586': 'bzImage', | ||
| 211 | } | ||
| 212 | return kernel_map.get(arch, 'Image') | ||
| 213 | |||
| 214 | KERNEL_IMAGETYPE_QEMU = "${@get_kernel_name(d)}" | ||
| 215 | |||
| 216 | # Map TARGET_ARCH to blob directory name (aarch64, x86_64) | ||
| 217 | def get_blob_arch(d): | ||
| 218 | """Map Yocto TARGET_ARCH to blob directory name""" | ||
| 219 | arch = d.getVar('TARGET_ARCH') | ||
| 220 | blob_map = { | ||
| 221 | 'aarch64': 'aarch64', | ||
| 222 | 'arm': 'aarch64', # Use aarch64 blobs for 32-bit ARM too | ||
| 223 | 'x86_64': 'x86_64', | ||
| 224 | 'i686': 'x86_64', | ||
| 225 | 'i586': 'x86_64', | ||
| 226 | } | ||
| 227 | return blob_map.get(arch, 'aarch64') | ||
| 228 | |||
| 229 | BLOB_ARCH = "${@get_blob_arch(d)}" | ||
| 230 | |||
| 231 | # ============================================================================ | ||
| 232 | # Merge container bundles installed via IMAGE_INSTALL | ||
| 233 | # This function processes packages created by container-bundle.bbclass | ||
| 234 | # ============================================================================ | ||
| 235 | merge_installed_bundles() { | ||
| 236 | # Disable errexit - we handle errors explicitly | ||
| 237 | set +e | ||
| 238 | |||
| 239 | BUNDLES_DIR="${IMAGE_ROOTFS}${datadir}/container-bundles" | ||
| 240 | |||
| 241 | if [ ! -d "${BUNDLES_DIR}" ]; then | ||
| 242 | bbnote "No container bundles found in ${BUNDLES_DIR}" | ||
| 243 | return 0 | ||
| 244 | fi | ||
| 245 | |||
| 246 | bbnote "Processing installed container bundles from ${BUNDLES_DIR}" | ||
| 247 | |||
| 248 | # Collect all OCI directories from bundle packages | ||
| 249 | # Bundle packages now contain OCI directories, not storage tars | ||
| 250 | # We run vrunner ONCE with all containers to create a single storage tar | ||
| 251 | |||
| 252 | local docker_containers="" | ||
| 253 | local podman_containers="" | ||
| 254 | |||
| 255 | # Collect Docker OCI directories and refs | ||
| 256 | if [ -d "${BUNDLES_DIR}/docker/oci" ]; then | ||
| 257 | for refs_file in ${BUNDLES_DIR}/docker/*.refs; do | ||
| 258 | [ -f "$refs_file" ] || continue | ||
| 259 | while IFS=: read -r oci_name image_ref; do | ||
| 260 | [ -z "$oci_name" ] && continue | ||
| 261 | local oci_path="${BUNDLES_DIR}/docker/oci/${oci_name}" | ||
| 262 | if [ -d "$oci_path" ]; then | ||
| 263 | # Format for vrunner batch-import: path:image:tag | ||
| 264 | docker_containers="${docker_containers} ${oci_path}:${image_ref}" | ||
| 265 | bbnote "Docker container: ${oci_path} -> ${image_ref}" | ||
| 266 | fi | ||
| 267 | done < "$refs_file" | ||
| 268 | done | ||
| 269 | fi | ||
| 270 | |||
| 271 | # Collect Podman OCI directories and refs | ||
| 272 | if [ -d "${BUNDLES_DIR}/podman/oci" ]; then | ||
| 273 | for refs_file in ${BUNDLES_DIR}/podman/*.refs; do | ||
| 274 | [ -f "$refs_file" ] || continue | ||
| 275 | while IFS=: read -r oci_name image_ref; do | ||
| 276 | [ -z "$oci_name" ] && continue | ||
| 277 | local oci_path="${BUNDLES_DIR}/podman/oci/${oci_name}" | ||
| 278 | if [ -d "$oci_path" ]; then | ||
| 279 | podman_containers="${podman_containers} ${oci_path}:${image_ref}" | ||
| 280 | bbnote "Podman container: ${oci_path} -> ${image_ref}" | ||
| 281 | fi | ||
| 282 | done < "$refs_file" | ||
| 283 | done | ||
| 284 | fi | ||
| 285 | |||
| 286 | # Import all Docker containers via vrunner (single invocation) | ||
| 287 | if [ -n "${docker_containers}" ]; then | ||
| 288 | bbnote "Importing Docker containers via vrunner: ${docker_containers}" | ||
| 289 | |||
| 290 | local docker_storage="${WORKDIR}/docker-storage-$$.tar" | ||
| 291 | |||
| 292 | ${VRUNNER_PATH} \ | ||
| 293 | --runtime docker \ | ||
| 294 | --arch ${BLOB_ARCH} \ | ||
| 295 | --blob-dir ${VDKR_BLOB_DIR} \ | ||
| 296 | --batch-import \ | ||
| 297 | --output "${docker_storage}" \ | ||
| 298 | --verbose \ | ||
| 299 | -- ${docker_containers} | ||
| 300 | |||
| 301 | if [ $? -ne 0 ]; then | ||
| 302 | bbfatal "Docker container import failed" | ||
| 303 | fi | ||
| 304 | |||
| 305 | if [ -f "${docker_storage}" ]; then | ||
| 306 | mkdir -p "${IMAGE_ROOTFS}/var/lib" | ||
| 307 | bbnote "Extracting Docker storage to rootfs..." | ||
| 308 | if ! tar -xf "${docker_storage}" -C "${IMAGE_ROOTFS}/var/lib" --no-same-owner; then | ||
| 309 | bbwarn "Docker storage extraction failed, trying with verbose..." | ||
| 310 | tar -xvf "${docker_storage}" -C "${IMAGE_ROOTFS}/var/lib" --no-same-owner 2>&1 | head -50 | ||
| 311 | fi | ||
| 312 | rm -f "${docker_storage}" | ||
| 313 | bbnote "Docker storage extraction complete" | ||
| 314 | fi | ||
| 315 | fi | ||
| 316 | |||
| 317 | # Import all Podman containers via vrunner (single invocation) | ||
| 318 | if [ -n "${podman_containers}" ]; then | ||
| 319 | bbnote "Importing Podman containers via vrunner: ${podman_containers}" | ||
| 320 | |||
| 321 | local podman_storage="${WORKDIR}/podman-storage-$$.tar" | ||
| 322 | |||
| 323 | ${VRUNNER_PATH} \ | ||
| 324 | --runtime podman \ | ||
| 325 | --arch ${BLOB_ARCH} \ | ||
| 326 | --blob-dir ${VPDMN_BLOB_DIR} \ | ||
| 327 | --batch-import \ | ||
| 328 | --output "${podman_storage}" \ | ||
| 329 | --verbose \ | ||
| 330 | -- ${podman_containers} | ||
| 331 | |||
| 332 | if [ $? -ne 0 ]; then | ||
| 333 | bbfatal "Podman container import failed" | ||
| 334 | fi | ||
| 335 | |||
| 336 | if [ -f "${podman_storage}" ]; then | ||
| 337 | mkdir -p "${IMAGE_ROOTFS}/var/lib/containers/storage" | ||
| 338 | bbnote "Extracting Podman storage to rootfs..." | ||
| 339 | if ! tar -xf "${podman_storage}" -C "${IMAGE_ROOTFS}/var/lib/containers/storage" --no-same-owner; then | ||
| 340 | bbwarn "Podman storage extraction failed, trying with verbose..." | ||
| 341 | tar -xvf "${podman_storage}" -C "${IMAGE_ROOTFS}/var/lib/containers/storage" --no-same-owner 2>&1 | head -50 | ||
| 342 | fi | ||
| 343 | rm -f "${podman_storage}" | ||
| 344 | bbnote "Podman storage extraction complete" | ||
| 345 | fi | ||
| 346 | fi | ||
| 347 | |||
| 348 | # Process autostart metadata from bundle packages | ||
| 349 | for meta in ${BUNDLES_DIR}/*.meta; do | ||
| 350 | [ -f "$meta" ] || continue | ||
| 351 | bbnote "Processing autostart metadata: $(basename $meta)" | ||
| 352 | |||
| 353 | while IFS= read -r bundle || [ -n "$bundle" ]; do | ||
| 354 | [ -z "$bundle" ] && continue | ||
| 355 | |||
| 356 | # Parse: source:runtime[:autostart-policy] | ||
| 357 | # source may contain colons (e.g., docker.io/library/busybox:1.36) | ||
| 358 | # Parse from the end: extract autostart first, then runtime, rest is source | ||
| 359 | local autostart_policy="" | ||
| 360 | local runtime_type="" | ||
| 361 | local source="" | ||
| 362 | |||
| 363 | # Check if last field is an autostart policy | ||
| 364 | local last_field=$(echo "$bundle" | rev | cut -d: -f1 | rev) | ||
| 365 | case "$last_field" in | ||
| 366 | autostart|always|unless-stopped|on-failure|no) | ||
| 367 | autostart_policy="$last_field" | ||
| 368 | # Get runtime (second to last) and source (rest) | ||
| 369 | local without_autostart=$(echo "$bundle" | sed "s/:${autostart_policy}$//") | ||
| 370 | runtime_type=$(echo "$without_autostart" | rev | cut -d: -f1 | rev) | ||
| 371 | source=$(echo "$without_autostart" | sed "s/:${runtime_type}$//") | ||
| 372 | ;; | ||
| 373 | docker|podman) | ||
| 374 | # No autostart, last field is runtime | ||
| 375 | runtime_type="$last_field" | ||
| 376 | source=$(echo "$bundle" | sed "s/:${runtime_type}$//") | ||
| 377 | ;; | ||
| 378 | *) | ||
| 379 | # Unexpected format, try simple parsing | ||
| 380 | source=$(echo "$bundle" | cut -d: -f1) | ||
| 381 | runtime_type=$(echo "$bundle" | cut -d: -f2) | ||
| 382 | autostart_policy=$(echo "$bundle" | cut -d: -f3) | ||
| 383 | ;; | ||
| 384 | esac | ||
| 385 | |||
| 386 | # Skip if no autostart requested | ||
| 387 | [ -z "$autostart_policy" ] && continue | ||
| 388 | |||
| 389 | # Normalize autostart policy | ||
| 390 | local restart_policy | ||
| 391 | case "$autostart_policy" in | ||
| 392 | autostart|unless-stopped) | ||
| 393 | restart_policy="unless-stopped" | ||
| 394 | ;; | ||
| 395 | always|on-failure|no) | ||
| 396 | restart_policy="$autostart_policy" | ||
| 397 | ;; | ||
| 398 | *) | ||
| 399 | bbwarn "Unknown restart policy '$autostart_policy' for $source, using 'unless-stopped'" | ||
| 400 | restart_policy="unless-stopped" | ||
| 401 | ;; | ||
| 402 | esac | ||
| 403 | |||
| 404 | # Extract image name from source | ||
| 405 | local image_name | ||
| 406 | local image_tag="latest" | ||
| 407 | if echo "$source" | grep -qE '[/.]'; then | ||
| 408 | # Remote container URL | ||
| 409 | image_name=$(echo "$source" | sed 's|.*/||' | sed 's/:.*$//') | ||
| 410 | image_tag=$(echo "$source" | grep -oE ':[^:]+$' | sed 's/^://' || echo "latest") | ||
| 411 | else | ||
| 412 | # Local container name | ||
| 413 | image_name="$source" | ||
| 414 | image_tag="latest" | ||
| 415 | fi | ||
| 416 | |||
| 417 | local service_name="container-$(echo "$image_name" | sed 's/[^a-zA-Z0-9_-]/-/g' | tr '[:upper:]' '[:lower:]')" | ||
| 418 | |||
| 419 | bbnote "Creating autostart service for $source ($runtime_type, restart=$restart_policy)" | ||
| 420 | |||
| 421 | if [ "$runtime_type" = "docker" ]; then | ||
| 422 | generate_docker_service_from_bundle "$service_name" "$image_name" "$image_tag" "$restart_policy" | ||
| 423 | elif [ "$runtime_type" = "podman" ]; then | ||
| 424 | generate_podman_service_from_bundle "$service_name" "$image_name" "$image_tag" "$restart_policy" | ||
| 425 | fi | ||
| 426 | done < "$meta" | ||
| 427 | done | ||
| 428 | |||
| 429 | # Clean up bundle files from final image (they're just intermediate artifacts) | ||
| 430 | rm -rf "${BUNDLES_DIR}" | ||
| 431 | bbnote "Cleaned up container bundle files" | ||
| 432 | |||
| 433 | return 0 | ||
| 434 | } | ||
| 435 | |||
| 436 | # Generate Docker systemd service (for bundle packages) | ||
| 437 | generate_docker_service_from_bundle() { | ||
| 438 | local service_name="$1" | ||
| 439 | local image_name="$2" | ||
| 440 | local image_tag="$3" | ||
| 441 | local restart_policy="$4" | ||
| 442 | |||
| 443 | local service_dir="${IMAGE_ROOTFS}/lib/systemd/system" | ||
| 444 | local service_file="${service_dir}/${service_name}.service" | ||
| 445 | |||
| 446 | mkdir -p "$service_dir" | ||
| 447 | |||
| 448 | cat > "$service_file" << EOF | ||
| 449 | [Unit] | ||
| 450 | Description=Docker Container ${image_name}:${image_tag} | ||
| 451 | After=docker.service | ||
| 452 | Requires=docker.service | ||
| 453 | |||
| 454 | [Service] | ||
| 455 | Type=simple | ||
| 456 | Restart=${restart_policy} | ||
| 457 | RestartSec=5s | ||
| 458 | TimeoutStartSec=0 | ||
| 459 | ExecStartPre=-/usr/bin/docker rm -f ${image_name} | ||
| 460 | ExecStart=/usr/bin/docker run --rm --name ${image_name} ${image_name}:${image_tag} | ||
| 461 | ExecStop=/usr/bin/docker stop ${image_name} | ||
| 462 | |||
| 463 | [Install] | ||
| 464 | WantedBy=multi-user.target | ||
| 465 | EOF | ||
| 466 | |||
| 467 | local wants_dir="${IMAGE_ROOTFS}/etc/systemd/system/multi-user.target.wants" | ||
| 468 | mkdir -p "$wants_dir" | ||
| 469 | ln -sf "/lib/systemd/system/${service_name}.service" "${wants_dir}/${service_name}.service" | ||
| 470 | |||
| 471 | bbnote "Created and enabled ${service_name}.service for Docker container ${image_name}:${image_tag}" | ||
| 472 | } | ||
| 473 | |||
| 474 | # Generate Podman Quadlet container file (for bundle packages) | ||
| 475 | generate_podman_service_from_bundle() { | ||
| 476 | local service_name="$1" | ||
| 477 | local image_name="$2" | ||
| 478 | local image_tag="$3" | ||
| 479 | local restart_policy="$4" | ||
| 480 | |||
| 481 | local quadlet_dir="${IMAGE_ROOTFS}/etc/containers/systemd" | ||
| 482 | local container_file="${quadlet_dir}/${service_name}.container" | ||
| 483 | |||
| 484 | mkdir -p "$quadlet_dir" | ||
| 485 | |||
| 486 | cat > "$container_file" << EOF | ||
| 487 | # Quadlet container file for ${image_name}:${image_tag} | ||
| 488 | # Generated by container-cross-install | ||
| 489 | |||
| 490 | [Unit] | ||
| 491 | Description=Podman Container ${image_name}:${image_tag} | ||
| 492 | |||
| 493 | [Container] | ||
| 494 | Image=${image_name}:${image_tag} | ||
| 495 | ContainerName=${image_name} | ||
| 496 | |||
| 497 | [Service] | ||
| 498 | Restart=${restart_policy} | ||
| 499 | RestartSec=5s | ||
| 500 | |||
| 501 | [Install] | ||
| 502 | WantedBy=multi-user.target | ||
| 503 | EOF | ||
| 504 | |||
| 505 | bbnote "Created Quadlet file ${service_name}.container for Podman container ${image_name}:${image_tag}" | ||
| 506 | } | ||
| 507 | |||
| 508 | bundle_containers() { | ||
| 509 | set +e | ||
| 510 | |||
| 511 | # ======================================================================== | ||
| 512 | # Helper functions for autostart support | ||
| 513 | # These must be defined INSIDE bundle_containers() to be available in | ||
| 514 | # bitbake's ROOTFS_POSTPROCESS_COMMAND execution context | ||
| 515 | # ======================================================================== | ||
| 516 | |||
| 517 | # Extract container image name and tag from OCI directory name | ||
| 518 | # Sets: CONTAINER_IMAGE_NAME, CONTAINER_IMAGE_TAG | ||
| 519 | # Input: /path/to/container-base-latest-oci | ||
| 520 | # Note: Use _cci_ prefix to avoid conflicts with bitbake's environment variables | ||
| 521 | extract_container_info() { | ||
| 522 | _cci_oci_path="$1" | ||
| 523 | _cci_dir_name=$(basename "$_cci_oci_path" | sed 's/-oci$//') | ||
| 524 | |||
| 525 | CONTAINER_IMAGE_NAME="" | ||
| 526 | CONTAINER_IMAGE_TAG="latest" | ||
| 527 | |||
| 528 | # Three-part pattern: part1-part2-part3 (e.g., container-base-latest) | ||
| 529 | if echo "$_cci_dir_name" | grep -qE '^[^-]+-[^-]+-[^-]+$'; then | ||
| 530 | _cci_part1=$(echo "$_cci_dir_name" | cut -d- -f1) | ||
| 531 | _cci_part2=$(echo "$_cci_dir_name" | cut -d- -f2) | ||
| 532 | _cci_part3=$(echo "$_cci_dir_name" | cut -d- -f3) | ||
| 533 | CONTAINER_IMAGE_NAME="${_cci_part1}-${_cci_part2}" | ||
| 534 | CONTAINER_IMAGE_TAG="$_cci_part3" | ||
| 535 | # Two-part pattern: name-tag (e.g., myapp-1.0) | ||
| 536 | elif echo "$_cci_dir_name" | grep -qE '^[^-]+-[^-]+$'; then | ||
| 537 | CONTAINER_IMAGE_NAME=$(echo "$_cci_dir_name" | cut -d- -f1) | ||
| 538 | CONTAINER_IMAGE_TAG=$(echo "$_cci_dir_name" | cut -d- -f2) | ||
| 539 | # Single name (e.g., myapp) | ||
| 540 | else | ||
| 541 | CONTAINER_IMAGE_NAME="$_cci_dir_name" | ||
| 542 | CONTAINER_IMAGE_TAG="latest" | ||
| 543 | fi | ||
| 544 | } | ||
| 545 | |||
| 546 | # Resolve OCI directory - support both recipe names and OCI dir names | ||
| 547 | # Input: container name (e.g., "container-base" or "container-base-latest-oci") | ||
| 548 | # Output: full path to OCI directory, or empty string if not found | ||
| 549 | resolve_oci_dir() { | ||
| 550 | local name="$1" | ||
| 551 | # If already ends in -oci, use as-is | ||
| 552 | if echo "$name" | grep -q '\-oci$'; then | ||
| 553 | echo "${DEPLOY_DIR_IMAGE}/${name}" | ||
| 554 | return | ||
| 555 | fi | ||
| 556 | # Fallback search (same as container-bundle.bbclass) | ||
| 557 | if [ -d "${DEPLOY_DIR_IMAGE}/${name}-latest-oci" ]; then | ||
| 558 | echo "${DEPLOY_DIR_IMAGE}/${name}-latest-oci" | ||
| 559 | elif [ -d "${DEPLOY_DIR_IMAGE}/${name}-oci" ]; then | ||
| 560 | echo "${DEPLOY_DIR_IMAGE}/${name}-oci" | ||
| 561 | else | ||
| 562 | echo "" | ||
| 563 | fi | ||
| 564 | } | ||
| 565 | |||
| 566 | # Convert container name to valid systemd service name | ||
| 567 | # Input: my-app/special:latest | ||
| 568 | # Output: my-app-special-latest | ||
| 569 | sanitize_service_name() { | ||
| 570 | local name="$1" | ||
| 571 | echo "$name" | sed 's/[^a-zA-Z0-9_-]/-/g' | tr '[:upper:]' '[:lower:]' | ||
| 572 | } | ||
| 573 | |||
| 574 | # Generate Docker systemd service file | ||
| 575 | # Args: service_name image_name image_tag restart_policy | ||
| 576 | generate_docker_service() { | ||
| 577 | local service_name="$1" | ||
| 578 | local image_name="$2" | ||
| 579 | local image_tag="$3" | ||
| 580 | local restart_policy="$4" | ||
| 581 | |||
| 582 | # Use standard paths - systemd_system_unitdir is /lib/systemd/system | ||
| 583 | local service_dir="${IMAGE_ROOTFS}/lib/systemd/system" | ||
| 584 | local service_file="${service_dir}/${service_name}.service" | ||
| 585 | |||
| 586 | mkdir -p "$service_dir" | ||
| 587 | |||
| 588 | cat > "$service_file" << EOF | ||
| 589 | [Unit] | ||
| 590 | Description=Docker Container ${image_name}:${image_tag} | ||
| 591 | After=docker.service | ||
| 592 | Requires=docker.service | ||
| 593 | |||
| 594 | [Service] | ||
| 595 | Type=simple | ||
| 596 | Restart=${restart_policy} | ||
| 597 | RestartSec=5s | ||
| 598 | TimeoutStartSec=0 | ||
| 599 | ExecStartPre=-/usr/bin/docker rm -f ${image_name} | ||
| 600 | ExecStart=/usr/bin/docker run --rm --name ${image_name} ${image_name}:${image_tag} | ||
| 601 | ExecStop=/usr/bin/docker stop ${image_name} | ||
| 602 | |||
| 603 | [Install] | ||
| 604 | WantedBy=multi-user.target | ||
| 605 | EOF | ||
| 606 | |||
| 607 | # Enable the service via symlink | ||
| 608 | local wants_dir="${IMAGE_ROOTFS}/etc/systemd/system/multi-user.target.wants" | ||
| 609 | mkdir -p "$wants_dir" | ||
| 610 | ln -sf "/lib/systemd/system/${service_name}.service" "${wants_dir}/${service_name}.service" | ||
| 611 | |||
| 612 | bbnote "Created and enabled ${service_name}.service for Docker container ${image_name}:${image_tag}" | ||
| 613 | } | ||
| 614 | |||
| 615 | # Generate Podman Quadlet container file | ||
| 616 | # Args: service_name image_name image_tag restart_policy | ||
| 617 | generate_podman_service() { | ||
| 618 | local service_name="$1" | ||
| 619 | local image_name="$2" | ||
| 620 | local image_tag="$3" | ||
| 621 | local restart_policy="$4" | ||
| 622 | |||
| 623 | # Use Quadlet format for modern Podman | ||
| 624 | local quadlet_dir="${IMAGE_ROOTFS}/etc/containers/systemd" | ||
| 625 | local container_file="${quadlet_dir}/${service_name}.container" | ||
| 626 | |||
| 627 | mkdir -p "$quadlet_dir" | ||
| 628 | |||
| 629 | cat > "$container_file" << EOF | ||
| 630 | # Quadlet container file for ${image_name}:${image_tag} | ||
| 631 | # Generated by container-cross-install | ||
| 632 | |||
| 633 | [Unit] | ||
| 634 | Description=Podman Container ${image_name}:${image_tag} | ||
| 635 | |||
| 636 | [Container] | ||
| 637 | Image=${image_name}:${image_tag} | ||
| 638 | ContainerName=${image_name} | ||
| 639 | |||
| 640 | [Service] | ||
| 641 | Restart=${restart_policy} | ||
| 642 | RestartSec=5s | ||
| 643 | |||
| 644 | [Install] | ||
| 645 | WantedBy=multi-user.target | ||
| 646 | EOF | ||
| 647 | |||
| 648 | bbnote "Created Quadlet file ${service_name}.container for Podman container ${image_name}:${image_tag}" | ||
| 649 | } | ||
| 650 | |||
| 651 | # Install autostart services for containers with autostart policy | ||
| 652 | install_autostart_services() { | ||
| 653 | bbnote "Processing container autostart services..." | ||
| 654 | |||
| 655 | if [ -z "${BUNDLED_CONTAINERS}" ]; then | ||
| 656 | return 0 | ||
| 657 | fi | ||
| 658 | |||
| 659 | for bc in ${BUNDLED_CONTAINERS}; do | ||
| 660 | # Parse extended format: container:runtime[:autostart-policy] | ||
| 661 | local container_name="$(echo $bc | cut -d: -f1)" | ||
| 662 | local runtime_type="$(echo $bc | cut -d: -f2)" | ||
| 663 | local autostart_policy="$(echo $bc | cut -d: -f3)" | ||
| 664 | |||
| 665 | # Default runtime to docker if not specified | ||
| 666 | if [ "$container_name" = "$runtime_type" ]; then | ||
| 667 | runtime_type="docker" | ||
| 668 | autostart_policy="" | ||
| 669 | fi | ||
| 670 | |||
| 671 | # Skip if no autostart requested | ||
| 672 | if [ -z "$autostart_policy" ]; then | ||
| 673 | bbnote "Container $container_name: no autostart configured" | ||
| 674 | continue | ||
| 675 | fi | ||
| 676 | |||
| 677 | # Normalize autostart policy | ||
| 678 | local restart_policy | ||
| 679 | case "$autostart_policy" in | ||
| 680 | autostart|unless-stopped) | ||
| 681 | restart_policy="unless-stopped" | ||
| 682 | ;; | ||
| 683 | always|on-failure|no) | ||
| 684 | restart_policy="$autostart_policy" | ||
| 685 | ;; | ||
| 686 | *) | ||
| 687 | bbwarn "Unknown restart policy '$autostart_policy' for $container_name, using 'unless-stopped'" | ||
| 688 | restart_policy="unless-stopped" | ||
| 689 | ;; | ||
| 690 | esac | ||
| 691 | |||
| 692 | # Extract image name and tag from OCI directory | ||
| 693 | extract_container_info "${DEPLOY_DIR_IMAGE}/$container_name" | ||
| 694 | |||
| 695 | # Generate service name | ||
| 696 | local service_name="container-$(sanitize_service_name "${CONTAINER_IMAGE_NAME}")" | ||
| 697 | |||
| 698 | bbnote "Creating autostart service for $container_name ($runtime_type, restart=$restart_policy)" | ||
| 699 | |||
| 700 | if [ "$runtime_type" = "docker" ]; then | ||
| 701 | generate_docker_service "$service_name" "${CONTAINER_IMAGE_NAME}" "${CONTAINER_IMAGE_TAG}" "$restart_policy" | ||
| 702 | elif [ "$runtime_type" = "podman" ]; then | ||
| 703 | generate_podman_service "$service_name" "${CONTAINER_IMAGE_NAME}" "${CONTAINER_IMAGE_TAG}" "$restart_policy" | ||
| 704 | else | ||
| 705 | bbwarn "Unknown runtime '$runtime_type' for autostart, skipping service generation" | ||
| 706 | fi | ||
| 707 | done | ||
| 708 | } | ||
| 709 | |||
| 710 | # ======================================================================== | ||
| 711 | # End helper functions | ||
| 712 | # ======================================================================== | ||
| 713 | |||
| 714 | if [ -z "${BUNDLED_CONTAINERS}" ]; then | ||
| 715 | bbnote "No bundled containers specified" | ||
| 716 | return 0 | ||
| 717 | fi | ||
| 718 | |||
| 719 | bbnote "Processing bundled containers: ${BUNDLED_CONTAINERS}" | ||
| 720 | bbnote "Target architecture: ${QEMU_ARCH} (blob arch: ${BLOB_ARCH})" | ||
| 721 | |||
| 722 | # Locate vrunner from vcontainer-native | ||
| 723 | VRUNNER="${VRUNNER_PATH}" | ||
| 724 | bbnote "vrunner: ${VRUNNER}" | ||
| 725 | |||
| 726 | # Verify vrunner exists | ||
| 727 | if [ ! -f "${VRUNNER}" ]; then | ||
| 728 | bbfatal "vrunner not found at ${VRUNNER}. Ensure vcontainer-native is built." | ||
| 729 | fi | ||
| 730 | |||
| 731 | # ======================================================================== | ||
| 732 | # Collect containers by runtime for batch processing | ||
| 733 | # Format: path:image:tag (as expected by vrunner --batch-import) | ||
| 734 | # ======================================================================== | ||
| 735 | DOCKER_CONTAINERS="" | ||
| 736 | PODMAN_CONTAINERS="" | ||
| 737 | |||
| 738 | for bc in ${BUNDLED_CONTAINERS}; do | ||
| 739 | # Strip :external tag if present (used for third-party blobs) | ||
| 740 | # The :external tag only affects dependency generation (in __anonymous) | ||
| 741 | bc_clean="$bc" | ||
| 742 | if echo "$bc" | grep -q ':external'; then | ||
| 743 | bc_clean=$(echo "$bc" | sed 's/:external//') | ||
| 744 | fi | ||
| 745 | |||
| 746 | container_name="$(echo $bc_clean | cut -d: -f1)" | ||
| 747 | runtime_type="$(echo $bc_clean | cut -d: -f2)" | ||
| 748 | |||
| 749 | # Default runtime to docker if not specified | ||
| 750 | if [ "$container_name" = "$runtime_type" ]; then | ||
| 751 | runtime_type="docker" | ||
| 752 | fi | ||
| 753 | |||
| 754 | bbnote "Collecting container: $container_name (runtime: $runtime_type)" | ||
| 755 | |||
| 756 | # Resolve OCI directory (supports both recipe names and OCI dir names) | ||
| 757 | oci_dir=$(resolve_oci_dir "$container_name") | ||
| 758 | if [ -z "$oci_dir" ] || [ ! -e "$oci_dir" ]; then | ||
| 759 | bbfatal "============================================================ | ||
| 760 | MISSING CONTAINER: $container_name | ||
| 761 | ============================================================ | ||
| 762 | OCI directory not found for '$container_name' | ||
| 763 | |||
| 764 | Searched for: | ||
| 765 | ${DEPLOY_DIR_IMAGE}/${container_name}-latest-oci | ||
| 766 | ${DEPLOY_DIR_IMAGE}/${container_name}-oci | ||
| 767 | ${DEPLOY_DIR_IMAGE}/${container_name} | ||
| 768 | |||
| 769 | To fix, build the container for this machine: | ||
| 770 | MACHINE=${MACHINE} bitbake ${container_name} | ||
| 771 | |||
| 772 | Or remove it from BUNDLED_CONTAINERS if not needed. | ||
| 773 | ============================================================" | ||
| 774 | fi | ||
| 775 | |||
| 776 | # Extract image name and tag from OCI directory name | ||
| 777 | extract_container_info "$oci_dir" | ||
| 778 | BATCH_ENTRY="${oci_dir}:${CONTAINER_IMAGE_NAME}:${CONTAINER_IMAGE_TAG}" | ||
| 779 | |||
| 780 | if [ "$runtime_type" = "docker" ]; then | ||
| 781 | DOCKER_CONTAINERS="${DOCKER_CONTAINERS} ${BATCH_ENTRY}" | ||
| 782 | elif [ "$runtime_type" = "podman" ]; then | ||
| 783 | PODMAN_CONTAINERS="${PODMAN_CONTAINERS} ${BATCH_ENTRY}" | ||
| 784 | else | ||
| 785 | bbwarn "Unknown runtime type: $runtime_type for $container_name, skipping" | ||
| 786 | fi | ||
| 787 | done | ||
| 788 | |||
| 789 | # ======================================================================== | ||
| 790 | # Process Docker containers (batch import) | ||
| 791 | # ======================================================================== | ||
| 792 | if [ -n "${DOCKER_CONTAINERS}" ]; then | ||
| 793 | bbnote "Processing Docker containers: ${DOCKER_CONTAINERS}" | ||
| 794 | |||
| 795 | DOCKER_STORAGE_TAR="${WORKDIR}/docker-storage-$$.tar" | ||
| 796 | DOCKER_OUTPUT_DIR="${IMAGE_ROOTFS}/var/lib/docker" | ||
| 797 | |||
| 798 | # Verify Docker blobs exist | ||
| 799 | if [ ! -d "${VDKR_BLOB_DIR}" ]; then | ||
| 800 | bbfatal "Docker blob directory not found at ${VDKR_BLOB_DIR}. Build with: bitbake vdkr-initramfs-create" | ||
| 801 | fi | ||
| 802 | |||
| 803 | # Check for existing Docker storage in rootfs (additive support) | ||
| 804 | EXISTING_DOCKER_STORAGE="" | ||
| 805 | if [ -d "${DOCKER_OUTPUT_DIR}" ] && [ -n "$(ls -A ${DOCKER_OUTPUT_DIR} 2>/dev/null)" ]; then | ||
| 806 | bbnote "Found existing Docker storage, will merge additively" | ||
| 807 | EXISTING_DOCKER_STORAGE="${WORKDIR}/existing-docker-$$.tar" | ||
| 808 | tar -cf "${EXISTING_DOCKER_STORAGE}" -C "${IMAGE_ROOTFS}/var/lib" docker | ||
| 809 | fi | ||
| 810 | |||
| 811 | # Build vrunner batch-import command | ||
| 812 | VRUNNER_CMD="${VRUNNER} \ | ||
| 813 | --runtime docker \ | ||
| 814 | --arch ${BLOB_ARCH} \ | ||
| 815 | --blob-dir ${VDKR_BLOB_DIR} \ | ||
| 816 | --batch-import \ | ||
| 817 | --output ${DOCKER_STORAGE_TAR} \ | ||
| 818 | --verbose" | ||
| 819 | |||
| 820 | if [ -n "${EXISTING_DOCKER_STORAGE}" ]; then | ||
| 821 | VRUNNER_CMD="${VRUNNER_CMD} --input-storage ${EXISTING_DOCKER_STORAGE}" | ||
| 822 | fi | ||
| 823 | |||
| 824 | VRUNNER_CMD="${VRUNNER_CMD} -- ${DOCKER_CONTAINERS}" | ||
| 825 | |||
| 826 | bbnote "Running batch import for Docker containers..." | ||
| 827 | TMPDIR="${WORKDIR}" eval ${VRUNNER_CMD} | ||
| 828 | |||
| 829 | if [ $? -ne 0 ]; then | ||
| 830 | bbfatal "Docker batch import failed" | ||
| 831 | fi | ||
| 832 | |||
| 833 | # Simple tar extraction - no merger needed! | ||
| 834 | # The storage tar has correct structure with docker/ at root | ||
| 835 | if [ -f "${DOCKER_STORAGE_TAR}" ]; then | ||
| 836 | bbnote "Extracting Docker storage to rootfs..." | ||
| 837 | mkdir -p "${DOCKER_OUTPUT_DIR}" | ||
| 838 | # Extract with --strip-components=1 to remove the 'docker' prefix | ||
| 839 | # since we're extracting directly into /var/lib/docker | ||
| 840 | tar -xf "${DOCKER_STORAGE_TAR}" -C "${IMAGE_ROOTFS}/var/lib" --no-same-owner | ||
| 841 | bbnote "Docker storage extracted successfully" | ||
| 842 | fi | ||
| 843 | |||
| 844 | rm -f "${EXISTING_DOCKER_STORAGE}" | ||
| 845 | fi | ||
| 846 | |||
| 847 | # ======================================================================== | ||
| 848 | # Process Podman containers (batch import) | ||
| 849 | # ======================================================================== | ||
| 850 | if [ -n "${PODMAN_CONTAINERS}" ]; then | ||
| 851 | bbnote "Processing Podman containers: ${PODMAN_CONTAINERS}" | ||
| 852 | |||
| 853 | PODMAN_STORAGE_TAR="${WORKDIR}/podman-storage-$$.tar" | ||
| 854 | PODMAN_OUTPUT_DIR="${IMAGE_ROOTFS}/var/lib/containers/storage" | ||
| 855 | |||
| 856 | # Verify Podman blobs exist | ||
| 857 | if [ ! -d "${VPDMN_BLOB_DIR}" ]; then | ||
| 858 | bbfatal "Podman blob directory not found at ${VPDMN_BLOB_DIR}. Build with: bitbake vpdmn-initramfs-create" | ||
| 859 | fi | ||
| 860 | |||
| 861 | # Check for existing Podman storage in rootfs (additive support) | ||
| 862 | EXISTING_PODMAN_STORAGE="" | ||
| 863 | if [ -d "${PODMAN_OUTPUT_DIR}" ] && [ -n "$(ls -A ${PODMAN_OUTPUT_DIR} 2>/dev/null)" ]; then | ||
| 864 | bbnote "Found existing Podman storage, will merge additively" | ||
| 865 | EXISTING_PODMAN_STORAGE="${WORKDIR}/existing-podman-$$.tar" | ||
| 866 | tar -cf "${EXISTING_PODMAN_STORAGE}" -C "${IMAGE_ROOTFS}/var/lib/containers" storage | ||
| 867 | fi | ||
| 868 | |||
| 869 | # Build vrunner batch-import command | ||
| 870 | VRUNNER_CMD="${VRUNNER} \ | ||
| 871 | --runtime podman \ | ||
| 872 | --arch ${BLOB_ARCH} \ | ||
| 873 | --blob-dir ${VPDMN_BLOB_DIR} \ | ||
| 874 | --batch-import \ | ||
| 875 | --output ${PODMAN_STORAGE_TAR} \ | ||
| 876 | --verbose" | ||
| 877 | |||
| 878 | if [ -n "${EXISTING_PODMAN_STORAGE}" ]; then | ||
| 879 | VRUNNER_CMD="${VRUNNER_CMD} --input-storage ${EXISTING_PODMAN_STORAGE}" | ||
| 880 | fi | ||
| 881 | |||
| 882 | VRUNNER_CMD="${VRUNNER_CMD} -- ${PODMAN_CONTAINERS}" | ||
| 883 | |||
| 884 | bbnote "Running batch import for Podman containers..." | ||
| 885 | TMPDIR="${WORKDIR}" eval ${VRUNNER_CMD} | ||
| 886 | |||
| 887 | if [ $? -ne 0 ]; then | ||
| 888 | bbfatal "Podman batch import failed" | ||
| 889 | fi | ||
| 890 | |||
| 891 | # Simple tar extraction - no merger needed! | ||
| 892 | if [ -f "${PODMAN_STORAGE_TAR}" ]; then | ||
| 893 | bbnote "Extracting Podman storage to rootfs..." | ||
| 894 | mkdir -p "${PODMAN_OUTPUT_DIR}" | ||
| 895 | tar -xf "${PODMAN_STORAGE_TAR}" -C "${PODMAN_OUTPUT_DIR}" --no-same-owner | ||
| 896 | bbnote "Podman storage extracted successfully" | ||
| 897 | fi | ||
| 898 | |||
| 899 | rm -f "${EXISTING_PODMAN_STORAGE}" | ||
| 900 | fi | ||
| 901 | |||
| 902 | # ======================================================================== | ||
| 903 | # Install autostart services | ||
| 904 | # ======================================================================== | ||
| 905 | install_autostart_services | ||
| 906 | |||
| 907 | bbnote "Done processing all bundled containers" | ||
| 908 | } | ||
| 909 | |||
| 910 | # First merge any bundles installed via IMAGE_INSTALL, then process BUNDLED_CONTAINERS | ||
| 911 | ROOTFS_POSTPROCESS_COMMAND += "merge_installed_bundles; bundle_containers;" | ||
