deb-bootc-image-builder-new/bib/internal/apt/apt.go
2025-09-05 07:10:12 -07:00

254 lines
6.9 KiB
Go

package apt
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/sirupsen/logrus"
)
// PackageSpec represents a Debian package specification
type PackageSpec struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Arch string `json:"arch,omitempty"`
}
// RepoConfig represents an APT repository configuration
type RepoConfig struct {
BaseURL string `json:"baseurl"`
Components []string `json:"components,omitempty"`
Arch string `json:"arch,omitempty"`
Priority int `json:"priority,omitempty"`
}
// DepsolveResult contains the result of package dependency resolution
type DepsolveResult struct {
Packages []PackageSpec `json:"packages"`
Repos []RepoConfig `json:"repos"`
}
// Solver handles APT-based package dependency resolution
type Solver struct {
cacheRoot string
arch string
repos []RepoConfig
}
// NewSolver creates a new APT solver instance
func NewSolver(cacheRoot, arch string, repos []RepoConfig) (*Solver, error) {
// Ensure cache directory exists
if err := os.MkdirAll(cacheRoot, 0755); err != nil {
return nil, fmt.Errorf("cannot create cache directory %s: %w", cacheRoot, err)
}
return &Solver{
cacheRoot: cacheRoot,
arch: arch,
repos: repos,
}, nil
}
// Depsolve resolves package dependencies using APT
func (s *Solver) Depsolve(packages []string, seed int) (*DepsolveResult, error) {
logrus.Debugf("Depsolving packages: %v", packages)
// Try real APT first, fall back to mock if not available
result, err := s.realDepsolve(packages)
if err != nil {
logrus.Warnf("Real APT depsolve failed, using mock: %v", err)
return s.mockDepsolve(packages)
}
// If real APT succeeds, return its result
return result, nil
}
// realDepsolve uses actual APT commands to resolve dependencies
func (s *Solver) realDepsolve(packages []string) (*DepsolveResult, error) {
logrus.Debugf("Using real APT depsolve for packages: %v", packages)
// Create temporary APT configuration
aptConf, err := s.createAptConf()
if err != nil {
return nil, fmt.Errorf("cannot create APT configuration: %w", err)
}
// Keep the file for debugging
// defer os.Remove(aptConf)
// Use apt-cache to resolve dependencies
cmd := exec.Command("apt-cache", "depends", "--no-recommends", "--no-suggests")
cmd.Env = append(os.Environ(), fmt.Sprintf("APT_CONFIG=%s", aptConf))
cmd.Args = append(cmd.Args, packages...)
logrus.Debugf("Running command: %s %v", cmd.Path, cmd.Args)
logrus.Debugf("APT_CONFIG=%s", aptConf)
output, err := cmd.Output()
if err != nil {
logrus.Debugf("apt-cache command failed: %v", err)
return nil, fmt.Errorf("apt-cache depends failed: %w", err)
}
// Parse the output to extract package names
resolvedPackages := s.parseAptOutput(string(output))
// Get package versions
packageSpecs := make([]PackageSpec, 0, len(resolvedPackages))
for _, pkg := range resolvedPackages {
version, err := s.getPackageVersion(pkg, aptConf)
if err != nil {
logrus.Warnf("Cannot get version for package %s: %v", pkg, err)
version = ""
}
packageSpecs = append(packageSpecs, PackageSpec{
Name: pkg,
Version: version,
Arch: s.arch,
})
}
return &DepsolveResult{
Packages: packageSpecs,
Repos: s.repos,
}, nil
}
// mockDepsolve provides a mock implementation for testing
func (s *Solver) mockDepsolve(packages []string) (*DepsolveResult, error) {
logrus.Debugf("Using mock APT depsolve for packages: %v", packages)
// Create mock resolved packages
packageSpecs := make([]PackageSpec, 0, len(packages))
for _, pkg := range packages {
packageSpecs = append(packageSpecs, PackageSpec{
Name: pkg,
Version: "1.0.0",
Arch: s.arch,
})
}
return &DepsolveResult{
Packages: packageSpecs,
Repos: s.repos,
}, nil
}
// createAptConf creates a temporary APT configuration file
func (s *Solver) createAptConf() (string, error) {
confFile := filepath.Join(s.cacheRoot, "apt.conf")
// APT configuration directives
conf := "APT::Architecture \"" + s.arch + "\";\n"
conf += "APT::Get::Assume-Yes \"true\";\n"
conf += "APT::Get::AllowUnauthenticated \"true\";\n"
conf += "APT::Cache::Generate \"true\";\n"
// Create sources.list file
sourcesFile := filepath.Join(s.cacheRoot, "sources.list")
sources := ""
for _, repo := range s.repos {
sources += fmt.Sprintf("deb [arch=%s] %s %s\n", s.arch, repo.BaseURL, strings.Join(repo.Components, " "))
}
// Add sources.list to APT configuration
conf += fmt.Sprintf("Dir::Etc::SourceList \"%s\";\n", sourcesFile)
// Write sources.list
if err := os.WriteFile(sourcesFile, []byte(sources), 0644); err != nil {
return "", fmt.Errorf("cannot write sources.list: %w", err)
}
// Write APT configuration
if err := os.WriteFile(confFile, []byte(conf), 0644); err != nil {
return "", fmt.Errorf("cannot write APT configuration: %w", err)
}
return confFile, nil
}
// parseAptOutput parses the output from apt-cache depends
func (s *Solver) parseAptOutput(output string) []string {
packages := make(map[string]bool)
lines := strings.Split(output, "\n")
// Skip dependency relationship keywords
skipKeywords := map[string]bool{
"Depends:": true,
"PreDepends:": true,
"Breaks:": true,
"Replaces:": true,
"Conflicts:": true,
"Recommends:": true,
"Suggests:": true,
"Enhances:": true,
"|Depends": true,
"|PreDepends": true,
"Enhances": true,
}
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Skip dependency relationship keywords
skip := false
for keyword := range skipKeywords {
if strings.HasPrefix(line, keyword) {
skip = true
break
}
}
if skip {
continue
}
// Extract package name (remove version constraints and indentation)
if strings.Contains(line, " ") {
parts := strings.Fields(line)
if len(parts) > 0 {
pkg := strings.TrimSuffix(parts[0], ":")
// Only add if it looks like a real package name
if !strings.Contains(pkg, ":") && !strings.Contains(pkg, "<") && !strings.Contains(pkg, ">") && !strings.Contains(pkg, "|") {
packages[pkg] = true
}
}
} else if line != "" && !strings.Contains(line, ":") && !strings.Contains(line, "<") && !strings.Contains(line, ">") && !strings.Contains(line, "|") {
// Single word that's not a keyword
packages[line] = true
}
}
result := make([]string, 0, len(packages))
for pkg := range packages {
result = append(result, pkg)
}
return result
}
// getPackageVersion gets the version of a specific package
func (s *Solver) getPackageVersion(pkg, aptConf string) (string, error) {
cmd := exec.Command("apt-cache", "show", pkg)
cmd.Env = append(os.Environ(), fmt.Sprintf("APT_CONFIG=%s", aptConf))
output, err := cmd.Output()
if err != nil {
return "", err
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "Version:") {
return strings.TrimSpace(strings.TrimPrefix(line, "Version:")), nil
}
}
return "", fmt.Errorf("version not found for package %s", pkg)
}