deb-bootc-compose/internal/treefile/treefile.go
robojerk cca68c90f6 Add comprehensive phase system, types, and treefile support for deb-bootc-compose
- Add internal/phases/ with complete phase management system
- Add internal/types/ with core data structures
- Add internal/treefile/ for OSTree treefile generation
- Update examples with YAML configurations
- Update .gitignore to properly exclude test artifacts and build outputs
- Update dependencies and configuration files
2025-08-19 20:48:46 -07:00

258 lines
8.3 KiB
Go

package treefile
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// Treefile represents a Debian treefile manifest (equivalent to Pungi's compose definitions)
// Supports both legacy JSON format and apt-ostree v1 YAML format
type Treefile struct {
// Legacy JSON format fields
Name string `json:"name" yaml:"name"`
Version string `json:"version" yaml:"version"`
Description string `json:"description" yaml:"description"`
Release string `json:"release" yaml:"release"`
// Package configuration
Packages PackageSet `json:"packages" yaml:"packages"`
Exclude []string `json:"exclude" yaml:"exclude"`
// Repository configuration
Repositories []Repository `json:"repositories" yaml:"repositories"`
// Architecture and variant configuration
Architecture []string `json:"architecture" yaml:"architecture"`
Variants []Variant `json:"variants" yaml:"variants"`
// Build configuration
Build struct {
Type string `json:"type" yaml:"type"`
Environment map[string]string `json:"environment" yaml:"environment"`
Options map[string]interface{} `json:"options" yaml:"options"`
} `json:"build" yaml:"build"`
// OSTree configuration
OSTree struct {
Ref string `json:"ref" yaml:"ref"`
Parent string `json:"parent" yaml:"parent"`
Subject string `json:"subject" yaml:"subject"`
Body string `json:"body" yaml:"body"`
Timestamp string `json:"timestamp" yaml:"timestamp"`
} `json:"ostree" yaml:"ostree"`
// Output configuration
Output struct {
Formats []string `json:"formats" yaml:"formats"`
Container struct {
BaseImage string `json:"base_image" yaml:"base_image"`
Labels map[string]string `json:"labels" yaml:"labels"`
Entrypoint []string `json:"entrypoint" yaml:"entrypoint"`
CMD []string `json:"cmd" yaml:"cmd"`
} `json:"container" yaml:"container"`
DiskImage struct {
Size string `json:"size" yaml:"size"`
Formats []string `json:"formats" yaml:"formats"`
Bootloader string `json:"bootloader" yaml:"bootloader"`
Kernel string `json:"kernel" yaml:"kernel"`
Initramfs bool `json:"initramfs" yaml:"initramfs"`
} `json:"disk_image" yaml:"disk_image"`
} `json:"output" yaml:"output"`
// Metadata
Metadata map[string]interface{} `json:"metadata" yaml:"metadata"`
// apt-ostree v1 format fields
APIVersion string `json:"apiVersion" yaml:"apiVersion"`
Kind string `json:"kind" yaml:"kind"`
Spec struct {
Base struct {
Distribution string `json:"distribution" yaml:"distribution"`
Architecture string `json:"architecture" yaml:"architecture"`
Mirror string `json:"mirror" yaml:"mirror"`
} `json:"base" yaml:"base"`
Packages struct {
Include []string `json:"include" yaml:"include"`
Exclude []string `json:"exclude" yaml:"exclude"`
} `json:"packages" yaml:"packages"`
Customizations map[string]interface{} `json:"customizations" yaml:"customizations"`
OSTree struct {
Ref string `json:"ref" yaml:"ref"`
CommitMessage string `json:"commit_message" yaml:"commit_message"`
Metadata map[string]interface{} `json:"metadata" yaml:"metadata"`
} `json:"ostree" yaml:"ostree"`
Build map[string]interface{} `json:"build" yaml:"build"`
} `json:"spec" yaml:"spec"`
}
// PackageSet defines the packages to include in the image
type PackageSet struct {
Required []string `json:"required"` // Required packages
Optional []string `json:"optional"` // Optional packages
Recommended []string `json:"recommended"` // Recommended packages
Development []string `json:"development"` // Development packages
Kernel []string `json:"kernel"` // Kernel packages
Bootloader []string `json:"bootloader"` // Bootloader packages
}
// Repository represents a Debian package repository
type Repository struct {
Name string `json:"name"`
URL string `json:"url"`
Suite string `json:"suite"` // e.g., "bookworm"
Component string `json:"component"` // e.g., "main"
Arch string `json:"arch"` // e.g., "amd64"
Enabled bool `json:"enabled"`
}
// Variant represents a Debian variant
type Variant struct {
Name string `json:"name"`
Description string `json:"description"`
Architectures []string `json:"architectures"`
Packages PackageSet `json:"packages"`
Exclude []string `json:"exclude"`
Config map[string]interface{} `json:"config"`
}
// Load loads a treefile from a JSON or YAML file
func Load(filename string) (*Treefile, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read treefile: %w", err)
}
// Detect file format based on extension and content
ext := strings.ToLower(filepath.Ext(filename))
var treefile Treefile
if ext == ".yaml" || ext == ".yml" || strings.HasPrefix(string(data), "apiVersion:") {
// Parse as YAML (apt-ostree v1 format)
if err := yaml.Unmarshal(data, &treefile); err != nil {
return nil, fmt.Errorf("failed to parse treefile YAML: %w", err)
}
} else {
// Parse as JSON (legacy format)
if err := json.Unmarshal(data, &treefile); err != nil {
return nil, fmt.Errorf("failed to parse treefile JSON: %w", err)
}
}
// Validate the treefile
if err := validateTreefile(&treefile); err != nil {
return nil, fmt.Errorf("invalid treefile: %w", err)
}
return &treefile, nil
}
// Save saves a treefile to a JSON file
func (t *Treefile) Save(filename string) error {
data, err := json.MarshalIndent(t, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal treefile: %w", err)
}
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("failed to write treefile: %w", err)
}
return nil
}
// validateTreefile validates a treefile
func validateTreefile(t *Treefile) error {
// Check if this is apt-ostree v1 format
if t.APIVersion == "v1" && t.Kind == "Treefile" {
// Validate apt-ostree v1 format
if t.Spec.Base.Distribution == "" {
return fmt.Errorf("spec.base.distribution is required")
}
if t.Spec.Base.Architecture == "" {
return fmt.Errorf("spec.base.architecture is required")
}
if len(t.Spec.Packages.Include) == 0 {
return fmt.Errorf("at least one package must be included")
}
if t.Spec.OSTree.Ref == "" {
return fmt.Errorf("spec.ostree.ref is required")
}
return nil
}
// Validate legacy JSON format
if t.Name == "" {
return fmt.Errorf("name is required")
}
if t.Version == "" {
return fmt.Errorf("version is required")
}
if len(t.Variants) == 0 {
return fmt.Errorf("at least one variant must be specified")
}
return nil
}
// GetPackagesForVariant returns packages for a specific variant and architecture
func (t *Treefile) GetPackagesForVariant(variant string, arch string) []string {
// Check if this is apt-ostree v1 format
if t.APIVersion == "v1" && t.Kind == "Treefile" {
// For apt-ostree v1, return all included packages
return t.Spec.Packages.Include
}
// Legacy JSON format
for _, v := range t.Variants {
if v.Name == variant {
// Check if architecture is supported
for _, variantArch := range v.Architectures {
if variantArch == arch {
var packages []string
packages = append(packages, v.Packages.Required...)
packages = append(packages, v.Packages.Optional...)
packages = append(packages, v.Packages.Recommended...)
packages = append(packages, v.Packages.Development...)
packages = append(packages, v.Packages.Kernel...)
packages = append(packages, v.Packages.Bootloader...)
return packages
}
}
}
}
return []string{}
}
// GetExcludedPackagesForVariant returns excluded packages for a specific variant and architecture
func (t *Treefile) GetExcludedPackagesForVariant(variantName, arch string) []string {
var result []string
// Add global excludes
result = append(result, t.Exclude...)
// Find the variant and add its excludes
for _, v := range t.Variants {
if v.Name == variantName {
result = append(result, v.Exclude...)
break
}
}
return result
}
// GetOSTreeRef returns the OSTree reference for a variant and architecture
func (t *Treefile) GetOSTreeRef(variantName, arch string) string {
if t.OSTree.Ref != "" {
return t.OSTree.Ref
}
// Generate default reference
return fmt.Sprintf("%s/%s/%s/%s", t.Release, t.Version, arch, variantName)
}