Add comprehensive documentation, recipes, and testing framework
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
Tests / test (1.21.x) (push) Failing after 1s
Tests / test (1.22.x) (push) Failing after 1s
particle-os CI / Build and Release (push) Has been skipped
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
Tests / test (1.21.x) (push) Failing after 1s
Tests / test (1.22.x) (push) Failing after 1s
particle-os CI / Build and Release (push) Has been skipped
- Add extensive documentation covering current status, usage, and testing strategies - Add recipe files for various image configurations (minimal, debug, kernel test, etc.) - Add testing and management scripts for comprehensive testing workflows - Add Go module configuration and updated Go code - Add manual bootable image creation script - Update todo with current project status and next steps
This commit is contained in:
parent
65302755dd
commit
0409f1d67c
34 changed files with 5328 additions and 346 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package particle_os
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -10,17 +11,19 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Builder builds OS images from particle-os recipes
|
||||
type Builder struct {
|
||||
recipe *Recipe
|
||||
workDir string
|
||||
logger *logrus.Logger
|
||||
buildID string
|
||||
artifacts map[string]string
|
||||
container *ContainerProcessor
|
||||
packages *PackageManager
|
||||
recipe *Recipe
|
||||
workDir string
|
||||
logger *logrus.Logger
|
||||
buildID string
|
||||
artifacts map[string]string
|
||||
container *ContainerProcessor
|
||||
packages *PackageManager
|
||||
loopDevice string
|
||||
}
|
||||
|
||||
// BuildResult represents the result of a build
|
||||
|
|
@ -74,6 +77,20 @@ func (b *Builder) Build() (*BuildResult, error) {
|
|||
|
||||
// Execute stages
|
||||
if err := b.executeStages(); err != nil {
|
||||
// Enhanced error reporting for stage execution failures
|
||||
b.logger.Errorf("Build failed during stage execution:")
|
||||
b.logger.Errorf(" Recipe: %s", b.recipe.Name)
|
||||
b.logger.Errorf(" Base image: %s", b.recipe.BaseImage)
|
||||
b.logger.Errorf(" Work directory: %s", b.workDir)
|
||||
b.logger.Errorf(" Error: %v", err)
|
||||
|
||||
// Provide helpful debugging information
|
||||
b.logger.Errorf("Debugging tips:")
|
||||
b.logger.Errorf(" 1. Check work directory: %s", b.workDir)
|
||||
b.logger.Errorf(" 2. Review stage logs in: %s/stages/", b.workDir)
|
||||
b.logger.Errorf(" 3. Verify sudo access for file operations")
|
||||
b.logger.Errorf(" 4. Check available disk space")
|
||||
|
||||
return nil, fmt.Errorf("stage execution failed: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +116,11 @@ func (b *Builder) Build() (*BuildResult, error) {
|
|||
|
||||
// setupWorkDir creates and prepares the working directory
|
||||
func (b *Builder) setupWorkDir() error {
|
||||
// Check available disk space before proceeding
|
||||
if err := b.checkDiskSpace(); err != nil {
|
||||
return fmt.Errorf("insufficient disk space: %w", err)
|
||||
}
|
||||
|
||||
// Create work directory
|
||||
if err := os.MkdirAll(b.workDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create work directory: %w", err)
|
||||
|
|
@ -122,6 +144,44 @@ func (b *Builder) setupWorkDir() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// checkDiskSpace verifies sufficient disk space is available for the build
|
||||
func (b *Builder) checkDiskSpace() error {
|
||||
// Get disk space information for the work directory
|
||||
workDirPath := b.workDir
|
||||
if workDirPath == "" {
|
||||
// Default to /tmp if no work directory specified
|
||||
workDirPath = "/tmp"
|
||||
}
|
||||
|
||||
// Get absolute path
|
||||
absPath, err := filepath.Abs(workDirPath)
|
||||
if err != nil {
|
||||
// If we can't get absolute path, use original
|
||||
absPath = workDirPath
|
||||
}
|
||||
|
||||
// Get disk space info
|
||||
var stat unix.Statfs_t
|
||||
if err := unix.Statfs(absPath, &stat); err != nil {
|
||||
// If we can't check disk space, log warning but continue
|
||||
b.logger.Warnf("Could not check disk space for %s: %v", absPath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate available space in GB
|
||||
availableGB := (stat.Bavail * uint64(stat.Bsize)) / (1024 * 1024 * 1024)
|
||||
|
||||
// Require at least 5GB available for builds
|
||||
requiredGB := uint64(5)
|
||||
|
||||
if availableGB < requiredGB {
|
||||
return fmt.Errorf("insufficient disk space: %d GB available, %d GB required", availableGB, requiredGB)
|
||||
}
|
||||
|
||||
b.logger.Infof("Disk space check passed: %d GB available", availableGB)
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeStages runs all stages in the recipe
|
||||
func (b *Builder) executeStages() error {
|
||||
b.logger.Info("Executing recipe stages")
|
||||
|
|
@ -130,7 +190,24 @@ func (b *Builder) executeStages() error {
|
|||
b.logger.Infof("Executing stage %d/%d: %s", i+1, len(b.recipe.Stages), stage.Type)
|
||||
|
||||
if err := b.executeStage(stage, i); err != nil {
|
||||
return fmt.Errorf("stage %d (%s) failed: %w", i+1, stage.Type, err)
|
||||
// Enhanced error reporting with stage details
|
||||
stageError := fmt.Errorf("stage %d (%s) failed: %w", i+1, stage.Type, err)
|
||||
|
||||
// Log detailed error information for debugging
|
||||
b.logger.Errorf("Stage execution failed:")
|
||||
b.logger.Errorf(" Stage: %d/%d (%s)", i+1, len(b.recipe.Stages), stage.Type)
|
||||
b.logger.Errorf(" Error: %v", err)
|
||||
|
||||
// Log stage options for debugging
|
||||
if len(stage.Options) > 0 {
|
||||
b.logger.Errorf(" Stage options: %+v", stage.Options)
|
||||
}
|
||||
|
||||
// Log work directory location for manual inspection
|
||||
b.logger.Errorf(" Work directory: %s", b.workDir)
|
||||
b.logger.Errorf(" Stage directory: %s/stages/%02d-%s", b.workDir, i, stage.Type)
|
||||
|
||||
return stageError
|
||||
}
|
||||
|
||||
b.logger.Infof("Stage %d completed successfully", i+1)
|
||||
|
|
@ -167,6 +244,12 @@ func (b *Builder) executeStage(stage Stage, index int) error {
|
|||
return b.executeOSTreeBootStage(stage, stageDir)
|
||||
case "org.osbuild.ostree_deploy":
|
||||
return b.executeOSTreeDeployStage(stage, stageDir)
|
||||
case "org.osbuild.debian.bootc":
|
||||
return b.executeBootc(stage, stageDir)
|
||||
case "org.osbuild.debian.bootupd":
|
||||
return b.executeBootupd(stage, stageDir)
|
||||
case "org.osbuild.debian.kernel":
|
||||
return b.executeKernel(stage, stageDir)
|
||||
default:
|
||||
return fmt.Errorf("unknown stage type: %s", stage.Type)
|
||||
}
|
||||
|
|
@ -434,12 +517,205 @@ func (b *Builder) executeOSTree(stage Stage, stageDir string) error {
|
|||
func (b *Builder) executeBootupd(stage Stage, stageDir string) error {
|
||||
b.logger.Info("Executing bootupd stage")
|
||||
|
||||
// TODO: Implement actual bootupd configuration
|
||||
// Extract bootupd options
|
||||
configFile, _ := stage.Options["config_file"].(string)
|
||||
updateMethod, _ := stage.Options["update_method"].(string)
|
||||
deploymentSource, _ := stage.Options["deployment_source"].(string)
|
||||
autoUpdate, _ := stage.Options["auto_update"].(bool)
|
||||
|
||||
// Set defaults
|
||||
if configFile == "" {
|
||||
configFile = "/etc/bootupd/config.yaml"
|
||||
}
|
||||
if updateMethod == "" {
|
||||
updateMethod = "atomic"
|
||||
}
|
||||
if deploymentSource == "" {
|
||||
deploymentSource = "ostree"
|
||||
}
|
||||
|
||||
b.logger.Infof("Bootupd configuration: config=%s, update=%s, source=%s, auto_update=%t",
|
||||
configFile, updateMethod, deploymentSource, autoUpdate)
|
||||
|
||||
// Get rootfs directory from artifacts
|
||||
rootfsDir, exists := b.artifacts["rootfs"]
|
||||
if !exists {
|
||||
return fmt.Errorf("rootfs not found in artifacts")
|
||||
}
|
||||
|
||||
// Create bootupd configuration directory
|
||||
bootupdConfigDir := filepath.Join(rootfsDir, "etc/bootupd")
|
||||
if err := os.MkdirAll(bootupdConfigDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create bootupd config directory: %w", err)
|
||||
}
|
||||
|
||||
// Write bootupd configuration file
|
||||
bootupdConfig := fmt.Sprintf(`# Bootupd configuration for particle-os
|
||||
update_method: %s
|
||||
deployment_source: %s
|
||||
auto_update: %t
|
||||
ostree:
|
||||
enabled: true
|
||||
repository: /ostree/repo
|
||||
branch: main
|
||||
deployment_type: atomic
|
||||
`, updateMethod, deploymentSource, autoUpdate)
|
||||
|
||||
bootupdConfigPath := filepath.Join(bootupdConfigDir, "bootupd.conf")
|
||||
if err := os.WriteFile(bootupdConfigPath, []byte(bootupdConfig), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write bootupd config: %w", err)
|
||||
}
|
||||
b.logger.Info("Bootupd configuration written")
|
||||
|
||||
// Create bootupd systemd service directory
|
||||
systemdDir := filepath.Join(rootfsDir, "etc/systemd/system")
|
||||
if err := os.MkdirAll(systemdDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create systemd directory: %w", err)
|
||||
}
|
||||
|
||||
// Write bootupd systemd service
|
||||
bootupdService := `[Unit]
|
||||
Description=Bootupd Update Management Service
|
||||
After=network.target ostree-boot.service bootc.service
|
||||
Wants=ostree-boot.service bootc.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/bootupd status
|
||||
ExecStart=/usr/bin/bootupd check-update
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
bootupdServicePath := filepath.Join(systemdDir, "bootupd.service")
|
||||
if err := os.WriteFile(bootupdServicePath, []byte(bootupdService), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write bootupd service: %w", err)
|
||||
}
|
||||
b.logger.Info("Bootupd systemd service written")
|
||||
|
||||
// Create bootupd timer for auto-updates if enabled
|
||||
if autoUpdate {
|
||||
bootupdTimer := `[Unit]
|
||||
Description=Bootupd Auto-Update Timer
|
||||
After=network.target
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=1h
|
||||
RandomizedDelaySec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
`
|
||||
|
||||
bootupdTimerPath := filepath.Join(systemdDir, "bootupd.timer")
|
||||
if err := os.WriteFile(bootupdTimerPath, []byte(bootupdTimer), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write bootupd timer: %w", err)
|
||||
}
|
||||
b.logger.Info("Bootupd auto-update timer written")
|
||||
}
|
||||
|
||||
// Create placeholder
|
||||
placeholder := filepath.Join(stageDir, "bootupd-completed")
|
||||
if err := os.WriteFile(placeholder, []byte("bootupd stage completed"), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create placeholder: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeBootc runs the bootc stage
|
||||
func (b *Builder) executeBootc(stage Stage, stageDir string) error {
|
||||
b.logger.Info("Executing bootc stage")
|
||||
|
||||
// Extract bootc options
|
||||
configFile, _ := stage.Options["config_file"].(string)
|
||||
deploymentType, _ := stage.Options["deployment_type"].(string)
|
||||
bootMethod, _ := stage.Options["boot_method"].(string)
|
||||
kernelArgs, _ := stage.Options["kernel_args"].(string)
|
||||
|
||||
// Set defaults
|
||||
if configFile == "" {
|
||||
configFile = "/etc/bootc/config.yaml"
|
||||
}
|
||||
if deploymentType == "" {
|
||||
deploymentType = "atomic"
|
||||
}
|
||||
if bootMethod == "" {
|
||||
bootMethod = "ostree"
|
||||
}
|
||||
|
||||
b.logger.Infof("Bootc configuration: config=%s, deployment=%s, boot=%s, kernel_args=%s",
|
||||
configFile, deploymentType, bootMethod, kernelArgs)
|
||||
|
||||
// Get rootfs directory from artifacts
|
||||
rootfsDir, exists := b.artifacts["rootfs"]
|
||||
if !exists {
|
||||
return fmt.Errorf("rootfs not found in artifacts")
|
||||
}
|
||||
|
||||
// Create bootc configuration directory
|
||||
bootcConfigDir := filepath.Join(rootfsDir, "etc/bootc")
|
||||
if err := os.MkdirAll(bootcConfigDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create bootc config directory: %w", err)
|
||||
}
|
||||
|
||||
// Write bootc configuration file
|
||||
bootcConfig := fmt.Sprintf(`# Bootc configuration for particle-os
|
||||
deployment_type: %s
|
||||
boot_method: %s
|
||||
kernel_args: %s
|
||||
ostree:
|
||||
enabled: true
|
||||
deployment_source: ostree
|
||||
`, deploymentType, bootMethod, kernelArgs)
|
||||
|
||||
bootcConfigPath := filepath.Join(bootcConfigDir, "config.yaml")
|
||||
if err := os.WriteFile(bootcConfigPath, []byte(bootcConfig), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write bootc config: %w", err)
|
||||
}
|
||||
b.logger.Info("Bootc configuration written")
|
||||
|
||||
// Create bootc systemd service directory
|
||||
systemdDir := filepath.Join(rootfsDir, "etc/systemd/system")
|
||||
if err := os.MkdirAll(systemdDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create systemd directory: %w", err)
|
||||
}
|
||||
|
||||
// Write bootc systemd service
|
||||
bootcService := `[Unit]
|
||||
Description=Bootc Boot Management Service
|
||||
After=network.target ostree-boot.service
|
||||
Wants=ostree-boot.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/bootc status
|
||||
ExecStart=/usr/bin/bootc apply
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
bootcServicePath := filepath.Join(systemdDir, "bootc.service")
|
||||
if err := os.WriteFile(bootcServicePath, []byte(bootcService), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write bootc service: %w", err)
|
||||
}
|
||||
b.logger.Info("Bootc systemd service written")
|
||||
|
||||
// Create placeholder
|
||||
placeholder := filepath.Join(stageDir, "bootc-completed")
|
||||
if err := os.WriteFile(placeholder, []byte("bootc stage completed"), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create placeholder: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -704,6 +980,7 @@ func (b *Builder) createBootableImage(rootfsPath, outputPath string) error {
|
|||
return fmt.Errorf("failed to set up loop device: %w", err)
|
||||
}
|
||||
loopDevice := strings.TrimSpace(string(output))
|
||||
b.loopDevice = loopDevice
|
||||
b.logger.Infof("Loop device: %s", loopDevice)
|
||||
|
||||
// Clean up loop device on exit
|
||||
|
|
@ -805,7 +1082,8 @@ devpts /dev/pts devpts gid=5,mode=620 0 0
|
|||
tmpfs /run tmpfs defaults 0 0
|
||||
`
|
||||
fstabFile := filepath.Join(mountPoint, "etc", "fstab")
|
||||
if err := os.WriteFile(fstabFile, []byte(fstabContent), 0644); err != nil {
|
||||
cmd := exec.Command("sudo", "sh", "-c", fmt.Sprintf("echo '%s' > %s", fstabContent, fstabFile))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create fstab: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -821,7 +1099,8 @@ si::sysinit:/etc/init.d/rcS
|
|||
6:23:respawn:/sbin/getty 38400 tty6
|
||||
`
|
||||
inittabFile := filepath.Join(mountPoint, "etc", "inittab")
|
||||
if err := os.WriteFile(inittabFile, []byte(inittabContent), 0644); err != nil {
|
||||
cmd = exec.Command("sudo", "sh", "-c", fmt.Sprintf("echo '%s' > %s", inittabContent, inittabFile))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create inittab: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -838,30 +1117,69 @@ echo "particle-os started successfully"
|
|||
if err := os.MkdirAll(filepath.Dir(rcsFile), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create init.d directory: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(rcsFile, []byte(rcsContent), 0755); err != nil {
|
||||
cmd = exec.Command("sudo", "sh", "-c", fmt.Sprintf("echo '%s' > %s", rcsContent, rcsFile))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create rcS: %w", err)
|
||||
}
|
||||
// Set executable permissions
|
||||
cmd = exec.Command("sudo", "chmod", "755", rcsFile)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to set rcS permissions: %w", err)
|
||||
}
|
||||
|
||||
// Create a simple kernel command line
|
||||
cmdline := "root=/dev/sda1 rw console=ttyS0 init=/bin/sh"
|
||||
cmdlineFile := filepath.Join(bootDir, "cmdline.txt")
|
||||
if err := os.WriteFile(cmdlineFile, []byte(cmdline), 0644); err != nil {
|
||||
cmd = exec.Command("sudo", "sh", "-c", fmt.Sprintf("echo '%s' > %s", cmdline, cmdlineFile))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create cmdline.txt: %w", err)
|
||||
}
|
||||
|
||||
// Try to install extlinux bootloader
|
||||
if _, err := exec.LookPath("extlinux"); err == nil {
|
||||
b.logger.Info("Installing extlinux bootloader")
|
||||
cmd := exec.Command("sudo", "extlinux", "--install", bootDir)
|
||||
// Try to install GRUB bootloader
|
||||
if _, err := exec.LookPath("grub-install"); err == nil {
|
||||
b.logger.Info("Installing GRUB bootloader")
|
||||
// Install GRUB to the loop device
|
||||
cmd = exec.Command("sudo", "grub-install", "--target=i386-pc", "--boot-directory="+bootDir, "--modules=part_gpt ext2", b.loopDevice)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
b.logger.Warnf("extlinux installation failed: %v", err)
|
||||
b.logger.Warnf("GRUB installation failed: %v", err)
|
||||
} else {
|
||||
b.logger.Info("extlinux installed successfully")
|
||||
b.logger.Info("GRUB installed successfully")
|
||||
|
||||
// Create a basic grub.cfg
|
||||
grubCfg := fmt.Sprintf(`# GRUB configuration for particle-os
|
||||
set timeout=5
|
||||
set default=0
|
||||
|
||||
menuentry "Particle-OS" {
|
||||
set root=(hd0,1)
|
||||
linux /boot/vmlinuz-6.12.41+deb13-amd64 root=/dev/sda1 rw console=ttyS0
|
||||
initrd /boot/initrd.img-6.12.41+deb13-amd64
|
||||
}
|
||||
`)
|
||||
grubCfgFile := filepath.Join(bootDir, "grub", "grub.cfg")
|
||||
cmd = exec.Command("sudo", "sh", "-c", fmt.Sprintf("echo '%s' > %s", grubCfg, grubCfgFile))
|
||||
if err := cmd.Run(); err != nil {
|
||||
b.logger.Warnf("Failed to create grub.cfg: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b.logger.Info("extlinux not available, skipping bootloader installation")
|
||||
b.logger.Info("grub-install not available, trying extlinux")
|
||||
// Try to install extlinux bootloader as fallback
|
||||
if _, err := exec.LookPath("extlinux"); err == nil {
|
||||
b.logger.Info("Installing extlinux bootloader")
|
||||
cmd = exec.Command("sudo", "extlinux", "--install", bootDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
b.logger.Warnf("extlinux installation failed: %v", err)
|
||||
} else {
|
||||
b.logger.Info("extlinux installed successfully")
|
||||
}
|
||||
} else {
|
||||
b.logger.Info("No bootloader available, image will not be bootable")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -907,3 +1225,187 @@ func (b *Builder) Cleanup() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeKernel runs the kernel installation stage
|
||||
func (b *Builder) executeKernel(stage Stage, stageDir string) error {
|
||||
b.logger.Info("Executing kernel installation stage")
|
||||
|
||||
// Extract kernel options
|
||||
kernelPackage, _ := stage.Options["kernel_package"].(string)
|
||||
initramfs, _ := stage.Options["initramfs"].(bool)
|
||||
kernelVersion, _ := stage.Options["kernel_version"].(string)
|
||||
kernelArgs, _ := stage.Options["kernel_args"].(string)
|
||||
|
||||
// Set defaults
|
||||
if kernelPackage == "" {
|
||||
kernelPackage = "linux-image-amd64"
|
||||
}
|
||||
if kernelArgs == "" {
|
||||
kernelArgs = "root=/dev/sda1 rw console=ttyS0"
|
||||
}
|
||||
|
||||
b.logger.Infof("Kernel configuration: package=%s, initramfs=%t, version=%s, args=%s",
|
||||
kernelPackage, initramfs, kernelVersion, kernelArgs)
|
||||
|
||||
// Get rootfs directory from artifacts
|
||||
rootfsDir, exists := b.artifacts["rootfs"]
|
||||
if !exists {
|
||||
return fmt.Errorf("rootfs not found in artifacts")
|
||||
}
|
||||
|
||||
// Install kernel package using package manager
|
||||
if err := b.packages.InstallPackages([]string{kernelPackage}, false, false); err != nil {
|
||||
return fmt.Errorf("failed to install kernel package: %w", err)
|
||||
}
|
||||
b.logger.Info("Kernel package installed successfully")
|
||||
|
||||
// Create /boot directory if it doesn't exist
|
||||
bootDir := filepath.Join(rootfsDir, "boot")
|
||||
if err := os.MkdirAll(bootDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create boot directory: %w", err)
|
||||
}
|
||||
|
||||
// Find installed kernel files
|
||||
kernelFiles, err := b.findKernelFiles(rootfsDir, kernelPackage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find kernel files: %w", err)
|
||||
}
|
||||
|
||||
// Copy kernel files to /boot
|
||||
for _, kernelFile := range kernelFiles {
|
||||
if err := b.copyKernelFileWithSudo(kernelFile, bootDir); err != nil {
|
||||
b.logger.Warnf("Failed to copy kernel file %s: %v", kernelFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate initramfs if requested
|
||||
if initramfs {
|
||||
if err := b.generateInitramfs(rootfsDir, kernelVersion); err != nil {
|
||||
b.logger.Warnf("Failed to generate initramfs: %v", err)
|
||||
} else {
|
||||
b.logger.Info("Initramfs generated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// Create kernel command line file using sudo
|
||||
cmdlineFile := filepath.Join(bootDir, "cmdline.txt")
|
||||
if err := b.writeFileWithSudo(cmdlineFile, []byte(kernelArgs), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create kernel command line: %w", err)
|
||||
}
|
||||
|
||||
// Create kernel configuration using sudo
|
||||
kernelConfig := fmt.Sprintf(`# Kernel configuration for particle-os
|
||||
KERNEL_PACKAGE=%s
|
||||
KERNEL_VERSION=%s
|
||||
INITRAMFS=%t
|
||||
KERNEL_ARGS="%s"
|
||||
`, kernelPackage, kernelVersion, initramfs, kernelArgs)
|
||||
|
||||
configFile := filepath.Join(bootDir, "kernel.conf")
|
||||
if err := b.writeFileWithSudo(configFile, []byte(kernelConfig), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create kernel config: %w", err)
|
||||
}
|
||||
|
||||
// Create placeholder
|
||||
placeholder := filepath.Join(stageDir, "kernel-completed")
|
||||
if err := os.WriteFile(placeholder, []byte("kernel stage completed"), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create placeholder: %w", err)
|
||||
}
|
||||
|
||||
b.logger.Info("Kernel installation stage completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// findKernelFiles searches for kernel files in the rootfs
|
||||
func (b *Builder) findKernelFiles(rootfsDir, kernelPackage string) ([]string, error) {
|
||||
var kernelFiles []string
|
||||
|
||||
// Common kernel file locations
|
||||
kernelPaths := []string{
|
||||
filepath.Join(rootfsDir, "boot"),
|
||||
filepath.Join(rootfsDir, "lib", "modules"),
|
||||
filepath.Join(rootfsDir, "usr", "lib", "modules"),
|
||||
}
|
||||
|
||||
// Search for kernel files
|
||||
for _, kernelPath := range kernelPaths {
|
||||
if err := filepath.Walk(kernelPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
|
||||
// Look for kernel image files
|
||||
if !info.IsDir() {
|
||||
filename := info.Name()
|
||||
if strings.HasPrefix(filename, "vmlinuz") ||
|
||||
strings.HasPrefix(filename, "bzImage") ||
|
||||
strings.HasPrefix(filename, "Image") {
|
||||
kernelFiles = append(kernelFiles, path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
b.logger.Warnf("Error walking %s: %v", kernelPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(kernelFiles) == 0 {
|
||||
return nil, fmt.Errorf("no kernel files found in %s", rootfsDir)
|
||||
}
|
||||
|
||||
return kernelFiles, nil
|
||||
}
|
||||
|
||||
// copyKernelFileWithSudo copies a kernel file to the boot directory using sudo
|
||||
func (b *Builder) copyKernelFileWithSudo(srcPath, bootDir string) error {
|
||||
filename := filepath.Base(srcPath)
|
||||
dstPath := filepath.Join(bootDir, filename)
|
||||
|
||||
// Copy file to boot directory using sudo
|
||||
cmd := exec.Command("sudo", "cp", srcPath, dstPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy kernel file %s to %s: %w", srcPath, dstPath, err)
|
||||
}
|
||||
|
||||
b.logger.Infof("Copied kernel file: %s -> %s", srcPath, dstPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFileWithSudo writes a file to a path using sudo
|
||||
func (b *Builder) writeFileWithSudo(path string, data []byte, perm os.FileMode) error {
|
||||
cmd := exec.Command("sudo", "tee", path)
|
||||
cmd.Stdin = bytes.NewReader(data)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %w", path, err)
|
||||
}
|
||||
|
||||
b.logger.Infof("Wrote file: %s", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateInitramfs generates an initramfs for the kernel
|
||||
func (b *Builder) generateInitramfs(rootfsDir, kernelVersion string) error {
|
||||
// Check if update-initramfs is available
|
||||
if _, err := exec.LookPath("update-initramfs"); err != nil {
|
||||
b.logger.Warn("update-initramfs not available, skipping initramfs generation")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create initramfs using update-initramfs
|
||||
cmd := exec.Command("sudo", "chroot", rootfsDir, "update-initramfs", "-u", "-k", kernelVersion)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to generate initramfs: %w", err)
|
||||
}
|
||||
|
||||
b.logger.Info("Initramfs generated successfully")
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,53 @@ func NewPackageManager(rootfsDir, workDir string, logLevel logrus.Level) *Packag
|
|||
}
|
||||
}
|
||||
|
||||
// writeFileWithSudo writes a file to the rootfs using sudo to handle permission issues
|
||||
func (pm *PackageManager) writeFileWithSudo(path string, content []byte, mode os.FileMode) error {
|
||||
// Create a temporary file in the work directory
|
||||
tempFile := filepath.Join(pm.workDir, "temp_"+filepath.Base(path))
|
||||
if err := os.WriteFile(tempFile, content, mode); err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(tempFile) // Clean up temp file
|
||||
|
||||
// Use sudo to copy the file to the target location
|
||||
cmd := exec.Command("sudo", "cp", tempFile, path)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy file with sudo: %w", err)
|
||||
}
|
||||
|
||||
// Set the correct permissions
|
||||
chmodCmd := exec.Command("sudo", "chmod", fmt.Sprintf("%o", mode), path)
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
pm.logger.Warnf("Failed to set permissions on %s: %v", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeFileWithSudo removes a file from the rootfs using sudo
|
||||
func (pm *PackageManager) removeFileWithSudo(path string) error {
|
||||
cmd := exec.Command("sudo", "rm", "-f", path)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to remove file with sudo: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSymlinkWithSudo creates a symlink in the rootfs using sudo
|
||||
func (pm *PackageManager) createSymlinkWithSudo(target, linkPath string) error {
|
||||
// Remove existing symlink if it exists
|
||||
pm.removeFileWithSudo(linkPath)
|
||||
|
||||
// Create symlink with sudo
|
||||
cmd := exec.Command("sudo", "ln", "-s", target, linkPath)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create symlink with sudo: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallPackages installs packages using apt
|
||||
func (pm *PackageManager) InstallPackages(packages []string, update, clean bool) error {
|
||||
pm.logger.Infof("Installing packages: %v", packages)
|
||||
|
|
@ -314,7 +361,7 @@ func (pm *PackageManager) ConfigureLocale(language, defaultLocale string, additi
|
|||
}
|
||||
|
||||
localeGenPath := filepath.Join(pm.rootfsDir, "etc/locale.gen")
|
||||
if err := os.WriteFile(localeGenPath, []byte(localeGenContent), 0644); err != nil {
|
||||
if err := pm.writeFileWithSudo(localeGenPath, []byte(localeGenContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write locale.gen: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +378,7 @@ func (pm *PackageManager) ConfigureLocale(language, defaultLocale string, additi
|
|||
defaultLocalePath := filepath.Join(pm.rootfsDir, "etc/default/locale")
|
||||
defaultContent := fmt.Sprintf("LANG=%s\n", defaultLocale)
|
||||
|
||||
if err := os.WriteFile(defaultLocalePath, []byte(defaultContent), 0644); err != nil {
|
||||
if err := pm.writeFileWithSudo(defaultLocalePath, []byte(defaultContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write default locale: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -345,7 +392,7 @@ func (pm *PackageManager) ConfigureTimezone(timezone string) error {
|
|||
|
||||
// Create timezone file
|
||||
timezonePath := filepath.Join(pm.rootfsDir, "etc/timezone")
|
||||
if err := os.WriteFile(timezonePath, []byte(timezone+"\n"), 0644); err != nil {
|
||||
if err := pm.writeFileWithSudo(timezonePath, []byte(timezone+"\n"), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write timezone: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -354,10 +401,10 @@ func (pm *PackageManager) ConfigureTimezone(timezone string) error {
|
|||
localtimePath := filepath.Join(pm.rootfsDir, "etc/localtime")
|
||||
|
||||
// Remove existing localtime if it exists
|
||||
os.Remove(localtimePath)
|
||||
pm.removeFileWithSudo(localtimePath)
|
||||
|
||||
// Create symlink to zoneinfo
|
||||
if err := os.Symlink(zoneInfoPath, localtimePath); err != nil {
|
||||
if err := pm.createSymlinkWithSudo(zoneInfoPath, localtimePath); err != nil {
|
||||
return fmt.Errorf("failed to create localtime symlink: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue