summaryrefslogtreecommitdiffstats
path: root/classes/image-oci.bbclass
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-02-09 03:32:52 +0000
commit4fd9190b7f2f7260b90c7de1609944c96fcf6f64 (patch)
tree1f7268a586df34642a9b0562a9abaa3c1b97f35c /classes/image-oci.bbclass
parent4ddc9e489307a31b25600b9073edb09110740fb8 (diff)
downloadmeta-virtualization-4fd9190b7f2f7260b90c7de1609944c96fcf6f64.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/image-oci.bbclass')
-rw-r--r--classes/image-oci.bbclass198
1 files changed, 198 insertions, 0 deletions
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}