diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-14 20:58:34 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-21 18:00:26 -0500 |
| commit | 24c604854c6ffe79ac7973e333b2df7f7f82ddd9 (patch) | |
| tree | 95ec7fcdd9f1ac87977ab1d60773ba5178d7a210 /classes | |
| parent | f83a83eb3979e3bc671190650731acf8a5b9ecd3 (diff) | |
| download | meta-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.inc | 190 | ||||
| -rw-r--r-- | classes/image-oci.bbclass | 198 |
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 | |||
| 8 | python 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 | ||
| 65 | do_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" | |||
| 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} |
