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

- 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:
joe 2025-08-19 20:50:20 -07:00
parent 65302755dd
commit 0409f1d67c
34 changed files with 5328 additions and 346 deletions

View file

@ -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
}

View file

@ -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)
}