- 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
258 lines
8.3 KiB
Go
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)
|
|
}
|