summaryrefslogtreecommitdiffstats
path: root/classes
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-14 04:45:53 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-01-21 18:00:26 -0500
commit08ffbeef62a426193528c539bcb02fa4b8b1c191 (patch)
tree014a25e6507bd908bba7949857c389c20c69b2bd /classes
parentfb150a8564f4749dfff6651aaeba755e907ef315 (diff)
downloadmeta-virtualization-08ffbeef62a426193528c539bcb02fa4b8b1c191.tar.gz
image-oci: add multi-layer OCI support and CMD default
Add support for multi-layer OCI images, enabling base + app layer builds: Multi-layer support: - Add OCI_BASE_IMAGE variable to specify base layer (recipe name or path) - Add OCI_BASE_IMAGE_TAG for selecting base image tag (default: latest) - Resolve base image type (recipe/path/remote) at parse time - Copy base OCI layout before adding new layer via umoci repack - Fix merged-usr whiteout ordering issue for non-merged-usr base images (replaces problematic whiteouts with filtered entries to avoid Docker pull failures when layering merged-usr on traditional layout) CMD/ENTRYPOINT behavior change: - Add OCI_IMAGE_CMD variable (default: "/bin/sh") - Change OCI_IMAGE_ENTRYPOINT default to empty string - This makes `docker run image /bin/sh` work as expected (like Docker Hub images) - OCI_IMAGE_ENTRYPOINT_ARGS still works for legacy compatibility - Fix shlex.split() for proper shell quoting in CMD/ENTRYPOINT values The multi-layer feature requires umoci backend (default). The sloci backend only supports single-layer images and will error if OCI_BASE_IMAGE is set. Example usage: OCI_BASE_IMAGE = "container-base" IMAGE_INSTALL = "myapp" OCI_IMAGE_CMD = "/usr/bin/myapp" Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
-rw-r--r--classes/image-oci-umoci.inc305
-rw-r--r--classes/image-oci.bbclass105
2 files changed, 395 insertions, 15 deletions
diff --git a/classes/image-oci-umoci.inc b/classes/image-oci-umoci.inc
index 0dba347c..340f298b 100644
--- a/classes/image-oci-umoci.inc
+++ b/classes/image-oci-umoci.inc
@@ -1,3 +1,187 @@
1# Fix merged-usr whiteout issues in OCI layer
2# 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
4# files inside a symlink. This function replaces individual whiteouts with
5# opaque whiteouts and fixes tar ordering.
6oci_fix_merged_usr_whiteouts() {
7 local image_dir="$1"
8 local tag="$2"
9 local needs_fix=false
10
11 # Find the manifest for this tag
12 local manifest_digest=$(jq -r '.manifests[] | select(.annotations["org.opencontainers.image.ref.name"] == "'"$tag"'") | .digest' "$image_dir/index.json" | sed 's/sha256://')
13 if [ -z "$manifest_digest" ]; then
14 bbdebug 1 "OCI fix: Could not find manifest for tag $tag"
15 return 0
16 fi
17
18 # Get the last layer (newest, the one we just added)
19 local layer_digest=$(jq -r '.layers[-1].digest' "$image_dir/blobs/sha256/$manifest_digest" | sed 's/sha256://')
20 if [ -z "$layer_digest" ]; then
21 bbdebug 1 "OCI fix: Could not find layer digest"
22 return 0
23 fi
24
25 local layer_blob="$image_dir/blobs/sha256/$layer_digest"
26 if [ ! -f "$layer_blob" ]; then
27 bbdebug 1 "OCI fix: Layer blob not found: $layer_blob"
28 return 0
29 fi
30
31 # Convert to absolute path before we cd elsewhere
32 layer_blob=$(readlink -f "$layer_blob")
33 image_dir=$(readlink -f "$image_dir")
34
35 # Get tar listing with details to identify symlinks and whiteouts
36 local layer_listing=$(tar -tvzf "$layer_blob" 2>/dev/null || true)
37 local layer_files=$(tar -tzf "$layer_blob" 2>/dev/null || true)
38
39 # Find directories that are symlinks but have whiteouts listed inside
40 # Include merged-usr dirs (bin, sbin, lib) and var/* symlinks
41 local dirs_to_fix=""
42 for dir in bin sbin lib lib64 var/lock var/log var/run var/tmp; do
43 # Check if $dir is a symlink in the tar (line starts with 'l')
44 if echo "$layer_listing" | grep -q "^l.* ${dir} -> "; then
45 # Check if there are whiteouts "inside" it
46 if echo "$layer_files" | grep -q "^${dir}/\.wh\."; then
47 bbnote "OCI fix: Found problematic whiteout pattern in $dir"
48 dirs_to_fix="$dirs_to_fix $dir"
49 needs_fix=true
50 fi
51 fi
52 done
53
54 if [ "$needs_fix" != "true" ]; then
55 bbdebug 1 "OCI fix: No merged-usr whiteout issues detected"
56 return 0
57 fi
58
59 bbnote "OCI fix: Fixing merged-usr whiteout ordering in layer"
60 bbnote "OCI fix: Directories to fix:$dirs_to_fix"
61
62 # Save current directory
63 local orig_dir=$(pwd)
64
65 # Create temp directory for fix
66 local fix_dir=$(mktemp -d)
67 local fixed_tar="$fix_dir/fixed-layer.tar"
68
69 cd "$fix_dir"
70
71 # Strategy: Simply remove the problematic whiteouts
72 # The symlink itself will hide the base directory contents.
73 # We don't need opaque whiteouts - they would hide ALL base content.
74
75 # Build exclude pattern - whiteouts in symlinked dirs
76 local exclude_pattern=""
77 for dir in $dirs_to_fix; do
78 exclude_pattern="${exclude_pattern}|^${dir}/\\.wh\\."
79 done
80 exclude_pattern="${exclude_pattern#|}" # Remove leading |
81
82 # Use Python to filter the tar - just remove problematic whiteouts
83 python3 << PYEOF
84import tarfile
85import gzip
86import re
87
88src_blob = "$layer_blob"
89dst_tar = "$fixed_tar"
90exclude_re = re.compile(r'$exclude_pattern')
91
92removed_count = 0
93
94# Read source tar and filter out problematic whiteouts
95with gzip.open(src_blob, 'rb') as gz:
96 with tarfile.open(fileobj=gz, mode='r:') as src:
97 with tarfile.open(dst_tar, 'w') as dst:
98 for member in src.getmembers():
99 # Skip whiteouts in dirs that became symlinks
100 if exclude_re.match(member.name):
101 removed_count += 1
102 continue
103
104 # Copy the member
105 if member.isfile():
106 dst.addfile(member, src.extractfile(member))
107 else:
108 dst.addfile(member)
109
110print(f"Removed {removed_count} problematic whiteouts from layer")
111PYEOF
112
113 # Calculate diff_id (uncompressed digest) before compressing
114 local new_diff_id=$(sha256sum "$fixed_tar" | cut -d' ' -f1)
115 local old_diff_id=$(gunzip -c "$layer_blob" | sha256sum | cut -d' ' -f1)
116
117 # Compress the fixed tar
118 gzip -n -f "$fixed_tar"
119 local fixed_blob="$fixed_tar.gz"
120
121 # Calculate new digest (compressed)
122 local new_digest=$(sha256sum "$fixed_blob" | cut -d' ' -f1)
123 local new_size=$(stat -c%s "$fixed_blob")
124
125 bbnote "OCI fix: New layer digest: sha256:$new_digest (was sha256:$layer_digest)"
126 bbnote "OCI fix: New diff_id: sha256:$new_diff_id (was sha256:$old_diff_id)"
127
128 # Replace the blob
129 cp "$fixed_blob" "$image_dir/blobs/sha256/$new_digest"
130 rm -f "$image_dir/blobs/sha256/$layer_digest"
131
132 # Update manifest with new layer digest and size
133 local manifest_file="$image_dir/blobs/sha256/$manifest_digest"
134 jq --arg old "sha256:$layer_digest" --arg new "sha256:$new_digest" --argjson size "$new_size" \
135 '(.layers[] | select(.digest == $old)) |= (.digest = $new | .size = $size)' \
136 "$manifest_file" > "$manifest_file.new"
137 mv "$manifest_file.new" "$manifest_file"
138
139 # Get config digest from manifest and update diff_ids in config
140 local config_digest=$(jq -r '.config.digest' "$manifest_file" | sed 's/sha256://')
141 local config_file="$image_dir/blobs/sha256/$config_digest"
142
143 bbnote "OCI fix: Updating config $config_digest"
144
145 # Update the last diff_id in the config (our layer)
146 # Use direct index replacement since we know which layer we fixed
147 jq --arg new "sha256:$new_diff_id" \
148 '.rootfs.diff_ids[-1] = $new' \
149 "$config_file" > "$config_file.new"
150 mv "$config_file.new" "$config_file"
151
152 # Recalculate config digest
153 local new_config_digest=$(sha256sum "$config_file" | cut -d' ' -f1)
154 local new_config_size=$(stat -c%s "$config_file")
155
156 if [ "$new_config_digest" != "$config_digest" ]; then
157 mv "$config_file" "$image_dir/blobs/sha256/$new_config_digest"
158 # Update manifest with new config digest
159 jq --arg old "sha256:$config_digest" --arg new "sha256:$new_config_digest" --argjson size "$new_config_size" \
160 '.config |= (if .digest == $old then .digest = $new | .size = $size else . end)' \
161 "$manifest_file" > "$manifest_file.new"
162 mv "$manifest_file.new" "$manifest_file"
163 fi
164
165 # Recalculate manifest digest
166 local new_manifest_digest=$(sha256sum "$manifest_file" | cut -d' ' -f1)
167 local new_manifest_size=$(stat -c%s "$manifest_file")
168
169 if [ "$new_manifest_digest" != "$manifest_digest" ]; then
170 mv "$manifest_file" "$image_dir/blobs/sha256/$new_manifest_digest"
171 # Update index.json
172 jq --arg old "sha256:$manifest_digest" --arg new "sha256:$new_manifest_digest" --argjson size "$new_manifest_size" \
173 '(.manifests[] | select(.digest == $old)) |= (.digest = $new | .size = $size)' \
174 "$image_dir/index.json" > "$image_dir/index.json.new"
175 mv "$image_dir/index.json.new" "$image_dir/index.json"
176 fi
177
178 # Restore original directory and cleanup
179 cd "$orig_dir"
180 rm -rf "$fix_dir"
181
182 bbnote "OCI fix: Layer whiteout fix complete"
183}
184
1IMAGE_CMD:oci() { 185IMAGE_CMD:oci() {
2 umoci_options="" 186 umoci_options=""
3 187
@@ -69,22 +253,97 @@ IMAGE_CMD:oci() {
69 OCI_IMAGE_TAG="initial-tag" 253 OCI_IMAGE_TAG="initial-tag"
70 fi 254 fi
71 255
72 if [ -n "$new_image" ]; then 256 # ========================================================================
73 bbdebug 1 "OCI: umoci init --layout $image_name" 257 # PHASE 1: Initialize OCI layout (from scratch or from base image)
74 umoci init --layout $image_name 258 # ========================================================================
75 umoci new --image $image_name:${OCI_IMAGE_TAG} 259 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then
76 umoci unpack --rootless --image $image_name:${OCI_IMAGE_TAG} $image_bundle_name 260 # Using a base image
261 base_oci_dir=""
262 base_tag="${OCI_BASE_IMAGE_TAG}"
263
264 if [ -n "${_OCI_BASE_RECIPE}" ]; then
265 # Use exact symlink naming: ${recipe}-${tag}-oci
266 base_oci_dir="${DEPLOY_DIR_IMAGE}/${_OCI_BASE_RECIPE}-${base_tag}-oci"
267
268 if [ ! -d "$base_oci_dir" ] || [ ! -f "$base_oci_dir/index.json" ]; then
269 bbfatal "OCI: Base image '${_OCI_BASE_RECIPE}' not found at expected path: $base_oci_dir"
270 fi
271 elif [ -n "${_OCI_BASE_PATH}" ]; then
272 base_oci_dir="${_OCI_BASE_PATH}"
273 if [ ! -d "$base_oci_dir" ] || [ ! -f "$base_oci_dir/index.json" ]; then
274 bbfatal "OCI: Base image path not valid: $base_oci_dir"
275 fi
276 fi
277
278 # Resolve symlinks to get actual directory
279 base_oci_dir=$(readlink -f "$base_oci_dir")
280 bbnote "OCI: Using base image from: $base_oci_dir"
281
282 # Copy base image layout to our image directory (-L to follow symlinks)
283 cp -rL "$base_oci_dir" "$image_name"
284
285 # Count existing layers for logging (simplified)
286 base_layers=$(ls "$image_name/blobs/sha256/" 2>/dev/null | wc -l)
287 bbnote "OCI: Base image has approximately $base_layers blob(s)"
288
289 # Unpack base image for modification
290 umoci unpack --rootless --image "$image_name:$base_tag" "$image_bundle_name"
291 elif [ -n "$new_image" ]; then
292 # No base image - create empty OCI layout
293 bbdebug 1 "OCI: umoci init --layout $image_name"
294 umoci init --layout $image_name
295 umoci new --image $image_name:${OCI_IMAGE_TAG}
296 umoci unpack --rootless --image $image_name:${OCI_IMAGE_TAG} $image_bundle_name
77 else 297 else
78 # todo: create a different tag, after checking if the passed one exists 298 # todo: create a different tag, after checking if the passed one exists
79 true 299 true
80 fi 300 fi
81 301
302 # ========================================================================
303 # PHASE 2: Add content layer(s)
304 # ========================================================================
82 bbdebug 1 "OCI: populating rootfs" 305 bbdebug 1 "OCI: populating rootfs"
83 bbdebug 1 "OCI: cp -r ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs/"
84 cp -r -a --no-preserve=ownership ${IMAGE_ROOTFS}/* $image_bundle_name/rootfs
85 306
86 bbdebug 1 "OCI: umoci repack --image $image_name:${OCI_IMAGE_TAG} $image_bundle_name" 307 # Use rsync for robust merging when base image exists (handles symlink vs dir conflicts)
87 umoci repack --image $image_name:${OCI_IMAGE_TAG} $image_bundle_name 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
329 repack_tag="${OCI_IMAGE_TAG}"
330 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then
331 repack_tag="${OCI_BASE_IMAGE_TAG}"
332 fi
333
334 bbdebug 1 "OCI: umoci repack --image $image_name:$repack_tag $image_bundle_name"
335 umoci repack --image $image_name:$repack_tag $image_bundle_name
336
337 # 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
339 if [ "$repack_tag" != "${OCI_IMAGE_TAG}" ]; then
340 umoci tag --image "$image_name:$repack_tag" "${OCI_IMAGE_TAG}"
341 fi
342
343 # Log final layer count (simplified - count blobs minus config/manifest)
344 final_blobs=$(ls "$image_name/blobs/sha256/" 2>/dev/null | wc -l)
345 bbnote "OCI: Final image has approximately $final_blobs blob(s)"
346 fi
88 347
89 bbdebug 1 "OCI: configuring image" 348 bbdebug 1 "OCI: configuring image"
90 if [ -n "${OCI_IMAGE_LABELS}" ]; then 349 if [ -n "${OCI_IMAGE_LABELS}" ]; then
@@ -137,13 +396,31 @@ IMAGE_CMD:oci() {
137 bbnote "OCI: image subarch is set to: ${OCI_IMAGE_SUBARCH}, but umoci does not" 396 bbnote "OCI: image subarch is set to: ${OCI_IMAGE_SUBARCH}, but umoci does not"
138 bbnote " expose variants. use sloci instead if this is important" 397 bbnote " expose variants. use sloci instead if this is important"
139 fi 398 fi
140 umoci config --image $image_name:${OCI_IMAGE_TAG} \ 399 # Set entrypoint if specified (for wrapper script patterns)
141 ${@" ".join("--config.entrypoint %s" % s for s in d.getVar("OCI_IMAGE_ENTRYPOINT").split())} 400 if [ -n "${OCI_IMAGE_ENTRYPOINT}" ]; then
401 umoci config --image $image_name:${OCI_IMAGE_TAG} \
402 ${@" ".join("--config.entrypoint '%s'" % s for s in __import__('shlex').split(d.getVar("OCI_IMAGE_ENTRYPOINT")))}
403 fi
404 # Set CMD: use OCI_IMAGE_ENTRYPOINT_ARGS if set (legacy), otherwise OCI_IMAGE_CMD
142 if [ -n "${OCI_IMAGE_ENTRYPOINT_ARGS}" ]; then 405 if [ -n "${OCI_IMAGE_ENTRYPOINT_ARGS}" ]; then
143 umoci config --image $image_name:${OCI_IMAGE_TAG} ${@" ".join("--config.cmd %s" % s for s in d.getVar("OCI_IMAGE_ENTRYPOINT_ARGS").split())} 406 umoci config --image $image_name:${OCI_IMAGE_TAG} ${@" ".join("--config.cmd '%s'" % s for s in __import__('shlex').split(d.getVar("OCI_IMAGE_ENTRYPOINT_ARGS")))}
407 elif [ -n "${OCI_IMAGE_CMD}" ]; then
408 umoci config --image $image_name:${OCI_IMAGE_TAG} ${@" ".join("--config.cmd '%s'" % s for s in __import__('shlex').split(d.getVar("OCI_IMAGE_CMD")))}
144 fi 409 fi
145 umoci config --image $image_name:${OCI_IMAGE_TAG} --author ${OCI_IMAGE_AUTHOR_EMAIL} 410 umoci config --image $image_name:${OCI_IMAGE_TAG} --author ${OCI_IMAGE_AUTHOR_EMAIL}
146 411
412 # ========================================================================
413 # PHASE 3: Fix merged-usr whiteout issues for non-merged-usr base images
414 # ========================================================================
415 # When layering merged-usr (symlinks) on traditional layout (directories),
416 # umoci creates whiteouts like bin/.wh.file but puts them AFTER the bin symlink
417 # in the tar. Docker can't create files inside a symlink, causing pull failures.
418 # Fix: Replace individual whiteouts with opaque whiteouts, reorder tar entries.
419 # NOTE: Must run AFTER all umoci config commands since they create new config blobs.
420 if [ -n "${_OCI_BASE_RECIPE}" ] || [ -n "${_OCI_BASE_PATH}" ]; then
421 oci_fix_merged_usr_whiteouts "$image_name" "${OCI_IMAGE_TAG}"
422 fi
423
147 # OCI_IMAGE_TAG may contain ":", but these are not allowed in OCI file 424 # OCI_IMAGE_TAG may contain ":", but these are not allowed in OCI file
148 # names so replace them 425 # names so replace them
149 image_tag="${@d.getVar("OCI_IMAGE_TAG").replace(":", "_")}" 426 image_tag="${@d.getVar("OCI_IMAGE_TAG").replace(":", "_")}"
diff --git a/classes/image-oci.bbclass b/classes/image-oci.bbclass
index 70f32bf1..6f8011ca 100644
--- a/classes/image-oci.bbclass
+++ b/classes/image-oci.bbclass
@@ -42,6 +42,8 @@ IMAGE_TYPEDEP:oci = "container tar.bz2"
42# OCI_IMAGE_BACKEND ?= "sloci-image" 42# OCI_IMAGE_BACKEND ?= "sloci-image"
43OCI_IMAGE_BACKEND ?= "umoci" 43OCI_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
46do_image_oci[depends] += "jq-native:do_populate_sysroot"
45 47
46# 48#
47# image type configuration block 49# image type configuration block
@@ -55,8 +57,12 @@ OCI_IMAGE_RUNTIME_UID ?= ""
55OCI_IMAGE_ARCH ?= "${@oe.go.map_arch(d.getVar('TARGET_ARCH'))}" 57OCI_IMAGE_ARCH ?= "${@oe.go.map_arch(d.getVar('TARGET_ARCH'))}"
56OCI_IMAGE_SUBARCH ?= "${@oci_map_subarch(d.getVar('TARGET_ARCH'), d.getVar('TUNE_FEATURES'), d)}" 58OCI_IMAGE_SUBARCH ?= "${@oci_map_subarch(d.getVar('TARGET_ARCH'), d.getVar('TUNE_FEATURES'), d)}"
57 59
58OCI_IMAGE_ENTRYPOINT ?= "sh" 60# OCI_IMAGE_ENTRYPOINT: If set, this command always runs (args appended).
61# OCI_IMAGE_CMD: Default command (replaced when user passes arguments).
62# Most base images use CMD only for flexibility. Use ENTRYPOINT for wrapper scripts.
63OCI_IMAGE_ENTRYPOINT ?= ""
59OCI_IMAGE_ENTRYPOINT_ARGS ?= "" 64OCI_IMAGE_ENTRYPOINT_ARGS ?= ""
65OCI_IMAGE_CMD ?= "/bin/sh"
60OCI_IMAGE_WORKINGDIR ?= "" 66OCI_IMAGE_WORKINGDIR ?= ""
61OCI_IMAGE_STOPSIGNAL ?= "" 67OCI_IMAGE_STOPSIGNAL ?= ""
62 68
@@ -112,6 +118,27 @@ OCI_IMAGE_BUILD_DATE ?= ""
112# Enable/disable auto-detection of git metadata (set to "0" to disable) 118# Enable/disable auto-detection of git metadata (set to "0" to disable)
113OCI_IMAGE_AUTO_LABELS ?= "1" 119OCI_IMAGE_AUTO_LABELS ?= "1"
114 120
121# =============================================================================
122# Multi-Layer OCI Support
123# =============================================================================
124#
125# OCI_BASE_IMAGE: Base image to build on top of
126# - Recipe name: "container-base" (uses local recipe's OCI output)
127# - Path: "/path/to/oci-dir" (uses existing OCI layout)
128# - Registry URL: "docker.io/library/alpine:3.19" (fetches external image)
129#
130# OCI_LAYER_MODE: How to create layers
131# - "single" (default): Single layer with complete rootfs (backward compatible)
132# - "multi": Multiple layers from OCI_LAYERS definitions
133#
134# When OCI_BASE_IMAGE is set:
135# - Base image layers are preserved
136# - New content from IMAGE_ROOTFS is added as additional layer(s)
137#
138OCI_BASE_IMAGE ?= ""
139OCI_BASE_IMAGE_TAG ?= "latest"
140OCI_LAYER_MODE ?= "single"
141
115# whether the oci image dir should be left as a directory, or 142# whether the oci image dir should be left as a directory, or
116# bundled into a tarball. 143# bundled into a tarball.
117OCI_IMAGE_TAR_OUTPUT ?= "true" 144OCI_IMAGE_TAR_OUTPUT ?= "true"
@@ -131,6 +158,82 @@ def oci_map_subarch(a, f, d):
131 return '' 158 return ''
132 return '' 159 return ''
133 160
161# =============================================================================
162# Base Image Resolution and Dependency Setup
163# =============================================================================
164
165def oci_resolve_base_image(d):
166 """Resolve OCI_BASE_IMAGE to determine its type.
167
168 Returns dict with 'type' key:
169 - {'type': 'recipe', 'name': 'container-base'}
170 - {'type': 'path', 'path': '/path/to/oci-dir'}
171 - {'type': 'remote', 'url': 'docker.io/library/alpine:3.19'}
172 - None if no base image
173 """
174 base = d.getVar('OCI_BASE_IMAGE') or ''
175 if not base:
176 return None
177
178 # Check if it's a path (starts with /)
179 if base.startswith('/'):
180 return {'type': 'path', 'path': base}
181
182 # Check if it looks like a registry URL (contains / or has registry prefix)
183 if '/' in base or '.' in base.split(':')[0]:
184 return {'type': 'remote', 'url': base}
185
186 # Assume it's a recipe name
187 return {'type': 'recipe', 'name': base}
188
189python __anonymous() {
190 import os
191
192 backend = d.getVar('OCI_IMAGE_BACKEND') or 'umoci'
193 base_image = d.getVar('OCI_BASE_IMAGE') or ''
194 layer_mode = d.getVar('OCI_LAYER_MODE') or 'single'
195
196 # sloci doesn't support multi-layer
197 if backend == 'sloci-image':
198 if layer_mode != 'single' or base_image:
199 bb.fatal("Multi-layer OCI requires umoci backend. "
200 "Set OCI_IMAGE_BACKEND = 'umoci' or remove OCI_BASE_IMAGE")
201
202 # Resolve base image and set up dependencies
203 if base_image:
204 resolved = oci_resolve_base_image(d)
205 if resolved:
206 if resolved['type'] == 'recipe':
207 # Add dependency on base recipe's OCI output
208 # Use do_build as it works for both image recipes and oci-fetch recipes
209 base_recipe = resolved['name']
210 d.setVar('_OCI_BASE_RECIPE', base_recipe)
211 d.appendVarFlag('do_image_oci', 'depends',
212 f" {base_recipe}:do_build rsync-native:do_populate_sysroot")
213 bb.debug(1, f"OCI: Using base image from recipe: {base_recipe}")
214
215 elif resolved['type'] == 'path':
216 d.setVar('_OCI_BASE_PATH', resolved['path'])
217 d.appendVarFlag('do_image_oci', 'depends',
218 " rsync-native:do_populate_sysroot")
219 bb.debug(1, f"OCI: Using base image from path: {resolved['path']}")
220
221 elif resolved['type'] == 'remote':
222 # Remote URLs are not supported directly - use a container-bundle recipe
223 remote_url = resolved['url']
224 # Create sanitized key for CONTAINER_DIGESTS varflag
225 sanitized_key = remote_url.replace('/', '_').replace(':', '_')
226 bb.fatal(f"Remote base images cannot be used directly: {remote_url}\n\n"
227 f"Create a container-bundle recipe to fetch the external image:\n\n"
228 f" # recipes-containers/oci-base-images/my-base.bb\n"
229 f" inherit container-bundle\n"
230 f" CONTAINER_BUNDLES = \"{remote_url}\"\n"
231 f" CONTAINER_DIGESTS[{sanitized_key}] = \"sha256:...\"\n"
232 f" CONTAINER_BUNDLE_DEPLOY = \"1\"\n\n"
233 f"Get digest with: skopeo inspect docker://{remote_url} | jq -r '.Digest'\n\n"
234 f"Then use: OCI_BASE_IMAGE = \"my-base\"")
235}
236
134# the IMAGE_CMD:oci comes from the .inc 237# the IMAGE_CMD:oci comes from the .inc
135OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}" 238OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}"
136include ${OCI_IMAGE_BACKEND_INC} 239include ${OCI_IMAGE_BACKEND_INC}