deb-bootc-image-builder/bib/internal/builder/package_manager.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

490 lines
15 KiB
Go

package particle_os
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/sirupsen/logrus"
)
// PackageManager handles package operations in the extracted container
type PackageManager struct {
logger *logrus.Logger
rootfsDir string
workDir string
}
// NewPackageManager creates a new package manager
func NewPackageManager(rootfsDir, workDir string, logLevel logrus.Level) *PackageManager {
logger := logrus.New()
logger.SetLevel(logLevel)
return &PackageManager{
logger: logger,
rootfsDir: rootfsDir,
workDir: workDir,
}
}
// 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)
if len(packages) == 0 {
pm.logger.Info("No packages to install")
return nil
}
// Update package lists if requested
if update {
if err := pm.updatePackageLists(); err != nil {
return fmt.Errorf("failed to update package lists: %w", err)
}
}
// Install packages
if err := pm.installPackages(packages); err != nil {
return fmt.Errorf("failed to install packages: %w", err)
}
// Clean package cache if requested
if clean {
if err := pm.cleanPackageCache(); err != nil {
return fmt.Errorf("failed to clean package cache: %w", err)
}
}
pm.logger.Info("Package installation completed successfully")
return nil
}
// updatePackageLists updates the package lists
func (pm *PackageManager) updatePackageLists() error {
pm.logger.Info("Updating package lists")
// Create a chroot environment for apt with sudo
cmd := exec.Command("sudo", "/usr/sbin/chroot", pm.rootfsDir, "apt-get", "update")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("apt-get update failed: %w", err)
}
pm.logger.Info("Package lists updated successfully")
return nil
}
// installPackages installs the specified packages
func (pm *PackageManager) installPackages(packages []string) error {
pm.logger.Infof("Installing %d packages", len(packages))
// Prepare apt-get install command
args := []string{"apt-get", "install", "-y", "--no-install-recommends"}
args = append(args, packages...)
// Execute in chroot with sudo
chrootArgs := append([]string{"/usr/sbin/chroot", pm.rootfsDir}, args...)
cmd := exec.Command("sudo", chrootArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("apt-get install failed: %w", err)
}
pm.logger.Infof("Successfully installed %d packages", len(packages))
return nil
}
// cleanPackageCache cleans the package cache
func (pm *PackageManager) cleanPackageCache() error {
pm.logger.Info("Cleaning package cache")
cmd := exec.Command("sudo", "/usr/sbin/chroot", pm.rootfsDir, "apt-get", "clean")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("apt-get clean failed: %w", err)
}
pm.logger.Info("Package cache cleaned successfully")
return nil
}
// ConfigureSources configures package sources with optional apt-cacher-ng support
func (pm *PackageManager) ConfigureSources(mirror string, components []string, additionalSources []string) error {
pm.logger.Infof("Configuring package sources: %s", mirror)
// Create sources.list.d directory
sourcesDir := filepath.Join(pm.rootfsDir, "etc/apt/sources.list.d")
if err := os.MkdirAll(sourcesDir, 0755); err != nil {
return fmt.Errorf("failed to create sources directory: %w", err)
}
// Check for apt-cacher-ng configuration (optional)
aptCacheURL := pm.detectAptCacherNG()
if aptCacheURL != "" {
pm.logger.Infof("apt-cacher-ng detected at: %s (optional enhancement)", aptCacheURL)
mirror = pm.convertToAptCacherNG(mirror, aptCacheURL)
} else {
pm.logger.Info("apt-cacher-ng not detected, using direct repository URLs")
}
// Create main sources.list
mainSources := filepath.Join(pm.rootfsDir, "etc/apt/sources.list")
mainContent := fmt.Sprintf("deb %s trixie %s\n", mirror, strings.Join(components, " "))
if err := os.WriteFile(mainSources, []byte(mainContent), 0644); err != nil {
return fmt.Errorf("failed to write main sources.list: %w", err)
}
// Add additional sources with optional apt-cacher-ng support
for i, source := range additionalSources {
// Convert additional sources to use apt-cacher-ng if available
if aptCacheURL != "" {
source = pm.convertToAptCacherNG(source, aptCacheURL)
}
sourceFile := filepath.Join(sourcesDir, fmt.Sprintf("additional-%d.list", i))
if err := os.WriteFile(sourceFile, []byte(source+"\n"), 0644); err != nil {
return fmt.Errorf("failed to write additional source %d: %w", i, err)
}
}
pm.logger.Info("Package sources configured successfully")
return nil
}
// detectAptCacherNG detects if apt-cacher-ng is available and returns the URL (optional)
func (pm *PackageManager) detectAptCacherNG() string {
// Check environment variables first (CI/CD friendly)
if envURL := os.Getenv("APT_CACHER_NG_URL"); envURL != "" {
return envURL
}
// Check common apt-cacher-ng URLs (optional enhancement)
commonURLs := []string{
"http://192.168.1.101:3142", // Your specific setup
"http://localhost:3142", // Local development
"http://apt-cacher-ng:3142", // Docker container
"http://192.168.1.100:3142", // Common local network
}
for _, url := range commonURLs {
if pm.isAptCacherNGAvailable(url) {
return url
}
}
return "" // No apt-cacher-ng found, which is perfectly fine
}
// isAptCacherNGAvailable checks if apt-cacher-ng is responding at the given URL (optional)
func (pm *PackageManager) isAptCacherNGAvailable(url string) bool {
// Simple HTTP check - we could make this more sophisticated
cmd := exec.Command("curl", "-s", "--connect-timeout", "5", "--max-time", "10", url)
if err := cmd.Run(); err != nil {
return false
}
return true
}
// convertToAptCacherNG converts a standard Debian mirror URL to use apt-cacher-ng (optional enhancement)
func (pm *PackageManager) convertToAptCacherNG(originalURL, cacheURL string) string {
// Handle different URL patterns
if strings.HasPrefix(originalURL, "https://") {
// Convert https://deb.debian.org/debian to http://cache:3142/HTTPS///deb.debian.org/debian
url := strings.TrimPrefix(originalURL, "https://")
return fmt.Sprintf("%s/HTTPS///%s", cacheURL, url)
} else if strings.HasPrefix(originalURL, "http://") {
// Convert http://mirror to http://cache:3142/mirror
url := strings.TrimPrefix(originalURL, "http://")
return fmt.Sprintf("%s/%s", cacheURL, url)
} else if strings.HasPrefix(originalURL, "deb ") {
// Handle deb lines
parts := strings.Fields(originalURL)
if len(parts) >= 2 {
url := parts[1]
if strings.HasPrefix(url, "https://") {
url = strings.TrimPrefix(url, "https://")
parts[1] = fmt.Sprintf("%s/HTTPS///%s", cacheURL, url)
} else if strings.HasPrefix(url, "http://") {
url = strings.TrimPrefix(url, "http://")
parts[1] = fmt.Sprintf("%s/%s", cacheURL, url)
}
return strings.Join(parts, " ")
}
}
// Return original if we can't parse it
return originalURL
}
// InstallDebootstrap installs debootstrap if not present
func (pm *PackageManager) InstallDebootstrap() error {
pm.logger.Info("Checking debootstrap installation")
// Check if debootstrap is already installed in the host system
if _, err := exec.LookPath("debootstrap"); err == nil {
pm.logger.Info("Debootstrap already available in host system")
return nil
}
// Also check common locations
commonPaths := []string{"/usr/sbin/debootstrap", "/usr/bin/debootstrap", "/bin/debootstrap"}
for _, path := range commonPaths {
if _, err := os.Stat(path); err == nil {
pm.logger.Infof("Debootstrap found at: %s", path)
return nil
}
}
pm.logger.Info("Debootstrap not found in host system")
pm.logger.Info("Please install debootstrap manually: sudo apt-get install debootstrap")
// For now, return an error to inform the user
return fmt.Errorf("debootstrap not available in host system - please install manually")
}
// CreateDebootstrap installs a base system using debootstrap
func (pm *PackageManager) CreateDebootstrap(suite, target, arch, variant string, components []string) error {
pm.logger.Infof("Creating debootstrap system: %s/%s (%s)", suite, arch, variant)
// Ensure debootstrap is installed
if err := pm.InstallDebootstrap(); err != nil {
return fmt.Errorf("debootstrap not available: %w", err)
}
// Find debootstrap path
debootstrapPath := "/usr/sbin/debootstrap"
if _, err := os.Stat(debootstrapPath); err != nil {
// Try alternative paths
altPaths := []string{"/usr/bin/debootstrap", "/bin/debootstrap"}
for _, path := range altPaths {
if _, err := os.Stat(path); err == nil {
debootstrapPath = path
break
}
}
}
// Prepare debootstrap command
args := []string{}
if variant != "" {
args = append(args, "--variant", variant)
}
if len(components) > 0 {
args = append(args, "--components", strings.Join(components, ","))
}
args = append(args, suite, target, "https://deb.debian.org/debian")
// Execute debootstrap with full path
cmd := exec.Command(debootstrapPath, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("debootstrap failed: %w", err)
}
pm.logger.Info("Debootstrap system created successfully")
return nil
}
// ConfigureLocale configures system locale
func (pm *PackageManager) ConfigureLocale(language, defaultLocale string, additionalLocales []string) error {
pm.logger.Infof("Configuring locale: %s (default: %s)", language, defaultLocale)
// Generate locales
locales := []string{language}
locales = append(locales, additionalLocales...)
// Create locale.gen content
localeGenContent := ""
for _, locale := range locales {
localeGenContent += locale + " UTF-8\n"
}
localeGenPath := filepath.Join(pm.rootfsDir, "etc/locale.gen")
if err := pm.writeFileWithSudo(localeGenPath, []byte(localeGenContent), 0644); err != nil {
return fmt.Errorf("failed to write locale.gen: %w", err)
}
// Generate locales in chroot with sudo
cmd := exec.Command("sudo", "/usr/sbin/chroot", pm.rootfsDir, "locale-gen")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("locale-gen failed: %w", err)
}
// Set default locale
defaultLocalePath := filepath.Join(pm.rootfsDir, "etc/default/locale")
defaultContent := fmt.Sprintf("LANG=%s\n", defaultLocale)
if err := pm.writeFileWithSudo(defaultLocalePath, []byte(defaultContent), 0644); err != nil {
return fmt.Errorf("failed to write default locale: %w", err)
}
pm.logger.Info("Locale configuration completed successfully")
return nil
}
// ConfigureTimezone configures system timezone
func (pm *PackageManager) ConfigureTimezone(timezone string) error {
pm.logger.Infof("Configuring timezone: %s", timezone)
// Create timezone file
timezonePath := filepath.Join(pm.rootfsDir, "etc/timezone")
if err := pm.writeFileWithSudo(timezonePath, []byte(timezone+"\n"), 0644); err != nil {
return fmt.Errorf("failed to write timezone: %w", err)
}
// Create localtime symlink
zoneInfoPath := filepath.Join(pm.rootfsDir, "usr/share/zoneinfo", timezone)
localtimePath := filepath.Join(pm.rootfsDir, "etc/localtime")
// Remove existing localtime if it exists
pm.removeFileWithSudo(localtimePath)
// Create symlink to zoneinfo
if err := pm.createSymlinkWithSudo(zoneInfoPath, localtimePath); err != nil {
return fmt.Errorf("failed to create localtime symlink: %w", err)
}
pm.logger.Info("Timezone configuration completed successfully")
return nil
}
// CreateUser creates a user account
func (pm *PackageManager) CreateUser(username, password, shell string, groups []string, uid, gid int, home, comment string) error {
pm.logger.Infof("Creating user: %s (UID: %d, GID: %d)", username, uid, gid)
// Create user using chroot with sudo
useraddArgs := []string{"useradd", "--create-home", "--shell", shell, "--uid", fmt.Sprintf("%d", uid), "--gid", fmt.Sprintf("%d", gid), "--comment", comment, username}
chrootArgs := append([]string{"/usr/sbin/chroot", pm.rootfsDir}, useraddArgs...)
cmd := exec.Command("sudo", chrootArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("useradd failed: %w", err)
}
// Add user to groups
if len(groups) > 0 {
usermodArgs := []string{"usermod", "--append", "--groups", strings.Join(groups, ","), username}
chrootArgs := append([]string{"/usr/sbin/chroot", pm.rootfsDir}, usermodArgs...)
cmd := exec.Command("sudo", chrootArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("usermod failed: %w", err)
}
}
// Set password if provided
if password != "" {
chpasswdCmd := fmt.Sprintf("echo '%s:%s' | chpasswd", username, password)
cmd := exec.Command("sudo", "/usr/sbin/chroot", pm.rootfsDir, "bash", "-c", chpasswdCmd)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("password setting failed: %w", err)
}
}
pm.logger.Infof("User %s created successfully", username)
return nil
}
// InstallEssentialPackages installs essential system packages
func (pm *PackageManager) InstallEssentialPackages() error {
pm.logger.Info("Installing essential system packages")
essentialPackages := []string{
"systemd",
"systemd-sysv",
"systemd-resolved",
"dbus",
"udev",
"init",
"bash",
"coreutils",
"util-linux",
"procps",
"grep",
"sed",
"gawk",
"tar",
"gzip",
"bzip2",
"xz-utils",
"ca-certificates",
"wget",
"curl",
}
return pm.InstallPackages(essentialPackages, false, false)
}