From 0137d9f4715b4e7c06d97a280003333831059f73 Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Tue, 11 Aug 2026 16:15:03 +0200 Subject: [PATCH] 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 --- .../uv/backend/UvToolManagerProvider.kt | 23 +++++++++++++++---- .../python/uv/backend/cli/uv/UvTool.kt | 8 +++++-- .../SystemPythonToolManagerProvider.kt | 10 ++++---- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/python/python-uv/backend/src/com/intellij/python/uv/backend/UvToolManagerProvider.kt b/python/python-uv/backend/src/com/intellij/python/uv/backend/UvToolManagerProvider.kt index 2f8c8939ed00..0c8bf1f71fb2 100644 --- a/python/python-uv/backend/src/com/intellij/python/uv/backend/UvToolManagerProvider.kt +++ b/python/python-uv/backend/src/com/intellij/python/uv/backend/UvToolManagerProvider.kt @@ -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 { - 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) } } diff --git a/python/python-uv/backend/src/com/intellij/python/uv/backend/cli/uv/UvTool.kt b/python/python-uv/backend/src/com/intellij/python/uv/backend/cli/uv/UvTool.kt index 04fb4444f154..e46db0c9e052 100644 --- a/python/python-uv/backend/src/com/intellij/python/uv/backend/cli/uv/UvTool.kt +++ b/python/python-uv/backend/src/com/intellij/python/uv/backend/cli/uv/UvTool.kt @@ -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 = 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 = TODO() + suspend fun updateShell(): PyResult = + 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 diff --git a/python/src/com/jetbrains/python/sdk/configuration/SystemPythonToolManagerProvider.kt b/python/src/com/jetbrains/python/sdk/configuration/SystemPythonToolManagerProvider.kt index 34e489b37077..07e53be98851 100644 --- a/python/src/com/jetbrains/python/sdk/configuration/SystemPythonToolManagerProvider.kt +++ b/python/src/com/jetbrains/python/sdk/configuration/SystemPythonToolManagerProvider.kt @@ -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 { - 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. */