Skip to content

Commit 6d0eb0d

Browse files
committed
Add generic system tool detection with MVX_USE_SYSTEM_* environment variables
Implements a generic system tool detection mechanism that allows using pre-installed tools instead of downloading them, addressing issue #2 with a scalable solution for multiple tools. Features: - Generic useSystemTool(toolName) function for any tool - SystemToolDetector interface for consistent detection pattern - Individual environment variables: MVX_USE_SYSTEM_JAVA, MVX_USE_SYSTEM_MAVEN - Extensible design for future tools (Node.js, Go, etc.) Java Support (MVX_USE_SYSTEM_JAVA=true): - Detection via JAVA_HOME environment variable - Version parsing for both old (1.8.x) and new (21.x) formats - Version compatibility validation - Symlink integration with mvx tool management Maven Support (MVX_USE_SYSTEM_MAVEN=true): - Detection via MAVEN_HOME, M2_HOME, or mvn in PATH - ANSI color code handling in version parsing - Version compatibility validation - Symlink integration with mvx tool management Benefits: - Faster CI builds (no download time) - More reliable (avoids network issues) - Better resource usage (uses existing installations) - Selective control (enable/disable per tool) - Backward compatible (no changes to existing behavior) - Robust fallback (downloads when system tools unavailable) Testing: - Comprehensive unit tests for both Java and Maven - Integration tests with proper development binary handling - Benchmark tests with correct testing.B usage - All existing tests pass with no breaking changes Documentation: - Updated README.md with multi-tool CI examples - Updated website/content/tools.md with detailed usage - GitHub Actions examples showing multiple tools Closes #2
1 parent bba9356 commit 6d0eb0d

File tree

8 files changed

+907
-6
lines changed

8 files changed

+907
-6
lines changed

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,74 @@ mvx automatically detects the configuration format:
413413
- Simplified CI/CD setup
414414
- Better collaboration with consistent tooling
415415

416+
## 🚀 CI/CD Integration
417+
418+
mvx is designed to work seamlessly in CI/CD environments. For faster builds and better reliability, you can configure mvx to use pre-installed tools instead of downloading them.
419+
420+
### Using System Tools in CI
421+
422+
When running in CI environments like GitHub Actions, the runners often have tools like Java and Maven pre-installed. You can configure mvx to use system tools instead of downloading them:
423+
424+
```bash
425+
# Use system Java
426+
export MVX_USE_SYSTEM_JAVA=true
427+
428+
# Use system Maven
429+
export MVX_USE_SYSTEM_MAVEN=true
430+
431+
# Use system Node.js (when implemented)
432+
export MVX_USE_SYSTEM_NODE=true
433+
434+
./mvx setup
435+
./mvx build
436+
```
437+
438+
**Supported Tools:**
439+
-**Java**: Uses `JAVA_HOME` environment variable
440+
-**Maven**: Uses `MAVEN_HOME`, `M2_HOME`, or finds `mvn` in PATH
441+
- 🚧 **Node.js**: Coming soon
442+
- 🚧 **Go**: Coming soon
443+
444+
**Benefits:**
445+
-**Faster builds**: No time spent downloading tools
446+
- 🛡️ **More reliable**: Avoids network/download issues
447+
- 💾 **Better resource usage**: Uses existing installations
448+
- 🎯 **Selective control**: Enable/disable per tool independently
449+
450+
**Example GitHub Actions workflow:**
451+
452+
```yaml
453+
name: Build
454+
on: [push, pull_request]
455+
456+
jobs:
457+
build:
458+
runs-on: ubuntu-latest
459+
steps:
460+
- uses: actions/checkout@v4
461+
462+
- name: Set up JDK 21
463+
uses: actions/setup-java@v4
464+
with:
465+
java-version: '21'
466+
distribution: 'temurin'
467+
468+
- name: Set up Maven
469+
uses: actions/setup-maven@v4
470+
with:
471+
maven-version: '3.9.6'
472+
473+
- name: Build with mvx
474+
env:
475+
MVX_USE_SYSTEM_JAVA: true
476+
MVX_USE_SYSTEM_MAVEN: true
477+
run: |
478+
./mvx setup
479+
./mvx build
480+
```
481+
482+
This approach works with any CI system that provides Java pre-installed or allows you to install it separately.
483+
416484
## 💡 Example Configuration
417485
418486
mvx supports both **JSON5** and **YAML** configuration formats, inspired by [Maven Mason](https://github.com/maveniverse/mason).

pkg/tools/java.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ func logVerbose(format string, args ...interface{}) {
2929
}
3030
}
3131

32+
// useSystemJava checks if system Java should be used instead of downloading
33+
func useSystemJava() bool {
34+
return useSystemTool("java")
35+
}
36+
37+
// getSystemJavaDetector returns a system detector for Java
38+
func getSystemJavaDetector() SystemToolDetector {
39+
return &JavaSystemDetector{}
40+
}
41+
3242
// JavaTool implements Tool interface for Java/JDK management
3343
type JavaTool struct {
3444
manager *Manager
@@ -48,6 +58,31 @@ func (j *JavaTool) Install(version string, cfg config.ToolConfig) error {
4858

4959
installDir := j.manager.GetToolVersionDir("java", version, distribution)
5060

61+
// Check if we should use system Java instead of downloading
62+
if useSystemJava() {
63+
logVerbose("%s=true, attempting to use system Java", getSystemToolEnvVar("java"))
64+
65+
detector := getSystemJavaDetector()
66+
systemJavaHome, err := detector.GetSystemHome()
67+
if err != nil {
68+
logVerbose("System Java not available: %v", err)
69+
fmt.Printf(" ⚠️ System Java not available (%v), falling back to download\n", err)
70+
} else {
71+
systemVersion, err := detector.GetSystemVersion(systemJavaHome)
72+
if err != nil {
73+
logVerbose("Could not determine system Java version: %v", err)
74+
fmt.Printf(" ⚠️ Could not determine system Java version (%v), falling back to download\n", err)
75+
} else if !detector.IsVersionCompatible(systemVersion, version) {
76+
logVerbose("System Java version %s does not match requested version %s", systemVersion, version)
77+
fmt.Printf(" ⚠️ System Java version %s does not match requested version %s, falling back to download\n", systemVersion, version)
78+
} else {
79+
// Use system Java by creating a symlink
80+
fmt.Printf(" 🔗 Using system Java %s from JAVA_HOME: %s\n", systemVersion, systemJavaHome)
81+
return detector.CreateSystemLink(systemJavaHome, installDir)
82+
}
83+
}
84+
}
85+
5186
// Create installation directory
5287
if err := os.MkdirAll(installDir, 0755); err != nil {
5388
return fmt.Errorf("failed to create installation directory: %w", err)
@@ -75,6 +110,20 @@ func (j *JavaTool) IsInstalled(version string, cfg config.ToolConfig) bool {
75110
distribution = "temurin"
76111
}
77112

113+
// If using system Java, check if system Java is available and compatible
114+
if useSystemJava() {
115+
detector := getSystemJavaDetector()
116+
if systemJavaHome, err := detector.GetSystemHome(); err == nil {
117+
if systemVersion, err := detector.GetSystemVersion(systemJavaHome); err == nil {
118+
if detector.IsVersionCompatible(systemVersion, version) {
119+
logVerbose("System Java %s is available and compatible with requested version %s", systemVersion, version)
120+
return true
121+
}
122+
}
123+
}
124+
// If system Java is not available or compatible, fall through to check downloaded version
125+
}
126+
78127
// Try to get the actual Java path (which handles nested directories)
79128
javaPath, err := j.GetPath(version, cfg)
80129
if err != nil {
@@ -93,6 +142,20 @@ func (j *JavaTool) GetPath(version string, cfg config.ToolConfig) (string, error
93142
distribution = "temurin"
94143
}
95144

145+
// If using system Java, return system JAVA_HOME if available and compatible
146+
if useSystemJava() {
147+
detector := getSystemJavaDetector()
148+
if systemJavaHome, err := detector.GetSystemHome(); err == nil {
149+
if systemVersion, err := detector.GetSystemVersion(systemJavaHome); err == nil {
150+
if detector.IsVersionCompatible(systemVersion, version) {
151+
logVerbose("Using system Java %s from JAVA_HOME: %s", systemVersion, systemJavaHome)
152+
return systemJavaHome, nil
153+
}
154+
}
155+
}
156+
// If system Java is not available or compatible, fall through to check downloaded version
157+
}
158+
96159
installDir := j.manager.GetToolVersionDir("java", version, distribution)
97160
logVerbose("Checking Java installation in: %s", installDir)
98161

pkg/tools/java_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package tools
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
"testing"
8+
9+
"github.com/gnodet/mvx/pkg/config"
10+
)
11+
12+
func TestUseSystemTool(t *testing.T) {
13+
// Test when MVX_USE_SYSTEM_JAVA is not set
14+
os.Unsetenv("MVX_USE_SYSTEM_JAVA")
15+
if useSystemTool("java") {
16+
t.Error("useSystemTool('java') should return false when MVX_USE_SYSTEM_JAVA is not set")
17+
}
18+
19+
// Test when MVX_USE_SYSTEM_JAVA is set to false
20+
os.Setenv("MVX_USE_SYSTEM_JAVA", "false")
21+
if useSystemTool("java") {
22+
t.Error("useSystemTool('java') should return false when MVX_USE_SYSTEM_JAVA=false")
23+
}
24+
25+
// Test when MVX_USE_SYSTEM_JAVA is set to true
26+
os.Setenv("MVX_USE_SYSTEM_JAVA", "true")
27+
if !useSystemTool("java") {
28+
t.Error("useSystemTool('java') should return true when MVX_USE_SYSTEM_JAVA=true")
29+
}
30+
31+
// Test Maven tool
32+
os.Unsetenv("MVX_USE_SYSTEM_MAVEN")
33+
if useSystemTool("maven") {
34+
t.Error("useSystemTool('maven') should return false when MVX_USE_SYSTEM_MAVEN is not set")
35+
}
36+
37+
os.Setenv("MVX_USE_SYSTEM_MAVEN", "true")
38+
if !useSystemTool("maven") {
39+
t.Error("useSystemTool('maven') should return true when MVX_USE_SYSTEM_MAVEN=true")
40+
}
41+
42+
// Clean up
43+
os.Unsetenv("MVX_USE_SYSTEM_JAVA")
44+
os.Unsetenv("MVX_USE_SYSTEM_MAVEN")
45+
}
46+
47+
func TestUseSystemJava(t *testing.T) {
48+
// Test when MVX_USE_SYSTEM_JAVA is not set
49+
os.Unsetenv("MVX_USE_SYSTEM_JAVA")
50+
if useSystemJava() {
51+
t.Error("useSystemJava() should return false when MVX_USE_SYSTEM_JAVA is not set")
52+
}
53+
54+
// Test when MVX_USE_SYSTEM_JAVA is set to false
55+
os.Setenv("MVX_USE_SYSTEM_JAVA", "false")
56+
if useSystemJava() {
57+
t.Error("useSystemJava() should return false when MVX_USE_SYSTEM_JAVA=false")
58+
}
59+
60+
// Test when MVX_USE_SYSTEM_JAVA is set to true
61+
os.Setenv("MVX_USE_SYSTEM_JAVA", "true")
62+
if !useSystemJava() {
63+
t.Error("useSystemJava() should return true when MVX_USE_SYSTEM_JAVA=true")
64+
}
65+
66+
// Clean up
67+
os.Unsetenv("MVX_USE_SYSTEM_JAVA")
68+
}
69+
70+
func TestJavaSystemDetector(t *testing.T) {
71+
detector := &JavaSystemDetector{}
72+
73+
// Save original JAVA_HOME
74+
originalJavaHome := os.Getenv("JAVA_HOME")
75+
defer func() {
76+
if originalJavaHome != "" {
77+
os.Setenv("JAVA_HOME", originalJavaHome)
78+
} else {
79+
os.Unsetenv("JAVA_HOME")
80+
}
81+
}()
82+
83+
// Test when JAVA_HOME is not set
84+
os.Unsetenv("JAVA_HOME")
85+
_, err := detector.GetSystemHome()
86+
if err == nil {
87+
t.Error("GetSystemHome() should return error when JAVA_HOME is not set")
88+
}
89+
90+
// Test when JAVA_HOME points to non-existent directory
91+
os.Setenv("JAVA_HOME", "/non/existent/path")
92+
_, err = detector.GetSystemHome()
93+
if err == nil {
94+
t.Error("GetSystemHome() should return error when JAVA_HOME points to non-existent directory")
95+
}
96+
}
97+
98+
func TestJavaSystemDetectorVersion(t *testing.T) {
99+
detector := &JavaSystemDetector{}
100+
101+
// This test requires a real Java installation, so we'll skip it if JAVA_HOME is not set
102+
javaHome := os.Getenv("JAVA_HOME")
103+
if javaHome == "" {
104+
t.Skip("Skipping test because JAVA_HOME is not set")
105+
}
106+
107+
// Check if Java executable exists
108+
javaExe := filepath.Join(javaHome, "bin", "java")
109+
if runtime.GOOS == "windows" {
110+
javaExe += ".exe"
111+
}
112+
113+
if _, err := os.Stat(javaExe); err != nil {
114+
t.Skip("Skipping test because Java executable not found at JAVA_HOME")
115+
}
116+
117+
version, err := detector.GetSystemVersion(javaHome)
118+
if err != nil {
119+
t.Errorf("GetSystemVersion() failed: %v", err)
120+
}
121+
122+
if version == "" {
123+
t.Error("GetSystemVersion() returned empty version")
124+
}
125+
126+
t.Logf("Detected Java version: %s", version)
127+
}
128+
129+
func TestJavaVersionCompatibility(t *testing.T) {
130+
detector := &JavaSystemDetector{}
131+
132+
tests := []struct {
133+
systemVersion string
134+
requestedVersion string
135+
expected bool
136+
}{
137+
{"21", "21", true},
138+
{"17", "17", true},
139+
{"11", "11", true},
140+
{"8", "8", true},
141+
{"21", "17", false},
142+
{"17", "21", false},
143+
{"11", "8", false},
144+
}
145+
146+
for _, test := range tests {
147+
result := detector.IsVersionCompatible(test.systemVersion, test.requestedVersion)
148+
if result != test.expected {
149+
t.Errorf("IsVersionCompatible(%s, %s) = %v, expected %v",
150+
test.systemVersion, test.requestedVersion, result, test.expected)
151+
}
152+
}
153+
}
154+
155+
func TestJavaToolWithSystemJava(t *testing.T) {
156+
// Save original environment variables
157+
originalUseSystemJava := os.Getenv("MVX_USE_SYSTEM_JAVA")
158+
originalJavaHome := os.Getenv("JAVA_HOME")
159+
defer func() {
160+
if originalUseSystemJava != "" {
161+
os.Setenv("MVX_USE_SYSTEM_JAVA", originalUseSystemJava)
162+
} else {
163+
os.Unsetenv("MVX_USE_SYSTEM_JAVA")
164+
}
165+
if originalJavaHome != "" {
166+
os.Setenv("JAVA_HOME", originalJavaHome)
167+
} else {
168+
os.Unsetenv("JAVA_HOME")
169+
}
170+
}()
171+
172+
// Create a mock manager
173+
manager, err := NewManager()
174+
if err != nil {
175+
t.Fatalf("Failed to create manager: %v", err)
176+
}
177+
178+
javaTool := &JavaTool{manager: manager}
179+
180+
// Test with MVX_USE_SYSTEM_JAVA=false (default behavior)
181+
os.Unsetenv("MVX_USE_SYSTEM_JAVA")
182+
os.Unsetenv("JAVA_HOME")
183+
184+
cfg := config.ToolConfig{
185+
Version: "11", // Use version 11 which won't be installed
186+
Distribution: "temurin",
187+
}
188+
189+
// IsInstalled should return false when no Java is installed and MVX_USE_SYSTEM_JAVA is not set
190+
if javaTool.IsInstalled("11", cfg) {
191+
t.Error("IsInstalled should return false when no Java is installed")
192+
}
193+
194+
// Test with MVX_USE_SYSTEM_JAVA=true but no JAVA_HOME
195+
os.Setenv("MVX_USE_SYSTEM_JAVA", "true")
196+
os.Unsetenv("JAVA_HOME")
197+
198+
// IsInstalled should still return false when JAVA_HOME is not set
199+
if javaTool.IsInstalled("11", cfg) {
200+
t.Error("IsInstalled should return false when MVX_USE_SYSTEM_JAVA=true but JAVA_HOME is not set")
201+
}
202+
203+
// Test with MVX_USE_SYSTEM_JAVA=true and JAVA_HOME set but version mismatch
204+
os.Setenv("MVX_USE_SYSTEM_JAVA", "true")
205+
os.Setenv("JAVA_HOME", "/usr/lib/jvm/java-21-openjdk-amd64")
206+
207+
// IsInstalled should return false when system Java version doesn't match
208+
if javaTool.IsInstalled("11", cfg) {
209+
t.Error("IsInstalled should return false when system Java version doesn't match requested version")
210+
}
211+
}

0 commit comments

Comments
 (0)