package python import ( "context" "errors" "fmt" "io/fs" "os" "os/exec" "path/filepath" "runtime" ) // DetectExecutable looks up the path to the python3 executable from the PATH // environment variable. // // If virtualenv is activated, executable from the virtualenv is returned, // because activating virtualenv adds python3 executable on a PATH. // // If python3 executable is not found on the PATH, the interpreter with the // least version that satisfies minimal 3.8 version is returned, e.g. // python3.10. func DetectExecutable(ctx context.Context) (string, error) { // TODO: add a shortcut if .python-version file is detected somewhere in // the parent directory tree. // // See https://github.com/pyenv/pyenv#understanding-python-version-selection out, err := exec.LookPath("python3") // most of the OS'es have python3 in $PATH, but for those which don't, // we perform the latest version lookup if err != nil && !errors.Is(err, exec.ErrNotFound) { return "", err } if out != "" { return out, nil } // otherwise, detect all interpreters and pick the least that satisfies // minimal version requirements all, err := DetectInterpreters(ctx) if err != nil { return "", err } interpreter, err := all.AtLeast("3.8") if err != nil { return "", err } return interpreter.Path, nil } // DetectVEnvExecutable returns the path to the python3 executable inside venvPath, // that is not necessarily activated. // // If virtualenv is not created, or executable doesn't exist, the error is returned. func DetectVEnvExecutable(venvPath string) (string, error) { interpreterPath := filepath.Join(venvPath, "bin", "python3") if runtime.GOOS == "windows" { interpreterPath = filepath.Join(venvPath, "Scripts", "python3.exe") } if _, err := os.Stat(interpreterPath); err != nil { if errors.Is(err, fs.ErrNotExist) { return "", fmt.Errorf("can't find %q, check if virtualenv is created", interpreterPath) } else { return "", fmt.Errorf("can't find %q: %w", interpreterPath, err) } } // pyvenv.cfg must be always present in correctly configured virtualenv, // read more in https://snarky.ca/how-virtual-environments-work/ pyvenvPath := filepath.Join(venvPath, "pyvenv.cfg") if _, err := os.Stat(pyvenvPath); err != nil { if errors.Is(err, fs.ErrNotExist) { return "", fmt.Errorf("expected %q to be virtualenv, but pyvenv.cfg is missing", venvPath) } else { return "", fmt.Errorf("can't find %q: %w", pyvenvPath, err) } } return interpreterPath, nil }