Skip to content

Commit 6630d96

Browse files
committed
Implement automatic tool commands and improve Maven download strategy
- Add automatic tool command generation for all registered tools (go, java, node, etc.) - Implement improved Maven/mvnd download strategy with alternating URLs instead of exhausting retries on first URL - Add ExecuteTool method to executor for running mvx-managed tools - Update CI workflow to use mvx-managed Go instead of GitHub Actions setup-go - Tools are automatically detected from manager registry instead of hardcoded list - Automatic commands work alongside existing dedicated commands (like mvn) - Each tool command auto-installs the tool if needed and sets up proper environment
1 parent 7c98576 commit 6630d96

File tree

6 files changed

+444
-50
lines changed

6 files changed

+444
-50
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
- name: Run integration tests
7171
run: |
7272
cd test
73-
go test -v -timeout=5m ./...
73+
go test -v -timeout=10m ./...
7474
7575
- name: Run benchmarks
7676
run: |

cmd/root.go

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,18 @@ func Execute() error {
6060
return fmt.Errorf("auto-setup failed: %w", err)
6161
}
6262

63-
// Add dynamic custom commands before execution
63+
// Add dynamic custom commands and tool commands before execution
6464
if err := addCustomCommands(); err != nil {
6565
// If we can't load custom commands, continue with built-in commands only
6666
printVerbose("Failed to load custom commands: %v", err)
6767
}
6868

69+
// Add automatic tool commands
70+
if err := addToolCommands(); err != nil {
71+
// If we can't load tool commands, continue without them
72+
printVerbose("Failed to load tool commands: %v", err)
73+
}
74+
6975
return rootCmd.Execute()
7076
}
7177

@@ -384,6 +390,86 @@ func createCustomCommand(cmdName string, cmdConfig config.CommandConfig, exec *e
384390
return cmd
385391
}
386392

393+
// addToolCommands dynamically adds tool commands (mvn, go, node, etc.) as top-level commands
394+
func addToolCommands() error {
395+
// Try to find project root and load configuration
396+
projectRoot, err := findProjectRoot()
397+
if err != nil {
398+
return err // No project root found, skip tool commands
399+
}
400+
401+
// Load configuration
402+
cfg, err := config.LoadConfig(projectRoot)
403+
if err != nil {
404+
return err // No configuration found, skip tool commands
405+
}
406+
407+
// Create tool manager
408+
manager, err := tools.NewManager()
409+
if err != nil {
410+
return fmt.Errorf("failed to create tool manager: %w", err)
411+
}
412+
413+
// Create executor
414+
exec := executor.NewExecutor(cfg, manager, projectRoot)
415+
416+
// Get all registered tool names from the manager
417+
registeredToolNames := manager.GetToolNames()
418+
419+
for _, toolName := range registeredToolNames {
420+
// Check if this tool is configured in the project
421+
if _, exists := cfg.Tools[toolName]; !exists {
422+
continue // Skip tools not configured in this project
423+
}
424+
425+
// Check if a command with this name already exists (avoid conflicts)
426+
if hasCommand(rootCmd, toolName) {
427+
continue // Skip if command already exists
428+
}
429+
430+
// Create the tool command
431+
toolCmd := createToolCommand(toolName, exec)
432+
rootCmd.AddCommand(toolCmd)
433+
printVerbose("Added automatic tool command: %s", toolName)
434+
}
435+
436+
return nil
437+
}
438+
439+
// hasCommand checks if a command with the given name already exists
440+
func hasCommand(cmd *cobra.Command, name string) bool {
441+
for _, subCmd := range cmd.Commands() {
442+
if subCmd.Name() == name {
443+
return true
444+
}
445+
}
446+
return false
447+
}
448+
449+
// createToolCommand creates a cobra command for a specific tool
450+
func createToolCommand(toolName string, exec *executor.Executor) *cobra.Command {
451+
return &cobra.Command{
452+
Use: toolName + " [tool-args...]",
453+
Short: fmt.Sprintf("Run %s with mvx-managed environment", toolName),
454+
Long: fmt.Sprintf(`Run %s with the mvx-managed %s installation and proper environment setup.
455+
456+
This command automatically uses the %s version specified in your mvx configuration
457+
and sets up the appropriate environment variables.
458+
459+
Examples:
460+
mvx %s --version # Show %s version
461+
mvx %s [args...] # Run %s with arguments`, toolName, toolName, toolName, toolName, toolName, toolName, toolName),
462+
463+
DisableFlagParsing: true, // Allow all flags to be passed through to the tool
464+
Run: func(cmd *cobra.Command, args []string) {
465+
if err := exec.ExecuteTool(toolName, args); err != nil {
466+
printError("%v", err)
467+
os.Exit(1)
468+
}
469+
},
470+
}
471+
}
472+
387473
// isBuiltinCommand checks if a command name is a built-in mvx command
388474
func isBuiltinCommand(commandName string) bool {
389475
builtinCommands := map[string]bool{

pkg/executor/executor.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,63 @@ func (e *Executor) ExecuteCommand(commandName string, args []string) error {
7474
return e.executeScriptWithInterpreter(processedScript, workDir, env, interpreter)
7575
}
7676

77+
// ExecuteTool executes a tool command with mvx-managed environment
78+
func (e *Executor) ExecuteTool(toolName string, args []string) error {
79+
// Check if the tool is configured
80+
toolConfig, exists := e.config.Tools[toolName]
81+
if !exists {
82+
return fmt.Errorf("tool %s is not configured in this project", toolName)
83+
}
84+
85+
// Get the tool instance
86+
tool, err := e.toolManager.GetTool(toolName)
87+
if err != nil {
88+
return fmt.Errorf("failed to get tool %s: %w", toolName, err)
89+
}
90+
91+
// Resolve tool version using the manager
92+
resolvedVersion, err := e.toolManager.ResolveVersion(toolName, toolConfig)
93+
if err != nil {
94+
return fmt.Errorf("failed to resolve %s version %s: %w", toolName, toolConfig.Version, err)
95+
}
96+
97+
// Check if tool is installed, auto-install if needed
98+
if !tool.IsInstalled(resolvedVersion, toolConfig) {
99+
fmt.Printf("🔧 Auto-installing %s %s...\n", toolName, resolvedVersion)
100+
if err := tool.Install(resolvedVersion, toolConfig); err != nil {
101+
return fmt.Errorf("failed to install %s %s: %w", toolName, resolvedVersion, err)
102+
}
103+
}
104+
105+
// Get tool binary path
106+
toolBinPath, err := tool.GetPath(resolvedVersion, toolConfig)
107+
if err != nil {
108+
return fmt.Errorf("failed to get %s binary path: %w", toolName, err)
109+
}
110+
111+
// Setup environment with tool paths
112+
env, err := e.setupToolEnvironment(toolName, toolBinPath)
113+
if err != nil {
114+
return fmt.Errorf("failed to setup environment for %s: %w", toolName, err)
115+
}
116+
117+
// Execute the tool
118+
toolExecutable := toolName
119+
if len(args) == 0 {
120+
args = []string{"--version"} // Default to showing version if no args
121+
}
122+
123+
// Create and execute command
124+
cmd := exec.Command(toolExecutable, args...)
125+
cmd.Env = env
126+
cmd.Stdout = os.Stdout
127+
cmd.Stderr = os.Stderr
128+
cmd.Stdin = os.Stdin
129+
cmd.Dir = e.projectRoot
130+
131+
return cmd.Run()
132+
}
133+
77134
// ExecuteBuiltinCommand executes a built-in command with optional hooks and overrides
78135
func (e *Executor) ExecuteBuiltinCommand(commandName string, args []string, builtinFunc func([]string) error) error {
79136
// Check if command is overridden
@@ -388,3 +445,72 @@ func (e *Executor) ValidateCommand(commandName string) error {
388445

389446
return nil
390447
}
448+
449+
// setupToolEnvironment prepares the environment for tool execution
450+
func (e *Executor) setupToolEnvironment(toolName, toolBinPath string) ([]string, error) {
451+
// Create environment map starting with current environment
452+
envVars := make(map[string]string)
453+
for _, envVar := range os.Environ() {
454+
parts := strings.SplitN(envVar, "=", 2)
455+
if len(parts) == 2 {
456+
envVars[parts[0]] = parts[1]
457+
}
458+
}
459+
460+
// Add global environment variables from config
461+
globalEnv, err := e.toolManager.SetupEnvironment(e.config)
462+
if err != nil {
463+
return nil, err
464+
}
465+
466+
for key, value := range globalEnv {
467+
envVars[key] = value
468+
}
469+
470+
// Add tool binary directory to PATH
471+
pathDirs := []string{toolBinPath}
472+
473+
// Add existing PATH
474+
if existingPath, exists := envVars["PATH"]; exists {
475+
pathDirs = append(pathDirs, existingPath)
476+
}
477+
478+
// Set PATH with tool directory first
479+
envVars["PATH"] = strings.Join(pathDirs, string(os.PathListSeparator))
480+
481+
// Set tool-specific environment variables
482+
switch toolName {
483+
case "go":
484+
// Set GOROOT if we can determine it from the binary path
485+
if strings.HasSuffix(toolBinPath, "/bin") {
486+
goRoot := strings.TrimSuffix(toolBinPath, "/bin")
487+
envVars["GOROOT"] = goRoot
488+
}
489+
case "java":
490+
// Set JAVA_HOME if we can determine it from the binary path
491+
if strings.HasSuffix(toolBinPath, "/bin") {
492+
javaHome := strings.TrimSuffix(toolBinPath, "/bin")
493+
envVars["JAVA_HOME"] = javaHome
494+
}
495+
case "mvn", "maven":
496+
// Set MAVEN_HOME if we can determine it from the binary path
497+
if strings.HasSuffix(toolBinPath, "/bin") {
498+
mavenHome := strings.TrimSuffix(toolBinPath, "/bin")
499+
envVars["MAVEN_HOME"] = mavenHome
500+
}
501+
case "mvnd":
502+
// Set MVND_HOME if we can determine it from the binary path
503+
if strings.HasSuffix(toolBinPath, "/bin") {
504+
mvndHome := strings.TrimSuffix(toolBinPath, "/bin")
505+
envVars["MVND_HOME"] = mvndHome
506+
}
507+
}
508+
509+
// Convert map back to slice
510+
env := make([]string, 0, len(envVars))
511+
for key, value := range envVars {
512+
env = append(env, fmt.Sprintf("%s=%s", key, value))
513+
}
514+
515+
return env, nil
516+
}

pkg/tools/maven.go

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,22 +135,17 @@ func (m *MavenTool) installWithFallback(version string, cfg config.ToolConfig) e
135135
return InstallError("maven", version, fmt.Errorf("failed to create install directory: %w", err))
136136
}
137137

138-
// Try primary URL first
138+
// Try both URLs with reduced retries instead of exhausting retries on first URL
139139
primaryURL := m.getDownloadURL(version)
140+
archiveURL := m.getArchiveDownloadURL(version)
140141
m.PrintDownloadMessage(version)
141142

142143
options := m.GetDownloadOptions()
143144

144-
// Try to download from primary URL
145-
archivePath, err := m.Download(primaryURL, version, cfg, options)
145+
// Try to download with alternating URLs and reduced retries per URL
146+
archivePath, err := m.downloadWithAlternatingURLs(primaryURL, archiveURL, version, cfg, options)
146147
if err != nil {
147-
// If primary URL fails, try archive URL
148-
fmt.Printf(" 🔄 Primary download failed, trying archive URL...\n")
149-
archiveURL := m.getArchiveDownloadURL(version)
150-
archivePath, err = m.Download(archiveURL, version, cfg, options)
151-
if err != nil {
152-
return InstallError("maven", version, fmt.Errorf("both primary and archive downloads failed: %w", err))
153-
}
148+
return InstallError("maven", version, fmt.Errorf("download failed from both primary and archive URLs: %w", err))
154149
}
155150
defer os.Remove(archivePath) // Clean up downloaded file
156151

@@ -392,3 +387,65 @@ func (m *MavenTool) fetchChecksumFromURL(url string) (string, error) {
392387

393388
return checksum, nil
394389
}
390+
391+
// downloadWithAlternatingURLs tries downloading from both URLs with reduced retries per URL
392+
// instead of exhausting all retries on the first URL before trying the second
393+
func (m *MavenTool) downloadWithAlternatingURLs(primaryURL, archiveURL, version string, cfg config.ToolConfig, options DownloadOptions) (string, error) {
394+
urls := []struct {
395+
url string
396+
name string
397+
}{
398+
{primaryURL, "primary"},
399+
{archiveURL, "archive"},
400+
}
401+
402+
maxRetries := 3 // Total retries across both URLs
403+
retriesPerURL := 1 // Reduced retries per URL to allow trying both
404+
405+
var lastErr error
406+
407+
for attempt := 0; attempt < maxRetries; attempt++ {
408+
urlIndex := attempt % len(urls)
409+
currentURL := urls[urlIndex]
410+
411+
if attempt > 0 && urlIndex == 0 {
412+
fmt.Printf(" 🔄 Trying %s URL again (attempt %d)...\n", currentURL.name, (attempt/len(urls))+1)
413+
} else if urlIndex == 1 {
414+
fmt.Printf(" 🔄 Switching to %s URL...\n", currentURL.name)
415+
}
416+
417+
// Create download config with reduced retries per URL
418+
downloadConfig := DefaultDownloadConfig(currentURL.url, "")
419+
downloadConfig.MaxRetries = retriesPerURL
420+
downloadConfig.ExpectedType = options.ExpectedType
421+
downloadConfig.MinSize = options.MinSize
422+
downloadConfig.MaxSize = options.MaxSize
423+
downloadConfig.ToolName = "maven"
424+
downloadConfig.Version = version
425+
downloadConfig.Config = cfg
426+
downloadConfig.ChecksumRegistry = m.manager.GetChecksumRegistry()
427+
downloadConfig.Tool = m
428+
429+
// Create temporary file for download
430+
tmpFile, err := os.CreateTemp("", fmt.Sprintf("maven-*%s", options.FileExtension))
431+
if err != nil {
432+
lastErr = fmt.Errorf("failed to create temporary file: %w", err)
433+
continue
434+
}
435+
downloadConfig.DestPath = tmpFile.Name()
436+
tmpFile.Close()
437+
438+
_, err = RobustDownload(downloadConfig)
439+
if err == nil {
440+
fmt.Printf(" ✅ Successfully downloaded from %s URL\n", currentURL.name)
441+
return downloadConfig.DestPath, nil
442+
}
443+
444+
lastErr = err
445+
fmt.Printf(" ⚠️ Download from %s URL failed: %v\n", currentURL.name, err)
446+
// Clean up failed download
447+
os.Remove(downloadConfig.DestPath)
448+
}
449+
450+
return "", fmt.Errorf("all download attempts failed, last error: %w", lastErr)
451+
}

0 commit comments

Comments
 (0)