Skip to content

Commit 87a964c

Browse files
authored
🐍 Add comprehensive Python support with complete project isolation (#1)
✨ Features: - Project-local virtual environments for complete isolation - Automatic requirements.txt installation in isolated environments - System Python protection (no global modifications) - Cross-platform support (Linux, macOS, Windows) - Environment variable management for project isolation 🔧 Implementation: - New PythonTool with isolation methods in pkg/tools/python.go - Project-specific venv creation in {project}/.mvx/venv/ - Automatic dependency installation from requirements files - Environment setup with PYTHONPATH, VIRTUAL_ENV, etc. - Integration with existing tool management system - Command-line interface integration in cmd/tools.go 🧪 Testing: - Comprehensive test suite for Python isolation features - All existing tests continue to pass - Cross-platform compatibility verified 📚 Documentation: - Updated README with Python examples and configuration - Comprehensive tools documentation with isolation best practices - Website content updated with Python ecosystem section 🎯 Benefits: - Zero cross-project contamination - No system Python modifications - Seamless project switching - Automatic dependency management - Complete environment isolation Each project gets its own isolated Python environment in: {project}/.mvx/venv/ Follows existing tool patterns while adding Python-specific isolation features essential for Python development. 🔧 CI Fixes: - Fixed Go code formatting with gofmt - Restored mvx bootstrap script (was accidentally compiled binary) - Improved venv storage from global to project-local design
1 parent 5be3d30 commit 87a964c

File tree

8 files changed

+1177
-37
lines changed

8 files changed

+1177
-37
lines changed

.mvx/config.json5

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,34 @@
2020
node: {
2121
version: "lts"
2222
},
23+
python: {
24+
version: "3.12"
25+
},
2326
go: {
2427
version: "1.24.2"
2528
}
2629
},
2730

2831
// Custom commands
2932
commands: {
30-
build: {
31-
description: "Build mvx binary with platform-optimized settings",
32-
script: "go build -o mvx-binary .",
33-
override: true
34-
},
35-
36-
test: {
37-
description: "Run all tests",
38-
script: "go test -v ./...",
39-
override: true
33+
deps: {
34+
description: "Download dependencies",
35+
script: "go mod download && go mod tidy"
4036
},
4137

42-
"build-all": {
43-
description: "Build for all platforms",
44-
script: "./scripts/build-all.sh"
38+
hello: {
39+
description: "Say hello",
40+
script: "echo Hello from mvx! && mvn -v"
4541
},
4642

47-
checksums: {
48-
description: "Generate checksums for release binaries",
49-
script: "./scripts/checksums.sh"
43+
benchmark: {
44+
description: "Run benchmarks",
45+
script: "cd test && go test -bench=. -benchtime=3s ./..."
5046
},
5147

52-
clean: {
53-
description: "Clean build artifacts",
54-
script: "rm -f mvx-binary mvx-dev && rm -rf dist/ && rm -rf .mvx/local/ .mvx/tools/ .mvx/versions/",
48+
build: {
49+
description: "Build mvx binary with platform-optimized settings",
50+
script: "go build -o mvx-binary .",
5551
override: true
5652
},
5753

@@ -65,26 +61,17 @@
6561
script: "golangci-lint run"
6662
},
6763

68-
deps: {
69-
description: "Download dependencies",
70-
script: "go mod download && go mod tidy"
64+
test: {
65+
description: "Run all tests",
66+
script: "go test -v ./...",
67+
override: true
7168
},
7269

7370
"test-integration": {
7471
description: "Run integration tests",
7572
script: "cd test && go test -v -timeout=5m ./..."
7673
},
7774

78-
benchmark: {
79-
description: "Run benchmarks",
80-
script: "cd test && go test -bench=. -benchtime=3s ./..."
81-
},
82-
83-
hello: {
84-
description: "Say hello",
85-
script: "echo Hello from mvx! && mvn -v"
86-
},
87-
8875
"website build": {
8976
description: "Build the website using ROQ",
9077
script: "cd website && QUARKUS_ROQ_GENERATOR_BATCH=true mvn clean package quarkus\\:run --no-transfer-progress",
@@ -95,6 +82,22 @@
9582
description: "Serve the website using ROQ dev mode",
9683
script: "cd website && mvn quarkus\\:dev -Dquarkus.http.port=8081 --no-transfer-progress",
9784
override: true
85+
},
86+
87+
"build-all": {
88+
description: "Build for all platforms",
89+
script: "./scripts/build-all.sh"
90+
},
91+
92+
checksums: {
93+
description: "Generate checksums for release binaries",
94+
script: "./scripts/checksums.sh"
95+
},
96+
97+
clean: {
98+
description: "Clean build artifacts",
99+
script: "rm -f mvx-binary mvx-dev && rm -rf dist/ && rm -rf .mvx/local/ .mvx/tools/ .mvx/versions/",
100+
override: true
98101
}
99102
}
100103
}

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ The bootstrap scripts (`mvx` and `mvx.cmd`) are **shell/batch scripts** (not bin
347347
- [x] **Java Development Kit** - Multiple distributions (Temurin, GraalVM, Oracle, Corretto, Liberica, Zulu, Microsoft)
348348
- [x] **Apache Maven** - All versions (3.x, 4.x including pre-releases)
349349
- [x] **Maven Daemon (mvnd)** - High-performance Maven alternative
350+
- [x] **Node.js** - All LTS and current versions with npm/yarn support
351+
- [x] **Go** - All stable versions from golang.org
352+
- [x] **Python** - All stable versions (3.8+) with pip support
350353
- [x] Tool installation and caching
351354
- [x] Environment setup and PATH management
352355
- [x] Version resolution (latest, major.minor, exact versions)
@@ -363,7 +366,7 @@ The bootstrap scripts (`mvx` and `mvx.cmd`) are **shell/batch scripts** (not bin
363366
#### Extended Tool Support
364367

365368
- [x] **Node.js and npm/yarn support****IMPLEMENTED**
366-
- [ ] Python and pip/poetry support
369+
- [x] **Python and pip/poetry support****IMPLEMENTED**
367370
- [ ] Custom tool definitions and installers
368371

369372
#### Enhanced Commands
@@ -516,6 +519,14 @@ mvx supports both **JSON5** and **YAML** configuration formats, inspired by [Mav
516519
mvnd: {
517520
version: "1.0.2",
518521
},
522+
523+
// Python for scripting and automation (with project isolation)
524+
python: {
525+
version: "3.12",
526+
options: {
527+
requirements: "requirements.txt", // Auto-install project dependencies
528+
},
529+
},
519530
},
520531
521532
environment: {
@@ -573,6 +584,12 @@ tools:
573584
mvnd:
574585
version: "1.0.2"
575586
587+
# Python for scripting and automation (with project isolation)
588+
python:
589+
version: "3.12"
590+
options:
591+
requirements: "requirements.txt" # Auto-install project dependencies
592+
576593
environment:
577594
# Increase memory for large builds
578595
JAVA_OPTS: "-Xmx2g -XX:+UseG1GC"
@@ -630,8 +647,8 @@ The bootstrap system is fully functional and provides:
630647
**Phase 2: Multi-Tool Support** ✅ **COMPLETED**
631648

632649
- [x] Node.js and npm integration ✅ **IMPLEMENTED**
650+
- [x] Python and pip support ✅ **IMPLEMENTED**
633651
- [x] Security improvements (checksum verification) ✅ **IMPLEMENTED**
634-
- [ ] Python and pip support
635652

636653
## 🤝 Contributing
637654

cmd/tools.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,19 @@ func listTools() error {
169169
}
170170
}
171171

172+
// Python
173+
printInfo("🐍 Python Programming Language")
174+
if versions, err := registry.GetPythonVersions(); err == nil && len(versions) > 0 {
175+
shown := versions
176+
if len(versions) > 8 {
177+
shown = versions[:8]
178+
}
179+
printInfo(" Versions: %s", strings.Join(shown, ", "))
180+
if len(versions) > 8 {
181+
printInfo(" ... and %d more", len(versions)-8)
182+
}
183+
}
184+
172185
printInfo("")
173186

174187
printInfo("Usage:")
@@ -177,13 +190,15 @@ func listTools() error {
177190
printInfo(" mvx tools search mvnd # Search Maven Daemon versions")
178191
printInfo(" mvx tools search node # Search Node.js versions")
179192
printInfo(" mvx tools search go # Search Go versions")
193+
printInfo(" mvx tools search python # Search Python versions")
180194
printInfo(" mvx tools info java # Show Java details")
181195
printInfo("")
182196
printInfo("Add tools to your project:")
183197
printInfo(" mvx tools add java 21 # Add Java 21 (Temurin)")
184198
printInfo(" mvx tools add java 17 zulu # Add Java 17 (Azul Zulu)")
185199
printInfo(" mvx tools add maven 4.0.0-rc-4 # Add Maven 4.0.0-rc-4")
186200
printInfo(" mvx tools add node lts # Add Node.js LTS")
201+
printInfo(" mvx tools add python 3.12 # Add Python 3.12")
187202
printInfo(" mvx tools add go 1.23.1 # Add Go 1.23.1")
188203

189204
return nil
@@ -209,6 +224,8 @@ func searchTool(toolName string, filters []string) error {
209224
return searchNodeVersions(registry, filters)
210225
case "go":
211226
return searchGoVersions(registry, filters)
227+
case "python":
228+
return searchPythonVersions(registry, filters)
212229
default:
213230
return fmt.Errorf("unknown tool: %s", toolName)
214231
}
@@ -497,6 +514,57 @@ func searchGoVersions(registry *tools.ToolRegistry, filters []string) error {
497514
return nil
498515
}
499516

517+
// searchPythonVersions searches for Python versions
518+
func searchPythonVersions(registry *tools.ToolRegistry, filters []string) error {
519+
versions, err := registry.GetPythonVersions()
520+
if err != nil {
521+
return fmt.Errorf("failed to get Python versions: %w", err)
522+
}
523+
524+
printInfo("🐍 Python Versions")
525+
printInfo("")
526+
527+
// Group by major.minor version
528+
majorVersions := make(map[string][]string)
529+
for _, v := range versions {
530+
parts := strings.Split(v, ".")
531+
if len(parts) >= 2 {
532+
major := parts[0] + "." + parts[1]
533+
majorVersions[major] = append(majorVersions[major], v)
534+
}
535+
}
536+
537+
// Sort major versions
538+
var majors []string
539+
for major := range majorVersions {
540+
majors = append(majors, major)
541+
}
542+
sort.Slice(majors, func(i, j int) bool { return majors[i] > majors[j] })
543+
544+
for _, major := range majors {
545+
versions := majorVersions[major]
546+
printInfo("Python %s.x:", major)
547+
shown := versions
548+
if len(versions) > 5 {
549+
shown = versions[:5]
550+
}
551+
for _, v := range shown {
552+
printInfo(" %s", v)
553+
}
554+
if len(versions) > 5 {
555+
printInfo(" ... and %d more", len(versions)-5)
556+
}
557+
printInfo("")
558+
}
559+
560+
printInfo("Usage examples:")
561+
printInfo(" version: \"3.12\" # Latest Python 3.12.x")
562+
printInfo(" version: \"3.11\" # Latest Python 3.11.x")
563+
printInfo(" version: \"3.12.0\" # Exact version")
564+
565+
return nil
566+
}
567+
500568
// showToolInfo shows detailed information about a tool
501569
func showToolInfo(toolName string) error {
502570
manager, err := tools.NewManager()
@@ -571,10 +639,10 @@ func addTool(toolName, version, distribution string) error {
571639

572640
// Validate that the tool exists
573641
switch toolName {
574-
case "java", "maven", "mvnd", "node", "go":
642+
case "java", "maven", "mvnd", "node", "go", "python":
575643
// Valid tools
576644
default:
577-
return fmt.Errorf("unknown tool: %s (supported: java, maven, mvnd, node, go)", toolName)
645+
return fmt.Errorf("unknown tool: %s (supported: java, maven, mvnd, node, go, python)", toolName)
578646
}
579647

580648
// Validate version exists for the tool
@@ -723,6 +791,22 @@ func validateToolVersion(registry *tools.ToolRegistry, toolName, version, distri
723791
printInfo("🔍 Go %s - version will be resolved during installation", version)
724792
return nil
725793

794+
case "python":
795+
versions, err := registry.GetPythonVersions()
796+
if err != nil {
797+
return fmt.Errorf("failed to get Python versions: %w", err)
798+
}
799+
800+
// Check if exact version exists
801+
for _, v := range versions {
802+
if v == version {
803+
return nil
804+
}
805+
}
806+
807+
printInfo("🔍 Python %s - version will be resolved during installation", version)
808+
return nil
809+
726810
default:
727811
return fmt.Errorf("unknown tool: %s", toolName)
728812
}

pkg/tools/manager.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func NewManager() (*Manager, error) {
7676
manager.RegisterTool(&MvndTool{manager: manager})
7777
manager.RegisterTool(&NodeTool{manager: manager})
7878
manager.RegisterTool(&GoTool{manager: manager})
79+
manager.RegisterTool(&PythonTool{manager: manager})
7980

8081
return manager, nil
8182
}
@@ -352,6 +353,71 @@ func (m *Manager) SetupEnvironment(cfg *config.Config) (map[string]string, error
352353
env["MAVEN_HOME"] = toolPath
353354
case "node":
354355
env["NODE_HOME"] = toolPath
356+
case "python":
357+
env["PYTHON_HOME"] = toolPath
358+
}
359+
}
360+
361+
return env, nil
362+
}
363+
364+
// SetupProjectEnvironment sets up project-specific environment for tools like Python
365+
func (m *Manager) SetupProjectEnvironment(cfg *config.Config, projectPath string) (map[string]string, error) {
366+
env := make(map[string]string)
367+
368+
// Copy base environment
369+
baseEnv, err := m.SetupEnvironment(cfg)
370+
if err != nil {
371+
return nil, err
372+
}
373+
for key, value := range baseEnv {
374+
env[key] = value
375+
}
376+
377+
// Handle Python project-specific environment
378+
if pythonConfig, exists := cfg.Tools["python"]; exists {
379+
tool, err := m.GetTool("python")
380+
if err != nil {
381+
return env, nil // Skip if Python tool not available
382+
}
383+
384+
pythonTool, ok := tool.(*PythonTool)
385+
if !ok {
386+
return env, nil // Skip if not a Python tool
387+
}
388+
389+
// Create project virtual environment if it doesn't exist
390+
if err := pythonTool.CreateProjectVenv(pythonConfig.Version, pythonConfig, projectPath); err != nil {
391+
return nil, fmt.Errorf("failed to create Python virtual environment: %w", err)
392+
}
393+
394+
// Get project-specific Python environment
395+
pythonEnv, err := pythonTool.GetProjectEnvironment(pythonConfig.Version, pythonConfig, projectPath)
396+
if err != nil {
397+
return nil, fmt.Errorf("failed to get Python project environment: %w", err)
398+
}
399+
400+
// Merge Python environment variables
401+
for key, value := range pythonEnv {
402+
env[key] = value
403+
}
404+
405+
// Install requirements if specified
406+
if requirementsFile, ok := pythonConfig.Options["requirements"]; ok {
407+
if err := pythonTool.InstallProjectRequirements(pythonConfig.Version, pythonConfig, projectPath, requirementsFile); err != nil {
408+
return nil, fmt.Errorf("failed to install Python requirements: %w", err)
409+
}
410+
} else {
411+
// Try common requirements file names
412+
for _, reqFile := range []string{"requirements.txt", "requirements.pip", "pyproject.toml"} {
413+
if _, err := os.Stat(filepath.Join(projectPath, reqFile)); err == nil {
414+
if err := pythonTool.InstallProjectRequirements(pythonConfig.Version, pythonConfig, projectPath, reqFile); err != nil {
415+
// Don't fail if requirements installation fails, just warn
416+
fmt.Printf(" ⚠️ Warning: failed to install requirements from %s: %v\n", reqFile, err)
417+
}
418+
break
419+
}
420+
}
355421
}
356422
}
357423

@@ -375,6 +441,8 @@ func (m *Manager) resolveVersion(toolName string, toolConfig config.ToolConfig)
375441
return m.registry.ResolveNodeVersion(toolConfig.Version)
376442
case "go":
377443
return m.registry.ResolveGoVersion(toolConfig.Version)
444+
case "python":
445+
return m.registry.ResolvePythonVersion(toolConfig.Version)
378446
default:
379447
// For unknown tools, return version as-is
380448
return toolConfig.Version, nil

0 commit comments

Comments
 (0)