Auto activate of virtualenv in terminal for projects with Python Virtualenv interpreters (PY-10498)

This commit is contained in:
Dmitry Trofimov
2016-09-01 21:58:10 +02:00
parent f59ce78a2e
commit 2b5babe3da
11 changed files with 225 additions and 41 deletions

View File

@@ -53,6 +53,7 @@ public class EnvironmentUtil {
private static final String LC_CTYPE = "LC_CTYPE"; private static final String LC_CTYPE = "LC_CTYPE";
private static final Future<Map<String, String>> ourEnvGetter; private static final Future<Map<String, String>> ourEnvGetter;
static { static {
if (SystemInfo.isMac && "unlocked".equals(System.getProperty("__idea.mac.env.lock")) && Registry.is("idea.fix.mac.env")) { if (SystemInfo.isMac && "unlocked".equals(System.getProperty("__idea.mac.env.lock")) && Registry.is("idea.fix.mac.env")) {
ourEnvGetter = AppExecutorUtil.getAppExecutorService().submit(new Callable<Map<String, String>>() { ourEnvGetter = AppExecutorUtil.getAppExecutorService().submit(new Callable<Map<String, String>>() {
@@ -90,7 +91,8 @@ public class EnvironmentUtil {
} }
} }
private EnvironmentUtil() { } private EnvironmentUtil() {
}
public static boolean isEnvironmentReady() { public static boolean isEnvironmentReady() {
return ourEnvGetter.isDone(); return ourEnvGetter.isDone();
@@ -152,39 +154,60 @@ public class EnvironmentUtil {
private static final String DISABLE_OMZ_AUTO_UPDATE = "DISABLE_AUTO_UPDATE"; private static final String DISABLE_OMZ_AUTO_UPDATE = "DISABLE_AUTO_UPDATE";
private static Map<String, String> getShellEnv() throws Exception { private static Map<String, String> getShellEnv() throws Exception {
String shell = System.getenv("SHELL"); return new ShellEnvReader().readShellEnv();
if (shell == null || !new File(shell).canExecute()) { }
throw new Exception("shell:" + shell);
}
File reader = FileUtil.findFirstThatExist(
PathManager.getBinPath() + "/printenv.py",
PathManager.getHomePath() + "/community/bin/mac/printenv.py",
PathManager.getHomePath() + "/bin/mac/printenv.py");
if (reader == null) {
throw new Exception("bin:" + PathManager.getBinPath());
}
File envFile = FileUtil.createTempFile("intellij-shell-env.", ".tmp", false); public static class ShellEnvReader {
try {
String[] command = {shell, "-l", "-i", "-c", ("'" + reader.getAbsolutePath() + "' '" + envFile.getAbsolutePath() + "'")};
LOG.info("loading shell env: " + StringUtil.join(command, " "));
ProcessBuilder builder = new ProcessBuilder(command).redirectErrorStream(true); public Map<String, String> readShellEnv() throws Exception {
builder.environment().put(DISABLE_OMZ_AUTO_UPDATE, "true"); File reader = FileUtil.findFirstThatExist(
Process process = builder.start(); PathManager.getBinPath() + "/printenv.py",
StreamGobbler gobbler = new StreamGobbler(process.getInputStream()); PathManager.getHomePath() + "/community/bin/mac/printenv.py",
int rv = waitAndTerminateAfter(process, SHELL_ENV_READING_TIMEOUT); PathManager.getHomePath() + "/bin/mac/printenv.py");
gobbler.stop(); if (reader == null) {
throw new Exception("bin:" + PathManager.getBinPath());
String lines = FileUtil.loadFile(envFile); }
if (rv != 0 || lines.isEmpty()) {
throw new Exception("rv:" + rv + " text:" + lines.length() + " out:" + StringUtil.trimEnd(gobbler.getText(), '\n')); File envFile = FileUtil.createTempFile("intellij-shell-env.", ".tmp", false);
try {
List<String> command = getShellProcessCommand();
command.add("-c");
command.add("'" + reader.getAbsolutePath() + "' '" + envFile.getAbsolutePath() + "'");
LOG.info("loading shell env: " + StringUtil.join(command, " "));
ProcessBuilder builder = new ProcessBuilder(command).redirectErrorStream(true);
builder.environment().put(DISABLE_OMZ_AUTO_UPDATE, "true");
Process process = builder.start();
StreamGobbler gobbler = new StreamGobbler(process.getInputStream());
int rv = waitAndTerminateAfter(process, SHELL_ENV_READING_TIMEOUT);
gobbler.stop();
String lines = FileUtil.loadFile(envFile);
if (rv != 0 || lines.isEmpty()) {
throw new Exception("rv:" + rv + " text:" + lines.length() + " out:" + StringUtil.trimEnd(gobbler.getText(), '\n'));
}
return parseEnv(lines);
}
finally {
FileUtil.delete(envFile);
} }
return parseEnv(lines);
} }
finally {
FileUtil.delete(envFile); protected List<String> getShellProcessCommand() throws Exception {
String shell = getShell();
return new ArrayList<String>(Arrays.asList(shell, "-l", "-i"));
}
@NotNull
protected String getShell() throws Exception {
String shell = System.getenv("SHELL");
if (shell == null || !new File(shell).canExecute()) {
throw new Exception("shell:" + shell);
}
return shell;
} }
} }
@@ -241,7 +264,8 @@ public class EnvironmentUtil {
try { try {
return process.exitValue(); return process.exitValue();
} }
catch (IllegalThreadStateException ignore) { } catch (IllegalThreadStateException ignore) {
}
} }
return null; return null;
} }
@@ -303,8 +327,15 @@ public class EnvironmentUtil {
private static class StreamGobbler extends BaseOutputReader { private static class StreamGobbler extends BaseOutputReader {
private static final Options OPTIONS = new Options() { private static final Options OPTIONS = new Options() {
@Override public SleepingPolicy policy() { return SleepingPolicy.BLOCKING; } @Override
@Override public boolean splitToLines() { return false; } public SleepingPolicy policy() {
return SleepingPolicy.BLOCKING;
}
@Override
public boolean splitToLines() {
return false;
}
}; };
private final StringBuffer myBuffer; private final StringBuffer myBuffer;

View File

@@ -52,3 +52,8 @@ export -f generate_command_executed_sequence
#generate escape sequence after command is executed to notify jediterm emulator #generate escape sequence after command is executed to notify jediterm emulator
trap "generate_command_executed_sequence" DEBUG trap "generate_command_executed_sequence" DEBUG
if [ -n "$JEDITERM_SOURCE" ]
then
source $JEDITERM_SOURCE
fi

View File

@@ -22,11 +22,16 @@ import org.jetbrains.annotations.Nullable;
import java.util.Map; import java.util.Map;
public interface LocalTerminalCustomizer { public abstract class LocalTerminalCustomizer {
ExtensionPointName<LocalTerminalCustomizer> EP_NAME = ExtensionPointName.create("org.jetbrains.plugins.terminal.localTerminalCustomizer"); public static ExtensionPointName<LocalTerminalCustomizer> EP_NAME = ExtensionPointName.create("org.jetbrains.plugins.terminal.localTerminalCustomizer");
void setupEnvironment(@NotNull Project project, @NotNull Map<String, String> envs); public String[] customizeCommandAndEnvironment(Project project, String[] command, Map<String, String> envs) {
return command;
}
@Nullable @Nullable
String getDefaultFolder(); protected String getDefaultFolder() {
return null;
}
} }

View File

@@ -102,8 +102,10 @@ public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess
} }
EncodingEnvironmentUtil.setLocaleEnvironmentIfMac(envs, myDefaultCharset); EncodingEnvironmentUtil.setLocaleEnvironmentIfMac(envs, myDefaultCharset);
String[] command = getCommand();
for (LocalTerminalCustomizer customizer : LocalTerminalCustomizer.EP_NAME.getExtensions()) { for (LocalTerminalCustomizer customizer : LocalTerminalCustomizer.EP_NAME.getExtensions()) {
customizer.setupEnvironment(myProject, envs); command = customizer.customizeCommandAndEnvironment(myProject, command, envs);
if (directory == null) { if (directory == null) {
directory = customizer.getDefaultFolder(); directory = customizer.getDefaultFolder();
@@ -111,7 +113,7 @@ public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess
} }
try { try {
return PtyProcess.exec(getCommand(), envs, directory != null ? directory : currentProjectFolder()); return PtyProcess.exec(command, envs, directory != null ? directory : currentProjectFolder());
} }
catch (IOException e) { catch (IOException e) {
throw new ExecutionException(e); throw new ExecutionException(e);
@@ -184,9 +186,7 @@ public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess
if (hasLoginArgument(shellName) && SystemInfo.isMac) { if (hasLoginArgument(shellName) && SystemInfo.isMac) {
result.add("--login"); result.add("--login");
} }
else { result.add("-i");
result.add("-i");
}
} }
result.addAll(command); result.addAll(command);

View File

@@ -6,6 +6,7 @@ hg4idea
tasks-core tasks-core
github github
terminal terminal
python-terminal
IntelliLang IntelliLang
IntelliLang-xml IntelliLang-xml
IntelliLang-python IntelliLang-python

View File

@@ -4,6 +4,7 @@
--> -->
<depends optional="true" config-file="python-rest-plugin.xml">org.jetbrains.plugins.rest</depends> <depends optional="true" config-file="python-rest-plugin.xml">org.jetbrains.plugins.rest</depends>
<depends optional="true" config-file="intellilang-python-support.xml">org.intellij.intelliLang</depends> <depends optional="true" config-file="intellilang-python-support.xml">org.intellij.intelliLang</depends>
<depends optional="true" config-file="python-terminal-plugin.xml">org.jetbrains.plugins.terminal</depends>
<xi:include href="/META-INF/ipython-notebook.xml" xpointer="xpointer(/idea-plugin/*)"/> <xi:include href="/META-INF/ipython-notebook.xml" xpointer="xpointer(/idea-plugin/*)"/>

View File

@@ -21,5 +21,6 @@
<orderEntry type="module" module-name="ShortcutPromoter" /> <orderEntry type="module" module-name="ShortcutPromoter" />
<orderEntry type="module" module-name="ipnb" /> <orderEntry type="module" module-name="ipnb" />
<orderEntry type="module" module-name="settings-repository" /> <orderEntry type="module" module-name="settings-repository" />
<orderEntry type="module" module-name="python-terminal" />
</component> </component>
</module> </module>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="python-community" />
<orderEntry type="module" module-name="terminal" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
<orderEntry type="module" module-name="core-api" />
<orderEntry type="module" module-name="projectModel-api" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="module" module-name="lang-api" />
</component>
</module>

View File

@@ -0,0 +1,11 @@
<idea-plugin version="2" xmlns:xi="http://www.w3.org/2001/XInclude">
<name>Python Terminal</name>
<id>org.jetbrains.plugins.python-terminal</id>
<version>VERSION</version>
<vendor>JetBrains</vendor>
<depends>com.intellij.modules.python</depends>
<xi:include href="/META-INF/python-terminal-plugin.xml" xpointer="xpointer(/idea-plugin/*)"/>
</idea-plugin>

View File

@@ -0,0 +1,7 @@
<idea-plugin version="2">
<extensions defaultExtensionNs="org.jetbrains.plugins.terminal">
<localTerminalCustomizer implementation="com.jetbrains.python.sdk.PyVirtualEnvTerminalCustomizer"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.sdk
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.EnvironmentUtil
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
import org.jetbrains.plugins.terminal.LocalTerminalCustomizer
import java.io.File
/**
* @author traff
*/
class PyVirtualEnvTerminalCustomizer : LocalTerminalCustomizer() {
override fun customizeCommandAndEnvironment(project: Project,
command: Array<out String>,
envs: MutableMap<String, String>): Array<out String> {
val sdk: Sdk? = findSdk(project)
if (sdk != null && PythonSdkType.isVirtualEnv(sdk) && SystemInfo.isUnix) {
// in case of virtualenv sdk on unix we activate virtualenv
val path = sdk.homePath
if (path != null) {
val shellPath = command[0]
val shellName = File(shellPath).name
if (shellName == "bash" || shellName == "sh") {
//for bash and sh we pass activate script to jediterm shell integration (see jediterm-sh.in)
findActivateScript(path, shellPath)?.let { activate -> envs.put("JEDITERM_SOURCE", activate) }
}
else {
//for other shells we read envs from activate script by the default shell and pass it to the process
val pyVirtualEnvReader = PyVirtualEnvReader(path)
pyVirtualEnvReader.activate?.let { envs.putAll(pyVirtualEnvReader.readShellEnv()) }
}
}
}
// for some reason virtualenv isn't activated in the rcfile for the login shell, so we make it non-login
return command.filter { arg -> arg != "--login" }.toTypedArray()
}
private fun findSdk(project: Project): Sdk? {
for (m in ModuleManager.getInstance(project).modules) {
val sdk: Sdk? = PythonSdkType.findPythonSdk(m)
if (sdk != null && !PythonSdkType.isRemote(sdk)) {
return sdk
}
}
return null
}
override fun getDefaultFolder(): String? {
return null
}
}
class PyVirtualEnvReader(virtualEnvSdkPath: String) : EnvironmentUtil.ShellEnvReader() {
val activate = findActivateScript(virtualEnvSdkPath, shell)
override fun getShellProcessCommand(): MutableList<String>? {
return if (activate != null) mutableListOf(shell, "--rcfile", activate, "-i")
else super.getShellProcessCommand()
}
}
private fun findActivateScript(path: String?, shellPath: String): String? {
val shellName = File(shellPath).name
val activate = if (shellName == "fish" || shellName == "csh") File(File(path).parentFile, "activate." + shellName)
else File(File(path).parentFile, "activate")
return if (activate.exists()) activate.absolutePath else null
}