From 1e3bf294c7c5430f8a937fffb91f41a15a2d3409 Mon Sep 17 00:00:00 2001 From: Aleksandr Sorotskii Date: Tue, 10 Jun 2025 19:18:34 +0200 Subject: [PATCH] system python providers for unix/mac/win/legacy platform; pyenv/brew system python providers; PY-47379 GitOrigin-RevId: 02c3c3f3e682aa60e48bd429b7862d431ce5ddac --- .../intellij.python.community.impl.xml | 15 ++- .../python/sdk}/WinRegistryService.kt | 3 +- python/services/system-python/BUILD.bazel | 4 +- ...honCommunityServicesSystemPythonIcons.java | 19 +++ ...python.community.services.systemPython.iml | 2 + .../system/providers/expui/homebrew.svg | 1 + .../system/providers/expui/pyenv.svg | 6 + .../providers/BrewSystemPythonProvider.kt | 45 +++++++ .../LegacyPythonProvider.kt} | 19 +-- .../impl/providers/MacSystemPythonProvider.kt | 59 +++++++++ .../providers/PyenvSystemPythonProvider.kt | 64 ++++++++++ .../providers/UnixSystemPythonProvider.kt | 56 +++++++++ .../systemPython/impl/providers/Utils.kt | 44 +++++++ .../providers/WindowsSystemPythonProvider.kt | 119 ++++++++++++++++++ .../systemPython/systemPythonServiceImpl.kt | 6 +- .../sdk/flavors/WinPythonSdkFlavor.java | 1 + .../sdk/poetry/PoetryCommandExecutor.kt | 1 + .../sdk/flavors/WinPythonSdkFlavorTest.java | 1 + 18 files changed, 448 insertions(+), 17 deletions(-) rename python/{src/com/jetbrains/python/sdk/flavors => python-sdk/src/com/jetbrains/python/sdk}/WinRegistryService.kt (95%) create mode 100644 python/services/system-python/gen/com/intellij/python/community/services/systemPython/PythonCommunityServicesSystemPythonIcons.java create mode 100644 python/services/system-python/resources/icons/com/intellij/python/community/system/providers/expui/homebrew.svg create mode 100644 python/services/system-python/resources/icons/com/intellij/python/community/system/providers/expui/pyenv.svg create mode 100644 python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/BrewSystemPythonProvider.kt rename python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/{CoreSystemPythonProvider.kt => providers/LegacyPythonProvider.kt} (67%) create mode 100644 python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/MacSystemPythonProvider.kt create mode 100644 python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/PyenvSystemPythonProvider.kt create mode 100644 python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/UnixSystemPythonProvider.kt create mode 100644 python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/Utils.kt create mode 100644 python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/WindowsSystemPythonProvider.kt diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml index be253d183174..0615596c2e45 100644 --- a/python/pluginResources/intellij.python.community.impl.xml +++ b/python/pluginResources/intellij.python.community.impl.xml @@ -138,8 +138,8 @@ - + @@ -461,6 +461,9 @@ + + @@ -895,6 +898,12 @@ + + + + + + @@ -1119,4 +1128,4 @@ dynamic="true" interface="com.intellij.python.community.services.systemPython.SystemPythonProvider"/> - \ No newline at end of file + diff --git a/python/src/com/jetbrains/python/sdk/flavors/WinRegistryService.kt b/python/python-sdk/src/com/jetbrains/python/sdk/WinRegistryService.kt similarity index 95% rename from python/src/com/jetbrains/python/sdk/flavors/WinRegistryService.kt rename to python/python-sdk/src/com/jetbrains/python/sdk/WinRegistryService.kt index a5d70ae158f1..0926ba87cbc4 100644 --- a/python/src/com/jetbrains/python/sdk/flavors/WinRegistryService.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/WinRegistryService.kt @@ -1,5 +1,5 @@ // 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.flavors +package com.jetbrains.python.sdk import com.intellij.openapi.util.io.WindowsRegistryUtil import org.jetbrains.annotations.ApiStatus @@ -10,7 +10,6 @@ import org.jetbrains.annotations.ApiStatus * @author Ilya.Kazakevich */ @ApiStatus.Internal - interface WinRegistryService { /** * @param basePath path like "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node" diff --git a/python/services/system-python/BUILD.bazel b/python/services/system-python/BUILD.bazel index ca81b66e771c..32467f8e4092 100644 --- a/python/services/system-python/BUILD.bazel +++ b/python/services/system-python/BUILD.bazel @@ -18,7 +18,7 @@ jvm_library( name = "system-python", module_name = "intellij.python.community.services.systemPython", visibility = ["//visibility:public"], - srcs = glob(["src/**/*.kt", "src/**/*.java"], allow_empty = True), + srcs = glob(["gen/**/*.kt", "gen/**/*.java", "src/**/*.kt", "src/**/*.java"], allow_empty = True), deps = [ "@lib//:kotlin-stdlib", "//python/python-sdk:sdk", @@ -34,6 +34,7 @@ jvm_library( "//platform/util", "//python/installer", "//python/python-venv:community-impl-venv", + "//python/python-psi-impl:psi-impl", ], exports = ["//python/services/shared"], runtime_deps = [":system-python_resources"] @@ -75,6 +76,7 @@ jvm_library( "@lib//:io-mockk-jvm", "//platform/testFramework", "//platform/testFramework:testFramework_test_lib", + "//python/python-psi-impl:psi-impl", ], runtime_deps = [ ":system-python_resources", diff --git a/python/services/system-python/gen/com/intellij/python/community/services/systemPython/PythonCommunityServicesSystemPythonIcons.java b/python/services/system-python/gen/com/intellij/python/community/services/systemPython/PythonCommunityServicesSystemPythonIcons.java new file mode 100644 index 000000000000..1a842014212c --- /dev/null +++ b/python/services/system-python/gen/com/intellij/python/community/services/systemPython/PythonCommunityServicesSystemPythonIcons.java @@ -0,0 +1,19 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.community.services.systemPython; + +import com.intellij.ui.IconManager; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +/** + * NOTE THIS FILE IS AUTO-GENERATED + * DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead + */ +public final class PythonCommunityServicesSystemPythonIcons { + private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) { + return IconManager.getInstance().loadRasterizedIcon(path, PythonCommunityServicesSystemPythonIcons.class.getClassLoader(), cacheKey, flags); + } + /** 16x16 */ public static final @NotNull Icon Homebrew = load("icons/com/intellij/python/community/system/providers/expui/homebrew.svg", -2104807435, 0); + /** 16x16 */ public static final @NotNull Icon Pyenv = load("icons/com/intellij/python/community/system/providers/expui/pyenv.svg", -770384196, 0); +} diff --git a/python/services/system-python/intellij.python.community.services.systemPython.iml b/python/services/system-python/intellij.python.community.services.systemPython.iml index 3a0cef502ca5..a665ee86be8d 100644 --- a/python/services/system-python/intellij.python.community.services.systemPython.iml +++ b/python/services/system-python/intellij.python.community.services.systemPython.iml @@ -3,6 +3,7 @@ + @@ -33,5 +34,6 @@ + \ No newline at end of file diff --git a/python/services/system-python/resources/icons/com/intellij/python/community/system/providers/expui/homebrew.svg b/python/services/system-python/resources/icons/com/intellij/python/community/system/providers/expui/homebrew.svg new file mode 100644 index 000000000000..a977351328b4 --- /dev/null +++ b/python/services/system-python/resources/icons/com/intellij/python/community/system/providers/expui/homebrew.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/python/services/system-python/resources/icons/com/intellij/python/community/system/providers/expui/pyenv.svg b/python/services/system-python/resources/icons/com/intellij/python/community/system/providers/expui/pyenv.svg new file mode 100644 index 000000000000..003e39edf1c2 --- /dev/null +++ b/python/services/system-python/resources/icons/com/intellij/python/community/system/providers/expui/pyenv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/BrewSystemPythonProvider.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/BrewSystemPythonProvider.kt new file mode 100644 index 000000000000..d9cdb216b1d8 --- /dev/null +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/BrewSystemPythonProvider.kt @@ -0,0 +1,45 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.community.services.systemPython.impl.providers + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.isMac +import com.intellij.python.community.services.shared.UICustomization +import com.intellij.python.community.services.systemPython.PythonCommunityServicesSystemPythonIcons +import com.intellij.python.community.services.systemPython.SystemPythonProvider +import com.jetbrains.python.PythonBinary +import com.jetbrains.python.errorProcessing.PyResult +import java.nio.file.Path +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + + +private class BrewSystemPythonProvider : SystemPythonProvider { + private val LOGGER: Logger = Logger.getInstance(BrewSystemPythonProvider::class.java) + private val binDirectory = Path.of("/opt/homebrew/bin") + + override suspend fun findSystemPythons(eelApi: EelApi): PyResult> { + if (!eelApi.platform.isMac) { + return PyResult.success(emptySet()) + } + + val pythons = withContext(Dispatchers.IO) { + try { + return@withContext collectPythonsInPaths(eelApi, listOf(binDirectory), listOf(python3XNamePattern)) + } + catch (e: RuntimeException) { + LOGGER.error("failed to discover brew pythons", e) + } + + return@withContext emptySet() + } + + return PyResult.success(pythons) + } + + override val uiCustomization: UICustomization? + get() { + // TODO: proper icon + return UICustomization(title = "homebrew", icon = PythonCommunityServicesSystemPythonIcons.Homebrew) + } +} diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/CoreSystemPythonProvider.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/LegacyPythonProvider.kt similarity index 67% rename from python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/CoreSystemPythonProvider.kt rename to python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/LegacyPythonProvider.kt index b8fa47f11cf7..6f4911f5a8a9 100644 --- a/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/CoreSystemPythonProvider.kt +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/LegacyPythonProvider.kt @@ -1,28 +1,33 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.python.community.services.systemPython.impl +package com.intellij.python.community.services.systemPython.impl.providers import com.intellij.platform.eel.EelApi import com.intellij.platform.eel.provider.localEel import com.intellij.python.community.services.systemPython.SystemPythonProvider import com.jetbrains.python.PythonBinary -import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.sdk.flavors.PythonSdkFlavor import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext /** - * [SystemPythonProvider] based ob [PythonSdkFlavor] (kind of bridge) + * [SystemPythonProvider] based of [PythonSdkFlavor] */ -internal object CoreSystemPythonProvider : SystemPythonProvider { +internal class LegacySystemPythonProvider : SystemPythonProvider { override suspend fun findSystemPythons(eelApi: EelApi): PyResult> { - if (eelApi != localEel) return Result.success(emptySet()) - with(Dispatchers.IO) { + if (eelApi != localEel || !useLegacyPythonProvider()) { + return PyResult.success(emptySet()) + } + + val pythons = withContext(Dispatchers.IO) { val paths = PythonSdkFlavor.getApplicableFlavors(false) .flatMap { it.dropCaches() it.suggestLocalHomePaths(null, null) } - return Result.success(paths.toSet()) + return@withContext PyResult.success(paths.toSet()) } + + return pythons } } \ No newline at end of file diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/MacSystemPythonProvider.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/MacSystemPythonProvider.kt new file mode 100644 index 000000000000..3e5ba4f4c4d0 --- /dev/null +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/MacSystemPythonProvider.kt @@ -0,0 +1,59 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.community.services.systemPython.impl.providers + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.isMac +import com.intellij.python.community.services.shared.UICustomization +import com.intellij.python.community.services.systemPython.SystemPythonProvider +import com.jetbrains.python.PythonBinary +import com.jetbrains.python.errorProcessing.PyResult +import java.nio.file.Path +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + + +private class MacSystemPythonProvider : SystemPythonProvider { + private val LOGGER: Logger = Logger.getInstance(MacSystemPythonProvider::class.java) + + private val directories = listOf( + Path.of("/usr/bin"), + Path.of("/usr/local/bin"), + Path.of("/usr/local/Cellar/python"), + Path.of("/Library/Frameworks/Python.framework/Versions"), + Path.of("/System/Library/Frameworks/Python.framework/Versions"), + ) + + // Patterns to match Python executable filenames + private val names = listOf( + python3NamePattern, + python3XNamePattern, + pypyNamePattern, + ) + + override suspend fun findSystemPythons(eelApi: EelApi): PyResult> { + // Check if we're on a Unix system that's not Mac + if (!eelApi.platform.isMac || useLegacyPythonProvider()) { + return PyResult.success(emptySet()) + } + + val pythons = withContext(Dispatchers.IO) { + try { + return@withContext collectPythonsInPaths(eelApi, directories, names) + } + catch (e: RuntimeException) { + LOGGER.error("Failed to discover mac system pythons", e) + } + + return@withContext emptySet() + } + + return PyResult.success(pythons) + } + + override val uiCustomization: UICustomization? + get() { + // TODO: + return null + } +} \ No newline at end of file diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/PyenvSystemPythonProvider.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/PyenvSystemPythonProvider.kt new file mode 100644 index 000000000000..1f785118c841 --- /dev/null +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/PyenvSystemPythonProvider.kt @@ -0,0 +1,64 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.community.services.systemPython.impl.providers + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.getOrNull +import com.intellij.platform.eel.path.EelPath +import com.intellij.platform.eel.provider.asNioPath +import com.intellij.python.community.services.shared.UICustomization +import com.intellij.python.community.services.systemPython.PythonCommunityServicesSystemPythonIcons +import com.intellij.python.community.services.systemPython.SystemPythonProvider +import com.jetbrains.python.PythonBinary +import com.jetbrains.python.errorProcessing.PyResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + + +private class PyenvSystemPythonProvider : SystemPythonProvider { + private val LOGGER: Logger = Logger.getInstance(PyenvSystemPythonProvider::class.java) + + override suspend fun findSystemPythons(eelApi: EelApi): PyResult> { + if (useLegacyPythonProvider()) { + return PyResult.success(emptySet()) + } + + val pythons = withContext(Dispatchers.IO) { + try { + val env = eelApi.exec.fetchLoginShellEnvVariables() + val pyenvRoot = if ("PYENV_ROOT" in env) { + EelPath.parse(env["PYENV_ROOT"]!!, eelApi.descriptor) + } + else { + eelApi.userInfo.home.resolve(".pyenv") + } + + val versionsDir = pyenvRoot.resolve("versions") + val entries = eelApi.fs.listDirectory(versionsDir) + .getOrNull() + + if (entries == null) { + return@withContext emptySet() + } + + val paths = entries + .map { versionsDir.resolve(it).resolve("bin").asNioPath() } + + return@withContext collectPythonsInPaths(eelApi, paths, listOf(python3NamePattern)) + } + catch (e: RuntimeException) { + LOGGER.error("failed to discover pyenv pythons", e) + } + + return@withContext emptySet() + } + + return PyResult.success(pythons) + } + + override val uiCustomization: UICustomization? + get() { + // TODO: proper icon + return UICustomization(title = "pyenv", icon = PythonCommunityServicesSystemPythonIcons.Pyenv) + } +} diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/UnixSystemPythonProvider.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/UnixSystemPythonProvider.kt new file mode 100644 index 000000000000..79de99b2d68a --- /dev/null +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/UnixSystemPythonProvider.kt @@ -0,0 +1,56 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.community.services.systemPython.impl.providers + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.EelPlatform +import com.intellij.platform.eel.isMac +import com.intellij.python.community.services.systemPython.SystemPythonProvider +import com.intellij.python.community.services.shared.UICustomization +import com.jetbrains.python.PythonBinary +import com.jetbrains.python.errorProcessing.PyResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Path + + +private class UnixSystemPythonProvider : SystemPythonProvider { + private val LOGGER: Logger = Logger.getInstance(UnixSystemPythonProvider::class.java) + + private val directories = listOf( + Path.of("/usr/bin"), + Path.of("/usr/local/bin")) + + // Patterns to match Python executable filenames + private val names = listOf( + python3NamePattern, + python3XNamePattern, + pypyNamePattern, + ) + + override suspend fun findSystemPythons(eelApi: EelApi): PyResult> { + // Check if we're on a Unix system that's not Mac + if (eelApi.platform !is EelPlatform.Posix || eelApi.platform.isMac || useLegacyPythonProvider()) { + return PyResult.success(emptySet()) + } + + val pythons = withContext(Dispatchers.IO) { + try { + return@withContext collectPythonsInPaths(eelApi, directories, names) + } + catch (e: RuntimeException) { + LOGGER.error("Failed to discover unix system pythons", e) + } + + return@withContext emptySet() + } + + return PyResult.success(pythons) + } + + override val uiCustomization: UICustomization? + get() { + // TODO: + return null + } +} \ No newline at end of file diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/Utils.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/Utils.kt new file mode 100644 index 000000000000..b3bc7b1d83e9 --- /dev/null +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/Utils.kt @@ -0,0 +1,44 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.community.services.systemPython.impl.providers + +import com.intellij.openapi.util.registry.Registry +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.fs.EelFileSystemApi.StatError +import com.intellij.platform.eel.fs.stat +import com.intellij.platform.eel.getOrNull +import com.intellij.platform.eel.path.EelPath +import com.intellij.platform.eel.provider.asNioPath +import java.nio.file.Path +import java.util.regex.Pattern +import kotlin.collections.map +import kotlin.io.path.pathString + + +internal val pypyNamePattern: Pattern = Pattern.compile("pypy$") +internal val python3NamePattern: Pattern = Pattern.compile("python3$") +internal val python3XNamePattern: Pattern = Pattern.compile("python3\\.[0-9]+$") + +internal fun useLegacyPythonProvider(): Boolean { + return Registry.`is`("python.use.system.legacy.provider") +} + +internal suspend fun collectPythonsInPaths(eelApi: EelApi, paths: List, names: List): Set { + val pythons = mutableSetOf() + + for (path in paths) { + val directory = EelPath.parse(path.pathString, eelApi.descriptor) + if (eelApi.fs.stat(directory).eelIt() is StatError) { + continue + } + + val entries = eelApi.fs.listDirectory(directory) + .getOrNull() + + entries + ?.map { directory.resolve(it).asNioPath() } + ?.filter { names.firstOrNull { name -> name.matcher(it.fileName.toString()).matches() } != null } + ?.let { pythons.addAll(it) } + } + + return pythons +} \ No newline at end of file diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/WindowsSystemPythonProvider.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/WindowsSystemPythonProvider.kt new file mode 100644 index 000000000000..34451485bed9 --- /dev/null +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/impl/providers/WindowsSystemPythonProvider.kt @@ -0,0 +1,119 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.python.community.services.systemPython.impl.providers + +import com.intellij.execution.configurations.PathEnvironmentVariableUtil +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.isWindows +import com.intellij.platform.eel.provider.localEel +import com.intellij.python.community.services.systemPython.SystemPythonProvider +import com.jetbrains.python.PythonBinary +import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.sdk.PythonSdkUtil +import com.jetbrains.python.sdk.WinRegistryService +import com.jetbrains.python.sdk.getAppxFiles +import com.jetbrains.python.venvReader.tryResolvePath +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Path +import java.util.regex.Pattern +import kotlin.collections.iterator +import kotlin.io.path.exists +import kotlin.io.path.pathString + + +class WindowsSystemPythonProvider(val winRegistryBase: WinRegistryService? = null) : SystemPythonProvider { + private val LOGGER: Logger = Logger.getInstance(WindowsSystemPythonProvider::class.java) + + private val names = listOf( + "pypy.exe", + "python.exe") + + // Registry roots and product mappings from WinPythonSdkFlavor + private val REG_ROOTS = arrayOf("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER") + private val REGISTRY_MAP = mapOf( + "Python" to "python.exe", + "IronPython" to "ipy.exe" + ) + val winRegistry: Lazy = lazy { + winRegistryBase ?: ApplicationManager.getApplication().getService(WinRegistryService::class.java) + } + + // Windows Store Python product name + private val APPX_PRODUCT = "Python" + private val pythonVersionedExePattern = Pattern.compile("python[0-9.]*?\\.exe$") + + override suspend fun findSystemPythons(eelApi: EelApi): PyResult> { + if (eelApi != localEel || !eelApi.platform.isWindows || useLegacyPythonProvider()) { + return PyResult.success(emptySet()) + } + + val pythons = withContext(Dispatchers.IO) { + try { + val candidates = mutableSetOf() + + for (name in names) { + val binaries = PathEnvironmentVariableUtil.findAllExeFilesInPath(name) + .mapNotNull { it.toPath() } + .filter { !PythonSdkUtil.isConda(it.pathString) } + .toSet() + + candidates.addAll(binaries) + } + + candidates.addAll(getPythonsFromStore()) + candidates.addAll(getPythonsFromRegistry()) + + return@withContext candidates + } + catch (e: RuntimeException) { + LOGGER.error("Failed to discover Windows system pythons", e) + } + + return@withContext emptySet() + } + + return PyResult.success(pythons) + } + + // Check https://www.python.org/dev/peps/pep-0514/ for windows registry layout to understand + private fun getPythonsFromRegistry(): Set { + val candidates = mutableSetOf() + + for (regRoot in REG_ROOTS) { + for ((productId, exe) in REGISTRY_MAP) { + val companiesPath = "$regRoot\\SOFTWARE\\$productId" + val companiesPathWow = "$regRoot\\SOFTWARE\\Wow6432Node\\$productId" + + for (path in arrayOf(companiesPath, companiesPathWow)) { + for (company in winRegistry.value.listBranches(path)) { + val pathToCompany = "$path\\$company" + + for (version in winRegistry.value.listBranches(pathToCompany)) { + val folder = winRegistry.value.getDefaultKey("$pathToCompany\\$version\\InstallPath") + tryResolvePath(folder) + ?.resolve(exe) + ?.takeIf(Path::exists) + ?.let { candidates.add(it) } + } + } + } + } + } + + return candidates + } + + private fun getPythonsFromStore(): Set { + return try { + getAppxFiles(APPX_PRODUCT, pythonVersionedExePattern.toRegex()) + .map { it.toAbsolutePath() } + .toSet() + } + catch (e: Exception) { + LOGGER.debug("Error getting Python from Windows Store", e) + emptySet() + } + } +} diff --git a/python/services/system-python/src/com/intellij/python/community/services/systemPython/systemPythonServiceImpl.kt b/python/services/system-python/src/com/intellij/python/community/services/systemPython/systemPythonServiceImpl.kt index b6262a4fb0ae..74ae45611d3f 100644 --- a/python/services/system-python/src/com/intellij/python/community/services/systemPython/systemPythonServiceImpl.kt +++ b/python/services/system-python/src/com/intellij/python/community/services/systemPython/systemPythonServiceImpl.kt @@ -15,7 +15,6 @@ import com.intellij.python.community.services.internal.impl.VanillaPythonWithLan import com.intellij.python.community.services.shared.UICustomization import com.intellij.python.community.services.systemPython.SystemPythonServiceImpl.MyServiceState import com.intellij.python.community.services.systemPython.impl.Cache -import com.intellij.python.community.services.systemPython.impl.CoreSystemPythonProvider import com.intellij.python.community.services.systemPython.impl.PySystemPythonBundle import com.jetbrains.python.PythonBinary import com.jetbrains.python.Result @@ -93,7 +92,7 @@ internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonServ // Only strings are supported by serializer var userProvidedPythons by list() val userProvidedPythonsAsPath: Collection - get() = userProvidedPythons.filterNotNull().mapNotNull { + get() = userProvidedPythons.mapNotNull { try { Path.of(it) } @@ -109,8 +108,7 @@ internal class SystemPythonServiceImpl(scope: CoroutineScope) : SystemPythonServ findPythonsMutex.withLock { val pythonsUi = mutableMapOf() - val pythonsFromExtensions = (SystemPythonProvider.EP - .extensionList + listOf(CoreSystemPythonProvider)) + val pythonsFromExtensions = SystemPythonProvider.EP.extensionList .flatMap { provider -> val pythons = provider.findSystemPythons(eelApi).getOrNull() ?: emptyList() val ui = provider.uiCustomization diff --git a/python/src/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavor.java b/python/src/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavor.java index aa12207ee181..d8b334874f2d 100644 --- a/python/src/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavor.java +++ b/python/src/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavor.java @@ -16,6 +16,7 @@ import com.intellij.python.community.helpersLocator.PythonHelpersLocator; import com.intellij.util.concurrency.SynchronizedClearableLazy; import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.python.sdk.WinRegistryService; import kotlin.text.Regex; import org.jetbrains.annotations.*; diff --git a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt index 598cd862e7da..a22cb65d47ca 100644 --- a/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt +++ b/python/src/com/jetbrains/python/sdk/poetry/PoetryCommandExecutor.kt @@ -117,6 +117,7 @@ suspend fun setupPoetry(projectPath: Path, python: String?, installPackages: Boo if (init) { runPoetry(projectPath, *listOf("init", "-n").toTypedArray()) .getOr { return it } + if (python != null) { // Replace a python version in toml ExecService().execGetStdout(Path.of(python), listOf("-c", REPLACE_PYTHON_VERSION), ExecOptions(workingDirectory = projectPath)) .getOr { return it } diff --git a/python/testSrc/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavorTest.java b/python/testSrc/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavorTest.java index 502a53ef80ce..ad9790d579f3 100644 --- a/python/testSrc/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavorTest.java +++ b/python/testSrc/com/jetbrains/python/sdk/flavors/WinPythonSdkFlavorTest.java @@ -2,6 +2,7 @@ package com.jetbrains.python.sdk.flavors; import com.intellij.openapi.util.io.FileUtil; +import com.jetbrains.python.sdk.WinRegistryService; import org.easymock.EasyMock; import org.easymock.IMocksControl; import org.easymock.MockType;