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) }