package distros import ( "context" "fmt" "os/exec" "runtime" "strings" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" ) var GentooGlobalUseFlags = []string{ "dbus", "udev", "alsa", "policykit", "jpeg", "png", "webp", "gif", "tiff", "svg", "brotli", "gdbm", "accessibility", "gtk", "qt6", "egl", "gbm", } func init() { Register("gentoo", "#54487A", FamilyGentoo, func(config DistroConfig, logChan chan<- string) Distribution { return NewGentooDistribution(config, logChan) }) } type GentooDistribution struct { *BaseDistribution *ManualPackageInstaller config DistroConfig skipGlobalUseFlags bool } func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDistribution { base := NewBaseDistribution(logChan) return &GentooDistribution{ BaseDistribution: base, ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base}, config: config, } } func (g *GentooDistribution) getArchKeyword() string { arch := runtime.GOARCH switch arch { case "amd64": return "~amd64" case "arm64": return "~arm64" default: return "~amd64" } } func (g *GentooDistribution) GetID() string { return g.config.ID } func (g *GentooDistribution) GetColorHex() string { return g.config.ColorHex } func (g *GentooDistribution) GetFamily() DistroFamily { return g.config.Family } func (g *GentooDistribution) GetPackageManager() PackageManagerType { return PackageManagerPortage } func (g *GentooDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { return g.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) } func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) { var dependencies []deps.Dependency dependencies = append(dependencies, g.detectDMS()) dependencies = append(dependencies, g.detectSpecificTerminal(terminal)) dependencies = append(dependencies, g.detectGit()) dependencies = append(dependencies, g.detectWindowManager(wm)) dependencies = append(dependencies, g.detectQuickshell()) dependencies = append(dependencies, g.detectXDGPortal()) dependencies = append(dependencies, g.detectPolkitAgent()) dependencies = append(dependencies, g.detectAccountsService()) if wm == deps.WindowManagerHyprland { dependencies = append(dependencies, g.detectHyprlandTools()...) } if wm == deps.WindowManagerNiri { dependencies = append(dependencies, g.detectXwaylandSatellite()) } dependencies = append(dependencies, g.detectMatugen()) dependencies = append(dependencies, g.detectDgop()) dependencies = append(dependencies, g.detectHyprpicker()) dependencies = append(dependencies, g.detectClipboardTools()...) return dependencies, nil } func (g *GentooDistribution) detectXDGPortal() deps.Dependency { status := deps.StatusMissing if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") { status = deps.StatusInstalled } return deps.Dependency{ Name: "xdg-desktop-portal-gtk", Status: status, Description: "Desktop integration portal for GTK", Required: true, } } func (g *GentooDistribution) detectPolkitAgent() deps.Dependency { status := deps.StatusMissing if g.packageInstalled("mate-extra/mate-polkit") { status = deps.StatusInstalled } return deps.Dependency{ Name: "mate-polkit", Status: status, Description: "PolicyKit authentication agent", Required: true, } } func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency { status := deps.StatusMissing if g.packageInstalled("gui-apps/xwayland-satellite") { status = deps.StatusInstalled } return deps.Dependency{ Name: "xwayland-satellite", Status: status, Description: "Xwayland support", Required: true, } } func (g *GentooDistribution) detectAccountsService() deps.Dependency { status := deps.StatusMissing if g.packageInstalled("sys-apps/accountsservice") { status = deps.StatusInstalled } return deps.Dependency{ Name: "accountsservice", Status: status, Description: "D-Bus interface for user account query and manipulation", Required: true, } } func (g *GentooDistribution) packageInstalled(pkg string) bool { cmd := exec.Command("qlist", "-I", pkg) err := cmd.Run() return err == nil } func (g *GentooDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { return g.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) } func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { archKeyword := g.getArchKeyword() packages := map[string]PackageMapping{ "git": {Name: "dev-vcs/git", Repository: RepoTypeSystem}, "kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"}, "alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"}, "wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"}, "mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem}, "accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem}, "hyprpicker": g.getHyprpickerMapping(variants["hyprland"]), "qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"}, "qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"}, "qtwayland": {Name: "dev-qt/qtwayland", Repository: RepoTypeSystem}, "mesa": {Name: "media-libs/mesa", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"}, "quickshell": g.getQuickshellMapping(variants["quickshell"]), "matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}, "cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}, "dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]), "dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"}, } switch wm { case deps.WindowManagerHyprland: packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"]) packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem} packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem} packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"]) packages["grimblast"] = PackageMapping{Name: "gui-wm/hyprland-contrib", Repository: RepoTypeGURU, AcceptKeywords: archKeyword} packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem} case deps.WindowManagerNiri: packages["niri"] = g.getNiriMapping(variants["niri"]) packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword} } return packages } func (g *GentooDistribution) getQuickshellMapping(_ deps.PackageVariant) PackageMapping { return PackageMapping{Name: "gui-apps/quickshell", Repository: RepoTypeGURU, UseFlags: "breakpad jemalloc sockets wayland layer-shell session-lock toplevel-management screencopy X pipewire tray mpris pam hyprland hyprland-global-shortcuts hyprland-focus-grab i3 i3-ipc bluetooth", AcceptKeywords: "**"} } func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping { return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"} } func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping { archKeyword := g.getArchKeyword() if variant == deps.VariantGit { return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword} } return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword} } func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping { return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()} } func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping { return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()} } func (g *GentooDistribution) getPrerequisites() []string { return []string{ "app-eselect/eselect-repository", "dev-vcs/git", "dev-build/make", "app-arch/unzip", "dev-util/pkgconf", "dev-qt/qtdeclarative", } } func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword string) error { useFlags := strings.Join(GentooGlobalUseFlags, " ") checkCmd := exec.CommandContext(ctx, "grep", "-q", "^USE=", "/etc/portage/make.conf") hasUse := checkCmd.Run() == nil var cmd *exec.Cmd if hasUse { cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags)) } else { cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags)) } output, err := cmd.CombinedOutput() if err != nil { g.log(fmt.Sprintf("Failed to set global USE flags: %s", string(output))) return err } g.log(fmt.Sprintf("Set global USE flags: %s", useFlags)) return nil } func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { prerequisites := g.getPrerequisites() var missingPkgs []string if !g.skipGlobalUseFlags { progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.05, Step: "Setting global USE flags...", IsComplete: false, LogOutput: "Configuring global USE flags in /etc/portage/make.conf", } if err := g.setGlobalUseFlags(ctx, sudoPassword); err != nil { g.logError("failed to set global USE flags", err) return fmt.Errorf("failed to set global USE flags: %w", err) } } else { progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.05, Step: "Skipping global USE flags...", IsComplete: false, LogOutput: "Skipping global USE flags configuration (using existing configuration)", } } progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.06, Step: "Checking prerequisites...", IsComplete: false, LogOutput: "Checking prerequisite packages", } for _, pkg := range prerequisites { checkCmd := exec.CommandContext(ctx, "qlist", "-I", pkg) if err := checkCmd.Run(); err != nil { missingPkgs = append(missingPkgs, pkg) } } _, err := exec.LookPath("go") if err != nil { g.log("go not found in PATH, will install dev-lang/go") missingPkgs = append(missingPkgs, "dev-lang/go") } else { g.log("go already available in PATH") } if len(missingPkgs) == 0 { g.log("All prerequisites already installed") return nil } progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.07, Step: "Syncing Portage tree...", IsComplete: false, NeedsSudo: true, CommandInfo: "sudo emerge --sync", LogOutput: "Syncing Portage tree with emerge --sync", } syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet") syncOutput, syncErr := syncCmd.CombinedOutput() if syncErr != nil { g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput))) return fmt.Errorf("failed to sync Portage tree: %w\nOutput: %s", syncErr, string(syncOutput)) } g.log("Portage tree synced successfully") g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", "))) progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.08, Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)), IsComplete: false, NeedsSudo: true, CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")), LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), } args := []string{"emerge", "--ask=n", "--quiet"} args = append(args, missingPkgs...) cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) output, err := cmd.CombinedOutput() if err != nil { g.logError("failed to install prerequisites", err) g.log(fmt.Sprintf("Prerequisites command output: %s", string(output))) return fmt.Errorf("failed to install prerequisites: %w\nOutput: %s", err, string(output)) } g.log(fmt.Sprintf("Prerequisites install output: %s", string(output))) return nil } func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error { g.skipGlobalUseFlags = skipGlobalUseFlags progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.05, Step: "Checking system prerequisites...", IsComplete: false, LogOutput: "Starting prerequisite check...", } if err := g.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install prerequisites: %w", err) } systemPkgs, guruPkgs, manualPkgs, variantMap := g.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) g.log(fmt.Sprintf("CATEGORIZED PACKAGES: system=%d, guru=%d, manual=%d", len(systemPkgs), len(guruPkgs), len(manualPkgs))) if len(systemPkgs) > 0 { systemPkgNames := g.extractPackageNames(systemPkgs) progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.35, Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)), IsComplete: false, NeedsSudo: true, LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgNames, ", ")), } if err := g.installPortagePackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install Portage packages: %w", err) } } if len(guruPkgs) > 0 { g.log(fmt.Sprintf("FOUND %d GURU PACKAGES - WILL SYNC GURU REPO", len(guruPkgs))) progressChan <- InstallProgressMsg{ Phase: PhaseAURPackages, Progress: 0.60, Step: "Syncing GURU repository...", IsComplete: false, LogOutput: "Syncing GURU repository to fetch latest ebuilds", } g.log("ABOUT TO CALL syncGURURepo") if err := g.syncGURURepo(ctx, sudoPassword, progressChan); err != nil { g.log(fmt.Sprintf("syncGURURepo RETURNED ERROR: %v", err)) return fmt.Errorf("failed to sync GURU repository: %w", err) } g.log("syncGURURepo COMPLETED SUCCESSFULLY") guruPkgNames := g.extractPackageNames(guruPkgs) progressChan <- InstallProgressMsg{ Phase: PhaseAURPackages, Progress: 0.65, Step: fmt.Sprintf("Installing %d GURU packages...", len(guruPkgs)), IsComplete: false, LogOutput: fmt.Sprintf("Installing GURU packages: %s", strings.Join(guruPkgNames, ", ")), } if err := g.installGURUPackages(ctx, guruPkgs, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install GURU packages: %w", err) } } if len(manualPkgs) > 0 { progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.85, Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)), IsComplete: false, LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")), } if err := g.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install manual packages: %w", err) } } progressChan <- InstallProgressMsg{ Phase: PhaseConfiguration, Progress: 0.90, Step: "Configuring system...", IsComplete: false, LogOutput: "Starting post-installation configuration...", } progressChan <- InstallProgressMsg{ Phase: PhaseComplete, Progress: 1.0, Step: "Installation complete!", IsComplete: true, LogOutput: "All packages installed and configured successfully", } return nil } func (g *GentooDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]PackageMapping, []PackageMapping, []string, map[string]deps.PackageVariant) { systemPkgs := []PackageMapping{} guruPkgs := []PackageMapping{} manualPkgs := []string{} variantMap := make(map[string]deps.PackageVariant) for _, dep := range dependencies { variantMap[dep.Name] = dep.Variant } packageMap := g.GetPackageMappingWithVariants(wm, variantMap) for _, dep := range dependencies { if disabledFlags[dep.Name] { continue } if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] { continue } pkgInfo, exists := packageMap[dep.Name] if !exists { g.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name)) continue } switch pkgInfo.Repository { case RepoTypeSystem: systemPkgs = append(systemPkgs, pkgInfo) case RepoTypeGURU: guruPkgs = append(guruPkgs, pkgInfo) case RepoTypeManual: manualPkgs = append(manualPkgs, dep.Name) } } return systemPkgs, guruPkgs, manualPkgs, variantMap } func (g *GentooDistribution) extractPackageNames(packages []PackageMapping) []string { names := make([]string, len(packages)) for i, pkg := range packages { names[i] = pkg.Name } return names } func (g *GentooDistribution) installPortagePackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { if len(packages) == 0 { return nil } packageNames := g.extractPackageNames(packages) g.log(fmt.Sprintf("Installing Portage packages: %s", strings.Join(packageNames, ", "))) for _, pkg := range packages { if pkg.AcceptKeywords != "" { if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil { return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err) } } if pkg.UseFlags != "" { if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil { return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err) } } } args := []string{"emerge", "--ask=n", "--quiet"} args = append(args, packageNames...) progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.40, Step: "Installing system packages...", IsComplete: false, NeedsSudo: true, CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), } cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0) } func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error { packageUseDir := "/etc/portage/package.use" mkdirCmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("mkdir -p %s", packageUseDir)) if output, err := mkdirCmd.CombinedOutput(); err != nil { g.log(fmt.Sprintf("mkdir output: %s", string(output))) return fmt.Errorf("failed to create package.use directory: %w", err) } useFlagLine := fmt.Sprintf("%s %s", packageName, useFlags) checkExistingCmd := exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, packageUseDir)) if checkExistingCmd.Run() == nil { g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName)) escapedPkg := strings.ReplaceAll(packageName, "/", "\\/") replaceCmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir)) if output, err := replaceCmd.CombinedOutput(); err != nil { g.log(fmt.Sprintf("sed delete output: %s", string(output))) return fmt.Errorf("failed to remove old USE flags: %w", err) } } appendCmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir)) output, err := appendCmd.CombinedOutput() if err != nil { g.log(fmt.Sprintf("append output: %s", string(output))) return fmt.Errorf("failed to write USE flags to package.use: %w", err) } g.log(fmt.Sprintf("Set USE flags for %s: %s", packageName, useFlags)) return nil } func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.55, Step: "Enabling GURU repository...", IsComplete: false, NeedsSudo: true, CommandInfo: "sudo eselect repository enable guru", LogOutput: "Enabling GURU repository with eselect", } // Enable GURU repository enableCmd := ExecSudoCommand(ctx, sudoPassword, "eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code") output, err := enableCmd.CombinedOutput() g.log(fmt.Sprintf("eselect repository enable guru output:\n%s", string(output))) progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.55, LogOutput: "GURU repository enabled", } if err != nil { progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.55, LogOutput: fmt.Sprintf("ERROR enabling GURU: %v", err), Error: err, } return fmt.Errorf("failed to enable GURU repository: %w\nOutput: %s", err, string(output)) } // Sync GURU repository progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.57, Step: "Syncing GURU repository...", IsComplete: false, NeedsSudo: true, CommandInfo: "sudo emaint sync --repo guru", LogOutput: "Syncing GURU repository", } syncCmd := ExecSudoCommand(ctx, sudoPassword, "emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code") syncOutput, syncErr := syncCmd.CombinedOutput() g.log(fmt.Sprintf("emaint sync --repo guru output:\n%s", string(syncOutput))) progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.57, LogOutput: "GURU repository synced", } if syncErr != nil { progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.57, LogOutput: fmt.Sprintf("ERROR syncing GURU: %v", syncErr), Error: syncErr, } return fmt.Errorf("failed to sync GURU repository: %w\nOutput: %s", syncErr, string(syncOutput)) } return nil } func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packageName, keywords, sudoPassword string) error { checkCmd := exec.CommandContext(ctx, "portageq", "match", "/", packageName) if output, err := checkCmd.CombinedOutput(); err == nil && len(output) > 0 { g.log(fmt.Sprintf("Package %s is already available (may already be unmasked)", packageName)) return nil } acceptKeywordsDir := "/etc/portage/package.accept_keywords" mkdirCmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("mkdir -p %s", acceptKeywordsDir)) if output, err := mkdirCmd.CombinedOutput(); err != nil { g.log(fmt.Sprintf("mkdir output: %s", string(output))) return fmt.Errorf("failed to create package.accept_keywords directory: %w", err) } keywordLine := fmt.Sprintf("%s %s", packageName, keywords) checkExistingCmd := exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, acceptKeywordsDir)) if checkExistingCmd.Run() == nil { g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName)) escapedPkg := strings.ReplaceAll(packageName, "/", "\\/") replaceCmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir)) if output, err := replaceCmd.CombinedOutput(); err != nil { g.log(fmt.Sprintf("sed delete output: %s", string(output))) return fmt.Errorf("failed to remove old accept keywords: %w", err) } } appendCmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir)) output, err := appendCmd.CombinedOutput() if err != nil { g.log(fmt.Sprintf("append output: %s", string(output))) return fmt.Errorf("failed to write accept keywords: %w", err) } g.log(fmt.Sprintf("Set accept keywords for %s: %s", packageName, keywords)) return nil } func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { if len(packages) == 0 { return nil } packageNames := g.extractPackageNames(packages) g.log(fmt.Sprintf("Installing GURU packages: %s", strings.Join(packageNames, ", "))) for _, pkg := range packages { if pkg.AcceptKeywords != "" { if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil { return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err) } } if pkg.UseFlags != "" { if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil { return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err) } } } guruPackages := make([]string, len(packageNames)) for i, pkg := range packageNames { guruPackages[i] = pkg + "::guru" } args := []string{"emerge", "--ask=n", "--quiet"} args = append(args, guruPackages...) progressChan <- InstallProgressMsg{ Phase: PhaseAURPackages, Progress: 0.70, Step: "Installing GURU packages...", IsComplete: false, NeedsSudo: true, CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), } cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0) }