diff --git a/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/targets.kt b/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/targets.kt index 791834635cad..2b8ec7d805c1 100644 --- a/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/targets.kt +++ b/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/targets.kt @@ -10,12 +10,12 @@ import com.intellij.execution.target.local.LocalTargetEnvironmentRequest import com.intellij.execution.target.local.LocalTargetPtyOptions import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.platform.eel.provider.utils.ProcessFunctions import com.intellij.platform.eel.provider.utils.bindProcessToScopeImpl import com.intellij.python.community.execService.BinOnTarget import com.intellij.python.community.execService.ExecuteGetProcessError import com.intellij.python.community.execService.spi.TargetEnvironmentRequestHandler +import com.intellij.remoteServer.util.ServerRuntimeException import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.Exe import com.jetbrains.python.errorProcessing.ExecErrorReason @@ -24,8 +24,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import com.intellij.remoteServer.util.ServerRuntimeException -import java.nio.file.Path import kotlin.io.path.pathString import kotlin.time.Duration.Companion.milliseconds @@ -54,6 +52,10 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau val targetEnv = try { request.prepareEnvironment(TargetProgressIndicator.EMPTY) } + catch (e: RuntimeException) { // some types like DockerRemoteRequest throw base RuntimeException instead of anything meaningful, need to change platform code first + fileLogger().warn("Failed to start $target", e) // TODO: i18n + return@withContext Result.failure(ExecuteGetProcessError.EnvironmentError(MessageError("Failed to start environment due to ${e.localizedMessage}"))) + } catch (e: ExecutionException) { fileLogger().warn("Failed to start $target", e) // TODO: i18n return@withContext Result.failure(ExecuteGetProcessError.EnvironmentError(MessageError("Failed to start environment due to ${e.localizedMessage}"))) @@ -104,7 +106,7 @@ private class TargetProcessCommands( } targetEnv.shutdown() }, killProcess = { - process?.destroyForcibly(); + process?.destroyForcibly() targetEnv.shutdown() }) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index cacb1d191df2..444cf9a15064 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -1,18 +1,24 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk.add.v2 +import com.intellij.execution.target.BrowsableTargetEnvironmentType import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.execution.target.getTargetType import com.intellij.execution.target.joinTargetPaths import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.util.io.FileUtil import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.provider.asNioPath import com.intellij.platform.eel.provider.localEel +import com.intellij.platform.eel.where +import com.intellij.python.community.execService.Args import com.intellij.python.community.execService.BinOnEel import com.intellij.python.community.execService.BinOnTarget import com.intellij.python.community.execService.BinaryToExec import com.intellij.python.community.execService.ExecService +import com.intellij.python.community.execService.execGetStdout import com.intellij.python.community.execService.python.validatePythonAndGetInfo import com.intellij.python.community.services.internal.impl.VanillaPythonWithPythonInfoImpl import com.intellij.python.community.services.shared.VanillaPythonWithPythonInfo @@ -55,6 +61,7 @@ internal class VenvAlreadyExistsError

( sealed interface FileSystem

{ val isReadOnly: Boolean + val isBrowseable: Boolean fun parsePath(raw: String): PyResult

@@ -68,11 +75,13 @@ sealed interface FileSystem

{ suspend fun resolvePythonBinary(pythonHome: P): P? fun getBinaryToExec(path: P): BinaryToExec + suspend fun which(cmd: String): P? data class Eel( val eelApi: EelApi, - override val isReadOnly: Boolean = false, ) : FileSystem { + override val isBrowseable: Boolean = true + override val isReadOnly: Boolean = false override fun getBinaryToExec(path: PathHolder.Eel): BinaryToExec { return BinOnEel(path.path) } @@ -172,6 +181,10 @@ sealed interface FileSystem

{ override suspend fun resolvePythonBinary(pythonHome: PathHolder.Eel): PathHolder.Eel? { return pythonHome.path.resolvePythonBinary()?.let { PathHolder.Eel(it) } } + + override suspend fun which(cmd: String): PathHolder.Eel? { + return eelApi.exec.where(cmd)?.asNioPath()?.let { PathHolder.Eel(it) } + } } data class Target( @@ -180,6 +193,8 @@ sealed interface FileSystem

{ ) : FileSystem { override val isReadOnly: Boolean get() = !PythonInterpreterTargetEnvironmentFactory.isMutable(targetEnvironmentConfiguration) + override val isBrowseable: Boolean + get() = targetEnvironmentConfiguration.getTargetType() is BrowsableTargetEnvironmentType private val systemPythonCache = ArrayList>() @@ -270,6 +285,13 @@ sealed interface FileSystem

{ return pythonBinaryPath } + + override suspend fun which(cmd: String): PathHolder.Target? { + val which = getBinaryToExec(PathHolder.Target("which")) + val condaPathString = ExecService().execGetStdout(which, Args(cmd)).getOr { return null } + val condaPathOnFS = parsePath(condaPathString).getOr { return null } + return condaPathOnFS + } } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt index 8eed1fe8dbd2..dfb14aa198eb 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt @@ -59,7 +59,7 @@ internal class PythonAddCustomInterpreter

( private val newInterpreterCreators = if (model.fileSystem.isReadOnly) emptyMap() else mapOf( VIRTUALENV to { EnvironmentCreatorVenv(model) }, - CONDA to { CondaNewEnvironmentCreator(model, errorSink) }, + CONDA to { CondaNewEnvironmentCreator(model) }, PIPENV to { EnvironmentCreatorPip(model, errorSink) }, POETRY to { EnvironmentCreatorPoetry(model, module, errorSink) }, UV to { EnvironmentCreatorUv(model, module, errorSink) }, @@ -68,7 +68,7 @@ internal class PythonAddCustomInterpreter

( private val existingInterpreterSelectors = buildMap { put(PYTHON) { PythonExistingEnvironmentSelector(model, module) } - put(CONDA) { CondaExistingEnvironmentSelector(model, errorSink) } + put(CONDA) { CondaExistingEnvironmentSelector(model) } if (!limitExistingEnvironments) { put(POETRY) { PoetryExistingEnvironmentSelector(model, module) } put(UV) { UvExistingEnvironmentSelector(model, module) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt index d8d25964bfa2..138792baab11 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkPanelBuilderAndSdkCreator.kt @@ -133,7 +133,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator( validationRequestor = validationRequestor, labelText = message("sdk.create.custom.venv.executable.path", "conda"), missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"), - installAction = createInstallCondaFix(model, errorSink), + installAction = createInstallCondaFix(model), ) }.visibleIf(_baseConda) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt index 18ca44ab84f6..a2d649f24854 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt @@ -338,23 +338,18 @@ internal class ValidatedPathField>( private fun > Panel.installToolRow( fileSystem: FileSystem<*>, missingExecutableText: @Nls String, - installAction: ActionLink? = null, + installAction: ActionLink, validatedPathField: ValidatedPathField, ): Row { - val selectExecutableLink = ActionLink(message("sdk.create.custom.select.executable.link")) { + val selectExecutableLink = if (fileSystem.isBrowseable) ActionLink(message("sdk.create.custom.select.executable.link")) { validatedPathField.button.doClick() } - val (firstFix, secondFix) = if (installAction == null || fileSystem.isReadOnly) { - Pair(selectExecutableLink, null) - } - else { - Pair(installAction, selectExecutableLink) - } + else null return row("") { validationTooltip(missingExecutableText, - firstFix, - secondFix, + installAction, + selectExecutableLink, validationType = ValidationType.WARNING, inline = true) .align(Align.FILL) @@ -379,7 +374,7 @@ internal fun > Panel.validatablePath isFileSelectionMode = isFileSelectionMode, ) - missingExecutableText?.let { + if (missingExecutableText != null && installAction != null && !fileSystem.isReadOnly) { installToolRow( fileSystem = fileSystem, missingExecutableText = missingExecutableText, diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt index 06fdcb7678b9..c501c7a7ed64 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt @@ -38,7 +38,7 @@ import java.awt.event.ActionEvent import javax.swing.AbstractAction -internal class CondaExistingEnvironmentSelector

(model: PythonAddInterpreterModel

, private val errorSink: ErrorSink) : PythonExistingEnvironmentConfigurator

(model) { +internal class CondaExistingEnvironmentSelector

(model: PythonAddInterpreterModel

) : PythonExistingEnvironmentConfigurator

(model) { private lateinit var envComboBox: ComboBox private lateinit var condaExecutable: ValidatedPathField> private lateinit var reloadLink: ActionLink @@ -53,7 +53,7 @@ internal class CondaExistingEnvironmentSelector

(model: PythonAdd validationRequestor = validationRequestor, labelText = message("sdk.create.custom.venv.executable.path", "conda"), missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"), - installAction = createInstallCondaFix(model, errorSink) + installAction = createInstallCondaFix(model) ) rowsRange { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaNewEnvironmentCreator.kt index f250c3855da9..90e2ac4188d4 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaNewEnvironmentCreator.kt @@ -23,7 +23,7 @@ import com.jetbrains.python.statistics.InterpreterType import com.jetbrains.python.ui.flow.bindText import kotlinx.coroutines.CoroutineScope -internal class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel

, private val errorSink: ErrorSink) : PythonNewEnvironmentCreator

(model) { +internal class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel

) : PythonNewEnvironmentCreator

(model) { private lateinit var pythonVersion: ObservableMutableProperty private lateinit var versionComboBox: ComboBox @@ -49,7 +49,7 @@ internal class CondaNewEnvironmentCreator(model: PythonMutableTar validationRequestor = validationRequestor, labelText = message("sdk.create.custom.venv.executable.path", "conda"), missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"), - installAction = createInstallCondaFix(model, errorSink) + installAction = createInstallCondaFix(model) ) } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt index 28b804fc939f..f07e334095ce 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaViewModel.kt @@ -2,16 +2,11 @@ package com.jetbrains.python.sdk.add.v2.conda import com.intellij.openapi.application.UI -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.diagnostic.fileLogger -import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.PropertyGraph import com.jetbrains.python.PyBundle.message import com.jetbrains.python.errorProcessing.PyResult -import com.jetbrains.python.getOrLogException import com.jetbrains.python.sdk.add.v2.* -import com.jetbrains.python.sdk.conda.suggestCondaPath import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity import kotlinx.coroutines.CoroutineScope @@ -20,8 +15,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -private val LOG: Logger = fileLogger() - class CondaViewModel

( val fileSystem: FileSystem

, propertyGraph: PropertyGraph, @@ -39,16 +32,7 @@ class CondaViewModel

( toolVersionPrefix = "conda", backProperty = condaExecutable, propertyGraph = propertyGraph, - defaultPathSupplier = { - val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration - val executor = targetEnvironmentConfiguration.toExecutor() - val suggestedCondaPath = runCatching { - suggestCondaPath(targetCommandExecutor = executor) - }.getOrLogException(LOG) - - val condaPathOnFS = suggestedCondaPath?.let { fileSystem.parsePath(suggestedCondaPath).getOrLogException(LOG) } - condaPathOnFS - } + defaultPathSupplier = { fileSystem.which("conda") } ) override fun initialize(scope: CoroutineScope) { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/condaUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/condaUtils.kt index d5cbc4e96a73..baee771836d0 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/condaUtils.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/condaUtils.kt @@ -1,8 +1,6 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk.add.v2.conda -import com.intellij.execution.target.TargetEnvironmentConfiguration -import com.intellij.execution.target.local.LocalTargetEnvironmentRequest import com.intellij.openapi.application.EDT import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk @@ -52,11 +50,6 @@ internal suspend fun PythonAddInterpreterModel<*>.createCondaEnvironment(moduleO return result } -internal fun TargetEnvironmentConfiguration?.toExecutor(): TargetCommandExecutor { - return TargetEnvironmentRequestCommandExecutor(this?.createEnvironmentRequest(project = null) ?: LocalTargetEnvironmentRequest()) -} - - internal fun PythonAddInterpreterModel<*>.getBaseCondaOrError(): PyResult { val baseConda = condaViewModel.baseCondaEnv.get() return if (baseConda != null) PyResult.success(baseConda) else PyResult.localizedError(message("python.sdk.conda.no.base.env.error")) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt index 20e994c004cb..e24bbafbd09e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt @@ -4,6 +4,7 @@ package com.jetbrains.python.sdk.add.v2 import com.intellij.icons.AllIcons import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.application.EDT +import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableProperty import com.intellij.openapi.observable.properties.PropertyGraph @@ -14,6 +15,7 @@ import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED import com.intellij.openapi.ui.validation.and import com.intellij.openapi.util.IconLoader import com.intellij.openapi.util.NlsSafe +import com.intellij.platform.eel.provider.localEel import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.python.community.impl.installer.CondaInstallManager @@ -43,7 +45,11 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity import com.jetbrains.python.util.ShowingMessageErrorSync import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls @@ -259,29 +265,19 @@ internal fun

Panel.pythonInterpreterComboBox( preferredSize = JBUI.size(preferredSize) isEditable = true } - .validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(selectedSdkProperty)) + .validationRequestor( + validationRequestor + and WHEN_PROPERTY_CHANGED(selectedSdkProperty) + and WHEN_PROPERTY_CHANGED(comboBox.isLoading) + ) .validationInfo { when { - !comboBox.isVisible -> null - selectedSdkProperty.get() == null -> { - if (comboBox.isBusy) { - ValidationInfo(message("python.add.sdk.panel.wait")) - } - else { - ValidationInfo(message("sdk.create.custom.existing.error.no.interpreters.to.select")) - } - } + !it.isVisible -> null + it.isLoading.get() -> ValidationInfo(message("python.add.sdk.panel.wait")) + selectedSdkProperty.get() == null -> ValidationInfo(message("sdk.create.custom.existing.error.no.interpreters.to.select")) else -> null } } - .validationOnApply { - if (!comboBox.isVisible) return@validationOnApply null - // This component must set sdk: clients expect it not to be null (PY-77463) - if (comboBox.isBusy || selectedSdkProperty.get() == null) { - ValidationInfo(message("python.add.sdk.panel.wait")) - } - else null - } .align(Align.FILL) } }.also { customizer(it) } @@ -294,9 +290,10 @@ internal class PythonInterpreterComboBox

( val fileSystem: FileSystem

, private val errorSink: ErrorSink, ) : ComboBox?>() { + val isLoading: ObservableMutableProperty = AtomicBooleanProperty(true) init { - renderer = PythonSdkComboBoxListCellRenderer { isBusy } + renderer = PythonSdkComboBoxListCellRenderer { isLoading.get() } val newOnPathSelected: (String) -> Unit = { rawPath -> runWithModalProgressBlocking(ModalTaskOwner.guess(), message("python.sdk.validating.environment")) { val pathOnFileSystem = fileSystem.parsePath(rawPath).onFailure { error -> @@ -331,6 +328,7 @@ internal class PythonInterpreterComboBox

( selectedItemReminder?.let { selectedItem = it } setBusy(false) + isLoading.set(false) }.launchIn(scope + Dispatchers.EDT) } @@ -420,7 +418,9 @@ private fun ExtendableTextComponent.removeLoadingExtension() { removeExtension(loaderExtension) } -internal fun

createInstallCondaFix(model: PythonAddInterpreterModel

, errorSink: ErrorSink): ActionLink { +internal fun

createInstallCondaFix(model: PythonAddInterpreterModel

): ActionLink? { + if ((model.fileSystem as? FileSystem.Eel)?.eelApi != localEel) return null + return ActionLink(message("sdk.create.custom.venv.install.fix.title", "Miniconda", "")) { PythonSdkFlavor.clearExecutablesCache() CondaInstallManager.installLatest(null)