555 lines
16 KiB
Go
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
|
|
}
|