detached f7886b1

fix(repos): improve multilib parsing, package installation resilience, and Chaotic-AUR handling

commit
f7886b10165a649f6aa417a574dc9389dcf8870e
author
bnema · · 2025-10-27 18:07 UTC
committer
bnema · · 2025-10-27 18:26 UTC
parent
f981a2c
signature
unsigned
1 file · +185 -40
M internal/phases/repos.go +185 -40
@@ -5,7 +5,6 @@ import (
55 "fmt"
66 "os"
77 "path/filepath"
8- "regexp"
98 "strings"
109
1110 "github.com/bnema/archup/internal/config"
@@ -13,6 +12,37 @@ import (
1312 "github.com/bnema/archup/internal/system"
1413 )
1514
15+// Pacman configuration constants
16+const (
17+ pacmanMultilibSectionDisabled = "#[multilib]"
18+ pacmanMultilibSectionEnabled = "[multilib]"
19+ pacmanMultilibIncludeDisabled = "#Include = /etc/pacman.d/mirrorlist"
20+ pacmanMultilibIncludeEnabled = "Include = /etc/pacman.d/mirrorlist"
21+ pacmanSectionPrefix = "["
22+)
23+
24+// AUR helper constants and validation
25+const (
26+ AURHelperParu = "paru"
27+ AURHelperYay = "yay"
28+)
29+
30+// ValidAURHelpers contains the list of supported AUR helpers
31+var ValidAURHelpers = []string{AURHelperParu, AURHelperYay}
32+
33+// IsValidAURHelper validates if the given AUR helper is supported
34+func IsValidAURHelper(helper string) bool {
35+ if helper == "" {
36+ return true // Empty is valid (no AUR helper)
37+ }
38+ for _, valid := range ValidAURHelpers {
39+ if helper == valid {
40+ return true
41+ }
42+ }
43+ return false
44+}
45+
1646 // ReposPhase handles repository configuration
1747 type ReposPhase struct {
1848 *BasePhase
@@ -29,6 +59,12 @@ func NewReposPhase(cfg *config.Config, log *logger.Logger) *ReposPhase {
2959
3060 // PreCheck validates repos prerequisites
3161 func (p *ReposPhase) PreCheck() error {
62+ // Validate AUR helper first (config validation before system checks)
63+ switch {
64+ case p.config.AURHelper != "" && !IsValidAURHelper(p.config.AURHelper):
65+ return fmt.Errorf("invalid AUR helper: %s (must be one of: %v)", p.config.AURHelper, ValidAURHelpers)
66+ }
67+
3268 // Verify /mnt is mounted
3369 result := system.RunSimple("mountpoint", "-q", config.PathMnt)
3470 switch {
@@ -47,7 +83,7 @@ func (p *ReposPhase) PreCheck() error {
4783
4884 // Execute runs the repos phase
4985 func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
50- p.SendProgress(progressChan, "Starting repository configuration...", 1, 4)
86+ p.SendProgress(progressChan, "Starting repository configuration...", 1, 5)
5187
5288 // Step 1: Enable multilib (optional)
5389 switch {
@@ -61,9 +97,18 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
6197 p.SendOutput(progressChan, "[SKIP] Multilib disabled")
6298 }
6399
64- p.SendProgress(progressChan, "Installing extra packages...", 2, 4)
100+ // Step 2: Sync package databases after enabling multilib
101+ p.SendProgress(progressChan, "Syncing package databases...", 2, 5)
102+ switch err := p.syncDatabases(progressChan); {
103+ case err != nil:
104+ p.logger.Warn("Database sync failed", "error", err)
105+ p.SendOutput(progressChan, fmt.Sprintf("[WARN] Database sync failed: %v", err))
106+ // Don't fail the phase, just warn
107+ }
65108
66- // Step 2: Install extra packages (always, from official repos)
109+ p.SendProgress(progressChan, "Installing extra packages...", 3, 5)
110+
111+ // Step 3: Install extra packages (always, from official repos)
67112 switch err := p.installExtraPackages(progressChan); {
68113 case err != nil:
69114 // Don't fail the phase, just warn
@@ -71,9 +116,9 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
71116 p.SendOutput(progressChan, fmt.Sprintf("[WARN] Some extra packages failed: %v", err))
72117 }
73118
74- p.SendProgress(progressChan, "Configuring Chaotic-AUR...", 3, 4)
119+ p.SendProgress(progressChan, "Configuring Chaotic-AUR...", 4, 5)
75120
76- // Step 3: Enable Chaotic-AUR (optional)
121+ // Step 4: Enable Chaotic-AUR (optional)
77122 switch {
78123 case p.config.EnableChaotic:
79124 switch err := p.enableChaoticAUR(progressChan); {
@@ -85,9 +130,9 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
85130 p.SendOutput(progressChan, "[SKIP] Chaotic-AUR disabled")
86131 }
87132
88- p.SendProgress(progressChan, "Installing AUR helper...", 4, 4)
133+ p.SendProgress(progressChan, "Installing AUR helper...", 5, 5)
89134
90- // Step 4: Install AUR helper (optional)
135+ // Step 5: Install AUR helper (optional)
91136 switch {
92137 case p.config.AURHelper != "":
93138 switch err := p.installAURHelper(progressChan); {
@@ -99,12 +144,25 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
99144 p.SendOutput(progressChan, "[SKIP] AUR helper disabled")
100145 }
101146
102- p.SendProgress(progressChan, "Repository configuration complete", 4, 4)
147+ p.SendProgress(progressChan, "Repository configuration complete", 5, 5)
103148 p.SendComplete(progressChan, "Repositories configured successfully")
104149
105150 return PhaseResult{Success: true, Message: "Repos configuration complete"}
106151 }
107152
153+// syncDatabases syncs pacman package databases
154+func (p *ReposPhase) syncDatabases(progressChan chan<- ProgressUpdate) error {
155+ p.SendOutput(progressChan, "Syncing package databases...")
156+
157+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, "pacman -Sy --noconfirm"); {
158+ case err != nil:
159+ return fmt.Errorf("failed to sync databases: %w", err)
160+ }
161+
162+ p.SendOutput(progressChan, "[OK] Package databases synced")
163+ return nil
164+}
165+
108166 // enableMultilib enables multilib repository
109167 func (p *ReposPhase) enableMultilib(progressChan chan<- ProgressUpdate) error {
110168 p.SendOutput(progressChan, "Enabling multilib repository...")
@@ -118,19 +176,40 @@ func (p *ReposPhase) enableMultilib(progressChan chan<- ProgressUpdate) error {
118176
119177 contentStr := string(content)
120178
121- // Check if already enabled
179+ // Check if already fully enabled (both section and Include line uncommented)
180+ hasMultilibSection := strings.Contains(contentStr, "[multilib]") && !strings.Contains(contentStr, "#[multilib]")
181+ hasMultilibInclude := strings.Contains(contentStr, pacmanMultilibIncludeEnabled)
182+
122183 switch {
123- case strings.Contains(contentStr, "[multilib]") && !strings.Contains(contentStr, "#[multilib]"):
184+ case hasMultilibSection && hasMultilibInclude:
124185 p.SendOutput(progressChan, "[OK] Multilib already enabled")
125186 return nil
126187 }
127188
128- // Uncomment multilib section
129- multilibRegex := regexp.MustCompile(`(?m)^#\[multilib\]`)
130- contentStr = multilibRegex.ReplaceAllString(contentStr, "[multilib]")
189+ // Uncomment multilib section and its Include line in context-aware manner
190+ // This ensures we only uncomment the Include that's right after [multilib]
191+ lines := strings.Split(contentStr, "\n")
192+ inMultilib := false
193+ for i, line := range lines {
194+ trimmedLine := strings.TrimSpace(line)
131195
132- includeRegex := regexp.MustCompile(`(?m)^#Include = /etc/pacman.d/mirrorlist`)
133- contentStr = includeRegex.ReplaceAllString(contentStr, "Include = /etc/pacman.d/mirrorlist")
196+ switch {
197+ case trimmedLine == pacmanMultilibSectionDisabled:
198+ lines[i] = pacmanMultilibSectionEnabled
199+ inMultilib = true
200+ case trimmedLine == pacmanMultilibSectionEnabled:
201+ // Already enabled, track that we're in multilib section
202+ inMultilib = true
203+ case inMultilib && trimmedLine == pacmanMultilibIncludeDisabled:
204+ // Uncomment the Include line, preserving indentation
205+ lines[i] = strings.Replace(line, "#Include", "Include", 1)
206+ inMultilib = false // Only uncomment the first Include after [multilib]
207+ case inMultilib && strings.HasPrefix(trimmedLine, pacmanSectionPrefix) && trimmedLine != pacmanMultilibSectionEnabled:
208+ // Exited multilib section without finding Include
209+ inMultilib = false
210+ }
211+ }
212+ contentStr = strings.Join(lines, "\n")
134213
135214 // Write updated config
136215 switch err := os.WriteFile(config.PathMntEtcPacmanConf, []byte(contentStr), 0644); {
@@ -201,26 +280,31 @@ func (p *ReposPhase) enableChaoticAUR(progressChan chan<- ProgressUpdate) error
201280
202281 // Import GPG key
203282 recvKeyCmd := fmt.Sprintf("pacman-key --recv-key %s --keyserver %s", keyID, keyserver)
204- switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, recvKeyCmd); {
283+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, recvKeyCmd); {
205284 case err != nil:
206- return fmt.Errorf("failed to receive Chaotic GPG key: %w", err)
285+ p.logger.Warn("Failed to receive Chaotic GPG key, skipping Chaotic-AUR", "error", err)
286+ p.SendOutput(progressChan, "[WARN] Chaotic-AUR unavailable (GPG key fetch failed)")
287+ return nil // Don't fail, just skip
207288 }
208289
209290 // Locally sign the key
210291 lsignKeyCmd := fmt.Sprintf("pacman-key --lsign-key %s", keyID)
211- switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, lsignKeyCmd); {
292+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, lsignKeyCmd); {
212293 case err != nil:
213- return fmt.Errorf("failed to sign Chaotic GPG key: %w", err)
294+ p.logger.Warn("Failed to sign Chaotic GPG key, skipping Chaotic-AUR", "error", err)
295+ p.SendOutput(progressChan, "[WARN] Chaotic-AUR unavailable (GPG key signing failed)")
296+ return nil // Don't fail, just skip
214297 }
215298
216299 p.SendOutput(progressChan, "[OK] Chaotic GPG key imported")
217300
218301 // Download and install chaotic-keyring and chaotic-mirrorlist
219302 p.SendOutput(progressChan, "Downloading Chaotic-AUR packages...")
220- if err := system.DownloadAndInstallPackages(p.logger.LogPath(), config.PathMnt, keyringURL, mirrorlistURL); err != nil {
303+ switch err := system.DownloadAndInstallPackages(p.logger.LogPath(), config.PathMnt, keyringURL, mirrorlistURL); {
304+ case err != nil:
221305 p.logger.Warn("Chaotic-AUR packages unavailable, skipping", "error", err)
222306 p.SendOutput(progressChan, "[WARN] Chaotic-AUR unavailable, skipping (AUR helper will be built from source)")
223- return nil
307+ return nil // IMPORTANT: Exit early, don't add to pacman.conf
224308 }
225309
226310 p.SendOutput(progressChan, "[OK] Chaotic packages installed")
@@ -271,6 +355,25 @@ func (p *ReposPhase) enableChaoticAUR(progressChan chan<- ProgressUpdate) error
271355 return nil
272356 }
273357
358+// installPackagesIndividually tries to install packages one by one for resilience
359+func (p *ReposPhase) installPackagesIndividually(packages []string, progressChan chan<- ProgressUpdate) (int, []string) {
360+ failedPkgs := []string{}
361+ successCount := 0
362+
363+ for _, pkg := range packages {
364+ individualCmd := fmt.Sprintf("pacman -S --noconfirm --needed %s", pkg)
365+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, individualCmd); {
366+ case err != nil:
367+ p.logger.Warn("Failed to install package", "package", pkg, "error", err)
368+ failedPkgs = append(failedPkgs, pkg)
369+ default:
370+ successCount++
371+ }
372+ }
373+
374+ return successCount, failedPkgs
375+}
376+
274377 // installExtraPackages installs packages from extra.packages file
275378 func (p *ReposPhase) installExtraPackages(progressChan chan<- ProgressUpdate) error {
276379 p.SendOutput(progressChan, "Installing extra packages...")
@@ -288,16 +391,36 @@ func (p *ReposPhase) installExtraPackages(progressChan chan<- ProgressUpdate) er
288391 p.logger.Info("Installing extra packages", "count", len(packages), "packages", strings.Join(packages, ", "))
289392 p.SendOutput(progressChan, fmt.Sprintf("Installing %d extra packages...", len(packages)))
290393
291- // Install packages
292- installCmd := fmt.Sprintf("pacman -S --noconfirm %s", strings.Join(packages, " "))
293- switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, installCmd); {
294- case err != nil:
295- p.logger.Error("Failed to install extra packages", "error", err, "packages", strings.Join(packages, ", "))
296- return fmt.Errorf("failed to install extra packages: %w", err)
394+ // Try batch installation first
395+ installCmd := fmt.Sprintf("pacman -S --noconfirm --needed %s", strings.Join(packages, " "))
396+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, installCmd); {
397+ case err == nil:
398+ p.logger.Info("Extra packages installed successfully", "count", len(packages))
399+ p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d extra packages", len(packages)))
400+ return nil
401+ }
402+
403+ // Batch installation failed, try individual packages for resilience
404+ p.logger.Warn("Batch installation failed, trying individual packages", "error", err)
405+ p.SendOutput(progressChan, "[WARN] Batch installation failed, trying individual packages...")
406+
407+ successCount, failedPkgs := p.installPackagesIndividually(packages, progressChan)
408+
409+ // Report results
410+ switch {
411+ case len(failedPkgs) > 0:
412+ p.SendOutput(progressChan, fmt.Sprintf("[WARN] Failed to install: %s", strings.Join(failedPkgs, ", ")))
413+ p.logger.Warn("Some packages failed to install", "failed_count", len(failedPkgs), "packages", failedPkgs)
297414 }
298415
299- p.logger.Info("Extra packages installed successfully", "count", len(packages))
300- p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d extra packages", len(packages)))
416+ switch {
417+ case successCount > 0:
418+ p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d/%d packages", successCount, len(packages)))
419+ p.logger.Info("Partial package installation successful", "success_count", successCount, "total", len(packages))
420+ default:
421+ p.SendOutput(progressChan, "[ERROR] Failed to install any packages")
422+ return fmt.Errorf("failed to install all %d packages", len(packages))
423+ }
301424
302425 return nil
303426 }
@@ -342,25 +465,47 @@ func (p *ReposPhase) loadExtraPackages() ([]string, error) {
342465 func (p *ReposPhase) installAURHelper(progressChan chan<- ProgressUpdate) error {
343466 p.SendOutput(progressChan, fmt.Sprintf("Building %s from AUR...", p.config.AURHelper))
344467
468+ // Ensure build dependencies are installed
469+ p.SendOutput(progressChan, "Verifying build dependencies...")
470+ depsCmd := "pacman -S --needed --noconfirm base-devel git"
471+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, depsCmd); {
472+ case err != nil:
473+ p.logger.Warn("Failed to install build dependencies", "error", err)
474+ // Don't fail, they might already be installed
475+ }
476+
345477 // Build AUR helper from source
346478 buildCmd := fmt.Sprintf(`
347479 su - %s -c '
348- cd /tmp &&
349- git clone https://aur.archlinux.org/%s.git &&
350- cd %s &&
351- makepkg -si --noconfirm
480+ set -e
481+ cd /tmp || exit 1
482+
483+ # Clean up any previous build
484+ rm -rf %s
485+
486+ # Clone AUR repository
487+ git clone https://aur.archlinux.org/%s.git || exit 1
488+ cd %s || exit 1
489+
490+ # Build and install
491+ makepkg -si --noconfirm || exit 1
352492 '
353- `, p.config.Username, p.config.AURHelper, p.config.AURHelper)
493+ `, p.config.Username, p.config.AURHelper, p.config.AURHelper, p.config.AURHelper)
354494
355- if err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, buildCmd); err != nil {
495+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, buildCmd); {
496+ case err != nil:
356497 return fmt.Errorf("failed to build %s: %w", p.config.AURHelper, err)
357498 }
358499
359- // Cleanup
360- cleanupCmd := fmt.Sprintf("rm -rf /home/%s/tmp/%s", p.config.Username, p.config.AURHelper)
361- system.ChrootExec(p.logger.LogPath(), config.PathMnt, cleanupCmd)
500+ // Cleanup build directory
501+ cleanupCmd := fmt.Sprintf("rm -rf /tmp/%s", p.config.AURHelper)
502+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, cleanupCmd); {
503+ case err != nil:
504+ p.logger.Warn("Failed to cleanup AUR helper build directory", "error", err)
505+ // Don't fail, just warn
506+ }
362507
363- p.SendOutput(progressChan, fmt.Sprintf("[OK] %s built and installed", p.config.AURHelper))
508+ p.SendOutput(progressChan, fmt.Sprintf("[OK] %s built and installed successfully", p.config.AURHelper))
364509
365510 return nil
366511 }
@@ -5,7 +5,6 @@ import (
5 "fmt"5 "fmt"
6 "os"6 "os"
7 "path/filepath"7 "path/filepath"
8- "regexp"
9 "strings"8 "strings"
10 9
11 "github.com/bnema/archup/internal/config"10 "github.com/bnema/archup/internal/config"
@@ -13,6 +12,37 @@ import (
13 "github.com/bnema/archup/internal/system"12 "github.com/bnema/archup/internal/system"
14 )13 )
15 14
15+// Pacman configuration constants
16+const (
17+ pacmanMultilibSectionDisabled = "#[multilib]"
18+ pacmanMultilibSectionEnabled = "[multilib]"
19+ pacmanMultilibIncludeDisabled = "#Include = /etc/pacman.d/mirrorlist"
20+ pacmanMultilibIncludeEnabled = "Include = /etc/pacman.d/mirrorlist"
21+ pacmanSectionPrefix = "["
22+)
23+
24+// AUR helper constants and validation
25+const (
26+ AURHelperParu = "paru"
27+ AURHelperYay = "yay"
28+)
29+
30+// ValidAURHelpers contains the list of supported AUR helpers
31+var ValidAURHelpers = []string{AURHelperParu, AURHelperYay}
32+
33+// IsValidAURHelper validates if the given AUR helper is supported
34+func IsValidAURHelper(helper string) bool {
35+ if helper == "" {
36+ return true // Empty is valid (no AUR helper)
37+ }
38+ for _, valid := range ValidAURHelpers {
39+ if helper == valid {
40+ return true
41+ }
42+ }
43+ return false
44+}
45+
16 // ReposPhase handles repository configuration46 // ReposPhase handles repository configuration
17 type ReposPhase struct {47 type ReposPhase struct {
18 *BasePhase48 *BasePhase
@@ -29,6 +59,12 @@ func NewReposPhase(cfg *config.Config, log *logger.Logger) *ReposPhase {
29 59
30 // PreCheck validates repos prerequisites60 // PreCheck validates repos prerequisites
31 func (p *ReposPhase) PreCheck() error {61 func (p *ReposPhase) PreCheck() error {
62+ // Validate AUR helper first (config validation before system checks)
63+ switch {
64+ case p.config.AURHelper != "" && !IsValidAURHelper(p.config.AURHelper):
65+ return fmt.Errorf("invalid AUR helper: %s (must be one of: %v)", p.config.AURHelper, ValidAURHelpers)
66+ }
67+
32 // Verify /mnt is mounted68 // Verify /mnt is mounted
33 result := system.RunSimple("mountpoint", "-q", config.PathMnt)69 result := system.RunSimple("mountpoint", "-q", config.PathMnt)
34 switch {70 switch {
@@ -47,7 +83,7 @@ func (p *ReposPhase) PreCheck() error {
47 83
48 // Execute runs the repos phase84 // Execute runs the repos phase
49 func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {85 func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
50- p.SendProgress(progressChan, "Starting repository configuration...", 1, 4)86+ p.SendProgress(progressChan, "Starting repository configuration...", 1, 5)
51 87
52 // Step 1: Enable multilib (optional)88 // Step 1: Enable multilib (optional)
53 switch {89 switch {
@@ -61,9 +97,18 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
61 p.SendOutput(progressChan, "[SKIP] Multilib disabled")97 p.SendOutput(progressChan, "[SKIP] Multilib disabled")
62 }98 }
63 99
64- p.SendProgress(progressChan, "Installing extra packages...", 2, 4)100+ // Step 2: Sync package databases after enabling multilib
101+ p.SendProgress(progressChan, "Syncing package databases...", 2, 5)
102+ switch err := p.syncDatabases(progressChan); {
103+ case err != nil:
104+ p.logger.Warn("Database sync failed", "error", err)
105+ p.SendOutput(progressChan, fmt.Sprintf("[WARN] Database sync failed: %v", err))
106+ // Don't fail the phase, just warn
107+ }
65 108
66- // Step 2: Install extra packages (always, from official repos)109+ p.SendProgress(progressChan, "Installing extra packages...", 3, 5)
110+
111+ // Step 3: Install extra packages (always, from official repos)
67 switch err := p.installExtraPackages(progressChan); {112 switch err := p.installExtraPackages(progressChan); {
68 case err != nil:113 case err != nil:
69 // Don't fail the phase, just warn114 // Don't fail the phase, just warn
@@ -71,9 +116,9 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
71 p.SendOutput(progressChan, fmt.Sprintf("[WARN] Some extra packages failed: %v", err))116 p.SendOutput(progressChan, fmt.Sprintf("[WARN] Some extra packages failed: %v", err))
72 }117 }
73 118
74- p.SendProgress(progressChan, "Configuring Chaotic-AUR...", 3, 4)119+ p.SendProgress(progressChan, "Configuring Chaotic-AUR...", 4, 5)
75 120
76- // Step 3: Enable Chaotic-AUR (optional)121+ // Step 4: Enable Chaotic-AUR (optional)
77 switch {122 switch {
78 case p.config.EnableChaotic:123 case p.config.EnableChaotic:
79 switch err := p.enableChaoticAUR(progressChan); {124 switch err := p.enableChaoticAUR(progressChan); {
@@ -85,9 +130,9 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
85 p.SendOutput(progressChan, "[SKIP] Chaotic-AUR disabled")130 p.SendOutput(progressChan, "[SKIP] Chaotic-AUR disabled")
86 }131 }
87 132
88- p.SendProgress(progressChan, "Installing AUR helper...", 4, 4)133+ p.SendProgress(progressChan, "Installing AUR helper...", 5, 5)
89 134
90- // Step 4: Install AUR helper (optional)135+ // Step 5: Install AUR helper (optional)
91 switch {136 switch {
92 case p.config.AURHelper != "":137 case p.config.AURHelper != "":
93 switch err := p.installAURHelper(progressChan); {138 switch err := p.installAURHelper(progressChan); {
@@ -99,12 +144,25 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
99 p.SendOutput(progressChan, "[SKIP] AUR helper disabled")144 p.SendOutput(progressChan, "[SKIP] AUR helper disabled")
100 }145 }
101 146
102- p.SendProgress(progressChan, "Repository configuration complete", 4, 4)147+ p.SendProgress(progressChan, "Repository configuration complete", 5, 5)
103 p.SendComplete(progressChan, "Repositories configured successfully")148 p.SendComplete(progressChan, "Repositories configured successfully")
104 149
105 return PhaseResult{Success: true, Message: "Repos configuration complete"}150 return PhaseResult{Success: true, Message: "Repos configuration complete"}
106 }151 }
107 152
153+// syncDatabases syncs pacman package databases
154+func (p *ReposPhase) syncDatabases(progressChan chan<- ProgressUpdate) error {
155+ p.SendOutput(progressChan, "Syncing package databases...")
156+
157+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, "pacman -Sy --noconfirm"); {
158+ case err != nil:
159+ return fmt.Errorf("failed to sync databases: %w", err)
160+ }
161+
162+ p.SendOutput(progressChan, "[OK] Package databases synced")
163+ return nil
164+}
165+
108 // enableMultilib enables multilib repository166 // enableMultilib enables multilib repository
109 func (p *ReposPhase) enableMultilib(progressChan chan<- ProgressUpdate) error {167 func (p *ReposPhase) enableMultilib(progressChan chan<- ProgressUpdate) error {
110 p.SendOutput(progressChan, "Enabling multilib repository...")168 p.SendOutput(progressChan, "Enabling multilib repository...")
@@ -118,19 +176,40 @@ func (p *ReposPhase) enableMultilib(progressChan chan<- ProgressUpdate) error {
118 176
119 contentStr := string(content)177 contentStr := string(content)
120 178
121- // Check if already enabled179+ // Check if already fully enabled (both section and Include line uncommented)
180+ hasMultilibSection := strings.Contains(contentStr, "[multilib]") && !strings.Contains(contentStr, "#[multilib]")
181+ hasMultilibInclude := strings.Contains(contentStr, pacmanMultilibIncludeEnabled)
182+
122 switch {183 switch {
123- case strings.Contains(contentStr, "[multilib]") && !strings.Contains(contentStr, "#[multilib]"):184+ case hasMultilibSection && hasMultilibInclude:
124 p.SendOutput(progressChan, "[OK] Multilib already enabled")185 p.SendOutput(progressChan, "[OK] Multilib already enabled")
125 return nil186 return nil
126 }187 }
127 188
128- // Uncomment multilib section189+ // Uncomment multilib section and its Include line in context-aware manner
129- multilibRegex := regexp.MustCompile(`(?m)^#\[multilib\]`)190+ // This ensures we only uncomment the Include that's right after [multilib]
130- contentStr = multilibRegex.ReplaceAllString(contentStr, "[multilib]")191+ lines := strings.Split(contentStr, "\n")
192+ inMultilib := false
193+ for i, line := range lines {
194+ trimmedLine := strings.TrimSpace(line)
131 195
132- includeRegex := regexp.MustCompile(`(?m)^#Include = /etc/pacman.d/mirrorlist`)196+ switch {
133- contentStr = includeRegex.ReplaceAllString(contentStr, "Include = /etc/pacman.d/mirrorlist")197+ case trimmedLine == pacmanMultilibSectionDisabled:
198+ lines[i] = pacmanMultilibSectionEnabled
199+ inMultilib = true
200+ case trimmedLine == pacmanMultilibSectionEnabled:
201+ // Already enabled, track that we're in multilib section
202+ inMultilib = true
203+ case inMultilib && trimmedLine == pacmanMultilibIncludeDisabled:
204+ // Uncomment the Include line, preserving indentation
205+ lines[i] = strings.Replace(line, "#Include", "Include", 1)
206+ inMultilib = false // Only uncomment the first Include after [multilib]
207+ case inMultilib && strings.HasPrefix(trimmedLine, pacmanSectionPrefix) && trimmedLine != pacmanMultilibSectionEnabled:
208+ // Exited multilib section without finding Include
209+ inMultilib = false
210+ }
211+ }
212+ contentStr = strings.Join(lines, "\n")
134 213
135 // Write updated config214 // Write updated config
136 switch err := os.WriteFile(config.PathMntEtcPacmanConf, []byte(contentStr), 0644); {215 switch err := os.WriteFile(config.PathMntEtcPacmanConf, []byte(contentStr), 0644); {
@@ -201,26 +280,31 @@ func (p *ReposPhase) enableChaoticAUR(progressChan chan<- ProgressUpdate) error
201 280
202 // Import GPG key281 // Import GPG key
203 recvKeyCmd := fmt.Sprintf("pacman-key --recv-key %s --keyserver %s", keyID, keyserver)282 recvKeyCmd := fmt.Sprintf("pacman-key --recv-key %s --keyserver %s", keyID, keyserver)
204- switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, recvKeyCmd); {283+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, recvKeyCmd); {
205 case err != nil:284 case err != nil:
206- return fmt.Errorf("failed to receive Chaotic GPG key: %w", err)285+ p.logger.Warn("Failed to receive Chaotic GPG key, skipping Chaotic-AUR", "error", err)
286+ p.SendOutput(progressChan, "[WARN] Chaotic-AUR unavailable (GPG key fetch failed)")
287+ return nil // Don't fail, just skip
207 }288 }
208 289
209 // Locally sign the key290 // Locally sign the key
210 lsignKeyCmd := fmt.Sprintf("pacman-key --lsign-key %s", keyID)291 lsignKeyCmd := fmt.Sprintf("pacman-key --lsign-key %s", keyID)
211- switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, lsignKeyCmd); {292+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, lsignKeyCmd); {
212 case err != nil:293 case err != nil:
213- return fmt.Errorf("failed to sign Chaotic GPG key: %w", err)294+ p.logger.Warn("Failed to sign Chaotic GPG key, skipping Chaotic-AUR", "error", err)
295+ p.SendOutput(progressChan, "[WARN] Chaotic-AUR unavailable (GPG key signing failed)")
296+ return nil // Don't fail, just skip
214 }297 }
215 298
216 p.SendOutput(progressChan, "[OK] Chaotic GPG key imported")299 p.SendOutput(progressChan, "[OK] Chaotic GPG key imported")
217 300
218 // Download and install chaotic-keyring and chaotic-mirrorlist301 // Download and install chaotic-keyring and chaotic-mirrorlist
219 p.SendOutput(progressChan, "Downloading Chaotic-AUR packages...")302 p.SendOutput(progressChan, "Downloading Chaotic-AUR packages...")
220- if err := system.DownloadAndInstallPackages(p.logger.LogPath(), config.PathMnt, keyringURL, mirrorlistURL); err != nil {303+ switch err := system.DownloadAndInstallPackages(p.logger.LogPath(), config.PathMnt, keyringURL, mirrorlistURL); {
304+ case err != nil:
221 p.logger.Warn("Chaotic-AUR packages unavailable, skipping", "error", err)305 p.logger.Warn("Chaotic-AUR packages unavailable, skipping", "error", err)
222 p.SendOutput(progressChan, "[WARN] Chaotic-AUR unavailable, skipping (AUR helper will be built from source)")306 p.SendOutput(progressChan, "[WARN] Chaotic-AUR unavailable, skipping (AUR helper will be built from source)")
223- return nil307+ return nil // IMPORTANT: Exit early, don't add to pacman.conf
224 }308 }
225 309
226 p.SendOutput(progressChan, "[OK] Chaotic packages installed")310 p.SendOutput(progressChan, "[OK] Chaotic packages installed")
@@ -271,6 +355,25 @@ func (p *ReposPhase) enableChaoticAUR(progressChan chan<- ProgressUpdate) error
271 return nil355 return nil
272 }356 }
273 357
358+// installPackagesIndividually tries to install packages one by one for resilience
359+func (p *ReposPhase) installPackagesIndividually(packages []string, progressChan chan<- ProgressUpdate) (int, []string) {
360+ failedPkgs := []string{}
361+ successCount := 0
362+
363+ for _, pkg := range packages {
364+ individualCmd := fmt.Sprintf("pacman -S --noconfirm --needed %s", pkg)
365+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, individualCmd); {
366+ case err != nil:
367+ p.logger.Warn("Failed to install package", "package", pkg, "error", err)
368+ failedPkgs = append(failedPkgs, pkg)
369+ default:
370+ successCount++
371+ }
372+ }
373+
374+ return successCount, failedPkgs
375+}
376+
274 // installExtraPackages installs packages from extra.packages file377 // installExtraPackages installs packages from extra.packages file
275 func (p *ReposPhase) installExtraPackages(progressChan chan<- ProgressUpdate) error {378 func (p *ReposPhase) installExtraPackages(progressChan chan<- ProgressUpdate) error {
276 p.SendOutput(progressChan, "Installing extra packages...")379 p.SendOutput(progressChan, "Installing extra packages...")
@@ -288,16 +391,36 @@ func (p *ReposPhase) installExtraPackages(progressChan chan<- ProgressUpdate) er
288 p.logger.Info("Installing extra packages", "count", len(packages), "packages", strings.Join(packages, ", "))391 p.logger.Info("Installing extra packages", "count", len(packages), "packages", strings.Join(packages, ", "))
289 p.SendOutput(progressChan, fmt.Sprintf("Installing %d extra packages...", len(packages)))392 p.SendOutput(progressChan, fmt.Sprintf("Installing %d extra packages...", len(packages)))
290 393
291- // Install packages394+ // Try batch installation first
292- installCmd := fmt.Sprintf("pacman -S --noconfirm %s", strings.Join(packages, " "))395+ installCmd := fmt.Sprintf("pacman -S --noconfirm --needed %s", strings.Join(packages, " "))
293- switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, installCmd); {396+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, installCmd); {
294- case err != nil:397+ case err == nil:
295- p.logger.Error("Failed to install extra packages", "error", err, "packages", strings.Join(packages, ", "))398+ p.logger.Info("Extra packages installed successfully", "count", len(packages))
296- return fmt.Errorf("failed to install extra packages: %w", err)399+ p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d extra packages", len(packages)))
400+ return nil
401+ }
402+
403+ // Batch installation failed, try individual packages for resilience
404+ p.logger.Warn("Batch installation failed, trying individual packages", "error", err)
405+ p.SendOutput(progressChan, "[WARN] Batch installation failed, trying individual packages...")
406+
407+ successCount, failedPkgs := p.installPackagesIndividually(packages, progressChan)
408+
409+ // Report results
410+ switch {
411+ case len(failedPkgs) > 0:
412+ p.SendOutput(progressChan, fmt.Sprintf("[WARN] Failed to install: %s", strings.Join(failedPkgs, ", ")))
413+ p.logger.Warn("Some packages failed to install", "failed_count", len(failedPkgs), "packages", failedPkgs)
297 }414 }
298 415
299- p.logger.Info("Extra packages installed successfully", "count", len(packages))416+ switch {
300- p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d extra packages", len(packages)))417+ case successCount > 0:
418+ p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d/%d packages", successCount, len(packages)))
419+ p.logger.Info("Partial package installation successful", "success_count", successCount, "total", len(packages))
420+ default:
421+ p.SendOutput(progressChan, "[ERROR] Failed to install any packages")
422+ return fmt.Errorf("failed to install all %d packages", len(packages))
423+ }
301 424
302 return nil425 return nil
303 }426 }
@@ -342,25 +465,47 @@ func (p *ReposPhase) loadExtraPackages() ([]string, error) {
342 func (p *ReposPhase) installAURHelper(progressChan chan<- ProgressUpdate) error {465 func (p *ReposPhase) installAURHelper(progressChan chan<- ProgressUpdate) error {
343 p.SendOutput(progressChan, fmt.Sprintf("Building %s from AUR...", p.config.AURHelper))466 p.SendOutput(progressChan, fmt.Sprintf("Building %s from AUR...", p.config.AURHelper))
344 467
468+ // Ensure build dependencies are installed
469+ p.SendOutput(progressChan, "Verifying build dependencies...")
470+ depsCmd := "pacman -S --needed --noconfirm base-devel git"
471+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, depsCmd); {
472+ case err != nil:
473+ p.logger.Warn("Failed to install build dependencies", "error", err)
474+ // Don't fail, they might already be installed
475+ }
476+
345 // Build AUR helper from source477 // Build AUR helper from source
346 buildCmd := fmt.Sprintf(`478 buildCmd := fmt.Sprintf(`
347 su - %s -c '479 su - %s -c '
348- cd /tmp &&480+ set -e
349- git clone https://aur.archlinux.org/%s.git &&481+ cd /tmp || exit 1
350- cd %s &&482+
351- makepkg -si --noconfirm483+ # Clean up any previous build
484+ rm -rf %s
485+
486+ # Clone AUR repository
487+ git clone https://aur.archlinux.org/%s.git || exit 1
488+ cd %s || exit 1
489+
490+ # Build and install
491+ makepkg -si --noconfirm || exit 1
352 '492 '
353- `, p.config.Username, p.config.AURHelper, p.config.AURHelper)493+ `, p.config.Username, p.config.AURHelper, p.config.AURHelper, p.config.AURHelper)
354 494
355- if err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, buildCmd); err != nil {495+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, buildCmd); {
496+ case err != nil:
356 return fmt.Errorf("failed to build %s: %w", p.config.AURHelper, err)497 return fmt.Errorf("failed to build %s: %w", p.config.AURHelper, err)
357 }498 }
358 499
359- // Cleanup500+ // Cleanup build directory
360- cleanupCmd := fmt.Sprintf("rm -rf /home/%s/tmp/%s", p.config.Username, p.config.AURHelper)501+ cleanupCmd := fmt.Sprintf("rm -rf /tmp/%s", p.config.AURHelper)
361- system.ChrootExec(p.logger.LogPath(), config.PathMnt, cleanupCmd)502+ switch err := system.ChrootExec(p.logger.LogPath(), config.PathMnt, cleanupCmd); {
503+ case err != nil:
504+ p.logger.Warn("Failed to cleanup AUR helper build directory", "error", err)
505+ // Don't fail, just warn
506+ }
362 507
363- p.SendOutput(progressChan, fmt.Sprintf("[OK] %s built and installed", p.config.AURHelper))508+ p.SendOutput(progressChan, fmt.Sprintf("[OK] %s built and installed successfully", p.config.AURHelper))
364 509
365 return nil510 return nil
366 }511 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close