main 6af0a61

repos_handler.go RawHistoryFolder

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}

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