diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-14 20:58:34 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:32:52 +0000 |
| commit | 4fd9190b7f2f7260b90c7de1609944c96fcf6f64 (patch) | |
| tree | 1f7268a586df34642a9b0562a9abaa3c1b97f35c /classes/image-oci.bbclass | |
| parent | 4ddc9e489307a31b25600b9073edb09110740fb8 (diff) | |
| download | meta-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.bbclass | 198 |
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" | |||
| 44 | do_image_oci[depends] += "${OCI_IMAGE_BACKEND}-native:do_populate_sysroot" | 44 | do_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 |
| 46 | do_image_oci[depends] += "jq-native:do_populate_sysroot" | 46 | do_image_oci[depends] += "jq-native:do_populate_sysroot" |
| 47 | # Package manager native tools for multi-layer mode with package installation | ||
| 48 | OCI_PM_DEPENDS = "${@oci_get_pm_depends(d)}" | ||
| 49 | do_image_oci[depends] += "${OCI_PM_DEPENDS}" | ||
| 50 | |||
| 51 | def 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 ?= "" | |||
| 139 | OCI_BASE_IMAGE_TAG ?= "latest" | 159 | OCI_BASE_IMAGE_TAG ?= "latest" |
| 140 | OCI_LAYER_MODE ?= "single" | 160 | OCI_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 | # | ||
| 196 | OCI_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. |
| 144 | OCI_IMAGE_TAR_OUTPUT ?= "true" | 200 | OCI_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 | |||
| 353 | def 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 |
| 238 | OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}" | 436 | OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}" |
| 239 | include ${OCI_IMAGE_BACKEND_INC} | 437 | include ${OCI_IMAGE_BACKEND_INC} |
