I'm having some difficulty using the Apache Commons Exec library to change the PATH environment variable to point to a created Python virtualenv in my target directory.
Ideally, I want something that is equivalent to activating the Python virtualenv, but in Java. The best way to do this as far as I know is to change environment variables so that its pip and python executables are discovered before my othervenv (which is another virtualenv that I use mainly).
I have this method in my PluginUtils class:
public static String callAndGetOutput(CommandLine commandLine, Map<String, String> environment) throws IOException
{
CollectingLogOutputStream outputStream = new CollectingLogOutputStream();
Executor executor = new DefaultExecutor();
DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream);
executor.setStreamHandler(streamHandler);
executor.execute(commandLine, environment, resultHandler);
try
{
// Wait for the subprocess to finish.
resultHandler.waitFor();
}
catch(InterruptedException e)
{
throw new IOException(e);
}
return outputStream.getOuput();
}
And then this class that calls this method.
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.environment.EnvironmentUtils;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
public class Example
{
public void run() throws Exception
{
Map<String, String> env = EnvironmentUtils.getProcEnvironment();
// env.forEach((k,v) -> System.out.println(k + "=" + v));
System.out.println(PluginUtilities.callAndGetOutput(CommandLine.parse("which python"), env));
System.out.println(PluginUtilities.callAndGetOutput(CommandLine.parse("which pip"), env));
Path venvDir = Paths.get("", "target", "testvenv");
Path venvBin = venvDir.resolve("bin");
assert(Files.isDirectory(venvDir));
assert(Files.isDirectory(venvBin));
env.put("PATH", venvBin.toAbsolutePath().toString()+ File.pathSeparator +env.get("PATH"));
env.put("VIRTUAL_ENV", venvDir.toAbsolutePath().toString());
// env.forEach((k,v) -> System.out.println(k + "=" + v));
System.out.println(PluginUtilities.callAndGetOutput(CommandLine.parse("which python"), env));
System.out.println(PluginUtilities.callAndGetOutput(CommandLine.parse("which pip"), env));
Path venvPip = venvBin.resolve("pip");
System.out.println(PluginUtilities.callAndGetOutput(CommandLine.parse("pip install jinja2"), env));
}
public static void main(String[] args) throws Exception
{
Example example = new Example();
example.run();
}
}
The output of this is as follows:
/home/lucas/.virtualenvs/othervenv/bin/python
/home/lucas/.virtualenvs/othervenv/bin/pip
/home/lucas/projects/myproject/mymodule/target/testvenv/bin/python
/home/lucas/projects/myproject/mymodule/target/testvenv/bin/pip
Requirement already satisfied: jinja2 in /home/lucas/.virtualenvs/othervenv/lib/python2.7/site-packages
Requirement already satisfied: MarkupSafe in /home/lucas/.virtualenvs/othervenv/lib/python2.7/site-packages (from jinja2)
I'm confused why which pip would return the correct pip executable while running pip calls the incorrect executable. I was able to use venvPip directly to install jinja2, but I want to avoid passing in absolute paths to pip and instead have it discoverable on the PATH.
I'm thinking that there's possibly a race condition, but I added the DefaultExecuteResultHandler so all the subprocess calls to be synchronous and that doesn't seem to help.
Short answer: One needs to refer to the correct
pythonorpipexecutable when constructing the command line. One way to make it easy is to store the venv location in a placeholder map e.g.Technically it should also be possible to launch the command via shell e.g.
sh pip install jinja2but this will not be portable to non-unix systems.Long answer: The PATH that Java
Runtime#exec(that commons.exec ultimately calls on most platforms) uses to search for an executable is not affected by the environment that is later passed to the spawned process.This is what happens when
which pipis launchedRuntime#execconsults the PATH passed to the JVM and scans these directories for an executable file calledwhichRuntime#execfinds/usr/bin/whichand launches it with a new environment that contains an updated PATH/usr/bin/whichconsults the PATH passed to it and scans these directories for an executable file calledpip/usr/bin/whichoperates off of an updated PATH it findstestvenv/bin/pipand prints its locationThis is what happens when
pip install jinja2is launched:Runtime#execconsults the PATH passed to the JVM and scans these directories for an executable file calledpipRuntime#execfindsotherenv/bin/pipand launches it with with a new environment that contains an updated PATHotherenv/bin/pipattempts to operate onotherenvand thus fails to perofm the task