1package config
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "strings"
8)
9
10const (
11 DefaultConfigPath = "/var/log/archup-install.conf"
12 DefaultLogPath = "/var/log/archup-install.log"
13
14 // Install paths
15 DefaultInstallDir = "/tmp/archup-install"
16 DefaultInstallRepoDir = "/tmp/archup-install/repo"
17 DefaultInstallPath = ".local/share/archup/install"
18 BasePackagesFile = "base.packages"
19 ExtraPackagesFile = "extra.packages"
20 LimineConfigTemplate = "configs/limine.conf.template"
21)
22
23// Encryption types
24const (
25 EncryptionNone = "none"
26 EncryptionLUKS = "luks"
27 EncryptionLUKSLVM = "luks-lvm"
28)
29
30// Bootloader types
31const (
32 BootloaderLimine = "limine"
33)
34
35// Kernel choices
36const (
37 KernelLinux = "linux"
38 KernelLinuxZen = "linux-zen"
39 KernelLinuxLTS = "linux-lts"
40 KernelLinuxHardened = "linux-hardened"
41 KernelLinuxCachyOS = "linux-cachyos"
42)
43
44// System paths
45const (
46 PathMnt = "/mnt"
47)
48
49// Mkinitcpio hooks
50const (
51 MkinitcpioHooksPlymouth = "HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block plymouth filesystems fsck)"
52 MkinitcpioHooksEncrypted = "HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block plymouth encrypt filesystems fsck)"
53)
54
55// Kernel parameters
56const (
57 KernelParamsQuiet = "quiet splash loglevel=3 rd.udev.log_priority=3 systemd.show_status=auto"
58)
59
60// Limine configuration
61const (
62 LimineColor = "6"
63)
64
65// UEFI boot entry
66const (
67 UEFIBootLabel = "Arch Linux"
68 UEFIBootLoader = "\\EFI\\limine\\BOOTX64.EFI"
69)
70
71// PostInstall paths
72const (
73 PathMntPlymouthThemes = "/mnt/usr/share/plymouth/themes"
74 PathMntEtcPacmanDHooks = "/mnt/etc/pacman.d/hooks"
75 PathBootLimineConf = "/boot/limine.conf"
76 PlymouthThemeName = "archup"
77)
78
79// Zram configuration
80const (
81 ZramConfigContent = `[zram0]
82zram-size = min(ram / 2, 4096)
83compression-algorithm = zstd
84`
85 ZramSysctlContent = `vm.swappiness = 180
86vm.watermark_boost_factor = 0
87vm.watermark_scale_factor = 125
88vm.page-cluster = 0
89`
90)
91
92// Zram file names
93const (
94 FileZramGenerator = "zram-generator.conf"
95 FileSysctlZramParams = "99-vm-zram-parameters.conf"
96)
97
98// Sudoers configuration
99const (
100 SudoersWheelContent = "%wheel ALL=(ALL:ALL) ALL\n"
101 SudoersWheelPerms = 0440
102)
103
104// Plymouth files
105var PlymouthFiles = []string{
106 "archup.plymouth",
107 "archup.script",
108 "logo.png",
109 "bullet.png",
110 "entry.png",
111 "lock.png",
112 "progress_bar.png",
113 "progress_box.png",
114}
115
116// Post-boot paths and files
117const (
118 PathMntPostBoot = "/mnt/usr/local/share/archup/post-boot"
119 PathMntSystemdSystem = "/mnt/etc/systemd/system"
120 PostBootServiceName = "archup-first-boot.service"
121)
122
123// Post-boot script files to download
124var PostBootScripts = []string{
125 "all.sh",
126 "snapper.sh",
127 "firewalld.sh",
128 "ssh-keygen.sh",
129 "blesh.sh",
130 "cli-tools.sh",
131 "dms-opt-in.sh",
132}
133
134// Post-boot service template URL path
135const PostBootServiceTemplate = "install/mandatory/post-boot/archup-first-boot.service"
136
137// Config holds the installation configuration
138// This mirrors the shell script's config format for compatibility
139type Config struct {
140 // Preflight
141 Hostname string
142 Username string
143 UserPassword string
144 RootPassword string
145 Email string // Optional, for git and SSH configuration
146 Keymap string
147 Timezone string
148 Locale string
149 Bootloader string // "limine" (only supported bootloader)
150 EncryptionType string // "none", "luks", or "luks-lvm"
151 EncryptPassword string // Separate encryption password if different from user password
152
153 // Form-only fields (not persisted)
154 ConfirmPassword string // Temporary field for password confirmation
155 UseSamePasswordForEncryption bool // Checkbox state for using same password for encryption
156
157 // Partitioning
158 TargetDisk string
159 BootPartition string
160 RootPartition string
161 SwapPartition string
162 EFIPartition string
163 CryptDevice string // /dev/mapper/cryptroot if encrypted
164
165 // Base
166 KernelChoice string // "linux", "linux-zen", "linux-lts", "linux-hardened"
167 Microcode string // "intel-ucode" or "amd-ucode"
168 CPUVendor string // "Intel", "AMD", or "Unknown"
169 AMDPState string // "active", "passive", "guided", or empty
170
171 // Config
172 NetworkManager string // "NetworkManager" by default
173
174 // Repos
175 AURHelper string // "paru" or "yay"
176 EnableMultilib bool
177
178 // Paths
179 ConfigPath string
180 LogPath string
181 InstallPath string // ~/.local/share/archup/install
182 RepoURL string
183 RawURL string
184}
185
186// NewConfig creates a new Config with sensible defaults.
187// version determines which git ref to use for downloads and bootstrap assets.
188// ENV=dev forces the dev ref regardless of the version string.
189func NewConfig(version string) *Config {
190 ref := bootstrapRef(version)
191
192 return &Config{
193 Hostname: "arch",
194 Locale: "en_US.UTF-8",
195 Timezone: "UTC",
196 Keymap: "us",
197 Bootloader: "limine",
198 EncryptionType: "none",
199 KernelChoice: "linux",
200 NetworkManager: "NetworkManager",
201 AURHelper: "paru",
202 EnableMultilib: true,
203 UseSamePasswordForEncryption: true, // Default to using same password for encryption
204 ConfigPath: DefaultConfigPath,
205 LogPath: DefaultLogPath,
206 RepoURL: "https://github.com/bnema/archup",
207 RawURL: fmt.Sprintf("https://raw.githubusercontent.com/bnema/archup/%s", ref),
208 }
209}
210
211func bootstrapRef(version string) string {
212 if os.Getenv("ENV") == "dev" {
213 return "dev"
214 }
215
216 version = strings.TrimSpace(version)
217 switch {
218 case version == "" || version == "dev":
219 return "dev"
220 case strings.Contains(version, "-dev"):
221 return "dev"
222 case strings.Contains(version, "-next"):
223 return "dev"
224 default:
225 if !strings.HasPrefix(version, "v") {
226 return "v" + version
227 }
228 return version
229 }
230}
231
232// Branch returns the git ref to use for asset downloads and repo cloning.
233func (c *Config) Branch() string {
234 rawURL := strings.TrimSpace(c.RawURL)
235 if rawURL == "" {
236 return "main"
237 }
238 // Extract branch from RawURL: "https://raw.githubusercontent.com/bnema/archup/<branch>"
239 parts := strings.Split(rawURL, "/")
240 last := parts[len(parts)-1]
241 if last == "" {
242 return "main"
243 }
244 return last
245}
246
247// Load reads configuration from a shell-compatible config file
248func Load(path string, version string) (*Config, error) {
249 cfg := NewConfig(version)
250 cfg.ConfigPath = path
251
252 file, err := os.Open(path)
253 if err != nil {
254 if os.IsNotExist(err) {
255 return cfg, nil // Return defaults if file doesn't exist
256 }
257 return nil, fmt.Errorf("failed to open config: %w", err)
258 }
259
260 scanner := bufio.NewScanner(file)
261 for scanner.Scan() {
262 line := strings.TrimSpace(scanner.Text())
263
264 // Skip comments and empty lines
265 if line == "" || strings.HasPrefix(line, "#") {
266 continue
267 }
268
269 // Parse KEY=VALUE format
270 parts := strings.SplitN(line, "=", 2)
271 if len(parts) != 2 {
272 continue
273 }
274
275 key := strings.TrimSpace(parts[0])
276 value := strings.Trim(strings.TrimSpace(parts[1]), `"'`) // Remove quotes
277
278 cfg.setValue(key, value)
279 }
280
281 if err := scanner.Err(); err != nil {
282 return nil, fmt.Errorf("failed to read config: %w", err)
283 }
284
285 if err := file.Close(); err != nil {
286 return nil, fmt.Errorf("failed to close config file: %w", err)
287 }
288
289 return cfg, nil
290}
291
292// Save writes configuration to a shell-compatible config file
293func (c *Config) Save() error {
294 file, err := os.Create(c.ConfigPath)
295 if err != nil {
296 return fmt.Errorf("failed to create config: %w", err)
297 }
298 // Set restrictive permissions (600) since config contains passwords
299 if err := os.Chmod(c.ConfigPath, 0600); err != nil {
300 return fmt.Errorf("failed to set config permissions: %w", err)
301 }
302
303 writer := bufio.NewWriter(file)
304
305 // Write header
306 if _, err := fmt.Fprintln(writer, "# ArchUp Installation Configuration"); err != nil {
307 return fmt.Errorf("failed to write config header: %w", err)
308 }
309 if _, err := fmt.Fprintln(writer, "# This file is compatible with shell scripts"); err != nil {
310 return fmt.Errorf("failed to write config header: %w", err)
311 }
312 if _, err := fmt.Fprintln(writer); err != nil {
313 return fmt.Errorf("failed to write config header: %w", err)
314 }
315
316 // Write all values
317 entries := []struct {
318 key string
319 value string
320 }{
321 {"ARCHUP_HOSTNAME", c.Hostname},
322 {"ARCHUP_USERNAME", c.Username},
323 {"ARCHUP_USER_PASSWORD", c.UserPassword},
324 {"ARCHUP_ROOT_PASSWORD", c.RootPassword},
325 {"ARCHUP_KEYMAP", c.Keymap},
326 {"ARCHUP_TIMEZONE", c.Timezone},
327 {"ARCHUP_LOCALE", c.Locale},
328 {"ARCHUP_BOOTLOADER", c.Bootloader},
329 {"ARCHUP_ENCRYPTION", c.EncryptionType},
330 {"ARCHUP_ENCRYPTION_PASSWORD", c.EncryptPassword},
331 {"ARCHUP_TARGET_DISK", c.TargetDisk},
332 {"ARCHUP_BOOT_PARTITION", c.BootPartition},
333 {"ARCHUP_ROOT_PARTITION", c.RootPartition},
334 {"ARCHUP_SWAP_PARTITION", c.SwapPartition},
335 {"ARCHUP_EFI_PARTITION", c.EFIPartition},
336 {"ARCHUP_KERNEL", c.KernelChoice},
337 {"ARCHUP_AMD_PSTATE", c.AMDPState},
338 {"ARCHUP_NETWORK_MANAGER", c.NetworkManager},
339 {"ARCHUP_AUR_HELPER", c.AURHelper},
340 {"ARCHUP_ENABLE_MULTILIB", boolToString(c.EnableMultilib)},
341 }
342
343 for _, entry := range entries {
344 if entry.value != "" {
345 if _, err := fmt.Fprintf(writer, "%s=\"%s\"\n", entry.key, entry.value); err != nil {
346 return fmt.Errorf("failed to write config entry: %w", err)
347 }
348 }
349 }
350
351 if err := writer.Flush(); err != nil {
352 return fmt.Errorf("failed to flush config writer: %w", err)
353 }
354
355 if err := file.Close(); err != nil {
356 return fmt.Errorf("failed to close config file: %w", err)
357 }
358
359 return nil
360}
361
362// setValue sets a config value based on key name
363func (c *Config) setValue(key, value string) {
364 switch key {
365 case "ARCHUP_HOSTNAME":
366 c.Hostname = value
367 case "ARCHUP_USERNAME":
368 c.Username = value
369 case "ARCHUP_USER_PASSWORD":
370 c.UserPassword = value
371 case "ARCHUP_ROOT_PASSWORD":
372 c.RootPassword = value
373 case "ARCHUP_KEYMAP":
374 c.Keymap = value
375 case "ARCHUP_TIMEZONE":
376 c.Timezone = value
377 case "ARCHUP_LOCALE":
378 c.Locale = value
379 case "ARCHUP_BOOTLOADER":
380 c.Bootloader = value
381 case "ARCHUP_ENCRYPTION":
382 c.EncryptionType = value
383 case "ARCHUP_ENCRYPTION_PASSWORD":
384 c.EncryptPassword = value
385 case "ARCHUP_TARGET_DISK":
386 c.TargetDisk = value
387 case "ARCHUP_BOOT_PARTITION":
388 c.BootPartition = value
389 case "ARCHUP_ROOT_PARTITION":
390 c.RootPartition = value
391 case "ARCHUP_SWAP_PARTITION":
392 c.SwapPartition = value
393 case "ARCHUP_EFI_PARTITION":
394 c.EFIPartition = value
395 case "ARCHUP_KERNEL":
396 c.KernelChoice = value
397 case "ARCHUP_AMD_PSTATE":
398 c.AMDPState = value
399 case "ARCHUP_NETWORK_MANAGER":
400 c.NetworkManager = value
401 case "ARCHUP_AUR_HELPER":
402 c.AURHelper = value
403 case "ARCHUP_ENABLE_MULTILIB":
404 c.EnableMultilib = stringToBool(value)
405 }
406}
407
408func boolToString(b bool) string {
409 if b {
410 return "true"
411 }
412 return "false"
413}
414
415func stringToBool(s string) bool {
416 return s == "true" || s == "1" || s == "yes"
417}