diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-15 21:50:38 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-21 18:00:26 -0500 |
| commit | 02bce5b72e8725ba58d82627c780e376ac59a84b (patch) | |
| tree | 8e01635166b3d475fbf4fef34150ccde7ccda21a /classes | |
| parent | 640fc9278435c49b8c59d78c18d024c66b3d6e6a (diff) | |
| download | meta-virtualization-02bce5b72e8725ba58d82627c780e376ac59a84b.tar.gz | |
vcontainer: add multi-arch OCI support
Add functions to detect and handle multi-architecture OCI Image Index
format with automatic platform selection during import. Also add
oci-multiarch.bbclass for build-time multi-arch OCI creation.
Runtime support (vcontainer-common.sh):
- is_oci_image_index() - detect multi-arch OCI images
- get_oci_platforms() - list available platforms
- select_platform_manifest() - select manifest for target architecture
- extract_platform_oci() - extract single platform to new OCI dir
- normalize_arch_to_oci/from_oci() - architecture name mapping
- Update vimport to auto-select platform from multi-arch images
Build-time support (oci-multiarch.bbclass):
- Create OCI Image Index from multiconfig builds
- Collect images from vruntime-aarch64, vruntime-x86-64
- Combine blobs and create unified manifest list
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'classes')
| -rw-r--r-- | classes/oci-multiarch.bbclass | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/classes/oci-multiarch.bbclass b/classes/oci-multiarch.bbclass new file mode 100644 index 00000000..9960b7cc --- /dev/null +++ b/classes/oci-multiarch.bbclass | |||
| @@ -0,0 +1,243 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | # | ||
| 5 | # oci-multiarch.bbclass | ||
| 6 | # =========================================================================== | ||
| 7 | # Build multi-architecture OCI container images locally | ||
| 8 | # =========================================================================== | ||
| 9 | # | ||
| 10 | # This class creates OCI Image Index (manifest list) from multiple | ||
| 11 | # single-architecture OCI images built via multiconfig. | ||
| 12 | # | ||
| 13 | # USAGE: | ||
| 14 | # # In your recipe (e.g., myapp-container-multiarch.bb): | ||
| 15 | # inherit oci-multiarch | ||
| 16 | # | ||
| 17 | # OCI_MULTIARCH_RECIPE = "myapp-container" | ||
| 18 | # OCI_MULTIARCH_PLATFORMS = "aarch64 x86_64" | ||
| 19 | # | ||
| 20 | # # Optional: custom multiconfig mapping | ||
| 21 | # OCI_MULTIARCH_MC[aarch64] = "vruntime-aarch64" | ||
| 22 | # OCI_MULTIARCH_MC[x86_64] = "vruntime-x86-64" | ||
| 23 | # | ||
| 24 | # OUTPUT: | ||
| 25 | # ${DEPLOY_DIR_IMAGE}/${PN}-multiarch-oci/ | ||
| 26 | # index.json - OCI Image Index with platform entries | ||
| 27 | # oci-layout - OCI layout marker | ||
| 28 | # blobs/sha256/ - Combined blobs from all architectures | ||
| 29 | # | ||
| 30 | # REQUIREMENTS: | ||
| 31 | # - Multiconfig must be enabled in local.conf: | ||
| 32 | # BBMULTICONFIG = "vruntime-aarch64 vruntime-x86-64" | ||
| 33 | # - OCI_MULTIARCH_RECIPE must inherit image-oci or produce OCI output | ||
| 34 | # | ||
| 35 | # =========================================================================== | ||
| 36 | |||
| 37 | inherit nopackages | ||
| 38 | |||
| 39 | INHIBIT_DEFAULT_DEPS = "1" | ||
| 40 | |||
| 41 | # Required variables | ||
| 42 | OCI_MULTIARCH_RECIPE ?= "" | ||
| 43 | OCI_MULTIARCH_PLATFORMS ?= "" | ||
| 44 | |||
| 45 | # Default multiconfig mapping (uses existing vruntime-* configs) | ||
| 46 | OCI_MULTIARCH_MC[aarch64] ?= "vruntime-aarch64" | ||
| 47 | OCI_MULTIARCH_MC[x86_64] ?= "vruntime-x86-64" | ||
| 48 | |||
| 49 | # Machine mapping for deploy directory paths | ||
| 50 | OCI_MULTIARCH_MACHINE[aarch64] ?= "qemuarm64" | ||
| 51 | OCI_MULTIARCH_MACHINE[x86_64] ?= "qemux86-64" | ||
| 52 | |||
| 53 | # Architecture to OCI platform name mapping | ||
| 54 | OCI_ARCH_TO_PLATFORM[aarch64] = "arm64" | ||
| 55 | OCI_ARCH_TO_PLATFORM[x86_64] = "amd64" | ||
| 56 | |||
| 57 | # Output directory | ||
| 58 | OCI_MULTIARCH_OUTPUT = "${DEPLOY_DIR_IMAGE}/${PN}-multiarch-oci" | ||
| 59 | |||
| 60 | # Delete standard tasks we don't need | ||
| 61 | deltask do_fetch | ||
| 62 | deltask do_unpack | ||
| 63 | deltask do_patch | ||
| 64 | deltask do_configure | ||
| 65 | deltask do_compile | ||
| 66 | deltask do_install | ||
| 67 | deltask do_populate_lic | ||
| 68 | deltask do_populate_sysroot | ||
| 69 | deltask do_package | ||
| 70 | deltask do_package_qa | ||
| 71 | deltask do_packagedata | ||
| 72 | |||
| 73 | # Generate mcdepends at parse time | ||
| 74 | python __anonymous() { | ||
| 75 | recipe = d.getVar('OCI_MULTIARCH_RECIPE') | ||
| 76 | platforms = d.getVar('OCI_MULTIARCH_PLATFORMS') | ||
| 77 | |||
| 78 | if not recipe: | ||
| 79 | bb.fatal("OCI_MULTIARCH_RECIPE must be set") | ||
| 80 | |||
| 81 | if not platforms: | ||
| 82 | bb.fatal("OCI_MULTIARCH_PLATFORMS must be set (e.g., 'aarch64 x86_64')") | ||
| 83 | |||
| 84 | # Build mcdepends string for each platform | ||
| 85 | mcdepends = [] | ||
| 86 | for platform in platforms.split(): | ||
| 87 | mc = d.getVarFlag('OCI_MULTIARCH_MC', platform) | ||
| 88 | if not mc: | ||
| 89 | bb.fatal(f"No multiconfig defined for platform '{platform}'. Set OCI_MULTIARCH_MC[{platform}]") | ||
| 90 | mcdepends.append(f"mc::{mc}:{recipe}:do_image_oci") | ||
| 91 | |||
| 92 | # Set the mcdepends for our main task | ||
| 93 | d.setVarFlag('do_create_multiarch_index', 'mcdepends', ' '.join(mcdepends)) | ||
| 94 | |||
| 95 | bb.note(f"OCI multi-arch: building {recipe} for platforms: {platforms}") | ||
| 96 | } | ||
| 97 | |||
| 98 | python do_create_multiarch_index() { | ||
| 99 | import os | ||
| 100 | import json | ||
| 101 | import shutil | ||
| 102 | import hashlib | ||
| 103 | |||
| 104 | recipe = d.getVar('OCI_MULTIARCH_RECIPE') | ||
| 105 | platforms = d.getVar('OCI_MULTIARCH_PLATFORMS').split() | ||
| 106 | topdir = d.getVar('TOPDIR') | ||
| 107 | output_dir = d.getVar('OCI_MULTIARCH_OUTPUT') | ||
| 108 | |||
| 109 | bb.plain(f"Creating multi-arch OCI Image Index for {recipe}") | ||
| 110 | bb.plain(f"Platforms: {' '.join(platforms)}") | ||
| 111 | |||
| 112 | # Clean output directory | ||
| 113 | if os.path.exists(output_dir): | ||
| 114 | shutil.rmtree(output_dir) | ||
| 115 | os.makedirs(os.path.join(output_dir, 'blobs', 'sha256')) | ||
| 116 | |||
| 117 | # Collect manifests from each platform | ||
| 118 | index_manifests = [] | ||
| 119 | |||
| 120 | for platform in platforms: | ||
| 121 | mc = d.getVarFlag('OCI_MULTIARCH_MC', platform) | ||
| 122 | machine = d.getVarFlag('OCI_MULTIARCH_MACHINE', platform) | ||
| 123 | oci_platform = d.getVarFlag('OCI_ARCH_TO_PLATFORM', platform) or platform | ||
| 124 | |||
| 125 | if not mc or not machine: | ||
| 126 | bb.fatal(f"Missing configuration for platform {platform}") | ||
| 127 | |||
| 128 | # Find the OCI image in the multiconfig's deploy directory | ||
| 129 | # Pattern: tmp-<mc>/deploy/images/<machine>/<recipe>-latest-oci/ | ||
| 130 | mc_deploy_base = os.path.join(topdir, f'tmp-{mc}', 'deploy', 'images', machine) | ||
| 131 | |||
| 132 | # Try different naming patterns | ||
| 133 | oci_patterns = [ | ||
| 134 | f"{recipe}-latest-oci", | ||
| 135 | f"{recipe}-{machine}-latest-oci", | ||
| 136 | f"{recipe}-oci", | ||
| 137 | ] | ||
| 138 | |||
| 139 | oci_dir = None | ||
| 140 | for pattern in oci_patterns: | ||
| 141 | candidate = os.path.join(mc_deploy_base, pattern) | ||
| 142 | if os.path.isdir(candidate) and os.path.exists(os.path.join(candidate, 'index.json')): | ||
| 143 | oci_dir = candidate | ||
| 144 | break | ||
| 145 | |||
| 146 | if not oci_dir: | ||
| 147 | bb.fatal(f"OCI image not found for {platform} ({mc}:{recipe})") | ||
| 148 | bb.fatal(f"Looked in: {mc_deploy_base}") | ||
| 149 | continue | ||
| 150 | |||
| 151 | bb.plain(f" Found {platform} OCI: {oci_dir}") | ||
| 152 | |||
| 153 | # Read the source index.json | ||
| 154 | with open(os.path.join(oci_dir, 'index.json'), 'r') as f: | ||
| 155 | src_index = json.load(f) | ||
| 156 | |||
| 157 | # Get the manifest entry (should be first/only one for single-arch) | ||
| 158 | if not src_index.get('manifests'): | ||
| 159 | bb.warn(f"No manifests found in {oci_dir}/index.json") | ||
| 160 | continue | ||
| 161 | |||
| 162 | src_manifest_entry = src_index['manifests'][0] | ||
| 163 | manifest_digest = src_manifest_entry['digest'] | ||
| 164 | manifest_size = src_manifest_entry['size'] | ||
| 165 | |||
| 166 | # Copy all blobs from source to output | ||
| 167 | src_blobs = os.path.join(oci_dir, 'blobs', 'sha256') | ||
| 168 | dst_blobs = os.path.join(output_dir, 'blobs', 'sha256') | ||
| 169 | |||
| 170 | if os.path.isdir(src_blobs): | ||
| 171 | for blob in os.listdir(src_blobs): | ||
| 172 | src_blob = os.path.join(src_blobs, blob) | ||
| 173 | dst_blob = os.path.join(dst_blobs, blob) | ||
| 174 | if not os.path.exists(dst_blob): | ||
| 175 | shutil.copy2(src_blob, dst_blob) | ||
| 176 | bb.note(f" Copied blob: {blob[:12]}...") | ||
| 177 | |||
| 178 | # Create manifest entry with platform info | ||
| 179 | manifest_entry = { | ||
| 180 | 'mediaType': 'application/vnd.oci.image.manifest.v1+json', | ||
| 181 | 'digest': manifest_digest, | ||
| 182 | 'size': manifest_size, | ||
| 183 | 'platform': { | ||
| 184 | 'architecture': oci_platform, | ||
| 185 | 'os': 'linux' | ||
| 186 | } | ||
| 187 | } | ||
| 188 | index_manifests.append(manifest_entry) | ||
| 189 | bb.plain(f" Added {oci_platform}/linux manifest: {manifest_digest[:19]}...") | ||
| 190 | |||
| 191 | if not index_manifests: | ||
| 192 | bb.fatal("No manifests collected - cannot create multi-arch index") | ||
| 193 | |||
| 194 | # Create the OCI Image Index | ||
| 195 | image_index = { | ||
| 196 | 'schemaVersion': 2, | ||
| 197 | 'mediaType': 'application/vnd.oci.image.index.v1+json', | ||
| 198 | 'manifests': index_manifests | ||
| 199 | } | ||
| 200 | |||
| 201 | # Write index.json | ||
| 202 | index_path = os.path.join(output_dir, 'index.json') | ||
| 203 | with open(index_path, 'w') as f: | ||
| 204 | json.dump(image_index, f, indent=2) | ||
| 205 | |||
| 206 | # Write oci-layout | ||
| 207 | layout_path = os.path.join(output_dir, 'oci-layout') | ||
| 208 | with open(layout_path, 'w') as f: | ||
| 209 | json.dump({'imageLayoutVersion': '1.0.0'}, f) | ||
| 210 | |||
| 211 | bb.plain("") | ||
| 212 | bb.plain(f"Created multi-arch OCI Image Index:") | ||
| 213 | bb.plain(f" {output_dir}") | ||
| 214 | bb.plain(f" Platforms: {', '.join(d.getVarFlag('OCI_ARCH_TO_PLATFORM', p) or p for p in platforms)}") | ||
| 215 | bb.plain("") | ||
| 216 | bb.plain("To import into vdkr (will auto-select platform):") | ||
| 217 | bb.plain(f" vdkr vimport {output_dir} {recipe}:latest") | ||
| 218 | } | ||
| 219 | |||
| 220 | addtask do_create_multiarch_index before do_build | ||
| 221 | |||
| 222 | # Stamp includes platforms to rebuild when platforms change | ||
| 223 | do_create_multiarch_index[stamp-extra-info] = "${OCI_MULTIARCH_PLATFORMS}" | ||
| 224 | |||
| 225 | # Deploy the multi-arch OCI | ||
| 226 | python do_deploy() { | ||
| 227 | import os | ||
| 228 | import shutil | ||
| 229 | |||
| 230 | output_dir = d.getVar('OCI_MULTIARCH_OUTPUT') | ||
| 231 | deploy_dir = d.getVar('DEPLOY_DIR_IMAGE') | ||
| 232 | |||
| 233 | # Already deployed in place (output_dir is in deploy_dir) | ||
| 234 | # Just verify it exists | ||
| 235 | if not os.path.exists(os.path.join(output_dir, 'index.json')): | ||
| 236 | bb.fatal(f"Multi-arch OCI not found: {output_dir}") | ||
| 237 | |||
| 238 | bb.plain(f"Multi-arch OCI available at: {output_dir}") | ||
| 239 | } | ||
| 240 | |||
| 241 | addtask do_deploy after do_create_multiarch_index before do_build | ||
| 242 | |||
| 243 | EXCLUDE_FROM_WORLD = "1" | ||
