summaryrefslogtreecommitdiffstats
path: root/classes/container-cross-install.bbclass
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-01 17:08:35 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:32:52 +0000
commitc32e1081c81ba27f0d5a21a1885601f04d329d21 (patch)
treea7e44880e69068d44e7e89d49af8ce709784a03f /classes/container-cross-install.bbclass
parent79d03d5350c446223c847135c7115a656adc01d9 (diff)
downloadmeta-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.bbclass911
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)
106DEPENDS += "qemuwrapper-cross qemu-system-native skopeo-native"
107DEPENDS += "vcontainer-native coreutils-native"
108
109# Determine multiconfig name for blob building based on target architecture
110def 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)
120def 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
129VRUNTIME_MULTICONFIG = "${@get_vruntime_multiconfig(d)}"
130VRUNTIME_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
135do_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
142python __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
174VRUNNER_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/)
180VDKR_BLOB_DIR = "${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/vdkr"
181VPDMN_BLOB_DIR = "${TOPDIR}/tmp-${VRUNTIME_MULTICONFIG}/deploy/images/${VRUNTIME_MACHINE}/vpdmn"
182
183bundle_containers[network] = "1"
184do_testsdkext[nostamp] = "1"
185
186# Map TARGET_ARCH to QEMU architecture names
187def 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
199QEMU_ARCH = "${@get_qemu_arch(d)}"
200
201# Map TARGET_ARCH to kernel image name
202def 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
214KERNEL_IMAGETYPE_QEMU = "${@get_kernel_name(d)}"
215
216# Map TARGET_ARCH to blob directory name (aarch64, x86_64)
217def 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
229BLOB_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# ============================================================================
235merge_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)
437generate_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]
450Description=Docker Container ${image_name}:${image_tag}
451After=docker.service
452Requires=docker.service
453
454[Service]
455Type=simple
456Restart=${restart_policy}
457RestartSec=5s
458TimeoutStartSec=0
459ExecStartPre=-/usr/bin/docker rm -f ${image_name}
460ExecStart=/usr/bin/docker run --rm --name ${image_name} ${image_name}:${image_tag}
461ExecStop=/usr/bin/docker stop ${image_name}
462
463[Install]
464WantedBy=multi-user.target
465EOF
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)
475generate_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]
491Description=Podman Container ${image_name}:${image_tag}
492
493[Container]
494Image=${image_name}:${image_tag}
495ContainerName=${image_name}
496
497[Service]
498Restart=${restart_policy}
499RestartSec=5s
500
501[Install]
502WantedBy=multi-user.target
503EOF
504
505 bbnote "Created Quadlet file ${service_name}.container for Podman container ${image_name}:${image_tag}"
506}
507
508bundle_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]
590Description=Docker Container ${image_name}:${image_tag}
591After=docker.service
592Requires=docker.service
593
594[Service]
595Type=simple
596Restart=${restart_policy}
597RestartSec=5s
598TimeoutStartSec=0
599ExecStartPre=-/usr/bin/docker rm -f ${image_name}
600ExecStart=/usr/bin/docker run --rm --name ${image_name} ${image_name}:${image_tag}
601ExecStop=/usr/bin/docker stop ${image_name}
602
603[Install]
604WantedBy=multi-user.target
605EOF
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]
634Description=Podman Container ${image_name}:${image_tag}
635
636[Container]
637Image=${image_name}:${image_tag}
638ContainerName=${image_name}
639
640[Service]
641Restart=${restart_policy}
642RestartSec=5s
643
644[Install]
645WantedBy=multi-user.target
646EOF
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 "============================================================
760MISSING CONTAINER: $container_name
761============================================================
762OCI directory not found for '$container_name'
763
764Searched for:
765 ${DEPLOY_DIR_IMAGE}/${container_name}-latest-oci
766 ${DEPLOY_DIR_IMAGE}/${container_name}-oci
767 ${DEPLOY_DIR_IMAGE}/${container_name}
768
769To fix, build the container for this machine:
770 MACHINE=${MACHINE} bitbake ${container_name}
771
772Or 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
911ROOTFS_POSTPROCESS_COMMAND += "merge_installed_bundles; bundle_containers;"