1package handlers
2
3import (
4 "bufio"
5 "context"
6 "fmt"
7 "path/filepath"
8 "strings"
9
10 "github.com/bnema/archup/internal/application/commands"
11 "github.com/bnema/archup/internal/application/dto"
12 "github.com/bnema/archup/internal/config"
13 "github.com/bnema/archup/internal/domain/packages"
14 "github.com/bnema/archup/internal/domain/ports"
15)
16
17// ReposHandler handles repository configuration
18type ReposHandler struct {
19 fs ports.FileSystem
20 chrExec ports.ChrootExecutor
21 logger ports.Logger
22}
23
24// NewReposHandler creates a new repositories handler
25func NewReposHandler(fs ports.FileSystem, chrExec ports.ChrootExecutor, logger ports.Logger) *ReposHandler {
26 return &ReposHandler{
27 fs: fs,
28 chrExec: chrExec,
29 logger: logger,
30 }
31}
32
33// Handle configures package repositories
34func (h *ReposHandler) Handle(ctx context.Context, cmd commands.SetupRepositoriesCommand) (*dto.RepositoriesResult, error) {
35 h.logger.Info("Starting repository configuration", "multilib", cmd.EnableMultilib)
36
37 result := &dto.RepositoriesResult{
38 Success: false,
39 Multilib: cmd.EnableMultilib,
40 AURHelper: "",
41 ErrorDetail: "",
42 }
43
44 fail := func(message string, err error) (*dto.RepositoriesResult, error) {
45 h.logger.Error(message, "error", err)
46 result.ErrorDetail = fmt.Sprintf("%s: %v", message, err)
47 return result, err
48 }
49
50 // Create repository domain object — Chaotic AUR is always enabled
51 repo, err := packages.NewRepository(cmd.EnableMultilib, cmd.AURHelper)
52 if err != nil {
53 h.logger.Error("Invalid repository configuration", "error", err)
54 result.ErrorDetail = fmt.Sprintf("Invalid repository configuration: %v", err)
55 return result, err
56 }
57
58 h.logger.Info("Repository configuration validated",
59 "multilib", repo.EnableMultilib(),
60 "aurHelper", repo.AURHelper())
61
62 pacmanConfPath := filepath.Join(cmd.MountPoint, "etc", "pacman.conf")
63
64 // Enable multilib first (doesn't require external files)
65 if repo.EnableMultilib() {
66 confBytes, err := h.fs.ReadFile(pacmanConfPath)
67 if err != nil {
68 return fail("Failed to read pacman.conf", err)
69 }
70 conf := enableMultilibSection(string(confBytes))
71 if err := h.fs.WriteFile(pacmanConfPath, []byte(conf), 0644); err != nil {
72 return fail("Failed to write pacman.conf", err)
73 }
74 }
75
76 // For CachyOS kernel: setup CachyOS repo BEFORE sync
77 if cmd.KernelVariant == packages.KernelCachyOS {
78 if err := h.setupCachyOSRepo(ctx, cmd.MountPoint); err != nil {
79 return fail("Failed to setup CachyOS repository", err)
80 }
81 }
82
83 // Chaotic-AUR: always enabled — install keyring and mirrorlist BEFORE adding to pacman.conf
84 if _, err := h.chrExec.ExecuteInChroot(ctx, cmd.MountPoint, "pacman-key", "--recv-key", "3056513887B78AEB", "--keyserver", "keyserver.ubuntu.com"); err != nil {
85 return fail("Failed to receive Chaotic-AUR key", err)
86 }
87 if _, err := h.chrExec.ExecuteInChroot(ctx, cmd.MountPoint, "pacman-key", "--lsign-key", "3056513887B78AEB"); err != nil {
88 return fail("Failed to sign Chaotic-AUR key", err)
89 }
90 if _, err := h.chrExec.ExecuteInChroot(ctx, cmd.MountPoint, "pacman", "-U", "--noconfirm",
91 "https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-keyring.pkg.tar.zst",
92 "https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-mirrorlist.pkg.tar.zst"); err != nil {
93 return fail("Failed to install Chaotic-AUR keyring", err)
94 }
95
96 confBytes, err := h.fs.ReadFile(pacmanConfPath)
97 if err != nil {
98 return fail("Failed to read pacman.conf", err)
99 }
100 if err := h.fs.WriteFile(pacmanConfPath, []byte(ensureChaoticRepo(string(confBytes))), 0644); err != nil {
101 return fail("Failed to write pacman.conf", err)
102 }
103
104 // Sync all repos
105 if _, err := h.chrExec.ExecuteInChroot(ctx, cmd.MountPoint, "pacman", "-Sy", "--noconfirm"); err != nil {
106 return fail("Failed to sync pacman repositories", err)
107 }
108
109 // Install AUR helper from Chaotic-AUR
110 if _, err := h.chrExec.ExecuteInChroot(ctx, cmd.MountPoint, "pacman", "-S", "--noconfirm", repo.AURHelper().String()); err != nil {
111 return fail("Failed to install AUR helper", err)
112 }
113
114 // Install extra packages from extra.packages file
115 extraPkgs, err := h.loadExtraPackages()
116 if err != nil {
117 h.logger.Warn("Could not load extra.packages", "error", err)
118 } else if len(extraPkgs) > 0 {
119 h.logger.Info("Installing extra packages", "count", len(extraPkgs))
120 args := append([]string{"-S", "--noconfirm", "--needed"}, extraPkgs...)
121 if _, err := h.chrExec.ExecuteInChroot(ctx, cmd.MountPoint, "pacman", args...); err != nil {
122 return fail("Failed to install extra packages", err)
123 }
124 h.logger.Info("Extra packages installed successfully")
125
126 // Enable power-profiles-daemon after it has been installed via extra.packages
127 if err := h.chrExec.ChrootSystemctl(ctx, "", cmd.MountPoint, "enable", "power-profiles-daemon.service"); err != nil {
128 h.logger.Warn("Failed to enable power-profiles-daemon", "error", err)
129 }
130 }
131
132 result.Success = true
133 result.AURHelper = repo.AURHelper().String()
134
135 h.logger.Info("Repository configuration completed successfully")
136 return result, nil
137}
138
139// loadExtraPackages reads the extra package list from file
140func (h *ReposHandler) loadExtraPackages() ([]string, error) {
141 packageFile := filepath.Join(config.DefaultInstallDir, config.ExtraPackagesFile)
142
143 content, err := h.fs.ReadFile(packageFile)
144 if err != nil {
145 return nil, fmt.Errorf("failed to read %s: %w", packageFile, err)
146 }
147
148 var pkgs []string
149 scanner := bufio.NewScanner(strings.NewReader(string(content)))
150
151 for scanner.Scan() {
152 line := strings.TrimSpace(scanner.Text())
153
154 // Skip comments and empty lines
155 if line == "" || strings.HasPrefix(line, "#") {
156 continue
157 }
158
159 pkgs = append(pkgs, line)
160 }
161
162 if err := scanner.Err(); err != nil {
163 return nil, fmt.Errorf("error reading package file: %w", err)
164 }
165
166 h.logger.Info("Loaded extra packages from file", "file", packageFile, "count", len(pkgs))
167 return pkgs, nil
168}
169
170func enableMultilibSection(conf string) string {
171 lines := strings.Split(conf, "\n")
172 inSection := false
173 for i, line := range lines {
174 trimmed := strings.TrimSpace(line)
175 switch {
176 case strings.HasPrefix(trimmed, "#[multilib]"):
177 lines[i] = strings.Replace(line, "#", "", 1)
178 inSection = true
179 case strings.HasPrefix(trimmed, "[multilib]"):
180 inSection = true
181 case strings.HasPrefix(trimmed, "["):
182 inSection = false
183 case inSection && strings.HasPrefix(trimmed, "#Include"):
184 lines[i] = strings.Replace(line, "#", "", 1)
185 }
186 }
187 return strings.Join(lines, "\n")
188}
189
190func ensureChaoticRepo(conf string) string {
191 if strings.Contains(conf, "[chaotic-aur]") {
192 return conf
193 }
194
195 conf = strings.TrimRight(conf, "\n")
196 chaotic := "\n[chaotic-aur]\nInclude = /etc/pacman.d/chaotic-mirrorlist\n"
197 return conf + chaotic
198}
199
200// setupCachyOSRepo configures the CachyOS repo inside the chroot. Mirrors the
201// bash approach: import+sign key, write mirrorlist manually (no versioned
202// package URLs), patch pacman.conf, then sync.
203func (h *ReposHandler) setupCachyOSRepo(ctx context.Context, mountPoint string) error {
204 const (
205 cachyOSKeyID = "F3B607488DB35A47"
206 keyserver = "keyserver.ubuntu.com"
207 cachyOSMirrorlist = "## CachyOS mirrorlist\nServer = https://mirror.cachyos.org/repo/$arch/$repo\n"
208 )
209
210 // Ensure keyring is initialized inside the chroot.
211 if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--init"); err != nil {
212 return fmt.Errorf("failed to init pacman keyring in chroot: %w", err)
213 }
214
215 // Only fetch from keyserver if key is not already present in chroot.
216 if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--list-keys", cachyOSKeyID); err != nil {
217 if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--recv-keys",
218 cachyOSKeyID, "--keyserver", keyserver); err != nil {
219 return fmt.Errorf("failed to receive CachyOS key: %w", err)
220 }
221 }
222
223 if _, err := h.chrExec.ExecuteInChroot(ctx, mountPoint, "pacman-key", "--lsign-key", cachyOSKeyID); err != nil {
224 return fmt.Errorf("failed to sign CachyOS key: %w", err)
225 }
226
227 // Ensure /etc/pacman.d exists inside the chroot before writing mirrorlist.
228 mirrorlistDir := filepath.Join(mountPoint, "etc", "pacman.d")
229 if err := h.fs.MkdirAll(mirrorlistDir, 0755); err != nil {
230 return fmt.Errorf("failed to create pacman.d in chroot: %w", err)
231 }
232
233 // Write static mirrorlist — no versioned package URLs that can go stale.
234 mirrorlistPath := filepath.Join(mountPoint, "etc", "pacman.d", "cachyos-mirrorlist")
235 if err := h.fs.WriteFile(mirrorlistPath, []byte(cachyOSMirrorlist), 0644); err != nil {
236 return fmt.Errorf("failed to write CachyOS mirrorlist: %w", err)
237 }
238
239 pacmanConfPath := filepath.Join(mountPoint, "etc", "pacman.conf")
240 confBytes, err := h.fs.ReadFile(pacmanConfPath)
241 if err != nil {
242 return fmt.Errorf("failed to read pacman.conf: %w", err)
243 }
244 conf := ensureCachyOSRepo(string(confBytes))
245 return h.fs.WriteFile(pacmanConfPath, []byte(conf), 0644)
246}
247
248func ensureCachyOSRepo(conf string) string {
249 if strings.Contains(conf, "[cachyos]") {
250 return conf
251 }
252
253 conf = strings.TrimRight(conf, "\n")
254 if conf == "" {
255 return "[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n"
256 }
257
258 block := []string{
259 "# CachyOS repositories",
260 "[cachyos]",
261 "Include = /etc/pacman.d/cachyos-mirrorlist",
262 "",
263 }
264
265 lines := strings.Split(conf, "\n")
266 for i, line := range lines {
267 if strings.TrimSpace(line) == "[core]" {
268 out := make([]string, 0, len(lines)+len(block))
269 out = append(out, lines[:i]...)
270 out = append(out, block...)
271 out = append(out, lines[i:]...)
272 return strings.Join(out, "\n") + "\n"
273 }
274 }
275
276 // Fallback: append if [core] section is not found.
277 return conf + "\n[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n"
278}