main 6af0a61

install_base_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// InstallBaseHandler handles base system installation
18type InstallBaseHandler struct {
19	fs      ports.FileSystem
20	cmdExec ports.CommandExecutor
21	chrExec ports.ChrootExecutor
22	logger  ports.Logger
23}
24
25// NewInstallBaseHandler creates a new base installation handler
26func NewInstallBaseHandler(fs ports.FileSystem, cmdExec ports.CommandExecutor, chrExec ports.ChrootExecutor, logger ports.Logger) *InstallBaseHandler {
27	return &InstallBaseHandler{
28		fs:      fs,
29		cmdExec: cmdExec,
30		chrExec: chrExec,
31		logger:  logger,
32	}
33}
34
35// Handle installs the base system
36func (h *InstallBaseHandler) Handle(ctx context.Context, cmd commands.InstallBaseCommand) (*dto.InstallBaseResult, error) {
37	h.logger.Info("Starting base system installation", "kernel", cmd.KernelVariant)
38
39	result := &dto.InstallBaseResult{
40		Success:           false,
41		PackagesInstalled: []string{},
42		ErrorDetail:       "",
43	}
44
45	// Validate kernel variant
46	kernel, err := packages.NewKernel(cmd.KernelVariant)
47	if err != nil {
48		h.logger.Error("Invalid kernel variant", "error", err)
49		result.ErrorDetail = fmt.Sprintf("Invalid kernel variant: %v", err)
50		return result, err
51	}
52
53	// Load base packages from file (downloaded during bootstrap)
54	basePackages, err := h.loadBasePackages()
55	if err != nil {
56		h.logger.Error("Failed to load base.packages", "error", err)
57		result.ErrorDetail = fmt.Sprintf("Failed to load base.packages: %v - run bootstrap first", err)
58		return result, err
59	}
60
61	// Add kernel (not in base.packages, selected dynamically)
62	basePackages = append(basePackages, kernel.PackageName())
63	h.logger.Info("Adding kernel", "kernel", kernel.PackageName())
64
65	// Add microcode if requested
66	if cmd.IncludeMicrocode {
67		basePackages = append(basePackages, "intel-ucode", "amd-ucode")
68		h.logger.Info("Adding CPU microcode packages")
69	}
70
71	// Add cryptsetup for encrypted installs
72	if cmd.Encrypted {
73		basePackages = append(basePackages, "cryptsetup")
74		h.logger.Info("Adding cryptsetup for encrypted install")
75	}
76
77	// Add any additional packages from command
78	if len(cmd.Packages) > 0 {
79		basePackages = append(basePackages, cmd.Packages...)
80		h.logger.Info("Adding additional packages", "count", len(cmd.Packages))
81	}
82
83	// For CachyOS kernel: add the repo to the live host before pacstrap
84	if cmd.KernelVariant == packages.KernelCachyOS {
85		if err := h.setupCachyOSOnHost(ctx); err != nil {
86			h.logger.Error("Failed to setup CachyOS repo on host", "error", err)
87			result.ErrorDetail = fmt.Sprintf("Failed to setup CachyOS repo on host: %v", err)
88			return result, err
89		}
90	}
91
92	h.logger.Info("Installing base packages", "count", len(basePackages))
93	args := append([]string{cmd.MountPoint}, basePackages...)
94	if _, err := h.cmdExec.Execute(ctx, "pacstrap", args...); err != nil {
95		h.logger.Error("Pacstrap failed", "error", err)
96		result.ErrorDetail = fmt.Sprintf("Pacstrap failed: %v", err)
97		return result, err
98	}
99
100	fstabOutput, err := h.cmdExec.Execute(ctx, "genfstab", "-U", cmd.MountPoint)
101	if err != nil {
102		h.logger.Error("Failed to generate fstab", "error", err)
103		result.ErrorDetail = fmt.Sprintf("Failed to generate fstab: %v", err)
104		return result, err
105	}
106
107	fstabPath := filepath.Join(cmd.MountPoint, "etc", "fstab")
108	if err := h.fs.WriteFile(fstabPath, fstabOutput, 0644); err != nil {
109		h.logger.Error("Failed to write fstab", "error", err)
110		result.ErrorDetail = fmt.Sprintf("Failed to write fstab: %v", err)
111		return result, err
112	}
113
114	result.PackagesInstalled = basePackages
115	result.Success = true
116
117	h.logger.Info("Base system installation completed", "packages", len(basePackages))
118	return result, nil
119}
120
121// setupCachyOSOnHost configures the CachyOS repo on the live host so that
122// pacstrap can resolve linux-cachyos. Mirrors the working bash approach:
123// import+sign key, write mirrorlist manually (no versioned package URLs),
124// patch pacman.conf, then sync.
125func (h *InstallBaseHandler) setupCachyOSOnHost(ctx context.Context) error {
126	h.logger.Info("Setting up CachyOS repo on host for pacstrap")
127
128	const (
129		cachyOSKeyID          = "F3B607488DB35A47"
130		keyserver             = "keyserver.ubuntu.com"
131		hostPacmanConf        = "/etc/pacman.conf"
132		cachyOSMirrorlistDir  = "/etc/pacman.d"
133		cachyOSMirrorlistPath = "/etc/pacman.d/cachyos-mirrorlist"
134		cachyOSMirrorlist     = "## CachyOS mirrorlist\nServer = https://mirror.cachyos.org/repo/$arch/$repo\n"
135	)
136
137	// Ensure the pacman keyring is initialized before any key operations.
138	if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--init"); err != nil {
139		return fmt.Errorf("failed to init pacman keyring on host: %w", err)
140	}
141
142	// Only fetch from keyserver if key is not already present.
143	if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--list-keys", cachyOSKeyID); err != nil {
144		if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--recv-keys",
145			cachyOSKeyID, "--keyserver", keyserver); err != nil {
146			return fmt.Errorf("failed to receive CachyOS key on host: %w", err)
147		}
148	}
149
150	// Local-sign the key so pacman trusts packages from this unofficial repo.
151	if _, err := h.cmdExec.Execute(ctx, "pacman-key", "--lsign-key", cachyOSKeyID); err != nil {
152		return fmt.Errorf("failed to sign CachyOS key on host: %w", err)
153	}
154
155	// Ensure /etc/pacman.d exists before writing the mirrorlist.
156	if err := h.fs.MkdirAll(cachyOSMirrorlistDir, 0755); err != nil {
157		return fmt.Errorf("failed to create pacman.d directory on host: %w", err)
158	}
159
160	// Write static mirrorlist — no versioned package URLs that can go stale.
161	if err := h.fs.WriteFile(cachyOSMirrorlistPath, []byte(cachyOSMirrorlist), 0644); err != nil {
162		return fmt.Errorf("failed to write host CachyOS mirrorlist: %w", err)
163	}
164
165	confBytes, err := h.fs.ReadFile(hostPacmanConf)
166	if err != nil {
167		return fmt.Errorf("failed to read host pacman.conf: %w", err)
168	}
169	conf := ensureCachyOSHostRepo(string(confBytes))
170	if err := h.fs.WriteFile(hostPacmanConf, []byte(conf), 0644); err != nil {
171		return fmt.Errorf("failed to write host pacman.conf: %w", err)
172	}
173
174	if _, err := h.cmdExec.Execute(ctx, "pacman", "-Sy", "--noconfirm"); err != nil {
175		return fmt.Errorf("failed to sync host pacman repos: %w", err)
176	}
177
178	h.logger.Info("CachyOS repo ready on host")
179	return nil
180}
181
182func ensureCachyOSHostRepo(conf string) string {
183	if strings.Contains(conf, "[cachyos]") {
184		return conf
185	}
186
187	conf = strings.TrimRight(conf, "\n")
188	if conf == "" {
189		return "[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n"
190	}
191
192	block := []string{
193		"# CachyOS repositories",
194		"[cachyos]",
195		"Include = /etc/pacman.d/cachyos-mirrorlist",
196		"",
197	}
198
199	lines := strings.Split(conf, "\n")
200	for i, line := range lines {
201		if strings.TrimSpace(line) == "[core]" {
202			out := make([]string, 0, len(lines)+len(block))
203			out = append(out, lines[:i]...)
204			out = append(out, block...)
205			out = append(out, lines[i:]...)
206			return strings.Join(out, "\n") + "\n"
207		}
208	}
209
210	// Fallback: append if [core] section is not found.
211	return conf + "\n[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n"
212}
213
214// loadBasePackages reads the base package list from file
215func (h *InstallBaseHandler) loadBasePackages() ([]string, error) {
216	// Read from install directory (downloaded during bootstrap)
217	packageFile := filepath.Join(config.DefaultInstallDir, config.BasePackagesFile)
218
219	content, err := h.fs.ReadFile(packageFile)
220	if err != nil {
221		return nil, fmt.Errorf("failed to read %s: %w", packageFile, err)
222	}
223
224	var packages []string
225	scanner := bufio.NewScanner(strings.NewReader(string(content)))
226
227	for scanner.Scan() {
228		line := strings.TrimSpace(scanner.Text())
229
230		// Skip comments and empty lines
231		if line == "" || strings.HasPrefix(line, "#") {
232			continue
233		}
234
235		packages = append(packages, line)
236	}
237
238	if err := scanner.Err(); err != nil {
239		return nil, fmt.Errorf("error reading package file: %w", err)
240	}
241
242	h.logger.Info("Loaded packages from file", "file", packageFile, "count", len(packages))
243	return packages, nil
244}

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