detached e32ab76

fix(post-install): comment out limine enrollment commands when limine-entry-tool is absent

commit
e32ab76217d2a90285f6da96da59653ce63d754e
author
brice · · 2026-03-02 05:56 UTC
parent
a3b57b9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +133 -0
M internal/application/handlers/post_install_handler.go +31 -0
@@ -217,9 +217,40 @@ func (h *PostInstallHandler) setupSnapperSync(ctx context.Context, mountPoint st
217217 if err := h.chrExec.ChrootSystemctl(ctx, h.logger.LogPath(), mountPoint, "enable", "limine-snapper-sync.service"); err != nil {
218218 return fmt.Errorf("failed to enable limine-snapper-sync.service: %w", err)
219219 }
220+ // Sanitize config: comment out enrollment commands when limine-entry-tool is absent
221+ if err := h.sanitizeLimineSnapperSyncConfig(mountPoint); err != nil {
222+ h.logger.Warn("Failed to sanitize limine-snapper-sync config", "error", err)
223+ }
220224 return nil
221225 }
222226
227+func (h *PostInstallHandler) sanitizeLimineSnapperSyncConfig(mountPoint string) error {
228+ confPath := filepath.Join(mountPoint, "etc", "limine-snapper-sync.conf")
229+ exists, err := h.fs.Exists(confPath)
230+ if err != nil || !exists {
231+ return err // config missing, nothing to sanitize
232+ }
233+
234+ // If the binary is present, enrollment workflow is intentionally configured — leave config alone
235+ enrollBin := filepath.Join(mountPoint, "usr", "bin", "limine-reset-enroll")
236+ if _, err := h.fs.Stat(enrollBin); err == nil {
237+ return nil
238+ }
239+
240+ b, err := h.fs.ReadFile(confPath)
241+ if err != nil {
242+ return fmt.Errorf("failed to read limine-snapper-sync.conf: %w", err)
243+ }
244+
245+ s := string(b)
246+ reBefore := regexp.MustCompile(`(?m)^COMMANDS_BEFORE_SAVE=.*$`)
247+ reAfter := regexp.MustCompile(`(?m)^COMMANDS_AFTER_SAVE=.*$`)
248+ s = reBefore.ReplaceAllString(s, `# COMMANDS_BEFORE_SAVE="" # disabled: limine-entry-tool not installed`)
249+ s = reAfter.ReplaceAllString(s, `# COMMANDS_AFTER_SAVE="" # disabled: limine-entry-tool not installed`)
250+
251+ return h.fs.WriteFile(confPath, []byte(s), 0644)
252+}
253+
223254 // limineDiskPlaceholder is the placeholder in limine-update.hook replaced at install time.
224255 const limineDiskPlaceholder = "DISK_PLACEHOLDER"
225256
@@ -217,9 +217,40 @@ func (h *PostInstallHandler) setupSnapperSync(ctx context.Context, mountPoint st
217 if err := h.chrExec.ChrootSystemctl(ctx, h.logger.LogPath(), mountPoint, "enable", "limine-snapper-sync.service"); err != nil {217 if err := h.chrExec.ChrootSystemctl(ctx, h.logger.LogPath(), mountPoint, "enable", "limine-snapper-sync.service"); err != nil {
218 return fmt.Errorf("failed to enable limine-snapper-sync.service: %w", err)218 return fmt.Errorf("failed to enable limine-snapper-sync.service: %w", err)
219 }219 }
220+ // Sanitize config: comment out enrollment commands when limine-entry-tool is absent
221+ if err := h.sanitizeLimineSnapperSyncConfig(mountPoint); err != nil {
222+ h.logger.Warn("Failed to sanitize limine-snapper-sync config", "error", err)
223+ }
220 return nil224 return nil
221 }225 }
222 226
227+func (h *PostInstallHandler) sanitizeLimineSnapperSyncConfig(mountPoint string) error {
228+ confPath := filepath.Join(mountPoint, "etc", "limine-snapper-sync.conf")
229+ exists, err := h.fs.Exists(confPath)
230+ if err != nil || !exists {
231+ return err // config missing, nothing to sanitize
232+ }
233+
234+ // If the binary is present, enrollment workflow is intentionally configured — leave config alone
235+ enrollBin := filepath.Join(mountPoint, "usr", "bin", "limine-reset-enroll")
236+ if _, err := h.fs.Stat(enrollBin); err == nil {
237+ return nil
238+ }
239+
240+ b, err := h.fs.ReadFile(confPath)
241+ if err != nil {
242+ return fmt.Errorf("failed to read limine-snapper-sync.conf: %w", err)
243+ }
244+
245+ s := string(b)
246+ reBefore := regexp.MustCompile(`(?m)^COMMANDS_BEFORE_SAVE=.*$`)
247+ reAfter := regexp.MustCompile(`(?m)^COMMANDS_AFTER_SAVE=.*$`)
248+ s = reBefore.ReplaceAllString(s, `# COMMANDS_BEFORE_SAVE="" # disabled: limine-entry-tool not installed`)
249+ s = reAfter.ReplaceAllString(s, `# COMMANDS_AFTER_SAVE="" # disabled: limine-entry-tool not installed`)
250+
251+ return h.fs.WriteFile(confPath, []byte(s), 0644)
252+}
253+
223 // limineDiskPlaceholder is the placeholder in limine-update.hook replaced at install time.254 // limineDiskPlaceholder is the placeholder in limine-update.hook replaced at install time.
224 const limineDiskPlaceholder = "DISK_PLACEHOLDER"255 const limineDiskPlaceholder = "DISK_PLACEHOLDER"
225 256
M internal/application/handlers/post_install_handler_test.go +102 -0
@@ -536,6 +536,108 @@ func TestPostInstallHandler_Handle_RunsVerification(t *testing.T) {
536536 }
537537 }
538538
539+func TestPostInstallHandler_SanitizeLimineSnapperSyncConfig(t *testing.T) {
540+ const mountPoint = "/mnt"
541+ const confPath = "/mnt/etc/limine-snapper-sync.conf"
542+ const enrollBin = "/mnt/usr/bin/limine-reset-enroll"
543+ const sampleConfig = `# limine-snapper-sync config
544+COMMANDS_BEFORE_SAVE="limine-reset-enroll"
545+COMMANDS_AFTER_SAVE="limine-enroll-config"
546+OTHER_OPTION="keep"
547+`
548+
549+ tests := []struct {
550+ name string
551+ confExists bool
552+ binStatErr error
553+ expectWrite bool
554+ checkContent func(t *testing.T, content string)
555+ }{
556+ {
557+ name: "binary missing config present — comments out enrollment lines",
558+ confExists: true,
559+ binStatErr: fmt.Errorf("no such file"),
560+ expectWrite: true,
561+ checkContent: func(t *testing.T, content string) {
562+ t.Helper()
563+ if strings.Contains(content, `COMMANDS_BEFORE_SAVE="limine-reset-enroll"`) {
564+ t.Error("expected COMMANDS_BEFORE_SAVE to be commented out")
565+ }
566+ if strings.Contains(content, `COMMANDS_AFTER_SAVE="limine-enroll-config"`) {
567+ t.Error("expected COMMANDS_AFTER_SAVE to be commented out")
568+ }
569+ if !strings.Contains(content, "# COMMANDS_BEFORE_SAVE=") {
570+ t.Error("expected commented COMMANDS_BEFORE_SAVE line")
571+ }
572+ if !strings.Contains(content, "# COMMANDS_AFTER_SAVE=") {
573+ t.Error("expected commented COMMANDS_AFTER_SAVE line")
574+ }
575+ if !strings.Contains(content, "disabled: limine-entry-tool not installed") {
576+ t.Error("expected comment suffix 'disabled: limine-entry-tool not installed'")
577+ }
578+ if !strings.Contains(content, `OTHER_OPTION="keep"`) {
579+ t.Error("expected OTHER_OPTION line to be preserved")
580+ }
581+ },
582+ },
583+ {
584+ name: "binary present — no modification",
585+ confExists: true,
586+ binStatErr: nil,
587+ expectWrite: false,
588+ },
589+ {
590+ name: "config absent — no modification",
591+ confExists: false,
592+ expectWrite: false,
593+ },
594+ }
595+
596+ for _, tc := range tests {
597+ t.Run(tc.name, func(t *testing.T) {
598+ ctrl := gomock.NewController(t)
599+ defer ctrl.Finish()
600+
601+ mockFS := mocks.NewMockFileSystem(ctrl)
602+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
603+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
604+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
605+ mockLogger := mocks.NewMockLogger(ctrl)
606+
607+ // conf exists check
608+ mockFS.EXPECT().Exists(gomock.Eq(confPath)).Return(tc.confExists, nil)
609+
610+ if tc.confExists {
611+ // binary stat check
612+ mockFS.EXPECT().Stat(gomock.Eq(enrollBin)).Return(nil, tc.binStatErr)
613+
614+ if tc.binStatErr != nil {
615+ // binary absent: expect ReadFile then WriteFile
616+ mockFS.EXPECT().ReadFile(gomock.Eq(confPath)).Return([]byte(sampleConfig), nil)
617+ if tc.expectWrite {
618+ mockFS.EXPECT().WriteFile(gomock.Eq(confPath), gomock.Any(), gomock.Any()).DoAndReturn(
619+ func(_ string, data []byte, _ interface{}) error {
620+ if tc.checkContent != nil {
621+ tc.checkContent(t, string(data))
622+ }
623+ return nil
624+ },
625+ )
626+ }
627+ }
628+ // binary present: no ReadFile, no WriteFile
629+ }
630+ // config absent: no further calls
631+
632+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
633+ err := handler.sanitizeLimineSnapperSyncConfig(mountPoint)
634+ if err != nil {
635+ t.Fatalf("expected no error, got %v", err)
636+ }
637+ })
638+ }
639+}
640+
539641 func TestPostInstallHandler_Handle_VerificationWarnsOnMissingFiles(t *testing.T) {
540642 ctrl := gomock.NewController(t)
541643 defer ctrl.Finish()
@@ -536,6 +536,108 @@ func TestPostInstallHandler_Handle_RunsVerification(t *testing.T) {
536 }536 }
537 }537 }
538 538
539+func TestPostInstallHandler_SanitizeLimineSnapperSyncConfig(t *testing.T) {
540+ const mountPoint = "/mnt"
541+ const confPath = "/mnt/etc/limine-snapper-sync.conf"
542+ const enrollBin = "/mnt/usr/bin/limine-reset-enroll"
543+ const sampleConfig = `# limine-snapper-sync config
544+COMMANDS_BEFORE_SAVE="limine-reset-enroll"
545+COMMANDS_AFTER_SAVE="limine-enroll-config"
546+OTHER_OPTION="keep"
547+`
548+
549+ tests := []struct {
550+ name string
551+ confExists bool
552+ binStatErr error
553+ expectWrite bool
554+ checkContent func(t *testing.T, content string)
555+ }{
556+ {
557+ name: "binary missing config present — comments out enrollment lines",
558+ confExists: true,
559+ binStatErr: fmt.Errorf("no such file"),
560+ expectWrite: true,
561+ checkContent: func(t *testing.T, content string) {
562+ t.Helper()
563+ if strings.Contains(content, `COMMANDS_BEFORE_SAVE="limine-reset-enroll"`) {
564+ t.Error("expected COMMANDS_BEFORE_SAVE to be commented out")
565+ }
566+ if strings.Contains(content, `COMMANDS_AFTER_SAVE="limine-enroll-config"`) {
567+ t.Error("expected COMMANDS_AFTER_SAVE to be commented out")
568+ }
569+ if !strings.Contains(content, "# COMMANDS_BEFORE_SAVE=") {
570+ t.Error("expected commented COMMANDS_BEFORE_SAVE line")
571+ }
572+ if !strings.Contains(content, "# COMMANDS_AFTER_SAVE=") {
573+ t.Error("expected commented COMMANDS_AFTER_SAVE line")
574+ }
575+ if !strings.Contains(content, "disabled: limine-entry-tool not installed") {
576+ t.Error("expected comment suffix 'disabled: limine-entry-tool not installed'")
577+ }
578+ if !strings.Contains(content, `OTHER_OPTION="keep"`) {
579+ t.Error("expected OTHER_OPTION line to be preserved")
580+ }
581+ },
582+ },
583+ {
584+ name: "binary present — no modification",
585+ confExists: true,
586+ binStatErr: nil,
587+ expectWrite: false,
588+ },
589+ {
590+ name: "config absent — no modification",
591+ confExists: false,
592+ expectWrite: false,
593+ },
594+ }
595+
596+ for _, tc := range tests {
597+ t.Run(tc.name, func(t *testing.T) {
598+ ctrl := gomock.NewController(t)
599+ defer ctrl.Finish()
600+
601+ mockFS := mocks.NewMockFileSystem(ctrl)
602+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
603+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
604+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
605+ mockLogger := mocks.NewMockLogger(ctrl)
606+
607+ // conf exists check
608+ mockFS.EXPECT().Exists(gomock.Eq(confPath)).Return(tc.confExists, nil)
609+
610+ if tc.confExists {
611+ // binary stat check
612+ mockFS.EXPECT().Stat(gomock.Eq(enrollBin)).Return(nil, tc.binStatErr)
613+
614+ if tc.binStatErr != nil {
615+ // binary absent: expect ReadFile then WriteFile
616+ mockFS.EXPECT().ReadFile(gomock.Eq(confPath)).Return([]byte(sampleConfig), nil)
617+ if tc.expectWrite {
618+ mockFS.EXPECT().WriteFile(gomock.Eq(confPath), gomock.Any(), gomock.Any()).DoAndReturn(
619+ func(_ string, data []byte, _ interface{}) error {
620+ if tc.checkContent != nil {
621+ tc.checkContent(t, string(data))
622+ }
623+ return nil
624+ },
625+ )
626+ }
627+ }
628+ // binary present: no ReadFile, no WriteFile
629+ }
630+ // config absent: no further calls
631+
632+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
633+ err := handler.sanitizeLimineSnapperSyncConfig(mountPoint)
634+ if err != nil {
635+ t.Fatalf("expected no error, got %v", err)
636+ }
637+ })
638+ }
639+}
640+
539 func TestPostInstallHandler_Handle_VerificationWarnsOnMissingFiles(t *testing.T) {641 func TestPostInstallHandler_Handle_VerificationWarnsOnMissingFiles(t *testing.T) {
540 ctrl := gomock.NewController(t)642 ctrl := gomock.NewController(t)
541 defer ctrl.Finish()643 defer ctrl.Finish()

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