From cc21ccbb20ff11e37276cfb846bea5ffd7f4a3cf Mon Sep 17 00:00:00 2001 From: Vince Perri <5596945+vinceaperri@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:19:21 +0000 Subject: [PATCH 1/7] new changes --- toolkit/tools/internal/targetos/initrd.go | 120 ++++++++++++++++++ toolkit/tools/internal/targetos/targetos.go | 53 +++++++- .../internal/targetos/zstd_readcloser.go | 16 +++ .../artifactsinputoutput.go | 2 +- .../pkg/imagecustomizerlib/customizeuki.go | 14 +- .../pkg/imagecustomizerlib/distrohandler.go | 15 +++ .../imagecustomizerlib/distrohandler_acl.go | 4 + .../distrohandler_azurelinux.go | 4 + .../distrohandler_azurelinux4.go | 4 + .../distrohandler_fedora.go | 53 +++++++- .../distrohandler_ubuntu.go | 4 + .../pkg/imagecustomizerlib/imagecustomizer.go | 2 +- .../pkg/imagecustomizerlib/imageutils.go | 2 +- .../liveosisoartifactstore.go | 55 +++++++- .../imagecustomizerlib/liveosisobuilder.go | 14 +- .../pkg/imagecustomizerlib/liveosisoutils.go | 24 ++-- .../tools/pkg/imagecustomizerlib/liveospxe.go | 11 +- 17 files changed, 349 insertions(+), 48 deletions(-) create mode 100644 toolkit/tools/internal/targetos/initrd.go create mode 100644 toolkit/tools/internal/targetos/zstd_readcloser.go diff --git a/toolkit/tools/internal/targetos/initrd.go b/toolkit/tools/internal/targetos/initrd.go new file mode 100644 index 0000000000..f651e06430 --- /dev/null +++ b/toolkit/tools/internal/targetos/initrd.go @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package targetos + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/fs" + "os" + + "github.com/cavaliergopher/cpio" + "github.com/klauspost/compress/zstd" + "github.com/klauspost/pgzip" +) + +// Magic byte signatures for the compression formats we recognize at the start +// of an initramfs stream. +// +// - gzip: RFC 1952 section 2.3.1 ("ID1 ID2" = 1F 8B) +// https://www.rfc-editor.org/rfc/rfc1952#section-2.3.1 +// - zstd: RFC 8478 section 3.1.1 ("Magic_Number" = 0xFD2FB528 little-endian) +// https://www.rfc-editor.org/rfc/rfc8478#section-3.1.1 +var ( + magicGzip = []byte{0x1f, 0x8b} + magicZstd = []byte{0x28, 0xb5, 0x2f, 0xfd} + longestMagicSize = len(magicZstd) +) + +// readFirstFileFromInitrd scans an initramfs cpio archive once and returns the content of the first candidate path in +// the provided list that exists as a regular file. Returns an error wrapping fs.ErrNotExist if none of the candidates +// is present. +func readFirstFileFromInitrd(initrdPath string, candidates []string) (content []byte, foundPath string, err error) { + f, err := os.Open(initrdPath) + if err != nil { + return nil, "", fmt.Errorf("failed to open initrd (%s):\n%w", initrdPath, err) + } + defer f.Close() + + decompressed, err := openInitrdDecompressor(f) + if err != nil { + return nil, "", fmt.Errorf("failed to decompress initrd (%s):\n%w", initrdPath, err) + } + defer decompressed.Close() + + wanted := make(map[string]struct{}, len(candidates)) + for _, c := range candidates { + wanted[c] = struct{}{} + } + + found := make(map[string][]byte, len(candidates)) + cpioReader := cpio.NewReader(decompressed) + for { + hdr, err := cpioReader.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, "", fmt.Errorf("failed to read cpio header from initrd (%s):\n%w", initrdPath, err) + } + + if _, ok := wanted[hdr.Name]; !ok { + continue + } + + // Only read regular files. + if hdr.Mode&cpio.ModeType != cpio.TypeReg { + continue + } + + data, err := io.ReadAll(cpioReader) + if err != nil { + return nil, "", fmt.Errorf("failed to read (%s) from initrd (%s):\n%w", hdr.Name, initrdPath, err) + } + + found[hdr.Name] = data + } + + for _, candidate := range candidates { + if data, ok := found[candidate]; ok { + return data, candidate, nil + } + } + + return nil, "", fmt.Errorf("failed to find any of %v in initrd (%s): %w", candidates, initrdPath, fs.ErrNotExist) +} + +// openInitrdDecompressor auto-detects the compression format of an initramfs stream from its leading magic bytes and +// returns a reader over the decompressed (cpio) content. +func openInitrdDecompressor(r io.Reader) (io.ReadCloser, error) { + br := bufio.NewReader(r) + head, err := br.Peek(longestMagicSize) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("failed to peek initrd magic bytes:\n%w", err) + } + + switch { + case bytes.HasPrefix(head, magicGzip): + gz, err := pgzip.NewReader(br) + if err != nil { + return nil, fmt.Errorf("failed to open gzip reader for initrd:\n%w", err) + } + return gz, nil + + case bytes.HasPrefix(head, magicZstd): + zr, err := zstd.NewReader(br) + if err != nil { + return nil, fmt.Errorf("failed to open zstd reader for initrd:\n%w", err) + } + + // zstd.Decoder satisfies io.Reader but not io.Closer since its Close() returns no error, so wrap it to + // implement io.ReadCloser, like pgzip.Reader. + return zstdReadCloser{Decoder: zr}, nil + + default: + return nil, fmt.Errorf("unrecognized initrd compression format (leading bytes: % x)", head) + } +} diff --git a/toolkit/tools/internal/targetos/targetos.go b/toolkit/tools/internal/targetos/targetos.go index 164e5d4c4e..c3e994e4ce 100644 --- a/toolkit/tools/internal/targetos/targetos.go +++ b/toolkit/tools/internal/targetos/targetos.go @@ -74,10 +74,25 @@ var ( Version: []int{24, 4}, } + // OsReleaseCandidates lists the on-rootfs paths to probe for the os-release(5) file, in preference order. OsReleaseFileCandidates = []string{ "/etc/os-release", "/usr/lib/os-release", } + + // initrdReleaseCandidates lists the in-initrd paths to probe for an os-release(5)-type file, in preference order. + // + // os-release is the canonical filename and is the only candidate present in full-OS initrds built by Image + // Customizer (which pack the source rootfs directly, in which initrd-release does not exist at all). + // + // initrd-release is the dracut-runtime variant emitted by dracut's 99base module. os-release symlinks to this file + // in such cases, so it is a necessary fallback. + initrdReleaseCandidates = []string{ + "/etc/os-release", + "/usr/lib/os-release", + "/etc/initrd-release", + "/usr/lib/initrd-release", + } ) func New(distroId Distro, versionId string) TargetOs { @@ -90,11 +105,13 @@ func New(distroId Distro, versionId string) TargetOs { } } +// GetInstalledTargetOs reads the os-release(5) file from the given rootfs and resolves it to a TargetOs. +// +// Returns an error wrapping fs.ErrNotExist when none of the candidates exist on the rootfs. func GetInstalledTargetOs(rootfs string) (TargetOs, error) { var err error var fields map[string]string - found := false for _, candidate := range OsReleaseFileCandidates { fields, err = envfile.ParseEnvFile(filepath.Join(rootfs, candidate)) if errors.Is(err, fs.ErrNotExist) { @@ -104,23 +121,45 @@ func GetInstalledTargetOs(rootfs string) (TargetOs, error) { return TargetOs{}, fmt.Errorf("failed to read os-release file (%s):\n%w", candidate, err) } - found = true break } - if !found { + if fields == nil { return TargetOs{}, fmt.Errorf("no os-release file found (candidates=%s):\n%w", OsReleaseFileCandidates, err) } - targetOs, err := GetInstalledTargetOsFromEnvFields(fields) + targetOs, err := GetInstalledTargetOsFromEnvFields(fields, "os-release") + if err != nil { + return TargetOs{}, fmt.Errorf("failed to determine target OS from os-release file:\n%w", err) + } + + return targetOs, nil +} + +// GetInitrdTargetOs reads an os-release(5)-type file (os-release or dracut's initrd-release) from the given initrd and +// resolves it to a TargetOs. +// +// Returns an error wrapping fs.ErrNotExist when none of the candidates exist in the initrd. +func GetInitrdTargetOs(initrdPath string) (TargetOs, error) { + content, foundPath, err := readFirstFileFromInitrd(initrdPath, initrdReleaseCandidates) if err != nil { return TargetOs{}, err } - return targetOs, err + fields, err := envfile.ParseEnv(string(content)) + if err != nil { + return TargetOs{}, fmt.Errorf("failed to read (%s) file from initrd (%s):\n%w", foundPath, initrdPath, err) + } + + targetOs, err := GetInstalledTargetOsFromEnvFields(fields, "initrd-release") + if err != nil { + return TargetOs{}, fmt.Errorf("failed to determine target OS from initrd-release file:\n%w", err) + } + + return targetOs, nil } -func GetInstalledTargetOsFromEnvFields(fields map[string]string) (TargetOs, error) { +func GetInstalledTargetOsFromEnvFields(fields map[string]string, sourceLabel string) (TargetOs, error) { distroId := fields["ID"] versionId := fields["VERSION_ID"] @@ -171,7 +210,7 @@ func GetInstalledTargetOsFromEnvFields(fields map[string]string) (TargetOs, erro }, nil default: - return TargetOs{}, fmt.Errorf("unknown ID (%s) in os-release", distroId) + return TargetOs{}, fmt.Errorf("unknown ID (%s) in %s", distroId, sourceLabel) } } diff --git a/toolkit/tools/internal/targetos/zstd_readcloser.go b/toolkit/tools/internal/targetos/zstd_readcloser.go new file mode 100644 index 0000000000..02ee783110 --- /dev/null +++ b/toolkit/tools/internal/targetos/zstd_readcloser.go @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package targetos + +import ( + "github.com/klauspost/compress/zstd" +) + +// zstdReadCloser adapts *zstd.Decoder (whose Close() returns no error) to io.ReadCloser. +type zstdReadCloser struct{ *zstd.Decoder } + +func (z zstdReadCloser) Close() error { + z.Decoder.Close() + return nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput.go b/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput.go index 4d09a486c5..447fe86522 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput.go +++ b/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput.go @@ -96,7 +96,7 @@ func outputArtifacts(ctx context.Context, items []imagecustomizerapi.OutputArtif defer systemBootPartitionMount.Close() // Detect system architecture - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go index e791135927..39f0601d7e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go @@ -258,7 +258,7 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer if uki.Mode == imagecustomizerapi.UkiModeModify { logger.Log.Infof("UKI mode is 'modify', skipping UKI preparation (will modify addon only)") - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } @@ -295,7 +295,7 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer } // Detect system architecture. - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } @@ -503,13 +503,13 @@ func findKernelsAndInitramfs(bootDir string) (map[string]string, error) { } // createUkiInModifyMode modifies UKI addons without touching the main UKI files. -func createUkiInModifyMode(ctx context.Context, rc *ResolvedConfig) error { +func createUkiInModifyMode(ctx context.Context, rc *ResolvedConfig, distroHandler DistroHandler) error { _, span := otel.GetTracerProvider().Tracer(OtelTracerName).Start(ctx, "customize_uki_modify_mode") defer span.End() var err error - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } @@ -618,7 +618,7 @@ func modifyUkiAddon(ukiFilePath string, stubPath string, rc *ResolvedConfig) err return nil } -func createUki(ctx context.Context, rc *ResolvedConfig) error { +func createUki(ctx context.Context, rc *ResolvedConfig, distroHandler DistroHandler) error { logger.Log.Infof("Creating UKIs") // If mode is 'passthrough', skip UKI creation to preserve existing UKIs @@ -630,7 +630,7 @@ func createUki(ctx context.Context, rc *ResolvedConfig) error { // If mode is 'modify', only modify UKI addons (preserve main UKI) if rc.Uki != nil && rc.Uki.Mode == imagecustomizerapi.UkiModeModify { logger.Log.Infof("UKI mode is 'modify', modifying UKI addons only") - err := createUkiInModifyMode(ctx, rc) + err := createUkiInModifyMode(ctx, rc, distroHandler) if err != nil { return fmt.Errorf("failed to modify UKI addons in modify mode:\n%w", err) } @@ -642,7 +642,7 @@ func createUki(ctx context.Context, rc *ResolvedConfig) error { var err error - _, bootConfig, err := getBootArchConfig() + bootConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go index ab304bcd07..3db9264855 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go @@ -133,6 +133,10 @@ type DistroHandler interface { // Distro has a root partition that is missing placeholder directories for special mounts like /dev. RootMissingMountDirectories() bool + + // GetBootArchConfig returns the boot-files configuration appropriate for this distro on the current runtime + // architecture. + GetBootArchConfig() (BootFilesArchConfig, error) } // NewDistroHandler creates a distro handler directly from TargetOs @@ -185,3 +189,14 @@ func handleUnsupportedDistroVersion(rc *ResolvedConfig, targetOs targetos.Target return nil } + +// NewDistroHandlerFromInitrd creates a distro handler by detecting the OS from the dracut-emitted initrd-release file +// inside the initramfs at initrdPath. Used in ISO-to-ISO pipelines where no rootfs is mounted yet but a canonical +// distro identity is needed to pick the correct boot-file layout. +func NewDistroHandlerFromInitrd(initrdPath string) (DistroHandler, error) { + targetOs, err := targetos.GetInitrdTargetOs(initrdPath) + if err != nil { + return nil, fmt.Errorf("failed to determine the target OS from initrd (%s):\n%w", initrdPath, err) + } + return NewDistroHandler(targetOs) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go index 818f1f9d3c..ca1fb1fcf0 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go @@ -287,3 +287,7 @@ func (d *aclDistroHandler) GrubEfiPackage() string { func (d *aclDistroHandler) RootMissingMountDirectories() bool { return true } + +func (d *aclDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfig) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go index e8acb20244..ae48be5f10 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go @@ -224,3 +224,7 @@ func (d *azureLinuxDistroHandler) GrubEfiPackage() string { func (d *azureLinuxDistroHandler) RootMissingMountDirectories() bool { return false } + +func (d *azureLinuxDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfig) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go index 9c0fe32eb3..581068404b 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go @@ -274,3 +274,7 @@ func (d *azureLinux4DistroHandler) GrubEfiPackage() string { func (d *azureLinux4DistroHandler) RootMissingMountDirectories() bool { return false } + +func (d *azureLinux4DistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfigFedora) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go index e7819f463f..d58d31ab0c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go @@ -29,14 +29,55 @@ type fedoraDistroHandler struct { } const ( - grubEfiPackageFedoraAmd64 = "grub2-efi-x64" - grubEfiPackageFedoraArm64 = "grub2-efi-aa64" - shimPackageFedoraAmd64 = "shim-x64" - shimPackageFedoraArm64 = "shim-aa64" + grubEfiPackageFedoraAmd64 = "grub2-efi-x64" + grubEfiPackageFedoraArm64 = "grub2-efi-aa64" + shimPackageFedoraAmd64 = "shim-x64" + shimPackageFedoraArm64 = "shim-aa64" + + isoBootloaderDirFedora = "/EFI/BOOT" + bootx64BinaryFedora = "BOOTX64.EFI" + bootAA64BinaryFedora = "BOOTAA64.EFI" + grubToolsPackageFedora = "grub2-tools" grubPcModulesPackageFedora = "grub2-pc-modules" ) +// bootloaderFilesConfigFedora is the boot-files map for Fedora-style ESPs (Azure Linux 4 and Fedora). +var bootloaderFilesConfigFedora = map[string]BootFilesArchConfig{ + "amd64": { + bootBinary: bootx64BinaryFedora, + grubBinary: grubx64Binary, + grubNoPrefixBinary: grubx64NoPrefixBinary, + espBootBinaryPath: espBootloaderDir + "/" + bootx64BinaryFedora, + espGrubBinaryPath: espBootloaderDir + "/" + grubx64Binary, + osEspBootBinaryPath: osEspBootloaderDir + "/" + bootx64BinaryFedora, + osEspGrubBinaryPath: osEspBootloaderDir + "/" + grubx64Binary, + osEspGrubNoPrefixBinaryPath: osEspBootloaderDir + "/" + grubx64NoPrefixBinary, + isoBootBinaryPath: isoBootloaderDirFedora + "/" + bootx64BinaryFedora, + isoGrubBinaryPath: isoBootloaderDirFedora + "/" + grubx64Binary, + ukiEfiStubBinary: ukiEfiStubx64Binary, + ukiEfiStubBinaryPath: ukiEfiStubDir + "/" + ukiEfiStubx64Binary, + ukiAddonStubBinary: ukiAddonStubx64Binary, + ukiAddonStubBinaryPath: ukiEfiStubDir + "/" + ukiAddonStubx64Binary, + }, + "arm64": { + bootBinary: bootAA64BinaryFedora, + grubBinary: grubAA64Binary, + grubNoPrefixBinary: grubAA64NoPrefixBinary, + espBootBinaryPath: espBootloaderDir + "/" + bootAA64BinaryFedora, + espGrubBinaryPath: espBootloaderDir + "/" + grubAA64Binary, + osEspBootBinaryPath: osEspBootloaderDir + "/" + bootAA64BinaryFedora, + osEspGrubBinaryPath: osEspBootloaderDir + "/" + grubAA64Binary, + osEspGrubNoPrefixBinaryPath: osEspBootloaderDir + "/" + grubAA64NoPrefixBinary, + isoBootBinaryPath: isoBootloaderDirFedora + "/" + bootAA64BinaryFedora, + isoGrubBinaryPath: isoBootloaderDirFedora + "/" + grubAA64Binary, + ukiEfiStubBinary: ukiEfiStubAA64Binary, + ukiEfiStubBinaryPath: ukiEfiStubDir + "/" + ukiEfiStubAA64Binary, + ukiAddonStubBinary: ukiAddonStubAA64Binary, + ukiAddonStubBinaryPath: ukiEfiStubDir + "/" + ukiAddonStubAA64Binary, + }, +} + func newFedoraDistroHandler(targetOs targetos.TargetOs) *fedoraDistroHandler { logger.Log.Debugf("Distro handler: Fedora (distro='%s', versionid='%s')", targetOs.Distro, targetOs.VersionId) @@ -236,3 +277,7 @@ func (d *fedoraDistroHandler) GrubEfiPackage() string { func (d *fedoraDistroHandler) RootMissingMountDirectories() bool { return false } + +func (d *fedoraDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfigFedora) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go index 70d72155d0..1754b8c28c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go @@ -242,3 +242,7 @@ func (d *ubuntuDistroHandler) GrubEfiPackage() string { func (d *ubuntuDistroHandler) RootMissingMountDirectories() bool { return false } + +func (d *ubuntuDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { + return bootArchConfigFromMap(bootloaderFilesConfig) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 6552ddf023..0ed83d233f 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -498,7 +498,7 @@ func customizeOSContents(ctx context.Context, rc *ResolvedConfig) (imageMetadata } if rc.Uki != nil { - err = createUki(ctx, rc) + err = createUki(ctx, rc, im.distroHandler) if err != nil { return im, fmt.Errorf("%w:\n%w", ErrCustomizeCreateUkis, err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go index 5137d2c41f..fc01701aae 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go @@ -543,7 +543,7 @@ func getInstalledTargetOsFromPartitionLayout(diskPartitions []diskutils.Partitio return targetos.TargetOs{}, err } - targetOs, err := targetos.GetInstalledTargetOsFromEnvFields(fields) + targetOs, err := targetos.GetInstalledTargetOsFromEnvFields(fields, "os-release") if err != nil { return targetos.TargetOs{}, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go index abe2fcc302..8c97ca7a6d 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go @@ -81,8 +81,8 @@ func (b *IsoArtifactsStore) cleanUp() error { return nil } -func containsGrubNoPrefix(filePaths []string) (bool, error) { - _, bootFilesConfig, err := getBootArchConfig() +func containsGrubNoPrefix(filePaths []string, distroHandler DistroHandler) (bool, error) { + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return false, err } @@ -218,7 +218,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, return nil, fmt.Errorf("failed to scan /boot folder:\n%w", err) } - usingGrubNoPrefix, err := containsGrubNoPrefix(bootFolderFilePaths) + usingGrubNoPrefix, err := containsGrubNoPrefix(bootFolderFilePaths, distroHandler) if err != nil { return nil, err } @@ -252,7 +252,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, // in them (i.e. user files that we do not manipulate - but copy as-is). scheduleAdditionalFile := true - _, bootFilesConfig, err := getBootArchConfig() + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return nil, err } @@ -367,7 +367,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, } } - _, bootFilesConfig, err := getBootArchConfig() + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return nil, err } @@ -448,7 +448,50 @@ func createIsoFilesStoreFromIsoImage(isoImageFile, storeDir string) (filesStore filesStore.kernelBootFiles = make(map[string]*KernelBootFiles) filesStore.kdumpBootFiles = make(map[string]*KdumpBootFiles) - _, bootFilesConfig, err := getBootArchConfig() + // Resolve the distro from the ISO's initrd. Which initrd to read depends on the ISO's initramfsType. + squashfsPath := filepath.Join(artifactsDir, liveOSImagePath) + squashfsExists, err := file.PathExists(squashfsPath) + if err != nil { + return nil, fmt.Errorf("failed to check for squashfs at (%s):\n%w", squashfsPath, err) + } + if squashfsExists { + filesStore.squashfsImagePath = squashfsPath + } + + var initrdPath string + switch initramfsTypeFromFilesStore(filesStore) { + case imagecustomizerapi.InitramfsImageTypeBootstrap: + // Any of the per-kernel dracut initrds is fine for distro detection since they're all from the same OS. + for _, f := range isoFiles { + base := filepath.Base(f) + isInitrd := strings.HasPrefix(base, initramfsPrefix) || strings.HasPrefix(base, initrdPrefix) + if isInitrd && strings.HasSuffix(base, ".img") { + initrdPath = f + break + } + } + if initrdPath == "" { + return nil, fmt.Errorf("bootstrap ISO has no per-kernel initrd") + } + case imagecustomizerapi.InitramfsImageTypeFullOS: + initrdPath = filepath.Join(artifactsDir, isoInitrdPath) + initrdExists, err := file.PathExists(initrdPath) + if err != nil { + return nil, fmt.Errorf("failed to check for initrd at (%s):\n%w", initrdPath, err) + } + if !initrdExists { + return nil, fmt.Errorf("full OS ISO has no initrd at expected path (%s)", initrdPath) + } + default: + return nil, fmt.Errorf("unrecognized initramfs image type for ISO image") + } + + distroHandler, err := NewDistroHandlerFromInitrd(initrdPath) + if err != nil { + return nil, fmt.Errorf("failed to detect distro from initrd (%s):\n%w", initrdPath, err) + } + + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return nil, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go index beef461cc1..2dfaf11fcc 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go @@ -28,6 +28,14 @@ type LiveOSConfig struct { bootstrapFileUrl string } +// initramfsTypeFromFilesStore detects the initramfs type based on the files in the provided file store. +func initramfsTypeFromFilesStore(filesStore *IsoFilesStore) imagecustomizerapi.InitramfsImageType { + if filesStore == nil || filesStore.squashfsImagePath == "" { + return imagecustomizerapi.InitramfsImageTypeFullOS + } + return imagecustomizerapi.InitramfsImageTypeBootstrap +} + func resolveInitramfsType(inputArtifactsStore *IsoArtifactsStore, outputInitramfsType imagecustomizerapi.InitramfsImageType, defaultInitramfsType imagecustomizerapi.InitramfsImageType) ( resolvedInitramfsType imagecustomizerapi.InitramfsImageType, convertingInitramfsType bool, @@ -36,11 +44,7 @@ func resolveInitramfsType(inputArtifactsStore *IsoArtifactsStore, outputInitramf // , then we should follow the input image. var inputInitramfsType imagecustomizerapi.InitramfsImageType if inputArtifactsStore != nil { - if inputArtifactsStore.files.squashfsImagePath != "" { - inputInitramfsType = imagecustomizerapi.InitramfsImageTypeBootstrap - } else { - inputInitramfsType = imagecustomizerapi.InitramfsImageTypeFullOS - } + inputInitramfsType = initramfsTypeFromFilesStore(inputArtifactsStore.files) } resolvedInitramfsType = outputInitramfsType diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go index fb44f31b82..d07c4b9ce1 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go @@ -44,13 +44,12 @@ const ( initrdImage = "initrd.img" // In vhd(x)/qcow/iso images, the kernel is named 'vmlinuz-'. - vmLinuzPrefix = "vmlinuz-" - initramfsPrefix = "initramfs-" // AZL3, Fedora, etc. - initrdPrefix = "initrd.img-" // AZL2, Ubuntu, etc. - isoKernelDir = "/boot" - isoInitrdPath = "/boot/" + initrdImage - isoBootloadersDir = "/efi/boot" - isoBootImagePath = "/boot/grub2/efiboot.img" + vmLinuzPrefix = "vmlinuz-" + initramfsPrefix = "initramfs-" // AZL3, Fedora, etc. + initrdPrefix = "initrd.img-" // AZL2, Ubuntu, etc. + isoKernelDir = "/boot" + isoInitrdPath = "/boot/" + initrdImage + isoBootImagePath = "/boot/grub2/efiboot.img" // Minimum dracut version required to enable PXE booting. LiveOsPxeDracutMinVersion = 102 @@ -124,12 +123,15 @@ var bootloaderFilesConfig = map[string]BootFilesArchConfig{ }, } -func getBootArchConfig() (string, BootFilesArchConfig, error) { +// bootArchConfigFromMap looks up the current runtime architecture in the provided per-arch boot files config map. +func bootArchConfigFromMap(configByArch map[string]BootFilesArchConfig) (BootFilesArchConfig, error) { arch := runtime.GOARCH - if arch != "amd64" && arch != "arm64" { - return "", BootFilesArchConfig{}, fmt.Errorf("unsupported architecture: %s", arch) + switch arch { + case "amd64", "arm64": + return configByArch[arch], nil + default: + return BootFilesArchConfig{}, fmt.Errorf("unsupported architecture: %s", arch) } - return arch, bootloaderFilesConfig[arch], nil } // verifies that the dracut package supports PXE booting for LiveOS images. diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go b/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go index de3fb0ab4c..c7d38d9069 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go @@ -116,12 +116,13 @@ func createPXEArtifacts(buildDir string, outputFormat imagecustomizerapi.ImageFo // created because some of these files are needed by the ISO so it is // bootable. - // Move bootloader files from under '/efi/boot' to '/' - _, bootFilesConfig, err := getBootArchConfig() + // Move bootloader files from under '/' to '/'. + bootFilesConfig, err := distroHandler.GetBootArchConfig() if err != nil { return err } - bootloaderSrcDir := filepath.Join(outputPXEArtifactsDir, isoBootloadersDir) + isoBootloaderDir := filepath.Dir(bootFilesConfig.isoBootBinaryPath) + bootloaderSrcDir := filepath.Join(outputPXEArtifactsDir, isoBootloaderDir) bootloaderFiles := []string{bootFilesConfig.bootBinary, bootFilesConfig.grubBinary} for _, bootloaderFile := range bootloaderFiles { @@ -133,8 +134,8 @@ func createPXEArtifacts(buildDir string, outputFormat imagecustomizerapi.ImageFo } } - // Remove the empty 'pxe-folder>/efi' folder. - isoEFIDir := filepath.Join(outputPXEArtifactsDir, "efi") + // Remove the now-empty parent of '/'. + isoEFIDir := filepath.Join(outputPXEArtifactsDir, filepath.Dir(isoBootloaderDir)) err = os.RemoveAll(isoEFIDir) if err != nil { return fmt.Errorf("failed to remove folder (%s):\n%w", isoEFIDir, err) From 687b630082ed6dd6254c6426e695cfb23d60a9f9 Mon Sep 17 00:00:00 2001 From: Vince Perri <5596945+vinceaperri@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:13:57 +0000 Subject: [PATCH 2/7] Pass distroHandler to createPXEArtifacts createPXEArtifacts calls distroHandler.GetBootArchConfig() but never received a distroHandler parameter, so the package failed to build with 'undefined: distroHandler'. Add distroHandler as the final parameter and pass it from both call sites in liveosisobuilder.go, which already have a distroHandler in scope. --- toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go | 4 ++-- toolkit/tools/pkg/imagecustomizerlib/liveospxe.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go index 2dfaf11fcc..019074d87e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go @@ -345,7 +345,7 @@ func createLiveOSFromRawHelper(ctx context.Context, buildDir string, inputArtifa case imagecustomizerapi.ImageFormatTypePxeDir, imagecustomizerapi.ImageFormatTypePxeTar: err = createPXEArtifacts(isoBuildDir, outputFormat, liveosConfig.initramfsType, artifactsStore, liveosConfig.kdumpBootFiles, liveosConfig.additionalFiles, - liveosConfig.bootstrapBaseUrl, liveosConfig.bootstrapFileUrl, outputPath) + liveosConfig.bootstrapBaseUrl, liveosConfig.bootstrapFileUrl, outputPath, distroHandler) if err != nil { return fmt.Errorf("failed to generate PXE artifacts\n%w", err) } @@ -396,7 +396,7 @@ func repackageLiveOSHelper(isoBuildDir string, liveosConfig LiveOSConfig, inputA case imagecustomizerapi.ImageFormatTypePxeDir, imagecustomizerapi.ImageFormatTypePxeTar: err = createPXEArtifacts(isoBuildDir, outputFormat, liveosConfig.initramfsType, inputArtifactsStore, liveosConfig.kdumpBootFiles, liveosConfig.additionalFiles, - liveosConfig.bootstrapBaseUrl, liveosConfig.bootstrapFileUrl, outputPath) + liveosConfig.bootstrapBaseUrl, liveosConfig.bootstrapFileUrl, outputPath, distroHandler) if err != nil { return fmt.Errorf("failed to generate PXE artifacts folder\n%w", err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go b/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go index c7d38d9069..11e73bddf6 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveospxe.go @@ -45,7 +45,7 @@ func getPxeBootstrapFileName(bootstrapBaseUrl, bootstrapFileUrl string) (string, func createPXEArtifacts(buildDir string, outputFormat imagecustomizerapi.ImageFormatType, initramfsType imagecustomizerapi.InitramfsImageType, artifactsStore *IsoArtifactsStore, kdumpBootFiles *imagecustomizerapi.KdumpBootFilesType, additionalIsoFiles imagecustomizerapi.AdditionalFileList, - bootstrapBaseUrl, bootstrapFileUrl, outputPath string) (err error) { + bootstrapBaseUrl, bootstrapFileUrl, outputPath string, distroHandler DistroHandler) (err error) { logger.Log.Infof("Creating PXE output at (%s)", outputPath) outputPXEArtifactsDir := "" From 3ba5cd3cebb051f03d920a437ff4d25c99ffe36f Mon Sep 17 00:00:00 2001 From: Vince Perri <5596945+vinceaperri@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:20:28 +0000 Subject: [PATCH 3/7] Match initrd os-release candidates as relative cpio paths --- toolkit/tools/internal/targetos/targetos.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/toolkit/tools/internal/targetos/targetos.go b/toolkit/tools/internal/targetos/targetos.go index c3e994e4ce..9d6d7c2d21 100644 --- a/toolkit/tools/internal/targetos/targetos.go +++ b/toolkit/tools/internal/targetos/targetos.go @@ -81,6 +81,8 @@ var ( } // initrdReleaseCandidates lists the in-initrd paths to probe for an os-release(5)-type file, in preference order. + // These are matched against cpio member names, which are stored relative to the archive root, so they must not have + // a leading slash. // // os-release is the canonical filename and is the only candidate present in full-OS initrds built by Image // Customizer (which pack the source rootfs directly, in which initrd-release does not exist at all). @@ -88,10 +90,10 @@ var ( // initrd-release is the dracut-runtime variant emitted by dracut's 99base module. os-release symlinks to this file // in such cases, so it is a necessary fallback. initrdReleaseCandidates = []string{ - "/etc/os-release", - "/usr/lib/os-release", - "/etc/initrd-release", - "/usr/lib/initrd-release", + "etc/os-release", + "usr/lib/os-release", + "etc/initrd-release", + "usr/lib/initrd-release", } ) From 0652d3091d04ce95e54966354f10067ac9c5ec28 Mon Sep 17 00:00:00 2001 From: Vince Perri <5596945+vinceaperri@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:20:43 +0000 Subject: [PATCH 4/7] Assert output artifact shim and bootloader ESP paths per distro --- .../artifactsinputoutput_test.go | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput_test.go b/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput_test.go index 1c677d7f44..2dd4eae139 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/artifactsinputoutput_test.go @@ -55,7 +55,7 @@ func TestOutputAndInjectArtifacts(t *testing.T) { return } - espFiles := verifyAndSignOutputtedArtifacts(t, outputArtifactsDir, false) + espFiles := verifyAndSignOutputtedArtifacts(t, baseImageInfo, outputArtifactsDir, false) // Use new buildDir to ensure the buildDir is created if it doesn't exist. buildDirInject := filepath.Join(buildDir, "inject") @@ -163,7 +163,7 @@ func TestOutputAndInjectArtifactsCosi(t *testing.T) { return } - espFiles := verifyAndSignOutputtedArtifacts(t, outputArtifactsDir, true) + espFiles := verifyAndSignOutputtedArtifacts(t, baseImageInfo, outputArtifactsDir, true) // Inject artifacts into image. options := InjectFilesOptions{ @@ -305,7 +305,19 @@ func TestOutputAndInjectArtifactsCosi(t *testing.T) { "root", buildDir, "", "restart-on-corruption", false /*inlineVerity*/) } -func verifyAndSignOutputtedArtifacts(t *testing.T, outputArtifactsDir string, expectVerityHash bool) []string { +func verifyAndSignOutputtedArtifacts(t *testing.T, baseImageInfo testBaseImageInfo, outputArtifactsDir string, expectVerityHash bool) []string { + // Resolve the distro-version's boot file layout so artifact destinations can be checked against the distro's + // actual ESP paths. The boot file names differ per distro-version (for example, Azure Linux 4 uses the Fedora-style + // uppercase BOOTX64.EFI shim, while Azure Linux 3 uses lowercase bootx64.efi). + distroHandler, err := NewDistroHandler(baseImageInfo.TargetOs()) + if !assert.NoError(t, err) { + return nil + } + bootConfig, err := distroHandler.GetBootArchConfig() + if !assert.NoError(t, err) { + return nil + } + // Confirm inject-files.yaml was generated injectConfigPath := filepath.Join(outputArtifactsDir, "inject-files.yaml") exists, err := file.PathExists(injectConfigPath) @@ -333,15 +345,15 @@ func verifyAndSignOutputtedArtifacts(t *testing.T, outputArtifactsDir string, ex switch entry.Type { case imagecustomizerapi.OutputArtifactsItemShim: - assert.True(t, strings.HasPrefix(entry.Destination, "/EFI/BOOT/boot"), "Expected shim destination to start with /EFI/BOOT/boot") - assert.True(t, strings.HasSuffix(entry.Destination, ".efi"), "Expected shim destination to end with .efi") + expectedShimDestination := filepath.Join("/", bootConfig.espBootBinaryPath) + assert.Equal(t, expectedShimDestination, entry.Destination, "Expected shim destination to match the distro ESP boot binary path") assert.True(t, strings.HasPrefix(entry.Source, "./shim/"), "Expected shim source to be in shim/ subdirectory") hasShim = true espFiles = append(espFiles, entry.Destination) case imagecustomizerapi.OutputArtifactsItemBootloader: - assert.True(t, strings.HasPrefix(entry.Destination, "/EFI/BOOT/grub"), "Expected bootloader destination to start with /EFI/BOOT/grub") - assert.True(t, strings.HasSuffix(entry.Destination, ".efi"), "Expected bootloader destination to end with .efi") + expectedBootloaderDestination := filepath.Join("/", bootConfig.espGrubBinaryPath) + assert.Equal(t, expectedBootloaderDestination, entry.Destination, "Expected bootloader destination to match the distro ESP grub binary path") assert.True(t, strings.HasPrefix(entry.Source, "./bootloader/"), "Expected bootloader source to be in bootloader/ subdirectory") hasBootloader = true espFiles = append(espFiles, entry.Destination) From 095970f3a8b201e1dd828785ff2ed9763a6342f0 Mon Sep 17 00:00:00 2001 From: Vince Perri <5596945+vinceaperri@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:44:11 +0000 Subject: [PATCH 5/7] address review comments Move the initrd reading helpers into the initrdutils package. Drop the sourceLabel parameter from GetInstalledTargetOsFromEnvFields and let callers add context. Restore the found boolean in GetInstalledTargetOs. Rename bootloaderFilesConfig to bootloaderFilesConfigAzureLinux. Resolve the distro from squashfs presence directly instead of via initramfsTypeFromFilesStore. Add an explicit error check in NewDistroHandlerFromInitrd. --- .../initrd.go => initrdutils/initrdread.go} | 6 +++--- .../zstdreadcloser.go} | 2 +- toolkit/tools/internal/targetos/targetos.go | 15 +++++++++------ .../tools/pkg/imagecustomizerlib/distrohandler.go | 8 +++++++- .../pkg/imagecustomizerlib/distrohandler_acl.go | 2 +- .../distrohandler_azurelinux.go | 2 +- .../imagecustomizerlib/distrohandler_ubuntu.go | 2 +- .../tools/pkg/imagecustomizerlib/imageutils.go | 4 ++-- .../imagecustomizerlib/liveosisoartifactstore.go | 13 ++++++------- .../pkg/imagecustomizerlib/liveosisoutils.go | 2 +- 10 files changed, 32 insertions(+), 24 deletions(-) rename toolkit/tools/internal/{targetos/initrd.go => initrdutils/initrdread.go} (95%) rename toolkit/tools/internal/{targetos/zstd_readcloser.go => initrdutils/zstdreadcloser.go} (94%) diff --git a/toolkit/tools/internal/targetos/initrd.go b/toolkit/tools/internal/initrdutils/initrdread.go similarity index 95% rename from toolkit/tools/internal/targetos/initrd.go rename to toolkit/tools/internal/initrdutils/initrdread.go index f651e06430..a1a56e1458 100644 --- a/toolkit/tools/internal/targetos/initrd.go +++ b/toolkit/tools/internal/initrdutils/initrdread.go @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -package targetos +package initrdutils import ( "bufio" @@ -29,10 +29,10 @@ var ( longestMagicSize = len(magicZstd) ) -// readFirstFileFromInitrd scans an initramfs cpio archive once and returns the content of the first candidate path in +// ReadFirstFileFromInitrd scans an initramfs cpio archive once and returns the content of the first candidate path in // the provided list that exists as a regular file. Returns an error wrapping fs.ErrNotExist if none of the candidates // is present. -func readFirstFileFromInitrd(initrdPath string, candidates []string) (content []byte, foundPath string, err error) { +func ReadFirstFileFromInitrd(initrdPath string, candidates []string) (content []byte, foundPath string, err error) { f, err := os.Open(initrdPath) if err != nil { return nil, "", fmt.Errorf("failed to open initrd (%s):\n%w", initrdPath, err) diff --git a/toolkit/tools/internal/targetos/zstd_readcloser.go b/toolkit/tools/internal/initrdutils/zstdreadcloser.go similarity index 94% rename from toolkit/tools/internal/targetos/zstd_readcloser.go rename to toolkit/tools/internal/initrdutils/zstdreadcloser.go index 02ee783110..164639a094 100644 --- a/toolkit/tools/internal/targetos/zstd_readcloser.go +++ b/toolkit/tools/internal/initrdutils/zstdreadcloser.go @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -package targetos +package initrdutils import ( "github.com/klauspost/compress/zstd" diff --git a/toolkit/tools/internal/targetos/targetos.go b/toolkit/tools/internal/targetos/targetos.go index 9d6d7c2d21..617483bf0d 100644 --- a/toolkit/tools/internal/targetos/targetos.go +++ b/toolkit/tools/internal/targetos/targetos.go @@ -10,6 +10,7 @@ import ( "path/filepath" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/envfile" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/initrdutils" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/version" ) @@ -114,6 +115,7 @@ func GetInstalledTargetOs(rootfs string) (TargetOs, error) { var err error var fields map[string]string + found := false for _, candidate := range OsReleaseFileCandidates { fields, err = envfile.ParseEnvFile(filepath.Join(rootfs, candidate)) if errors.Is(err, fs.ErrNotExist) { @@ -123,14 +125,15 @@ func GetInstalledTargetOs(rootfs string) (TargetOs, error) { return TargetOs{}, fmt.Errorf("failed to read os-release file (%s):\n%w", candidate, err) } + found = true break } - if fields == nil { + if !found { return TargetOs{}, fmt.Errorf("no os-release file found (candidates=%s):\n%w", OsReleaseFileCandidates, err) } - targetOs, err := GetInstalledTargetOsFromEnvFields(fields, "os-release") + targetOs, err := GetInstalledTargetOsFromEnvFields(fields) if err != nil { return TargetOs{}, fmt.Errorf("failed to determine target OS from os-release file:\n%w", err) } @@ -143,7 +146,7 @@ func GetInstalledTargetOs(rootfs string) (TargetOs, error) { // // Returns an error wrapping fs.ErrNotExist when none of the candidates exist in the initrd. func GetInitrdTargetOs(initrdPath string) (TargetOs, error) { - content, foundPath, err := readFirstFileFromInitrd(initrdPath, initrdReleaseCandidates) + content, foundPath, err := initrdutils.ReadFirstFileFromInitrd(initrdPath, initrdReleaseCandidates) if err != nil { return TargetOs{}, err } @@ -153,7 +156,7 @@ func GetInitrdTargetOs(initrdPath string) (TargetOs, error) { return TargetOs{}, fmt.Errorf("failed to read (%s) file from initrd (%s):\n%w", foundPath, initrdPath, err) } - targetOs, err := GetInstalledTargetOsFromEnvFields(fields, "initrd-release") + targetOs, err := GetInstalledTargetOsFromEnvFields(fields) if err != nil { return TargetOs{}, fmt.Errorf("failed to determine target OS from initrd-release file:\n%w", err) } @@ -161,7 +164,7 @@ func GetInitrdTargetOs(initrdPath string) (TargetOs, error) { return targetOs, nil } -func GetInstalledTargetOsFromEnvFields(fields map[string]string, sourceLabel string) (TargetOs, error) { +func GetInstalledTargetOsFromEnvFields(fields map[string]string) (TargetOs, error) { distroId := fields["ID"] versionId := fields["VERSION_ID"] @@ -212,7 +215,7 @@ func GetInstalledTargetOsFromEnvFields(fields map[string]string, sourceLabel str }, nil default: - return TargetOs{}, fmt.Errorf("unknown ID (%s) in %s", distroId, sourceLabel) + return TargetOs{}, fmt.Errorf("unknown ID (%s)", distroId) } } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go index 3db9264855..41ae313757 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go @@ -198,5 +198,11 @@ func NewDistroHandlerFromInitrd(initrdPath string) (DistroHandler, error) { if err != nil { return nil, fmt.Errorf("failed to determine the target OS from initrd (%s):\n%w", initrdPath, err) } - return NewDistroHandler(targetOs) + + distroHandler, err := NewDistroHandler(targetOs) + if err != nil { + return nil, err + } + + return distroHandler, nil } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go index ca1fb1fcf0..6c055f9128 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go @@ -289,5 +289,5 @@ func (d *aclDistroHandler) RootMissingMountDirectories() bool { } func (d *aclDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { - return bootArchConfigFromMap(bootloaderFilesConfig) + return bootArchConfigFromMap(bootloaderFilesConfigAzureLinux) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go index ae48be5f10..4125c7f0bf 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go @@ -226,5 +226,5 @@ func (d *azureLinuxDistroHandler) RootMissingMountDirectories() bool { } func (d *azureLinuxDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { - return bootArchConfigFromMap(bootloaderFilesConfig) + return bootArchConfigFromMap(bootloaderFilesConfigAzureLinux) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go index 1754b8c28c..bf271b3b3b 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go @@ -244,5 +244,5 @@ func (d *ubuntuDistroHandler) RootMissingMountDirectories() bool { } func (d *ubuntuDistroHandler) GetBootArchConfig() (BootFilesArchConfig, error) { - return bootArchConfigFromMap(bootloaderFilesConfig) + return bootArchConfigFromMap(bootloaderFilesConfigAzureLinux) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go index fc01701aae..a6709997cb 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go @@ -543,9 +543,9 @@ func getInstalledTargetOsFromPartitionLayout(diskPartitions []diskutils.Partitio return targetos.TargetOs{}, err } - targetOs, err := targetos.GetInstalledTargetOsFromEnvFields(fields, "os-release") + targetOs, err := targetos.GetInstalledTargetOsFromEnvFields(fields) if err != nil { - return targetos.TargetOs{}, err + return targetos.TargetOs{}, fmt.Errorf("failed to determine target OS from os-release file:\n%w", err) } return targetOs, nil diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go index 8c97ca7a6d..2fbf7e7558 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go @@ -454,13 +454,14 @@ func createIsoFilesStoreFromIsoImage(isoImageFile, storeDir string) (filesStore if err != nil { return nil, fmt.Errorf("failed to check for squashfs at (%s):\n%w", squashfsPath, err) } + + // A bootstrap ISO carries a squashfs root plus per-kernel dracut initrds, while a full-OS ISO has no squashfs and + // instead packs the rootfs into a single initrd. Use the squashfs presence to pick which initrd to read for distro + // detection. + var initrdPath string if squashfsExists { filesStore.squashfsImagePath = squashfsPath - } - var initrdPath string - switch initramfsTypeFromFilesStore(filesStore) { - case imagecustomizerapi.InitramfsImageTypeBootstrap: // Any of the per-kernel dracut initrds is fine for distro detection since they're all from the same OS. for _, f := range isoFiles { base := filepath.Base(f) @@ -473,7 +474,7 @@ func createIsoFilesStoreFromIsoImage(isoImageFile, storeDir string) (filesStore if initrdPath == "" { return nil, fmt.Errorf("bootstrap ISO has no per-kernel initrd") } - case imagecustomizerapi.InitramfsImageTypeFullOS: + } else { initrdPath = filepath.Join(artifactsDir, isoInitrdPath) initrdExists, err := file.PathExists(initrdPath) if err != nil { @@ -482,8 +483,6 @@ func createIsoFilesStoreFromIsoImage(isoImageFile, storeDir string) (filesStore if !initrdExists { return nil, fmt.Errorf("full OS ISO has no initrd at expected path (%s)", initrdPath) } - default: - return nil, fmt.Errorf("unrecognized initramfs image type for ISO image") } distroHandler, err := NewDistroHandlerFromInitrd(initrdPath) diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go index d07c4b9ce1..83c284b765 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoutils.go @@ -88,7 +88,7 @@ type BootFilesArchConfig struct { ukiAddonStubBinaryPath string } -var bootloaderFilesConfig = map[string]BootFilesArchConfig{ +var bootloaderFilesConfigAzureLinux = map[string]BootFilesArchConfig{ "amd64": { bootBinary: bootx64Binary, grubBinary: grubx64Binary, From b4401ccccba2e39615c2d4a44517ed002be9a881 Mon Sep 17 00:00:00 2001 From: Vince Perri <5596945+vinceaperri@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:36:52 +0000 Subject: [PATCH 6/7] inline --- .../pkg/imagecustomizerlib/liveosisobuilder.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go index 019074d87e..8afe64f30e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go @@ -28,14 +28,6 @@ type LiveOSConfig struct { bootstrapFileUrl string } -// initramfsTypeFromFilesStore detects the initramfs type based on the files in the provided file store. -func initramfsTypeFromFilesStore(filesStore *IsoFilesStore) imagecustomizerapi.InitramfsImageType { - if filesStore == nil || filesStore.squashfsImagePath == "" { - return imagecustomizerapi.InitramfsImageTypeFullOS - } - return imagecustomizerapi.InitramfsImageTypeBootstrap -} - func resolveInitramfsType(inputArtifactsStore *IsoArtifactsStore, outputInitramfsType imagecustomizerapi.InitramfsImageType, defaultInitramfsType imagecustomizerapi.InitramfsImageType) ( resolvedInitramfsType imagecustomizerapi.InitramfsImageType, convertingInitramfsType bool, @@ -44,7 +36,11 @@ func resolveInitramfsType(inputArtifactsStore *IsoArtifactsStore, outputInitramf // , then we should follow the input image. var inputInitramfsType imagecustomizerapi.InitramfsImageType if inputArtifactsStore != nil { - inputInitramfsType = initramfsTypeFromFilesStore(inputArtifactsStore.files) + if inputArtifactsStore.files.squashfsImagePath != "" { + inputInitramfsType = imagecustomizerapi.InitramfsImageTypeBootstrap + } else { + inputInitramfsType = imagecustomizerapi.InitramfsImageTypeFullOS + } } resolvedInitramfsType = outputInitramfsType From 803041326e20f59270c4a41844ff57ad8dbdc2d1 Mon Sep 17 00:00:00 2001 From: Vince Perri <5596945+vinceaperri@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:45:59 +0000 Subject: [PATCH 7/7] no noprefix --- .../distrohandler_fedora.go | 11 +++++--- .../liveosisoartifactstore.go | 27 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go index d58d31ab0c..5b168fff3e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go @@ -43,16 +43,19 @@ const ( ) // bootloaderFilesConfigFedora is the boot-files map for Fedora-style ESPs (Azure Linux 4 and Fedora). +// +// Fedora-style distros do not ship a grub-noprefix binary, so grubNoPrefixBinary and +// osEspGrubNoPrefixBinaryPath are left empty. var bootloaderFilesConfigFedora = map[string]BootFilesArchConfig{ "amd64": { bootBinary: bootx64BinaryFedora, grubBinary: grubx64Binary, - grubNoPrefixBinary: grubx64NoPrefixBinary, + grubNoPrefixBinary: "", espBootBinaryPath: espBootloaderDir + "/" + bootx64BinaryFedora, espGrubBinaryPath: espBootloaderDir + "/" + grubx64Binary, osEspBootBinaryPath: osEspBootloaderDir + "/" + bootx64BinaryFedora, osEspGrubBinaryPath: osEspBootloaderDir + "/" + grubx64Binary, - osEspGrubNoPrefixBinaryPath: osEspBootloaderDir + "/" + grubx64NoPrefixBinary, + osEspGrubNoPrefixBinaryPath: "", isoBootBinaryPath: isoBootloaderDirFedora + "/" + bootx64BinaryFedora, isoGrubBinaryPath: isoBootloaderDirFedora + "/" + grubx64Binary, ukiEfiStubBinary: ukiEfiStubx64Binary, @@ -63,12 +66,12 @@ var bootloaderFilesConfigFedora = map[string]BootFilesArchConfig{ "arm64": { bootBinary: bootAA64BinaryFedora, grubBinary: grubAA64Binary, - grubNoPrefixBinary: grubAA64NoPrefixBinary, + grubNoPrefixBinary: "", espBootBinaryPath: espBootloaderDir + "/" + bootAA64BinaryFedora, espGrubBinaryPath: espBootloaderDir + "/" + grubAA64Binary, osEspBootBinaryPath: osEspBootloaderDir + "/" + bootAA64BinaryFedora, osEspGrubBinaryPath: osEspBootloaderDir + "/" + grubAA64Binary, - osEspGrubNoPrefixBinaryPath: osEspBootloaderDir + "/" + grubAA64NoPrefixBinary, + osEspGrubNoPrefixBinaryPath: "", isoBootBinaryPath: isoBootloaderDirFedora + "/" + bootAA64BinaryFedora, isoGrubBinaryPath: isoBootloaderDirFedora + "/" + grubAA64Binary, ukiEfiStubBinary: ukiEfiStubAA64Binary, diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go index 2fbf7e7558..912cf3dbdb 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go @@ -86,6 +86,12 @@ func containsGrubNoPrefix(filePaths []string, distroHandler DistroHandler) (bool if err != nil { return false, err } + + // Distros without a grub-noprefix binary leave grubNoPrefixBinary empty. + if bootFilesConfig.grubNoPrefixBinary == "" { + return false, nil + } + for _, filePath := range filePaths { if filepath.Base(filePath) == bootFilesConfig.grubNoPrefixBinary { return true, nil @@ -260,16 +266,17 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, osEspGrubBinaryPath := bootFilesConfig.osEspGrubBinaryPath osEspGrubNoPrefixBinaryPath := bootFilesConfig.osEspGrubNoPrefixBinaryPath - switch relativeFilePath { - case osEspBootBinaryPath: + switch { + case relativeFilePath == osEspBootBinaryPath: filesStore.bootEfiPath = targetPath scheduleAdditionalFile = false // No additional file scheduling - case osEspGrubBinaryPath, osEspGrubNoPrefixBinaryPath: + case relativeFilePath == osEspGrubBinaryPath, + osEspGrubNoPrefixBinaryPath != "" && relativeFilePath == osEspGrubNoPrefixBinaryPath: filesStore.grubEfiPath = targetPath scheduleAdditionalFile = false // No additional file scheduling - case isoGrubCfgPath: + case relativeFilePath == isoGrubCfgPath: if usingGrubNoPrefix { // When using the grubx64-noprefix.efi, the 'prefix' grub // variable is set to an empty string. When 'prefix' is an @@ -277,7 +284,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, // media, the bootloader defaults to looking for grub.cfg at // /EFI/BOOT/grub.cfg. // So, below, we ensure that grub.cfg file will be placed where - // grubx64-nopreifx.efi will be looking for it. + // grubx64-noprefix.efi will be looking for it. // // Note that this grub.cfg is the only file that needs to be // copied to that EFI/BOOT location. The rest of the files (like @@ -292,7 +299,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, // We will place the pxe grub config next to the iso grub config. filesStore.pxeGrubCfgPath = filepath.Join(filepath.Dir(filesStore.isoGrubCfgPath), pxeGrubCfg) scheduleAdditionalFile = false - case isoInitrdPath: + case relativeFilePath == isoInitrdPath: filesStore.initrdImagePath = targetPath scheduleAdditionalFile = false default: @@ -378,9 +385,13 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, } if filesStore.grubEfiPath == "" { - return nil, fmt.Errorf("failed to find the grub efi file (%s or %s):\n"+ + grubBinaryNames := bootFilesConfig.grubBinary + if bootFilesConfig.grubNoPrefixBinary != "" { + grubBinaryNames = fmt.Sprintf("%s or %s", bootFilesConfig.grubBinary, bootFilesConfig.grubNoPrefixBinary) + } + return nil, fmt.Errorf("failed to find the grub efi file (%s):\n"+ "this file is provided by the %s package", - bootFilesConfig.grubBinary, bootFilesConfig.grubNoPrefixBinary, + grubBinaryNames, distroHandler.GrubEfiPackage()) }