detached 15bbba1

feat(debug): add local mode to skip GitHub downloads during VM testing

- bootstrap: skip clone+download when ARCHUP_LOCAL=1 env var is set
- postinstall: add fetchFile() helper that checks /tmp/archup-install/ first
- postboot: same local-first fetchFile() helper
- push-binary-to-vm.sh: add --local flag to sync install/ assets/ logo.txt
  to VM and write /tmp/archup-local-env with ARCHUP_LOCAL=1
commit
15bbba1828b8e0405c14bc0cde7f33ae9d28936f
author
brice · · 2026-02-28 04:10 UTC
parent
5a80716
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +185 -264
M internal/phases/bootstrap.go +8 -0
@@ -4,6 +4,7 @@ import (
44 "fmt"
55 "io"
66 "net/http"
7+ "os"
78 "os/exec"
89 "path/filepath"
910 "strings"
@@ -53,6 +54,13 @@ func (p *BootstrapPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult
5354 return PhaseResult{Success: false, Error: err}
5455 }
5556
57+ // Local mode: files pre-seeded by push-binary-to-vm.sh --local, skip all network activity
58+ if os.Getenv("ARCHUP_LOCAL") == "1" {
59+ p.SendOutput(progressChan, "[LOCAL] Skipping download — using pre-seeded files in "+config.DefaultInstallDir)
60+ p.SendComplete(progressChan, "Bootstrap complete")
61+ return PhaseResult{Success: true, Message: "Local mode: using pre-seeded configuration files"}
62+ }
63+
5664 if err := p.cloneRepo(); err == nil {
5765 if err := p.copyRepoFiles(progressChan); err != nil {
5866 p.SendError(progressChan, err)
@@ -4,6 +4,7 @@ import (
4 "fmt"4 "fmt"
5 "io"5 "io"
6 "net/http"6 "net/http"
7+ "os"
7 "os/exec"8 "os/exec"
8 "path/filepath"9 "path/filepath"
9 "strings"10 "strings"
@@ -53,6 +54,13 @@ func (p *BootstrapPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult
53 return PhaseResult{Success: false, Error: err}54 return PhaseResult{Success: false, Error: err}
54 }55 }
55 56
57+ // Local mode: files pre-seeded by push-binary-to-vm.sh --local, skip all network activity
58+ if os.Getenv("ARCHUP_LOCAL") == "1" {
59+ p.SendOutput(progressChan, "[LOCAL] Skipping download — using pre-seeded files in "+config.DefaultInstallDir)
60+ p.SendComplete(progressChan, "Bootstrap complete")
61+ return PhaseResult{Success: true, Message: "Local mode: using pre-seeded configuration files"}
62+ }
63+
56 if err := p.cloneRepo(); err == nil {64 if err := p.cloneRepo(); err == nil {
57 if err := p.copyRepoFiles(progressChan); err != nil {65 if err := p.copyRepoFiles(progressChan); err != nil {
58 p.SendError(progressChan, err)66 p.SendError(progressChan, err)
M internal/phases/postinstall.go +55 -165
@@ -35,6 +35,30 @@ func NewPostInstallPhase(cfg *config.Config, log *logger.Logger, fs interfaces.F
3535 }
3636 }
3737
38+// fetchFile returns the content of a repo-relative path.
39+// It checks /tmp/archup-install/<repoPath> first (populated by push-binary-to-vm.sh --local),
40+// then falls back to downloading from GitHub.
41+func (p *PostInstallPhase) fetchFile(repoPath string) ([]byte, error) {
42+ localPath := filepath.Join(config.DefaultInstallDir, repoPath)
43+ if content, err := p.fs.ReadFile(localPath); err == nil {
44+ return content, nil
45+ }
46+ url := fmt.Sprintf("%s/%s", p.config.RawURL, repoPath)
47+ resp, err := p.http.Get(url)
48+ if err != nil {
49+ return nil, fmt.Errorf("failed to fetch %s: %w", repoPath, err)
50+ }
51+ defer resp.Body.Close()
52+ if resp.StatusCode != http.StatusOK {
53+ return nil, fmt.Errorf("failed to fetch %s: HTTP %d", repoPath, resp.StatusCode)
54+ }
55+ data, err := io.ReadAll(resp.Body)
56+ if err != nil {
57+ return nil, fmt.Errorf("failed to read %s: %w", repoPath, err)
58+ }
59+ return data, nil
60+}
61+
3862 // PreCheck validates post-install prerequisites
3963 func (p *PostInstallPhase) PreCheck() error {
4064 // Verify /mnt is mounted
@@ -146,41 +170,13 @@ func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResu
146170 func (p *PostInstallPhase) installBootLogo(progressChan chan<- ProgressUpdate) error {
147171 p.SendOutput(progressChan, "Downloading Arch logo...")
148172
149- // Build logo URL from config
150- logoURL := fmt.Sprintf("%s/%s", p.config.RawURL, config.ArchLogoURL)
151-
152- resp, err := p.http.Get(logoURL)
153- switch {
154- case err != nil:
155- return fmt.Errorf("failed to download logo: %w", err)
173+ data, err := p.fetchFile(config.ArchLogoURL)
174+ if err != nil {
175+ return fmt.Errorf("failed to fetch logo: %w", err)
156176 }
157- defer func() {
158- if err := resp.Body.Close(); err != nil {
159- p.SendOutput(progressChan, fmt.Sprintf("[WARN] Failed to close response body: %v", err))
160- }
161- }()
162177
163- switch {
164- case resp.StatusCode != http.StatusOK:
165- return fmt.Errorf("failed to download logo: HTTP %d", resp.StatusCode)
166- }
167-
168- // Save to boot partition
169- logoFile, err := p.fs.Create(config.PathMntBootLogo)
170- switch {
171- case err != nil:
172- return fmt.Errorf("failed to create logo file: %w", err)
173- }
174- defer func() {
175- if err := logoFile.Close(); err != nil {
176- p.SendOutput(progressChan, fmt.Sprintf("[WARN] Failed to close logo file: %v", err))
177- }
178- }()
179-
180- _, err = io.Copy(logoFile, resp.Body)
181- switch {
182- case err != nil:
183- return fmt.Errorf("failed to save logo: %w", err)
178+ if err := p.fs.WriteFile(config.PathMntBootLogo, data, 0644); err != nil {
179+ return fmt.Errorf("failed to write logo: %w", err)
184180 }
185181
186182 p.SendOutput(progressChan, "[OK] Logo downloaded")
@@ -226,53 +222,18 @@ func (p *PostInstallPhase) configurePlymouth(progressChan chan<- ProgressUpdate)
226222 return fmt.Errorf("failed to create theme directory: %w", err)
227223 }
228224
229- // Download all Plymouth files from assets (local files in repo)
225+ // Fetch all Plymouth files (local-first, then remote)
230226 for _, filename := range config.PlymouthFiles {
231- p.SendOutput(progressChan, fmt.Sprintf("Downloading %s...", filename))
232-
233- fileURL := fmt.Sprintf("%s/assets/plymouth/%s", p.config.RawURL, filename)
234- resp, err := p.http.Get(fileURL)
235- switch {
236- case err != nil:
237- return fmt.Errorf("failed to download %s: %w", filename, err)
238- }
227+ p.SendOutput(progressChan, fmt.Sprintf("Fetching %s...", filename))
239228
240- switch {
241- case resp.StatusCode != http.StatusOK:
242- if closeErr := resp.Body.Close(); closeErr != nil {
243- return fmt.Errorf("failed to close %s response: %w", filename, closeErr)
244- }
245- return fmt.Errorf("failed to download %s: HTTP %d", filename, resp.StatusCode)
229+ data, err := p.fetchFile(filepath.Join("assets", "plymouth", filename))
230+ if err != nil {
231+ return fmt.Errorf("failed to fetch %s: %w", filename, err)
246232 }
247233
248- // Save file
249234 destPath := filepath.Join(themeDir, filename)
250- destFile, err := p.fs.Create(destPath)
251- switch {
252- case err != nil:
253- if closeErr := resp.Body.Close(); closeErr != nil {
254- return fmt.Errorf("failed to close response for %s: %w", filename, closeErr)
255- }
256- return fmt.Errorf("failed to create %s: %w", filename, err)
257- }
258-
259- _, err = io.Copy(destFile, resp.Body)
260- if closeErr := destFile.Close(); closeErr != nil {
261- return fmt.Errorf("failed to close %s: %w", filename, closeErr)
262- }
263- if closeErr := resp.Body.Close(); closeErr != nil {
264- return fmt.Errorf("failed to close response for %s: %w", filename, closeErr)
265- }
266-
267- switch {
268- case err != nil:
269- return fmt.Errorf("failed to save %s: %w", filename, err)
270- }
271-
272- // Set permissions
273- switch err := p.fs.Chmod(destPath, 0644); {
274- case err != nil:
275- return fmt.Errorf("failed to set permissions on %s: %w", filename, err)
235+ if err := p.fs.WriteFile(destPath, data, 0644); err != nil {
236+ return fmt.Errorf("failed to write %s: %w", filename, err)
276237 }
277238 }
278239
@@ -679,110 +640,39 @@ func (p *PostInstallPhase) setupPostBoot(progressChan chan<- ProgressUpdate) err
679640 return fmt.Errorf("failed to create post-boot directory: %w", err)
680641 }
681642
682- // Download logo.txt
683- p.SendOutput(progressChan, "Downloading logo.txt...")
684- logoURL := fmt.Sprintf("%s/logo.txt", p.config.RawURL)
685- resp, err := p.http.Get(logoURL)
686- switch {
687- case err != nil:
688- return fmt.Errorf("failed to download logo.txt: %w", err)
689- case resp.StatusCode != http.StatusOK:
690- if closeErr := resp.Body.Close(); closeErr != nil {
691- return fmt.Errorf("failed to close logo response: %w", closeErr)
692- }
693- return fmt.Errorf("failed to download logo.txt: HTTP %d", resp.StatusCode)
643+ // Fetch logo.txt (local-first, then remote)
644+ p.SendOutput(progressChan, "Fetching logo.txt...")
645+ logoData, err := p.fetchFile("logo.txt")
646+ if err != nil {
647+ return fmt.Errorf("failed to fetch logo.txt: %w", err)
694648 }
695-
696649 logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
697- logoFile, err := p.fs.Create(logoPath)
698- switch {
699- case err != nil:
700- if closeErr := resp.Body.Close(); closeErr != nil {
701- return fmt.Errorf("failed to close logo response: %w", closeErr)
702- }
703- return fmt.Errorf("failed to create logo.txt: %w", err)
650+ if err := p.fs.WriteFile(logoPath, logoData, 0644); err != nil {
651+ return fmt.Errorf("failed to write logo.txt: %w", err)
704652 }
705653
706- _, err = io.Copy(logoFile, resp.Body)
707- if closeErr := logoFile.Close(); closeErr != nil {
708- return fmt.Errorf("failed to close logo.txt: %w", closeErr)
709- }
710- if closeErr := resp.Body.Close(); closeErr != nil {
711- return fmt.Errorf("failed to close logo response: %w", closeErr)
712- }
713- switch {
714- case err != nil:
715- return fmt.Errorf("failed to save logo.txt: %w", err)
716- }
717-
718- // Download all post-boot scripts
654+ // Fetch all post-boot scripts (local-first, then remote)
719655 for _, script := range config.PostBootScripts {
720- p.SendOutput(progressChan, fmt.Sprintf("Downloading %s...", script))
656+ p.SendOutput(progressChan, fmt.Sprintf("Fetching %s...", script))
721657
722- scriptURL := fmt.Sprintf("%s/install/mandatory/post-boot/%s", p.config.RawURL, script)
723- resp, err := p.http.Get(scriptURL)
724- switch {
725- case err != nil:
726- return fmt.Errorf("failed to download %s: %w", script, err)
727- case resp.StatusCode != http.StatusOK:
728- if closeErr := resp.Body.Close(); closeErr != nil {
729- return fmt.Errorf("failed to close %s response: %w", script, closeErr)
730- }
731- return fmt.Errorf("failed to download %s: HTTP %d", script, resp.StatusCode)
658+ data, err := p.fetchFile(filepath.Join("install", "mandatory", "post-boot", script))
659+ if err != nil {
660+ return fmt.Errorf("failed to fetch %s: %w", script, err)
732661 }
733662
734663 scriptPath := filepath.Join(config.PathMntPostBoot, script)
735- scriptFile, err := p.fs.Create(scriptPath)
736- switch {
737- case err != nil:
738- if closeErr := resp.Body.Close(); closeErr != nil {
739- return fmt.Errorf("failed to close %s response: %w", script, closeErr)
740- }
741- return fmt.Errorf("failed to create %s: %w", script, err)
742- }
743-
744- _, err = io.Copy(scriptFile, resp.Body)
745- if closeErr := scriptFile.Close(); closeErr != nil {
746- return fmt.Errorf("failed to close %s: %w", script, closeErr)
747- }
748- if closeErr := resp.Body.Close(); closeErr != nil {
749- return fmt.Errorf("failed to close %s response: %w", script, closeErr)
750- }
751-
752- switch {
753- case err != nil:
754- return fmt.Errorf("failed to save %s: %w", script, err)
755- }
756-
757- switch err := p.fs.Chmod(scriptPath, 0755); {
758- case err != nil:
759- return fmt.Errorf("failed to set permissions on %s: %w", script, err)
664+ if err := p.fs.WriteFile(scriptPath, data, 0755); err != nil {
665+ return fmt.Errorf("failed to write %s: %w", script, err)
760666 }
761667 }
762668
763- p.SendOutput(progressChan, "[OK] Post-boot scripts downloaded")
669+ p.SendOutput(progressChan, "[OK] Post-boot scripts ready")
764670
765- // Download service template
671+ // Fetch service template (local-first, then remote)
766672 p.SendOutput(progressChan, "Creating systemd service...")
767- serviceURL := fmt.Sprintf("%s/%s", p.config.RawURL, config.PostBootServiceTemplate)
768- resp, err = p.http.Get(serviceURL)
769- switch {
770- case err != nil:
771- return fmt.Errorf("failed to download service template: %w", err)
772- case resp.StatusCode != http.StatusOK:
773- if closeErr := resp.Body.Close(); closeErr != nil {
774- return fmt.Errorf("failed to close service response: %w", closeErr)
775- }
776- return fmt.Errorf("failed to download service template: HTTP %d", resp.StatusCode)
777- }
778-
779- templateBytes, err := io.ReadAll(resp.Body)
780- if closeErr := resp.Body.Close(); closeErr != nil {
781- return fmt.Errorf("failed to close service response: %w", closeErr)
782- }
783- switch {
784- case err != nil:
785- return fmt.Errorf("failed to read service template: %w", err)
673+ templateBytes, err := p.fetchFile(config.PostBootServiceTemplate)
674+ if err != nil {
675+ return fmt.Errorf("failed to fetch service template: %w", err)
786676 }
787677
788678 serviceContent := string(templateBytes)
@@ -35,6 +35,30 @@ func NewPostInstallPhase(cfg *config.Config, log *logger.Logger, fs interfaces.F
35 }35 }
36 }36 }
37 37
38+// fetchFile returns the content of a repo-relative path.
39+// It checks /tmp/archup-install/<repoPath> first (populated by push-binary-to-vm.sh --local),
40+// then falls back to downloading from GitHub.
41+func (p *PostInstallPhase) fetchFile(repoPath string) ([]byte, error) {
42+ localPath := filepath.Join(config.DefaultInstallDir, repoPath)
43+ if content, err := p.fs.ReadFile(localPath); err == nil {
44+ return content, nil
45+ }
46+ url := fmt.Sprintf("%s/%s", p.config.RawURL, repoPath)
47+ resp, err := p.http.Get(url)
48+ if err != nil {
49+ return nil, fmt.Errorf("failed to fetch %s: %w", repoPath, err)
50+ }
51+ defer resp.Body.Close()
52+ if resp.StatusCode != http.StatusOK {
53+ return nil, fmt.Errorf("failed to fetch %s: HTTP %d", repoPath, resp.StatusCode)
54+ }
55+ data, err := io.ReadAll(resp.Body)
56+ if err != nil {
57+ return nil, fmt.Errorf("failed to read %s: %w", repoPath, err)
58+ }
59+ return data, nil
60+}
61+
38 // PreCheck validates post-install prerequisites62 // PreCheck validates post-install prerequisites
39 func (p *PostInstallPhase) PreCheck() error {63 func (p *PostInstallPhase) PreCheck() error {
40 // Verify /mnt is mounted64 // Verify /mnt is mounted
@@ -146,41 +170,13 @@ func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResu
146 func (p *PostInstallPhase) installBootLogo(progressChan chan<- ProgressUpdate) error {170 func (p *PostInstallPhase) installBootLogo(progressChan chan<- ProgressUpdate) error {
147 p.SendOutput(progressChan, "Downloading Arch logo...")171 p.SendOutput(progressChan, "Downloading Arch logo...")
148 172
149- // Build logo URL from config173+ data, err := p.fetchFile(config.ArchLogoURL)
150- logoURL := fmt.Sprintf("%s/%s", p.config.RawURL, config.ArchLogoURL)174+ if err != nil {
151-175+ return fmt.Errorf("failed to fetch logo: %w", err)
152- resp, err := p.http.Get(logoURL)
153- switch {
154- case err != nil:
155- return fmt.Errorf("failed to download logo: %w", err)
156 }176 }
157- defer func() {
158- if err := resp.Body.Close(); err != nil {
159- p.SendOutput(progressChan, fmt.Sprintf("[WARN] Failed to close response body: %v", err))
160- }
161- }()
162 177
163- switch {178+ if err := p.fs.WriteFile(config.PathMntBootLogo, data, 0644); err != nil {
164- case resp.StatusCode != http.StatusOK:179+ return fmt.Errorf("failed to write logo: %w", err)
165- return fmt.Errorf("failed to download logo: HTTP %d", resp.StatusCode)
166- }
167-
168- // Save to boot partition
169- logoFile, err := p.fs.Create(config.PathMntBootLogo)
170- switch {
171- case err != nil:
172- return fmt.Errorf("failed to create logo file: %w", err)
173- }
174- defer func() {
175- if err := logoFile.Close(); err != nil {
176- p.SendOutput(progressChan, fmt.Sprintf("[WARN] Failed to close logo file: %v", err))
177- }
178- }()
179-
180- _, err = io.Copy(logoFile, resp.Body)
181- switch {
182- case err != nil:
183- return fmt.Errorf("failed to save logo: %w", err)
184 }180 }
185 181
186 p.SendOutput(progressChan, "[OK] Logo downloaded")182 p.SendOutput(progressChan, "[OK] Logo downloaded")
@@ -226,53 +222,18 @@ func (p *PostInstallPhase) configurePlymouth(progressChan chan<- ProgressUpdate)
226 return fmt.Errorf("failed to create theme directory: %w", err)222 return fmt.Errorf("failed to create theme directory: %w", err)
227 }223 }
228 224
229- // Download all Plymouth files from assets (local files in repo)225+ // Fetch all Plymouth files (local-first, then remote)
230 for _, filename := range config.PlymouthFiles {226 for _, filename := range config.PlymouthFiles {
231- p.SendOutput(progressChan, fmt.Sprintf("Downloading %s...", filename))227+ p.SendOutput(progressChan, fmt.Sprintf("Fetching %s...", filename))
232-
233- fileURL := fmt.Sprintf("%s/assets/plymouth/%s", p.config.RawURL, filename)
234- resp, err := p.http.Get(fileURL)
235- switch {
236- case err != nil:
237- return fmt.Errorf("failed to download %s: %w", filename, err)
238- }
239 228
240- switch {229+ data, err := p.fetchFile(filepath.Join("assets", "plymouth", filename))
241- case resp.StatusCode != http.StatusOK:230+ if err != nil {
242- if closeErr := resp.Body.Close(); closeErr != nil {231+ return fmt.Errorf("failed to fetch %s: %w", filename, err)
243- return fmt.Errorf("failed to close %s response: %w", filename, closeErr)
244- }
245- return fmt.Errorf("failed to download %s: HTTP %d", filename, resp.StatusCode)
246 }232 }
247 233
248- // Save file
249 destPath := filepath.Join(themeDir, filename)234 destPath := filepath.Join(themeDir, filename)
250- destFile, err := p.fs.Create(destPath)235+ if err := p.fs.WriteFile(destPath, data, 0644); err != nil {
251- switch {236+ return fmt.Errorf("failed to write %s: %w", filename, err)
252- case err != nil:
253- if closeErr := resp.Body.Close(); closeErr != nil {
254- return fmt.Errorf("failed to close response for %s: %w", filename, closeErr)
255- }
256- return fmt.Errorf("failed to create %s: %w", filename, err)
257- }
258-
259- _, err = io.Copy(destFile, resp.Body)
260- if closeErr := destFile.Close(); closeErr != nil {
261- return fmt.Errorf("failed to close %s: %w", filename, closeErr)
262- }
263- if closeErr := resp.Body.Close(); closeErr != nil {
264- return fmt.Errorf("failed to close response for %s: %w", filename, closeErr)
265- }
266-
267- switch {
268- case err != nil:
269- return fmt.Errorf("failed to save %s: %w", filename, err)
270- }
271-
272- // Set permissions
273- switch err := p.fs.Chmod(destPath, 0644); {
274- case err != nil:
275- return fmt.Errorf("failed to set permissions on %s: %w", filename, err)
276 }237 }
277 }238 }
278 239
@@ -679,110 +640,39 @@ func (p *PostInstallPhase) setupPostBoot(progressChan chan<- ProgressUpdate) err
679 return fmt.Errorf("failed to create post-boot directory: %w", err)640 return fmt.Errorf("failed to create post-boot directory: %w", err)
680 }641 }
681 642
682- // Download logo.txt643+ // Fetch logo.txt (local-first, then remote)
683- p.SendOutput(progressChan, "Downloading logo.txt...")644+ p.SendOutput(progressChan, "Fetching logo.txt...")
684- logoURL := fmt.Sprintf("%s/logo.txt", p.config.RawURL)645+ logoData, err := p.fetchFile("logo.txt")
685- resp, err := p.http.Get(logoURL)646+ if err != nil {
686- switch {647+ return fmt.Errorf("failed to fetch logo.txt: %w", err)
687- case err != nil:
688- return fmt.Errorf("failed to download logo.txt: %w", err)
689- case resp.StatusCode != http.StatusOK:
690- if closeErr := resp.Body.Close(); closeErr != nil {
691- return fmt.Errorf("failed to close logo response: %w", closeErr)
692- }
693- return fmt.Errorf("failed to download logo.txt: HTTP %d", resp.StatusCode)
694 }648 }
695-
696 logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")649 logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
697- logoFile, err := p.fs.Create(logoPath)650+ if err := p.fs.WriteFile(logoPath, logoData, 0644); err != nil {
698- switch {651+ return fmt.Errorf("failed to write logo.txt: %w", err)
699- case err != nil:
700- if closeErr := resp.Body.Close(); closeErr != nil {
701- return fmt.Errorf("failed to close logo response: %w", closeErr)
702- }
703- return fmt.Errorf("failed to create logo.txt: %w", err)
704 }652 }
705 653
706- _, err = io.Copy(logoFile, resp.Body)654+ // Fetch all post-boot scripts (local-first, then remote)
707- if closeErr := logoFile.Close(); closeErr != nil {
708- return fmt.Errorf("failed to close logo.txt: %w", closeErr)
709- }
710- if closeErr := resp.Body.Close(); closeErr != nil {
711- return fmt.Errorf("failed to close logo response: %w", closeErr)
712- }
713- switch {
714- case err != nil:
715- return fmt.Errorf("failed to save logo.txt: %w", err)
716- }
717-
718- // Download all post-boot scripts
719 for _, script := range config.PostBootScripts {655 for _, script := range config.PostBootScripts {
720- p.SendOutput(progressChan, fmt.Sprintf("Downloading %s...", script))656+ p.SendOutput(progressChan, fmt.Sprintf("Fetching %s...", script))
721 657
722- scriptURL := fmt.Sprintf("%s/install/mandatory/post-boot/%s", p.config.RawURL, script)658+ data, err := p.fetchFile(filepath.Join("install", "mandatory", "post-boot", script))
723- resp, err := p.http.Get(scriptURL)659+ if err != nil {
724- switch {660+ return fmt.Errorf("failed to fetch %s: %w", script, err)
725- case err != nil:
726- return fmt.Errorf("failed to download %s: %w", script, err)
727- case resp.StatusCode != http.StatusOK:
728- if closeErr := resp.Body.Close(); closeErr != nil {
729- return fmt.Errorf("failed to close %s response: %w", script, closeErr)
730- }
731- return fmt.Errorf("failed to download %s: HTTP %d", script, resp.StatusCode)
732 }661 }
733 662
734 scriptPath := filepath.Join(config.PathMntPostBoot, script)663 scriptPath := filepath.Join(config.PathMntPostBoot, script)
735- scriptFile, err := p.fs.Create(scriptPath)664+ if err := p.fs.WriteFile(scriptPath, data, 0755); err != nil {
736- switch {665+ return fmt.Errorf("failed to write %s: %w", script, err)
737- case err != nil:
738- if closeErr := resp.Body.Close(); closeErr != nil {
739- return fmt.Errorf("failed to close %s response: %w", script, closeErr)
740- }
741- return fmt.Errorf("failed to create %s: %w", script, err)
742- }
743-
744- _, err = io.Copy(scriptFile, resp.Body)
745- if closeErr := scriptFile.Close(); closeErr != nil {
746- return fmt.Errorf("failed to close %s: %w", script, closeErr)
747- }
748- if closeErr := resp.Body.Close(); closeErr != nil {
749- return fmt.Errorf("failed to close %s response: %w", script, closeErr)
750- }
751-
752- switch {
753- case err != nil:
754- return fmt.Errorf("failed to save %s: %w", script, err)
755- }
756-
757- switch err := p.fs.Chmod(scriptPath, 0755); {
758- case err != nil:
759- return fmt.Errorf("failed to set permissions on %s: %w", script, err)
760 }666 }
761 }667 }
762 668
763- p.SendOutput(progressChan, "[OK] Post-boot scripts downloaded")669+ p.SendOutput(progressChan, "[OK] Post-boot scripts ready")
764 670
765- // Download service template671+ // Fetch service template (local-first, then remote)
766 p.SendOutput(progressChan, "Creating systemd service...")672 p.SendOutput(progressChan, "Creating systemd service...")
767- serviceURL := fmt.Sprintf("%s/%s", p.config.RawURL, config.PostBootServiceTemplate)673+ templateBytes, err := p.fetchFile(config.PostBootServiceTemplate)
768- resp, err = p.http.Get(serviceURL)674+ if err != nil {
769- switch {675+ return fmt.Errorf("failed to fetch service template: %w", err)
770- case err != nil:
771- return fmt.Errorf("failed to download service template: %w", err)
772- case resp.StatusCode != http.StatusOK:
773- if closeErr := resp.Body.Close(); closeErr != nil {
774- return fmt.Errorf("failed to close service response: %w", closeErr)
775- }
776- return fmt.Errorf("failed to download service template: HTTP %d", resp.StatusCode)
777- }
778-
779- templateBytes, err := io.ReadAll(resp.Body)
780- if closeErr := resp.Body.Close(); closeErr != nil {
781- return fmt.Errorf("failed to close service response: %w", closeErr)
782- }
783- switch {
784- case err != nil:
785- return fmt.Errorf("failed to read service template: %w", err)
786 }676 }
787 677
788 serviceContent := string(templateBytes)678 serviceContent := string(templateBytes)
M internal/postboot/setup.go +39 -83
@@ -12,6 +12,30 @@ import (
1212 "github.com/bnema/archup/internal/logger"
1313 )
1414
15+// fetchFile returns the content of a repo-relative path.
16+// It checks /tmp/archup-install/<repoPath> first (populated by push-binary-to-vm.sh --local),
17+// then falls back to downloading from GitHub.
18+func (s *Setup) fetchFile(repoPath string) ([]byte, error) {
19+ localPath := filepath.Join(config.DefaultInstallDir, repoPath)
20+ if content, err := s.fs.ReadFile(localPath); err == nil {
21+ return content, nil
22+ }
23+ url := fmt.Sprintf("%s/%s", s.config.RawURL, repoPath)
24+ resp, err := s.http.Get(url)
25+ if err != nil {
26+ return nil, fmt.Errorf("failed to fetch %s: %w", repoPath, err)
27+ }
28+ defer resp.Body.Close()
29+ if resp.StatusCode != http.StatusOK {
30+ return nil, fmt.Errorf("failed to fetch %s: HTTP %d", repoPath, resp.StatusCode)
31+ }
32+ data, err := io.ReadAll(resp.Body)
33+ if err != nil {
34+ return nil, fmt.Errorf("failed to read %s: %w", repoPath, err)
35+ }
36+ return data, nil
37+}
38+
1539 // SetupResult holds the result of post-boot setup
1640 type SetupResult struct {
1741 ScriptsDownloaded int
@@ -55,79 +79,26 @@ func (s *Setup) Configure() (SetupResult, error) {
5579 return result, fmt.Errorf("failed to create post-boot directory: %w", err)
5680 }
5781
58- // Download logo.txt
59- logoURL := fmt.Sprintf("%s/logo.txt", s.config.RawURL)
60- resp, err := s.http.Get(logoURL)
61- switch {
62- case err != nil:
63- return result, fmt.Errorf("failed to download logo.txt: %w", err)
64- case resp.StatusCode != http.StatusOK:
65- if closeErr := resp.Body.Close(); closeErr != nil {
66- return result, fmt.Errorf("failed to close logo response: %w", closeErr)
67- }
68- return result, fmt.Errorf("failed to download logo.txt: HTTP %d", resp.StatusCode)
69- }
70-
71- logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
72- logoFile, err := s.fs.Create(logoPath)
82+ // Fetch logo.txt (local-first, then remote)
83+ logoData, err := s.fetchFile("logo.txt")
7384 if err != nil {
74- if closeErr := resp.Body.Close(); closeErr != nil {
75- return result, fmt.Errorf("failed to close logo response: %w", closeErr)
76- }
77- return result, fmt.Errorf("failed to create logo.txt: %w", err)
78- }
79-
80- _, err = io.Copy(logoFile, resp.Body)
81- if closeErr := logoFile.Close(); closeErr != nil {
82- if respCloseErr := resp.Body.Close(); respCloseErr != nil {
83- return result, fmt.Errorf("failed to close logo response: %w", respCloseErr)
84- }
85- return result, fmt.Errorf("failed to close logo.txt: %w", closeErr)
85+ return result, fmt.Errorf("failed to fetch logo.txt: %w", err)
8686 }
87- if closeErr := resp.Body.Close(); closeErr != nil {
88- return result, fmt.Errorf("failed to close logo response: %w", closeErr)
89- }
90- if err != nil {
91- return result, fmt.Errorf("failed to save logo.txt: %w", err)
87+ logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
88+ if err := s.fs.WriteFile(logoPath, logoData, 0644); err != nil {
89+ return result, fmt.Errorf("failed to write logo.txt: %w", err)
9290 }
9391
94- // Download all post-boot scripts
92+ // Fetch all post-boot scripts (local-first, then remote)
9593 for _, script := range config.PostBootScripts {
96-
97- scriptURL := fmt.Sprintf("%s/install/mandatory/post-boot/%s", s.config.RawURL, script)
98- resp, err := s.http.Get(scriptURL)
99- switch {
100- case err != nil:
101- return result, fmt.Errorf("failed to download %s: %w", script, err)
102- case resp.StatusCode != http.StatusOK:
103- if closeErr := resp.Body.Close(); closeErr != nil {
104- return result, fmt.Errorf("failed to close %s response: %w", script, closeErr)
105- }
106- return result, fmt.Errorf("failed to download %s: HTTP %d", script, resp.StatusCode)
107- }
108-
109- scriptPath := filepath.Join(config.PathMntPostBoot, script)
110- scriptFile, err := s.fs.Create(scriptPath)
94+ data, err := s.fetchFile(filepath.Join("install", "mandatory", "post-boot", script))
11195 if err != nil {
112- if closeErr := resp.Body.Close(); closeErr != nil {
113- return result, fmt.Errorf("failed to close %s response: %w", script, closeErr)
114- }
115- return result, fmt.Errorf("failed to create %s: %w", script, err)
116- }
117-
118- _, err = io.Copy(scriptFile, resp.Body)
119- if closeErr := scriptFile.Close(); closeErr != nil {
120- if respCloseErr := resp.Body.Close(); respCloseErr != nil {
121- return result, fmt.Errorf("failed to close %s response: %w", script, respCloseErr)
122- }
123- return result, fmt.Errorf("failed to close %s: %w", script, closeErr)
124- }
125- if closeErr := resp.Body.Close(); closeErr != nil {
126- return result, fmt.Errorf("failed to close %s response: %w", script, closeErr)
96+ return result, fmt.Errorf("failed to fetch %s: %w", script, err)
12797 }
12898
129- if err != nil {
130- return result, fmt.Errorf("failed to save %s: %w", script, err)
99+ scriptPath := filepath.Join(config.PathMntPostBoot, script)
100+ if err := s.fs.WriteFile(scriptPath, data, 0755); err != nil {
101+ return result, fmt.Errorf("failed to write %s: %w", script, err)
131102 }
132103
133104 if err := s.fs.Chmod(scriptPath, 0755); err != nil {
@@ -135,25 +106,10 @@ func (s *Setup) Configure() (SetupResult, error) {
135106 }
136107 }
137108
138- // Download service template
139- serviceURL := fmt.Sprintf("%s/%s", s.config.RawURL, config.PostBootServiceTemplate)
140- resp, err = s.http.Get(serviceURL)
141- switch {
142- case err != nil:
143- return result, fmt.Errorf("failed to download service template: %w", err)
144- case resp.StatusCode != http.StatusOK:
145- if closeErr := resp.Body.Close(); closeErr != nil {
146- return result, fmt.Errorf("failed to close service response: %w", closeErr)
147- }
148- return result, fmt.Errorf("failed to download service template: HTTP %d", resp.StatusCode)
149- }
150-
151- templateBytes, err := io.ReadAll(resp.Body)
152- if closeErr := resp.Body.Close(); closeErr != nil {
153- return result, fmt.Errorf("failed to close service response: %w", closeErr)
154- }
109+ // Fetch service template (local-first, then remote)
110+ templateBytes, err := s.fetchFile(config.PostBootServiceTemplate)
155111 if err != nil {
156- return result, fmt.Errorf("failed to read service template: %w", err)
112+ return result, fmt.Errorf("failed to fetch service template: %w", err)
157113 }
158114
159115 serviceContent := string(templateBytes)
@@ -12,6 +12,30 @@ import (
12 "github.com/bnema/archup/internal/logger"12 "github.com/bnema/archup/internal/logger"
13 )13 )
14 14
15+// fetchFile returns the content of a repo-relative path.
16+// It checks /tmp/archup-install/<repoPath> first (populated by push-binary-to-vm.sh --local),
17+// then falls back to downloading from GitHub.
18+func (s *Setup) fetchFile(repoPath string) ([]byte, error) {
19+ localPath := filepath.Join(config.DefaultInstallDir, repoPath)
20+ if content, err := s.fs.ReadFile(localPath); err == nil {
21+ return content, nil
22+ }
23+ url := fmt.Sprintf("%s/%s", s.config.RawURL, repoPath)
24+ resp, err := s.http.Get(url)
25+ if err != nil {
26+ return nil, fmt.Errorf("failed to fetch %s: %w", repoPath, err)
27+ }
28+ defer resp.Body.Close()
29+ if resp.StatusCode != http.StatusOK {
30+ return nil, fmt.Errorf("failed to fetch %s: HTTP %d", repoPath, resp.StatusCode)
31+ }
32+ data, err := io.ReadAll(resp.Body)
33+ if err != nil {
34+ return nil, fmt.Errorf("failed to read %s: %w", repoPath, err)
35+ }
36+ return data, nil
37+}
38+
15 // SetupResult holds the result of post-boot setup39 // SetupResult holds the result of post-boot setup
16 type SetupResult struct {40 type SetupResult struct {
17 ScriptsDownloaded int41 ScriptsDownloaded int
@@ -55,79 +79,26 @@ func (s *Setup) Configure() (SetupResult, error) {
55 return result, fmt.Errorf("failed to create post-boot directory: %w", err)79 return result, fmt.Errorf("failed to create post-boot directory: %w", err)
56 }80 }
57 81
58- // Download logo.txt82+ // Fetch logo.txt (local-first, then remote)
59- logoURL := fmt.Sprintf("%s/logo.txt", s.config.RawURL)83+ logoData, err := s.fetchFile("logo.txt")
60- resp, err := s.http.Get(logoURL)
61- switch {
62- case err != nil:
63- return result, fmt.Errorf("failed to download logo.txt: %w", err)
64- case resp.StatusCode != http.StatusOK:
65- if closeErr := resp.Body.Close(); closeErr != nil {
66- return result, fmt.Errorf("failed to close logo response: %w", closeErr)
67- }
68- return result, fmt.Errorf("failed to download logo.txt: HTTP %d", resp.StatusCode)
69- }
70-
71- logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
72- logoFile, err := s.fs.Create(logoPath)
73 if err != nil {84 if err != nil {
74- if closeErr := resp.Body.Close(); closeErr != nil {85+ return result, fmt.Errorf("failed to fetch logo.txt: %w", err)
75- return result, fmt.Errorf("failed to close logo response: %w", closeErr)
76- }
77- return result, fmt.Errorf("failed to create logo.txt: %w", err)
78- }
79-
80- _, err = io.Copy(logoFile, resp.Body)
81- if closeErr := logoFile.Close(); closeErr != nil {
82- if respCloseErr := resp.Body.Close(); respCloseErr != nil {
83- return result, fmt.Errorf("failed to close logo response: %w", respCloseErr)
84- }
85- return result, fmt.Errorf("failed to close logo.txt: %w", closeErr)
86 }86 }
87- if closeErr := resp.Body.Close(); closeErr != nil {87+ logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
88- return result, fmt.Errorf("failed to close logo response: %w", closeErr)88+ if err := s.fs.WriteFile(logoPath, logoData, 0644); err != nil {
89- }89+ return result, fmt.Errorf("failed to write logo.txt: %w", err)
90- if err != nil {
91- return result, fmt.Errorf("failed to save logo.txt: %w", err)
92 }90 }
93 91
94- // Download all post-boot scripts92+ // Fetch all post-boot scripts (local-first, then remote)
95 for _, script := range config.PostBootScripts {93 for _, script := range config.PostBootScripts {
96-94+ data, err := s.fetchFile(filepath.Join("install", "mandatory", "post-boot", script))
97- scriptURL := fmt.Sprintf("%s/install/mandatory/post-boot/%s", s.config.RawURL, script)
98- resp, err := s.http.Get(scriptURL)
99- switch {
100- case err != nil:
101- return result, fmt.Errorf("failed to download %s: %w", script, err)
102- case resp.StatusCode != http.StatusOK:
103- if closeErr := resp.Body.Close(); closeErr != nil {
104- return result, fmt.Errorf("failed to close %s response: %w", script, closeErr)
105- }
106- return result, fmt.Errorf("failed to download %s: HTTP %d", script, resp.StatusCode)
107- }
108-
109- scriptPath := filepath.Join(config.PathMntPostBoot, script)
110- scriptFile, err := s.fs.Create(scriptPath)
111 if err != nil {95 if err != nil {
112- if closeErr := resp.Body.Close(); closeErr != nil {96+ return result, fmt.Errorf("failed to fetch %s: %w", script, err)
113- return result, fmt.Errorf("failed to close %s response: %w", script, closeErr)
114- }
115- return result, fmt.Errorf("failed to create %s: %w", script, err)
116- }
117-
118- _, err = io.Copy(scriptFile, resp.Body)
119- if closeErr := scriptFile.Close(); closeErr != nil {
120- if respCloseErr := resp.Body.Close(); respCloseErr != nil {
121- return result, fmt.Errorf("failed to close %s response: %w", script, respCloseErr)
122- }
123- return result, fmt.Errorf("failed to close %s: %w", script, closeErr)
124- }
125- if closeErr := resp.Body.Close(); closeErr != nil {
126- return result, fmt.Errorf("failed to close %s response: %w", script, closeErr)
127 }97 }
128 98
129- if err != nil {99+ scriptPath := filepath.Join(config.PathMntPostBoot, script)
130- return result, fmt.Errorf("failed to save %s: %w", script, err)100+ if err := s.fs.WriteFile(scriptPath, data, 0755); err != nil {
101+ return result, fmt.Errorf("failed to write %s: %w", script, err)
131 }102 }
132 103
133 if err := s.fs.Chmod(scriptPath, 0755); err != nil {104 if err := s.fs.Chmod(scriptPath, 0755); err != nil {
@@ -135,25 +106,10 @@ func (s *Setup) Configure() (SetupResult, error) {
135 }106 }
136 }107 }
137 108
138- // Download service template109+ // Fetch service template (local-first, then remote)
139- serviceURL := fmt.Sprintf("%s/%s", s.config.RawURL, config.PostBootServiceTemplate)110+ templateBytes, err := s.fetchFile(config.PostBootServiceTemplate)
140- resp, err = s.http.Get(serviceURL)
141- switch {
142- case err != nil:
143- return result, fmt.Errorf("failed to download service template: %w", err)
144- case resp.StatusCode != http.StatusOK:
145- if closeErr := resp.Body.Close(); closeErr != nil {
146- return result, fmt.Errorf("failed to close service response: %w", closeErr)
147- }
148- return result, fmt.Errorf("failed to download service template: HTTP %d", resp.StatusCode)
149- }
150-
151- templateBytes, err := io.ReadAll(resp.Body)
152- if closeErr := resp.Body.Close(); closeErr != nil {
153- return result, fmt.Errorf("failed to close service response: %w", closeErr)
154- }
155 if err != nil {111 if err != nil {
156- return result, fmt.Errorf("failed to read service template: %w", err)112+ return result, fmt.Errorf("failed to fetch service template: %w", err)
157 }113 }
158 114
159 serviceContent := string(templateBytes)115 serviceContent := string(templateBytes)
M test/vm/push-binary-to-vm.sh +83 -16
@@ -1,19 +1,32 @@
11 #!/bin/bash
22 # Push binary to VM for testing
3-# Usage: ./push-binary-to-vm.sh /path/to/binary
3+# Usage: ./push-binary-to-vm.sh [--local] /path/to/binary
4+#
5+# --local Also sync local install/ assets/ and logo.txt into /tmp/archup-install/ on
6+# the VM and set ARCHUP_LOCAL=1 in /tmp/archup-local-env so the installer
7+# skips all GitHub downloads and uses the pre-seeded files instead.
48
59 set -eo pipefail
610
7-if [ $# -eq 0 ]; then
8- echo "Usage: $0 /path/to/binary"
11+LOCAL_MODE=0
12+BINARY_PATH=""
13+
14+for arg in "$@"; do
15+ case "$arg" in
16+ --local) LOCAL_MODE=1 ;;
17+ *) BINARY_PATH="$arg" ;;
18+ esac
19+done
20+
21+if [ -z "$BINARY_PATH" ]; then
22+ echo "Usage: $0 [--local] /path/to/binary"
923 echo "Example: $0 ../../../dist/archup_linux_amd64_v1/archup"
24+ echo " $0 --local ../../../dist/archup_linux_amd64_v1/archup"
1025 exit 1
1126 fi
1227
13-BINARY_PATH="$1"
1428 BINARY_NAME=$(basename "$BINARY_PATH")
1529
16-# Check if file exists
1730 if [ ! -f "$BINARY_PATH" ]; then
1831 echo "Error: Binary file not found: $BINARY_PATH"
1932 exit 1
@@ -24,29 +37,83 @@ VM_PORT="2222"
2437 VM_USER="root"
2538 VM_PASS="test"
2639 REMOTE_PATH="/tmp"
40+REMOTE_INSTALL_DIR="/tmp/archup-install"
2741
28-echo "Pushing $BINARY_NAME to VM..."
42+SSH_OPTS="-p $VM_PORT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
43+SCP_OPTS="-P $VM_PORT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
44+SSHPASS="sshpass -p $VM_PASS"
2945
30-# Create remote directory
31-sshpass -p "$VM_PASS" ssh -p "$VM_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
32- "$VM_USER@$VM_HOST" "mkdir -p $REMOTE_PATH" 2>&1 | grep -v "Warning: Permanently added" || true
46+ssh_cmd() {
47+ $SSHPASS ssh $SSH_OPTS "$VM_USER@$VM_HOST" "$@" 2>&1 | grep -v "Warning: Permanently added" || true
48+}
3349
34-# Send binary
35-sshpass -p "$VM_PASS" scp -P "$VM_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
36- "$BINARY_PATH" "$VM_USER@$VM_HOST:$REMOTE_PATH/$BINARY_NAME" 2>&1 | grep -v "Warning: Permanently added" || true
50+scp_cmd() {
51+ $SSHPASS scp $SCP_OPTS "$@" 2>&1 | grep -v "Warning: Permanently added" || true
52+}
3753
38-# Verify
54+rsync_cmd() {
55+ $SSHPASS rsync -az --no-perms -e "ssh $SSH_OPTS" "$@" 2>&1 | grep -v "Warning: Permanently added" || true
56+}
57+
58+echo "Pushing $BINARY_NAME to VM..."
59+
60+ssh_cmd "mkdir -p $REMOTE_PATH"
61+scp_cmd "$BINARY_PATH" "$VM_USER@$VM_HOST:$REMOTE_PATH/$BINARY_NAME"
62+
63+# Verify binary
3964 FILE_SIZE=$(stat -c%s "$BINARY_PATH" 2>/dev/null || stat -f%z "$BINARY_PATH" 2>/dev/null)
40-REMOTE_SIZE=$(sshpass -p "$VM_PASS" ssh -p "$VM_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
41- "$VM_USER@$VM_HOST" "stat -c%s $REMOTE_PATH/$BINARY_NAME" 2>&1 | grep -v "Warning: Permanently added" || true)
65+REMOTE_SIZE=$(ssh_cmd "stat -c%s $REMOTE_PATH/$BINARY_NAME")
4266
4367 if [ "$FILE_SIZE" = "$REMOTE_SIZE" ]; then
4468 echo "[OK] Binary pushed successfully!"
4569 echo " Location: $REMOTE_PATH/$BINARY_NAME"
4670 echo " Size: $FILE_SIZE bytes"
4771 else
48- echo "⚠ Size mismatch - transfer may have failed"
72+ echo "Warning: Size mismatch - transfer may have failed"
4973 echo " Local: $FILE_SIZE bytes"
5074 echo " Remote: $REMOTE_SIZE bytes"
5175 exit 1
5276 fi
77+
78+if [ "$LOCAL_MODE" = "1" ]; then
79+ # Resolve repo root (two levels up from test/vm/)
80+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
81+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
82+
83+ echo ""
84+ echo "Syncing local install files to VM ($REMOTE_INSTALL_DIR)..."
85+
86+ ssh_cmd "mkdir -p \
87+ $REMOTE_INSTALL_DIR/configs/shell \
88+ $REMOTE_INSTALL_DIR/assets/plymouth \
89+ $REMOTE_INSTALL_DIR/install/configs/shell \
90+ $REMOTE_INSTALL_DIR/install/mandatory/post-boot"
91+
92+ # install/ configs (bootstrap files land here: /tmp/archup-install/<file>)
93+ rsync_cmd "$REPO_ROOT/install/base.packages" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/"
94+ rsync_cmd "$REPO_ROOT/install/extra.packages" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/"
95+ rsync_cmd "$REPO_ROOT/install/configs/limine.conf.template" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/configs/"
96+ rsync_cmd "$REPO_ROOT/install/configs/chaotic-aur.conf" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/configs/"
97+ rsync_cmd "$REPO_ROOT/install/configs/limine-update.hook" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/configs/"
98+ rsync_cmd "$REPO_ROOT/install/configs/shell/" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/configs/shell/"
99+
100+ # install/ configs also at repo-relative path (used by tryReadLocal fallback)
101+ rsync_cmd "$REPO_ROOT/install/configs/limine.conf.template" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/install/configs/"
102+ rsync_cmd "$REPO_ROOT/install/configs/limine-update.hook" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/install/configs/"
103+
104+ # assets
105+ rsync_cmd "$REPO_ROOT/assets/plymouth/" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/assets/plymouth/"
106+ rsync_cmd "$REPO_ROOT/assets/Arch_Linux__Crystal__icon.png" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/assets/"
107+
108+ # logo.txt
109+ rsync_cmd "$REPO_ROOT/logo.txt" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/"
110+
111+ # post-boot scripts
112+ rsync_cmd "$REPO_ROOT/install/mandatory/post-boot/" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/install/mandatory/post-boot/"
113+
114+ # Write env file so the installer wrapper can source ARCHUP_LOCAL=1
115+ ssh_cmd "echo 'export ARCHUP_LOCAL=1' > /tmp/archup-local-env"
116+
117+ echo "[OK] Local files synced to $REMOTE_INSTALL_DIR"
118+ echo "[OK] /tmp/archup-local-env written — run: source /tmp/archup-local-env && $REMOTE_PATH/$BINARY_NAME"
119+fi
@@ -1,19 +1,32 @@
1 #!/bin/bash1 #!/bin/bash
2 # Push binary to VM for testing2 # Push binary to VM for testing
3-# Usage: ./push-binary-to-vm.sh /path/to/binary3+# Usage: ./push-binary-to-vm.sh [--local] /path/to/binary
4+#
5+# --local Also sync local install/ assets/ and logo.txt into /tmp/archup-install/ on
6+# the VM and set ARCHUP_LOCAL=1 in /tmp/archup-local-env so the installer
7+# skips all GitHub downloads and uses the pre-seeded files instead.
4 8
5 set -eo pipefail9 set -eo pipefail
6 10
7-if [ $# -eq 0 ]; then11+LOCAL_MODE=0
8- echo "Usage: $0 /path/to/binary"12+BINARY_PATH=""
13+
14+for arg in "$@"; do
15+ case "$arg" in
16+ --local) LOCAL_MODE=1 ;;
17+ *) BINARY_PATH="$arg" ;;
18+ esac
19+done
20+
21+if [ -z "$BINARY_PATH" ]; then
22+ echo "Usage: $0 [--local] /path/to/binary"
9 echo "Example: $0 ../../../dist/archup_linux_amd64_v1/archup"23 echo "Example: $0 ../../../dist/archup_linux_amd64_v1/archup"
24+ echo " $0 --local ../../../dist/archup_linux_amd64_v1/archup"
10 exit 125 exit 1
11 fi26 fi
12 27
13-BINARY_PATH="$1"
14 BINARY_NAME=$(basename "$BINARY_PATH")28 BINARY_NAME=$(basename "$BINARY_PATH")
15 29
16-# Check if file exists
17 if [ ! -f "$BINARY_PATH" ]; then30 if [ ! -f "$BINARY_PATH" ]; then
18 echo "Error: Binary file not found: $BINARY_PATH"31 echo "Error: Binary file not found: $BINARY_PATH"
19 exit 132 exit 1
@@ -24,29 +37,83 @@ VM_PORT="2222"
24 VM_USER="root"37 VM_USER="root"
25 VM_PASS="test"38 VM_PASS="test"
26 REMOTE_PATH="/tmp"39 REMOTE_PATH="/tmp"
40+REMOTE_INSTALL_DIR="/tmp/archup-install"
27 41
28-echo "Pushing $BINARY_NAME to VM..."42+SSH_OPTS="-p $VM_PORT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
43+SCP_OPTS="-P $VM_PORT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
44+SSHPASS="sshpass -p $VM_PASS"
29 45
30-# Create remote directory46+ssh_cmd() {
31-sshpass -p "$VM_PASS" ssh -p "$VM_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \47+ $SSHPASS ssh $SSH_OPTS "$VM_USER@$VM_HOST" "$@" 2>&1 | grep -v "Warning: Permanently added" || true
32- "$VM_USER@$VM_HOST" "mkdir -p $REMOTE_PATH" 2>&1 | grep -v "Warning: Permanently added" || true48+}
33 49
34-# Send binary50+scp_cmd() {
35-sshpass -p "$VM_PASS" scp -P "$VM_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \51+ $SSHPASS scp $SCP_OPTS "$@" 2>&1 | grep -v "Warning: Permanently added" || true
36- "$BINARY_PATH" "$VM_USER@$VM_HOST:$REMOTE_PATH/$BINARY_NAME" 2>&1 | grep -v "Warning: Permanently added" || true52+}
37 53
38-# Verify54+rsync_cmd() {
55+ $SSHPASS rsync -az --no-perms -e "ssh $SSH_OPTS" "$@" 2>&1 | grep -v "Warning: Permanently added" || true
56+}
57+
58+echo "Pushing $BINARY_NAME to VM..."
59+
60+ssh_cmd "mkdir -p $REMOTE_PATH"
61+scp_cmd "$BINARY_PATH" "$VM_USER@$VM_HOST:$REMOTE_PATH/$BINARY_NAME"
62+
63+# Verify binary
39 FILE_SIZE=$(stat -c%s "$BINARY_PATH" 2>/dev/null || stat -f%z "$BINARY_PATH" 2>/dev/null)64 FILE_SIZE=$(stat -c%s "$BINARY_PATH" 2>/dev/null || stat -f%z "$BINARY_PATH" 2>/dev/null)
40-REMOTE_SIZE=$(sshpass -p "$VM_PASS" ssh -p "$VM_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \65+REMOTE_SIZE=$(ssh_cmd "stat -c%s $REMOTE_PATH/$BINARY_NAME")
41- "$VM_USER@$VM_HOST" "stat -c%s $REMOTE_PATH/$BINARY_NAME" 2>&1 | grep -v "Warning: Permanently added" || true)
42 66
43 if [ "$FILE_SIZE" = "$REMOTE_SIZE" ]; then67 if [ "$FILE_SIZE" = "$REMOTE_SIZE" ]; then
44 echo "[OK] Binary pushed successfully!"68 echo "[OK] Binary pushed successfully!"
45 echo " Location: $REMOTE_PATH/$BINARY_NAME"69 echo " Location: $REMOTE_PATH/$BINARY_NAME"
46 echo " Size: $FILE_SIZE bytes"70 echo " Size: $FILE_SIZE bytes"
47 else71 else
48- echo "⚠ Size mismatch - transfer may have failed"72+ echo "Warning: Size mismatch - transfer may have failed"
49 echo " Local: $FILE_SIZE bytes"73 echo " Local: $FILE_SIZE bytes"
50 echo " Remote: $REMOTE_SIZE bytes"74 echo " Remote: $REMOTE_SIZE bytes"
51 exit 175 exit 1
52 fi76 fi
77+
78+if [ "$LOCAL_MODE" = "1" ]; then
79+ # Resolve repo root (two levels up from test/vm/)
80+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
81+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
82+
83+ echo ""
84+ echo "Syncing local install files to VM ($REMOTE_INSTALL_DIR)..."
85+
86+ ssh_cmd "mkdir -p \
87+ $REMOTE_INSTALL_DIR/configs/shell \
88+ $REMOTE_INSTALL_DIR/assets/plymouth \
89+ $REMOTE_INSTALL_DIR/install/configs/shell \
90+ $REMOTE_INSTALL_DIR/install/mandatory/post-boot"
91+
92+ # install/ configs (bootstrap files land here: /tmp/archup-install/<file>)
93+ rsync_cmd "$REPO_ROOT/install/base.packages" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/"
94+ rsync_cmd "$REPO_ROOT/install/extra.packages" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/"
95+ rsync_cmd "$REPO_ROOT/install/configs/limine.conf.template" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/configs/"
96+ rsync_cmd "$REPO_ROOT/install/configs/chaotic-aur.conf" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/configs/"
97+ rsync_cmd "$REPO_ROOT/install/configs/limine-update.hook" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/configs/"
98+ rsync_cmd "$REPO_ROOT/install/configs/shell/" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/configs/shell/"
99+
100+ # install/ configs also at repo-relative path (used by tryReadLocal fallback)
101+ rsync_cmd "$REPO_ROOT/install/configs/limine.conf.template" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/install/configs/"
102+ rsync_cmd "$REPO_ROOT/install/configs/limine-update.hook" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/install/configs/"
103+
104+ # assets
105+ rsync_cmd "$REPO_ROOT/assets/plymouth/" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/assets/plymouth/"
106+ rsync_cmd "$REPO_ROOT/assets/Arch_Linux__Crystal__icon.png" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/assets/"
107+
108+ # logo.txt
109+ rsync_cmd "$REPO_ROOT/logo.txt" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/"
110+
111+ # post-boot scripts
112+ rsync_cmd "$REPO_ROOT/install/mandatory/post-boot/" "$VM_USER@$VM_HOST:$REMOTE_INSTALL_DIR/install/mandatory/post-boot/"
113+
114+ # Write env file so the installer wrapper can source ARCHUP_LOCAL=1
115+ ssh_cmd "echo 'export ARCHUP_LOCAL=1' > /tmp/archup-local-env"
116+
117+ echo "[OK] Local files synced to $REMOTE_INSTALL_DIR"
118+ echo "[OK] /tmp/archup-local-env written — run: source /tmp/archup-local-env && $REMOTE_PATH/$BINARY_NAME"
119+fi

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