diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go | 856 |
1 files changed, 856 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go new file mode 100644 index 0000000..62f40cd --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/distros/opensuse.go @@ -0,0 +1,856 @@ +package distros + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "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) + }) + Register("opensuse-leap", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution { + return NewOpenSUSEDistribution(config, logChan) + }) + Register("opensuse-slowroll", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution { + return NewOpenSUSEDistribution(config, logChan) + }) +} + +type OpenSUSEDistribution struct { + *BaseDistribution + *ManualPackageInstaller + config DistroConfig +} + +const openSUSENiriWaylandServerPackage = "libwayland-server0" + +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.detectDMSGreeter()) + dependencies = append(dependencies, o.detectXDGPortal()) + 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()) + } + + dependencies = append(dependencies, o.detectMatugen()) + dependencies = append(dependencies, o.detectDgop()) + + return dependencies, nil +} + +func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency { + return o.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", o.packageInstalled("xdg-desktop-portal-gtk")) +} + +func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool { + cmd := exec.Command("rpm", "-q", pkg) + err := cmd.Run() + return err == nil +} + +func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency { + return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter")) +} + +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}, + "kitty": {Name: "kitty", Repository: RepoTypeSystem}, + "alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, + "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, + "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, + + // DMS packages from OBS + "dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]), + "quickshell": o.getQuickshellMapping(variants["quickshell"]), + "dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, + "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["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} + 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{} +} + +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.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil { + return fmt.Errorf("failed to disable install media repositories: %w", err) + } + + 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, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); 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, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); 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...", + } + + terminal := o.DetectTerminalFromDeps(dependencies) + if err := o.WriteEnvironmentConfig(terminal); err != nil { + o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err)) + } + + if err := o.WriteWindowManagerConfig(wm); err != nil { + o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err)) + } + + if err := o.EnableDMSService(ctx, wm); err != nil { + o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err)) + } + + // 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) + } + } + + systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags)) + + return systemPkgs, obsPkgs, manualPkgs, variantMap +} + +func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string { + if wm != deps.WindowManagerNiri || disabledFlags["niri"] { + return nil + } + + return []string{openSUSENiriWaylandServerPackage} +} + +func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string { + for _, pkg := range extraPkgs { + if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) { + continue + } + + o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg)) + systemPkgs = append(systemPkgs, pkg) + } + + return systemPkgs +} + +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) + + osInfo, err := GetOSInfo() + if err != nil { + return fmt.Errorf("failed to get OS info: %w", err) + } + + obsDistroVersion := "openSUSE_Tumbleweed" + switch osInfo.Distribution.ID { + case "opensuse-leap": + obsDistroVersion = fmt.Sprintf("openSUSE_Leap_%s", osInfo.VersionID) + case "opensuse-slowroll": + obsDistroVersion = "openSUSE_Slowroll" + } + + 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/%s/%s.repo", + repoPath, obsDistroVersion, 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 { + o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", 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 isOpenSUSEInstallMediaURI(uri string) bool { + normalizedURI := strings.ToLower(strings.TrimSpace(uri)) + + return strings.HasPrefix(normalizedURI, "cd:/") || + strings.HasPrefix(normalizedURI, "dvd:/") || + strings.HasPrefix(normalizedURI, "hd:/") || + strings.HasPrefix(normalizedURI, "iso:/") +} + +func parseZypperInstallMediaAliases(output string) []string { + var aliases []string + + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line == "" || !strings.Contains(line, "|") { + continue + } + + parts := strings.Split(line, "|") + if len(parts) < 7 { + continue + } + + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + + alias := parts[1] + enabled := strings.ToLower(parts[3]) + uri := parts[len(parts)-1] + + if alias == "" || strings.EqualFold(alias, "alias") { + continue + } + if enabled != "" && enabled != "yes" { + continue + } + if !isOpenSUSEInstallMediaURI(uri) { + continue + } + + aliases = append(aliases, alias) + } + + return aliases +} + +func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { + listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u") + output, err := listCmd.CombinedOutput() + if err != nil { + o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output)))) + return fmt.Errorf("failed to list zypper repositories: %w", err) + } + + aliases := parseZypperInstallMediaAliases(string(output)) + if len(aliases) == 0 { + return nil + } + + o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", "))) + progressChan <- InstallProgressMsg{ + Phase: PhasePrerequisites, + Progress: 0.055, + Step: "Disabling install media repositories...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")), + LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")), + } + + for _, alias := range aliases { + cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias))) + repoOutput, err := cmd.CombinedOutput() + if err != nil { + o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput)))) + return fmt.Errorf("failed to disable install media repo %s: %w", alias, err) + } + o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput)))) + } + + return nil +} + +func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string { + args := []string{"zypper", "install", "-y"} + if minimal { + args = append(args, "--no-recommends") + } + return append(args, packages...) +} + +func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { + if len(packages) == 0 { + return nil + } + + o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", "))) + + groups := orderedMinimalInstallGroups(packages) + totalGroups := len(groups) + + groupIndex := 0 + installGroup := func(groupPackages []string, minimal bool) error { + if len(groupPackages) == 0 { + return nil + } + + groupIndex++ + groupStart := startProgress + groupEnd := endProgress + if totalGroups > 1 { + midpoint := startProgress + ((endProgress - startProgress) / 2) + if groupIndex == 1 { + groupEnd = midpoint + } else { + groupStart = midpoint + } + } + + args := o.zypperInstallArgs(groupPackages, minimal) + progressChan <- InstallProgressMsg{ + Phase: phase, + Progress: groupStart, + Step: step, + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) + } + + for _, group := range groups { + if err := installGroup(group.packages, group.minimal); err != nil { + return err + } + } + return nil +} + +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, 0o755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + tmpDir := filepath.Join(cacheDir, "quickshell-build") + if err := os.MkdirAll(tmpDir, 0o755); 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, 0o755); 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 +} |