From 4fd9190b7f2f7260b90c7de1609944c96fcf6f64 Mon Sep 17 00:00:00 2001 From: Bruce Ashfield Date: Wed, 14 Jan 2026 20:58:34 +0000 Subject: 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 --- classes/image-oci-umoci.inc | 190 ++++++++++++++++++++--- classes/image-oci.bbclass | 198 ++++++++++++++++++++++++ docs/container-bundling.md | 96 ++++++++++-- recipes-demo/images/app-container-multilayer.bb | 38 +++++ 4 files changed, 488 insertions(+), 34 deletions(-) create mode 100644 recipes-demo/images/app-container-multilayer.bb 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 @@ +# ============================================================================= +# Python function to pre-install packages for multi-layer OCI +# ============================================================================= +# This function runs before IMAGE_CMD:oci and installs packages to temp rootfs +# directories using Yocto's package manager classes. The shell code then copies +# from these pre-installed directories. + +python oci_multilayer_install_packages() { + """ + Pre-install packages for each packages layer in OCI_LAYERS. + + Creates temp rootfs directories with packages installed using Yocto's PM. + The shell IMAGE_CMD:oci then copies from these directories. + """ + import os + import shutil + + layer_mode = d.getVar('OCI_LAYER_MODE') or 'single' + if layer_mode != 'multi': + bb.debug(1, "OCI: Not in multi-layer mode, skipping pre-install") + return + + oci_layers = d.getVar('OCI_LAYERS') or '' + if not oci_layers.strip(): + return + + workdir = d.getVar('WORKDIR') + layer_rootfs_base = os.path.join(workdir, 'oci-layer-rootfs') + + # Clean up any previous layer rootfs directories + if os.path.exists(layer_rootfs_base): + shutil.rmtree(layer_rootfs_base) + bb.utils.mkdirhier(layer_rootfs_base) + + bb.note("OCI: Pre-installing packages for multi-layer mode") + + # Parse OCI_LAYERS and install packages for each packages layer + layer_num = 0 + for layer_def in oci_layers.split(): + parts = layer_def.split(':') + if len(parts) < 3: + continue + layer_name = parts[0] + layer_type = parts[1] + layer_content = ':'.join(parts[2:]).replace('+', ' ') + + if layer_type == 'packages': + layer_num += 1 + layer_rootfs = os.path.join(layer_rootfs_base, f'layer-{layer_num}-{layer_name}') + + bb.note(f"OCI: Pre-installing layer {layer_num} '{layer_name}' to {layer_rootfs}") + + # Call the package installation function + oci_install_layer_packages(d, layer_rootfs, layer_content, layer_name) + + # Store the path for the shell code to use + d.setVar(f'OCI_LAYER_{layer_num}_ROOTFS', layer_rootfs) + d.setVar(f'OCI_LAYER_{layer_num}_NAME', layer_name) + + d.setVar('OCI_LAYER_COUNT', str(layer_num)) + bb.note(f"OCI: Pre-installed packages for {layer_num} layers") +} + +# Run the Python function before IMAGE_CMD:oci +do_image_oci[prefuncs] += "oci_multilayer_install_packages" + # Fix merged-usr whiteout issues in OCI layer # When a directory becomes a symlink, umoci creates whiteouts inside it, but # puts them after the symlink in the tar. Docker fails because it can't create @@ -304,35 +370,113 @@ IMAGE_CMD:oci() { # ======================================================================== bbdebug 1 "OCI: populating rootfs" - # Use rsync for robust merging when base image exists (handles symlink vs dir conflicts) - # For no-base builds, cp is sufficient and faster - # Note: When source has symlinks replacing dest directories, we first remove conflicting dirs - if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then - # Handle Yocto's merged-usr symlinks (/bin -> /usr/bin) and /var symlinks - # replacing Alpine's or other base image directories - for p in bin lib lib64 sbin var/lock var/log var/tmp; do - src="${IMAGE_ROOTFS}/$p" - dst="$image_bundle_name/rootfs/$p" - if [ -L "$src" ] && [ -d "$dst" ] && [ ! -L "$dst" ]; then - bbdebug 1 "OCI: removing directory $dst to replace with symlink" - rm -rf "$dst" - fi - done - bbdebug 1 "OCI: rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/" - rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/ - else - bbdebug 1 "OCI: cp -r ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs/" - cp -r -a --no-preserve=ownership ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs - fi - # Determine which tag to use for repack repack_tag="${OCI_IMAGE_TAG}" if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then repack_tag="${OCI_BASE_IMAGE_TAG}" fi - bbdebug 1 "OCI: umoci repack --image $image_name:$repack_tag $image_bundle_name" - umoci repack --image $image_name:$repack_tag $image_bundle_name + if [ "${OCI_LAYER_MODE}" = "multi" ]; then + # ================================================================== + # Multi-layer mode: Use pre-installed layer rootfs from Python + # ================================================================== + # The Python prefunc oci_multilayer_install_packages() has already + # installed packages to temp rootfs directories using Yocto's PM classes. + # We just need to copy from those directories and repack each layer. + + bbnote "OCI: Using multi-layer mode (packages pre-installed by Python PM classes)" + + # Process each layer from OCI_LAYERS + oci_layer_num=0 + oci_pkg_layer_num=0 + oci_total_layers=0 + for oci_tmp in ${OCI_LAYERS}; do + oci_total_layers=`expr $oci_total_layers + 1` + done + + for oci_layer_def in ${OCI_LAYERS}; do + oci_layer_num=`expr $oci_layer_num + 1` + oci_layer_name=$(echo "$oci_layer_def" | cut -d: -f1) + oci_layer_type=$(echo "$oci_layer_def" | cut -d: -f2) + oci_layer_content=$(echo "$oci_layer_def" | cut -d: -f3- | tr '+' ' ') + + bbnote "OCI: Processing layer $oci_layer_num/$oci_total_layers: $oci_layer_name ($oci_layer_type)" + + if [ "$oci_layer_type" = "packages" ]; then + # Packages were pre-installed by Python. Copy from temp rootfs. + oci_pkg_layer_num=`expr $oci_pkg_layer_num + 1` + oci_preinstall_rootfs="${WORKDIR}/oci-layer-rootfs/layer-${oci_pkg_layer_num}-${oci_layer_name}" + + if [ -d "$oci_preinstall_rootfs" ]; then + bbnote "OCI: Copying pre-installed packages from $oci_preinstall_rootfs" + # Use rsync to merge into bundle rootfs (handles symlinks properly) + rsync -a --no-owner --no-group "$oci_preinstall_rootfs/" "$image_bundle_name/rootfs/" + else + bbwarn "OCI: Pre-installed rootfs not found at $oci_preinstall_rootfs" + fi + + elif [ "$oci_layer_type" = "directories" ]; then + # Copy directories from IMAGE_ROOTFS + for oci_dir in $oci_layer_content; do + if [ -d "${IMAGE_ROOTFS}$oci_dir" ]; then + mkdir -p "$image_bundle_name/rootfs$(dirname $oci_dir)" + cp -a "${IMAGE_ROOTFS}$oci_dir" "$image_bundle_name/rootfs$oci_dir" + bbnote "OCI: Added directory $oci_dir" + fi + done + + elif [ "$oci_layer_type" = "files" ]; then + # Copy specific files from IMAGE_ROOTFS + for oci_file in $oci_layer_content; do + if [ -e "${IMAGE_ROOTFS}$oci_file" ]; then + mkdir -p "$image_bundle_name/rootfs$(dirname $oci_file)" + cp -a "${IMAGE_ROOTFS}$oci_file" "$image_bundle_name/rootfs$oci_file" + bbnote "OCI: Added file $oci_file" + fi + done + fi + + # Repack to create layer + bbnote "OCI: Repacking layer $oci_layer_name" + umoci repack --image "$image_name:$repack_tag" "$image_bundle_name" + + # Re-unpack for next layer if not the last one + if [ "$oci_layer_num" -lt "$oci_total_layers" ]; then + rm -rf "$image_bundle_name" + umoci unpack --rootless --image "$image_name:$repack_tag" "$image_bundle_name" + fi + done + + bbnote "OCI: Created $oci_layer_num layers" + + else + # ================================================================== + # Single-layer mode: Copy entire rootfs as one layer + # ================================================================== + # Use rsync for robust merging when base image exists (handles symlink vs dir conflicts) + # For no-base builds, cp is sufficient and faster + # Note: When source has symlinks replacing dest directories, we first remove conflicting dirs + if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then + # Handle Yocto's merged-usr symlinks (/bin -> /usr/bin) and /var symlinks + # replacing Alpine's or other base image directories + for p in bin lib lib64 sbin var/lock var/log var/tmp; do + src="${IMAGE_ROOTFS}/$p" + dst="$image_bundle_name/rootfs/$p" + if [ -L "$src" ] && [ -d "$dst" ] && [ ! -L "$dst" ]; then + bbdebug 1 "OCI: removing directory $dst to replace with symlink" + rm -rf "$dst" + fi + done + bbdebug 1 "OCI: rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/" + rsync -a --no-owner --no-group ${IMAGE_ROOTFS}/ $image_bundle_name/rootfs/ + else + bbdebug 1 "OCI: cp -r ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs/" + cp -r -a --no-preserve=ownership ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs + fi + + bbdebug 1 "OCI: umoci repack --image $image_name:$repack_tag $image_bundle_name" + umoci repack --image $image_name:$repack_tag $image_bundle_name + fi # If we used a base image with different tag, re-tag to our target tag 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" do_image_oci[depends] += "${OCI_IMAGE_BACKEND}-native:do_populate_sysroot" # jq-native is needed for the merged-usr whiteout fix do_image_oci[depends] += "jq-native:do_populate_sysroot" +# Package manager native tools for multi-layer mode with package installation +OCI_PM_DEPENDS = "${@oci_get_pm_depends(d)}" +do_image_oci[depends] += "${OCI_PM_DEPENDS}" + +def oci_get_pm_depends(d): + """Get native package manager dependency for multi-layer mode.""" + if d.getVar('OCI_LAYER_MODE') != 'multi': + return '' + if 'packages' not in (d.getVar('OCI_LAYERS') or ''): + return '' + # rsync-native is needed to copy pre-installed packages to bundle rootfs + deps = 'rsync-native:do_populate_sysroot' + pkg_type = d.getVar('IMAGE_PKGTYPE') or 'rpm' + if pkg_type == 'rpm': + deps += ' dnf-native:do_populate_sysroot createrepo-c-native:do_populate_sysroot' + elif pkg_type == 'ipk': + deps += ' opkg-native:do_populate_sysroot' + elif pkg_type == 'deb': + deps += ' apt-native:do_populate_sysroot' + return deps # # image type configuration block @@ -139,6 +159,42 @@ OCI_BASE_IMAGE ?= "" OCI_BASE_IMAGE_TAG ?= "latest" OCI_LAYER_MODE ?= "single" +# ============================================================================= +# Multi-Layer Mode (OCI_LAYER_MODE = "multi") +# ============================================================================= +# +# OCI_LAYERS defines explicit layers when OCI_LAYER_MODE = "multi". +# Each layer is defined as: "name:type:content" +# +# Layer Types: +# packages - Copy files installed by specified packages +# directories - Copy specific directories from IMAGE_ROOTFS +# files - Copy specific files from IMAGE_ROOTFS +# +# Format: Space-separated list of layer definitions +# OCI_LAYERS = "layer1:type:content layer2:type:content ..." +# +# For packages type, content is package names (use + as delimiter): +# "base:packages:base-files+busybox+netbase" +# +# For directories/files type, content is paths (use + as delimiter): +# "app:directories:/opt/myapp+/etc/myapp" +# "config:files:/etc/myapp.conf+/etc/default/myapp" +# +# Note: Use + as delimiter because ; is interpreted as shell command separator +# +# Example: +# OCI_LAYER_MODE = "multi" +# OCI_LAYERS = "\ +# base:packages:base-files+base-passwd+netbase+busybox \ +# python:packages:python3+python3-pip \ +# app:directories:/opt/myapp \ +# " +# +# Result: 3 layers (base, python, app) plus any base image layers +# +OCI_LAYERS ?= "" + # whether the oci image dir should be left as a directory, or # bundled into a tarball. OCI_IMAGE_TAR_OUTPUT ?= "true" @@ -199,6 +255,59 @@ python __anonymous() { bb.fatal("Multi-layer OCI requires umoci backend. " "Set OCI_IMAGE_BACKEND = 'umoci' or remove OCI_BASE_IMAGE") + # Validate multi-layer mode configuration and add dependencies + if layer_mode == 'multi': + oci_layers = d.getVar('OCI_LAYERS') or '' + if not oci_layers.strip(): + bb.fatal("OCI_LAYER_MODE = 'multi' requires OCI_LAYERS to be defined") + + has_packages_layer = False + + # Parse and validate layer definitions + for layer_def in oci_layers.split(): + parts = layer_def.split(':') + if len(parts) < 3: + bb.fatal(f"Invalid OCI_LAYERS entry '{layer_def}': " + "format is 'name:type:content'") + layer_name, layer_type, layer_content = parts[0], parts[1], ':'.join(parts[2:]) + if layer_type not in ('packages', 'directories', 'files'): + bb.fatal(f"Invalid layer type '{layer_type}' in '{layer_def}': " + "must be 'packages', 'directories', or 'files'") + if layer_type == 'packages': + has_packages_layer = True + + # Add package manager native dependency if using 'packages' layer type + if has_packages_layer: + pkg_type = d.getVar('IMAGE_PKGTYPE') or 'ipk' + if pkg_type == 'ipk': + d.appendVarFlag('do_image_oci', 'depends', + " opkg-native:do_populate_sysroot opkg-utils-native:do_populate_sysroot") + bb.debug(1, "OCI: Added opkg-native dependency for packages layers") + elif pkg_type == 'rpm': + d.appendVarFlag('do_image_oci', 'depends', + " dnf-native:do_populate_sysroot") + bb.debug(1, "OCI: Added dnf-native dependency for packages layers") + elif pkg_type == 'deb': + d.appendVarFlag('do_image_oci', 'depends', + " apt-native:do_populate_sysroot") + bb.debug(1, "OCI: Added apt-native dependency for packages layers") + + # Extract all packages from OCI_LAYERS and add do_package_write dependencies + # This allows IMAGE_INSTALL = "" for pure multi-layer builds + all_packages = set() + for layer_def in oci_layers.split(): + parts = layer_def.split(':') + if len(parts) >= 3 and parts[1] == 'packages': + layer_content = ':'.join(parts[2:]) + # Use + as delimiter (not ; which is shell command separator) + for pkg in layer_content.replace('+', ' ').split(): + all_packages.add(pkg) + + if all_packages: + # Note: Packages need to be in IMAGE_INSTALL to trigger builds + # via do_rootfs recrdeptask. We just log which packages we found. + bb.debug(1, f"OCI multi-layer: Found packages in OCI_LAYERS: {' '.join(all_packages)}") + # Resolve base image and set up dependencies if base_image: resolved = oci_resolve_base_image(d) @@ -234,6 +343,95 @@ python __anonymous() { f"Then use: OCI_BASE_IMAGE = \"my-base\"") } +# ============================================================================= +# Multi-Layer Package Installation using Yocto's PM Classes +# ============================================================================= +# +# This function uses the same package management infrastructure as do_rootfs, +# ensuring consistency and maintainability. + +def oci_install_layer_packages(d, layer_rootfs, layer_packages, layer_name): + """ + Install packages to a layer rootfs using Yocto's package manager classes. + + This uses the same PM infrastructure as do_rootfs for consistency. + + Args: + d: BitBake datastore + layer_rootfs: Path to the layer's rootfs directory + layer_packages: Space-separated list of packages to install + layer_name: Name of the layer (for logging) + """ + import os + import oe.path + + packages = layer_packages.split() + if not packages: + bb.note(f"OCI: No packages to install for layer {layer_name}") + return + + bb.note(f"OCI: Installing packages for layer '{layer_name}': {' '.join(packages)}") + + pkg_type = d.getVar('IMAGE_PKGTYPE') or 'rpm' + + # Ensure layer rootfs directory exists + bb.utils.mkdirhier(layer_rootfs) + + if pkg_type == 'rpm': + from oe.package_manager.rpm import RpmPM + + # Create PM instance for layer rootfs + pm = RpmPM(d, + layer_rootfs, + d.getVar('TARGET_VENDOR'), + task_name='oci-layer', + filterbydependencies=False) + + # Setup configs in layer rootfs + pm.create_configs() + + # Generate/update repo indexes + pm.write_index() + + # Install packages + # Use attempt_only=True to allow unresolved deps (resolved in later layers) + try: + pm.install(packages, attempt_only=True) + except Exception as e: + bb.warn(f"OCI: Package installation had issues (may be resolved in later layers): {e}") + + elif pkg_type == 'ipk': + from oe.package_manager.ipk import OpkgPM + + # Create config file for this layer + config_file = os.path.join(d.getVar('WORKDIR'), f'opkg-{layer_name}.conf') + archs = d.getVar('PACKAGE_ARCHS') + + # Create PM instance + pm = OpkgPM(d, + layer_rootfs, + config_file, + archs, + task_name='oci-layer', + filterbydependencies=False) + + # Write indexes + pm.write_index() + + # Install packages + try: + pm.install(packages, attempt_only=True) + except Exception as e: + bb.warn(f"OCI: Package installation had issues (may be resolved in later layers): {e}") + + elif pkg_type == 'deb': + bb.warn("OCI: deb package type not yet fully implemented for multi-layer") + + else: + bb.fatal(f"OCI: Unsupported package type: {pkg_type}") + + bb.note(f"OCI: Package installation complete for layer '{layer_name}'") + # the IMAGE_CMD:oci comes from the .inc OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}" include ${OCI_IMAGE_BACKEND_INC} diff --git a/docs/container-bundling.md b/docs/container-bundling.md index fcbb4981..a695a5b8 100644 --- a/docs/container-bundling.md +++ b/docs/container-bundling.md @@ -109,31 +109,90 @@ OCI Multi-Layer Images ---------------------- By default, OCI images are single-layer (the entire rootfs in one layer). -To create multi-layer images with shared base layers, set `OCI_BASE_IMAGE`. +Multi-layer images enable: +- Shared base layers across images +- Faster rebuilds via layer caching +- Smaller delta updates when only app layer changes -### Single vs Multi-Layer +### Layer Modes - # Single layer (default) - full rootfs in one layer - inherit image image-oci - IMAGE_INSTALL = "base-files busybox myapp" +| Mode | Variable | Layers | Use Case | +|------|----------|--------|----------| +| Single | (default) | 1 | Simple containers, backward compat | +| Two-layer | `OCI_BASE_IMAGE` | 2 | Base + app (shared base across images) | +| Multi-layer | `OCI_LAYER_MODE="multi"` | 3+ | Fine-grained layers (base, deps, app) | + +### Two-Layer Mode (OCI_BASE_IMAGE) + +Build on top of another OCI image recipe: - # Multi-layer - app layer on top of base layer + # myapp-container.bb inherit image image-oci OCI_BASE_IMAGE = "container-base" IMAGE_INSTALL = "base-files busybox myapp" -### OCI_BASE_IMAGE +Result: 2 layers (container-base layer + myapp layer) -Specifies the base image to build on top of: - -| Value | Description | -|-------|-------------| +| OCI_BASE_IMAGE Value | Description | +|----------------------|-------------| | Recipe name | `"container-base"` - uses OCI output from another recipe | | Absolute path | `"/path/to/oci-dir"` - uses existing OCI layout | For external images (docker.io, quay.io), use `container-bundle` with `CONTAINER_BUNDLE_DEPLOY = "1"` to fetch and deploy them first. +### Multi-Layer Mode (OCI_LAYERS) + +Create explicit layers with fine-grained control: + + # app-container-multilayer.bb + inherit image image-oci + + OCI_LAYER_MODE = "multi" + OCI_LAYERS = "\ + base:packages:base-files+base-passwd+netbase \ + shell:packages:busybox \ + app:packages:curl \ + " + + # IMAGE_INSTALL must include all packages to trigger builds + IMAGE_INSTALL = "base-files base-passwd netbase busybox curl" + +Result: 3 layers (base, shell, app) + +#### Layer Definition Format + + name:type:content + +| Type | Content Format | Description | +|------|----------------|-------------| +| `packages` | `pkg1+pkg2+pkg3` | Install packages (use + delimiter) | +| `directories` | `/path1+/path2` | Copy directories from IMAGE_ROOTFS | +| `files` | `/file1+/file2` | Copy specific files from IMAGE_ROOTFS | + +#### Example Recipes + +**Three-layer with explicit packages:** +```bitbake +OCI_LAYER_MODE = "multi" +OCI_LAYERS = "\ + base:packages:base-files+base-passwd+netbase \ + python:packages:python3+python3-pip \ + app:directories:/opt/myapp \ +" +IMAGE_INSTALL = "base-files base-passwd netbase python3 python3-pip myapp" +``` + +**Two-layer with base image + multi-layer app:** +```bitbake +OCI_BASE_IMAGE = "container-base" +OCI_LAYER_MODE = "multi" +OCI_LAYERS = "\ + deps:packages:python3+python3-pip \ + app:directories:/opt/myapp \ +" +``` + ### OCI_IMAGE_CMD vs OCI_IMAGE_ENTRYPOINT # CMD (default) - replaced when user passes arguments @@ -149,6 +208,21 @@ For external images (docker.io, quay.io), use `container-bundle` with Use CMD for base images (flexible). Use ENTRYPOINT for wrapper tools. +### Verifying Layer Count + + # Check layer count with skopeo + skopeo inspect oci:tmp/deploy/images/qemux86-64/myapp-latest-oci | jq '.Layers | length' + +### Testing Multi-Layer OCI + + cd /opt/bruce/poky/meta-virtualization + + # Quick tests (no builds) + pytest tests/test_multilayer_oci.py -v -k "not slow" + + # Full tests (with builds) + pytest tests/test_multilayer_oci.py -v --poky-dir /opt/bruce/poky + Using BUNDLED_CONTAINERS ------------------------ diff --git a/recipes-demo/images/app-container-multilayer.bb b/recipes-demo/images/app-container-multilayer.bb new file mode 100644 index 00000000..6a3f2042 --- /dev/null +++ b/recipes-demo/images/app-container-multilayer.bb @@ -0,0 +1,38 @@ +SUMMARY = "Multi-layer Application container - test OCI_LAYERS" +DESCRIPTION = "Demonstrates OCI_LAYER_MODE = 'multi' with explicit layer definitions" +LICENSE = "MIT" +LIC_FILES_CHKSUM = "file://${COREBASE}/meta/COPYING.MIT;md5=3da9cfbcb788c80a0384361b4de20420" + +# Multi-layer mode: create explicit layers instead of single rootfs layer +OCI_LAYER_MODE = "multi" + +# Define layers: each layer contains specific packages +# Format: "name:type:content" where content uses + as delimiter for multiple items +OCI_LAYERS = "\ + base:packages:base-files+base-passwd+netbase \ + shell:packages:busybox \ + app:packages:curl \ +" + +# Use CMD so `docker run image /bin/sh` works as expected +OCI_IMAGE_CMD = "/bin/sh -c 'echo Hello from multi-layer container && curl --version'" + +IMAGE_FSTYPES = "container oci" +inherit image +inherit image-oci + +IMAGE_FEATURES = "" +IMAGE_LINGUAS = "" +NO_RECOMMENDATIONS = "1" + +# IMAGE_INSTALL triggers package builds via do_rootfs recrdeptask. +# Even for multi-layer mode, list packages here to ensure they're built. +# The PM will install them directly to layers from DEPLOY_DIR_IPK. +# Note: IMAGE_ROOTFS is still created but ignored for packages layers. +IMAGE_INSTALL = "base-files base-passwd netbase busybox curl" + +# Allow build with or without a specific kernel +IMAGE_CONTAINER_NO_DUMMY = "1" + +# Note: No ROOTFS_POSTPROCESS_COMMAND needed - IMAGE_ROOTFS is empty +# and PM handles installation directly to OCI layers -- cgit v1.2.3-54-g00ecf