feat(post-install): install limine pacman hook for kernel upgrades
- commit
- 81998aaeedae0351d8c5fec7f21769f229e02fe2
- author
- brice · · 2026-02-27 05:30 UTC
- parent
- 5e43fec
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +102 -0
A install/configs/limine-update.hook +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | [Trigger] | |
| 2 | + | Operation = Install | |
| 3 | + | Operation = Upgrade | |
| 4 | + | Type = Package | |
| 5 | + | Target = linux | |
| 6 | + | Target = linux-lts | |
| 7 | + | Target = linux-zen | |
| 8 | + | Target = linux-hardened | |
| 9 | + | Target = linux-cachyos | |
| 10 | + | ||
| 11 | + | [Action] | |
| 12 | + | Description = Updating Limine bootloader after kernel upgrade... | |
| 13 | + | When = PostTransaction | |
| 14 | + | Exec = /usr/bin/limine bios-install DISK_PLACEHOLDER | |
| 15 | + | Depends = limine | |
| @@ -0,0 +1,15 @@ | |||||
| 1 | + | [Trigger] | |||
| 2 | + | Operation = Install | |||
| 3 | + | Operation = Upgrade | |||
| 4 | + | Type = Package | |||
| 5 | + | Target = linux | |||
| 6 | + | Target = linux-lts | |||
| 7 | + | Target = linux-zen | |||
| 8 | + | Target = linux-hardened | |||
| 9 | + | Target = linux-cachyos | |||
| 10 | + | ||||
| 11 | + | [Action] | |||
| 12 | + | Description = Updating Limine bootloader after kernel upgrade... | |||
| 13 | + | When = PostTransaction | |||
| 14 | + | Exec = /usr/bin/limine bios-install DISK_PLACEHOLDER | |||
| 15 | + | Depends = limine | |||
M internal/application/commands/post_install.go +1 -0
| @@ -8,4 +8,5 @@ type PostInstallCommand struct { | |||
| 8 | 8 | RunPostBootScripts bool // Whether to run post-boot scripts | |
| 9 | 9 | PlymouthTheme string // Plymouth theme to install (optional) | |
| 10 | 10 | InstallDankLinux bool // Whether to write the Dank Linux flag file for first-boot auto-install | |
| 11 | + | TargetDisk string // Target disk for bootloader hook (e.g. /dev/sda) | |
| 11 | 12 | } | |
| @@ -8,4 +8,5 @@ type PostInstallCommand struct { | |||||
| 8 | RunPostBootScripts bool // Whether to run post-boot scripts | 8 | RunPostBootScripts bool // Whether to run post-boot scripts | ||
| 9 | PlymouthTheme string // Plymouth theme to install (optional) | 9 | PlymouthTheme string // Plymouth theme to install (optional) | ||
| 10 | InstallDankLinux bool // Whether to write the Dank Linux flag file for first-boot auto-install | 10 | InstallDankLinux bool // Whether to write the Dank Linux flag file for first-boot auto-install | ||
| 11 | + | TargetDisk string // Target disk for bootloader hook (e.g. /dev/sda) | |||
| 11 | } | 12 | } | ||
M internal/application/handlers/post_install_handler.go +20 -0
| @@ -104,6 +104,10 @@ func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstal | |||
| 104 | 104 | h.logger.Warn("Failed to tune pacman.conf", "error", err) | |
| 105 | 105 | } | |
| 106 | 106 | ||
| 107 | + | if err := h.installLimineHook(cmd.MountPoint, cmd.TargetDisk); err != nil { | |
| 108 | + | h.logger.Warn("Failed to install limine hook", "error", err) | |
| 109 | + | } | |
| 110 | + | ||
| 107 | 111 | // Final cleanup and verification | |
| 108 | 112 | h.logger.Info("Post-installation tasks completed") | |
| 109 | 113 | result.Success = true | |
| @@ -230,6 +234,22 @@ func (h *PostInstallHandler) writeDankLinuxFlag(mountPoint string) error { | |||
| 230 | 234 | return h.fs.WriteFile(flagPath, []byte(""), 0644) | |
| 231 | 235 | } | |
| 232 | 236 | ||
| 237 | + | func (h *PostInstallHandler) installLimineHook(mountPoint, targetDisk string) error { | |
| 238 | + | hooksDir := filepath.Join(mountPoint, "etc", "pacman.d", "hooks") | |
| 239 | + | if err := h.fs.MkdirAll(hooksDir, 0755); err != nil { | |
| 240 | + | return fmt.Errorf("failed to create hooks dir: %w", err) | |
| 241 | + | } | |
| 242 | + | content, err := h.tryReadLocal("install/configs/limine-update.hook") | |
| 243 | + | if err != nil { | |
| 244 | + | content, err = h.downloadTemplate("install/configs/limine-update.hook") | |
| 245 | + | if err != nil { | |
| 246 | + | return fmt.Errorf("failed to get limine hook template: %w", err) | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | hookContent := strings.ReplaceAll(string(content), "DISK_PLACEHOLDER", targetDisk) | |
| 250 | + | return h.fs.WriteFile(filepath.Join(hooksDir, "limine-update.hook"), []byte(hookContent), 0644) | |
| 251 | + | } | |
| 252 | + | ||
| 233 | 253 | func (h *PostInstallHandler) tunePacmanConfig(mountPoint string) error { | |
| 234 | 254 | confPath := filepath.Join(mountPoint, "etc", "pacman.conf") | |
| 235 | 255 | content, err := h.fs.ReadFile(confPath) | |
| @@ -104,6 +104,10 @@ func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstal | |||||
| 104 | h.logger.Warn("Failed to tune pacman.conf", "error", err) | 104 | h.logger.Warn("Failed to tune pacman.conf", "error", err) | ||
| 105 | } | 105 | } | ||
| 106 | 106 | ||||
| 107 | + | if err := h.installLimineHook(cmd.MountPoint, cmd.TargetDisk); err != nil { | |||
| 108 | + | h.logger.Warn("Failed to install limine hook", "error", err) | |||
| 109 | + | } | |||
| 110 | + | ||||
| 107 | // Final cleanup and verification | 111 | // Final cleanup and verification | ||
| 108 | h.logger.Info("Post-installation tasks completed") | 112 | h.logger.Info("Post-installation tasks completed") | ||
| 109 | result.Success = true | 113 | result.Success = true | ||
| @@ -230,6 +234,22 @@ func (h *PostInstallHandler) writeDankLinuxFlag(mountPoint string) error { | |||||
| 230 | return h.fs.WriteFile(flagPath, []byte(""), 0644) | 234 | return h.fs.WriteFile(flagPath, []byte(""), 0644) | ||
| 231 | } | 235 | } | ||
| 232 | 236 | ||||
| 237 | + | func (h *PostInstallHandler) installLimineHook(mountPoint, targetDisk string) error { | |||
| 238 | + | hooksDir := filepath.Join(mountPoint, "etc", "pacman.d", "hooks") | |||
| 239 | + | if err := h.fs.MkdirAll(hooksDir, 0755); err != nil { | |||
| 240 | + | return fmt.Errorf("failed to create hooks dir: %w", err) | |||
| 241 | + | } | |||
| 242 | + | content, err := h.tryReadLocal("install/configs/limine-update.hook") | |||
| 243 | + | if err != nil { | |||
| 244 | + | content, err = h.downloadTemplate("install/configs/limine-update.hook") | |||
| 245 | + | if err != nil { | |||
| 246 | + | return fmt.Errorf("failed to get limine hook template: %w", err) | |||
| 247 | + | } | |||
| 248 | + | } | |||
| 249 | + | hookContent := strings.ReplaceAll(string(content), "DISK_PLACEHOLDER", targetDisk) | |||
| 250 | + | return h.fs.WriteFile(filepath.Join(hooksDir, "limine-update.hook"), []byte(hookContent), 0644) | |||
| 251 | + | } | |||
| 252 | + | ||||
| 233 | func (h *PostInstallHandler) tunePacmanConfig(mountPoint string) error { | 253 | func (h *PostInstallHandler) tunePacmanConfig(mountPoint string) error { | ||
| 234 | confPath := filepath.Join(mountPoint, "etc", "pacman.conf") | 254 | confPath := filepath.Join(mountPoint, "etc", "pacman.conf") | ||
| 235 | content, err := h.fs.ReadFile(confPath) | 255 | content, err := h.fs.ReadFile(confPath) | ||
M internal/application/handlers/post_install_handler_test.go +65 -0
| @@ -278,6 +278,71 @@ func TestPostInstallHandler_Handle_TunesPacman(t *testing.T) { | |||
| 278 | 278 | } | |
| 279 | 279 | } | |
| 280 | 280 | ||
| 281 | + | func TestPostInstallHandler_Handle_InstallsLimineHook(t *testing.T) { | |
| 282 | + | ctrl := gomock.NewController(t) | |
| 283 | + | defer ctrl.Finish() | |
| 284 | + | ||
| 285 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |
| 286 | + | mockHTTP := mocks.NewMockHTTPClient(ctrl) | |
| 287 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |
| 288 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |
| 289 | + | mockLogger := mocks.NewMockLogger(ctrl) | |
| 290 | + | ||
| 291 | + | hooksDir := "/mnt/etc/pacman.d/hooks" | |
| 292 | + | hookFile := hooksDir + "/limine-update.hook" | |
| 293 | + | targetDisk := "/dev/sda" | |
| 294 | + | hookTemplate := "Exec = /usr/bin/limine bios-install DISK_PLACEHOLDER" | |
| 295 | + | ||
| 296 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |
| 297 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |
| 298 | + | mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes() | |
| 299 | + | mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes() | |
| 300 | + | mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes() | |
| 301 | + | mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | |
| 302 | + | mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte(hookTemplate)), nil).AnyTimes() | |
| 303 | + | mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | |
| 304 | + | ||
| 305 | + | hookWritten := false | |
| 306 | + | mockFS.EXPECT().WriteFile(gomock.Eq(hookFile), gomock.Any(), gomock.Any()).DoAndReturn( | |
| 307 | + | func(path string, data []byte, perm interface{}) error { | |
| 308 | + | hookWritten = true | |
| 309 | + | content := string(data) | |
| 310 | + | if strings.Contains(content, "DISK_PLACEHOLDER") { | |
| 311 | + | t.Errorf("hook content should not contain DISK_PLACEHOLDER, got: %s", content) | |
| 312 | + | } | |
| 313 | + | if !strings.Contains(content, targetDisk) { | |
| 314 | + | t.Errorf("hook content should contain target disk %s, got: %s", targetDisk, content) | |
| 315 | + | } | |
| 316 | + | return nil | |
| 317 | + | }, | |
| 318 | + | ).AnyTimes() | |
| 319 | + | mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | |
| 320 | + | ||
| 321 | + | handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev") | |
| 322 | + | ||
| 323 | + | cmd := commands.PostInstallCommand{ | |
| 324 | + | MountPoint: "/mnt", | |
| 325 | + | Username: "testuser", | |
| 326 | + | RunPostBootScripts: false, | |
| 327 | + | PlymouthTheme: "", | |
| 328 | + | TargetDisk: targetDisk, | |
| 329 | + | } | |
| 330 | + | ||
| 331 | + | result, err := handler.Handle(context.Background(), cmd) | |
| 332 | + | ||
| 333 | + | if err != nil { | |
| 334 | + | t.Fatalf("expected no error, got %v", err) | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | if !result.Success { | |
| 338 | + | t.Error("expected success") | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | if !hookWritten { | |
| 342 | + | t.Error("expected limine hook file to be written") | |
| 343 | + | } | |
| 344 | + | } | |
| 345 | + | ||
| 281 | 346 | func TestPostInstallHandler_Handle_Everything(t *testing.T) { | |
| 282 | 347 | ctrl := gomock.NewController(t) | |
| 283 | 348 | defer ctrl.Finish() | |
| @@ -278,6 +278,71 @@ func TestPostInstallHandler_Handle_TunesPacman(t *testing.T) { | |||||
| 278 | } | 278 | } | ||
| 279 | } | 279 | } | ||
| 280 | 280 | ||||
| 281 | + | func TestPostInstallHandler_Handle_InstallsLimineHook(t *testing.T) { | |||
| 282 | + | ctrl := gomock.NewController(t) | |||
| 283 | + | defer ctrl.Finish() | |||
| 284 | + | ||||
| 285 | + | mockFS := mocks.NewMockFileSystem(ctrl) | |||
| 286 | + | mockHTTP := mocks.NewMockHTTPClient(ctrl) | |||
| 287 | + | mockChrExec := mocks.NewMockChrootExecutor(ctrl) | |||
| 288 | + | mockScriptExec := mocks.NewMockScriptExecutor(ctrl) | |||
| 289 | + | mockLogger := mocks.NewMockLogger(ctrl) | |||
| 290 | + | ||||
| 291 | + | hooksDir := "/mnt/etc/pacman.d/hooks" | |||
| 292 | + | hookFile := hooksDir + "/limine-update.hook" | |||
| 293 | + | targetDisk := "/dev/sda" | |||
| 294 | + | hookTemplate := "Exec = /usr/bin/limine bios-install DISK_PLACEHOLDER" | |||
| 295 | + | ||||
| 296 | + | mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 297 | + | mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() | |||
| 298 | + | mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes() | |||
| 299 | + | mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes() | |||
| 300 | + | mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes() | |||
| 301 | + | mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | |||
| 302 | + | mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte(hookTemplate)), nil).AnyTimes() | |||
| 303 | + | mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | |||
| 304 | + | ||||
| 305 | + | hookWritten := false | |||
| 306 | + | mockFS.EXPECT().WriteFile(gomock.Eq(hookFile), gomock.Any(), gomock.Any()).DoAndReturn( | |||
| 307 | + | func(path string, data []byte, perm interface{}) error { | |||
| 308 | + | hookWritten = true | |||
| 309 | + | content := string(data) | |||
| 310 | + | if strings.Contains(content, "DISK_PLACEHOLDER") { | |||
| 311 | + | t.Errorf("hook content should not contain DISK_PLACEHOLDER, got: %s", content) | |||
| 312 | + | } | |||
| 313 | + | if !strings.Contains(content, targetDisk) { | |||
| 314 | + | t.Errorf("hook content should contain target disk %s, got: %s", targetDisk, content) | |||
| 315 | + | } | |||
| 316 | + | return nil | |||
| 317 | + | }, | |||
| 318 | + | ).AnyTimes() | |||
| 319 | + | mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() | |||
| 320 | + | ||||
| 321 | + | handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev") | |||
| 322 | + | ||||
| 323 | + | cmd := commands.PostInstallCommand{ | |||
| 324 | + | MountPoint: "/mnt", | |||
| 325 | + | Username: "testuser", | |||
| 326 | + | RunPostBootScripts: false, | |||
| 327 | + | PlymouthTheme: "", | |||
| 328 | + | TargetDisk: targetDisk, | |||
| 329 | + | } | |||
| 330 | + | ||||
| 331 | + | result, err := handler.Handle(context.Background(), cmd) | |||
| 332 | + | ||||
| 333 | + | if err != nil { | |||
| 334 | + | t.Fatalf("expected no error, got %v", err) | |||
| 335 | + | } | |||
| 336 | + | ||||
| 337 | + | if !result.Success { | |||
| 338 | + | t.Error("expected success") | |||
| 339 | + | } | |||
| 340 | + | ||||
| 341 | + | if !hookWritten { | |||
| 342 | + | t.Error("expected limine hook file to be written") | |||
| 343 | + | } | |||
| 344 | + | } | |||
| 345 | + | ||||
| 281 | func TestPostInstallHandler_Handle_Everything(t *testing.T) { | 346 | func TestPostInstallHandler_Handle_Everything(t *testing.T) { | ||
| 282 | ctrl := gomock.NewController(t) | 347 | ctrl := gomock.NewController(t) | ||
| 283 | defer ctrl.Finish() | 348 | defer ctrl.Finish() | ||
M internal/interfaces/tui/handlers/install_handler.go +1 -0
| @@ -216,6 +216,7 @@ func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd | |||
| 216 | 216 | PlymouthTheme: config.PlymouthThemeName, | |
| 217 | 217 | RunPostBootScripts: true, | |
| 218 | 218 | InstallDankLinux: formData.InstallDankLinux, | |
| 219 | + | TargetDisk: formData.TargetDisk, | |
| 219 | 220 | } | |
| 220 | 221 | if _, err := svc.RunPostInstall(ctx, postCmd); err != nil { | |
| 221 | 222 | logger.Error("Post-installation failed", "error", err) | |
| @@ -216,6 +216,7 @@ func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd | |||||
| 216 | PlymouthTheme: config.PlymouthThemeName, | 216 | PlymouthTheme: config.PlymouthThemeName, | ||
| 217 | RunPostBootScripts: true, | 217 | RunPostBootScripts: true, | ||
| 218 | InstallDankLinux: formData.InstallDankLinux, | 218 | InstallDankLinux: formData.InstallDankLinux, | ||
| 219 | + | TargetDisk: formData.TargetDisk, | |||
| 219 | } | 220 | } | ||
| 220 | if _, err := svc.RunPostInstall(ctx, postCmd); err != nil { | 221 | if _, err := svc.RunPostInstall(ctx, postCmd); err != nil { | ||
| 221 | logger.Error("Post-installation failed", "error", err) | 222 | logger.Error("Post-installation failed", "error", err) | ||