From 908e1f600e93051a3a62bc603b4f6dd2cfce1a12 Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 12 Mar 2026 14:55:02 -0400 Subject: [PATCH] dankinstall(distros): Enhance DMS minimal install logic -Updated for Debian, Ubuntu, Fedora, and OpenSUSE - New shared minimal installation logic to streamline package handling across distros --- core/internal/distros/debian.go | 82 +++++++-- core/internal/distros/fedora.go | 92 +++++----- core/internal/distros/minimal_install.go | 44 +++++ core/internal/distros/opensuse.go | 206 ++++++++++++++++++----- core/internal/distros/ubuntu.go | 80 +++++---- 5 files changed, 375 insertions(+), 129 deletions(-) create mode 100644 core/internal/distros/minimal_install.go diff --git a/core/internal/distros/debian.go b/core/internal/distros/debian.go index 13d90987..ea3da7c0 100644 --- a/core/internal/distros/debian.go +++ b/core/internal/distros/debian.go @@ -92,9 +92,25 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency { } func (d *DebianDistribution) packageInstalled(pkg string) bool { - cmd := exec.Command("dpkg", "-l", pkg) - err := cmd.Run() - return err == nil + return debianPackageInstalledPrecisely(pkg) +} + +func debianPackageInstalledPrecisely(pkg string) bool { + cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg) + output, err := cmd.Output() + if err != nil { + return false + } + return strings.TrimSpace(string(output)) == "installed" +} + +func containsString(values []string, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false } func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { @@ -194,12 +210,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw Step: "Installing development dependencies...", IsComplete: false, NeedsSudo: true, - CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev", + CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev", LogOutput: "Installing additional development tools", } devToolsCmd := ExecSudoCommand(ctx, sudoPassword, - "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") + "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { return fmt.Errorf("failed to install development tools: %w", err) } @@ -379,6 +395,14 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st return names } +func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string { + args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} + if minimal { + args = append(args, "--no-install-recommends") + } + return append(args, packages...) +} + func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { enabledRepos := make(map[string]bool) @@ -482,20 +506,46 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages [] d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) - args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} - args = append(args, packages...) + groups := orderedMinimalInstallGroups(packages) + totalGroups := len(groups) - progressChan <- InstallProgressMsg{ - Phase: PhaseSystemPackages, - Progress: 0.40, - Step: "Installing system packages...", - IsComplete: false, - NeedsSudo: true, - CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + groupIndex := 0 + installGroup := func(groupPackages []string, minimal bool) error { + if len(groupPackages) == 0 { + return nil + } + + groupIndex++ + startProgress := 0.40 + endProgress := 0.60 + if totalGroups > 1 { + if groupIndex == 1 { + endProgress = 0.50 + } else { + startProgress = 0.50 + } + } + + args := d.aptInstallArgs(groupPackages, minimal) + progressChan <- InstallProgressMsg{ + Phase: PhaseSystemPackages, + Progress: startProgress, + Step: "Installing system packages...", + IsComplete: false, + NeedsSudo: true, + CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + } + + cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) + return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress) } - cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) - return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) + for _, group := range groups { + if err := installGroup(group.packages, group.minimal); err != nil { + return err + } + } + return nil } func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { diff --git a/core/internal/distros/fedora.go b/core/internal/distros/fedora.go index 42140bed..4c16d4ea 100644 --- a/core/internal/distros/fedora.go +++ b/core/internal/distros/fedora.go @@ -484,28 +484,7 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages [] f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) - args := []string{"dnf", "install", "-y"} - - for _, pkg := range packages { - if pkg == "niri" || pkg == "niri-git" { - args = append(args, "--setopt=install_weak_deps=False") - break - } - } - - 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 f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) + return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60) } func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { @@ -515,26 +494,57 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [ f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", "))) - args := []string{"dnf", "install", "-y"} + return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85) +} - for _, pkg := range packages { - if pkg == "niri" || pkg == "niri-git" { - args = append(args, "--setopt=install_weak_deps=False") - break +func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string { + args := []string{"dnf", "install", "-y"} + if minimal { + args = append(args, "--setopt=install_weak_deps=False") + } + return append(args, packages...) +} + +func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { + 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 := f.dnfInstallArgs(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 f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) + } + + for _, group := range groups { + if err := installGroup(group.packages, group.minimal); err != nil { + return err } } - - args = append(args, packages...) - - progressChan <- InstallProgressMsg{ - Phase: PhaseAURPackages, - Progress: 0.70, - Step: "Installing COPR packages...", - IsComplete: false, - NeedsSudo: true, - CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), - } - - cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) - return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85) + return nil } diff --git a/core/internal/distros/minimal_install.go b/core/internal/distros/minimal_install.go new file mode 100644 index 00000000..029f1ba7 --- /dev/null +++ b/core/internal/distros/minimal_install.go @@ -0,0 +1,44 @@ +package distros + +type minimalInstallGroup struct { + packages []string + minimal bool +} + +func shouldPreferMinimalInstall(pkg string) bool { + switch pkg { + case "niri", "niri-git": + return true + default: + return false + } +} + +func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) { + for _, pkg := range packages { + if shouldPreferMinimalInstall(pkg) { + minimal = append(minimal, pkg) + continue + } + normal = append(normal, pkg) + } + return normal, minimal +} + +func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup { + normal, minimal := splitMinimalInstallPackages(packages) + groups := make([]minimalInstallGroup, 0, 2) + if len(minimal) > 0 { + groups = append(groups, minimalInstallGroup{ + packages: minimal, + minimal: true, + }) + } + if len(normal) > 0 { + groups = append(groups, minimalInstallGroup{ + packages: normal, + minimal: false, + }) + } + return groups +} diff --git a/core/internal/distros/opensuse.go b/core/internal/distros/opensuse.go index 6420a0c5..a6425515 100644 --- a/core/internal/distros/opensuse.go +++ b/core/internal/distros/opensuse.go @@ -29,6 +29,8 @@ type OpenSUSEDistribution struct { config DistroConfig } +const openSUSENiriWaylandServerPackage = "libwayland-server0" + func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution { base := NewBaseDistribution(logChan) return &OpenSUSEDistribution{ @@ -199,35 +201,7 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency { } 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", - } + return []string{} } func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { @@ -297,6 +271,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies 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) } @@ -327,7 +305,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies NeedsSudo: true, LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), } - if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { + 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) } } @@ -342,7 +320,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies IsComplete: false, LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")), } - if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil { + 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) } } @@ -432,9 +410,32 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency } } + 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 containsString(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 { @@ -514,27 +515,146 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac return nil } -func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { +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, ", "))) - args := []string{"zypper", "install", "-y"} - args = append(args, packages...) + groups := orderedMinimalInstallGroups(packages) + totalGroups := len(groups) - progressChan <- InstallProgressMsg{ - Phase: PhaseSystemPackages, - Progress: 0.40, - Step: "Installing system packages...", - IsComplete: false, - NeedsSudo: true, - CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), + 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) } - cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) - return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) + 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 { diff --git a/core/internal/distros/ubuntu.go b/core/internal/distros/ubuntu.go index c5fb09ea..37911f04 100644 --- a/core/internal/distros/ubuntu.go +++ b/core/internal/distros/ubuntu.go @@ -100,9 +100,7 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency { } func (u *UbuntuDistribution) packageInstalled(pkg string) bool { - cmd := exec.Command("dpkg", "-l", pkg) - err := cmd.Run() - return err == nil + return debianPackageInstalledPrecisely(pkg) } func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { @@ -454,21 +452,7 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages [] } u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) - - args := []string{"apt-get", "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 u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) + return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60) } func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { @@ -477,21 +461,59 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages [] } u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", "))) + return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85) +} - args := []string{"apt-get", "install", "-y"} - args = append(args, packages...) +func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string { + args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} + if minimal { + args = append(args, "--no-install-recommends") + } + return append(args, packages...) +} - progressChan <- InstallProgressMsg{ - Phase: PhaseAURPackages, - Progress: 0.70, - Step: "Installing PPA packages...", - IsComplete: false, - NeedsSudo: true, - CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), +func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error { + 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 := u.aptInstallArgs(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 u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) } - cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) - return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85) + for _, group := range groups { + if err := installGroup(group.packages, group.minimal); err != nil { + return err + } + } + return nil } func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {