deb-bootc-compose/internal/pkg/manager.go
2025-08-18 23:32:51 -07:00

555 lines
16 KiB
Go

package pkg
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/sirupsen/logrus"
)
// Manager handles package operations for deb-bootc-compose
type Manager struct {
logger *logrus.Logger
cacheDir string
workDir string
repos []Repository
arch string
dist string
}
// Repository represents a Debian repository
type Repository struct {
URL string
Suite string
Component string
Arch string
}
// Package represents a Debian package
type Package struct {
Name string
Version string
Architecture string
Depends []string
Recommends []string
Source string
Size int64
Priority string
Section string
}
// NewManager creates a new package manager
func NewManager(cacheDir, workDir, arch, dist string) *Manager {
return &Manager{
logger: logrus.New(),
cacheDir: cacheDir,
workDir: workDir,
arch: arch,
dist: dist,
repos: []Repository{
{
URL: "http://deb.debian.org/debian",
Suite: dist,
Component: "main",
Arch: arch,
},
{
URL: "http://deb.debian.org/debian",
Suite: dist + "-updates",
Component: "main",
Arch: arch,
},
{
URL: "http://security.debian.org/debian-security",
Suite: dist + "-security",
Component: "main",
Arch: arch,
},
},
}
}
// UpdatePackageLists updates the package lists from repositories
func (m *Manager) UpdatePackageLists() error {
m.logger.Info("Updating package lists...")
for _, repo := range m.repos {
if err := m.updateRepoPackageList(repo); err != nil {
m.logger.Warnf("Failed to update package list for %s: %v", repo.URL, err)
continue
}
}
return nil
}
// updateRepoPackageList updates package list for a specific repository
func (m *Manager) updateRepoPackageList(repo Repository) error {
// Create repository directory
repoDir := filepath.Join(m.cacheDir, "repos", repo.Suite, repo.Component, repo.Arch)
if err := os.MkdirAll(repoDir, 0755); err != nil {
return fmt.Errorf("failed to create repo directory: %w", err)
}
// Download Packages.gz
packagesURL := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages.gz",
repo.URL, repo.Suite, repo.Component, repo.Arch)
packagesFile := filepath.Join(repoDir, "Packages.gz")
m.logger.Infof("Downloading package list from %s", packagesURL)
// Use curl to download (simple approach)
cmd := exec.Command("curl", "-L", "-o", packagesFile, packagesURL)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to download package list: %w", err)
}
// Extract Packages.gz
cmd = exec.Command("gunzip", "-f", packagesFile)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to extract package list: %w", err)
}
m.logger.Infof("Updated package list for %s/%s/%s", repo.Suite, repo.Component, repo.Arch)
return nil
}
// GetPackageList returns the package list for a variant
func (m *Manager) GetPackageList(variantName string) ([]Package, error) {
m.logger.Infof("Getting package list for variant: %s", variantName)
// For now, return a curated list of essential packages
// In the future, this will parse the treefile and resolve dependencies
packages := []Package{
{
Name: "systemd",
Version: "252.19-1",
Architecture: m.arch,
Depends: []string{"libc6", "libcap2"},
Recommends: []string{"dbus"},
Source: "systemd",
Size: 0,
Priority: "important",
Section: "admin",
},
{
Name: "udev",
Version: "252.19-1",
Architecture: m.arch,
Depends: []string{"libc6", "libcap2"},
Recommends: []string{},
Source: "systemd",
Size: 0,
Priority: "important",
Section: "admin",
},
{
Name: "dbus",
Version: "1.14.10-1",
Architecture: m.arch,
Depends: []string{"libc6"},
Recommends: []string{},
Source: "dbus",
Size: 0,
Priority: "important",
Section: "admin",
},
{
Name: "libc6",
Version: "1.0-1",
Architecture: m.arch,
Depends: []string{},
Recommends: []string{},
Source: "glibc",
Size: 0,
Priority: "required",
Section: "libs",
},
{
Name: "libcap2",
Version: "1.0-1",
Architecture: m.arch,
Depends: []string{},
Recommends: []string{},
Source: "libcap2",
Size: 0,
Priority: "optional",
Section: "libs",
},
}
return packages, nil
}
// ResolveDependencies resolves package dependencies recursively
func (m *Manager) ResolveDependencies(packages []Package) ([]Package, error) {
m.logger.Infof("Resolving package dependencies...")
// Create a map to track resolved packages
resolved := make(map[string]Package)
// Add initial packages
for _, pkg := range packages {
resolved[pkg.Name] = pkg
}
// Resolve dependencies recursively
for _, pkg := range packages {
if err := m.resolvePackageDeps(pkg, resolved); err != nil {
m.logger.Warnf("Failed to resolve dependencies for %s: %v", pkg.Name, err)
}
}
// Convert map back to slice
var result []Package
for _, pkg := range resolved {
result = append(result, pkg)
}
m.logger.Infof("Resolved %d packages with dependencies", len(result))
return result, nil
}
// resolvePackageDeps recursively resolves dependencies for a package
func (m *Manager) resolvePackageDeps(pkg Package, resolved map[string]Package) error {
// Check direct dependencies
for _, depName := range pkg.Depends {
if _, exists := resolved[depName]; !exists {
// Try to get real package info from APT
if depPkg, err := m.getPackageInfoFromAPT(depName); err == nil {
resolved[depName] = depPkg
// Recursively resolve this dependency's dependencies
if err := m.resolvePackageDeps(depPkg, resolved); err != nil {
m.logger.Warnf("Failed to resolve dependencies for %s: %v", depName, err)
}
} else {
// Create a placeholder dependency package if APT lookup fails
depPkg := Package{
Name: depName,
Version: "1.0-1", // Placeholder version
Architecture: m.arch,
Depends: []string{},
Recommends: []string{},
Source: depName,
Size: 0,
Priority: "optional",
Section: "libs",
}
resolved[depName] = depPkg
m.logger.Warnf("Using placeholder for dependency %s: %v", depName, err)
}
}
}
// Check recommended packages
for _, recName := range pkg.Recommends {
if _, exists := resolved[recName]; !exists {
// Try to get real package info from APT
if recPkg, err := m.getPackageInfoFromAPT(recName); err == nil {
resolved[recName] = recPkg
// Recursively resolve this recommended package's dependencies
if err := m.resolvePackageDeps(recPkg, resolved); err != nil {
m.logger.Warnf("Failed to resolve dependencies for %s: %v", recName, err)
}
} else {
// Create a placeholder recommended package if APT lookup fails
recPkg := Package{
Name: recName,
Version: "1.0-1", // Placeholder version
Architecture: m.arch,
Depends: []string{},
Recommends: []string{},
Source: recName,
Size: 0,
Priority: "optional",
Section: "libs",
}
resolved[recName] = recPkg
m.logger.Warnf("Using placeholder for recommended package %s: %v", recName, err)
}
}
}
return nil
}
// getPackageInfoFromAPT retrieves package information from APT cache
func (m *Manager) getPackageInfoFromAPT(pkgName string) (Package, error) {
// Use apt-cache to get package information
cmd := exec.Command("apt-cache", "show", pkgName)
output, err := cmd.Output()
if err != nil {
return Package{}, fmt.Errorf("apt-cache show failed: %w", err)
}
// Parse the output
return m.parseAPTShowOutput(string(output))
}
// parseAPTShowOutput parses the output of apt-cache show
func (m *Manager) parseAPTShowOutput(output string) (Package, error) {
pkg := Package{}
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.HasPrefix(line, "Package: ") {
pkg.Name = strings.TrimPrefix(line, "Package: ")
} else if strings.HasPrefix(line, "Version: ") {
pkg.Version = strings.TrimPrefix(line, "Version: ")
} else if strings.HasPrefix(line, "Architecture: ") {
pkg.Architecture = strings.TrimPrefix(line, "Architecture: ")
} else if strings.HasPrefix(line, "Depends: ") {
deps := strings.TrimPrefix(line, "Depends: ")
pkg.Depends = m.parseDependencyList(deps)
} else if strings.HasPrefix(line, "Recommends: ") {
recs := strings.TrimPrefix(line, "Recommends: ")
pkg.Recommends = m.parseDependencyList(recs)
} else if strings.HasPrefix(line, "Source: ") {
pkg.Source = strings.TrimPrefix(line, "Source: ")
} else if strings.HasPrefix(line, "Priority: ") {
pkg.Priority = strings.TrimPrefix(line, "Priority: ")
} else if strings.HasPrefix(line, "Section: ") {
pkg.Section = strings.TrimPrefix(line, "Section: ")
}
}
// Set default values if not found
if pkg.Architecture == "" {
pkg.Architecture = m.arch
}
if pkg.Source == "" {
pkg.Source = pkg.Name
}
if pkg.Priority == "" {
pkg.Priority = "optional"
}
if pkg.Section == "" {
pkg.Section = "libs"
}
return pkg, nil
}
// parseDependencyList parses a comma-separated dependency list
func (m *Manager) parseDependencyList(deps string) []string {
if deps == "" {
return []string{}
}
// Split by comma and clean up each dependency
var result []string
for _, dep := range strings.Split(deps, ",") {
dep = strings.TrimSpace(dep)
// Remove version constraints (e.g., "libc6 (>= 2.17)" -> "libc6")
if idx := strings.Index(dep, " ("); idx > 0 {
dep = dep[:idx]
}
if idx := strings.Index(dep, " |"); idx > 0 {
dep = dep[:idx]
}
if dep != "" {
result = append(result, dep)
}
}
return result
}
// DownloadPackage downloads a specific package
func (m *Manager) DownloadPackage(pkg Package) error {
m.logger.Infof("Downloading package: %s %s", pkg.Name, pkg.Version)
// Create package cache directory
pkgDir := filepath.Join(m.cacheDir, "packages", pkg.Architecture)
if err := os.MkdirAll(pkgDir, 0755); err != nil {
return fmt.Errorf("failed to create package directory: %w", err)
}
// Use apt-get to download the actual package
// This will resolve dependencies and download the real .deb file
cmd := exec.Command("apt-get", "download",
fmt.Sprintf("%s=%s", pkg.Name, pkg.Version),
"-o", "Acquire::Check-Valid-Until=false",
"-o", "APT::Get::AllowUnauthenticated=true")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
// Fallback: try without version constraint
m.logger.Warnf("Failed to download specific version %s=%s, trying latest", pkg.Name, pkg.Version)
cmd = exec.Command("apt-get", "download", pkg.Name,
"-o", "Acquire::Check-Valid-Until=false",
"-o", "APT::Get::AllowUnauthenticated=true")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to download package %s: %w", pkg.Name, err)
}
}
// Find the downloaded .deb file in the current directory
entries, err := os.ReadDir(".")
if err != nil {
return fmt.Errorf("failed to read current directory: %w", err)
}
var debFile string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".deb") && strings.Contains(entry.Name(), pkg.Name) {
debFile = entry.Name()
break
}
}
if debFile == "" {
return fmt.Errorf("downloaded .deb file not found for %s", pkg.Name)
}
// Move the downloaded file to our cache directory
targetFile := filepath.Join(pkgDir, debFile)
if err := os.Rename(debFile, targetFile); err != nil {
return fmt.Errorf("failed to move downloaded file: %w", err)
}
m.logger.Infof("Downloaded package to: %s", targetFile)
return nil
}
// InstallPackages installs packages to a target directory
func (m *Manager) InstallPackages(packages []Package, targetDir string) error {
m.logger.Infof("Installing %d packages to %s", len(packages), targetDir)
// Create target directory
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create target directory: %w", err)
}
// Find all .deb files in the package cache
pkgDir := filepath.Join(m.cacheDir, "packages", m.arch)
entries, err := os.ReadDir(pkgDir)
if err != nil {
return fmt.Errorf("failed to read package directory: %w", err)
}
var debFiles []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".deb") {
debFiles = append(debFiles, filepath.Join(pkgDir, entry.Name()))
}
}
if len(debFiles) == 0 {
return fmt.Errorf("no .deb files found in %s", pkgDir)
}
m.logger.Infof("Found %d .deb files to install", len(debFiles))
// Extract packages using a different approach to avoid DEBIAN conflicts
for _, debFile := range entries {
if !debFile.IsDir() && strings.HasSuffix(debFile.Name(), ".deb") {
debPath := filepath.Join(pkgDir, debFile.Name())
m.logger.Infof("Installing %s", debFile.Name())
// Extract files to target directory
cmd := exec.Command("dpkg-deb", "-x", debPath, targetDir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
m.logger.Warnf("Failed to extract files from %s: %v", debFile.Name(), err)
continue
}
// Extract control data to a package-specific location
controlDir := filepath.Join(targetDir, "var", "lib", "dpkg", "info", strings.TrimSuffix(debFile.Name(), ".deb"))
if err := os.MkdirAll(controlDir, 0755); err != nil {
m.logger.Warnf("Failed to create control directory for %s: %v", debFile.Name(), err)
continue
}
cmd = exec.Command("dpkg-deb", "-e", debPath, controlDir)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
m.logger.Warnf("Failed to extract control data from %s: %v", debFile.Name(), err)
continue
}
}
}
// Create basic filesystem structure if it doesn't exist
dirs := []string{
filepath.Join(targetDir, "etc"),
filepath.Join(targetDir, "var"),
filepath.Join(targetDir, "tmp"),
filepath.Join(targetDir, "proc"),
filepath.Join(targetDir, "sys"),
filepath.Join(targetDir, "dev"),
filepath.Join(targetDir, "var", "lib"),
filepath.Join(targetDir, "var", "lib", "dpkg"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
m.logger.Warnf("Failed to create directory %s: %v", dir, err)
}
}
// Create a basic dpkg status file
statusFile := filepath.Join(targetDir, "var", "lib", "dpkg", "status")
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(statusFile), 0755); err != nil {
m.logger.Warnf("Failed to create dpkg directory: %v", err)
}
// Remove existing status directory if it exists
if stat, err := os.Stat(statusFile); err == nil && stat.IsDir() {
if err := os.RemoveAll(statusFile); err != nil {
m.logger.Warnf("Failed to remove existing status directory: %v", err)
}
}
statusContent := "Package: deb-bootc-compose\nStatus: install ok installed\nPriority: optional\nSection: admin\nInstalled-Size: 0\n\n"
if err := os.WriteFile(statusFile, []byte(statusContent), 0644); err != nil {
m.logger.Warnf("Failed to create status file: %v", err)
}
m.logger.Infof("Installed packages to: %s", targetDir)
return nil
}
// GetPackageInfo returns detailed information about a package
func (m *Manager) GetPackageInfo(pkgName string) (*Package, error) {
// This would query the actual package database
// For now, return a placeholder
return &Package{
Name: pkgName,
Version: "1.0-1",
Architecture: m.arch,
Depends: []string{},
Priority: "optional",
Section: "misc",
}, nil
}
// Cleanup removes temporary files
func (m *Manager) Cleanup() error {
m.logger.Info("Cleaning up package manager...")
// Remove temporary files
tempDir := filepath.Join(m.cacheDir, "temp")
if err := os.RemoveAll(tempDir); err != nil {
m.logger.Warnf("Failed to remove temp directory: %v", err)
}
return nil
}