PY-91493 Fix "Cannot find executable" after installing Poetry/uv on Windows

Post-install detection looked up the tool on PATH only, but pip and uv drop
launchers into per-user dirs not on PATH (%APPDATA%\Python\Scripts, uv's bin).
Use the pip helper's reported path and uv's `tool list --show-paths` instead;
also run `uv tool update-shell` so the uv bin dir lands on the user's PATH.

GitOrigin-RevId: 3c09002a08135d7bb98733185815f3ded6e21c52
This commit is contained in:
Vitaly Legchilkin
2026-08-11 21:52:40 +00:00
committed by intellij-monorepo-bot
parent 6706df30f8
commit 0137d9f471
3 changed files with 28 additions and 13 deletions
@@ -1,6 +1,7 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.uv.backend
import com.intellij.openapi.diagnostic.Logger
import com.intellij.platform.eel.EelApi
import com.intellij.python.pytools.InstalledInfo
import com.intellij.python.pytools.PyTool
@@ -11,14 +12,16 @@ import com.intellij.python.uv.backend.runtime.uvCli
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrNull
import com.jetbrains.python.orLogException
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.impl.PySdkBundle
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
private val LOG = Logger.getInstance(UvToolManagerProvider::class.java)
/**
* Provides a [UvToolManager] when a local `uv` is available for the target environment. Registered
* `order="first"` so uv is preferred over the pip fallback.
@@ -69,9 +72,19 @@ private class UvToolManager(
}
private suspend fun run(tool: PyTool, reinstall: Boolean): PyResult<Path> {
createUvToolRuntime(uv).uvCli().tool().install(tool.packageName.name, reinstall = reinstall).getOr { return it }
val executable = fileSystem.detectTool(tool.packageName.name)
?: return PyResult.localizedError(PySdkBundle.message("cannot.find.executable", tool.packageName.name, fileSystem.userReadableName))
return Result.success(executable.path)
val name = tool.packageName.name
val uvTool = createUvToolRuntime(uv).uvCli().tool()
uvTool.install(name, reinstall = reinstall).getOr { return it }
// uv installs launchers into its own bin dir (`uv tool dir --bin`), which is not on PATH by default. Add it
// so the tool is also runnable from the user's shell; best-effort, since the IDE uses the resolved path below
// regardless (PY-91493).
uvTool.updateShell().orLogException(LOG)
// Resolve the executable from uv's own records rather than re-detecting it on PATH: the current process does
// not see the freshly added PATH entry, so a PATH lookup would spuriously fail with "cannot find executable".
val executable = uvTool.list(showPaths = true).getOrNull()
?.firstOrNull { PyTool.findByPackageName(it.name) == tool }
?.let { it.executables[name] ?: it.executables.values.firstOrNull() }
?: return PyResult.localizedError(PySdkBundle.message("cannot.find.executable", name, fileSystem.userReadableName))
return Result.success(executable)
}
}
@@ -5,6 +5,7 @@ import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
import com.intellij.python.pytools.runtime.PyToolRuntime
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.mapResult
import java.nio.file.Path
/**
@@ -98,9 +99,12 @@ class UvTool(runtime: PyToolRuntime) : UvCommand("tool", runtime) {
suspend fun uninstall(): PyResult<Unit> = TODO()
/**
* Ensure that the tool executable directory is on the PATH
* `uv tool update-shell` — ensure the tool executable directory (`uv tool dir --bin`) is on the `PATH`,
* updating the user's shell profile (Unix) or user `PATH` (Windows) if needed. Idempotent: a no-op when
* the directory is already present. uv prints nothing of interest, so the stdout is discarded.
*/
suspend fun updateShell(): PyResult<Unit> = TODO()
suspend fun updateShell(): PyResult<Unit> =
executeAndHandleErrors("update-shell", transformer = ZeroCodeStdoutTransformer).mapResult { Result.success(Unit) }
/**
* `uv tool dir` — the directory uv stores tools in, or (with [bin] = true) the directory their
@@ -11,7 +11,6 @@ import com.intellij.python.pytools.PyToolManager
import com.intellij.python.pytools.PyToolManagerProvider
import com.intellij.python.pytools.configuration.ConfigurablePyTool
import com.intellij.python.pytools.getToolVersion
import com.jetbrains.python.Result
import com.jetbrains.python.errorProcessing.PyResult
import com.jetbrains.python.getOrNull
import com.jetbrains.python.packaging.PyPackageVersionNormalizer
@@ -20,7 +19,6 @@ 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.impl.PySdkBundle
import com.jetbrains.python.sdk.installExecutableViaPythonScript
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -46,10 +44,10 @@ private class SystemPythonToolManager(
private val systemPython: SystemPython,
) : PyToolManager {
override suspend fun install(tool: PyTool): PyResult<Path> {
installExecutableViaPythonScript(systemPython.asExecutablePython.binary, "-n", tool.packageName.name).getOr { return it }
val executable = fileSystem.detectTool(tool.packageName.name)
?: return PyResult.localizedError(PySdkBundle.message("cannot.find.executable", tool.packageName.name, fileSystem.userReadableName))
return Result.success(executable.path)
// The pip helper drops the launcher into a per-user scripts directory that is frequently not on PATH
// (e.g. %APPDATA%\Python\Scripts on Windows), so trust the path it reports rather than re-detecting the
// tool on PATH, which would spuriously fail with "cannot find executable" (PY-91493).
return installExecutableViaPythonScript(systemPython.asExecutablePython.binary, "-n", tool.packageName.name)
}
/** The pip helper always installs the latest release, so an upgrade is just a fresh install. */