From 7f748b322b97545d2306e9a6fcd3be325b0890c5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Wed, 20 Nov 2024 18:28:33 +0800 Subject: [PATCH] refactor env --- cmd/internal/install/deps.go | 4 +- cmd/internal/install/env.go | 97 --------- cmd/internal/install/golang.go | 5 +- cmd/internal/install/mingw.go | 4 +- cmd/internal/install/python.go | 67 +----- cmd/internal/install/python_test.go | 135 +----------- cmd/internal/install/tiny_pkg_config.go | 4 +- cmd/internal/rungo/run.go | 28 ++- internal/env/env.go | 195 ++++++++++++++++++ internal/env/env_test.go | 144 +++++++++++++ .../python/env.go => internal/env/pyenv.go | 18 +- 11 files changed, 384 insertions(+), 317 deletions(-) delete mode 100644 cmd/internal/install/env.go create mode 100644 internal/env/env.go create mode 100644 internal/env/env_test.go rename cmd/internal/python/env.go => internal/env/pyenv.go (77%) diff --git a/cmd/internal/install/deps.go b/cmd/internal/install/deps.go index 1e90d39..eaf7ee0 100644 --- a/cmd/internal/install/deps.go +++ b/cmd/internal/install/deps.go @@ -5,6 +5,8 @@ import ( "os" "os/exec" "runtime" + + "github.com/cpunion/go-python/internal/env" ) // Dependencies installs all required dependencies for the project @@ -22,7 +24,7 @@ func Dependencies(projectPath string, goVersion, tinyPkgConfigVersion, pyVersion if err := installGo(projectPath, goVersion, verbose); err != nil { return err } - SetEnv(projectPath) + env.SetBuildEnv(projectPath) // Install Go dependencies if err := installGoDeps(projectPath); err != nil { diff --git a/cmd/internal/install/env.go b/cmd/internal/install/env.go deleted file mode 100644 index aa2e444..0000000 --- a/cmd/internal/install/env.go +++ /dev/null @@ -1,97 +0,0 @@ -package install - -import ( - "os" - "path/filepath" - "runtime" -) - -const ( - // DepsDir is the directory for all dependencies - DepsDir = ".deps" - // PythonDir is the directory name for Python installation - PythonDir = "python" - // GoDir is the directory name for Go installation - GoDir = "go" - // MingwDir is the directory name for Mingw installation - MingwDir = "mingw" - MingwRoot = MingwDir + "/mingw64" - - TinyPkgConfigDir = "tiny-pkg-config" -) - -// GetPythonRoot returns the Python installation root path relative to project path -func GetPythonRoot(projectPath string) string { - return filepath.Join(projectPath, DepsDir, PythonDir) -} - -// GetPythonBinDir returns the Python binary directory path relative to project path -func GetPythonBinDir(projectPath string) string { - return filepath.Join(GetPythonRoot(projectPath), "bin") -} - -// GetPythonLibDir returns the Python library directory path relative to project path -func GetPythonLibDir(projectPath string) string { - return filepath.Join(GetPythonRoot(projectPath), "lib") -} - -// GetPythonPkgConfigDir returns the pkg-config directory path relative to project path -func GetPythonPkgConfigDir(projectPath string) string { - return filepath.Join(GetPythonLibDir(projectPath), "pkgconfig") -} - -// GetGoRoot returns the Go installation root path relative to project path -func GetGoRoot(projectPath string) string { - return filepath.Join(projectPath, DepsDir, GoDir) -} - -// GetGoPath returns the Go path relative to project path -func GetGoPath(projectPath string) string { - return filepath.Join(GetGoRoot(projectPath), "packages") -} - -// GetGoBinDir returns the Go binary directory path relative to project path -func GetGoBinDir(projectPath string) string { - return filepath.Join(GetGoRoot(projectPath), "bin") -} - -// GetGoCacheDir returns the Go cache directory path relative to project path -func GetGoCacheDir(projectPath string) string { - return filepath.Join(GetGoRoot(projectPath), "go-build") -} - -func GetMingwDir(projectPath string) string { - return filepath.Join(projectPath, DepsDir, MingwDir) -} - -func GetMingwRoot(projectPath string) string { - return filepath.Join(projectPath, DepsDir, MingwRoot) -} - -func GetTinyPkgConfigDir(projectPath string) string { - return filepath.Join(projectPath, DepsDir, TinyPkgConfigDir) -} - -func SetEnv(projectPath string) { - absPath, err := filepath.Abs(projectPath) - if err != nil { - panic(err) - } - path := os.Getenv("PATH") - path = GetGoBinDir(absPath) + pathSeparator() + path - if runtime.GOOS == "windows" { - path = GetMingwRoot(absPath) + pathSeparator() + path - path = GetTinyPkgConfigDir(absPath) + pathSeparator() + path - } - os.Setenv("PATH", path) - os.Setenv("GOPATH", GetGoPath(absPath)) - os.Setenv("GOROOT", GetGoRoot(absPath)) - os.Setenv("GOCACHE", GetGoCacheDir(absPath)) -} - -func pathSeparator() string { - if runtime.GOOS == "windows" { - return ";" - } - return ":" -} diff --git a/cmd/internal/install/golang.go b/cmd/internal/install/golang.go index f6f02dc..f13b23c 100644 --- a/cmd/internal/install/golang.go +++ b/cmd/internal/install/golang.go @@ -2,8 +2,9 @@ package install import ( "fmt" - "path/filepath" "runtime" + + "github.com/cpunion/go-python/internal/env" ) const ( @@ -45,7 +46,7 @@ func getGoURL(version string) string { // installGo downloads and installs Go in the project directory func installGo(projectPath, version string, verbose bool) error { - goDir := filepath.Join(projectPath, DepsDir, GoDir) + goDir := env.GetGoDir(projectPath) fmt.Printf("Installing Go %s in %s\n", version, goDir) // Get download URL url := getGoURL(version) diff --git a/cmd/internal/install/mingw.go b/cmd/internal/install/mingw.go index f8b40c2..0fd9961 100644 --- a/cmd/internal/install/mingw.go +++ b/cmd/internal/install/mingw.go @@ -2,6 +2,8 @@ package install import ( "fmt" + + "github.com/cpunion/go-python/internal/env" ) const ( @@ -10,7 +12,7 @@ const ( ) func installMingw(projectPath string, verbose bool) error { - root := GetMingwDir(projectPath) + root := env.GetMingwDir(projectPath) fmt.Printf("Installing mingw in %v\n", root) return downloadAndExtract("mingw", mingwVersion, mingwURL, root, "", verbose) } diff --git a/cmd/internal/install/python.go b/cmd/internal/install/python.go index 00b3921..7b569e4 100644 --- a/cmd/internal/install/python.go +++ b/cmd/internal/install/python.go @@ -9,7 +9,7 @@ import ( "runtime" "strings" - "github.com/cpunion/go-python/cmd/internal/python" + "github.com/cpunion/go-python/internal/env" ) const ( @@ -166,8 +166,8 @@ func generatePkgConfig(pythonPath, pkgConfigDir string) error { } // Get Python version from the environment - env := python.New(pythonPath) - pythonBin, err := env.Python() + pyEnv := env.NewPythonEnv(pythonPath) + pythonBin, err := pyEnv.Python() if err != nil { return fmt.Errorf("failed to get Python executable: %v", err) } @@ -232,8 +232,8 @@ Cflags: -I${includedir} // updatePkgConfig updates the prefix in pkg-config files to use absolute path func updatePkgConfig(projectPath string) error { - pythonPath := GetPythonRoot(projectPath) - pkgConfigDir := GetPythonPkgConfigDir(projectPath) + pythonPath := env.GetPythonRoot(projectPath) + pkgConfigDir := env.GetPythonPkgConfigDir(projectPath) if runtime.GOOS == "windows" { if err := generatePkgConfig(pythonPath, pkgConfigDir); err != nil { @@ -326,59 +326,10 @@ func updatePkgConfig(projectPath string) error { return nil } -// writeEnvFile writes environment variables to .python/env.txt -func writeEnvFile(projectPath string) error { - pythonDir := GetPythonRoot(projectPath) - absPath, err := filepath.Abs(pythonDir) - if err != nil { - return fmt.Errorf("failed to get absolute path: %v", err) - } - - // Get Python path using python executable - env := python.New(absPath) - pythonBin, err := env.Python() - if err != nil { - return fmt.Errorf("failed to get Python executable: %v", err) - } - - // Execute Python to get sys.path - cmd := exec.Command(pythonBin, "-c", "import sys; print(':'.join(sys.path))") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to get Python path: %v", err) - } - - // Prepare environment variables - envVars := []string{ - fmt.Sprintf("PKG_CONFIG_PATH=%s", filepath.Join(absPath, "lib", "pkgconfig")), - fmt.Sprintf("PYTHONPATH=%s", strings.TrimSpace(string(output))), - fmt.Sprintf("PYTHONHOME=%s", absPath), - } - - // Write to env.txt - envFile := filepath.Join(pythonDir, "env.txt") - if err := os.WriteFile(envFile, []byte(strings.Join(envVars, "\n")), 0644); err != nil { - return fmt.Errorf("failed to write env file: %v", err) - } - - return nil -} - -// LoadEnvFile loads environment variables from .python/env.txt in the given directory -func LoadEnvFile(dir string) ([]string, error) { - envFile := filepath.Join(GetPythonRoot(dir), "env.txt") - content, err := os.ReadFile(envFile) - if err != nil { - return nil, fmt.Errorf("failed to read env file: %v", err) - } - - return strings.Split(strings.TrimSpace(string(content)), "\n"), nil -} - // installPythonEnv downloads and installs Python standalone build func installPythonEnv(projectPath string, version, buildDate string, freeThreaded, debug bool, verbose bool) error { fmt.Printf("Installing Python %s in %s\n", version, projectPath) - pythonDir := GetPythonRoot(projectPath) + pythonDir := env.GetPythonRoot(projectPath) // Remove existing Python directory if it exists if err := os.RemoveAll(pythonDir); err != nil { @@ -401,13 +352,13 @@ func installPythonEnv(projectPath string, version, buildDate string, freeThreade } // Create Python environment - env := python.New(pythonDir) + pyEnv := env.NewPythonEnv(pythonDir) if verbose { fmt.Println("Installing Python dependencies...") } - if err := env.RunPip("install", "--upgrade", "pip", "setuptools", "wheel"); err != nil { + if err := pyEnv.RunPip("install", "--upgrade", "pip", "setuptools", "wheel"); err != nil { return fmt.Errorf("error upgrading pip, setuptools, whell") } @@ -416,7 +367,7 @@ func installPythonEnv(projectPath string, version, buildDate string, freeThreade } // Write environment variables to env.txt - if err := writeEnvFile(projectPath); err != nil { + if err := env.WriteEnvFile(projectPath); err != nil { return fmt.Errorf("error writing environment file: %v", err) } diff --git a/cmd/internal/install/python_test.go b/cmd/internal/install/python_test.go index 2a5819d..f9a9b3f 100644 --- a/cmd/internal/install/python_test.go +++ b/cmd/internal/install/python_test.go @@ -4,10 +4,10 @@ import ( "fmt" "os" "path/filepath" - "reflect" - "runtime" "strings" "testing" + + "github.com/cpunion/go-python/internal/env" ) func TestGetPythonURL(t *testing.T) { @@ -161,52 +161,11 @@ func TestGetCacheDir(t *testing.T) { }) } -func TestLoadEnvFile(t *testing.T) { - t.Run("valid env file", func(t *testing.T) { - // Create temporary directory structure - tmpDir := t.TempDir() - pythonDir := GetPythonRoot(tmpDir) - if err := os.MkdirAll(pythonDir, 0755); err != nil { - t.Fatal(err) - } - - // Create test env.txt file - envContent := []string{ - "PKG_CONFIG_PATH=/test/lib/pkgconfig", - "PYTHONPATH=/test/lib/python3.9", - "PYTHONHOME=/test", - } - envFile := filepath.Join(pythonDir, "env.txt") - if err := os.WriteFile(envFile, []byte(strings.Join(envContent, "\n")), 0644); err != nil { - t.Fatal(err) - } - - // Test loading the env file - got, err := LoadEnvFile(tmpDir) - if err != nil { - t.Errorf("LoadEnvFile() error = %v, want nil", err) - return - } - - if !reflect.DeepEqual(got, envContent) { - t.Errorf("LoadEnvFile() = %v, want %v", got, envContent) - } - }) - - t.Run("missing env file", func(t *testing.T) { - tmpDir := t.TempDir() - _, err := LoadEnvFile(tmpDir) - if err == nil { - t.Error("LoadEnvFile() error = nil, want error for missing env file") - } - }) -} - func TestUpdatePkgConfig(t *testing.T) { t.Run("valid pkg-config files", func(t *testing.T) { // Create temporary directory structure tmpDir := t.TempDir() - pkgConfigDir := GetPythonPkgConfigDir(tmpDir) + pkgConfigDir := env.GetPythonPkgConfigDir(tmpDir) if err := os.MkdirAll(pkgConfigDir, 0755); err != nil { t.Fatal(err) } @@ -268,91 +227,3 @@ func TestUpdatePkgConfig(t *testing.T) { } }) } - -func TestWriteEnvFile(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode") - } - - t.Run("write env file", func(t *testing.T) { - // Create temporary directory structure - tmpDir := t.TempDir() - pythonDir := GetPythonRoot(tmpDir) - binDir := GetPythonBinDir(tmpDir) - if err := os.MkdirAll(binDir, 0755); err != nil { - t.Fatal(err) - } - - // Create mock Python executable - var pythonPath string - if runtime.GOOS == "windows" { - pythonPath = filepath.Join(binDir, "python.exe") - pythonScript := `@echo off -echo /mock/path1;/mock/path2 -` - if err := os.WriteFile(pythonPath, []byte(pythonScript), 0644); err != nil { - t.Fatal(err) - } - } else { - pythonPath = filepath.Join(binDir, "python") - pythonScript := `#!/bin/sh -echo "/mock/path1:/mock/path2" -` - if err := os.WriteFile(pythonPath, []byte(pythonScript), 0755); err != nil { - t.Fatal(err) - } - } - - // Test writing env file - if err := writeEnvFile(tmpDir); err != nil { - t.Errorf("writeEnvFile() error = %v, want nil", err) - return - } - - // Verify the env file was created - envFile := filepath.Join(pythonDir, "env.txt") - if _, err := os.Stat(envFile); os.IsNotExist(err) { - t.Error("writeEnvFile() did not create env.txt") - return - } - - // Read and verify content - content, err := os.ReadFile(envFile) - if err != nil { - t.Errorf("Failed to read env.txt: %v", err) - return - } - - // Get expected path separator - pathSep := ":" - if runtime.GOOS == "windows" { - pathSep = ";" - } - - // Verify the content contains expected environment variables - envContent := string(content) - expectedVars := []string{ - fmt.Sprintf("PKG_CONFIG_PATH=%s", filepath.Join(pythonDir, "lib", "pkgconfig")), - fmt.Sprintf("PYTHONPATH=/mock/path1%s/mock/path2", pathSep), - fmt.Sprintf("PYTHONHOME=%s", pythonDir), - } - - for _, v := range expectedVars { - if !strings.Contains(envContent, v) { - t.Errorf("env.txt missing expected variable %s", v) - } - } - }) - - t.Run("missing python executable", func(t *testing.T) { - tmpDir := t.TempDir() - if err := os.MkdirAll(filepath.Join(tmpDir, ".deps/python"), 0755); err != nil { - t.Fatal(err) - } - - err := writeEnvFile(tmpDir) - if err == nil { - t.Error("writeEnvFile() error = nil, want error for missing python executable") - } - }) -} diff --git a/cmd/internal/install/tiny_pkg_config.go b/cmd/internal/install/tiny_pkg_config.go index 83e6a17..d15748f 100644 --- a/cmd/internal/install/tiny_pkg_config.go +++ b/cmd/internal/install/tiny_pkg_config.go @@ -6,6 +6,8 @@ import ( "path/filepath" "runtime" "strings" + + "github.com/cpunion/go-python/internal/env" ) const ( @@ -13,7 +15,7 @@ const ( ) func installTinyPkgConfig(projectPath, version string, verbose bool) error { - dir := GetTinyPkgConfigDir(projectPath) + dir := env.GetTinyPkgConfigDir(projectPath) // Determine OS and architecture goos := runtime.GOOS arch := runtime.GOARCH diff --git a/cmd/internal/rungo/run.go b/cmd/internal/rungo/run.go index 2e5cb9c..2b4e540 100644 --- a/cmd/internal/rungo/run.go +++ b/cmd/internal/rungo/run.go @@ -10,7 +10,7 @@ import ( "runtime" "strings" - "github.com/cpunion/go-python/cmd/internal/install" + "github.com/cpunion/go-python/internal/env" ) type ListInfo struct { @@ -93,34 +93,30 @@ func RunGoCommand(command string, args []string) error { return fmt.Errorf("failed to parse module info: %v", err) } projectRoot := listInfo.Root - install.SetEnv(projectRoot) + env.SetBuildEnv(projectRoot) // Set up environment variables - env := os.Environ() + goEnv := []string{} // Get PYTHONPATH and PYTHONHOME from env.txt var pythonPath, pythonHome string - if additionalEnv, err := install.LoadEnvFile(projectRoot); err == nil { - env = append(env, additionalEnv...) - // Extract PYTHONPATH and PYTHONHOME from additionalEnv - for _, envVar := range additionalEnv { - if strings.HasPrefix(envVar, "PYTHONPATH=") { - pythonPath = strings.TrimPrefix(envVar, "PYTHONPATH=") - } else if strings.HasPrefix(envVar, "PYTHONHOME=") { - pythonHome = strings.TrimPrefix(envVar, "PYTHONHOME=") - } + if additionalEnv, err := env.ReadEnv(projectRoot); err == nil { + for key, value := range additionalEnv { + goEnv = append(goEnv, key+"="+value) } + pythonPath = additionalEnv["PYTHONPATH"] + pythonHome = additionalEnv["PYTHONHOME"] } else { fmt.Fprintf(os.Stderr, "Warning: could not load environment variables: %v\n", err) } // Process args to inject Python paths via ldflags - processedArgs := ProcessArgsWithLDFlags(args, pythonPath, pythonHome) + processedArgs := ProcessArgsWithLDFlags(args, projectRoot, pythonPath, pythonHome) // Prepare go command with processed arguments goArgs := append([]string{"go", command}, processedArgs...) cmd = exec.Command(goArgs[0], goArgs[1:]...) - cmd.Env = env + cmd.Env = append(goEnv, os.Environ()...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if command == "run" { @@ -139,13 +135,13 @@ func RunGoCommand(command string, args []string) error { } // ProcessArgsWithLDFlags processes command line arguments to inject Python paths via ldflags -func ProcessArgsWithLDFlags(args []string, pythonPath, pythonHome string) []string { +func ProcessArgsWithLDFlags(args []string, projectRoot, pythonPath, pythonHome string) []string { result := make([]string, 0, len(args)) // Prepare the -X flags we want to add var xFlags []string if pythonHome != "" { - xFlags = append(xFlags, fmt.Sprintf("-X 'github.com/cpunion/go-python.PythonHome=%s'", pythonHome)) + xFlags = append(xFlags, fmt.Sprintf("-X 'github.com/cpunion/go-python.ProjectRoot=%s'", projectRoot)) } // Prepare rpath flag if needed diff --git a/internal/env/env.go b/internal/env/env.go new file mode 100644 index 0000000..0c37bee --- /dev/null +++ b/internal/env/env.go @@ -0,0 +1,195 @@ +package env + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +const ( + // depsDir is the directory for all dependencies + depsDir = ".deps" + // pyDir is the directory name for Python installation + pyDir = "python" + // goDir is the directory name for Go installation + goDir = "go" + // mingwDir is the directory name for Mingw installation + mingwDir = "mingw" + mingwRoot = mingwDir + "/mingw64" + + tinyPkgConfigDir = "tiny-pkg-config" +) + +func GetDepsDir(projectPath string) string { + return filepath.Join(projectPath, depsDir) +} + +func GetGoDir(projectPath string) string { + return filepath.Join(GetDepsDir(projectPath), goDir) +} + +// GetPythonRoot returns the Python installation root path relative to project path +func GetPythonRoot(projectPath string) string { + return filepath.Join(projectPath, depsDir, pyDir) +} + +// GetPythonBinDir returns the Python binary directory path relative to project path +func GetPythonBinDir(projectPath string) string { + return filepath.Join(GetPythonRoot(projectPath), "bin") +} + +// GetPythonLibDir returns the Python library directory path relative to project path +func GetPythonLibDir(projectPath string) string { + return filepath.Join(GetPythonRoot(projectPath), "lib") +} + +// GetPythonPkgConfigDir returns the pkg-config directory path relative to project path +func GetPythonPkgConfigDir(projectPath string) string { + return filepath.Join(GetPythonLibDir(projectPath), "pkgconfig") +} + +// GetGoRoot returns the Go installation root path relative to project path +func GetGoRoot(projectPath string) string { + return filepath.Join(projectPath, depsDir, goDir) +} + +// GetGoPath returns the Go path relative to project path +func GetGoPath(projectPath string) string { + return filepath.Join(GetGoRoot(projectPath), "packages") +} + +// GetGoBinDir returns the Go binary directory path relative to project path +func GetGoBinDir(projectPath string) string { + return filepath.Join(GetGoRoot(projectPath), "bin") +} + +// GetGoCacheDir returns the Go cache directory path relative to project path +func GetGoCacheDir(projectPath string) string { + return filepath.Join(GetGoRoot(projectPath), "go-build") +} + +func GetMingwDir(projectPath string) string { + return filepath.Join(projectPath, depsDir, mingwDir) +} + +func GetMingwRoot(projectPath string) string { + return filepath.Join(projectPath, depsDir, mingwRoot) +} + +func GetTinyPkgConfigDir(projectPath string) string { + return filepath.Join(projectPath, depsDir, tinyPkgConfigDir) +} + +func GetEnvConfigPath(projectPath string) string { + return filepath.Join(GetDepsDir(projectPath), "env.txt") +} + +func SetBuildEnv(projectPath string) { + absPath, err := filepath.Abs(projectPath) + if err != nil { + panic(err) + } + path := os.Getenv("PATH") + path = GetGoBinDir(absPath) + pathSeparator() + path + if runtime.GOOS == "windows" { + path = GetMingwRoot(absPath) + pathSeparator() + path + path = GetTinyPkgConfigDir(absPath) + pathSeparator() + path + } + os.Setenv("PATH", path) + os.Setenv("GOPATH", GetGoPath(absPath)) + os.Setenv("GOROOT", GetGoRoot(absPath)) + os.Setenv("GOCACHE", GetGoCacheDir(absPath)) +} + +func pathSeparator() string { + if runtime.GOOS == "windows" { + return ";" + } + return ":" +} + +// WriteEnvFile writes environment variables to .python/env.txt +func WriteEnvFile(projectPath string) error { + pythonHome := GetPythonRoot(projectPath) + // Get Python path using python executable + env := NewPythonEnv(pythonHome) + pythonBin, err := env.Python() + if err != nil { + return fmt.Errorf("failed to get Python executable: %v", err) + } + + // Execute Python to get sys.path + cmd := exec.Command(pythonBin, "-c", "import sys; print(':'.join(sys.path))") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to get Python path: %v", err) + } + + // Prepare environment variables + envVars := []string{ + fmt.Sprintf("PKG_CONFIG_PATH=%s", filepath.Join(pythonHome, "lib", "pkgconfig")), + fmt.Sprintf("PYTHONPATH=%s", strings.TrimSpace(string(output))), + fmt.Sprintf("PYTHONHOME=%s", pythonHome), + } + + // Write to env.txt + envFile := GetEnvConfigPath(projectPath) + if err := os.WriteFile(envFile, []byte(strings.Join(envVars, "\n")), 0644); err != nil { + return fmt.Errorf("failed to write env file: %v", err) + } + + return nil +} + +// ReadEnvFile loads environment variables from .python/env.txt in the given directory +func ReadEnvFile(projectDir string) (map[string]string, error) { + envFile := GetEnvConfigPath(projectDir) + content, err := os.ReadFile(envFile) + if err != nil { + return nil, fmt.Errorf("failed to read env file: %v", err) + } + envs := map[string]string{} + for _, line := range strings.Split(strings.TrimSpace(string(content)), "\n") { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + envs[parts[0]] = parts[1] + } + } + return envs, nil +} + +func GeneratePythonEnv(pythonHome, pythonPath string) map[string]string { + path := os.Getenv("PATH") + if runtime.GOOS == "windows" { + path = filepath.Join(pythonHome) + ";" + path + } else { + path = filepath.Join(pythonHome, "bin") + ":" + path + } + return map[string]string{ + "PYTHONHOME": pythonHome, + "PYTHONPATH": pythonPath, + "PATH": path, + } +} + +func ReadEnv(projectDir string) (map[string]string, error) { + envs, err := ReadEnvFile(projectDir) + if err != nil { + return nil, err + } + pythonHome, ok := envs["PYTHONHOME"] + if !ok { + return nil, fmt.Errorf("PYTHONHOME is not set in env.txt") + } + pythonPath, ok := envs["PYTHONPATH"] + if !ok { + return nil, fmt.Errorf("PYTHONPATH is not set in env.txt") + } + for k, v := range GeneratePythonEnv(pythonHome, pythonPath) { + envs[k] = v + } + return envs, nil +} diff --git a/internal/env/env_test.go b/internal/env/env_test.go new file mode 100644 index 0000000..b6bc864 --- /dev/null +++ b/internal/env/env_test.go @@ -0,0 +1,144 @@ +package env + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +func TestLoadEnvFile(t *testing.T) { + t.Run("valid env file", func(t *testing.T) { + // Create temporary directory structure + projectDir := t.TempDir() + pythonDir := GetPythonRoot(projectDir) + if err := os.MkdirAll(pythonDir, 0755); err != nil { + t.Fatal(err) + } + + // Create test env.txt file + envContent := map[string]string{ + "PKG_CONFIG_PATH": "/test/lib/pkgconfig", + "PYTHONPATH": "/test/lib/python3.9", + "PYTHONHOME": "/test", + } + lines := []string{} + for key, value := range envContent { + lines = append(lines, fmt.Sprintf("%s=%s", key, value)) + } + envFile := GetEnvConfigPath(projectDir) + if err := os.WriteFile(envFile, []byte(strings.Join(lines, "\n")), 0644); err != nil { + t.Fatal(err) + } + + // Test loading the env file + got, err := ReadEnvFile(projectDir) + if err != nil { + t.Errorf("LoadEnvFile() error = %v, want nil", err) + return + } + + if !reflect.DeepEqual(got, envContent) { + t.Errorf("LoadEnvFile() = %v, want %v", got, envContent) + } + }) + + t.Run("missing env file", func(t *testing.T) { + tmpDir := t.TempDir() + _, err := ReadEnvFile(tmpDir) + if err == nil { + t.Error("LoadEnvFile() error = nil, want error for missing env file") + } + }) +} + +func TestWriteEnvFile(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + t.Run("write env file", func(t *testing.T) { + // Create temporary directory structure + projectDir := t.TempDir() + pythonDir := GetPythonRoot(projectDir) + binDir := GetPythonBinDir(projectDir) + if err := os.MkdirAll(binDir, 0755); err != nil { + t.Fatal(err) + } + + // Create mock Python executable + var pythonPath string + if runtime.GOOS == "windows" { + pythonPath = filepath.Join(binDir, "python.exe") + pythonScript := `@echo off +echo /mock/path1;/mock/path2 +` + if err := os.WriteFile(pythonPath, []byte(pythonScript), 0644); err != nil { + t.Fatal(err) + } + } else { + pythonPath = filepath.Join(binDir, "python") + pythonScript := `#!/bin/sh +echo "/mock/path1:/mock/path2" +` + if err := os.WriteFile(pythonPath, []byte(pythonScript), 0755); err != nil { + t.Fatal(err) + } + } + + // Test writing env file + if err := WriteEnvFile(projectDir); err != nil { + t.Errorf("writeEnvFile() error = %v, want nil", err) + return + } + + // Verify the env file was created + envFile := GetEnvConfigPath(projectDir) + if _, err := os.Stat(envFile); os.IsNotExist(err) { + t.Error("writeEnvFile() did not create env.txt") + return + } + + // Read and verify content + content, err := os.ReadFile(envFile) + if err != nil { + t.Errorf("Failed to read env.txt: %v", err) + return + } + + // Get expected path separator + pathSep := ":" + if runtime.GOOS == "windows" { + pathSep = ";" + } + + // Verify the content contains expected environment variables + envContent := string(content) + expectedVars := []string{ + fmt.Sprintf("PKG_CONFIG_PATH=%s", filepath.Join(pythonDir, "lib", "pkgconfig")), + fmt.Sprintf("PYTHONPATH=/mock/path1%s/mock/path2", pathSep), + fmt.Sprintf("PYTHONHOME=%s", pythonDir), + } + fmt.Printf("envContent:\n%v\n", envContent) + for _, v := range expectedVars { + if !strings.Contains(envContent, v) { + t.Errorf("env.txt missing expected variable %s", v) + } + } + }) + + t.Run("missing python executable", func(t *testing.T) { + tmpDir := t.TempDir() + if err := os.MkdirAll(filepath.Join(tmpDir, ".deps/python"), 0755); err != nil { + t.Fatal(err) + } + + err := WriteEnvFile(tmpDir) + if err == nil { + t.Error("writeEnvFile() error = nil, want error for missing python executable") + } + }) +} diff --git a/cmd/internal/python/env.go b/internal/env/pyenv.go similarity index 77% rename from cmd/internal/python/env.go rename to internal/env/pyenv.go index 8d5ef1f..62abb16 100644 --- a/cmd/internal/python/env.go +++ b/internal/env/pyenv.go @@ -1,4 +1,4 @@ -package python +package env import ( "fmt" @@ -9,20 +9,20 @@ import ( "runtime" ) -// Env represents a Python environment -type Env struct { +// PythonEnv represents a Python environment +type PythonEnv struct { Root string // Root directory of the Python installation } -// New creates a new Python environment instance -func New(pythonHome string) *Env { - return &Env{ +// NewPythonEnv creates a new Python environment instance +func NewPythonEnv(pythonHome string) *PythonEnv { + return &PythonEnv{ Root: pythonHome, } } // Python returns the path to the Python executable -func (e *Env) Python() (string, error) { +func (e *PythonEnv) Python() (string, error) { binDir := e.Root if runtime.GOOS != "windows" { binDir = filepath.Join(e.Root, "bin") @@ -50,12 +50,12 @@ func (e *Env) Python() (string, error) { } // RunPip executes pip with the given arguments -func (e *Env) RunPip(args ...string) error { +func (e *PythonEnv) RunPip(args ...string) error { return e.RunPython(append([]string{"-m", "pip"}, args...)...) } // RunPython executes python with the given arguments -func (e *Env) RunPython(args ...string) error { +func (e *PythonEnv) RunPython(args ...string) error { pythonPath, err := e.Python() if err != nil { return err