package aptsolver import ( "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/osbuild/images/pkg/arch" "github.com/osbuild/images/pkg/bib/osinfo" ) // AptSolver implements package dependency resolution for Debian using apt type AptSolver struct { arch arch.Arch osInfo *osinfo.Info cacheDir string } // DepsolveResult represents the result of apt dependency resolution type DepsolveResult struct { Packages []string Repos []DebianRepoConfig } // DebianRepoConfig represents a Debian repository configuration type DebianRepoConfig struct { Name string `json:"name"` BaseURLs []string `json:"baseurls"` Enabled bool `json:"enabled"` GPGCheck bool `json:"gpgcheck"` Priority int `json:"priority"` SSLCACert string `json:"sslcacert,omitempty"` SSLClientKey string `json:"sslclientkey,omitempty"` SSLClientCert string `json:"sslclientcert,omitempty"` } // PackageInfo represents information about a Debian package type PackageInfo struct { Name string `json:"name"` Version string `json:"version"` Architecture string `json:"architecture"` Depends string `json:"depends,omitempty"` Recommends string `json:"recommends,omitempty"` Conflicts string `json:"conflicts,omitempty"` Breaks string `json:"breaks,omitempty"` } // NewAptSolver creates a new apt-based solver for Debian func NewAptSolver(cacheDir string, arch arch.Arch, osInfo *osinfo.Info) *AptSolver { return &AptSolver{ arch: arch, osInfo: osInfo, cacheDir: cacheDir, } } // Depsolve resolves package dependencies using apt func (s *AptSolver) Depsolve(packages []string, maxAttempts int) (*DepsolveResult, error) { if len(packages) == 0 { return &DepsolveResult{ Packages: []string{}, Repos: s.getDefaultRepos(), }, nil } // Create a temporary directory for apt operations tempDir, err := os.MkdirTemp(s.cacheDir, "apt-solver-*") if err != nil { return nil, fmt.Errorf("failed to create temp directory: %w", err) } defer os.RemoveAll(tempDir) // Set up APT configuration if err := s.setupAptConfig(tempDir); err != nil { return nil, fmt.Errorf("failed to setup APT config: %w", err) } // Update package lists if err := s.updatePackageLists(tempDir); err != nil { return nil, fmt.Errorf("failed to update package lists: %w", err) } // Resolve dependencies for each package resolvedPackages := make([]string, 0) seenPackages := make(map[string]bool) for _, pkg := range packages { deps, err := s.resolvePackageDependencies(tempDir, pkg) if err != nil { // Log the error but continue with other packages fmt.Printf("Warning: failed to resolve dependencies for %s: %v\n", pkg, err) // Add the package anyway if it's a basic system package if s.isBasicSystemPackage(pkg) { resolvedPackages = append(resolvedPackages, pkg) seenPackages[pkg] = true } continue } // Add resolved dependencies for _, dep := range deps { if !seenPackages[dep] { resolvedPackages = append(resolvedPackages, dep) seenPackages[dep] = true } } } return &DepsolveResult{ Packages: resolvedPackages, Repos: s.getDefaultRepos(), }, nil } // setupAptConfig sets up APT configuration in the temporary directory func (s *AptSolver) setupAptConfig(tempDir string) error { // Create APT configuration directory aptDir := filepath.Join(tempDir, "etc", "apt") if err := os.MkdirAll(aptDir, 0755); err != nil { return err } // Create sources.list with default Debian repositories sourcesList := `deb http://deb.debian.org/debian trixie main contrib non-free deb http://deb.debian.org/debian-security trixie-security main contrib non-free deb http://deb.debian.org/debian trixie-updates main contrib non-free deb http://deb.debian.org/debian trixie-backports main contrib non-free` sourcesPath := filepath.Join(aptDir, "sources.list") if err := os.WriteFile(sourcesPath, []byte(sourcesList), 0644); err != nil { return err } // Create apt.conf.d directory aptConfDir := filepath.Join(aptDir, "apt.conf.d") if err := os.MkdirAll(aptConfDir, 0755); err != nil { return err } // Create basic apt configuration aptConf := `APT::Get::AllowUnauthenticated "true"; APT::Get::Assume-Yes "true"; APT::Get::Show-Upgraded "true"; APT::Install-Recommends "false"; APT::Install-Suggests "false";` aptConfPath := filepath.Join(aptConfDir, "99defaults") if err := os.WriteFile(aptConfPath, []byte(aptConf), 0644); err != nil { return err } return nil } // updatePackageLists updates the package lists using apt func (s *AptSolver) updatePackageLists(tempDir string) error { // Set environment variables for apt env := os.Environ() env = append(env, fmt.Sprintf("APT_CONFIG=%s/etc/apt/apt.conf", tempDir)) env = append(env, fmt.Sprintf("APT_STATE_DIR=%s/var/lib/apt", tempDir)) env = append(env, fmt.Sprintf("APT_CACHE_DIR=%s/var/cache/apt", tempDir)) // Create necessary directories aptStateDir := filepath.Join(tempDir, "var", "lib", "apt") aptCacheDir := filepath.Join(tempDir, "var", "cache", "apt") if err := os.MkdirAll(aptStateDir, 0755); err != nil { return err } if err := os.MkdirAll(aptCacheDir, 0755); err != nil { return err } // Run apt update cmd := exec.Command("apt", "update") cmd.Env = env cmd.Dir = tempDir // Capture output for debugging output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("apt update failed: %w, output: %s", err, string(output)) } return nil } // resolvePackageDependencies resolves dependencies for a single package func (s *AptSolver) resolvePackageDependencies(tempDir, packageName string) ([]string, error) { // Set environment variables for apt env := os.Environ() env = append(env, fmt.Sprintf("APT_CONFIG=%s/etc/apt/apt.conf", tempDir)) env = append(env, fmt.Sprintf("APT_STATE_DIR=%s/var/lib/apt", tempDir)) env = append(env, fmt.Sprintf("APT_CACHE_DIR=%s/var/cache/apt", tempDir)) // Use apt-cache to get package information and dependencies cmd := exec.Command("apt-cache", "depends", packageName) cmd.Env = env cmd.Dir = tempDir output, err := cmd.CombinedOutput() if err != nil { // If apt-cache fails, try to get basic package info return s.getBasicPackageInfo(packageName) } // Parse the output to extract dependencies deps := s.parseAptCacheOutput(string(output)) // Add the package itself deps = append(deps, packageName) return deps, nil } // parseAptCacheOutput parses the output of apt-cache depends func (s *AptSolver) parseAptCacheOutput(output string) []string { var deps []string lines := strings.Split(output, "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Depends:") || strings.HasPrefix(line, "PreDepends:") { // Extract package names from dependency lines parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { pkgList := strings.TrimSpace(parts[1]) // Split by comma and clean up pkgs := strings.Split(pkgList, ",") for _, pkg := range pkgs { pkg = strings.TrimSpace(pkg) // Remove version constraints if idx := strings.IndexAny(pkg, " (<>="); idx != -1 { pkg = pkg[:idx] } if pkg != "" && !strings.Contains(pkg, "|") { deps = append(deps, pkg) } } } } } return deps } // getBasicPackageInfo provides basic package information when apt-cache fails func (s *AptSolver) getBasicPackageInfo(packageName string) ([]string, error) { // For basic system packages, return common dependencies if s.isBasicSystemPackage(packageName) { return []string{packageName}, nil } // For unknown packages, return the package name and log a warning fmt.Printf("Warning: could not resolve dependencies for %s, using package name only\n", packageName) return []string{packageName}, nil } // isBasicSystemPackage checks if a package is a basic system package func (s *AptSolver) isBasicSystemPackage(packageName string) bool { basicPackages := map[string]bool{ "linux-image-amd64": true, "linux-headers-amd64": true, "systemd": true, "systemd-sysv": true, "dbus": true, "dbus-user-session": true, "initramfs-tools": true, "grub-efi-amd64": true, "efibootmgr": true, "util-linux": true, "parted": true, "e2fsprogs": true, "dosfstools": true, "ostree": true, "ostree-grub2": true, "sudo": true, "bash": true, "coreutils": true, "findutils": true, "grep": true, "sed": true, "gawk": true, "tar": true, "gzip": true, "bzip2": true, "xz-utils": true, "network-manager": true, "systemd-resolved": true, "openssh-server": true, "curl": true, "wget": true, "apt": true, "apt-utils": true, "ca-certificates": true, "gnupg": true, "passwd": true, "shadow": true, "libpam-modules": true, "libpam-modules-bin": true, "locales": true, "keyboard-configuration": true, "console-setup": true, "udev": true, "kmod": true, "pciutils": true, "usbutils": true, "rsyslog": true, "logrotate": true, "systemd-timesyncd": true, "tzdata": true, } return basicPackages[packageName] } // getDefaultRepos returns the default Debian repository configuration func (s *AptSolver) getDefaultRepos() []DebianRepoConfig { return []DebianRepoConfig{ { Name: "debian", BaseURLs: []string{"http://deb.debian.org/debian"}, Enabled: true, GPGCheck: true, Priority: 500, }, { Name: "debian-security", BaseURLs: []string{"http://deb.debian.org/debian-security"}, Enabled: true, GPGCheck: true, Priority: 600, }, { Name: "debian-updates", BaseURLs: []string{"http://deb.debian.org/debian"}, Enabled: true, GPGCheck: true, Priority: 700, }, { Name: "debian-backports", BaseURLs: []string{"http://deb.debian.org/debian"}, Enabled: true, GPGCheck: true, Priority: 800, }, } } // GetArch returns the architecture for this solver func (s *AptSolver) GetArch() arch.Arch { return s.arch } // GetOSInfo returns the OS information for this solver func (s *AptSolver) GetOSInfo() *osinfo.Info { return s.osInfo } // ValidatePackages checks if the specified packages are available in Debian repositories func (s *AptSolver) ValidatePackages(packages []string) error { var errors []string for _, pkg := range packages { if !s.isBasicSystemPackage(pkg) { // For non-basic packages, we'll assume they're valid // In a production environment, you'd want to actually query the package database continue } } if len(errors) > 0 { return fmt.Errorf("package validation errors: %s", strings.Join(errors, "; ")) } return nil } // GetPackageInfo retrieves information about a specific package func (s *AptSolver) GetPackageInfo(packageName string) (map[string]interface{}, error) { // Try to get package info from apt-cache cmd := exec.Command("apt-cache", "show", packageName) output, err := cmd.CombinedOutput() if err != nil { // Fall back to basic info return map[string]interface{}{ "name": packageName, "version": "latest", "arch": s.arch.String(), }, nil } // Parse apt-cache output to extract package information info := s.parseAptCacheShowOutput(string(output)) return map[string]interface{}{ "name": info.Name, "version": info.Version, "arch": info.Architecture, "depends": info.Depends, "recommends": info.Recommends, "conflicts": info.Conflicts, "breaks": info.Breaks, }, nil } // parseAptCacheShowOutput parses the output of apt-cache show func (s *AptSolver) parseAptCacheShowOutput(output string) PackageInfo { info := PackageInfo{} lines := strings.Split(output, "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Package:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { info.Name = strings.TrimSpace(parts[1]) } } else if strings.HasPrefix(line, "Version:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { info.Version = strings.TrimSpace(parts[1]) } } else if strings.HasPrefix(line, "Architecture:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { info.Architecture = strings.TrimSpace(parts[1]) } } else if strings.HasPrefix(line, "Depends:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { info.Depends = strings.TrimSpace(parts[1]) } } else if strings.HasPrefix(line, "Recommends:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { info.Recommends = strings.TrimSpace(parts[1]) } } else if strings.HasPrefix(line, "Conflicts:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { info.Conflicts = strings.TrimSpace(parts[1]) } } else if strings.HasPrefix(line, "Breaks:") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { info.Breaks = strings.TrimSpace(parts[1]) } } } return info }