254 lines
6.9 KiB
Go
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)
|
|
}
|