Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ sudo apt install systemd-ukify mmdebstrap

### 3. Compose an Image

> **ISO images require the `live-installer` binary.** Build it before starting
> an ISO build:
>
> ```bash
> go build -buildmode=pie -o ./build/live-installer ./cmd/live-installer
> ```
>
> If you use `earthly +build`, both binaries are built automatically.

```bash
# If built with go build:
sudo -E ./os-image-composer build image-templates/azl3-x86_64-edge-raw.yml
Expand Down
9 changes: 9 additions & 0 deletions cmd/os-image-composer/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/open-edge-platform/os-image-composer/internal/config"
"github.com/open-edge-platform/os-image-composer/internal/image/isomaker"
"github.com/open-edge-platform/os-image-composer/internal/provider"
"github.com/open-edge-platform/os-image-composer/internal/provider/azl"
"github.com/open-edge-platform/os-image-composer/internal/provider/elxr"
Expand Down Expand Up @@ -108,6 +109,14 @@ func executeBuild(cmd *cobra.Command, args []string) error {
log.Infof("Dependency graph will be written to %s", dotFilePath)
}

// For ISO builds, validate prerequisites (e.g., live-installer binary)
// before starting expensive provider init and package downloads
if template.Target.ImageType == "iso" {
if err := isomaker.ValidateISOPrerequisites(template); err != nil {
return fmt.Errorf("ISO prerequisites check failed: %w", err)
}
}

p, err := InitProvider(template.Target.OS, template.Target.Dist, template.Target.Arch)
if err != nil {
buildErr = fmt.Errorf("initializing provider failed: %v", err)
Expand Down
10 changes: 10 additions & 0 deletions docs/tutorial/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ For the full details on every command — including `inspect`, `compare`, and

## Building an Image

> **ISO images require the `live-installer` binary.** Build it before starting
> an ISO build:
>
> ```bash
> go build -buildmode=pie -o ./build/live-installer ./cmd/live-installer
> ```
>
> If you use `earthly +build`, both binaries are built automatically. See the
> [Installation Guide](./installation.md) for details.

```bash
# go build — binary is in the repo root
sudo -E ./os-image-composer build image-templates/azl3-x86_64-edge-raw.yml
Expand Down
57 changes: 57 additions & 0 deletions internal/image/isomaker/isomaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ func (isoMaker *IsoMaker) buildInitrd(template *config.ImageTemplate) error {
return fmt.Errorf("failed to get initrd template: %w", err)
}

if err := ValidateAdditionalFiles(initrdTemplate); err != nil {
return fmt.Errorf("ISO build prerequisites not met: %w", err)
}

isoMaker.InitrdMaker, err = initrdmaker.NewInitrdMaker(isoMaker.ChrootEnv, initrdTemplate)
if err != nil {
return fmt.Errorf("failed to create initrd maker: %w", err)
Expand Down Expand Up @@ -163,6 +167,59 @@ func (isoMaker *IsoMaker) getInitrdTemplate(template *config.ImageTemplate) (*co
return initrdTemplate, nil
}

// ValidateISOPrerequisites checks that all prerequisites for an ISO build are
// met before starting expensive operations. Call this early (before provider init
// or package download) to fail fast on missing files like live-installer.
func ValidateISOPrerequisites(template *config.ImageTemplate) error {
initrdTemplateFilePath, err := template.GetInitramfsTemplate()
if err != nil {
return fmt.Errorf("failed to resolve initramfs template: %w", err)
}

initrdTemplate, err := config.LoadAndMergeTemplate(initrdTemplateFilePath)
if err != nil {
return fmt.Errorf("failed to load initrd template for validation: %w", err)
}

return ValidateAdditionalFiles(initrdTemplate)
}

// ValidateAdditionalFiles checks that all additional files referenced in the
// template exist before starting expensive build operations. This catches
// missing dependencies like the live-installer binary early.
func ValidateAdditionalFiles(template *config.ImageTemplate) error {
for _, fileInfo := range template.SystemConfig.AdditionalFiles {
if fileInfo.Local == "" || fileInfo.Final == "" {
Comment thread
arodage marked this conversation as resolved.
continue
}

if filepath.IsAbs(fileInfo.Local) {
if _, err := os.Stat(fileInfo.Local); err != nil {
return fmt.Errorf("required file not found: %s (target: %s)", fileInfo.Local, fileInfo.Final)
}
continue
Comment thread
arodage marked this conversation as resolved.
}

found := false
for _, tmplPath := range template.PathList {
candidatePath := filepath.Join(filepath.Dir(tmplPath), fileInfo.Local)
if _, err := os.Stat(candidatePath); err == nil {
found = true
break
}
Comment thread
arodage marked this conversation as resolved.
}
if !found {
if strings.Contains(fileInfo.Local, "live-installer") {
return fmt.Errorf("live-installer binary not found (referenced as %s). "+
"Build it first: go build -buildmode=pie -o ./build/live-installer ./cmd/live-installer",
fileInfo.Local)
}
Comment thread
arodage marked this conversation as resolved.
Outdated
return fmt.Errorf("required additional file not found: %s (target: %s)", fileInfo.Local, fileInfo.Final)
}
}
return nil
}

func (isoMaker *IsoMaker) copyConfigFilesToIso(template *config.ImageTemplate, installRoot string) error {
// Copy general config files to ISO
generalConfigSrcDir, err := config.GetGeneralConfigDir()
Expand Down
192 changes: 192 additions & 0 deletions internal/image/isomaker/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package isomaker

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/open-edge-platform/os-image-composer/internal/config"
)

func TestValidateAdditionalFiles(t *testing.T) {
// Create a temp directory with a fake binary for the "exists" test cases
tempDir, err := os.MkdirTemp("", "validate-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)

existingFile := filepath.Join(tempDir, "live-installer")
if err := os.WriteFile(existingFile, []byte("binary"), 0755); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}

otherFile := filepath.Join(tempDir, "attendedinstaller")
if err := os.WriteFile(otherFile, []byte("script"), 0755); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}

// Template file path used to resolve relative paths
templatePath := filepath.Join(tempDir, "defaultconfigs", "default-initrd.yml")
if err := os.MkdirAll(filepath.Dir(templatePath), 0755); err != nil {
t.Fatalf("Failed to create template dir: %v", err)
}

tests := []struct {
name string
template *config.ImageTemplate
expectError bool
errorMsg string
Comment thread
arodage marked this conversation as resolved.
}{
{
name: "no_additional_files",
template: &config.ImageTemplate{
SystemConfig: config.SystemConfig{
AdditionalFiles: nil,
},
},
expectError: false,
},
{
name: "absolute_path_exists",
template: &config.ImageTemplate{
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: existingFile, Final: "/usr/bin/live-installer"},
},
},
},
expectError: false,
},
{
name: "absolute_path_missing",
template: &config.ImageTemplate{
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: "/nonexistent/path/live-installer", Final: "/usr/bin/live-installer"},
},
},
},
expectError: true,
errorMsg: "required file not found",
},
{
name: "relative_path_exists",
template: &config.ImageTemplate{
PathList: []string{templatePath},
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: "../attendedinstaller", Final: "/root/attendedinstaller"},
},
},
},
expectError: false,
},
{
name: "relative_path_missing_live_installer",
template: &config.ImageTemplate{
PathList: []string{templatePath},
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: "../../../../../../build/live-installer", Final: "/usr/bin/live-installer"},
},
},
},
expectError: true,
errorMsg: "live-installer binary not found",
},
{
name: "relative_path_missing_generic_file",
template: &config.ImageTemplate{
PathList: []string{templatePath},
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: "../nonexistent-file", Final: "/etc/some-config"},
},
},
},
expectError: true,
errorMsg: "required additional file not found",
},
{
name: "empty_local_path_skipped",
template: &config.ImageTemplate{
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: "", Final: "/usr/bin/something"},
},
},
},
expectError: false,
},
{
name: "empty_final_path_skipped",
template: &config.ImageTemplate{
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: "/some/path", Final: ""},
},
},
},
expectError: false,
},
{
name: "multiple_files_all_exist",
template: &config.ImageTemplate{
PathList: []string{templatePath},
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: existingFile, Final: "/usr/bin/live-installer"},
{Local: "../attendedinstaller", Final: "/root/attendedinstaller"},
},
},
},
expectError: false,
},
{
name: "multiple_files_one_missing",
template: &config.ImageTemplate{
PathList: []string{templatePath},
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: "../attendedinstaller", Final: "/root/attendedinstaller"},
{Local: "../../../../../../build/live-installer", Final: "/usr/bin/live-installer"},
},
},
},
expectError: true,
errorMsg: "live-installer binary not found",
},
{
name: "live_installer_error_includes_build_hint",
template: &config.ImageTemplate{
PathList: []string{templatePath},
SystemConfig: config.SystemConfig{
AdditionalFiles: []config.AdditionalFileInfo{
{Local: "../../../../../../build/live-installer", Final: "/usr/bin/live-installer"},
},
},
},
expectError: true,
errorMsg: "go build -buildmode=pie",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateAdditionalFiles(tt.template)
if tt.expectError {
if err == nil {
t.Error("Expected error, but got none")
} else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
t.Errorf("Expected error containing %q, but got: %v", tt.errorMsg, err)
}
} else {
if err != nil {
t.Errorf("Expected no error, but got: %v", err)
}
}
})
}
}
Loading