detached 5e43fec

feat(post-install): tune pacman.conf with Color, ParallelDownloads, ILoveCandy

commit
5e43feca70aadc38cab33748587b1175945c5e10
author
brice · · 2026-02-27 05:29 UTC
parent
5214853
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +87 -0
M internal/application/handlers/post_install_handler.go +21 -0
@@ -100,6 +100,10 @@ func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstal
100100 h.logger.Warn("Failed to install Limine logo", "error", err)
101101 }
102102
103+ if err := h.tunePacmanConfig(cmd.MountPoint); err != nil {
104+ h.logger.Warn("Failed to tune pacman.conf", "error", err)
105+ }
106+
103107 // Final cleanup and verification
104108 h.logger.Info("Post-installation tasks completed")
105109 result.Success = true
@@ -226,6 +230,23 @@ func (h *PostInstallHandler) writeDankLinuxFlag(mountPoint string) error {
226230 return h.fs.WriteFile(flagPath, []byte(""), 0644)
227231 }
228232
233+func (h *PostInstallHandler) tunePacmanConfig(mountPoint string) error {
234+ confPath := filepath.Join(mountPoint, "etc", "pacman.conf")
235+ content, err := h.fs.ReadFile(confPath)
236+ if err != nil {
237+ return fmt.Errorf("failed to read pacman.conf: %w", err)
238+ }
239+ conf := string(content)
240+ conf = enablePacmanOption(conf, "#Color", "Color")
241+ conf = enablePacmanOption(conf, "#ParallelDownloads = 5", "ParallelDownloads = 5")
242+ conf = enablePacmanOption(conf, "#ILoveCandy", "ILoveCandy")
243+ return h.fs.WriteFile(confPath, []byte(conf), 0644)
244+}
245+
246+func enablePacmanOption(conf, commented, replacement string) string {
247+ return strings.ReplaceAll(conf, commented, replacement)
248+}
249+
229250 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {
230251 url := fmt.Sprintf("%s/%s", h.rawURL, path)
231252 h.logger.Info("Downloading template", "url", url)
@@ -100,6 +100,10 @@ func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstal
100 h.logger.Warn("Failed to install Limine logo", "error", err)100 h.logger.Warn("Failed to install Limine logo", "error", err)
101 }101 }
102 102
103+ if err := h.tunePacmanConfig(cmd.MountPoint); err != nil {
104+ h.logger.Warn("Failed to tune pacman.conf", "error", err)
105+ }
106+
103 // Final cleanup and verification107 // Final cleanup and verification
104 h.logger.Info("Post-installation tasks completed")108 h.logger.Info("Post-installation tasks completed")
105 result.Success = true109 result.Success = true
@@ -226,6 +230,23 @@ func (h *PostInstallHandler) writeDankLinuxFlag(mountPoint string) error {
226 return h.fs.WriteFile(flagPath, []byte(""), 0644)230 return h.fs.WriteFile(flagPath, []byte(""), 0644)
227 }231 }
228 232
233+func (h *PostInstallHandler) tunePacmanConfig(mountPoint string) error {
234+ confPath := filepath.Join(mountPoint, "etc", "pacman.conf")
235+ content, err := h.fs.ReadFile(confPath)
236+ if err != nil {
237+ return fmt.Errorf("failed to read pacman.conf: %w", err)
238+ }
239+ conf := string(content)
240+ conf = enablePacmanOption(conf, "#Color", "Color")
241+ conf = enablePacmanOption(conf, "#ParallelDownloads = 5", "ParallelDownloads = 5")
242+ conf = enablePacmanOption(conf, "#ILoveCandy", "ILoveCandy")
243+ return h.fs.WriteFile(confPath, []byte(conf), 0644)
244+}
245+
246+func enablePacmanOption(conf, commented, replacement string) string {
247+ return strings.ReplaceAll(conf, commented, replacement)
248+}
249+
229 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {250 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {
230 url := fmt.Sprintf("%s/%s", h.rawURL, path)251 url := fmt.Sprintf("%s/%s", h.rawURL, path)
231 h.logger.Info("Downloading template", "url", url)252 h.logger.Info("Downloading template", "url", url)
M internal/application/handlers/post_install_handler_test.go +66 -0
@@ -3,6 +3,7 @@ package handlers
33 import (
44 "context"
55 "net/http"
6+ "strings"
67 "testing"
78
89 "github.com/bnema/archup/internal/application/commands"
@@ -212,6 +213,71 @@ func TestPostInstallHandler_Handle_WithDankLinux(t *testing.T) {
212213 }
213214 }
214215
216+func TestPostInstallHandler_Handle_TunesPacman(t *testing.T) {
217+ ctrl := gomock.NewController(t)
218+ defer ctrl.Finish()
219+
220+ mockFS := mocks.NewMockFileSystem(ctrl)
221+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
222+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
223+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
224+ mockLogger := mocks.NewMockLogger(ctrl)
225+
226+ pacmanContent := "#Color\n#ParallelDownloads = 5\n#ILoveCandy\n"
227+
228+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
229+ mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
230+ mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
231+ mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
232+ mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
233+ mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
234+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
235+
236+ // ReadFile for pacman.conf returns commented options; other reads return generic content
237+ mockFS.EXPECT().ReadFile(gomock.Eq("/mnt/etc/pacman.conf")).Return([]byte(pacmanContent), nil).AnyTimes()
238+ mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
239+
240+ // Capture the WriteFile call for pacman.conf and assert content
241+ mockFS.EXPECT().WriteFile(gomock.Eq("/mnt/etc/pacman.conf"), gomock.Any(), gomock.Any()).DoAndReturn(
242+ func(path string, data []byte, perm interface{}) error {
243+ content := string(data)
244+ if !strings.Contains(content, "Color") {
245+ t.Errorf("expected tuned pacman.conf to contain 'Color', got: %s", content)
246+ }
247+ if strings.Contains(content, "#Color") {
248+ t.Errorf("expected '#Color' to be uncommented in pacman.conf")
249+ }
250+ if !strings.Contains(content, "ParallelDownloads = 5") {
251+ t.Errorf("expected tuned pacman.conf to contain 'ParallelDownloads = 5'")
252+ }
253+ if !strings.Contains(content, "ILoveCandy") {
254+ t.Errorf("expected tuned pacman.conf to contain 'ILoveCandy'")
255+ }
256+ return nil
257+ },
258+ ).AnyTimes()
259+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
260+
261+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
262+
263+ cmd := commands.PostInstallCommand{
264+ MountPoint: "/mnt",
265+ Username: "testuser",
266+ RunPostBootScripts: false,
267+ PlymouthTheme: "",
268+ }
269+
270+ result, err := handler.Handle(context.Background(), cmd)
271+
272+ if err != nil {
273+ t.Fatalf("expected no error, got %v", err)
274+ }
275+
276+ if !result.Success {
277+ t.Error("expected success")
278+ }
279+}
280+
215281 func TestPostInstallHandler_Handle_Everything(t *testing.T) {
216282 ctrl := gomock.NewController(t)
217283 defer ctrl.Finish()
@@ -3,6 +3,7 @@ package handlers
3 import (3 import (
4 "context"4 "context"
5 "net/http"5 "net/http"
6+ "strings"
6 "testing"7 "testing"
7 8
8 "github.com/bnema/archup/internal/application/commands"9 "github.com/bnema/archup/internal/application/commands"
@@ -212,6 +213,71 @@ func TestPostInstallHandler_Handle_WithDankLinux(t *testing.T) {
212 }213 }
213 }214 }
214 215
216+func TestPostInstallHandler_Handle_TunesPacman(t *testing.T) {
217+ ctrl := gomock.NewController(t)
218+ defer ctrl.Finish()
219+
220+ mockFS := mocks.NewMockFileSystem(ctrl)
221+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
222+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
223+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
224+ mockLogger := mocks.NewMockLogger(ctrl)
225+
226+ pacmanContent := "#Color\n#ParallelDownloads = 5\n#ILoveCandy\n"
227+
228+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
229+ mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
230+ mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
231+ mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
232+ mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
233+ mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
234+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
235+
236+ // ReadFile for pacman.conf returns commented options; other reads return generic content
237+ mockFS.EXPECT().ReadFile(gomock.Eq("/mnt/etc/pacman.conf")).Return([]byte(pacmanContent), nil).AnyTimes()
238+ mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
239+
240+ // Capture the WriteFile call for pacman.conf and assert content
241+ mockFS.EXPECT().WriteFile(gomock.Eq("/mnt/etc/pacman.conf"), gomock.Any(), gomock.Any()).DoAndReturn(
242+ func(path string, data []byte, perm interface{}) error {
243+ content := string(data)
244+ if !strings.Contains(content, "Color") {
245+ t.Errorf("expected tuned pacman.conf to contain 'Color', got: %s", content)
246+ }
247+ if strings.Contains(content, "#Color") {
248+ t.Errorf("expected '#Color' to be uncommented in pacman.conf")
249+ }
250+ if !strings.Contains(content, "ParallelDownloads = 5") {
251+ t.Errorf("expected tuned pacman.conf to contain 'ParallelDownloads = 5'")
252+ }
253+ if !strings.Contains(content, "ILoveCandy") {
254+ t.Errorf("expected tuned pacman.conf to contain 'ILoveCandy'")
255+ }
256+ return nil
257+ },
258+ ).AnyTimes()
259+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
260+
261+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
262+
263+ cmd := commands.PostInstallCommand{
264+ MountPoint: "/mnt",
265+ Username: "testuser",
266+ RunPostBootScripts: false,
267+ PlymouthTheme: "",
268+ }
269+
270+ result, err := handler.Handle(context.Background(), cmd)
271+
272+ if err != nil {
273+ t.Fatalf("expected no error, got %v", err)
274+ }
275+
276+ if !result.Success {
277+ t.Error("expected success")
278+ }
279+}
280+
215 func TestPostInstallHandler_Handle_Everything(t *testing.T) {281 func TestPostInstallHandler_Handle_Everything(t *testing.T) {
216 ctrl := gomock.NewController(t)282 ctrl := gomock.NewController(t)
217 defer ctrl.Finish()283 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