From 018cd04b32a72643b42f71ad7bdb507fc1d74931 Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 13:30:25 +0700 Subject: [PATCH 01/12] chore: update Makefile to use bash as the default shell - Set the SHELL variable to /bin/bash in the Makefile for improved script compatibility. --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index e1833cae..8b735d08 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ #!/usr/bin/make -f +SHELL := /bin/bash + # Go version and build settings GO_VERSION := 1.23 GO_SYSTEM_VERSION := $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1-2) From e9f9a1390e641b3c7d02a5f8dd42a4e5f8acad8f Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 13:45:38 +0700 Subject: [PATCH 02/12] feat: enhance binary path resolution for Minitiad - Updated GetMinitiadBinaryPath to include OS and architecture in the binary path for Linux, improving compatibility. - Refactored NewMinitiadQuerier and downloadMinitiaApp to utilize the updated binary path resolution, ensuring correct binary retrieval based on the environment. --- cosmosutils/binary.go | 6 +++++- cosmosutils/cli_query.go | 6 +++++- models/minitia/launch.go | 11 +++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/cosmosutils/binary.go b/cosmosutils/binary.go index 5f9de435..1ce033a8 100644 --- a/cosmosutils/binary.go +++ b/cosmosutils/binary.go @@ -268,7 +268,11 @@ func GetMinitiadBinaryPath(vm, version string) (string, error) { switch runtime.GOOS { case "linux": - return filepath.Join(extractedPath, fmt.Sprintf("mini%s_%s", vm, version), "minitiad"), nil + goos, arch, err := getOSArch() + if err != nil { + return "", err + } + return filepath.Join(extractedPath, fmt.Sprintf("mini%s_%s_%s_%s", vm, version, goos, arch), "minitiad"), nil case "darwin": return filepath.Join(extractedPath, "minitiad"), nil default: diff --git a/cosmosutils/cli_query.go b/cosmosutils/cli_query.go index cba86f12..f9945f56 100644 --- a/cosmosutils/cli_query.go +++ b/cosmosutils/cli_query.go @@ -77,10 +77,14 @@ func NewMinitiadQuerier() (*MinitiadQuerier, error) { if err != nil { return nil, fmt.Errorf("failed to get user home dir: %v", err) } + binaryPath, err := GetMinitiadBinaryPath(DefaultMinitiadQuerierVM, version) + if err != nil { + return nil, fmt.Errorf("failed to get binary path: %v", err) + } + weaveDataPath := filepath.Join(userHome, common.WeaveDataDirectory) tarballPath := filepath.Join(weaveDataPath, "minitia.tar.gz") extractedPath := filepath.Join(weaveDataPath, fmt.Sprintf("mini%s@%s", DefaultMinitiadQuerierVM, version)) - binaryPath := filepath.Join(extractedPath, DefaultMinitiadQuerierAppName) if _, err := os.Stat(binaryPath); os.IsNotExist(err) { if _, err := os.Stat(extractedPath); os.IsNotExist(err) { diff --git a/models/minitia/launch.go b/models/minitia/launch.go index 1625b526..8224afca 100644 --- a/models/minitia/launch.go +++ b/models/minitia/launch.go @@ -2170,14 +2170,9 @@ func downloadMinitiaApp(ctx context.Context) tea.Cmd { tarballPath := filepath.Join(weaveDataPath, "minitia.tar.gz") extractedPath := filepath.Join(weaveDataPath, fmt.Sprintf("mini%s@%s", strings.ToLower(state.vmType), state.minitiadVersion)) - var binaryPath string - switch runtime.GOOS { - case "linux": - binaryPath = filepath.Join(extractedPath, fmt.Sprintf("mini%s_%s", strings.ToLower(state.vmType), state.minitiadVersion), AppName) - case "darwin": - binaryPath = filepath.Join(extractedPath, AppName) - default: - return ui.NonRetryableErrorLoading{Err: fmt.Errorf("unsupported OS: %v", runtime.GOOS)} + binaryPath, err := cosmosutils.GetMinitiadBinaryPath(strings.ToLower(state.vmType), state.minitiadVersion) + if err != nil { + return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to get binary path: %v", err)} } state.binaryPath = binaryPath From f949a948cdc85a984b20ee0271c71e316a121f1c Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 13:49:37 +0700 Subject: [PATCH 03/12] refactor: simplify binary path resolution for Minitiad - Consolidated the binary path resolution logic in GetMinitiadBinaryPath to handle both Darwin and Linux operating systems uniformly, enhancing code clarity and maintainability. --- cosmosutils/binary.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cosmosutils/binary.go b/cosmosutils/binary.go index 1ce033a8..cf066afb 100644 --- a/cosmosutils/binary.go +++ b/cosmosutils/binary.go @@ -267,13 +267,7 @@ func GetMinitiadBinaryPath(vm, version string) (string, error) { extractedPath := filepath.Join(userHome, common.WeaveDataDirectory, fmt.Sprintf("mini%s@%s", vm, version)) switch runtime.GOOS { - case "linux": - goos, arch, err := getOSArch() - if err != nil { - return "", err - } - return filepath.Join(extractedPath, fmt.Sprintf("mini%s_%s_%s_%s", vm, version, goos, arch), "minitiad"), nil - case "darwin": + case "darwin", "linux": return filepath.Join(extractedPath, "minitiad"), nil default: return "", fmt.Errorf("unsupported OS: %v", runtime.GOOS) From a4004c82e290ac43e3c66e38dfdc4da2ab387e08 Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 13:55:58 +0700 Subject: [PATCH 04/12] refactor: streamline permission setting for binaries - Updated the permission setting for binaries in InstallMinitiadBinary and NewMinitiadQuerier to use a more concise error handling approach. - Cleaned up whitespace in RecoverKeyFromMnemonicWithCoinType for improved readability. - Ensured consistent permission setting logic across multiple files, enhancing code clarity and maintainability. --- cosmosutils/binary.go | 7 +++---- cosmosutils/cli_query.go | 7 +++---- cosmosutils/keys.go | 4 ++-- models/minitia/launch.go | 9 ++++----- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/cosmosutils/binary.go b/cosmosutils/binary.go index cf066afb..45ab5b82 100644 --- a/cosmosutils/binary.go +++ b/cosmosutils/binary.go @@ -293,11 +293,10 @@ func InstallMinitiadBinary(vm, version, url, binaryPath string) error { if err = io.DownloadAndExtractTarGz(url, tarballPath, extractedPath); err != nil { return fmt.Errorf("failed to download and extract binary: %v", err) } + } - err = os.Chmod(binaryPath, 0755) - if err != nil { - return fmt.Errorf("failed to set permissions for binary: %v", err) - } + if err := os.Chmod(binaryPath, 0755); err != nil { + return fmt.Errorf("failed to set permissions for binary: %v", err) } if vm == "move" || vm == "wasm" { diff --git a/cosmosutils/cli_query.go b/cosmosutils/cli_query.go index f9945f56..2726bd27 100644 --- a/cosmosutils/cli_query.go +++ b/cosmosutils/cli_query.go @@ -97,11 +97,10 @@ func NewMinitiadQuerier() (*MinitiadQuerier, error) { if err = io.DownloadAndExtractTarGz(downloadURL, tarballPath, extractedPath); err != nil { return nil, fmt.Errorf("failed to download minitia binary: %v", err) } + } - err = os.Chmod(binaryPath, 0755) - if err != nil { - return nil, fmt.Errorf("failed to set permissions for binary: %v", err) - } + if err := os.Chmod(binaryPath, 0755); err != nil { + return nil, fmt.Errorf("failed to set permissions for binary: %v", err) } return &MinitiadQuerier{binaryPath: binaryPath}, nil diff --git a/cosmosutils/keys.go b/cosmosutils/keys.go index 3f998c4a..563dafce 100644 --- a/cosmosutils/keys.go +++ b/cosmosutils/keys.go @@ -98,13 +98,13 @@ func RecoverKeyFromMnemonicWithCoinType(appName, keyname, mnemonic string, coinT if coinType == 0 { return "", fmt.Errorf("coin type must be explicitly provided (60 or 118), got 0") } - + coinTypeStr := fmt.Sprintf("%d", coinType) keyType := "secp256k1" if coinType == 60 { keyType = "eth_secp256k1" } - + cmd = exec.Command(appName, "keys", "add", keyname, "--coin-type", coinTypeStr, "--key-type", keyType, "--recover", "--keyring-backend", "test", "--output", "json") } diff --git a/models/minitia/launch.go b/models/minitia/launch.go index 8224afca..266abada 100644 --- a/models/minitia/launch.go +++ b/models/minitia/launch.go @@ -2188,14 +2188,13 @@ func downloadMinitiaApp(ctx context.Context) tea.Cmd { return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to download and extract binary: %v", err)} } - err = os.Chmod(binaryPath, 0o755) - if err != nil { - return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to set permissions for binary: %v", err)} - } - state.downloadedNewBinary = true } + if err = os.Chmod(binaryPath, 0o755); err != nil { + return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to set permissions for binary: %v", err)} + } + if state.vmType == string(Move) || state.vmType == string(Wasm) { err = io.SetLibraryPaths(filepath.Dir(binaryPath)) if err != nil { From 103d047d5bc1f2476a3bb0f4af06f6d2c18cfd01 Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 13:59:35 +0700 Subject: [PATCH 05/12] refactor: remove unnecessary SHELL declaration in Makefile - Eliminated the SHELL variable assignment in the Makefile as it is no longer needed, streamlining the build configuration. --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 8b735d08..e1833cae 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,5 @@ #!/usr/bin/make -f -SHELL := /bin/bash - # Go version and build settings GO_VERSION := 1.23 GO_SYSTEM_VERSION := $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1-2) From 812247a9677ce1f607388c37232175f7be6184f6 Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 14:05:44 +0700 Subject: [PATCH 06/12] feat: add lingering state check for systemd user services - Introduced a new method to check if systemd lingering is enabled for the user by inspecting the linger state file directly, avoiding potential permission issues with loginctl. - Updated the ensureUserServicePrerequisites method to utilize the new check, improving error handling and user guidance when enabling lingering. --- service/systemd.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/service/systemd.go b/service/systemd.go index 513e3842..a1bd38d0 100644 --- a/service/systemd.go +++ b/service/systemd.go @@ -44,16 +44,29 @@ func (j *Systemd) GetServiceName() (string, error) { return slug + ".service", nil } +// isLingeringEnabled checks whether systemd lingering is already active for the user +// by inspecting the linger state file directly, avoiding a loginctl call that may be denied. +func (j *Systemd) isLingeringEnabled() bool { + lingerFile := filepath.Join("/var/lib/systemd/linger", j.user.Username) + _, err := os.Stat(lingerFile) + return err == nil +} + // ensureUserServicePrerequisites checks and sets up requirements before any systemd operation func (j *Systemd) ensureUserServicePrerequisites() error { if !j.userMode { return nil } - enableCmd := exec.Command("loginctl", "enable-linger", j.user.Username) - if output, err := enableCmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to enable lingering. Please run 'loginctl enable-linger %s' manually: %v (output: %s)", - j.user.Username, err, string(output)) + if !j.isLingeringEnabled() { + enableCmd := exec.Command("loginctl", "enable-linger", j.user.Username) + if output, err := enableCmd.CombinedOutput(); err != nil { + // Re-check after failure: root may have already enabled it via the linger file. + if !j.isLingeringEnabled() { + return fmt.Errorf("failed to enable lingering. Please run 'loginctl enable-linger %s' manually: %v (output: %s)", + j.user.Username, err, string(output)) + } + } } // Check and set XDG_RUNTIME_DIR if not set From 6df2c5df73e7c75b89311fc41818ad6a3cedf6b2 Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 14:10:22 +0700 Subject: [PATCH 07/12] feat: implement enableLingering method for systemd user services - Added a new method to enable systemd lingering for the current user, attempting both loginctl and sudo for improved compatibility on cloud VMs. - Updated ensureUserServicePrerequisites to utilize the new enableLingering method, enhancing error handling and user guidance. --- service/systemd.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/service/systemd.go b/service/systemd.go index a1bd38d0..5747f2b0 100644 --- a/service/systemd.go +++ b/service/systemd.go @@ -52,6 +52,25 @@ func (j *Systemd) isLingeringEnabled() bool { return err == nil } +// enableLingering attempts to enable systemd lingering for the current user. +// It first tries loginctl directly; if that is denied (common on cloud VMs with +// restricted polkit rules), it retries with sudo, which Ubuntu cloud instances +// typically allow without a password. +func (j *Systemd) enableLingering() error { + enableCmd := exec.Command("loginctl", "enable-linger", j.user.Username) + if output, err := enableCmd.CombinedOutput(); err != nil { + // Retry with sudo — Ubuntu cloud VMs usually grant NOPASSWD sudo. + sudoCmd := exec.Command("sudo", "loginctl", "enable-linger", j.user.Username) + if sudoOutput, sudoErr := sudoCmd.CombinedOutput(); sudoErr != nil { + // Neither attempt worked; give a clear, actionable error. + _ = output + return fmt.Errorf("failed to enable lingering. Please run 'sudo loginctl enable-linger %s' manually: %v (output: %s)", + j.user.Username, sudoErr, string(sudoOutput)) + } + } + return nil +} + // ensureUserServicePrerequisites checks and sets up requirements before any systemd operation func (j *Systemd) ensureUserServicePrerequisites() error { if !j.userMode { @@ -59,13 +78,8 @@ func (j *Systemd) ensureUserServicePrerequisites() error { } if !j.isLingeringEnabled() { - enableCmd := exec.Command("loginctl", "enable-linger", j.user.Username) - if output, err := enableCmd.CombinedOutput(); err != nil { - // Re-check after failure: root may have already enabled it via the linger file. - if !j.isLingeringEnabled() { - return fmt.Errorf("failed to enable lingering. Please run 'loginctl enable-linger %s' manually: %v (output: %s)", - j.user.Username, err, string(output)) - } + if err := j.enableLingering(); err != nil { + return err } } From a0deb10a4e613d1228ea419bb2d0076a614a57e9 Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 14:26:18 +0700 Subject: [PATCH 08/12] fix: update binary path resolution for Minitia in systemd service - Modified the binary path resolution logic in the Create method to include Minitia in the switch case, ensuring correct path handling for this command. - Removed redundant case handling for Minitia, streamlining the code and improving maintainability. --- service/systemd.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/service/systemd.go b/service/systemd.go index 5747f2b0..905ebd41 100644 --- a/service/systemd.go +++ b/service/systemd.go @@ -116,10 +116,8 @@ func (j *Systemd) Create(binaryVersion, appHome string) error { } var binaryPath string switch j.commandName { - case UpgradableInitia, NonUpgradableInitia: + case UpgradableInitia, NonUpgradableInitia, Minitia: binaryPath = filepath.Join(userHome, common.WeaveDataDirectory, binaryVersion) - case Minitia: - binaryPath = filepath.Join(userHome, common.WeaveDataDirectory, binaryVersion, strings.ReplaceAll(binaryVersion, "@", "_")) default: binaryPath = filepath.Join(userHome, common.WeaveDataDirectory) } From 85f5858b7907945c2d24464535dca4a83d015edf Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 14:53:49 +0700 Subject: [PATCH 09/12] feat: enhance binary path resolution for Minitia across services - Implemented FindBinaryDir function to dynamically locate the binary directory for Minitia, improving flexibility and compatibility with varying tarball structures. - Updated the Create methods in both systemd and launchd services to utilize the new binary path resolution logic, ensuring accurate binary retrieval for Minitia commands. - Enhanced error handling for binary location failures, providing clearer feedback on issues during the binary extraction process. --- cmd/upgrade.go | 7 ++++++- cosmosutils/binary.go | 25 +++++++++++++++++++++++++ service/launchd.go | 12 +++++++++++- service/systemd.go | 9 ++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 7ed810a5..56edebff 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -115,12 +115,17 @@ func downloadAndReplaceBinary(downloadURL string) error { tarballPath := filepath.Join(homeDir, common.WeaveDataDirectory, "weave-binary.tar.gz") extractedPath := filepath.Join(homeDir, common.WeaveDataDirectory) - binaryPath := filepath.Join(extractedPath, "weave") fmt.Printf("⬇️ Downloading from %s...\n", downloadURL) if err = io.DownloadAndExtractTarGz(downloadURL, tarballPath, extractedPath); err != nil { return fmt.Errorf("failed to download and extract binary: %v", err) } + + binaryDir, err := cosmosutils.FindBinaryDir(extractedPath, "weave") + if err != nil { + return fmt.Errorf("could not locate weave binary after extraction: %w", err) + } + binaryPath := filepath.Join(binaryDir, "weave") defer func() { _ = io.DeleteFile(binaryPath) }() diff --git a/cosmosutils/binary.go b/cosmosutils/binary.go index 45ab5b82..a2ccb06c 100644 --- a/cosmosutils/binary.go +++ b/cosmosutils/binary.go @@ -274,6 +274,31 @@ func GetMinitiadBinaryPath(vm, version string) (string, error) { } } +// FindBinaryDir walks versionDir to find the directory that contains the named +// executable. This avoids hardcoding assumptions about how a release tarball is +// structured, so the code stays correct even if a future tarball places the +// binary inside a subdirectory. +func FindBinaryDir(versionDir, binaryName string) (string, error) { + var result string + err := filepath.Walk(versionDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && info.Name() == binaryName && info.Mode()&0o111 != 0 { + result = filepath.Dir(path) + return filepath.SkipAll + } + return nil + }) + if err != nil { + return "", fmt.Errorf("failed to search for binary %q in %s: %w", binaryName, versionDir, err) + } + if result == "" { + return "", fmt.Errorf("binary %q not found in %s", binaryName, versionDir) + } + return result, nil +} + func InstallMinitiadBinary(vm, version, url, binaryPath string) error { userHome, err := os.UserHomeDir() if err != nil { diff --git a/service/launchd.go b/service/launchd.go index 31878dc5..b4fe5e94 100644 --- a/service/launchd.go +++ b/service/launchd.go @@ -15,6 +15,7 @@ import ( "time" "github.com/initia-labs/weave/common" + "github.com/initia-labs/weave/cosmosutils" weaveio "github.com/initia-labs/weave/io" ) @@ -54,7 +55,16 @@ func (j *Launchd) Create(binaryVersion, appHome string) error { if err != nil { return fmt.Errorf("failed to get binary name: %v", err) } - binaryPath := filepath.Join(weaveDataPath, binaryVersion) + var binaryPath string + if j.commandName == Minitia { + versionDir := filepath.Join(weaveDataPath, binaryVersion) + binaryPath, err = cosmosutils.FindBinaryDir(versionDir, binaryName) + if err != nil { + return fmt.Errorf("failed to locate %s binary: %w", binaryName, err) + } + } else { + binaryPath = filepath.Join(weaveDataPath, binaryVersion) + } if err = os.Setenv("HOME", userHome); err != nil { return fmt.Errorf("failed to set HOME: %v", err) } diff --git a/service/systemd.go b/service/systemd.go index 905ebd41..eac6cbff 100644 --- a/service/systemd.go +++ b/service/systemd.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/initia-labs/weave/common" + "github.com/initia-labs/weave/cosmosutils" ) const ( @@ -116,8 +117,14 @@ func (j *Systemd) Create(binaryVersion, appHome string) error { } var binaryPath string switch j.commandName { - case UpgradableInitia, NonUpgradableInitia, Minitia: + case UpgradableInitia, NonUpgradableInitia: binaryPath = filepath.Join(userHome, common.WeaveDataDirectory, binaryVersion) + case Minitia: + versionDir := filepath.Join(userHome, common.WeaveDataDirectory, binaryVersion) + binaryPath, err = cosmosutils.FindBinaryDir(versionDir, binaryName) + if err != nil { + return fmt.Errorf("failed to locate %s binary: %w", binaryName, err) + } default: binaryPath = filepath.Join(userHome, common.WeaveDataDirectory) } From b754d32a2320a6554321f999d1ce15935c6dc82a Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 15:07:21 +0700 Subject: [PATCH 10/12] refactor: improve binary extraction process in upgrade command - Changed the binary extraction process to utilize a temporary directory, preventing conflicts with existing binaries in the shared data root. - Updated the download and extraction logic to reflect the new temporary path, enhancing reliability during upgrades. - Improved error handling for directory creation and binary location, ensuring clearer feedback on potential issues. --- cmd/upgrade.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 56edebff..baa82e25 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -113,22 +113,28 @@ func downloadAndReplaceBinary(downloadURL string) error { return fmt.Errorf("failed to get user home directory: %v", err) } - tarballPath := filepath.Join(homeDir, common.WeaveDataDirectory, "weave-binary.tar.gz") - extractedPath := filepath.Join(homeDir, common.WeaveDataDirectory) + // Extract into a private temp directory so FindBinaryDir cannot match a + // stale weave binary that may already exist under the shared data root. + tempDir, err := os.MkdirTemp(filepath.Join(homeDir, common.WeaveDataDirectory), "weave-upgrade-*") + if err != nil { + return fmt.Errorf("failed to create temp extraction directory: %v", err) + } + defer func() { + _ = io.DeleteDirectory(tempDir) + }() + + tarballPath := filepath.Join(tempDir, "weave-binary.tar.gz") fmt.Printf("⬇️ Downloading from %s...\n", downloadURL) - if err = io.DownloadAndExtractTarGz(downloadURL, tarballPath, extractedPath); err != nil { + if err = io.DownloadAndExtractTarGz(downloadURL, tarballPath, tempDir); err != nil { return fmt.Errorf("failed to download and extract binary: %v", err) } - binaryDir, err := cosmosutils.FindBinaryDir(extractedPath, "weave") + binaryDir, err := cosmosutils.FindBinaryDir(tempDir, "weave") if err != nil { return fmt.Errorf("could not locate weave binary after extraction: %w", err) } binaryPath := filepath.Join(binaryDir, "weave") - defer func() { - _ = io.DeleteFile(binaryPath) - }() if err = doReplace(binaryPath); err != nil { return fmt.Errorf("failed to replace the new weave binary: %v", err) From 68f10b1ef5544c66026623bf519b5db8380bab0c Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 15:18:32 +0700 Subject: [PATCH 11/12] refactor: consolidate Minitiad binary management logic - Replaced GetMinitiadBinaryPath and InstallMinitiadBinary with a new EnsureMinitiadBinary function to streamline binary retrieval and installation. - Improved error handling and clarity in the binary extraction process, ensuring the binary is correctly located or downloaded as needed. - Updated related functions across the codebase to utilize the new binary management approach, enhancing maintainability and reducing redundancy. --- cosmosutils/binary.go | 58 +++++++++++++++++++--------------------- cosmosutils/cli_query.go | 33 ++--------------------- cosmosutils/cli_tx.go | 6 +---- models/minitia/launch.go | 41 +++++++--------------------- 4 files changed, 40 insertions(+), 98 deletions(-) diff --git a/cosmosutils/binary.go b/cosmosutils/binary.go index a2ccb06c..56e41a50 100644 --- a/cosmosutils/binary.go +++ b/cosmosutils/binary.go @@ -259,21 +259,6 @@ func getMinitiadBinaryURL(vm, version string) (string, error) { ), nil } -func GetMinitiadBinaryPath(vm, version string) (string, error) { - userHome, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get user home directory: %v", err) - } - extractedPath := filepath.Join(userHome, common.WeaveDataDirectory, fmt.Sprintf("mini%s@%s", vm, version)) - - switch runtime.GOOS { - case "darwin", "linux": - return filepath.Join(extractedPath, "minitiad"), nil - default: - return "", fmt.Errorf("unsupported OS: %v", runtime.GOOS) - } -} - // FindBinaryDir walks versionDir to find the directory that contains the named // executable. This avoids hardcoding assumptions about how a release tarball is // structured, so the code stays correct even if a future tarball places the @@ -299,36 +284,47 @@ func FindBinaryDir(versionDir, binaryName string) (string, error) { return result, nil } -func InstallMinitiadBinary(vm, version, url, binaryPath string) error { +// EnsureMinitiadBinary guarantees the minitiad binary for the given vm/version +// is present and returns its full path. It first checks the version directory +// with FindBinaryDir so it tolerates any tarball layout; only if the binary is +// absent does it download and re-extract. The caller therefore never needs to +// construct or assume a hardcoded sub-path inside the version directory. +func EnsureMinitiadBinary(vm, version, url string) (string, error) { userHome, err := os.UserHomeDir() if err != nil { - return fmt.Errorf("failed to get user home directory: %v", err) + return "", fmt.Errorf("failed to get user home directory: %v", err) } - tarballPath := filepath.Join(userHome, common.WeaveDataDirectory, "minitia.tar.gz") + extractedPath := filepath.Join(userHome, common.WeaveDataDirectory, fmt.Sprintf("mini%s@%s", vm, version)) - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - if _, err := os.Stat(extractedPath); os.IsNotExist(err) { - err := os.MkdirAll(extractedPath, os.ModePerm) - if err != nil { - return fmt.Errorf("failed to create weave data directory: %v", err) - } + binaryDir, findErr := FindBinaryDir(extractedPath, "minitiad") + if findErr != nil { + // Binary not present yet — download and extract. + if err := os.MkdirAll(extractedPath, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create version directory: %v", err) } - - if err = io.DownloadAndExtractTarGz(url, tarballPath, extractedPath); err != nil { - return fmt.Errorf("failed to download and extract binary: %v", err) + tarballPath := filepath.Join(userHome, common.WeaveDataDirectory, "minitia.tar.gz") + if err := io.DownloadAndExtractTarGz(url, tarballPath, extractedPath); err != nil { + return "", fmt.Errorf("failed to download and extract minitiad binary: %v", err) + } + binaryDir, err = FindBinaryDir(extractedPath, "minitiad") + if err != nil { + return "", fmt.Errorf("minitiad binary not found after extraction in %s: %w", extractedPath, err) } } - if err := os.Chmod(binaryPath, 0755); err != nil { - return fmt.Errorf("failed to set permissions for binary: %v", err) + binaryPath := filepath.Join(binaryDir, "minitiad") + if err := os.Chmod(binaryPath, 0o755); err != nil { + return "", fmt.Errorf("failed to set permissions for minitiad binary: %v", err) } if vm == "move" || vm == "wasm" { - return io.SetLibraryPaths(filepath.Dir(binaryPath)) + if err := io.SetLibraryPaths(binaryDir); err != nil { + return "", err + } } - return nil + return binaryPath, nil } func GetLatestOPInitBotVersion() (string, string, error) { diff --git a/cosmosutils/cli_query.go b/cosmosutils/cli_query.go index 2726bd27..320d372b 100644 --- a/cosmosutils/cli_query.go +++ b/cosmosutils/cli_query.go @@ -3,13 +3,9 @@ package cosmosutils import ( "encoding/json" "fmt" - "os" "os/exec" - "path/filepath" "github.com/initia-labs/weave/client" - "github.com/initia-labs/weave/common" - "github.com/initia-labs/weave/io" "github.com/initia-labs/weave/types" ) @@ -73,34 +69,9 @@ func NewMinitiadQuerier() (*MinitiadQuerier, error) { return nil, err } - userHome, err := os.UserHomeDir() + binaryPath, err := EnsureMinitiadBinary(DefaultMinitiadQuerierVM, version, downloadURL) if err != nil { - return nil, fmt.Errorf("failed to get user home dir: %v", err) - } - binaryPath, err := GetMinitiadBinaryPath(DefaultMinitiadQuerierVM, version) - if err != nil { - return nil, fmt.Errorf("failed to get binary path: %v", err) - } - - weaveDataPath := filepath.Join(userHome, common.WeaveDataDirectory) - tarballPath := filepath.Join(weaveDataPath, "minitia.tar.gz") - extractedPath := filepath.Join(weaveDataPath, fmt.Sprintf("mini%s@%s", DefaultMinitiadQuerierVM, version)) - - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - if _, err := os.Stat(extractedPath); os.IsNotExist(err) { - err := os.MkdirAll(extractedPath, os.ModePerm) - if err != nil { - return nil, fmt.Errorf("failed to create weave data directory: %v", err) - } - } - - if err = io.DownloadAndExtractTarGz(downloadURL, tarballPath, extractedPath); err != nil { - return nil, fmt.Errorf("failed to download minitia binary: %v", err) - } - } - - if err := os.Chmod(binaryPath, 0755); err != nil { - return nil, fmt.Errorf("failed to set permissions for binary: %v", err) + return nil, fmt.Errorf("failed to ensure minitiad binary: %v", err) } return &MinitiadQuerier{binaryPath: binaryPath}, nil diff --git a/cosmosutils/cli_tx.go b/cosmosutils/cli_tx.go index 6f12d185..0bdba451 100644 --- a/cosmosutils/cli_tx.go +++ b/cosmosutils/cli_tx.go @@ -130,11 +130,7 @@ func NewMinitiadTxExecutor(rest string) (*MinitiadTxExecutor, error) { if err != nil { return nil, err } - binaryPath, err := GetMinitiadBinaryPath(vm, version) - if err != nil { - return nil, err - } - err = InstallMinitiadBinary(vm, version, url, binaryPath) + binaryPath, err := EnsureMinitiadBinary(vm, version, url) if err != nil { return nil, err } diff --git a/models/minitia/launch.go b/models/minitia/launch.go index 266abada..934c7cf0 100644 --- a/models/minitia/launch.go +++ b/models/minitia/launch.go @@ -2166,41 +2166,20 @@ func downloadMinitiaApp(ctx context.Context) tea.Cmd { if err != nil { return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to get user home directory: %v", err)} } - weaveDataPath := filepath.Join(userHome, common.WeaveDataDirectory) - tarballPath := filepath.Join(weaveDataPath, "minitia.tar.gz") - extractedPath := filepath.Join(weaveDataPath, fmt.Sprintf("mini%s@%s", strings.ToLower(state.vmType), state.minitiadVersion)) - - binaryPath, err := cosmosutils.GetMinitiadBinaryPath(strings.ToLower(state.vmType), state.minitiadVersion) - if err != nil { - return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to get binary path: %v", err)} - } - state.binaryPath = binaryPath - - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - if _, err := os.Stat(extractedPath); os.IsNotExist(err) { - err := os.MkdirAll(extractedPath, os.ModePerm) - if err != nil { - return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to create weave data directory: %v", err)} - } - } - if err = io.DownloadAndExtractTarGz(state.minitiadEndpoint, tarballPath, extractedPath); err != nil { - return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to download and extract binary: %v", err)} - } + vm := strings.ToLower(state.vmType) + extractedPath := filepath.Join(userHome, common.WeaveDataDirectory, fmt.Sprintf("mini%s@%s", vm, state.minitiadVersion)) - state.downloadedNewBinary = true - } + // Determine whether a fresh download will be needed before delegating, + // so we can still set downloadedNewBinary accurately. + _, findErr := cosmosutils.FindBinaryDir(extractedPath, "minitiad") + state.downloadedNewBinary = findErr != nil - if err = os.Chmod(binaryPath, 0o755); err != nil { - return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to set permissions for binary: %v", err)} - } - - if state.vmType == string(Move) || state.vmType == string(Wasm) { - err = io.SetLibraryPaths(filepath.Dir(binaryPath)) - if err != nil { - return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to set library path: %v", err)} - } + binaryPath, err := cosmosutils.EnsureMinitiadBinary(vm, state.minitiadVersion, state.minitiadEndpoint) + if err != nil { + return ui.NonRetryableErrorLoading{Err: fmt.Errorf("failed to ensure minitiad binary: %v", err)} } + state.binaryPath = binaryPath return ui.EndLoading{ Ctx: weavecontext.SetCurrentState(ctx, state), From e964b1e956dadec2d49379464be79d5ebd078216 Mon Sep 17 00:00:00 2001 From: benzbeeb Date: Thu, 19 Mar 2026 15:33:29 +0700 Subject: [PATCH 12/12] test: add comprehensive tests for FindBinaryDir and EnsureMinitiadBinary - Introduced new test cases for FindBinaryDir to validate binary location logic across various directory structures and conditions. - Added tests for EnsureMinitiadBinary to ensure correct binary retrieval from nested directories, enhancing coverage and reliability of binary management functions. - Implemented helper function createExecutable to streamline test setup for executable files. --- cosmosutils/binary_test.go | 138 +++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/cosmosutils/binary_test.go b/cosmosutils/binary_test.go index b73d23b5..23b7fe0e 100644 --- a/cosmosutils/binary_test.go +++ b/cosmosutils/binary_test.go @@ -2,6 +2,8 @@ package cosmosutils import ( "fmt" + "os" + "path/filepath" "reflect" "strings" "testing" @@ -295,3 +297,139 @@ func TestFilterPreReleases(t *testing.T) { }) } } + +// createExecutable creates a file with 0755 permissions at the given path. +func createExecutable(t *testing.T, path string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("#!/bin/sh\n"), 0o755); err != nil { + t.Fatal(err) + } +} + +func TestFindBinaryDir(t *testing.T) { + tests := []struct { + name string + layout func(root string) + binaryName string + wantRel string // expected result relative to root, "" means error + }{ + { + name: "binary at root of version dir", + layout: func(root string) { + createExecutable(t, filepath.Join(root, "minitiad")) + }, + binaryName: "minitiad", + wantRel: ".", + }, + { + name: "binary in a subdirectory", + layout: func(root string) { + createExecutable(t, filepath.Join(root, "minimove_v0.6.0", "minitiad")) + }, + binaryName: "minitiad", + wantRel: "minimove_v0.6.0", + }, + { + name: "binary deeply nested", + layout: func(root string) { + createExecutable(t, filepath.Join(root, "a", "b", "c", "minitiad")) + }, + binaryName: "minitiad", + wantRel: filepath.Join("a", "b", "c"), + }, + { + name: "non-executable file is ignored", + layout: func(root string) { + os.MkdirAll(root, 0o755) + os.WriteFile(filepath.Join(root, "minitiad"), []byte("data"), 0o644) + }, + binaryName: "minitiad", + wantRel: "", + }, + { + name: "wrong name is ignored", + layout: func(root string) { + createExecutable(t, filepath.Join(root, "cosmovisor")) + }, + binaryName: "minitiad", + wantRel: "", + }, + { + name: "empty directory", + layout: func(root string) { + os.MkdirAll(root, 0o755) + }, + binaryName: "minitiad", + wantRel: "", + }, + { + name: "multiple matches returns first found", + layout: func(root string) { + createExecutable(t, filepath.Join(root, "minitiad")) + createExecutable(t, filepath.Join(root, "sub", "minitiad")) + }, + binaryName: "minitiad", + wantRel: ".", // Walk is lexical; root comes before "sub/" + }, + { + name: "nonexistent directory", + layout: func(_ string) { + // intentionally don't create anything + }, + binaryName: "minitiad", + wantRel: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := filepath.Join(t.TempDir(), "version") + tt.layout(root) + + dir, err := FindBinaryDir(root, tt.binaryName) + + if tt.wantRel == "" { + if err == nil { + t.Fatalf("expected error, got dir=%q", dir) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := filepath.Join(root, tt.wantRel) + if dir != want { + t.Errorf("got %q, want %q", dir, want) + } + }) + } +} + +func TestEnsureMinitiadBinary_NestedLayout(t *testing.T) { + origHome := os.Getenv("HOME") + tmpHome := t.TempDir() + os.Setenv("HOME", tmpHome) + defer os.Setenv("HOME", origHome) + + vm, version := "move", "v0.5.0" + versionDir := filepath.Join(tmpHome, ".weave", "data", fmt.Sprintf("mini%s@%s", vm, version)) + + // Stage binary inside a nested subdirectory (simulates a tarball that + // extracts into minimove_v0.5.0/minitiad). + createExecutable(t, filepath.Join(versionDir, "minimove_v0.5.0", "minitiad")) + + binaryPath, err := EnsureMinitiadBinary(vm, version, "http://should-not-be-called") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := filepath.Join(versionDir, "minimove_v0.5.0", "minitiad") + if binaryPath != want { + t.Errorf("got %q, want %q", binaryPath, want) + } +}