PY-87517 Check python binary name for targets

When adding new interpreter, we could either specify venv root or python
binary path. Since we cannot check actual files for targets, we check
path regex to determine whether it's a venv root or a binary.

GitOrigin-RevId: d46ef025d9681b4d697d5d9572d0302c9599fdfc
This commit is contained in:
Alexey Katsman
2026-02-11 13:18:26 +00:00
committed by intellij-monorepo-bot
parent 401c665020
commit 17afc5ef19
5 changed files with 38 additions and 8 deletions
+2
View File
@@ -32,6 +32,7 @@ jvm_library(
"//platform/projectModel-api:projectModel",
"//platform/eel",
"//platform/eel-provider",
"//platform/execution",
],
exports = [
"//python/python-psi-api:psi",
@@ -59,6 +60,7 @@ jvm_library(
"@lib//:junit5",
"//platform/eel",
"//platform/eel-provider",
"//platform/execution",
],
exports = [
"//python/python-psi-api:psi",
@@ -23,5 +23,6 @@
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="module" module-name="intellij.platform.eel" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
<orderEntry type="module" module-name="intellij.platform.execution" />
</component>
</module>
@@ -1,6 +1,8 @@
// 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.venvReader
import com.intellij.execution.Platform
import com.intellij.execution.target.FullPathOnTarget
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.util.io.toCanonicalPath
@@ -160,6 +162,23 @@ class VirtualEnvReader private constructor(
return findInterpreter(pathOrDir)
}
/**
* [binaryOrDir] is either a venv root or a python binary
*/
fun findPythonInPythonRootForTarget(binaryOrDir: FullPathOnTarget, platform: Platform): FullPathOnTarget {
val pythonPattern = getPythonBinaryPattern(platform)
val separator = platform.fileSeparator
val binaryOrDirWithoutSeparatorSuffix = binaryOrDir.removeSuffix(separator.toString())
if (pythonPattern.matches(binaryOrDirWithoutSeparatorSuffix.substringAfterLast(separator))) {
return binaryOrDir
}
return when (platform) {
Platform.WINDOWS -> "${binaryOrDirWithoutSeparatorSuffix}${separator}Scripts${separator}python.exe"
Platform.UNIX -> "${binaryOrDirWithoutSeparatorSuffix}${separator}bin${separator}python"
}
}
fun getVenvRootPath(path: Path): Path? {
val bin = path.parent
@@ -255,6 +274,11 @@ class VirtualEnvReader private constructor(
EelOsFamily.Windows -> WIN_PYTHON_PATTERN
}
}
private fun getPythonBinaryPattern(platform: Platform): Regex = when (platform) {
Platform.UNIX -> POSIX_PYTHON_PATTERN
Platform.WINDOWS -> WIN_PYTHON_PATTERN
}
}
}
@@ -45,7 +45,9 @@ import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.util.function.Consumer
@@ -149,13 +151,14 @@ internal class AddInterpreterOnTargetAction(
if (dialogWrapper.exitCode != OK_EXIT_CODE) return
val sdk = (dialogWrapper.currentStepObject as? TargetCustomToolWizardStep)?.customTool as? Sdk ?: return
PythonNewInterpreterAddedCollector.logPythonNewInterpreterAdded(sdk, isPreviouslyConfigured = true)
service<LogCollectorService>().coroutineScope.launch(Dispatchers.Default) {
PythonNewInterpreterAddedCollector.logPythonNewInterpreterAdded(sdk, isPreviouslyConfigured = true)
}
onSdkCreated(sdk)
}
}
@ApiStatus.Internal
fun switchToSdk(module: Module, sdk: Sdk, currentSdk: Sdk?) {
val project = module.project
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
@@ -171,6 +174,9 @@ fun switchToSdk(module: Module, sdk: Sdk, currentSdk: Sdk?) {
module.excludeInnerVirtualEnv(sdk)
}
@Service
private class LogCollectorService(val coroutineScope: CoroutineScope)
@Service(Service.Level.PROJECT)
@ApiStatus.Internal
private class ToolDetectionService(project: Project, val coroutineScope: CoroutineScope) {
@@ -336,12 +336,9 @@ sealed interface FileSystem<P : PathHolder> {
}
override suspend fun resolvePythonBinary(pythonHome: PathHolder.Target): PathHolder.Target {
val pythonHomeString = pythonHome.pathString.takeIf { !it.contains("/bin/python") }
?: return pythonHome
val pythonBinaryString = pythonHomeString.let { "${it.removeSuffix("/")}/bin/python" }
return PathHolder.Target(pythonBinaryString)
val pythonHomeString = pythonHome.pathString
val platform = targetEnvironmentConfiguration.getPlatformAndRoot().platform
return PathHolder.Target(VirtualEnvReader().findPythonInPythonRootForTarget(pythonHomeString, platform))
}
override suspend fun which(cmd: String): PathHolder.Target? {