summaryrefslogtreecommitdiffstats
path: root/classes
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-14 20:58:34 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-01-21 18:00:26 -0500
commit24c604854c6ffe79ac7973e333b2df7f7f82ddd9 (patch)
tree95ec7fcdd9f1ac87977ab1d60773ba5178d7a210 /classes
parentf83a83eb3979e3bc671190650731acf8a5b9ecd3 (diff)
downloadmeta-virtualization-24c604854c6ffe79ac7973e333b2df7f7f82ddd9.tar.gz
image-oci: add multi-layer OCI image support with OCI_LAYERS
Add support for creating multi-layer OCI images with explicit layer definitions via OCI_LAYERS variable. This enables fine-grained control over container layer composition. New variables: - OCI_LAYER_MODE: Set to "multi" for explicit layer definitions - OCI_LAYERS: Define layers as "name:type:content" entries - packages: Install specific packages in a layer - directories: Copy directories from IMAGE_ROOTFS - files: Copy specific files from IMAGE_ROOTFS Package installation uses Yocto's package manager classes (RpmPM, OpkgPM) for consistency with do_rootfs, rather than calling dnf/opkg directly. Example usage: OCI_LAYER_MODE = "multi" OCI_LAYERS = "\ base:packages:base-files+base-passwd+netbase \ shell:packages:busybox \ app:packages:curl \ " This creates a 3-layer OCI image with discrete base, shell, and app layers that can be shared and cached independently. Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
-rw-r--r--classes/image-oci-umoci.inc190
-rw-r--r--classes/image-oci.bbclass198
2 files changed, 365 insertions, 23 deletions
diff --git a/classes/image-oci-umoci.inc b/classes/image-oci-umoci.inc
index 340f298b..1ff36718 100644
--- a/classes/image-oci-umoci.inc
+++ b/classes/image-oci-umoci.inc
@@ -1,3 +1,69 @@
1# =============================================================================
2# Python function to pre-install packages for multi-layer OCI
3# =============================================================================
4# This function runs before IMAGE_CMD:oci and installs packages to temp rootfs
5# directories using Yocto's package manager classes. The shell code then copies
6# from these pre-installed directories.
7
8python oci_multilayer_install_packages() {
9 """
10 Pre-install packages for each packages layer in OCI_LAYERS.
11
12 Creates temp rootfs directories with packages installed using Yocto's PM.
13 The shell IMAGE_CMD:oci then copies from these directories.
14 """
15 import os
16 import shutil
17
18 layer_mode = d.getVar('OCI_LAYER_MODE') or 'single'
19 if layer_mode != 'multi':
20 bb.debug(1, "OCI: Not in multi-layer mode, skipping pre-install")
21 return
22
23 oci_layers = d.getVar('OCI_LAYERS') or ''
24 if not oci_layers.strip():
25 return
26
27 workdir = d.getVar('WORKDIR')
28 layer_rootfs_base = os.path.join(workdir, 'oci-layer-rootfs')
29
30 # Clean up any previous layer rootfs directories
31 if os.path.exists(layer_rootfs_base):
32 shutil.rmtree(layer_rootfs_base)
33 bb.utils.mkdirhier(layer_rootfs_base)
34
35 bb.note("OCI: Pre-installing packages for multi-layer mode")
36
37 # Parse OCI_LAYERS and install packages for each packages layer
38 layer_num = 0
39 for layer_def in oci_layers.split():
40 parts = layer_def.split(':')
41 if len(parts) < 3:
42 continue
43 layer_name = parts[0]
44 layer_type = parts[1]
45 layer_content = ':'.join(parts[2:]).replace('+', ' ')
46
47 if layer_type == 'packages':
48 layer_num += 1
49 layer_rootfs = os.path.join(layer_rootfs_base, f'layer-{layer_num}-{layer_name}')
50
51 bb.note(f"OCI: Pre-installing layer {layer_num} '{layer_name}' to {layer_rootfs}")
52
53 # Call the package installation function
54 oci_install_layer_packages(d, layer_rootfs, layer_content, layer_name)
55
56 # Store the path for the shell code to use
57 d.setVar(f'OCI_LAYER_{layer_num}_ROOTFS', layer_rootfs)
58 d.setVar(f'OCI_LAYER_{layer_num}_NAME', layer_name)
59
60 d.setVar('OCI_LAYER_COUNT', str(layer_num))
61 bb.note(f"OCI: Pre-installed packages for {layer_num} layers")
62}
63
64# Run the Python function before IMAGE_CMD:oci
65do_image_oci[prefuncs] += "oci_multilayer_install_packages"
66
1# Fix merged-usr whiteout issues in OCI layer 67# Fix merged-usr whiteout issues in OCI layer
2# When a directory becomes a symlink, umoci creates whiteouts inside it, but 68# When a directory becomes a symlink, umoci creates whiteouts inside it, but
3# puts them after the symlink in the tar. Docker fails because it can't create 69# puts them after the symlink in the tar. Docker fails because it can't create
@@ -304,35 +370,113 @@ IMAGE_CMD:oci() {
304 # ======================================================================== 370 # ========================================================================
305 bbdebug 1 "OCI: populating rootfs" 371 bbdebug 1 "OCI: populating rootfs"
306 372
307 # Use rsync for robust merging when base image exists (handles symlink vs dir conflicts)
308 # For no-base builds, cp is sufficient and faster
309 # Note: When source has symlinks replacing dest directories, we first remove conflicting dirs
310 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then
311 # Handle Yocto's merged-usr symlinks (/bin -> /usr/bin) and /var symlinks
312 # replacing Alpine's or other base image directories
313 for p in bin lib lib64 sbin var/lock var/log var/tmp; do
314 src="${IMAGE_ROOTFS}/$p"
315 dst="$image_bundle_name/rootfs/$p"
316 if [ -L "$src" ] && [ -d "$dst" ] && [ ! -L "$dst" ]; then
317 bbdebug 1 "OCI: removing directory $dst to replace with symlink"
318 rm -rf "$dst"
319 fi
320 done
321 bbdebug 1 "OCI: rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/"
322 rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/
323 else
324 bbdebug 1 "OCI: cp -r ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs/"
325 cp -r -a --no-preserve=ownership ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs
326 fi
327
328 # Determine which tag to use for repack 373 # Determine which tag to use for repack
329 repack_tag="${OCI_IMAGE_TAG}" 374 repack_tag="${OCI_IMAGE_TAG}"
330 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then 375 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then
331 repack_tag="${OCI_BASE_IMAGE_TAG}" 376 repack_tag="${OCI_BASE_IMAGE_TAG}"
332 fi 377 fi
333 378
334 bbdebug 1 "OCI: umoci repack --image $image_name:$repack_tag $image_bundle_name" 379 if [ "${OCI_LAYER_MODE}" = "multi" ]; then
335 umoci repack --image $image_name:$repack_tag $image_bundle_name 380 # ==================================================================
381 # Multi-layer mode: Use pre-installed layer rootfs from Python
382 # ==================================================================
383 # The Python prefunc oci_multilayer_install_packages() has already
384 # installed packages to temp rootfs directories using Yocto's PM classes.
385 # We just need to copy from those directories and repack each layer.
386
387 bbnote "OCI: Using multi-layer mode (packages pre-installed by Python PM classes)"
388
389 # Process each layer from OCI_LAYERS
390 oci_layer_num=0
391 oci_pkg_layer_num=0
392 oci_total_layers=0
393 for oci_tmp in ${OCI_LAYERS}; do
394 oci_total_layers=`expr $oci_total_layers + 1`
395 done
396
397 for oci_layer_def in ${OCI_LAYERS}; do
398 oci_layer_num=`expr $oci_layer_num + 1`
399 oci_layer_name=$(echo "$oci_layer_def" | cut -d: -f1)
400 oci_layer_type=$(echo "$oci_layer_def" | cut -d: -f2)
401 oci_layer_content=$(echo "$oci_layer_def" | cut -d: -f3- | tr '+' ' ')
402
403 bbnote "OCI: Processing layer $oci_layer_num/$oci_total_layers: $oci_layer_name ($oci_layer_type)"
404
405 if [ "$oci_layer_type" = "packages" ]; then
406 # Packages were pre-installed by Python. Copy from temp rootfs.
407 oci_pkg_layer_num=`expr $oci_pkg_layer_num + 1`
408 oci_preinstall_rootfs="${WORKDIR}/oci-layer-rootfs/layer-${oci_pkg_layer_num}-${oci_layer_name}"
409
410 if [ -d "$oci_preinstall_rootfs" ]; then
411 bbnote "OCI: Copying pre-installed packages from $oci_preinstall_rootfs"
412 # Use rsync to merge into bundle rootfs (handles symlinks properly)
413 rsync -a --no-owner --no-group "$oci_preinstall_rootfs/" "$image_bundle_name/rootfs/"
414 else
415 bbwarn "OCI: Pre-installed rootfs not found at $oci_preinstall_rootfs"
416 fi
417
418 elif [ "$oci_layer_type" = "directories" ]; then
419 # Copy directories from IMAGE_ROOTFS
420 for oci_dir in $oci_layer_content; do
421 if [ -d "${IMAGE_ROOTFS}$oci_dir" ]; then
422 mkdir -p "$image_bundle_name/rootfs$(dirname $oci_dir)"
423 cp -a "${IMAGE_ROOTFS}$oci_dir" "$image_bundle_name/rootfs$oci_dir"
424 bbnote "OCI: Added directory $oci_dir"
425 fi
426 done
427
428 elif [ "$oci_layer_type" = "files" ]; then
429 # Copy specific files from IMAGE_ROOTFS
430 for oci_file in $oci_layer_content; do
431 if [ -e "${IMAGE_ROOTFS}$oci_file" ]; then
432 mkdir -p "$image_bundle_name/rootfs$(dirname $oci_file)"
433 cp -a "${IMAGE_ROOTFS}$oci_file" "$image_bundle_name/rootfs$oci_file"
434 bbnote "OCI: Added file $oci_file"
435 fi
436 done
437 fi
438
439 # Repack to create layer
440 bbnote "OCI: Repacking layer $oci_layer_name"
441 umoci repack --image "$image_name:$repack_tag" "$image_bundle_name"
442
443 # Re-unpack for next layer if not the last one
444 if [ "$oci_layer_num" -lt "$oci_total_layers" ]; then
445 rm -rf "$image_bundle_name"
446 umoci unpack --rootless --image "$image_name:$repack_tag" "$image_bundle_name"
447 fi
448 done
449
450 bbnote "OCI: Created $oci_layer_num layers"
451
452 else
453 # ==================================================================
454 # Single-layer mode: Copy entire rootfs as one layer
455 # ==================================================================
456 # Use rsync for robust merging when base image exists (handles symlink vs dir conflicts)
457 # For no-base builds, cp is sufficient and faster
458 # Note: When source has symlinks replacing dest directories, we first remove conflicting dirs
459 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then
460 # Handle Yocto's merged-usr symlinks (/bin -> /usr/bin) and /var symlinks
461 # replacing Alpine's or other base image directories
462 for p in bin lib lib64 sbin var/lock var/log var/tmp; do
463 src="${IMAGE_ROOTFS}/$p"
464 dst="$image_bundle_name/rootfs/$p"
465 if [ -L "$src" ] && [ -d "$dst" ] && [ ! -L "$dst" ]; then
466 bbdebug 1 "OCI: removing directory $dst to replace with symlink"
467 rm -rf "$dst"
468 fi
469 done
470 bbdebug 1 "OCI: rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/"
471 rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/
472 else
473 bbdebug 1 "OCI: cp -r ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs/"
474 cp -r -a --no-preserve=ownership ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs
475 fi
476
477 bbdebug 1 "OCI: umoci repack --image $image_name:$repack_tag $image_bundle_name"
478 umoci repack --image $image_name:$repack_tag $image_bundle_name
479 fi
336 480
337 # If we used a base image with different tag, re-tag to our target tag 481 # If we used a base image with different tag, re-tag to our target tag
338 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then 482 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then
diff --git a/classes/image-oci.bbclass b/classes/image-oci.bbclass
index 6f8011ca..64b17d97 100644
--- a/classes/image-oci.bbclass
+++ b/classes/image-oci.bbclass
@@ -44,6 +44,26 @@ OCI_IMAGE_BACKEND ?= "umoci"
44do_image_oci[depends] += "${OCI_IMAGE_BACKEND}-native:do_populate_sysroot" 44do_image_oci[depends] += "${OCI_IMAGE_BACKEND}-native:do_populate_sysroot"
45# jq-native is needed for the merged-usr whiteout fix 45# jq-native is needed for the merged-usr whiteout fix
46do_image_oci[depends] += "jq-native:do_populate_sysroot" 46do_image_oci[depends] += "jq-native:do_populate_sysroot"
47# Package manager native tools for multi-layer mode with package installation
48OCI_PM_DEPENDS = "${@oci_get_pm_depends(d)}"
49do_image_oci[depends] += "${OCI_PM_DEPENDS}"
50
51def oci_get_pm_depends(d):
52 """Get native package manager dependency for multi-layer mode."""
53 if d.getVar('OCI_LAYER_MODE') != 'multi':
54 return ''
55 if 'packages' not in (d.getVar('OCI_LAYERS') or ''):
56 return ''
57 # rsync-native is needed to copy pre-installed packages to bundle rootfs
58 deps = 'rsync-native:do_populate_sysroot'
59 pkg_type = d.getVar('IMAGE_PKGTYPE') or 'rpm'
60 if pkg_type == 'rpm':
61 deps += ' dnf-native:do_populate_sysroot createrepo-c-native:do_populate_sysroot'
62 elif pkg_type == 'ipk':
63 deps += ' opkg-native:do_populate_sysroot'
64 elif pkg_type == 'deb':
65 deps += ' apt-native:do_populate_sysroot'
66 return deps
47 67
48# 68#
49# image type configuration block 69# image type configuration block
@@ -139,6 +159,42 @@ OCI_BASE_IMAGE ?= ""
139OCI_BASE_IMAGE_TAG ?= "latest" 159OCI_BASE_IMAGE_TAG ?= "latest"
140OCI_LAYER_MODE ?= "single" 160OCI_LAYER_MODE ?= "single"
141 161
162# =============================================================================
163# Multi-Layer Mode (OCI_LAYER_MODE = "multi")
164# =============================================================================
165#
166# OCI_LAYERS defines explicit layers when OCI_LAYER_MODE = "multi".
167# Each layer is defined as: "name:type:content"
168#
169# Layer Types:
170# packages - Copy files installed by specified packages
171# directories - Copy specific directories from IMAGE_ROOTFS
172# files - Copy specific files from IMAGE_ROOTFS
173#
174# Format: Space-separated list of layer definitions
175# OCI_LAYERS = "layer1:type:content layer2:type:content ..."
176#
177# For packages type, content is package names (use + as delimiter):
178# "base:packages:base-files+busybox+netbase"
179#
180# For directories/files type, content is paths (use + as delimiter):
181# "app:directories:/opt/myapp+/etc/myapp"
182# "config:files:/etc/myapp.conf+/etc/default/myapp"
183#
184# Note: Use + as delimiter because ; is interpreted as shell command separator
185#
186# Example:
187# OCI_LAYER_MODE = "multi"
188# OCI_LAYERS = "\
189# base:packages:base-files+base-passwd+netbase+busybox \
190# python:packages:python3+python3-pip \
191# app:directories:/opt/myapp \
192# "
193#
194# Result: 3 layers (base, python, app) plus any base image layers
195#
196OCI_LAYERS ?= ""
197
142# whether the oci image dir should be left as a directory, or 198# whether the oci image dir should be left as a directory, or
143# bundled into a tarball. 199# bundled into a tarball.
144OCI_IMAGE_TAR_OUTPUT ?= "true" 200OCI_IMAGE_TAR_OUTPUT ?= "true"
@@ -199,6 +255,59 @@ python __anonymous() {
199 bb.fatal("Multi-layer OCI requires umoci backend. " 255 bb.fatal("Multi-layer OCI requires umoci backend. "
200 "Set OCI_IMAGE_BACKEND = 'umoci' or remove OCI_BASE_IMAGE") 256 "Set OCI_IMAGE_BACKEND = 'umoci' or remove OCI_BASE_IMAGE")
201 257
258 # Validate multi-layer mode configuration and add dependencies
259 if layer_mode == 'multi':
260 oci_layers = d.getVar('OCI_LAYERS') or ''
261 if not oci_layers.strip():
262 bb.fatal("OCI_LAYER_MODE = 'multi' requires OCI_LAYERS to be defined")
263
264 has_packages_layer = False
265
266 # Parse and validate layer definitions
267 for layer_def in oci_layers.split():
268 parts = layer_def.split(':')
269 if len(parts) < 3:
270 bb.fatal(f"Invalid OCI_LAYERS entry '{layer_def}': "
271 "format is 'name:type:content'")
272 layer_name, layer_type, layer_content = parts[0], parts[1], ':'.join(parts[2:])
273 if layer_type not in ('packages', 'directories', 'files'):
274 bb.fatal(f"Invalid layer type '{layer_type}' in '{layer_def}': "
275 "must be 'packages', 'directories', or 'files'")
276 if layer_type == 'packages':
277 has_packages_layer = True
278
279 # Add package manager native dependency if using 'packages' layer type
280 if has_packages_layer:
281 pkg_type = d.getVar('IMAGE_PKGTYPE') or 'ipk'
282 if pkg_type == 'ipk':
283 d.appendVarFlag('do_image_oci', 'depends',
284 " opkg-native:do_populate_sysroot opkg-utils-native:do_populate_sysroot")
285 bb.debug(1, "OCI: Added opkg-native dependency for packages layers")
286 elif pkg_type == 'rpm':
287 d.appendVarFlag('do_image_oci', 'depends',
288 " dnf-native:do_populate_sysroot")
289 bb.debug(1, "OCI: Added dnf-native dependency for packages layers")
290 elif pkg_type == 'deb':
291 d.appendVarFlag('do_image_oci', 'depends',
292 " apt-native:do_populate_sysroot")
293 bb.debug(1, "OCI: Added apt-native dependency for packages layers")
294
295 # Extract all packages from OCI_LAYERS and add do_package_write dependencies
296 # This allows IMAGE_INSTALL = "" for pure multi-layer builds
297 all_packages = set()
298 for layer_def in oci_layers.split():
299 parts = layer_def.split(':')
300 if len(parts) >= 3 and parts[1] == 'packages':
301 layer_content = ':'.join(parts[2:])
302 # Use + as delimiter (not ; which is shell command separator)
303 for pkg in layer_content.replace('+', ' ').split():
304 all_packages.add(pkg)
305
306 if all_packages:
307 # Note: Packages need to be in IMAGE_INSTALL to trigger builds
308 # via do_rootfs recrdeptask. We just log which packages we found.
309 bb.debug(1, f"OCI multi-layer: Found packages in OCI_LAYERS: {' '.join(all_packages)}")
310
202 # Resolve base image and set up dependencies 311 # Resolve base image and set up dependencies
203 if base_image: 312 if base_image:
204 resolved = oci_resolve_base_image(d) 313 resolved = oci_resolve_base_image(d)
@@ -234,6 +343,95 @@ python __anonymous() {
234 f"Then use: OCI_BASE_IMAGE = \"my-base\"") 343 f"Then use: OCI_BASE_IMAGE = \"my-base\"")
235} 344}
236 345
346# =============================================================================
347# Multi-Layer Package Installation using Yocto's PM Classes
348# =============================================================================
349#
350# This function uses the same package management infrastructure as do_rootfs,
351# ensuring consistency and maintainability.
352
353def oci_install_layer_packages(d, layer_rootfs, layer_packages, layer_name):
354 """
355 Install packages to a layer rootfs using Yocto's package manager classes.
356
357 This uses the same PM infrastructure as do_rootfs for consistency.
358
359 Args:
360 d: BitBake datastore
361 layer_rootfs: Path to the layer's rootfs directory
362 layer_packages: Space-separated list of packages to install
363 layer_name: Name of the layer (for logging)
364 """
365 import os
366 import oe.path
367
368 packages = layer_packages.split()
369 if not packages:
370 bb.note(f"OCI: No packages to install for layer {layer_name}")
371 return
372
373 bb.note(f"OCI: Installing packages for layer '{layer_name}': {' '.join(packages)}")
374
375 pkg_type = d.getVar('IMAGE_PKGTYPE') or 'rpm'
376
377 # Ensure layer rootfs directory exists
378 bb.utils.mkdirhier(layer_rootfs)
379
380 if pkg_type == 'rpm':
381 from oe.package_manager.rpm import RpmPM
382
383 # Create PM instance for layer rootfs
384 pm = RpmPM(d,
385 layer_rootfs,
386 d.getVar('TARGET_VENDOR'),
387 task_name='oci-layer',
388 filterbydependencies=False)
389
390 # Setup configs in layer rootfs
391 pm.create_configs()
392
393 # Generate/update repo indexes
394 pm.write_index()
395
396 # Install packages
397 # Use attempt_only=True to allow unresolved deps (resolved in later layers)
398 try:
399 pm.install(packages, attempt_only=True)
400 except Exception as e:
401 bb.warn(f"OCI: Package installation had issues (may be resolved in later layers): {e}")
402
403 elif pkg_type == 'ipk':
404 from oe.package_manager.ipk import OpkgPM
405
406 # Create config file for this layer
407 config_file = os.path.join(d.getVar('WORKDIR'), f'opkg-{layer_name}.conf')
408 archs = d.getVar('PACKAGE_ARCHS')
409
410 # Create PM instance
411 pm = OpkgPM(d,
412 layer_rootfs,
413 config_file,
414 archs,
415 task_name='oci-layer',
416 filterbydependencies=False)
417
418 # Write indexes
419 pm.write_index()
420
421 # Install packages
422 try:
423 pm.install(packages, attempt_only=True)
424 except Exception as e:
425 bb.warn(f"OCI: Package installation had issues (may be resolved in later layers): {e}")
426
427 elif pkg_type == 'deb':
428 bb.warn("OCI: deb package type not yet fully implemented for multi-layer")
429
430 else:
431 bb.fatal(f"OCI: Unsupported package type: {pkg_type}")
432
433 bb.note(f"OCI: Package installation complete for layer '{layer_name}'")
434
237# the IMAGE_CMD:oci comes from the .inc 435# the IMAGE_CMD:oci comes from the .inc
238OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}" 436OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}"
239include ${OCI_IMAGE_BACKEND_INC} 437include ${OCI_IMAGE_BACKEND_INC}