deb-bootc-image-builder/bib/cmd/builder/main.go
robojerk 126ee1a849
Some checks failed
particle-os CI / Test particle-os (push) Failing after 1s
particle-os CI / Integration Test (push) Has been skipped
particle-os CI / Security & Quality (push) Failing after 1s
Test particle-os Basic Functionality / test-basic (push) Failing after 1s
particle-os CI / Build and Release (push) Has been skipped
cleanup
2025-08-27 12:30:24 -07:00

411 lines
11 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
builder "deb-bootc-image-builder/internal/builder"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
verbose bool
workDir string
)
func main() {
rootCmd := &cobra.Command{
Use: "particle-os",
Short: "particle-os: Debian-based OS image builder",
Long: `particle-os is a Debian-native OS image builder that creates bootable images
from container images and recipes, similar to ublue-os but with Debian foundation.
Features:
• Build from YAML recipes (like BlueBuild)
• Container-first workflow
• Multiple output formats (raw, qcow2, vmdk, vdi)
• OSTree + bootupd integration
• Debian-native stages and tools`,
Version: "0.1.0",
}
// Global flags
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
rootCmd.PersistentFlags().StringVarP(&workDir, "work-dir", "w", "", "Working directory for builds")
// Add commands
rootCmd.AddCommand(buildCmd())
rootCmd.AddCommand(listCmd())
rootCmd.AddCommand(validateCmd())
rootCmd.AddCommand(versionCmd())
rootCmd.AddCommand(containerCmd())
// Execute
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func buildCmd() *cobra.Command {
var output string
var clean bool
var jsonOutput bool
var quiet bool
cmd := &cobra.Command{
Use: "build [RECIPE_PATH]",
Short: "Build OS image from recipe",
Long: `Build an OS image from a particle-os recipe file.
Examples:
particle-os build recipes/debian-desktop.yml
particle-os build --output my-image.img recipes/debian-server.yml
particle-os build --work-dir /tmp/custom-build recipes/debian-gaming.yml
particle-os build --json --quiet recipes/debian-server.yml # CI/CD mode`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
recipePath := args[0]
// Setup logging
if verbose {
logrus.SetLevel(logrus.DebugLevel)
} else if quiet {
logrus.SetLevel(logrus.ErrorLevel) // Only show errors in quiet mode
} else {
logrus.SetLevel(logrus.InfoLevel) // Default info level
}
// Load recipe
recipe, err := builder.LoadRecipe(recipePath)
if err != nil {
return fmt.Errorf("failed to load recipe: %w", err)
}
// Validate recipe
if err := recipe.Validate(); err != nil {
return fmt.Errorf("recipe validation failed: %w", err)
}
if !quiet {
fmt.Printf("✅ Recipe loaded: %s\n", recipe.Name)
fmt.Printf("📦 Base image: %s\n", recipe.BaseImage)
fmt.Printf("🔧 Stages: %d\n", len(recipe.Stages))
fmt.Printf("📤 Output formats: %v\n", recipe.Output.Formats)
}
// Create builder with appropriate log level
var logLevel logrus.Level
if verbose {
logLevel = logrus.DebugLevel
} else if quiet {
logLevel = logrus.ErrorLevel
} else {
logLevel = logrus.InfoLevel
}
imageBuilder := builder.NewBuilder(recipe, workDir, logLevel)
// Build image
if !quiet {
fmt.Println("\n🚀 Starting build...")
}
result, err := imageBuilder.Build()
if err != nil {
return fmt.Errorf("build failed: %w", err)
}
// Handle CI/CD output
if jsonOutput {
// Create CI/CD friendly output
buildResult := map[string]interface{}{
"success": true,
"recipe": recipe.Name,
"base_image": recipe.BaseImage,
"stages": len(recipe.Stages),
"output_formats": recipe.Output.Formats,
"image_path": result.ImagePath,
"work_directory": result.Metadata["work_dir"],
"build_time": time.Now().Format(time.RFC3339),
"exit_code": 0,
}
// Add output file info if specified
if output != "" {
if err := copyFile(result.ImagePath, output); err != nil {
buildResult["success"] = false
buildResult["error"] = err.Error()
buildResult["exit_code"] = 1
} else {
buildResult["output_file"] = output
}
}
// Output JSON
jsonData, _ := json.MarshalIndent(buildResult, "", " ")
fmt.Println(string(jsonData))
} else {
// Human-friendly output
if !quiet {
fmt.Printf("\n✅ Build completed successfully!\n")
fmt.Printf("📁 Image created: %s\n", result.ImagePath)
fmt.Printf("📊 Work directory: %s\n", result.Metadata["work_dir"])
}
// Copy to output if specified
if output != "" {
if err := copyFile(result.ImagePath, output); err != nil {
return fmt.Errorf("failed to copy to output: %w", err)
}
if !quiet {
fmt.Printf("📋 Copied to: %s\n", output)
}
}
}
// Cleanup if requested
if clean {
if err := imageBuilder.Cleanup(); err != nil {
if !quiet {
fmt.Printf("⚠️ Warning: cleanup failed: %v\n", err)
}
} else if !quiet {
fmt.Println("🧹 Cleanup completed")
}
}
return nil
},
}
cmd.Flags().StringVarP(&output, "output", "o", "", "Output path for the image")
cmd.Flags().BoolVarP(&clean, "clean", "c", false, "Clean up work directory after build")
cmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output results in JSON format (CI/CD friendly)")
cmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "Suppress non-essential output (CI/CD friendly)")
return cmd
}
func listCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List available recipes",
Long: `List all available particle-os recipes in the recipes directory.
This command scans the recipes directory and shows all available recipe files
with their basic information.`,
RunE: func(cmd *cobra.Command, args []string) error {
recipesDir := "recipes"
if _, err := os.Stat(recipesDir); os.IsNotExist(err) {
fmt.Printf("📁 Recipes directory not found: %s\n", recipesDir)
fmt.Println("💡 Create some recipes first!")
return nil
}
entries, err := os.ReadDir(recipesDir)
if err != nil {
return fmt.Errorf("failed to read recipes directory: %w", err)
}
fmt.Printf("📚 Available recipes in %s:\n\n", recipesDir)
recipeCount := 0
for _, entry := range entries {
if entry.IsDir() || !isRecipeFile(entry.Name()) {
continue
}
recipePath := filepath.Join(recipesDir, entry.Name())
recipe, err := builder.LoadRecipe(recipePath)
if err != nil {
fmt.Printf("❌ %s: Failed to load (%v)\n", entry.Name(), err)
continue
}
fmt.Printf("📋 %s\n", entry.Name())
fmt.Printf(" Name: %s\n", recipe.Name)
fmt.Printf(" Description: %s\n", recipe.Description)
fmt.Printf(" Base Image: %s\n", recipe.BaseImage)
fmt.Printf(" Stages: %d\n", len(recipe.Stages))
fmt.Printf(" Output Formats: %v\n", recipe.Output.Formats)
fmt.Println()
recipeCount++
}
if recipeCount == 0 {
fmt.Println("💡 No recipes found. Create some recipe files first!")
} else {
fmt.Printf("📊 Total recipes: %d\n", recipeCount)
}
return nil
},
}
return cmd
}
func validateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "validate [RECIPE_PATH]",
Short: "Validate a particle-os recipe",
Long: `Validate a particle-os recipe file for syntax and configuration errors.
This command checks the recipe file for:
• YAML syntax validity
• Required fields presence
• Stage configuration correctness
• Output format specification`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
recipePath := args[0]
fmt.Printf("🔍 Validating recipe: %s\n\n", recipePath)
// Load recipe
recipe, err := builder.LoadRecipe(recipePath)
if err != nil {
return fmt.Errorf("❌ Recipe loading failed: %w", err)
}
fmt.Println("✅ Recipe loaded successfully")
// Validate recipe
if err := recipe.Validate(); err != nil {
return fmt.Errorf("❌ Recipe validation failed: %w", err)
}
fmt.Println("✅ Recipe validation passed")
// Show recipe details
fmt.Printf("\n📋 Recipe Details:\n")
fmt.Printf(" Name: %s\n", recipe.Name)
fmt.Printf(" Description: %s\n", recipe.Description)
fmt.Printf(" Base Image: %s\n", recipe.BaseImage)
fmt.Printf(" Image Version: %s\n", recipe.ImageVersion)
fmt.Printf(" Stages: %d\n", len(recipe.Stages))
fmt.Printf(" Output Formats: %v\n", recipe.Output.Formats)
fmt.Printf(" Output Size: %s\n", recipe.Output.Size)
// Show stages
fmt.Printf("\n🔧 Stages:\n")
for i, stage := range recipe.Stages {
fmt.Printf(" %d. %s\n", i+1, stage.Type)
if len(stage.Options) > 0 {
fmt.Printf(" Options: %v\n", stage.Options)
}
}
fmt.Printf("\n🎉 Recipe is valid and ready to build!\n")
return nil
},
}
return cmd
}
func versionCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Show particle-os version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("particle-os v0.1.0")
fmt.Println("Debian-native OS image builder")
fmt.Println("Built with ❤️ for the Debian community")
},
}
return cmd
}
func containerCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "container [IMAGE_REF]",
Short: "Show container image information",
Long: `Show detailed information about a container image.
This command inspects a container image and displays:
• Image ID and digest
• Size and creation date
• Architecture and OS
• Labels and metadata
• Layer information`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
imageRef := args[0]
fmt.Printf("🔍 Inspecting container: %s\n\n", imageRef)
// Create container processor
processor := builder.NewContainerProcessor("/tmp/particle-os-container-info", logrus.InfoLevel)
// Check availability
if !processor.IsAvailable() {
return fmt.Errorf("no container runtime (podman/docker) available")
}
fmt.Printf("📦 Container runtime: %s\n", processor.GetContainerRuntime())
// Get container info
info, err := processor.GetContainerInfo(imageRef)
if err != nil {
return fmt.Errorf("failed to get container info: %w", err)
}
// Display info
fmt.Printf("\n📋 Container Information:\n")
fmt.Printf(" ID: %s\n", info.ID)
fmt.Printf(" Digest: %s\n", info.Digest)
fmt.Printf(" Size: %d bytes (%.2f MB)\n", info.Size, float64(info.Size)/1024/1024)
fmt.Printf(" Created: %s\n", info.Created)
fmt.Printf(" OS: %s\n", info.OS)
fmt.Printf(" Architecture: %s\n", info.Arch)
fmt.Printf(" Variant: %s\n", info.Variant)
if len(info.Labels) > 0 {
fmt.Printf("\n🏷 Labels:\n")
for k, v := range info.Labels {
fmt.Printf(" %s: %s\n", k, v)
}
}
if len(info.Layers) > 0 {
fmt.Printf("\n📚 Layers: %d\n", len(info.Layers))
}
fmt.Printf("\n✅ Container inspection completed\n")
return nil
},
}
return cmd
}
// Helper functions
func isRecipeFile(filename string) bool {
ext := filepath.Ext(filename)
return ext == ".yml" || ext == ".yaml"
}
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = destFile.ReadFrom(sourceFile)
return err
}