mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-91493 Resolve tools in per-user install dirs, not just on PATH
PyTool.findExecutableInPath now takes the environment's eel and detects via ToolCommandExecutor (PATH + the known per-user install dirs) instead of a local PATH-only lookup, so both tool execution and the External Tools page find tools installed where they land on Windows (e.g. %APPDATA%\Python\Scripts, uv's bin). The pip list() uses the same detection, and the External Tools row is seeded with the exact path the installer reported. GitOrigin-RevId: c39129f31b6b40ea8fcfcf4a94b74a275ff0241c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
0137d9f471
commit
d79e980ebc
+16
-5
@@ -14,6 +14,8 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.Version as PlatformVersion
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.toEelApi
|
||||
import com.intellij.python.pytools.PyTool
|
||||
import com.intellij.python.pytools.PyToolsState
|
||||
import com.intellij.python.pytools.Version
|
||||
@@ -121,16 +123,23 @@ internal sealed interface PathFieldValue {
|
||||
/** User-supplied [PyToolsState.ToolEntry.customPathToExecutable]. */
|
||||
data class Custom(val path: Path) : PathFieldValue
|
||||
|
||||
/** Path derived from system PATH. */
|
||||
/** Path auto-detected on PATH or in a well-known per-user install directory. */
|
||||
data class AutoDetected(val path: Path) : PathFieldValue
|
||||
|
||||
/** Neither configured nor discoverable. */
|
||||
data object NotFound : PathFieldValue
|
||||
}
|
||||
|
||||
internal fun detect(tool: PyTool, customPath: Path?): PathFieldValue {
|
||||
/**
|
||||
* Resolve the row's displayed path. A user-supplied [customPath] wins; a [knownPath] (the exact path an
|
||||
* installer just reported) is trusted next; otherwise the tool is auto-detected via [findExecutableInPath],
|
||||
* which searches PATH **and** the well-known per-user install dirs — a plain PATH lookup misses the per-user
|
||||
* scripts dirs pip/uv install into, which are frequently not on PATH on Windows (PY-91493).
|
||||
*/
|
||||
internal suspend fun detect(project: Project, tool: PyTool, customPath: Path?, knownPath: Path? = null): PathFieldValue {
|
||||
if (customPath != null) return PathFieldValue.Custom(customPath)
|
||||
val auto = tool.findExecutableInPath()
|
||||
if (knownPath != null) return PathFieldValue.AutoDetected(knownPath)
|
||||
val auto = tool.findExecutableInPath(project.getEelDescriptor().toEelApi())
|
||||
return if (auto != null) PathFieldValue.AutoDetected(auto) else PathFieldValue.NotFound
|
||||
}
|
||||
|
||||
@@ -182,16 +191,18 @@ internal fun iconKindFor(
|
||||
*/
|
||||
internal fun ToolRow.probeVersion(
|
||||
scope: CoroutineScope,
|
||||
project: Project,
|
||||
isCustomEdit: Boolean = false,
|
||||
knownPath: Path? = null,
|
||||
onUpdated: (ToolRow) -> Unit,
|
||||
) {
|
||||
validationJob?.cancel()
|
||||
val mode = staged.mode
|
||||
val customPath = staged.customPath
|
||||
validationJob = scope.launch {
|
||||
// Step 1: resolve the displayed path off the EDT — `findInPath` does disk I/O.
|
||||
// Step 1: resolve the displayed path off the EDT — detection does disk I/O.
|
||||
val detected = withContext(Dispatchers.IO) {
|
||||
detect(tool, customPath)
|
||||
detect(project, tool, customPath, knownPath)
|
||||
}
|
||||
val path = when (detected) {
|
||||
is PathFieldValue.Custom -> detected.path
|
||||
|
||||
+1
-1
@@ -518,7 +518,7 @@ internal class PyExternalToolsTable(
|
||||
/** Start (or restart) the path-detection + `--version` probe for [item]. */
|
||||
private fun probeRow(item: ToolRow, isCustomEdit: Boolean = false) {
|
||||
val scope = scope ?: return
|
||||
item.probeVersion(scope, isCustomEdit, ::refreshRow)
|
||||
item.probeVersion(scope, project, isCustomEdit, onUpdated = ::refreshRow)
|
||||
}
|
||||
|
||||
/** Fire a single-row table update so the renderer picks up freshly-probed state. */
|
||||
|
||||
+13
-8
@@ -26,6 +26,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Backs the External Tools page's tool actions: the install / upgrade actions invoked from the Path
|
||||
@@ -211,7 +212,7 @@ internal class PyToolManagementController(
|
||||
toolRow: ToolRow,
|
||||
progressTitleKey: String,
|
||||
errorTitleKey: String,
|
||||
action: suspend () -> PyResult<*>,
|
||||
action: suspend () -> PyResult<Path>,
|
||||
onSuccess: () -> Unit = {},
|
||||
/**
|
||||
* Fires once after the post-action `--version` re-probe publishes a version (or skips if
|
||||
@@ -244,12 +245,14 @@ internal class PyToolManagementController(
|
||||
refreshRow(toolRow)
|
||||
throw e
|
||||
}
|
||||
val failure = result as? Result.Failure<*>
|
||||
if (failure != null) {
|
||||
toolRow.actionInProgress = false
|
||||
refreshRow(toolRow)
|
||||
Messages.showErrorDialog(project, failure.error.toString(), errorTitle)
|
||||
return
|
||||
val installedPath = when (result) {
|
||||
is Result.Success -> result.result
|
||||
is Result.Failure -> {
|
||||
toolRow.actionInProgress = false
|
||||
refreshRow(toolRow)
|
||||
Messages.showErrorDialog(project, result.error.toString(), errorTitle)
|
||||
return
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
// Invalidate the cached probe so the freshly installed/upgraded binary's version is re-fetched.
|
||||
@@ -264,7 +267,9 @@ internal class PyToolManagementController(
|
||||
}
|
||||
var probeCallbacks = 0
|
||||
var versionResolvedFired = false
|
||||
toolRow.probeVersion(activeScope) { updatedRow ->
|
||||
// Seed the probe with the path the installer just reported, so the row reflects the freshly
|
||||
// installed tool immediately even when its dir is not on PATH (PY-91493).
|
||||
toolRow.probeVersion(activeScope, project, knownPath = installedPath) { updatedRow ->
|
||||
probeCallbacks++
|
||||
if (!versionResolvedFired && updatedRow.version != null) {
|
||||
versionResolvedFired = true
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package com.intellij.python.pytools
|
||||
|
||||
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.EelOsFamily
|
||||
import com.intellij.platform.eel.provider.LocalEelDescriptor
|
||||
import com.intellij.platform.eel.provider.asNioPath
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.platform.eel.provider.toEelApi
|
||||
import com.intellij.platform.eel.provider.utils.stderrString
|
||||
import com.intellij.platform.eel.provider.utils.stdoutString
|
||||
import com.intellij.platform.eel.where
|
||||
@@ -25,6 +24,8 @@ import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.PyRichSdk
|
||||
import com.jetbrains.python.sdk.ToolCommandExecutor
|
||||
import com.jetbrains.python.sdk.add.v2.toFileSystem
|
||||
import com.jetbrains.python.sdk.baseDir
|
||||
import com.jetbrains.python.sdk.moduleIfExists
|
||||
import com.jetbrains.python.sdk.pyRichSdk
|
||||
@@ -44,13 +45,14 @@ suspend fun PyTool.getExecutableWithBaseArgs(
|
||||
workingDir: Path? = null,
|
||||
): PyResult<Pair<BinaryToExec, List<String>>> {
|
||||
val state = getState(moduleOrProject.project)
|
||||
val eelApi = moduleOrProject.project.getEelDescriptor().toEelApi()
|
||||
|
||||
val toolBinaryPath = when (state.discoveryMode) {
|
||||
ExecutableDiscoveryMode.INTERPRETER -> {
|
||||
val pyRichSdk = moduleOrProject.moduleIfExists?.pythonSdk?.pyRichSdk()
|
||||
pyRichSdk?.let { findExecutableInSdk(it, executableName) } ?: findExecutableInPath(state, executableName)
|
||||
pyRichSdk?.let { findExecutableInSdk(it, executableName) } ?: findExecutableInPath(eelApi, state, executableName)
|
||||
}
|
||||
ExecutableDiscoveryMode.PATH -> findExecutableInPath(state, executableName)
|
||||
ExecutableDiscoveryMode.PATH -> findExecutableInPath(eelApi, state, executableName)
|
||||
ExecutableDiscoveryMode.UVX -> null
|
||||
}
|
||||
|
||||
@@ -130,24 +132,22 @@ fun PyTool.findExecutableInSdk(pyRichSdk: PyRichSdk, executableName: String = pa
|
||||
}
|
||||
}
|
||||
|
||||
private fun PyTool.findExecutableInPath(state: PyToolsState.ToolEntry, executableName: String = packageName.name): Path? {
|
||||
return state.customToolBinaryPath ?: findExecutableInPath(executableName)
|
||||
}
|
||||
|
||||
fun PyTool.findExecutableInPath(
|
||||
private suspend fun PyTool.findExecutableInPath(
|
||||
eelApi: EelApi,
|
||||
state: PyToolsState.ToolEntry,
|
||||
executableName: String = packageName.name,
|
||||
osFamily: EelOsFamily = LocalEelDescriptor.osFamily,
|
||||
): Path? = resolveExecutableOnPath(executableName, osFamily)
|
||||
): Path? = state.customToolBinaryPath ?: findExecutableInPath(eelApi, executableName)
|
||||
|
||||
/**
|
||||
* Looks up [executableName] on the system PATH by its OS-specific binary name. This is how the
|
||||
* External Tools settings page resolves tool executables (via [findExecutableInPath]); shared so
|
||||
* other callers can resolve an installed executable the same way.
|
||||
* Resolve [executableName] in the environment [eelApi] describes: on `PATH` and in the well-known per-user
|
||||
* install directories tool installers use (pip's user scripts dir, uv/pipx's `~/.local/bin`, …). Detection
|
||||
* goes through [ToolCommandExecutor] so it matches how the tool was installed — a plain `PATH` lookup misses
|
||||
* those per-user dirs, which are frequently not on `PATH` on Windows (PY-91493).
|
||||
*/
|
||||
fun resolveExecutableOnPath(
|
||||
executableName: String,
|
||||
osFamily: EelOsFamily = LocalEelDescriptor.osFamily,
|
||||
): Path? = PathEnvironmentVariableUtil.findInPath(osFamily.getOsSpecificBinaryName(executableName))?.toPath()
|
||||
suspend fun PyTool.findExecutableInPath(
|
||||
eelApi: EelApi,
|
||||
executableName: String = packageName.name,
|
||||
): Path? = ToolCommandExecutor(executableName).detectToolExecutable(eelApi.toFileSystem()) { true }?.path
|
||||
|
||||
/**
|
||||
* Installs this tool's executable into the environment described by [eel], using the first available
|
||||
|
||||
@@ -28,7 +28,7 @@ import kotlin.time.Duration.Companion.minutes
|
||||
data class ToolCommandExecutor(
|
||||
private val toolName: @NlsSafe String,
|
||||
private val additionalSearchPaths: List<ToolSearchPath> = emptyList(),
|
||||
private val getToolPathFromSettings: PropertiesComponent.() -> @SystemIndependent String?,
|
||||
private val getToolPathFromSettings: PropertiesComponent.() -> @SystemIndependent String? = { null },
|
||||
) {
|
||||
companion object {
|
||||
private val KNOWN_SEARCH_PATHS = listOf(
|
||||
|
||||
+4
-2
@@ -15,9 +15,9 @@ import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.packaging.PyPackageVersionNormalizer
|
||||
import com.jetbrains.python.packaging.repository.PyPiPackageRepository
|
||||
import com.jetbrains.python.sdk.ToolCommandExecutor
|
||||
import com.jetbrains.python.sdk.add.v2.FileSystem
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.detectTool
|
||||
import com.jetbrains.python.sdk.add.v2.toFileSystem
|
||||
import com.jetbrains.python.sdk.installExecutableViaPythonScript
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -61,7 +61,9 @@ private class SystemPythonToolManager(
|
||||
override suspend fun list(): Map<PyTool, InstalledInfo> {
|
||||
return PyTool.EP_NAME.extensionList.filter { it is ConfigurablePyTool }.mapNotNull { tool ->
|
||||
val name = tool.packageName.name
|
||||
val executable = fileSystem.detectTool(name) ?: return@mapNotNull null
|
||||
// Resolve on PATH and in the per-user scripts dirs the pip helper installs into (e.g.
|
||||
// %APPDATA%\Python\Scripts on Windows), which are frequently not on PATH (PY-91493).
|
||||
val executable = ToolCommandExecutor(name).detectToolExecutable(fileSystem) { true } ?: return@mapNotNull null
|
||||
val installed = BinOnEel(executable.path).getToolVersion(name).getOrNull()?.value ?: return@mapNotNull null
|
||||
val latest = latestPyPiVersion(name) ?: installed
|
||||
tool to InstalledInfo(path = executable.path, installedVersion = installed, latestVersion = latest)
|
||||
|
||||
Reference in New Issue
Block a user