mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-05 01:50:56 +07:00
PY-71499: Spaces in a conda env path break terminal action on **nix.
To activate conda on **nix, we provide a path to `active` script and a path to the env directory. Terminal shell integration scripts then "source" `activate` providing an env path as an argument. The latter is called `JEDITERM_SOURCE` env var, the former is `JEDITERM_SOURCE_ARGS`. The problem is those integration scripts treat `JEDITERM_SOURCE_ARGS` as a list, so they use shell magic to break it into several arguments, so `/path/foo bar/` effectively presented as `['/path/foo', 'bar/']`. To fix it, we introduce the ` JEDITERM_SOURCE_SINGLE_ARG ` key which means "do not explode argument." (cherry picked from commit a0a7c7a7bc8789078dd6cf109f4fd4386c9b7da6) IJ-MR-159065 GitOrigin-RevId: 0c44ce6c43b292f30a094ac79d5f5d7e8935935c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3352209090
commit
5e4396a97c
@@ -71,8 +71,12 @@ then
|
||||
fi
|
||||
|
||||
if [ -n "${JEDITERM_SOURCE-}" ]
|
||||
then
|
||||
source "$JEDITERM_SOURCE" ${JEDITERM_SOURCE_ARGS-}
|
||||
then # JEDITERM_SOURCE_ARGS might be either list of args or one arg depending on JEDITERM_SOURCE_SINGLE_ARG
|
||||
if [ -n "${JEDITERM_SOURCE_SINGLE_ARG}" ]; then
|
||||
source "$JEDITERM_SOURCE" "${JEDITERM_SOURCE_ARGS}"
|
||||
else
|
||||
source "$JEDITERM_SOURCE" ${JEDITERM_SOURCE_ARGS-}
|
||||
fi
|
||||
unset JEDITERM_SOURCE
|
||||
unset JEDITERM_SOURCE_ARGS
|
||||
fi
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
|
||||
function __jetbrains_intellij_update_environment() {
|
||||
if [[ -n "${JEDITERM_SOURCE:-}" ]]; then
|
||||
builtin source -- "$JEDITERM_SOURCE" ${=JEDITERM_SOURCE_ARGS:-}
|
||||
if [[ -n "${JEDITERM_SOURCE_SINGLE_ARG}" ]]; then
|
||||
# JEDITERM_SOURCE_ARGS might be either list of args or one arg depending on JEDITERM_SOURCE_SINGLE_ARG
|
||||
builtin source -- "$JEDITERM_SOURCE" "${JEDITERM_SOURCE_ARGS}"
|
||||
else
|
||||
builtin source -- "$JEDITERM_SOURCE" ${=JEDITERM_SOURCE_ARGS:-}
|
||||
fi
|
||||
fi
|
||||
builtin unset JEDITERM_SOURCE JEDITERM_SOURCE_ARGS
|
||||
|
||||
|
||||
# Enable native zsh options to make coding easier.
|
||||
builtin emulate -L zsh
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
<orderEntry type="module" module-name="intellij.python.community.impl.venv" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.python.community.junit5Tests.framework.conda" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="JUnit5Params" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="JUnit5Pioneer" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -7,6 +7,7 @@ import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.State
|
||||
import com.intellij.openapi.components.Storage
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.options.UnnamedConfigurable
|
||||
import com.intellij.openapi.project.Project
|
||||
@@ -31,7 +32,15 @@ import kotlin.io.path.exists
|
||||
import kotlin.io.path.isExecutable
|
||||
import kotlin.io.path.name
|
||||
|
||||
|
||||
class PyVirtualEnvTerminalCustomizer : LocalTerminalCustomizer() {
|
||||
private companion object {
|
||||
const val JEDITERM_SOURCE = "JEDITERM_SOURCE"
|
||||
const val JEDITERM_SOURCE_ARGS = "JEDITERM_SOURCE_ARGS"
|
||||
const val JEDITERM_SOURCE_SINGLE_ARG = "JEDITERM_SOURCE_SINGLE_ARG"
|
||||
val logger = fileLogger()
|
||||
}
|
||||
|
||||
private fun generatePowerShellActivateScript(sdk: Sdk, sdkHomePath: VirtualFile): String? {
|
||||
// TODO: This should be migrated to Targets API: each target provides terminal
|
||||
val condaData = (sdk.sdkAdditionalData as? PythonSdkAdditionalData)?.flavorAndData?.data as? PyCondaFlavorData
|
||||
@@ -103,13 +112,18 @@ class PyVirtualEnvTerminalCustomizer : LocalTerminalCustomizer() {
|
||||
val shellName = Path(shellPath).name
|
||||
if (isPowerShell(shellName)) {
|
||||
generatePowerShellActivateScript(sdk, sdkHomePath)?.let {
|
||||
envs.put("JEDITERM_SOURCE", it)
|
||||
envs.put(JEDITERM_SOURCE, it)
|
||||
}
|
||||
}
|
||||
else {
|
||||
findActivateScript(sdkHomePath.path, shellPath)?.let { activate ->
|
||||
envs.put("JEDITERM_SOURCE", activate.first)
|
||||
envs.put("JEDITERM_SOURCE_ARGS", activate.second ?: "")
|
||||
envs.put(JEDITERM_SOURCE, activate.first)
|
||||
envs.put(JEDITERM_SOURCE_ARGS, activate.second ?: "")
|
||||
// **nix shell integration scripts split arguments;
|
||||
// since a path may contain spaces, we do not want it to be split into several arguments.
|
||||
if (activate.second != null) {
|
||||
envs.put(JEDITERM_SOURCE_SINGLE_ARG, "1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +138,7 @@ class PyVirtualEnvTerminalCustomizer : LocalTerminalCustomizer() {
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Running ${command.joinToString(" ")} with ${envs.entries.joinToString("\n")}")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -162,6 +177,7 @@ class PyVirtualEnvTerminalCustomizer : LocalTerminalCustomizer() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
internal class SettingsState {
|
||||
var virtualEnvActivate: Boolean = true
|
||||
|
||||
@@ -3,9 +3,12 @@ package com.intellij.python.junit5Tests.env.terminal
|
||||
|
||||
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
|
||||
import com.intellij.openapi.application.edtWriteAction
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ModuleRootModificationUtil
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.platform.eel.EelExecApi
|
||||
import com.intellij.platform.eel.getOrThrow
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
@@ -24,8 +27,10 @@ import com.intellij.testFramework.junit5.fixture.tempPathFixture
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.persist
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.hamcrest.CoreMatchers.*
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.jetbrains.plugins.terminal.ShellStartupOptions
|
||||
@@ -34,19 +39,18 @@ import org.jetbrains.plugins.terminal.util.ShellIntegration
|
||||
import org.jetbrains.plugins.terminal.util.ShellType
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs
|
||||
import org.junit.jupiter.api.condition.OS
|
||||
import org.junit.jupiter.api.Assumptions
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.ValueSource
|
||||
import org.junitpioneer.jupiter.cartesian.CartesianTest
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isExecutable
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
private const val WHERE_EXE = "where.exe"
|
||||
|
||||
/**
|
||||
* Run `powershell.exe` with a venv activation script and make sure there are no errors and python is correct
|
||||
@@ -75,17 +79,42 @@ class PyVirtualEnvTerminalCustomizerTest {
|
||||
}
|
||||
}
|
||||
|
||||
private val powerShell =
|
||||
PathEnvironmentVariableUtil.findInPath("powershell.exe")?.toPath()
|
||||
?: Path((System.getenv("SystemRoot") ?: "c:\\windows"), "system32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
||||
|
||||
@EnabledOnOs(value = [OS.WINDOWS])
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = [true, false])
|
||||
fun powershellActivationTest(useConda: Boolean, @CondaEnv condaEnv: PyCondaEnv, @TempDir path: Path): Unit = timeoutRunBlocking(10.minutes) {
|
||||
private fun getShellPath(shellType: ShellType): Path = when (shellType) {
|
||||
ShellType.POWERSHELL -> PathEnvironmentVariableUtil.findInPath("powershell.exe")?.toPath()
|
||||
?: Path((System.getenv("SystemRoot")
|
||||
?: "c:\\windows"), "system32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
||||
ShellType.FISH, ShellType.BASH, ShellType.ZSH -> Path("/usr/bin/${shellType.name.lowercase()}")
|
||||
}
|
||||
|
||||
|
||||
@CartesianTest
|
||||
fun shellActivationTest(
|
||||
@CartesianTest.Values(booleans = [true, false]) useConda: Boolean,
|
||||
@CartesianTest.Enum shellType: ShellType,
|
||||
@CondaEnv condaEnv: PyCondaEnv,
|
||||
@TempDir venvPath: Path,
|
||||
): Unit = timeoutRunBlocking(10.minutes) {
|
||||
when (shellType) {
|
||||
ShellType.POWERSHELL -> Assumptions.assumeTrue(SystemInfo.isWindows, "PowerShell is Windows only")
|
||||
ShellType.FISH -> Assumptions.abort("Fish terminal activation isn't supported")
|
||||
ShellType.ZSH, ShellType.BASH -> Assumptions.assumeFalse(SystemInfo.isWindows, "Unix shells do not work on Windows")
|
||||
}
|
||||
|
||||
val shellPath = getShellPath(shellType)
|
||||
if (!withContext(Dispatchers.IO) { shellPath.exists() && shellPath.isExecutable() }) {
|
||||
when (shellType) {
|
||||
ShellType.ZSH -> Assumptions.assumeFalse(SystemInfo.isMac, "Zsh is mandatory on mac")
|
||||
ShellType.BASH -> error("$shellPath not found")
|
||||
ShellType.FISH -> error("Fish must be ignored")
|
||||
ShellType.POWERSHELL -> error("Powershell is mandatory on Windows")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val (pythonBinary, venvDirName) =
|
||||
if (useConda) {
|
||||
val envDir = path.resolve("some path with spaces")
|
||||
val envDir = venvPath.resolve("some path with spaces")
|
||||
val sdk = createCondaEnv(condaEnv, envDir).createSdkFromThisEnv(null, emptyList())
|
||||
sdkToDelete = sdk
|
||||
sdk.persist()
|
||||
@@ -104,33 +133,49 @@ class PyVirtualEnvTerminalCustomizerTest {
|
||||
catch (_: IOException) {
|
||||
pythonBinary
|
||||
}
|
||||
val shellOptions = getShellStartupOptions(pythonBinary.parent)
|
||||
val shellOptions = getShellStartupOptions(pythonBinary.parent, shellType)
|
||||
val command = shellOptions.shellCommand!!
|
||||
val exe = command[0]
|
||||
val args = if (command.size == 1) emptyList() else command.subList(1, command.size)
|
||||
|
||||
val execOptions = EelExecApi.ExecuteProcessOptions.Builder(exe)
|
||||
.args(args)
|
||||
.env(shellOptions.envVariables)
|
||||
.env(shellOptions.envVariables + mapOf(Pair("TERM", "dumb")))
|
||||
// Unix shells do not activate with out tty
|
||||
.ptyOrStdErrSettings(if (SystemInfo.isWindows) null else EelExecApi.Pty(100, 100, true))
|
||||
.build()
|
||||
val process = localEel.exec.execute(execOptions).getOrThrow()
|
||||
try {
|
||||
val stderr = launch {
|
||||
val error = process.stderr.readWholeText().getOrThrow()
|
||||
Assertions.assertTrue(error.isEmpty(), "Unexpected text in stderr: $error")
|
||||
val stderr = async {
|
||||
process.stderr.readWholeText().getOrThrow()
|
||||
}
|
||||
val stdout = async {
|
||||
process.stdout.readWholeText().getOrThrow().split("\n").map { it.trim() }
|
||||
val separator = if (SystemInfo.isWindows) "\n" else "\r\n"
|
||||
process.stdout.readWholeText().getOrThrow().split(separator).map { it.trim() }
|
||||
}
|
||||
|
||||
val where = PathEnvironmentVariableUtil.findInPath(WHERE_EXE)?.toString() ?: WHERE_EXE
|
||||
process.stdin.sendWholeText("$where python\nexit\n").getOrThrow()
|
||||
stderr.join()
|
||||
val output = stdout.await()
|
||||
// tool -- where.exe Windows, "type(1)" **nix
|
||||
// "$TOOL python" returns $PREFIX [path-to-python] $POSTFIX
|
||||
val (locateTool, prefix, postfix) = if (SystemInfo.isWindows) {
|
||||
Triple(PathEnvironmentVariableUtil.findInPath("where.exe")?.toString() ?: "where.exe", "", "")
|
||||
}
|
||||
else {
|
||||
// zsh wraps text in ''
|
||||
val quot = if (shellType == ShellType.ZSH) "'" else ""
|
||||
Triple("type", "python is $quot", quot)
|
||||
}
|
||||
process.stdin.sendWholeText("$locateTool python\nexit\n").getOrThrow()
|
||||
val error = stderr.await()
|
||||
|
||||
assertThat("We ran `$where`, so we there should be python path", output,
|
||||
anyOf(hasItem(pythonBinary.pathString), hasItem(pythonBinaryReal.pathString)))
|
||||
assertThat("There must be a line with ($venvDirName)", output, hasItem(containsString("($venvDirName)")))
|
||||
Assertions.assertTrue(error.isEmpty(), "Unexpected text in stderr: $error")
|
||||
val output = stdout.await()
|
||||
fileLogger().info("Output was $output")
|
||||
|
||||
assertThat("We ran `$locateTool`, so we there should be python path", output,
|
||||
anyOf(hasItem(prefix + pythonBinary.pathString + postfix), hasItem(prefix + pythonBinaryReal.pathString + postfix)))
|
||||
if (SystemInfo.isWindows) {
|
||||
assertThat("There must be a line with ($venvDirName)", output, hasItem(containsString("($venvDirName)")))
|
||||
}
|
||||
|
||||
process.exitCode.await()
|
||||
}
|
||||
@@ -141,19 +186,19 @@ class PyVirtualEnvTerminalCustomizerTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getShellStartupOptions(workDir: Path): ShellStartupOptions {
|
||||
private fun getShellStartupOptions(workDir: Path, shellType: ShellType): ShellStartupOptions {
|
||||
val sut = PyVirtualEnvTerminalCustomizer()
|
||||
val env = mutableMapOf<String, String>()
|
||||
val command = sut.customizeCommandAndEnvironment(
|
||||
projectFixture.get(),
|
||||
workDir.pathString,
|
||||
arrayOf(powerShell.pathString),
|
||||
arrayOf(getShellPath(shellType).pathString),
|
||||
env)
|
||||
|
||||
val options = ShellStartupOptions.Builder()
|
||||
.envVariables(env)
|
||||
.shellCommand(command.toList())
|
||||
.shellIntegration(ShellIntegration(ShellType.POWERSHELL, null))
|
||||
.shellIntegration(ShellIntegration(shellType, null))
|
||||
.build()
|
||||
return LocalShellIntegrationInjector.injectShellIntegration(options, false, false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user