cleanup
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
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
This commit is contained in:
parent
d782a8a4fb
commit
126ee1a849
76 changed files with 1683 additions and 470 deletions
490
bib/internal/builder/package_manager.go
Normal file
490
bib/internal/builder/package_manager.go
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue