package distros import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps" ) func init() { Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution { return NewOpenSUSEDistribution(config, logChan) }) } type OpenSUSEDistribution struct { *BaseDistribution *ManualPackageInstaller config DistroConfig } func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution { base := NewBaseDistribution(logChan) return &OpenSUSEDistribution{ BaseDistribution: base, ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base}, config: config, } } func (o *OpenSUSEDistribution) GetID() string { return o.config.ID } func (o *OpenSUSEDistribution) GetColorHex() string { return o.config.ColorHex } func (o *OpenSUSEDistribution) GetFamily() DistroFamily { return o.config.Family } func (o *OpenSUSEDistribution) GetPackageManager() PackageManagerType { return PackageManagerZypper } func (o *OpenSUSEDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) { return o.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty) } func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) { var dependencies []deps.Dependency // DMS at the top (shell is prominent) dependencies = append(dependencies, o.detectDMS()) // Terminal with choice support dependencies = append(dependencies, o.detectSpecificTerminal(terminal)) // Common detections using base methods dependencies = append(dependencies, o.detectGit()) dependencies = append(dependencies, o.detectWindowManager(wm)) dependencies = append(dependencies, o.detectQuickshell()) dependencies = append(dependencies, o.detectXDGPortal()) dependencies = append(dependencies, o.detectPolkitAgent()) dependencies = append(dependencies, o.detectAccountsService()) // Hyprland-specific tools if wm == deps.WindowManagerHyprland { dependencies = append(dependencies, o.detectHyprlandTools()...) } // Niri-specific tools if wm == deps.WindowManagerNiri { dependencies = append(dependencies, o.detectXwaylandSatellite()) } // Base detections (common across distros) dependencies = append(dependencies, o.detectMatugen()) dependencies = append(dependencies, o.detectDgop()) dependencies = append(dependencies, o.detectClipboardTools()...) return dependencies, nil } func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency { status := deps.StatusMissing if o.packageInstalled("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 (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency { status := deps.StatusMissing if o.packageInstalled("mate-polkit") { status = deps.StatusInstalled } return deps.Dependency{ Name: "mate-polkit", Status: status, Description: "PolicyKit authentication agent", Required: true, } } func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool { cmd := exec.Command("rpm", "-q", pkg) err := cmd.Run() return err == nil } func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) } func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping { packages := map[string]PackageMapping{ // Standard zypper packages "git": {Name: "git", Repository: RepoTypeSystem}, "ghostty": {Name: "ghostty", Repository: RepoTypeSystem}, "kitty": {Name: "kitty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "cliphist": {Name: "cliphist", Repository: RepoTypeSystem}, // DMS packages from OBS "dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]), "quickshell": o.getQuickshellMapping(variants["quickshell"]), "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, } switch wm { case deps.WindowManagerHyprland: packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem} packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem} packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} case deps.WindowManagerNiri: // Niri stable has native package support on openSUSE niriVariant := variants["niri"] packages["niri"] = o.getNiriMapping(niriVariant) packages["xwayland-satellite"] = o.getXwaylandSatelliteMapping(niriVariant) } return packages } func (o *OpenSUSEDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping { if variant == deps.VariantGit { return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"} } return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"} } func (o *OpenSUSEDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping { if forceQuickshellGit || variant == deps.VariantGit { return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} } return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} } func (o *OpenSUSEDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping { if variant == deps.VariantGit { return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} } return PackageMapping{Name: "niri", Repository: RepoTypeSystem} } func (o *OpenSUSEDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping { if variant == deps.VariantGit { return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"} } return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem} } func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency { status := deps.StatusMissing if o.commandExists("xwayland-satellite") { status = deps.StatusInstalled } return deps.Dependency{ Name: "xwayland-satellite", Status: status, Description: "Xwayland support", Required: true, } } func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency { status := deps.StatusMissing if o.packageInstalled("accountsservice") { status = deps.StatusInstalled } return deps.Dependency{ Name: "accountsservice", Status: status, Description: "D-Bus interface for user account query and manipulation", Required: true, } } func (o *OpenSUSEDistribution) getPrerequisites() []string { return []string{ "make", "unzip", "gcc", "gcc-c++", "cmake", "ninja", "pkgconf-pkg-config", "git", "qt6-base-devel", "qt6-declarative-devel", "qt6-declarative-private-devel", "qt6-shadertools", "qt6-shadertools-devel", "qt6-wayland-devel", "qt6-waylandclient-private-devel", "spirv-tools-devel", "cli11-devel", "wayland-protocols-devel", "libgbm-devel", "libdrm-devel", "pipewire-devel", "jemalloc-devel", "wayland-utils", "Mesa-libGLESv3-devel", "pam-devel", "glib2-devel", "polkit-devel", } } func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { prerequisites := o.getPrerequisites() var missingPkgs []string progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.06, Step: "Checking prerequisites...", IsComplete: false, LogOutput: "Checking prerequisite packages", } for _, pkg := range prerequisites { checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg) if err := checkCmd.Run(); err != nil { missingPkgs = append(missingPkgs, pkg) } } _, err := exec.LookPath("go") if err != nil { o.log("go not found in PATH, will install go") missingPkgs = append(missingPkgs, "go") } else { o.log("go already available in PATH") } if len(missingPkgs) == 0 { o.log("All prerequisites already installed") return nil } o.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 zypper install -y %s", strings.Join(missingPkgs, " ")), LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), } args := []string{"zypper", "install", "-y"} args = append(args, missingPkgs...) cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) output, err := cmd.CombinedOutput() if err != nil { o.logError("failed to install prerequisites", err) o.log(fmt.Sprintf("Prerequisites command output: %s", string(output))) return fmt.Errorf("failed to install prerequisites: %w", err) } o.log(fmt.Sprintf("Prerequisites install output: %s", string(output))) return nil } func (o *OpenSUSEDistribution) 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 { // Phase 1: Check Prerequisites progressChan <- InstallProgressMsg{ Phase: PhasePrerequisites, Progress: 0.05, Step: "Checking system prerequisites...", IsComplete: false, LogOutput: "Starting prerequisite check...", } if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install prerequisites: %w", err) } systemPkgs, obsPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) // Enable OBS repositories if len(obsPkgs) > 0 { progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.15, Step: "Enabling OBS repositories...", IsComplete: false, LogOutput: "Setting up OBS repositories for additional packages", } if err := o.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to enable OBS repositories: %w", err) } } // Phase 3: System Packages (Zypper) if len(systemPkgs) > 0 { 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(systemPkgs, ", ")), } if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install zypper packages: %w", err) } } // OBS Packages obsPkgNames := o.extractPackageNames(obsPkgs) if len(obsPkgNames) > 0 { progressChan <- InstallProgressMsg{ Phase: PhaseAURPackages, Progress: 0.65, Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)), IsComplete: false, LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")), } if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install OBS packages: %w", err) } } // Manual Builds 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 := o.InstallManualPackages(ctx, manualPkgs, variantMap, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install manual packages: %w", err) } } // Configuration progressChan <- InstallProgressMsg{ Phase: PhaseConfiguration, Progress: 0.90, Step: "Configuring system...", IsComplete: false, LogOutput: "Starting post-installation configuration...", } // Complete progressChan <- InstallProgressMsg{ Phase: PhaseComplete, Progress: 1.0, Step: "Installation complete!", IsComplete: true, LogOutput: "All packages installed and configured successfully", } return nil } func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) { systemPkgs := []string{} obsPkgs := []PackageMapping{} manualPkgs := []string{} variantMap := make(map[string]deps.PackageVariant) for _, dep := range dependencies { variantMap[dep.Name] = dep.Variant } packageMap := o.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 { o.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name)) continue } switch pkgInfo.Repository { case RepoTypeSystem: systemPkgs = append(systemPkgs, pkgInfo.Name) case RepoTypeOBS: obsPkgs = append(obsPkgs, pkgInfo) case RepoTypeManual: manualPkgs = append(manualPkgs, dep.Name) } } return systemPkgs, obsPkgs, manualPkgs, variantMap } func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string { names := make([]string, len(packages)) for i, pkg := range packages { names[i] = pkg.Name } return names } func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { enabledRepos := make(map[string]bool) for _, pkg := range obsPkgs { if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] { o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL)) // RepoURL format: "home:AvengeMedia:danklinux" repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/") repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-") repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo", repoPath, pkg.RepoURL) checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName) if checkCmd.Run() == nil { o.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL)) enabledRepos[pkg.RepoURL] = true continue } progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.20, Step: fmt.Sprintf("Enabling OBS repo %s...", pkg.RepoURL), NeedsSudo: true, CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL), } cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper addrepo -f %s", repoURL)) if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err) } enabledRepos[pkg.RepoURL] = true o.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL)) } } // Refresh repositories with GPG auto-import if len(enabledRepos) > 0 { progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.25, Step: "Refreshing repositories...", NeedsSudo: true, CommandInfo: "sudo zypper --gpg-auto-import-keys refresh", } refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh") if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil { return fmt.Errorf("failed to refresh repositories: %w", err) } } return nil } func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { if len(packages) == 0 { return nil } o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", "))) args := []string{"zypper", "install", "-y"} args = append(args, packages...) 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 o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) } func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { o.log("Installing quickshell from source (with openSUSE-specific build flags)...") homeDir := os.Getenv("HOME") if homeDir == "" { return fmt.Errorf("HOME environment variable not set") } cacheDir := filepath.Join(homeDir, ".cache", "dankinstall") if err := os.MkdirAll(cacheDir, 0755); err != nil { return fmt.Errorf("failed to create cache directory: %w", err) } tmpDir := filepath.Join(cacheDir, "quickshell-build") if err := os.MkdirAll(tmpDir, 0755); err != nil { return fmt.Errorf("failed to create temp directory: %w", err) } defer os.RemoveAll(tmpDir) progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.1, Step: "Cloning quickshell repository...", IsComplete: false, CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git", } var cloneCmd *exec.Cmd if forceQuickshellGit || variant == deps.VariantGit { cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir) } else { latestTag := o.getLatestQuickshellTag(ctx) if latestTag != "" { o.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag)) cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir) } else { o.log("Warning: failed to fetch latest tag, using default branch") cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir) } } if err := cloneCmd.Run(); err != nil { return fmt.Errorf("failed to clone quickshell: %w", err) } buildDir := tmpDir + "/build" if err := os.MkdirAll(buildDir, 0755); err != nil { return fmt.Errorf("failed to create build directory: %w", err) } progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.3, Step: "Configuring quickshell build (with openSUSE flags)...", IsComplete: false, CommandInfo: "cmake -B build -S . -G Ninja", } // Get optflags from rpm optflagsCmd := exec.CommandContext(ctx, "rpm", "--eval", "%{optflags}") optflagsOutput, err := optflagsCmd.Output() optflags := strings.TrimSpace(string(optflagsOutput)) if err != nil || optflags == "" { o.log("Warning: Could not get optflags from rpm, using default -O2 -g") optflags = "-O2 -g" } // Set openSUSE-specific CFLAGS customCFLAGS := fmt.Sprintf("%s -I/usr/include/wayland", optflags) configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build", "-DCMAKE_BUILD_TYPE=RelWithDebInfo", "-DCRASH_REPORTER=off", "-DCMAKE_CXX_STANDARD=20") configureCmd.Dir = tmpDir configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir, "CFLAGS="+customCFLAGS, "CXXFLAGS="+customCFLAGS) o.log(fmt.Sprintf("Using CFLAGS: %s", customCFLAGS)) output, err := configureCmd.CombinedOutput() if err != nil { o.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output))) return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output)) } o.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output))) progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.4, Step: "Building quickshell (this may take a while)...", IsComplete: false, CommandInfo: "cmake --build build", } buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build") buildCmd.Dir = tmpDir buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir, "CFLAGS="+customCFLAGS, "CXXFLAGS="+customCFLAGS) if err := o.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil { return fmt.Errorf("failed to build quickshell: %w", err) } progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.8, Step: "Installing quickshell...", IsComplete: false, NeedsSudo: true, CommandInfo: "sudo cmake --install build", } installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build") installCmd.Dir = tmpDir if err := installCmd.Run(); err != nil { return fmt.Errorf("failed to install quickshell: %w", err) } o.log("quickshell installed successfully from source") return nil } func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { if o.commandExists("cargo") { return nil } progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.82, Step: "Installing rustup...", IsComplete: false, NeedsSudo: true, CommandInfo: "sudo zypper install rustup", } rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup") if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { return fmt.Errorf("failed to install rustup: %w", err) } progressChan <- InstallProgressMsg{ Phase: PhaseSystemPackages, Progress: 0.83, Step: "Installing stable Rust toolchain...", IsComplete: false, CommandInfo: "rustup install stable", } rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable") if err := o.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil { return fmt.Errorf("failed to install Rust toolchain: %w", err) } if !o.commandExists("cargo") { o.log("Warning: cargo not found in PATH after Rust installation, trying to source environment") } return nil } func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { if len(packages) == 0 { return nil } o.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", "))) for _, pkg := range packages { if pkg == "matugen" { if err := o.installRust(ctx, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install Rust: %w", err) } break } } for _, pkg := range packages { variant := variantMap[pkg] if pkg == "quickshell" { if err := o.installQuickshell(ctx, variant, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install quickshell: %w", err) } } else { if err := o.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil { return fmt.Errorf("failed to install %s: %w", pkg, err) } } } return nil }