summaryrefslogtreecommitdiffstats
path: root/classes
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-14 20:59:14 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:32:52 +0000
commit929d1609efefd3189b650facaaeb3d2a13ffbe1d (patch)
treefcbbf69ec29c7a45c3ed31ddb353c4927650f2fa /classes
parent4fd9190b7f2f7260b90c7de1609944c96fcf6f64 (diff)
downloadmeta-virtualization-929d1609efefd3189b650facaaeb3d2a13ffbe1d.tar.gz
image-oci: add layer caching for multi-layer OCI builds
Add layer caching to speed up multi-layer OCI image rebuilds. When enabled, pre-installed package layers are cached to disk and restored on subsequent builds, avoiding repeated package installation. New variables: - OCI_LAYER_CACHE: Enable/disable caching (default "1") - OCI_LAYER_CACHE_DIR: Cache location (default ${TOPDIR}/oci-layer-cache/${MACHINE}) Cache key is computed from: - Layer name and type - Sorted package list - Package versions from PKGDATA_DIR - MACHINE and TUNE_PKGARCH Cache automatically invalidates when: - Package versions change - Layer definition changes - Architecture changes Benefits: - First build: ~10-30s per layer (cache miss, packages installed) - Subsequent builds: ~1s per layer (cache hit, files copied) - Shared across recipes with identical layer definitions Build log shows cache status: NOTE: OCI Cache HIT: Layer 'base' (be88c180f651416b) NOTE: OCI: Pre-installed packages for 3 layers (cache: 3 hits, 0 misses) Also adds comprehensive pytest suite for multi-layer OCI functionality including tests for 1/2/3 layer modes and cache behavior. Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
-rw-r--r--classes/image-oci-umoci.inc177
-rw-r--r--classes/image-oci.bbclass26
2 files changed, 201 insertions, 2 deletions
diff --git a/classes/image-oci-umoci.inc b/classes/image-oci-umoci.inc
index 1ff36718..fbb77cd0 100644
--- a/classes/image-oci-umoci.inc
+++ b/classes/image-oci-umoci.inc
@@ -5,12 +5,152 @@
5# directories using Yocto's package manager classes. The shell code then copies 5# directories using Yocto's package manager classes. The shell code then copies
6# from these pre-installed directories. 6# from these pre-installed directories.
7 7
8def oci_compute_layer_cache_key(d, layer_name, layer_type, layer_packages):
9 """
10 Compute a cache key for a layer based on its definition and package versions.
11
12 The cache key is a SHA256 hash of:
13 - Layer name and type
14 - Sorted package list
15 - Package versions from PKGDATA_DIR
16 - Machine and architecture info
17
18 Returns: (cache_key, cache_info) tuple where cache_info is human-readable
19 """
20 import hashlib
21 import os
22 import json
23
24 packages = sorted(layer_packages.split())
25 pkgdata_dir = d.getVar('PKGDATA_DIR')
26 machine = d.getVar('MACHINE')
27 tune_pkgarch = d.getVar('TUNE_PKGARCH')
28
29 # Build cache key components
30 cache_components = {
31 'layer_name': layer_name,
32 'layer_type': layer_type,
33 'packages': packages,
34 'machine': machine,
35 'tune_pkgarch': tune_pkgarch,
36 'pkg_versions': {}
37 }
38
39 # Get package versions from pkgdata
40 for pkg in packages:
41 pkg_info_file = os.path.join(pkgdata_dir, 'runtime', pkg)
42 if os.path.exists(pkg_info_file):
43 try:
44 with open(pkg_info_file, 'r') as f:
45 for line in f:
46 if line.startswith('PKGV:'):
47 cache_components['pkg_versions'][pkg] = line.split(':', 1)[1].strip()
48 break
49 elif line.startswith(f'PKGV_{pkg}:'):
50 cache_components['pkg_versions'][pkg] = line.split(':', 1)[1].strip()
51 break
52 except Exception:
53 pass
54
55 # Create deterministic JSON for hashing
56 cache_json = json.dumps(cache_components, sort_keys=True)
57 cache_key = hashlib.sha256(cache_json.encode()).hexdigest()[:16]
58
59 # Human-readable info for logging
60 pkg_vers = [f"{p}={cache_components['pkg_versions'].get(p, '?')}" for p in packages[:3]]
61 if len(packages) > 3:
62 pkg_vers.append(f"...+{len(packages)-3} more")
63 cache_info = f"{layer_name}:{' '.join(pkg_vers)}"
64
65 return cache_key, cache_info
66
67
68def oci_check_layer_cache(d, cache_key, layer_name):
69 """
70 Check if a cached layer exists.
71
72 Returns: path to cached layer rootfs if found, None otherwise
73 """
74 import os
75
76 cache_dir = d.getVar('OCI_LAYER_CACHE_DIR')
77 if not cache_dir:
78 return None
79
80 cached_path = os.path.join(cache_dir, f'{cache_key}-{layer_name}')
81 marker_file = os.path.join(cached_path, '.oci-layer-cache')
82
83 if os.path.isdir(cached_path) and os.path.exists(marker_file):
84 return cached_path
85
86 return None
87
88
89def oci_cache_layer(d, cache_key, layer_name, layer_rootfs):
90 """
91 Save a layer rootfs to the cache.
92 """
93 import os
94 import shutil
95 import time
96
97 cache_dir = d.getVar('OCI_LAYER_CACHE_DIR')
98 if not cache_dir:
99 return
100
101 bb.utils.mkdirhier(cache_dir)
102
103 cached_path = os.path.join(cache_dir, f'{cache_key}-{layer_name}')
104
105 # Remove any existing cache for this key
106 if os.path.exists(cached_path):
107 shutil.rmtree(cached_path)
108
109 # Copy layer rootfs to cache
110 bb.note(f"OCI Cache: Saving layer '{layer_name}' to cache ({cache_key})")
111 shutil.copytree(layer_rootfs, cached_path, symlinks=True)
112
113 # Write cache marker with metadata
114 marker_file = os.path.join(cached_path, '.oci-layer-cache')
115 with open(marker_file, 'w') as f:
116 f.write(f"cache_key={cache_key}\n")
117 f.write(f"layer_name={layer_name}\n")
118 f.write(f"created={time.strftime('%Y-%m-%dT%H:%M:%SZ')}\n")
119 f.write(f"machine={d.getVar('MACHINE')}\n")
120
121
122def oci_restore_layer_from_cache(d, cached_path, layer_rootfs):
123 """
124 Restore a layer rootfs from the cache.
125 """
126 import os
127 import shutil
128
129 # Ensure target directory exists and is empty
130 if os.path.exists(layer_rootfs):
131 shutil.rmtree(layer_rootfs)
132 bb.utils.mkdirhier(layer_rootfs)
133
134 # Copy cached content (excluding the cache marker)
135 for item in os.listdir(cached_path):
136 if item == '.oci-layer-cache':
137 continue
138 src = os.path.join(cached_path, item)
139 dst = os.path.join(layer_rootfs, item)
140 if os.path.isdir(src):
141 shutil.copytree(src, dst, symlinks=True)
142 else:
143 shutil.copy2(src, dst)
144
145
8python oci_multilayer_install_packages() { 146python oci_multilayer_install_packages() {
9 """ 147 """
10 Pre-install packages for each packages layer in OCI_LAYERS. 148 Pre-install packages for each packages layer in OCI_LAYERS.
11 149
12 Creates temp rootfs directories with packages installed using Yocto's PM. 150 Creates temp rootfs directories with packages installed using Yocto's PM.
13 The shell IMAGE_CMD:oci then copies from these directories. 151 The shell IMAGE_CMD:oci then copies from these directories.
152
153 Supports layer caching when OCI_LAYER_CACHE = "1" to speed up rebuilds.
14 """ 154 """
15 import os 155 import os
16 import shutil 156 import shutil
@@ -32,7 +172,15 @@ python oci_multilayer_install_packages() {
32 shutil.rmtree(layer_rootfs_base) 172 shutil.rmtree(layer_rootfs_base)
33 bb.utils.mkdirhier(layer_rootfs_base) 173 bb.utils.mkdirhier(layer_rootfs_base)
34 174
35 bb.note("OCI: Pre-installing packages for multi-layer mode") 175 # Check if caching is enabled
176 cache_enabled = d.getVar('OCI_LAYER_CACHE') == '1'
177 if cache_enabled:
178 bb.note("OCI: Pre-installing packages for multi-layer mode (caching enabled)")
179 else:
180 bb.note("OCI: Pre-installing packages for multi-layer mode (caching disabled)")
181
182 cache_hits = 0
183 cache_misses = 0
36 184
37 # Parse OCI_LAYERS and install packages for each packages layer 185 # Parse OCI_LAYERS and install packages for each packages layer
38 layer_num = 0 186 layer_num = 0
@@ -48,17 +196,42 @@ python oci_multilayer_install_packages() {
48 layer_num += 1 196 layer_num += 1
49 layer_rootfs = os.path.join(layer_rootfs_base, f'layer-{layer_num}-{layer_name}') 197 layer_rootfs = os.path.join(layer_rootfs_base, f'layer-{layer_num}-{layer_name}')
50 198
199 # Check cache if enabled
200 if cache_enabled:
201 cache_key, cache_info = oci_compute_layer_cache_key(d, layer_name, layer_type, layer_content)
202 cached_path = oci_check_layer_cache(d, cache_key, layer_name)
203
204 if cached_path:
205 bb.note(f"OCI Cache HIT: Layer '{layer_name}' ({cache_key})")
206 oci_restore_layer_from_cache(d, cached_path, layer_rootfs)
207 cache_hits += 1
208 # Store the path for the shell code to use
209 d.setVar(f'OCI_LAYER_{layer_num}_ROOTFS', layer_rootfs)
210 d.setVar(f'OCI_LAYER_{layer_num}_NAME', layer_name)
211 continue
212 else:
213 bb.note(f"OCI Cache MISS: Layer '{layer_name}' ({cache_info})")
214 cache_misses += 1
215
51 bb.note(f"OCI: Pre-installing layer {layer_num} '{layer_name}' to {layer_rootfs}") 216 bb.note(f"OCI: Pre-installing layer {layer_num} '{layer_name}' to {layer_rootfs}")
52 217
53 # Call the package installation function 218 # Call the package installation function
54 oci_install_layer_packages(d, layer_rootfs, layer_content, layer_name) 219 oci_install_layer_packages(d, layer_rootfs, layer_content, layer_name)
55 220
221 # Cache the installed layer if caching is enabled
222 if cache_enabled:
223 oci_cache_layer(d, cache_key, layer_name, layer_rootfs)
224
56 # Store the path for the shell code to use 225 # Store the path for the shell code to use
57 d.setVar(f'OCI_LAYER_{layer_num}_ROOTFS', layer_rootfs) 226 d.setVar(f'OCI_LAYER_{layer_num}_ROOTFS', layer_rootfs)
58 d.setVar(f'OCI_LAYER_{layer_num}_NAME', layer_name) 227 d.setVar(f'OCI_LAYER_{layer_num}_NAME', layer_name)
59 228
60 d.setVar('OCI_LAYER_COUNT', str(layer_num)) 229 d.setVar('OCI_LAYER_COUNT', str(layer_num))
61 bb.note(f"OCI: Pre-installed packages for {layer_num} layers") 230
231 if cache_enabled:
232 bb.note(f"OCI: Pre-installed packages for {layer_num} layers (cache: {cache_hits} hits, {cache_misses} misses)")
233 else:
234 bb.note(f"OCI: Pre-installed packages for {layer_num} layers")
62} 235}
63 236
64# Run the Python function before IMAGE_CMD:oci 237# Run the Python function before IMAGE_CMD:oci
diff --git a/classes/image-oci.bbclass b/classes/image-oci.bbclass
index 64b17d97..41f7b3d1 100644
--- a/classes/image-oci.bbclass
+++ b/classes/image-oci.bbclass
@@ -195,6 +195,32 @@ OCI_LAYER_MODE ?= "single"
195# 195#
196OCI_LAYERS ?= "" 196OCI_LAYERS ?= ""
197 197
198# =============================================================================
199# Layer Caching (for multi-layer mode)
200# =============================================================================
201#
202# OCI_LAYER_CACHE: Enable/disable layer caching ("1" or "0")
203# When enabled, pre-installed package layers are cached to avoid
204# reinstalling packages on subsequent builds.
205#
206# OCI_LAYER_CACHE_DIR: Directory for storing cached layers
207# Default: ${TOPDIR}/oci-layer-cache/${MACHINE}
208# Cache is keyed by: layer definition + package versions + architecture
209#
210# Cache key components:
211# - Layer name and type
212# - Sorted package list
213# - Package versions (from PKGDATA_DIR)
214# - MACHINE and TUNE_PKGARCH
215#
216# Cache invalidation:
217# - Any package version change invalidates layers containing that package
218# - Layer definition changes invalidate that specific layer
219# - MACHINE/arch changes use separate cache directories
220#
221OCI_LAYER_CACHE ?= "1"
222OCI_LAYER_CACHE_DIR ?= "${TOPDIR}/oci-layer-cache/${MACHINE}"
223
198# whether the oci image dir should be left as a directory, or 224# whether the oci image dir should be left as a directory, or
199# bundled into a tarball. 225# bundled into a tarball.
200OCI_IMAGE_TAR_OUTPUT ?= "true" 226OCI_IMAGE_TAR_OUTPUT ?= "true"