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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 217 | 217 | if err := h.chrExec.ChrootSystemctl(ctx, h.logger.LogPath(), mountPoint, "enable", "limine-snapper-sync.service"); err != nil { | |
| 218 | 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 | 224 | 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 | 254 | // limineDiskPlaceholder is the placeholder in limine-update.hook replaced at install time. | |
| 224 | 255 | const limineDiskPlaceholder = "DISK_PLACEHOLDER" | |
| 225 | 256 | ||
| @@ -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 nil | 224 | 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) { | |||
| 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 | 641 | func TestPostInstallHandler_Handle_VerificationWarnsOnMissingFiles(t *testing.T) { | |
| 540 | 642 | ctrl := gomock.NewController(t) | |
| 541 | 643 | 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() | ||