diff --git a/plugins/terminal/resources/shell-integrations/bash/bash-integration.bash b/plugins/terminal/resources/shell-integrations/bash/bash-integration.bash
index 0381fafdc2a9..9fda8b262c68 100644
--- a/plugins/terminal/resources/shell-integrations/bash/bash-integration.bash
+++ b/plugins/terminal/resources/shell-integrations/bash/bash-integration.bash
@@ -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
diff --git a/plugins/terminal/resources/shell-integrations/powershell/powershell-integration.ps1 b/plugins/terminal/resources/shell-integrations/powershell/powershell-integration.ps1
index 4a38429ef7a8..c9078e724901 100644
--- a/plugins/terminal/resources/shell-integrations/powershell/powershell-integration.ps1
+++ b/plugins/terminal/resources/shell-integrations/powershell/powershell-integration.ps1
@@ -16,8 +16,12 @@ Get-ChildItem env:_INTELLIJ_FORCE_PREPEND_* | ForEach-Object {
}
# `JEDITERM_SOURCE` is executed in its own scope now. That means, it can only run code, and export env vars. It can't export PS variables.
# It might be better to source it. See MSDN for the difference between "Call operator &" and "Script scope and dot sourcing"
-if (($Env:JEDITERM_SOURCE -ne $null) -and (Test-Path $Env:JEDITERM_SOURCE)) {
- & $Env:JEDITERM_SOURCE
+if ($Env:JEDITERM_SOURCE -ne $null) {
+ if (Test-Path "$Env:JEDITERM_SOURCE" -ErrorAction SilentlyContinue) {
+ & "$Env:JEDITERM_SOURCE"
+ } else { # If file doesn't exist it might be a script
+ Invoke-Expression "$Env:JEDITERM_SOURCE"
+ }
Remove-Item "env:JEDITERM_SOURCE"
}
diff --git a/plugins/terminal/resources/shell-integrations/zsh/zsh-integration.zsh b/plugins/terminal/resources/shell-integrations/zsh/zsh-integration.zsh
index a6d540265db3..e4170c791328 100644
--- a/plugins/terminal/resources/shell-integrations/zsh/zsh-integration.zsh
+++ b/plugins/terminal/resources/shell-integrations/zsh/zsh-integration.zsh
@@ -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
diff --git a/python/junit5Tests-framework/conda/intellij.python.community.junit5Tests.framework.conda.iml b/python/junit5Tests-framework/conda/intellij.python.community.junit5Tests.framework.conda.iml
index 657e2bbdea22..84f3f4cc1a46 100644
--- a/python/junit5Tests-framework/conda/intellij.python.community.junit5Tests.framework.conda.iml
+++ b/python/junit5Tests-framework/conda/intellij.python.community.junit5Tests.framework.conda.iml
@@ -13,5 +13,7 @@
+
+
\ No newline at end of file
diff --git a/python/junit5Tests-framework/conda/src/com/intellij/python/community/junit5Tests/framework/conda/condaEnvTool.kt b/python/junit5Tests-framework/conda/src/com/intellij/python/community/junit5Tests/framework/conda/condaEnvTool.kt
new file mode 100644
index 000000000000..e03f116bf579
--- /dev/null
+++ b/python/junit5Tests-framework/conda/src/com/intellij/python/community/junit5Tests/framework/conda/condaEnvTool.kt
@@ -0,0 +1,30 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.python.community.junit5Tests.framework.conda
+
+import com.intellij.execution.processTools.getResultStdoutStr
+import com.jetbrains.python.psi.LanguageLevel
+import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
+import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
+import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
+import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
+import org.jetbrains.annotations.ApiStatus
+import java.nio.file.Path
+import kotlin.io.path.pathString
+
+@ApiStatus.Internal
+/**
+ * Create conda env in [pathToCreateNewEnvIn] using [existingEnv] as a base
+ */
+suspend fun createCondaEnv(
+ existingEnv: PyCondaEnv,
+ pathToCreateNewEnvIn: Path,
+): PyCondaEnv {
+ val process = PyCondaEnv.createEnv(
+ PyCondaCommand(existingEnv.fullCondaPathOnTarget, null),
+ NewCondaEnvRequest.EmptyUnnamedEnv(LanguageLevel.PYTHON311, pathToCreateNewEnvIn.pathString)
+ ).getOrThrow()
+ process.getResultStdoutStr().getOrThrow()
+
+ val env = PyCondaEnv(PyCondaEnvIdentity.UnnamedEnv(pathToCreateNewEnvIn.pathString, false), existingEnv.fullCondaPathOnTarget)
+ return env
+}
\ No newline at end of file
diff --git a/python/python-terminal/intellij.python.terminal.iml b/python/python-terminal/intellij.python.terminal.iml
index f75a1ea448ed..fb4ce8487ace 100644
--- a/python/python-terminal/intellij.python.terminal.iml
+++ b/python/python-terminal/intellij.python.terminal.iml
@@ -25,5 +25,8 @@
+
+
+
\ No newline at end of file
diff --git a/python/python-terminal/src/com/intellij/python/terminal/PyVirtualEnvTerminalCustomizer.kt b/python/python-terminal/src/com/intellij/python/terminal/PyVirtualEnvTerminalCustomizer.kt
index af908958ec10..02b468be6824 100644
--- a/python/python-terminal/src/com/intellij/python/terminal/PyVirtualEnvTerminalCustomizer.kt
+++ b/python/python-terminal/src/com/intellij/python/terminal/PyVirtualEnvTerminalCustomizer.kt
@@ -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
@@ -14,12 +15,12 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VirtualFile
-import com.jetbrains.python.packaging.PyCondaPackageService
import com.jetbrains.python.run.findActivateScript
import com.jetbrains.python.sdk.PySdkUtil
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkUtil
-import com.jetbrains.python.sdk.flavors.conda.CondaEnvSdkFlavor
+import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData
+import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.terminal.LocalTerminalCustomizer
import org.jetbrains.plugins.terminal.TerminalOptionsProvider
import java.io.File
@@ -31,18 +32,28 @@ 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
- if ((sdk.sdkAdditionalData as? PythonSdkAdditionalData)?.flavor is CondaEnvSdkFlavor) {
+ val condaData = (sdk.sdkAdditionalData as? PythonSdkAdditionalData)?.flavorAndData?.data as? PyCondaFlavorData
+ if (condaData != null) {
// Activate conda
- val condaPath = PyCondaPackageService.getCondaExecutable(sdk.homePath)?.let { Path(it) }
- return if (condaPath != null && condaPath.exists() && condaPath.isExecutable()) {
+ val condaPath = Path(condaData.env.fullCondaPathOnTarget)
+ return if (condaPath.exists() && condaPath.isExecutable()) {
getCondaActivationCommand(condaPath, sdkHomePath)
}
else {
logger().warn("Can't find $condaPath, will not activate conda")
- PyTerminalBundle.message("powershell.conda.not.activated", "conda")
+ val message = PyTerminalBundle.message("powershell.conda.not.activated", "conda")
+ "echo '$message'"
}
}
@@ -72,10 +83,12 @@ class PyVirtualEnvTerminalCustomizer : LocalTerminalCustomizer() {
""".trimIndent()
}
- override fun customizeCommandAndEnvironment(project: Project,
- workingDirectory: String?,
- command: Array,
- envs: MutableMap): Array {
+ override fun customizeCommandAndEnvironment(
+ project: Project,
+ workingDirectory: String?,
+ command: Array,
+ envs: MutableMap,
+ ): Array {
var sdkByDirectory: Sdk? = null
if (workingDirectory != null) {
runReadAction {
@@ -99,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")
+ }
}
}
}
@@ -120,6 +138,7 @@ class PyVirtualEnvTerminalCustomizer : LocalTerminalCustomizer() {
}
}
+ logger.debug("Running ${command.joinToString(" ")} with ${envs.entries.joinToString("\n")}")
return command
}
@@ -158,6 +177,7 @@ class PyVirtualEnvTerminalCustomizer : LocalTerminalCustomizer() {
}
+@ApiStatus.Internal
class SettingsState {
var virtualEnvActivate: Boolean = true
}
diff --git a/python/python-terminal/tests/com/intellij/python/junit5Tests/env/terminal/PyVirtualEnvTerminalCustomizerTest.kt b/python/python-terminal/tests/com/intellij/python/junit5Tests/env/terminal/PyVirtualEnvTerminalCustomizerTest.kt
index d150f0b42a8e..6ab8a858c8ea 100644
--- a/python/python-terminal/tests/com/intellij/python/junit5Tests/env/terminal/PyVirtualEnvTerminalCustomizerTest.kt
+++ b/python/python-terminal/tests/com/intellij/python/junit5Tests/env/terminal/PyVirtualEnvTerminalCustomizerTest.kt
@@ -2,44 +2,62 @@
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
import com.intellij.platform.eel.provider.utils.readWholeText
import com.intellij.platform.eel.provider.utils.sendWholeText
import com.intellij.python.community.impl.venv.tests.pyVenvFixture
-import com.intellij.python.junit5Tests.framework.env.PyEnvTestCase
+import com.intellij.python.community.junit5Tests.framework.conda.CondaEnv
+import com.intellij.python.community.junit5Tests.framework.conda.PyEnvTestCaseWithConda
+import com.intellij.python.community.junit5Tests.framework.conda.createCondaEnv
import com.intellij.python.junit5Tests.framework.env.pySdkFixture
import com.intellij.python.terminal.PyVirtualEnvTerminalCustomizer
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.fixture.moduleFixture
import com.intellij.testFramework.junit5.fixture.projectFixture
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
import org.jetbrains.plugins.terminal.runner.LocalShellIntegrationInjector
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.Test
-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.Disabled
+import org.junit.jupiter.api.io.TempDir
+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
*/
-@PyEnvTestCase
+@PyEnvTestCaseWithConda
+@Disabled
class PyVirtualEnvTerminalCustomizerTest {
private val projectFixture = projectFixture()
private val tempDirFixture = tempPathFixture(prefix = "some dir with spaces")
@@ -52,15 +70,63 @@ class PyVirtualEnvTerminalCustomizerTest {
moduleFixture = moduleFixture
)
+ private var sdkToDelete: Sdk? = null
- private val powerShell =
- PathEnvironmentVariableUtil.findInPath("powershell.exe")?.toPath()
- ?: Path((System.getenv("SystemRoot") ?: "c:\\windows"), "system32", "WindowsPowerShell", "v1.0", "powershell.exe")
+ @AfterEach
+ fun tearDown(): Unit = timeoutRunBlocking {
+ sdkToDelete?.let { sdk ->
+ edtWriteAction {
+ ProjectJdkTable.getInstance().removeJdk(sdk)
+ }
+ }
+ }
- @EnabledOnOs(value = [OS.WINDOWS])
- @Test
- fun powershellActivationTest(): Unit = timeoutRunBlocking(10.minutes) {
- val pythonBinary = VirtualEnvReader.Instance.findPythonInPythonRoot(tempDirFixture.get())!!
+
+ 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 = venvPath.resolve("some path with spaces")
+ val sdk = createCondaEnv(condaEnv, envDir).createSdkFromThisEnv(null, emptyList())
+ sdkToDelete = sdk
+ sdk.persist()
+ ModuleRootModificationUtil.setModuleSdk(moduleFixture.get(), sdk)
+ Pair(Path(sdk.homePath!!), envDir.toRealPath().pathString)
+ }
+ else {
+ val venv = VirtualEnvReader.Instance.findPythonInPythonRoot(tempDirFixture.get())!!
+ Pair(venv, tempDirFixture.get().name)
+ }
// binary might be like ~8.3, we need to expand it as venv might report both
val pythonBinaryReal = try {
@@ -69,34 +135,49 @@ class PyVirtualEnvTerminalCustomizerTest {
catch (_: IOException) {
pythonBinary
}
- val shellOptions = getShellStartupOptions()
+ 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)))
- val vendDirName = tempDirFixture.get().name
- assertThat("There must be a line with ($vendDirName)", output, hasItem(containsString("($vendDirName)")))
+ 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()
}
@@ -107,19 +188,19 @@ class PyVirtualEnvTerminalCustomizerTest {
}
}
- private fun getShellStartupOptions(): ShellStartupOptions {
+ private fun getShellStartupOptions(workDir: Path, shellType: ShellType): ShellStartupOptions {
val sut = PyVirtualEnvTerminalCustomizer()
val env = mutableMapOf()
val command = sut.customizeCommandAndEnvironment(
projectFixture.get(),
- tempDirFixture.get().pathString,
- arrayOf(powerShell.pathString),
+ workDir.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)
}