激活 Python venv 在 Go 的 os/exec 命令中。

huangapple go评论81阅读模式
英文:

Activating Python venv in Go os/exec Command

问题

我正在尝试从Go的os/execCommandRun方法中“激活”(或者更确切地说是伪激活)一个Python虚拟环境,以便在其他命令执行中使用。我知道每个命令执行实际上是一个隔离的运行,因此环境变量等不会被保留,因此我一直在尝试手动重新创建激活期间发生的环境更改。

根据文档,这应该是可能的:

你不需要专门激活一个环境;**激活只是将虚拟环境的二进制目录添加到你的路径中,这样“python”就会调用虚拟环境的Python解释器,你可以运行已安装的脚本而不必使用完整的路径。**然而,所有在虚拟环境中安装的脚本都应该可以在不激活它的情况下运行,并且会自动使用虚拟环境的Python运行。

然而,当我在Go中尝试这样做时,无法在虚拟环境中运行命令 - 例如pip install requests始终安装到全局pip缓存中。以下是我正在使用的代码:

func Run(cmd *exec.Cmd) (exitCode int, err error) {
	cmdErr := cmd.Run()
	if cmdErr != nil {	
		exitCode, err = getExitCode(cmdErr)	
	}
	return exitCode, err
}

func getExitCode(exitError error) (rc int, err error) {
	if exitErrorOnly, ok := exitError.(*exec.ExitError); ok {
		waitStatus := exitErrorOnly.Sys().(syscall.WaitStatus)
		rc = waitStatus.ExitStatus()
	} else {
		err = fmt.Errorf("could not get exit code, using default")
	}
	return rc, err
}

func main() {
    // using pre-existing venv for testing
    const venv = "C:\\Users\\acalder\\Projects\\go\\runinvenv\\venv"

    cmd := exec.Command("pip", "install", "requests")
	cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
	cmd.Env = append(os.Environ(),
        // these were the only ones i could see changing on 'activation'
		"VIRTUAL_ENV=" + venv,
		"PATH=" + venv + "\\Scripts;" + os.Getenv("PATH"),
	)
	exitCode, err := Run(cmd)
	fmt.Println("exitCode:", exitCode)
	fmt.Println("err:", err)
}

如我在下面的评论中提到的,不幸的是,LookPath在查找PATH时总是使用os.Environ()而不是cmd.Env。因此,如果你想避免指定可执行文件的cmd.Path,你将不得不修改操作系统环境本身。使用maxm的建议,这是我想出的解决方案 - 它实际上与venv/Scripts/Activatevenv/Scripts/Deactivate文件非常相似:

// file: venv_run_windows.go
func activateVenv(old_path, venv_path string) (err error) {
	err = os.Setenv("PATH", filepath.Join(venv_path, "Scripts") + ";" + old_path)
	if err != nil {
		return err
	}
	return os.Setenv("VIRTUAL_ENV", venv_path)
}

func deactivateVenv(old_path string) (err error) {
	err = os.Setenv("PATH", old_path)
	if err != nil {
		return err
	}
	return os.Unsetenv("VIRTUAL_ENV")
}

func VenvRun(cmd *exec.Cmd) (exitCode int, err error){
    old_path := os.Getenv("PATH")
	err = activateVenv(old_path, venv)
	if (err != nil) { 
		return exitCode, err
	}
	defer deactivateVenv(old_path)
    return Run(cmd)
}
英文:

I am trying to "activate" (or rather pseudo-activate) a python virtual environment from the Go os/exec Command & Run methods for use in other command executions. I am aware that each command execution is effectively an isolate run, so environment variables etc are not retained, thus I have been attempting to manually recreate the environment changes that occur during activation.

According to the docs, this should be possible:

> You don’t specifically need to activate an environment; activation just prepends the virtual environment’s binary directory to your path, so that “python” invokes the virtual environment’s Python interpreter and you can run installed scripts without having to use their full path. However, all scripts installed in a virtual environment should be runnable without activating it, and run with the virtual environment’s Python automatically.

However, when I attempt this in Go, I cannot get commands to run in the virtual environment - for example pip install requests always installs to global pip cache. Below is the code I am using:

  func Run(cmd *exec.Cmd) (exitCode int, err error) {
	cmdErr := cmd.Run()
	if cmdErr != nil {	
		exitCode, err = getExitCode(cmdErr)	
	}
	return exitCode, err
  }

  func getExitCode(exitError error) (rc int, err error) {
	if exitErrorOnly, ok := exitError.(*exec.ExitError); ok {
		waitStatus := exitErrorOnly.Sys().(syscall.WaitStatus)
		rc = waitStatus.ExitStatus()
	} else {
		err = fmt.Errorf("could not get exit code, using default")
	}
	return rc, err
  }

  func main() {
    // using pre-existing venv for testing
    const venv = "C:\\Users\\acalder\\Projects\\go\\runinvenv\\venv"

    cmd := exec.Command("pip", "install", "requests")
	cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
	cmd.Env = append(os.Environ(),
        // these were the only ones i could see changing on 'activation'
		"VIRTUAL_ENV=" + venv,
		"PATH=" + venv + "\\Scripts;" + os.Getenv("PATH"),
	)
	exitCode, err := Run(cmd)
	fmt.Println("exitCode:", exitCode)
	fmt.Println("err:", err)
  }

As I mentioned in a comment below; unfortunately it seems that LookPath will always use os.Environ() not cmd.Env, when looking up PATH. Thus, if you want to avoid specifying the cmd.Path to the executable you will have to modify the os environment itself. Using maxm's suggestions, this is the solution I came up with - it is actually very similar to the venv/Scripts/Activate & venv/Scripts/Deactivate files:

// file: venv_run_windows.go
func activateVenv(old_path, venv_path string) (err error) {
	err = os.Setenv("PATH", filepath.Join(venv_path, "Scripts") + ";" + old_path)
	if err != nil {
		return err
	}
	return os.Setenv("VIRTUAL_ENV", venv_path)
}

func deactivateVenv(old_path string) (err error) {
	err = os.Setenv("PATH", old_path)
	if err != nil {
		return err
	}
	return os.Unsetenv("VIRTUAL_ENV")
}

func VenvRun(cmd *exec.Cmd) (exitCode int, err error){
    old_path := os.Getenv("PATH")
	err = activateVenv(old_path, venv)
	if (err != nil) { 
		return exitCode, err
	}
	defer deactivateVenv(old_path)
    return Run(cmd)
}

答案1

得分: 2

当你运行以下代码时:

cmd := exec.Command("pip", "install", "requests")

Go调用 exec.LookPath 来查找 pip 可执行文件的文件路径。由于你在调用 exec.Command 之后添加了 PATH 调整到环境变量中,cmd.Path 将指向你的系统 Python。你可以在调用 exec.Command 之后打印 cmd.Path 来确认这一点。

我建议将 "pip" 替换为 venv 中 "pip" 可执行文件的位置。(提前抱歉,我不了解 Windows)类似于:

cmd := exec.Command("C:\\Users\\acalder\\Projects\\go\\runinvenv\\venv\\bin\\pip", "install", "requests")

或者:

cmd := exec.Command("pip", "install", "requests")
cmd.Path = "C:\\Users\\acalder\\Projects\\go\\runinvenv\\venv\\bin\\pip"

由于 exec.LookPath 依赖于 os.Getenv,另外我认为这种方法也可以:

os.Setenv("PATH", venv + "\\Scripts;" + os.Getenv("PATH"))
cmd := exec.Command("pip", "install", "requests")

一旦你让它正常工作,并且 "pip" 指向正确的位置,我猜你仍然需要更新 cmd.Env(就像你已经做的那样),以便任何对 "pip" 或 "python" 的底层调用也使用你的 venv 中的正确可执行文件。

英文:

When you run:

cmd := exec.Command("pip", "install", "requests")

Go calls exec.LookPath to find the filepath of the pip executable. Since you add the PATH adjustment to the environment variables after exec.Command call has been made, cmd.Path will point to your system python. You can confirm this by printing cmd.Path after exec.Command is called.

I would suggest replacing "pip" with the location to the "pip" executable within the venv. (Sorry in advance, I don't understand windows) Something like:

cmd := exec.Command("C:\\Users\\acalder\\Projects\\go\\runinvenv\\venv\\bin\\pip", "install", "requests")

Or:

    cmd := exec.Command("pip", "install", "requests")
    cmd.Path = "C:\\Users\\acalder\\Projects\\go\\runinvenv\\venv\\bin\\pip"

Since exec.LookPath relies on os.Getenv, alternatively I think this would work as well:

os.Setenv("PATH",  venv + "\\Scripts;" + os.Getenv("PATH"))
cmd := exec.Command("pip", "install", "requests")

Once you get that working and "pip" is pointed at the right location I would guess that you still need to updated cmd.Env (as you already have) so that any underlying calls to "pip" or "python" also use the right executables in your venv.

huangapple
  • 本文由 发表于 2021年12月13日 04:35:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/70327494.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定