Home > Software design >  Activating Python venv in Go os/exec Command
Activating Python venv in Go os/exec Command

Time:12-13

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)
}

CodePudding user response:

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.

  • Related