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}