summaryrefslogtreecommitdiffstats
path: root/classes
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-02-05 21:37:24 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:34:12 +0000
commitb4ad3f9eb2f022b6f69b2e78dbca80974d5bf84a (patch)
tree89da66f8e07714891b9f3f5e114568c84cb49dae /classes
parentbcddeedc6f657841bf0cbb9cf06e9de1633bbe6d (diff)
downloadmeta-virtualization-b4ad3f9eb2f022b6f69b2e78dbca80974d5bf84a.tar.gz
image-oci: add host layer type and delta-only copying
Add two enhancements to multi-layer OCI image support: 1. Delta-only copying for directories/files layers: - directories and files layers now only copy content that doesn't already exist in the bundle rootfs from earlier layers - Prevents duplication when a directories layer references paths that were already populated by a packages layer - Logs show "delta: N copied, M skipped" for visibility 2. New 'host' layer type for build machine content: - Copies files from the build machine filesystem (outside Yocto) - Format: name:host:source_path:dest_path - Multiple pairs: name:host:src1:dst1+src2:dst2 - Emits warning at parse time about reproducibility impact - Fatal error if source path doesn't exist - Use case: deployment-specific config, certificates, keys that cannot be packaged in recipes Example: OCI_LAYERS = "\ base:packages:busybox \ app:directories:/opt/myapp \ certs:host:/etc/ssl/certs/ca.crt:/etc/ssl/certs/ca.crt \ " Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
-rw-r--r--classes/image-oci-umoci.inc61
-rw-r--r--classes/image-oci.bbclass43
2 files changed, 90 insertions, 14 deletions
diff --git a/classes/image-oci-umoci.inc b/classes/image-oci-umoci.inc
index fbb77cd0..616c20ae 100644
--- a/classes/image-oci-umoci.inc
+++ b/classes/image-oci-umoci.inc
@@ -589,22 +589,67 @@ IMAGE_CMD:oci() {
589 fi 589 fi
590 590
591 elif [ "$oci_layer_type" = "directories" ]; then 591 elif [ "$oci_layer_type" = "directories" ]; then
592 # Copy directories from IMAGE_ROOTFS 592 # Copy directories from IMAGE_ROOTFS (delta-only: skip files already in bundle)
593 for oci_dir in $oci_layer_content; do 593 for oci_dir in $oci_layer_content; do
594 if [ -d "${IMAGE_ROOTFS}$oci_dir" ]; then 594 if [ -d "${IMAGE_ROOTFS}$oci_dir" ]; then
595 mkdir -p "$image_bundle_name/rootfs$(dirname $oci_dir)" 595 oci_delta_copied=0
596 cp -a "${IMAGE_ROOTFS}$oci_dir" "$image_bundle_name/rootfs$oci_dir" 596 oci_delta_skipped=0
597 bbnote "OCI: Added directory $oci_dir" 597 # Walk the directory and copy only files not in bundle
598 while IFS= read -r oci_src_file; do
599 oci_rel_path="${oci_src_file#${IMAGE_ROOTFS}}"
600 oci_dst_file="$image_bundle_name/rootfs$oci_rel_path"
601 if [ ! -e "$oci_dst_file" ]; then
602 mkdir -p "$(dirname "$oci_dst_file")"
603 cp -a "$oci_src_file" "$oci_dst_file"
604 oci_delta_copied=$((oci_delta_copied + 1))
605 else
606 oci_delta_skipped=$((oci_delta_skipped + 1))
607 fi
608 done < <(find "${IMAGE_ROOTFS}$oci_dir" -type f -o -type l)
609 # Also copy empty directories
610 while IFS= read -r oci_src_dir; do
611 oci_rel_path="${oci_src_dir#${IMAGE_ROOTFS}}"
612 oci_dst_dir="$image_bundle_name/rootfs$oci_rel_path"
613 if [ ! -e "$oci_dst_dir" ]; then
614 mkdir -p "$oci_dst_dir"
615 fi
616 done < <(find "${IMAGE_ROOTFS}$oci_dir" -type d)
617 bbnote "OCI: Added directory $oci_dir (delta: $oci_delta_copied copied, $oci_delta_skipped skipped)"
618 else
619 bbwarn "OCI: Directory not found in IMAGE_ROOTFS: $oci_dir"
598 fi 620 fi
599 done 621 done
600 622
601 elif [ "$oci_layer_type" = "files" ]; then 623 elif [ "$oci_layer_type" = "files" ]; then
602 # Copy specific files from IMAGE_ROOTFS 624 # Copy specific files from IMAGE_ROOTFS (delta-only: skip files already in bundle)
603 for oci_file in $oci_layer_content; do 625 for oci_file in $oci_layer_content; do
604 if [ -e "${IMAGE_ROOTFS}$oci_file" ]; then 626 if [ -e "${IMAGE_ROOTFS}$oci_file" ]; then
605 mkdir -p "$image_bundle_name/rootfs$(dirname $oci_file)" 627 oci_dst_file="$image_bundle_name/rootfs$oci_file"
606 cp -a "${IMAGE_ROOTFS}$oci_file" "$image_bundle_name/rootfs$oci_file" 628 if [ ! -e "$oci_dst_file" ]; then
607 bbnote "OCI: Added file $oci_file" 629 mkdir -p "$(dirname "$oci_dst_file")"
630 cp -a "${IMAGE_ROOTFS}$oci_file" "$oci_dst_file"
631 bbnote "OCI: Added file $oci_file"
632 else
633 bbnote "OCI: Skipped file $oci_file (already in bundle)"
634 fi
635 else
636 bbwarn "OCI: File not found in IMAGE_ROOTFS: $oci_file"
637 fi
638 done
639
640 elif [ "$oci_layer_type" = "host" ]; then
641 # Copy files from build machine filesystem (outside Yocto)
642 # Format: source_path:dest_path pairs separated by + (already converted to space)
643 for oci_host_pair in $oci_layer_content; do
644 # Split on last : to handle paths that might contain :
645 oci_host_src="${oci_host_pair%:*}"
646 oci_host_dst="${oci_host_pair##*:}"
647 if [ -e "$oci_host_src" ]; then
648 mkdir -p "$image_bundle_name/rootfs$(dirname $oci_host_dst)"
649 cp -a "$oci_host_src" "$image_bundle_name/rootfs$oci_host_dst"
650 bbnote "OCI: Added from host: $oci_host_src -> $oci_host_dst"
651 else
652 bbfatal "OCI: Host path not found: $oci_host_src"
608 fi 653 fi
609 done 654 done
610 fi 655 fi
diff --git a/classes/image-oci.bbclass b/classes/image-oci.bbclass
index 41f7b3d1..ea2b63df 100644
--- a/classes/image-oci.bbclass
+++ b/classes/image-oci.bbclass
@@ -167,9 +167,10 @@ OCI_LAYER_MODE ?= "single"
167# Each layer is defined as: "name:type:content" 167# Each layer is defined as: "name:type:content"
168# 168#
169# Layer Types: 169# Layer Types:
170# packages - Copy files installed by specified packages 170# packages - Install packages using Yocto's package manager
171# directories - Copy specific directories from IMAGE_ROOTFS 171# directories - Copy specific directories from IMAGE_ROOTFS (delta-only)
172# files - Copy specific files from IMAGE_ROOTFS 172# files - Copy specific files from IMAGE_ROOTFS (delta-only)
173# host - Copy files from build machine filesystem (outside Yocto)
173# 174#
174# Format: Space-separated list of layer definitions 175# Format: Space-separated list of layer definitions
175# OCI_LAYERS = "layer1:type:content layer2:type:content ..." 176# OCI_LAYERS = "layer1:type:content layer2:type:content ..."
@@ -181,6 +182,18 @@ OCI_LAYER_MODE ?= "single"
181# "app:directories:/opt/myapp+/etc/myapp" 182# "app:directories:/opt/myapp+/etc/myapp"
182# "config:files:/etc/myapp.conf+/etc/default/myapp" 183# "config:files:/etc/myapp.conf+/etc/default/myapp"
183# 184#
185# NOTE: directories/files only copy content NOT already present in
186# earlier layers (delta-only), avoiding duplication with packages layers.
187#
188# For host type, content is source:dest pairs (use + as delimiter):
189# "certs:host:/etc/ssl/certs/my-ca.crt:/etc/ssl/certs/my-ca.crt"
190# "config:host:/home/builder/config:/etc/myapp/config+/home/builder/keys:/etc/myapp/keys"
191#
192# WARNING: host layers copy content from the build machine that is NOT
193# part of the Yocto build. This affects reproducibility - the build output
194# depends on the state of the build machine. Use sparingly for deployment-
195# specific config, keys, or certificates that cannot be packaged.
196#
184# Note: Use + as delimiter because ; is interpreted as shell command separator 197# Note: Use + as delimiter because ; is interpreted as shell command separator
185# 198#
186# Example: 199# Example:
@@ -189,9 +202,10 @@ OCI_LAYER_MODE ?= "single"
189# base:packages:base-files+base-passwd+netbase+busybox \ 202# base:packages:base-files+base-passwd+netbase+busybox \
190# python:packages:python3+python3-pip \ 203# python:packages:python3+python3-pip \
191# app:directories:/opt/myapp \ 204# app:directories:/opt/myapp \
205# certs:host:/etc/ssl/certs/my-ca.crt:/etc/ssl/certs/ \
192# " 206# "
193# 207#
194# Result: 3 layers (base, python, app) plus any base image layers 208# Result: 4 layers (base, python, app, certs) plus any base image layers
195# 209#
196OCI_LAYERS ?= "" 210OCI_LAYERS ?= ""
197 211
@@ -288,6 +302,7 @@ python __anonymous() {
288 bb.fatal("OCI_LAYER_MODE = 'multi' requires OCI_LAYERS to be defined") 302 bb.fatal("OCI_LAYER_MODE = 'multi' requires OCI_LAYERS to be defined")
289 303
290 has_packages_layer = False 304 has_packages_layer = False
305 host_layer_warnings = []
291 306
292 # Parse and validate layer definitions 307 # Parse and validate layer definitions
293 for layer_def in oci_layers.split(): 308 for layer_def in oci_layers.split():
@@ -296,11 +311,27 @@ python __anonymous() {
296 bb.fatal(f"Invalid OCI_LAYERS entry '{layer_def}': " 311 bb.fatal(f"Invalid OCI_LAYERS entry '{layer_def}': "
297 "format is 'name:type:content'") 312 "format is 'name:type:content'")
298 layer_name, layer_type, layer_content = parts[0], parts[1], ':'.join(parts[2:]) 313 layer_name, layer_type, layer_content = parts[0], parts[1], ':'.join(parts[2:])
299 if layer_type not in ('packages', 'directories', 'files'): 314 if layer_type not in ('packages', 'directories', 'files', 'host'):
300 bb.fatal(f"Invalid layer type '{layer_type}' in '{layer_def}': " 315 bb.fatal(f"Invalid layer type '{layer_type}' in '{layer_def}': "
301 "must be 'packages', 'directories', or 'files'") 316 "must be 'packages', 'directories', 'files', or 'host'")
302 if layer_type == 'packages': 317 if layer_type == 'packages':
303 has_packages_layer = True 318 has_packages_layer = True
319 elif layer_type == 'host':
320 # Validate host layer format and collect warnings
321 # Format: source:dest pairs separated by +
322 for pair in layer_content.replace('+', ' ').split():
323 if ':' not in pair:
324 bb.fatal(f"Invalid host layer content '{pair}' in '{layer_def}': "
325 "format is 'source_path:dest_path'")
326 src_path = pair.rsplit(':', 1)[0]
327 host_layer_warnings.append(f" Layer '{layer_name}': {src_path}")
328
329 # Emit warning for host layers (content from build machine, not Yocto)
330 if host_layer_warnings:
331 bb.warn("OCI image includes content from build machine filesystem (host layers).\n"
332 "This content is NOT part of the Yocto build and affects reproducibility.\n"
333 "The build output will depend on the state of the build machine.\n"
334 "Host paths used:\n" + "\n".join(host_layer_warnings))
304 335
305 # Add package manager native dependency if using 'packages' layer type 336 # Add package manager native dependency if using 'packages' layer type
306 if has_packages_layer: 337 if has_packages_layer: