detached 81998aa

feat(post-install): install limine pacman hook for kernel upgrades

commit
81998aaeedae0351d8c5fec7f21769f229e02fe2
author
brice · · 2026-02-27 05:30 UTC
parent
5e43fec
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
88 RunPostBootScripts bool // Whether to run post-boot scripts
99 PlymouthTheme string // Plymouth theme to install (optional)
1010 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)
1112 }
@@ -8,4 +8,5 @@ type PostInstallCommand struct {
8 RunPostBootScripts bool // Whether to run post-boot scripts8 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-install10 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
104104 h.logger.Warn("Failed to tune pacman.conf", "error", err)
105105 }
106106
107+ if err := h.installLimineHook(cmd.MountPoint, cmd.TargetDisk); err != nil {
108+ h.logger.Warn("Failed to install limine hook", "error", err)
109+ }
110+
107111 // Final cleanup and verification
108112 h.logger.Info("Post-installation tasks completed")
109113 result.Success = true
@@ -230,6 +234,22 @@ func (h *PostInstallHandler) writeDankLinuxFlag(mountPoint string) error {
230234 return h.fs.WriteFile(flagPath, []byte(""), 0644)
231235 }
232236
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+
233253 func (h *PostInstallHandler) tunePacmanConfig(mountPoint string) error {
234254 confPath := filepath.Join(mountPoint, "etc", "pacman.conf")
235255 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 verification111 // Final cleanup and verification
108 h.logger.Info("Post-installation tasks completed")112 h.logger.Info("Post-installation tasks completed")
109 result.Success = true113 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) {
278278 }
279279 }
280280
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+
281346 func TestPostInstallHandler_Handle_Everything(t *testing.T) {
282347 ctrl := gomock.NewController(t)
283348 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
216216 PlymouthTheme: config.PlymouthThemeName,
217217 RunPostBootScripts: true,
218218 InstallDankLinux: formData.InstallDankLinux,
219+ TargetDisk: formData.TargetDisk,
219220 }
220221 if _, err := svc.RunPostInstall(ctx, postCmd); err != nil {
221222 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)

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