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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +185 -264
M internal/phases/bootstrap.go +8 -0
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "fmt" | |
| 5 | 5 | "io" | |
| 6 | 6 | "net/http" | |
| 7 | + | "os" | |
| 7 | 8 | "os/exec" | |
| 8 | 9 | "path/filepath" | |
| 9 | 10 | "strings" | |
| @@ -53,6 +54,13 @@ func (p *BootstrapPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult | |||
| 53 | 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 | 64 | if err := p.cloneRepo(); err == nil { | |
| 57 | 65 | if err := p.copyRepoFiles(progressChan); err != nil { | |
| 58 | 66 | 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 | |||
| 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 | 62 | // PreCheck validates post-install prerequisites | |
| 39 | 63 | func (p *PostInstallPhase) PreCheck() error { | |
| 40 | 64 | // Verify /mnt is mounted | |
| @@ -146,41 +170,13 @@ func (p *PostInstallPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResu | |||
| 146 | 170 | func (p *PostInstallPhase) installBootLogo(progressChan chan<- ProgressUpdate) error { | |
| 147 | 171 | p.SendOutput(progressChan, "Downloading Arch logo...") | |
| 148 | 172 | ||
| 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) | |
| 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 { | |
| 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) | |
| 184 | 180 | } | |
| 185 | 181 | ||
| 186 | 182 | p.SendOutput(progressChan, "[OK] Logo downloaded") | |
| @@ -226,53 +222,18 @@ func (p *PostInstallPhase) configurePlymouth(progressChan chan<- ProgressUpdate) | |||
| 226 | 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 | 226 | 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)) | |
| 239 | 228 | ||
| 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) | |
| 246 | 232 | } | |
| 247 | 233 | ||
| 248 | - | // Save file | |
| 249 | 234 | 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) | |
| 276 | 237 | } | |
| 277 | 238 | } | |
| 278 | 239 | ||
| @@ -679,110 +640,39 @@ func (p *PostInstallPhase) setupPostBoot(progressChan chan<- ProgressUpdate) err | |||
| 679 | 640 | return fmt.Errorf("failed to create post-boot directory: %w", err) | |
| 680 | 641 | } | |
| 681 | 642 | ||
| 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) | |
| 694 | 648 | } | |
| 695 | - | ||
| 696 | 649 | 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) | |
| 704 | 652 | } | |
| 705 | 653 | ||
| 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) | |
| 719 | 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) | |
| 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) | |
| 732 | 661 | } | |
| 733 | 662 | ||
| 734 | 663 | 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) | |
| 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 template | |
| 671 | + | // Fetch service template (local-first, then remote) | |
| 766 | 672 | 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) | |
| 786 | 676 | } | |
| 787 | 677 | ||
| 788 | 678 | 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 prerequisites | 62 | // PreCheck validates post-install prerequisites | ||
| 39 | func (p *PostInstallPhase) PreCheck() error { | 63 | func (p *PostInstallPhase) PreCheck() error { | ||
| 40 | // Verify /mnt is mounted | 64 | // 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 config | 173 | + | 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.txt | 643 | + | // 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 template | 671 | + | // 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 ( | |||
| 12 | 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 | 39 | // SetupResult holds the result of post-boot setup | |
| 16 | 40 | type SetupResult struct { | |
| 17 | 41 | ScriptsDownloaded int | |
| @@ -55,79 +79,26 @@ func (s *Setup) Configure() (SetupResult, error) { | |||
| 55 | 79 | return result, fmt.Errorf("failed to create post-boot directory: %w", err) | |
| 56 | 80 | } | |
| 57 | 81 | ||
| 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") | |
| 73 | 84 | 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) | |
| 86 | 86 | } | |
| 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) | |
| 92 | 90 | } | |
| 93 | 91 | ||
| 94 | - | // Download all post-boot scripts | |
| 92 | + | // Fetch all post-boot scripts (local-first, then remote) | |
| 95 | 93 | 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)) | |
| 111 | 95 | 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) | |
| 127 | 97 | } | |
| 128 | 98 | ||
| 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) | |
| 131 | 102 | } | |
| 132 | 103 | ||
| 133 | 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 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) | |
| 155 | 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 | 115 | 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 setup | 39 | // SetupResult holds the result of post-boot setup | ||
| 16 | type SetupResult struct { | 40 | type SetupResult struct { | ||
| 17 | ScriptsDownloaded int | 41 | 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.txt | 82 | + | // 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 scripts | 92 | + | // 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 template | 109 | + | // 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 @@ | |||
| 1 | 1 | #!/bin/bash | |
| 2 | 2 | # 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. | |
| 4 | 8 | ||
| 5 | 9 | set -eo pipefail | |
| 6 | 10 | ||
| 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" | |
| 9 | 23 | echo "Example: $0 ../../../dist/archup_linux_amd64_v1/archup" | |
| 24 | + | echo " $0 --local ../../../dist/archup_linux_amd64_v1/archup" | |
| 10 | 25 | exit 1 | |
| 11 | 26 | fi | |
| 12 | 27 | ||
| 13 | - | BINARY_PATH="$1" | |
| 14 | 28 | BINARY_NAME=$(basename "$BINARY_PATH") | |
| 15 | 29 | ||
| 16 | - | # Check if file exists | |
| 17 | 30 | if [ ! -f "$BINARY_PATH" ]; then | |
| 18 | 31 | echo "Error: Binary file not found: $BINARY_PATH" | |
| 19 | 32 | exit 1 | |
| @@ -24,29 +37,83 @@ VM_PORT="2222" | |||
| 24 | 37 | VM_USER="root" | |
| 25 | 38 | VM_PASS="test" | |
| 26 | 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 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 | + | } | |
| 33 | 49 | ||
| 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 | + | } | |
| 37 | 53 | ||
| 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 | |
| 39 | 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 \ | |
| 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") | |
| 42 | 66 | ||
| 43 | 67 | if [ "$FILE_SIZE" = "$REMOTE_SIZE" ]; then | |
| 44 | 68 | echo "[OK] Binary pushed successfully!" | |
| 45 | 69 | echo " Location: $REMOTE_PATH/$BINARY_NAME" | |
| 46 | 70 | echo " Size: $FILE_SIZE bytes" | |
| 47 | 71 | else | |
| 48 | - | echo "⚠ Size mismatch - transfer may have failed" | |
| 72 | + | echo "Warning: Size mismatch - transfer may have failed" | |
| 49 | 73 | echo " Local: $FILE_SIZE bytes" | |
| 50 | 74 | echo " Remote: $REMOTE_SIZE bytes" | |
| 51 | 75 | exit 1 | |
| 52 | 76 | 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/bash | 1 | #!/bin/bash | ||
| 2 | # Push binary to VM for testing | 2 | # 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. | |||
| 4 | 8 | ||||
| 5 | set -eo pipefail | 9 | set -eo pipefail | ||
| 6 | 10 | ||||
| 7 | - | if [ $# -eq 0 ]; then | 11 | + | 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 1 | 25 | exit 1 | ||
| 11 | fi | 26 | 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" ]; then | 30 | 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 1 | 32 | 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 directory | 46 | + | 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" || true | 48 | + | } |
| 33 | 49 | ||||
| 34 | - | # Send binary | 50 | + | 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" || true | 52 | + | } |
| 37 | 53 | ||||
| 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 | |||
| 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" ]; then | 67 | 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 | else | 71 | 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 1 | 75 | exit 1 | ||
| 52 | fi | 76 | 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 | |||