diff --git a/python/helpers/remote_sync.py b/python/helpers/remote_sync.py index da9a977ee474..16e8ff430b64 100644 --- a/python/helpers/remote_sync.py +++ b/python/helpers/remote_sync.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import argparse import json import os +import errno import re import sys import zipfile @@ -64,6 +65,19 @@ else: sort_keys=True) +def delete_if_exists(path): + try: + os.remove(path) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + +def create_empty_file(path): + with open(path, 'w', encoding='utf-8'): + pass + + # noinspection DuplicatedCode def is_source_file(path): # Skip directories, character and block special devices, named pipes @@ -129,8 +143,10 @@ class RemoteSync(object): self.in_state_json = state_json self._name_counts = defaultdict(int) self._test_root = None + self._success_file = os.path.join(self.output_dir, '.success') def run(self): + delete_if_exists(self._success_file) out_state_json = {'roots': []} for root in self.roots: zip_path = os.path.join(self.output_dir, self.root_zip_name(root)) @@ -141,6 +157,7 @@ class RemoteSync(object): if self.skipped_roots: out_state_json['skipped_roots'] = self.skipped_roots dump_json(out_state_json, os.path.join(self.output_dir, '.state.json')) + create_empty_file(self._success_file) def collect_sources_in_root(self, root, zip_path, old_state): new_state = self.empty_root_state() diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 1d8991dc0a3f..e3185a30d5d6 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -1800,3 +1800,4 @@ sdk.create.venv.suggestion.no.arg=Create a virtual environment sdk.cannot.find.venv.for.module=Can't find venv for the module sdk.set.up.uv.environment=Set up a uv {0} environment +sdk.cannot.find.uv.executable=Cannot find uv executable diff --git a/python/python-exec-service/src/com/intellij/python/community/execService/api.kt b/python/python-exec-service/src/com/intellij/python/community/execService/api.kt index de1eb7763bf1..1578a57e5e6e 100644 --- a/python/python-exec-service/src/com/intellij/python/community/execService/api.kt +++ b/python/python-exec-service/src/com/intellij/python/community/execService/api.kt @@ -31,6 +31,12 @@ import java.util.concurrent.CopyOnWriteArrayList import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes +/** + * A relative path on the target filesystem. + * Unlike [FullPathOnTarget], this represents a path relative to a working directory. + */ +typealias RelativePathOnTarget = String + /** * Default service implementation @@ -54,8 +60,8 @@ data class BinOnEel(val path: Path, internal val workDir: Path? = null) : Binary * Legacy Targets-based approach. Do not use it, unless you know what you are doing * if [target] "local" target is used */ -data class BinOnTarget(internal val configureTargetCmdLine: (TargetedCommandLineBuilder) -> Unit, val target: TargetEnvironmentConfiguration) : BinaryToExec { - constructor(exePath: FullPathOnTarget, target: TargetEnvironmentConfiguration) : this({ it.setExePath(exePath) }, target) +data class BinOnTarget(internal val configureTargetCmdLine: (TargetedCommandLineBuilder) -> Unit, val target: TargetEnvironmentConfiguration, val workingDir: Path? = null) : BinaryToExec { + constructor(exePath: FullPathOnTarget, target: TargetEnvironmentConfiguration, workingDir: Path? = null) : this({ it.setExePath(exePath) }, target, workingDir) @RequiresBackgroundThread fun getLocalExePath(): Lazy = lazy { @@ -231,12 +237,24 @@ enum class ConcurrentProcessWeight { HEAVY } +/** + * Configuration for downloading files after command execution. + * Uses existing upload volume mappings (from web deployment). + * + * @param relativePaths Relative paths to download from the working directory. + * Empty list means download entire working directory. + */ +data class DownloadConfig( + val relativePaths: List = emptyList(), +) + /** * @property[env] Environment variables to be applied with the process run * @property[timeout] Process gets killed after this timeout * @property[processDescription] optional description to be displayed to user * @property[tty] Much like [com.intellij.platform.eel.EelExecApi.Pty] * @property[weight] use it to limit the number of concurrent processes not to exhaust user resources, see [ConcurrentProcessWeight] + * @property[downloadAfterExecution] configuration for downloading files after command execution (Target-based execution only) */ data class ExecOptions( override val env: Map = emptyMap(), @@ -244,6 +262,7 @@ data class ExecOptions( val timeout: Duration = 5.minutes, override val tty: TtySize? = null, val weight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT, + val downloadAfterExecution: DownloadConfig? = null, ) : ExecOptionsBase diff --git a/python/python-exec-service/src/com/intellij/python/community/execService/impl/ExecServiceImpl.kt b/python/python-exec-service/src/com/intellij/python/community/execService/impl/ExecServiceImpl.kt index 223902ca9907..75dab2228cb2 100644 --- a/python/python-exec-service/src/com/intellij/python/community/execService/impl/ExecServiceImpl.kt +++ b/python/python-exec-service/src/com/intellij/python/community/execService/impl/ExecServiceImpl.kt @@ -56,7 +56,8 @@ internal class ExecServiceImpl private constructor() : ExecService { private suspend fun create(binary: BinaryToExec, args: Args, options: ExecOptionsBase, scopeToBind: CoroutineScope? = null): Result { val scope = scopeToBind ?: ApplicationManager.getApplication().service().scope - val request = LaunchRequest(scope, args, options.env, options.tty) + val downloadConfig = (options as? ExecOptions)?.downloadAfterExecution + val request = LaunchRequest(scope, args, options.env, options.tty, downloadConfig) return Result.success( when (binary) { is BinOnEel -> createProcessLauncherOnEel(binary, request) diff --git a/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/DefaultTargetEnvironmentRequestHandler.kt b/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/DefaultTargetEnvironmentRequestHandler.kt index 27562f889a89..80ca01b96901 100644 --- a/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/DefaultTargetEnvironmentRequestHandler.kt +++ b/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/DefaultTargetEnvironmentRequestHandler.kt @@ -7,7 +7,11 @@ import com.intellij.python.community.execService.spi.TargetEnvironmentRequestHan import java.nio.file.Path class DefaultTargetEnvironmentRequestHandler : TargetEnvironmentRequestHandler { - override fun mapUploadRoots(request: TargetEnvironmentRequest, localDirs: Set): List { + override fun mapUploadRoots( + request: TargetEnvironmentRequest, + localDirs: Set, + workingDirToDownload: Path?, + ): List { val result = localDirs.map { localDir -> TargetEnvironment.UploadRoot( localRootPath = localDir, diff --git a/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/ProcessLauncher.kt b/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/ProcessLauncher.kt index 26093adfc579..3ac182f1bb3c 100644 --- a/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/ProcessLauncher.kt +++ b/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/ProcessLauncher.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.diagnostic.fileLogger import com.intellij.platform.eel.provider.utils.ProcessFunctions import com.intellij.python.community.execService.Args import com.intellij.python.community.execService.ConcurrentProcessWeight +import com.intellij.python.community.execService.DownloadConfig import com.intellij.python.community.execService.TtySize import com.intellij.python.community.execService.impl.LoggingProcess import com.jetbrains.python.Result @@ -60,4 +61,5 @@ internal data class LaunchRequest( val args: Args, val env: Map, val usePty: TtySize?, -) \ No newline at end of file + val downloadConfig: DownloadConfig? = null, +) diff --git a/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/targets.kt b/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/targets.kt index 1d3daa147122..ca99a2596241 100644 --- a/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/targets.kt +++ b/python/python-exec-service/src/com/intellij/python/community/execService/impl/processLaunchers/targets.kt @@ -14,10 +14,12 @@ import com.intellij.execution.target.getTargetPaths import com.intellij.execution.target.local.LocalTargetEnvironmentRequest import com.intellij.execution.target.local.LocalTargetPtyOptions import com.intellij.openapi.diagnostic.fileLogger +import com.intellij.openapi.progress.coroutineToIndicator import com.intellij.openapi.project.ProjectManager import com.intellij.platform.eel.provider.utils.ProcessFunctions import com.intellij.platform.eel.provider.utils.bindProcessToScopeImpl import com.intellij.python.community.execService.BinOnTarget +import com.intellij.python.community.execService.DownloadConfig import com.intellij.python.community.execService.ExecuteGetProcessError import com.intellij.python.community.execService.impl.PyExecBundle import com.intellij.python.community.execService.spi.TargetEnvironmentRequestHandler @@ -30,6 +32,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import java.io.IOException import kotlin.io.path.pathString import kotlin.time.Duration.Companion.milliseconds @@ -50,11 +53,24 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau else LocalTargetEnvironmentRequest() // Broken Targets API can only upload the whole directory - val dirsToMap = launchRequest.args.localFiles.map { it.parent }.toSet() + val dirsToMap = buildSet { + addAll(launchRequest.args.localFiles.map { it.parent }) + binOnTarget.workingDir?.takeIf { it.pathString.isNotBlank() }?.also { + add(it) + } + } val handler = TargetEnvironmentRequestHandler.getHandler(request) - val uploadRoots = handler.mapUploadRoots(request, dirsToMap) + val uploadRoots = handler.mapUploadRoots(request, dirsToMap, binOnTarget.workingDir?.takeIf { it.pathString.isNotBlank() }) request.uploadVolumes.addAll(uploadRoots) + // Setup download roots if download is requested + val downloadConfig = launchRequest.downloadConfig + if (downloadConfig != null) { + val localDirsToDownload = binOnTarget.workingDir?.takeIf { it.pathString.isNotBlank() }?.let { setOf(it) } ?: emptySet() + val downloadRoots = handler.mapDownloadRoots(request, request.uploadVolumes, localDirsToDownload) + request.downloadVolumes.addAll(downloadRoots) + } + val targetEnv = try { request.prepareEnvironment(TargetProgressIndicator.EMPTY) } @@ -69,6 +85,7 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau targetEnv.uploadVolumes.forEach { _, volume -> volume.upload(".", TargetProgressIndicator.EMPTY) } + val args = launchRequest.args.getArgs { localFile -> targetEnv.getTargetPaths(localFile.pathString).first() } @@ -77,6 +94,12 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau binOnTarget.configureTargetCmdLine(commandLineBuilder) // exe path is always fixed (pre-presolved) promise. It can't be obtained directly because of Targets API limitation exePath = commandLineBuilder.exePath.localValue.blockingGet(1000) ?: error("Exe path not set: $binOnTarget is broken") + // Map working directory through upload volumes if it's a local path + binOnTarget.workingDir?.takeIf { it.pathString.isNotBlank() }?.let { workingDir -> + // Try to resolve through upload volumes (in case workingDir is a local path that needs mapping) + val workingDirOnTarget = targetEnv.getTargetPaths(workingDir.pathString).firstOrNull() ?: workingDir.pathString + commandLineBuilder.setWorkingDirectory(workingDirOnTarget) + } launchRequest.usePty?.let { val ptyOptions = LocalPtyOptions .defaults() @@ -92,7 +115,7 @@ internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, lau commandLineBuilder.addEnvironmentVariable(k, v) } }.build() - return@withContext Result.success(ProcessLauncher(exeForError = Exe.OnTarget(exePath), args = args, processCommands = TargetProcessCommands(launchRequest.scopeToBind, exePath, targetEnv, cmdLine))) + return@withContext Result.success(ProcessLauncher(exeForError = Exe.OnTarget(exePath), args = args, processCommands = TargetProcessCommands(launchRequest.scopeToBind, exePath, targetEnv, cmdLine, downloadConfig))) } private class TargetProcessCommands( @@ -100,6 +123,7 @@ private class TargetProcessCommands( private val exePath: FullPathOnTarget, private val targetEnv: TargetEnvironment, private val cmdLine: TargetedCommandLine, + private val downloadConfig: DownloadConfig?, ) : ProcessCommands { override val info: ProcessCommandsInfo get() = ProcessCommandsInfo( @@ -114,12 +138,34 @@ private class TargetProcessCommands( while (process?.isAlive == true) { delay(100.milliseconds) } + downloadAfterExecution() targetEnv.shutdown() }, killProcess = { process?.destroyForcibly() targetEnv.shutdown() }) + private suspend fun downloadAfterExecution() { + if (downloadConfig == null) return + + targetEnv.downloadVolumes.forEach { (_, volume) -> + val paths = downloadConfig.relativePaths.takeIf { it.isNotEmpty() } ?: listOf(".") + for (path in paths) { + coroutineToIndicator { + try { + volume.download(path, it) + } + catch (e: IOException) { + fileLogger().warn("Could not download $path: ${e.message}") + } + catch (e: RuntimeException) { // TODO: Unfortunately even though download is documented to throw IOException, in practice other random exceptions are possible for SSH at least + fileLogger().warn("Could not download $path: ${e.message}") + } + } + } + } + } + override suspend fun start(): Result { try { diff --git a/python/python-exec-service/src/com/intellij/python/community/execService/spi/TargetEnvironmentRequestHandler.kt b/python/python-exec-service/src/com/intellij/python/community/execService/spi/TargetEnvironmentRequestHandler.kt index 21c1b378d687..8939a96c41bd 100644 --- a/python/python-exec-service/src/com/intellij/python/community/execService/spi/TargetEnvironmentRequestHandler.kt +++ b/python/python-exec-service/src/com/intellij/python/community/execService/spi/TargetEnvironmentRequestHandler.kt @@ -7,11 +7,43 @@ import com.intellij.openapi.extensions.ExtensionPointName import org.jetbrains.annotations.ApiStatus import java.nio.file.Path +@ApiStatus.Internal interface TargetEnvironmentRequestHandler { fun isApplicable(request: TargetEnvironmentRequest): Boolean - fun mapUploadRoots(request: TargetEnvironmentRequest, localDirs: Set): List + fun mapUploadRoots( + request: TargetEnvironmentRequest, + localDirs: Set, + workingDirToDownload: Path? + ): List + + /** + * Maps download roots using existing upload roots. + * This allows downloading files modified on the target back to the local machine. + * + * @param request the target environment request + * @param uploadRoots set of upload roots + * @param localDirs local directories that were uploaded and may need to be downloaded + * @return list of download roots, empty by default + */ + fun mapDownloadRoots( + request: TargetEnvironmentRequest, + uploadRoots: Set, + localDirs: Set, + ): List = localDirs.mapNotNull { localDir -> + val matchingUpload = uploadRoots.find { localDir.startsWith(it.localRootPath) } + + if (matchingUpload != null) { + TargetEnvironment.DownloadRoot( + localRootPath = localDir, + targetRootPath = matchingUpload.targetRootPath, + ) + } + else { + null // No matching upload, skip download + } + } companion object { @JvmField diff --git a/python/python-sdk/src/com/jetbrains/python/pathValidation/PlatformAndRoot.kt b/python/python-sdk/src/com/jetbrains/python/pathValidation/PlatformAndRoot.kt index 3e0f0a035284..11a73ae6091d 100644 --- a/python/python-sdk/src/com/jetbrains/python/pathValidation/PlatformAndRoot.kt +++ b/python/python-sdk/src/com/jetbrains/python/pathValidation/PlatformAndRoot.kt @@ -3,6 +3,9 @@ package com.jetbrains.python.pathValidation import com.intellij.execution.Platform import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.isWindows +import com.intellij.platform.eel.provider.asNioPath import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.local import java.nio.file.Path @@ -17,6 +20,11 @@ class PlatformAndRoot private constructor(val root: Path?, val platform: Platfor */ val local: PlatformAndRoot = PlatformAndRoot(Path.of(""), Platform.current()) + fun EelApi?.getPlatformAndRoot(): PlatformAndRoot = when { + this == null -> local + else -> PlatformAndRoot(this.fs.user.home.root.asNioPath(), if (platform.isWindows) Platform.WINDOWS else Platform.UNIX) + } + /** * Creates [PlatformAndRoot] for [TargetEnvironmentConfiguration]. If null then returns either [local] or [platform] only depending * on [defaultIsLocal] diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkAdditionalData.java b/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkAdditionalData.java index e983a42d182e..48d1e73c65bb 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkAdditionalData.java +++ b/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkAdditionalData.java @@ -2,6 +2,11 @@ package com.jetbrains.python.sdk; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.projectRoots.SdkAdditionalData; @@ -23,6 +28,7 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.IOException; import java.nio.file.Path; import java.util.Collections; import java.util.LinkedHashSet; @@ -60,7 +66,7 @@ public class PythonSdkAdditionalData implements SdkAdditionalData { private String myAssociatedModulePath; private Path myRequiredTxtPath; - private final Gson myGson = new Gson(); + private final Gson myGson = new GsonBuilder().registerTypeAdapter(Path.class, new PathSerializer()).create(); public PythonSdkAdditionalData() { @@ -102,14 +108,6 @@ public class PythonSdkAdditionalData implements SdkAdditionalData { myUUID = from.myUUID; } - /** - * Temporary hack to deal with leagcy conda. Use constructor instead - */ - @ApiStatus.Internal - public final void changeFlavorAndData(@NotNull PyFlavorAndData flavorAndData) { - this.myFlavorAndData = flavorAndData; - } - /** * Persistent UUID of SDK. Could be used to point to "this particular" SDK. */ @@ -296,4 +294,26 @@ public class PythonSdkAdditionalData implements SdkAdditionalData { Collections.addAll(ret, paths.getFiles()); return ret; } + + private static class PathSerializer extends TypeAdapter<@Nullable Path> { + @Override + public void write(JsonWriter out, @Nullable Path value) throws IOException { + if (value == null) { + out.nullValue(); + } + else { + out.value(value.toString()); + } + } + + @Override + public @Nullable Path read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + return Path.of(in.nextString()); + } + } } diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/toolCli.kt b/python/python-sdk/src/com/jetbrains/python/sdk/toolCli.kt index 704576fe24ca..929ba1abc320 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/toolCli.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/toolCli.kt @@ -60,7 +60,6 @@ suspend fun detectTool( } paths.firstOrNull { it.isExecutable() } - } private fun MutableList.addUnixPaths(eel: EelApi, binaryName: String) { diff --git a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt index 7bde8fdbdef0..e14b8b6013f8 100644 --- a/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt +++ b/python/src/com/jetbrains/python/packaging/management/PythonPackageManager.kt @@ -24,6 +24,7 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.messages.Topic import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.extensions.toPsi import com.jetbrains.python.getOrNull import com.jetbrains.python.onFailure import com.jetbrains.python.packaging.PyPackageManager @@ -38,6 +39,7 @@ import com.jetbrains.python.sdk.PythonSdkType import com.jetbrains.python.sdk.isReadOnly import com.jetbrains.python.sdk.readOnlyErrorMessage import com.jetbrains.python.sdk.refreshPaths +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred import kotlinx.coroutines.async @@ -76,14 +78,18 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos @Volatile protected var outdatedPackages: Map = emptyMap() - private fun createCachedDependencies(dependencyFile: VirtualFile): CachedValue>?>> = - CachedValuesManager.getManager(project).createCachedValue { - val scope = PyPackageCoroutine.getScope(project) - val deferred = scope.async(NON_INTERACTIVE_ROOT_TRACE_CONTEXT, start = CoroutineStart.LAZY) { - extractDependencies() - } - CachedValueProvider.Result.create(deferred, dependencyFile) + private suspend fun createCachedDependencies(dependencyFile: VirtualFile): Deferred>?> { + val psiFile = readAction { dependencyFile.toPsi(project) } ?: return CompletableDeferred(value = null) + return CachedValuesManager.getManager(project).getCachedValue(psiFile, CACHE_KEY, { extractDependenciesAsync(dependencyFile) }, false) + } + + private fun extractDependenciesAsync(dependencyFile: VirtualFile): CachedValueProvider.Result>?>> { + val scope = PyPackageCoroutine.getScope(project) + val deferred = scope.async(NON_INTERACTIVE_ROOT_TRACE_CONTEXT, start = CoroutineStart.LAZY) { + extractDependencies() } + return CachedValueProvider.Result.create(deferred, dependencyFile) + } abstract val repositoryManager: PythonRepositoryManager @@ -97,7 +103,10 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos } @ApiStatus.Internal - suspend fun installPackage(installRequest: PythonPackageInstallRequest, options: List = emptyList()): PyResult> { + suspend fun installPackage( + installRequest: PythonPackageInstallRequest, + options: List = emptyList(), + ): PyResult> { if (sdk.isReadOnly) { return PyResult.localizedError(sdk.readOnlyErrorMessage) } @@ -108,7 +117,10 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos } @ApiStatus.Internal - suspend fun installPackageDetached(installRequest: PythonPackageInstallRequest, options: List = emptyList()): PyResult> { + suspend fun installPackageDetached( + installRequest: PythonPackageInstallRequest, + options: List = emptyList(), + ): PyResult> { waitForInit() installPackageDetachedCommand(installRequest, options).getOr { return it } @@ -226,7 +238,10 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos @ApiStatus.Internal @CheckReturnValue - protected open suspend fun installPackageDetachedCommand(installRequest: PythonPackageInstallRequest, options: List): PyResult = + protected open suspend fun installPackageDetachedCommand( + installRequest: PythonPackageInstallRequest, + options: List, + ): PyResult = installPackageCommand(installRequest, options) @ApiStatus.Internal @@ -262,7 +277,7 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos @ApiStatus.Internal suspend fun extractDependenciesCached(): PyResult>? { val dependencyFile = getDependencyFile() ?: return null - return createCachedDependencies(dependencyFile).value.await() + return createCachedDependencies(dependencyFile).await() } /** @@ -270,6 +285,7 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos * Returns null if no dependency file is associated with this package manager. */ @ApiStatus.Internal + @RequiresBackgroundThread open fun getDependencyFile(): VirtualFile? = null @@ -319,6 +335,8 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos private fun shouldBeInitInstantly(): Boolean = ApplicationManager.getApplication().isUnitTestMode companion object { + private val CACHE_KEY = Key.create>?>>>("PythonPackageManagerDependenciesCache") + @RequiresBackgroundThread fun forSdk(project: Project, sdk: Sdk): PythonPackageManager { val pythonPackageManagerService = project.service() @@ -332,7 +350,8 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos } @Topic.AppLevel - val PACKAGE_MANAGEMENT_TOPIC: Topic = Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN) + val PACKAGE_MANAGEMENT_TOPIC: Topic = + Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN) val RUNNING_PACKAGING_TASKS: Key = Key.create("PyPackageRequirementsInspection.RunningPackagingTasks") @ApiStatus.Internal diff --git a/python/src/com/jetbrains/python/run/PythonCommandLineState.java b/python/src/com/jetbrains/python/run/PythonCommandLineState.java index 79c71f9e4adb..6333fbc90ce8 100644 --- a/python/src/com/jetbrains/python/run/PythonCommandLineState.java +++ b/python/src/com/jetbrains/python/run/PythonCommandLineState.java @@ -390,7 +390,7 @@ public abstract class PythonCommandLineState extends CommandLineState { if (sdk != null && getEnableRunTool()) { PyRunToolProvider runToolProvider = PyRunToolProvider.forSdk(sdk); if (runToolProvider != null && useRunTool(myConfig, sdk)) { - runToolParameters = PythonCommandLineStateExKt.getRunToolParametersForJvm(runToolProvider); + runToolParameters = PythonCommandLineStateExKt.getRunToolParametersForJvm(runToolProvider, sdk); PyRunToolUsageCollector.logRun(myConfig.getProject(), PyRunToolIds.idOf(runToolProvider)); } } diff --git a/python/src/com/jetbrains/python/run/PythonCommandLineStateEx.kt b/python/src/com/jetbrains/python/run/PythonCommandLineStateEx.kt index 31aeccdf9977..3b5a97c6df83 100644 --- a/python/src/com/jetbrains/python/run/PythonCommandLineStateEx.kt +++ b/python/src/com/jetbrains/python/run/PythonCommandLineStateEx.kt @@ -2,6 +2,7 @@ package com.jetbrains.python.run import com.intellij.openapi.progress.runBlockingMaybeCancellable +import com.intellij.openapi.projectRoots.Sdk import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.run.features.PyRunToolParameters import com.jetbrains.python.run.features.PyRunToolProvider @@ -12,4 +13,4 @@ import org.jetbrains.annotations.ApiStatus */ @ApiStatus.Internal @RequiresBackgroundThread -fun PyRunToolProvider.getRunToolParametersForJvm(): PyRunToolParameters = runBlockingMaybeCancellable { getRunToolParameters() } \ No newline at end of file +fun PyRunToolProvider.getRunToolParametersForJvm(sdk: Sdk): PyRunToolParameters = runBlockingMaybeCancellable { getRunToolParameters(sdk) } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/PythonScripts.kt b/python/src/com/jetbrains/python/run/PythonScripts.kt index 3ee681764e70..6a0d85a1cbe4 100644 --- a/python/src/com/jetbrains/python/run/PythonScripts.kt +++ b/python/src/com/jetbrains/python/run/PythonScripts.kt @@ -16,13 +16,14 @@ import com.intellij.execution.target.TargetEnvironmentRequest import com.intellij.execution.target.TargetPlatform import com.intellij.execution.target.TargetedCommandLine import com.intellij.execution.target.TargetedCommandLineBuilder +import com.intellij.execution.target.getTargetPaths import com.intellij.execution.target.local.LocalTargetPtyOptions import com.intellij.execution.target.value.TargetEnvironmentFunction import com.intellij.execution.target.value.TargetValue import com.intellij.execution.target.value.constant import com.intellij.execution.target.value.getRelativeTargetPath import com.intellij.execution.target.value.joinToStringFunction -import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.fileLogger import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project @@ -50,7 +51,7 @@ import java.nio.file.Path import kotlin.io.path.pathString import kotlin.text.Charsets.UTF_8 -private val LOG = Logger.getInstance("#com.jetbrains.python.run.PythonScripts") +private val LOG = fileLogger() @JvmOverloads @ApiStatus.Internal @@ -68,7 +69,7 @@ fun PythonExecution.buildTargetedCommandLine( when (this) { is PythonToolExecution -> { - toolPath?.let { + toolPath.let { commandLineBuilder.exePath = TargetValue.fixed(it.pathString) commandLineBuilder.addParameters(listOf(*toolParams.toTypedArray())) } @@ -81,6 +82,10 @@ fun PythonExecution.buildTargetedCommandLine( if (runTool != null) { applyRunToolAsync(commandLineBuilder, runTool) + // TODO PY-87712 maybe need proper handling of envs (duplicates?) + runTool.envs.forEach { (k, v) -> + commandLineBuilder.addEnvironmentVariable(k, v) + } } when (this) { @@ -88,14 +93,8 @@ fun PythonExecution.buildTargetedCommandLine( ?: throw IllegalArgumentException("Python script path must be set") is PythonModuleExecution -> moduleName?.let { commandLineBuilder.addParameters(listOf("-m", it)) } ?: throw IllegalArgumentException("Python module name must be set") - is PythonToolScriptExecution -> pythonScriptPath?.let { commandLineBuilder.addParameter(it.apply(targetEnvironment).pathString) } - ?: throw IllegalArgumentException("Python script path must be set") - is PythonToolModuleExecution -> moduleName?.let { moduleName -> - moduleFlag?.let { moduleFlag -> - commandLineBuilder.addParameters(listOf(moduleFlag, moduleName)) - } ?: throw IllegalArgumentException("Module flag must be set") - } ?: throw IllegalArgumentException("Python module name must be set") - + is PythonToolScriptExecution -> commandLineBuilder.addParameter(pythonScriptPath.apply(targetEnvironment).pathString) + is PythonToolModuleExecution -> commandLineBuilder.addParameters(listOf(moduleFlag, moduleName)) } for (parameter in parameters) { @@ -138,7 +137,7 @@ private fun applyRunToolAsync( .onSuccess { originalExe: String? -> commandLineBuilder.exePath = TargetValue.fixed(runTool.exe) commandLineBuilder.addFixedParametersAt(0, runTool.args) - if (originalExe != null) { + if (!runTool.dropOldExe && originalExe != null) { commandLineBuilder.addParameterAt(runTool.args.size, originalExe) } } @@ -268,14 +267,8 @@ fun PythonExecution.addPythonScriptAsParameter(targetScript: PythonExecution) { is PythonModuleExecution -> targetScript.moduleName?.let { moduleName -> addParameters("-m", moduleName) } ?: throw IllegalArgumentException("Python module name must be set") - is PythonToolScriptExecution -> targetScript.pythonScriptPath?.let { pythonScriptPath -> addParameter(pythonScriptPath.andThen { it.pathString }) } - ?: throw IllegalArgumentException("Python script path must be set") - - is PythonToolModuleExecution -> targetScript.moduleName?.let { moduleName -> - targetScript.moduleFlag?.let { moduleFlag -> - addParameters(moduleFlag, moduleName) - } ?: throw java.lang.IllegalArgumentException("Module flag must be set") - } ?: throw IllegalArgumentException("Python module name must be set") + is PythonToolScriptExecution -> addParameter(targetScript.pythonScriptPath.andThen { it.pathString }) + is PythonToolModuleExecution -> addParameters(targetScript.moduleFlag, targetScript.moduleName) } } @@ -383,4 +376,4 @@ fun PythonExecution.disableBuiltinBreakpoint(sdk: Sdk?) { if (sdk != null && PythonSdkFlavor.getFlavor(sdk)?.getLanguageLevel(sdk)?.isAtLeast(LanguageLevel.PYTHON37) == true) { addEnvironmentVariable("PYTHONBREAKPOINT", "0") } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/run/features/PyRunToolProvider.kt b/python/src/com/jetbrains/python/run/features/PyRunToolProvider.kt index e931eeec07d5..cc43da7685c3 100644 --- a/python/src/com/jetbrains/python/run/features/PyRunToolProvider.kt +++ b/python/src/com/jetbrains/python/run/features/PyRunToolProvider.kt @@ -43,11 +43,15 @@ data class PyRunToolData( * * @property exe The path to the Python executable or script. * @property args A list of arguments to be passed to the executable. + * @property envs A map of environment variables to be used during execution. + * @property dropOldExe Since we replace the original exe with a new one, we can drop the old executable altogether with this flag. */ @ApiStatus.Internal data class PyRunToolParameters( val exe: String, val args: List, + val envs: Map, + val dropOldExe: Boolean, ) /** @@ -69,7 +73,7 @@ interface PyRunToolProvider { * Represents the parameters required to configure and run a Python tool. * This includes the path to the executable and a list of associated arguments. */ - suspend fun getRunToolParameters(): PyRunToolParameters + suspend fun getRunToolParameters(sdk: Sdk): PyRunToolParameters /** * Represents the initial state of the tool, determining whether it is enabled or not by default. diff --git a/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt b/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt index d6c86c22f2bb..a8e5ad276759 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkCommandRunner.kt @@ -5,6 +5,7 @@ import com.intellij.python.community.execService.Args import com.intellij.python.community.execService.BinOnEel import com.intellij.python.community.execService.BinaryToExec import com.intellij.python.community.execService.ConcurrentProcessWeight +import com.intellij.python.community.execService.DownloadConfig import com.intellij.python.community.execService.ExecOptions import com.intellij.python.community.execService.ExecService import com.intellij.python.community.execService.ProcessOutputTransformer @@ -35,9 +36,10 @@ suspend fun runExecutableWithProgress( vararg args: String, transformer: ProcessOutputTransformer, execService: ExecService = ExecService(), - processWeight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT + processWeight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT, + downloadConfig: DownloadConfig? = null, ): PyResult { - val execOptions = ExecOptions(timeout = timeout, env = env, weight = processWeight) + val execOptions = ExecOptions(timeout = timeout, env = env, weight = processWeight, downloadAfterExecution = downloadConfig) val errorHandlerTransformer: ProcessOutputTransformer = { output -> when { diff --git a/python/src/com/jetbrains/python/sdk/PySdkExt.kt b/python/src/com/jetbrains/python/sdk/PySdkExt.kt index bce997bc9bb9..c8e394457d97 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkExt.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkExt.kt @@ -262,6 +262,49 @@ suspend fun createSdk( ?: PyResult.localizedError(PyBundle.message("python.sdk.failed.to.create.interpreter.title")) } +@Internal +suspend fun

createSdk( + pythonBinaryPath: P, + suggestedSdkName: String, + sdkAdditionalData: PythonSdkAdditionalData? = null, +): PyResult { + val sdkType = PythonSdkType.getInstance() + val existingSdks = PythonSdkUtil.getAllSdks() + existingSdks.find { + it.sdkAdditionalData?.javaClass == sdkAdditionalData?.javaClass && + it.homePath == pythonBinaryPath.toString() + }?.let { return PyResult.success(it) } + + val sdk = when (pythonBinaryPath) { + is PathHolder.Eel -> { + val pythonBinaryVirtualFile = withContext(Dispatchers.IO) { + VirtualFileManager.getInstance().refreshAndFindFileByNioPath(pythonBinaryPath.path) + } ?: return PyResult.localizedError(PyBundle.message("python.sdk.python.executable.not.found", pythonBinaryPath)) + + SdkConfigurationUtil.setupSdk( + existingSdks.toTypedArray(), + pythonBinaryVirtualFile, + sdkType, + false, + sdkAdditionalData, + suggestedSdkName + ) + } + is PathHolder.Target -> { + SdkConfigurationUtil.createSdk( + existingSdks, + pythonBinaryPath.pathString, + sdkType, + sdkAdditionalData, + suggestedSdkName + ).also { sdk -> sdkType.setupSdkPaths(sdk) } + } + } + + return sdk?.let { PyResult.success(it) } + ?: PyResult.localizedError(PyBundle.message("python.sdk.failed.to.create.interpreter.title")) +} + internal fun showSdkExecutionException(sdk: Sdk?, e: ExecutionException, @NlsContexts.DialogTitle title: String) { runInEdt { val description = PyPackageManagementService.toErrorDescription(listOf(e), sdk) ?: return@runInEdt diff --git a/python/src/com/jetbrains/python/sdk/PyTargetsRemoteSourcesRefresher.kt b/python/src/com/jetbrains/python/sdk/PyTargetsRemoteSourcesRefresher.kt index d124bdd31801..d8ebd40656a7 100644 --- a/python/src/com/jetbrains/python/sdk/PyTargetsRemoteSourcesRefresher.kt +++ b/python/src/com/jetbrains/python/sdk/PyTargetsRemoteSourcesRefresher.kt @@ -38,15 +38,14 @@ import com.jetbrains.python.target.PyTargetAwareAdditionalData.Companion.pathsAd import com.jetbrains.python.target.PyTargetAwareAdditionalData.Companion.pathsRemovedByUser import org.jetbrains.annotations.ApiStatus import java.nio.file.Files -import java.nio.file.attribute.FileTime import java.nio.file.attribute.PosixFilePermissions -import java.time.Instant import kotlin.io.path.deleteExisting import kotlin.io.path.div import kotlin.io.path.setPosixFilePermissions private const val STATE_FILE = ".state.json" +private const val SUCCESS_FILE = ".success" @ApiStatus.Internal @@ -81,16 +80,11 @@ class PyTargetsRemoteSourcesRefresher(val sdk: Sdk, private val project: Project val execution = prepareHelperScriptExecution(helperPackage = PythonHelper.REMOTE_SYNC, helpersAwareTargetRequest = pyRequest) val stateFilePath = localRemoteSourcesRoot / STATE_FILE - val stateFilePrevTimestamp: FileTime if (Files.exists(stateFilePath)) { - stateFilePrevTimestamp = Files.getLastModifiedTime(stateFilePath) Files.copy(stateFilePath, localUploadDir / STATE_FILE) execution.addParameter("--state-file") execution.addParameter(uploadVolume.getTargetUploadPath().getRelativeTargetPath(STATE_FILE)) } - else { - stateFilePrevTimestamp = FileTime.from(Instant.MIN) - } execution.addParameter(downloadVolume.getTargetDownloadPath()) val targetWithVfs = sdk.targetEnvConfiguration?.let { PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(it) } @@ -128,8 +122,12 @@ class PyTargetsRemoteSourcesRefresher(val sdk: Sdk, private val project: Project if (!Files.exists(stateFilePath)) { throw IllegalStateException("$stateFilePath is missing") } - if (Files.getLastModifiedTime(stateFilePath) <= stateFilePrevTimestamp) { - throw IllegalStateException("$stateFilePath has not been updated") + val successFilePath = localRemoteSourcesRoot / SUCCESS_FILE + if (!Files.exists(successFilePath)) { + throw IllegalStateException("$successFilePath is missing") + } + else { + Files.delete(successFilePath) } val stateFile: StateFile diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index f6cd888bd93f..6a7bd3c981ec 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -12,6 +12,7 @@ import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.util.io.FileUtil import com.intellij.platform.eel.EelApi +import com.intellij.platform.eel.provider.asNioPath import com.intellij.platform.eel.provider.localEel import com.intellij.python.community.execService.Args import com.intellij.python.community.execService.BinOnEel @@ -19,6 +20,7 @@ import com.intellij.python.community.execService.BinOnTarget import com.intellij.python.community.execService.BinaryToExec import com.intellij.python.community.execService.ExecService import com.intellij.python.community.execService.execGetStdout +import com.intellij.python.community.execService.execute import com.intellij.python.community.execService.python.validatePythonAndGetInfo import com.intellij.python.community.services.internal.impl.VanillaPythonWithPythonInfoImpl import com.intellij.python.community.services.shared.VanillaPythonWithPythonInfo @@ -77,7 +79,8 @@ sealed interface FileSystem

{ val isLocal: Boolean fun parsePath(raw: String): PyResult

- fun validateExecutable(path: P): PyResult + suspend fun validateExecutable(path: P): PyResult + suspend fun fileExists(path: P): Boolean /** * [pathToPython] has to be system (not venv) if set [requireSystemPython] @@ -93,6 +96,7 @@ sealed interface FileSystem

{ fun getBinaryToExec(path: P): BinaryToExec suspend fun which(cmd: String): P? + suspend fun getHomePath(): P? data class Eel( val eelApi: EelApi, @@ -113,7 +117,7 @@ sealed interface FileSystem

{ PyResult.localizedError(e.localizedMessage) } - override fun validateExecutable(path: PathHolder.Eel): PyResult { + override suspend fun validateExecutable(path: PathHolder.Eel): PyResult { return when { !path.path.exists() -> PyResult.localizedError(message("sdk.create.not.executable.does.not.exist.error")) path.path.isDirectory() -> PyResult.localizedError(message("sdk.create.executable.directory.error")) @@ -121,6 +125,8 @@ sealed interface FileSystem

{ } } + override suspend fun fileExists(path: PathHolder.Eel): Boolean = path.path.exists() + override suspend fun validateVenv(homePath: PathHolder.Eel): PyResult = withContext(Dispatchers.IO) { val validationResult = when { !homePath.path.isAbsolute -> PyResult.localizedError(message("python.sdk.new.error.no.absolute")) @@ -235,6 +241,8 @@ sealed interface FileSystem

{ } override suspend fun which(cmd: String): PathHolder.Eel? = detectTool(cmd, eelApi)?.let { PathHolder.Eel(it) } + + override suspend fun getHomePath(): PathHolder.Eel = PathHolder.Eel(eelApi.userInfo.home.asNioPath()) } data class Target( @@ -248,15 +256,25 @@ sealed interface FileSystem

{ override val isLocal: Boolean = false private val systemPythonCache = ArrayList>() + private lateinit var shellImpl: PyResult override fun parsePath(raw: String): PyResult { return PyResult.success(PathHolder.Target(raw)) } /** - * Currently, we don't validate executable on target because there is no API to check path existence and its type on target. + * Currently, we don't validate the executable on target because there is no API to check its type on target. */ - override fun validateExecutable(path: PathHolder.Target): PyResult = PyResult.success(Unit) + override suspend fun validateExecutable(path: PathHolder.Target): PyResult = + if (fileExists(path)) { + PyResult.success(Unit) + } + else PyResult.localizedError(message("sdk.create.not.executable.does.not.exist.error")) + + override suspend fun fileExists(path: PathHolder.Target): Boolean { + val bin = getBinaryToExec(PathHolder.Target("/usr/bin/test")) + return ExecService().execute(bin, Args("-f", path.pathString), processOutputTransformer = { output -> PyResult.success(output.exitCode == 0) }).successOrNull ?: false + } override suspend fun validateVenv(homePath: PathHolder.Target): PyResult = withContext(Dispatchers.IO) { val pythonBinaryPath = resolvePythonBinary(homePath) @@ -342,10 +360,35 @@ sealed interface FileSystem

{ } override suspend fun which(cmd: String): PathHolder.Target? { - val which = getBinaryToExec(PathHolder.Target("which")) - val condaPathString = ExecService().execGetStdout(which, Args(cmd)).getOr { return null } - val condaPathOnFS = parsePath(condaPathString).getOr { return null } - return condaPathOnFS + val binaryPathString = executeCommand("which $cmd") ?: return null + val binaryPathOnFS = parsePath(binaryPathString).getOr { return null } + return binaryPathOnFS + } + + override suspend fun getHomePath(): PathHolder.Target? = executeCommand($$"echo ${HOME}")?.let { PathHolder.Target(it) } + + private suspend fun executeCommand(cmd: String): String? { + val shell = getShell().getOr { return null } + val bin = getBinaryToExec(PathHolder.Target(shell)) + return ExecService().execGetStdout(bin, Args("-l", "-c", cmd)).successOrNull + } + + private suspend fun getShell(): PyResult { + if (!this::shellImpl.isInitialized) { + shellImpl = getShellImpl() + } + return shellImpl + } + + private suspend fun getShellImpl(): PyResult { + val bin1 = getBinaryToExec(PathHolder.Target("getent")) + val execService = ExecService() + val res = execService.execGetStdout(bin1, Args("passwd")).getOr { return it } + val bin2 = getBinaryToExec(PathHolder.Target("whoami")) + val user = execService.execGetStdout(bin2).getOr { return it } + val shell = res.lines().firstOrNull { it.substringBefore(':').contains(user) }?.substringAfterLast(':') + @Suppress("HardCodedStringLiteral") + return shell?.let { PyResult.success(it) } ?: PyResult.localizedError("Could not get shell") } } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index 18563c165bec..0e47a81db750 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -179,7 +179,7 @@ enum class PythonSupportedEnvironmentManagers( CONDA(CONDA_TOOL_ID, "sdk.create.custom.conda", PythonCommunityImplCondaIcons.Anaconda, { true }), POETRY(POETRY_TOOL_ID, "sdk.create.custom.poetry", PythonCommunityImplPoetryCommonIcons.Poetry), PIPENV(PIPENV_TOOL_ID, "sdk.create.custom.pipenv", PIPENV_ICON), - UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonCommunityImplUVCommonIcons.UV), + UV(UV_TOOL_ID, "sdk.create.custom.uv", PythonCommunityImplUVCommonIcons.UV, { true }), HATCH(HATCH_TOOL_ID, "sdk.create.custom.hatch", PythonHatchIcons.Logo, { it is FileSystem.Eel }), PYTHON(VENV_TOOL_ID, "sdk.create.custom.python", PythonParserIcons.PythonFile, { true }) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/models.kt b/python/src/com/jetbrains/python/sdk/add/v2/models.kt index 3ba828b284ac..ed66f51a9273 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/models.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -66,7 +66,7 @@ abstract class PythonAddInterpreterModel

( open val state: AddInterpreterState

= AddInterpreterState(propertyGraph) val condaViewModel: CondaViewModel

= CondaViewModel(fileSystem, propertyGraph, projectPathFlows) - val uvViewModel: UvViewModel

= UvViewModel(fileSystem, propertyGraph) + val uvViewModel: UvViewModel

= UvViewModel(fileSystem, propertyGraph, projectPathFlows) val pipenvViewModel: PipenvViewModel

= PipenvViewModel(fileSystem, propertyGraph) val poetryViewModel: PoetryViewModel

= PoetryViewModel(fileSystem, propertyGraph) val hatchViewModel: HatchViewModel

= HatchViewModel(fileSystem, propertyGraph, projectPathFlows) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt index 77005f4be4af..7e97f18a1044 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/EnvironmentCreatorUv.kt @@ -29,16 +29,15 @@ import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.UV import com.jetbrains.python.sdk.add.v2.ToolValidator import com.jetbrains.python.sdk.add.v2.ValidatedPath -import com.jetbrains.python.sdk.add.v2.VenvExistenceValidationState +import com.jetbrains.python.sdk.add.v2.ValidatedPathField import com.jetbrains.python.sdk.add.v2.savePathForEelOnly import com.jetbrains.python.sdk.add.v2.validatablePathField import com.jetbrains.python.sdk.uv.impl.createUvCli import com.jetbrains.python.sdk.uv.impl.createUvLowLevel -import com.jetbrains.python.sdk.uv.impl.setUvExecutable +import com.jetbrains.python.sdk.uv.impl.setUvExecutableLocal import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv import com.jetbrains.python.statistics.InterpreterType import com.jetbrains.python.util.ShowingMessageErrorSync -import com.jetbrains.python.venvReader.VirtualEnvReader import io.github.z4kn4fein.semver.Version import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -48,7 +47,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import java.nio.file.Path -import java.nio.file.Paths import kotlin.io.path.exists import kotlin.io.path.readText @@ -73,9 +71,10 @@ internal class EnvironmentCreatorUv

( private val executableFlow = MutableStateFlow(model.uvViewModel.uvExecutable.get()) private val pythonVersion: ObservableMutableProperty = propertyGraph.property(null) private lateinit var versionComboBox: ComboBox + private lateinit var venvPathField: ValidatedPathField> override val toolExecutable: ObservableProperty?> = model.uvViewModel.uvExecutable override val toolExecutablePersister: suspend (P) -> Unit = { pathHolder -> - savePathForEelOnly(pathHolder) { path -> setUvExecutable(path) } + savePathForEelOnly(pathHolder) { path -> setUvExecutableLocal(path) } } private val loading = AtomicBooleanProperty(false) @@ -111,31 +110,27 @@ internal class EnvironmentCreatorUv

( installAction = createInstallFix(errorSink) ) - row("") { - venvExistenceValidationAlert(validationRequestor) { - onVenvSelectExisting() - } - } + // TODO PY-87712 Add banner if the venv does exist at the specified location + venvPathField = validatablePathField( + fileSystem = model.fileSystem, + pathValidator = model.uvViewModel.uvVenvValidator, + validationRequestor = validationRequestor, + labelText = message("sdk.create.custom.location"), + missingExecutableText = null, + isFileSelectionMode = false, + ) } } override fun onShown(scope: CoroutineScope) { executablePath.initialize(scope) + venvPathField.initialize(scope) model .projectPathFlows .projectPathWithDefault .combine(executableFlow) { projectPath, executable -> projectPath to executable } .onEach { (projectPath, executable) -> - val venvPath = projectPath.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) - - withContext(Dispatchers.IO) { - venvExistenceValidationState.set( - if (venvPath.exists()) - VenvExistenceValidationState.Error(Paths.get(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)) - else - VenvExistenceValidationState.Invisible - ) - } + model.uvViewModel.uvVenvValidator.autodetectFolder() versionComboBox.removeAllItems() versionComboBox.addItem(null) @@ -158,8 +153,9 @@ internal class EnvironmentCreatorUv

( null } - val cli = createUvCli((executable.pathHolder as PathHolder.Eel).path).getOr { return@withContext emptyList() } - val uvLowLevel = createUvLowLevel(Path.of(""), cli) + val cli = createUvCli(executable.pathHolder, model.fileSystem).getOr { return@withContext emptyList() } + val cwd = Path.of("") + val uvLowLevel = createUvLowLevel(cwd, cli, model.fileSystem, null) uvLowLevel.listSupportedPythonVersions(versionRequest) .getOr { return@withContext emptyList() } } @@ -188,9 +184,7 @@ internal class EnvironmentCreatorUv

( } override suspend fun setupEnvSdk(moduleBasePath: Path): PyResult { - return setupNewUvSdkAndEnv( - workingDir = moduleBasePath, - version = pythonVersion.get(), - ) + val uv = toolExecutable.get()?.pathHolder!! + return setupNewUvSdkAndEnv(uv, moduleBasePath, model.uvViewModel.uvVenvPath.get()?.pathHolder, model.fileSystem, pythonVersion.get()) } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt index 82873b9a86b0..98f1fcc27496 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt @@ -18,17 +18,14 @@ import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel import com.jetbrains.python.sdk.add.v2.ToolValidator import com.jetbrains.python.sdk.add.v2.ValidatedPath import com.jetbrains.python.sdk.add.v2.savePathForEelOnly -import com.jetbrains.python.sdk.associatedModulePath import com.jetbrains.python.sdk.baseDir import com.jetbrains.python.sdk.impl.resolvePythonBinary import com.jetbrains.python.sdk.isAssociatedWithModule import com.jetbrains.python.sdk.legacy.PythonSdkUtil -import com.jetbrains.python.sdk.uv.impl.setUvExecutable +import com.jetbrains.python.sdk.uv.impl.setUvExecutableLocal import com.jetbrains.python.sdk.uv.isUv import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk import com.jetbrains.python.statistics.InterpreterType -import com.jetbrains.python.venvReader.VirtualEnvReader -import com.jetbrains.python.venvReader.tryResolvePath import java.nio.file.Files import java.nio.file.Path import java.util.stream.Collectors @@ -42,38 +39,42 @@ internal class UvExistingEnvironmentSelector

(model: PythonMutabl override val toolState: ToolValidator

= model.uvViewModel.toolValidator override val toolExecutable: ObservableProperty?> = model.uvViewModel.uvExecutable override val toolExecutablePersister: suspend (P) -> Unit = { pathHolder -> - savePathForEelOnly(pathHolder) { path -> setUvExecutable(path) } + savePathForEelOnly(pathHolder) { path -> setUvExecutableLocal(path) } } override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult { val sdkHomePath = selectedEnv.get()?.homePath - val selectedInterpreterPath = sdkHomePath as? PathHolder.Eel - ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", sdkHomePath)) + val selectedInterpreterPath = sdkHomePath ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", sdkHomePath)) val allSdk = PythonSdkUtil.getAllSdks() - val existingSdk = allSdk.find { it.homePath == selectedInterpreterPath.path.pathString } + val existingSdk = allSdk.find { it.homePath == selectedInterpreterPath.toString() } + val venvPath = when (sdkHomePath) { + is PathHolder.Eel -> model.fileSystem.parsePath(sdkHomePath.path.parent.parent.pathString) + // TODO PY-87712 Move this logic to a better place + is PathHolder.Target -> model.fileSystem.parsePath(sdkHomePath.pathString.substringBeforeLast("/bin/")) + }.getOr { return it } val associatedModule = extractModule(moduleOrProject) - val basePathString = associatedModule?.baseDir?.path ?: moduleOrProject.project.basePath - val projectDir = tryResolvePath(basePathString) - ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", basePathString)) // uv sdk in current module if (existingSdk != null && existingSdk.isUv && existingSdk.isAssociatedWithModule(associatedModule)) { return Result.success(existingSdk) } - val workingDirectory = - VirtualEnvReader().getVenvRootPath(selectedInterpreterPath.path) - ?: tryResolvePath(existingSdk?.associatedModulePath) - ?: projectDir + val basePathString = associatedModule?.baseDir?.path + ?: moduleOrProject.project.basePath + ?: return PyResult.localizedError(PyBundle.message("python.sdk.provided.path.is.invalid", null)) + val workingDir = Path.of(basePathString) return setupExistingEnvAndSdk( - envExecutable = selectedInterpreterPath.path, - envWorkingDir = workingDirectory, - usePip = existingSdk?.isUv == true, - moduleDir = projectDir, + pythonBinary = selectedInterpreterPath, + uvPath = toolExecutable.get()!!.pathHolder!!, + workingDir = workingDir, + venvPath = venvPath, + fileSystem = model.fileSystem, + usePip = existingSdk?.isUv == true ) } + // TODO PY-87712 Support detection for remotes override suspend fun detectEnvironments(modulePath: Path): List> { val rootFolders = Files.walk(modulePath, 1) .filter(Files::isDirectory) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt index e47877967601..a34e33feca4f 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvInterpreterSection.kt @@ -14,7 +14,7 @@ import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel import com.jetbrains.python.sdk.add.v2.PythonNewEnvironmentDialogNavigator.Companion.FAV_MODE import com.jetbrains.python.sdk.add.v2.booleanProperty -import com.jetbrains.python.sdk.uv.impl.hasUvExecutable +import com.jetbrains.python.sdk.uv.impl.hasUvExecutableLocal import kotlinx.coroutines.CoroutineScope /** @@ -47,7 +47,7 @@ internal class UvInterpreterSection( private suspend fun selectUvIfExists() { if (PropertiesComponent.getInstance().getValue(FAV_MODE) != null) return - if (hasUvExecutable() && selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) { + if (hasUvExecutableLocal() && selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) { selectedMode.set(PythonInterpreterSelectionMode.PROJECT_UV) } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvViewModel.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvViewModel.kt index ab4746fa7a60..4cd5b000bf1c 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvViewModel.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvViewModel.kt @@ -3,20 +3,24 @@ package com.jetbrains.python.sdk.add.v2.uv import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.PropertyGraph -import com.intellij.platform.eel.provider.localEel +import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows import com.jetbrains.python.sdk.add.v2.FileSystem +import com.jetbrains.python.sdk.add.v2.FolderValidator import com.jetbrains.python.sdk.add.v2.PathHolder import com.jetbrains.python.sdk.add.v2.PythonToolViewModel import com.jetbrains.python.sdk.add.v2.ToolValidator import com.jetbrains.python.sdk.add.v2.ValidatedPath import com.jetbrains.python.sdk.uv.impl.getUvExecutable import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first class UvViewModel

( fileSystem: FileSystem

, propertyGraph: PropertyGraph, + projectPathFlows: ProjectPathFlows, ) : PythonToolViewModel { val uvExecutable: ObservableMutableProperty?> = propertyGraph.property(null) + val uvVenvPath: ObservableMutableProperty?> = propertyGraph.property(null) val toolValidator: ToolValidator

= ToolValidator( fileSystem = fileSystem, @@ -24,17 +28,23 @@ class UvViewModel

( backProperty = uvExecutable, propertyGraph = propertyGraph, defaultPathSupplier = { - when (fileSystem) { - is FileSystem.Eel -> { - if (fileSystem.eelApi == localEel) getUvExecutable()?.let { PathHolder.Eel(it) } as P? - else null // getUvExecutable() works only with localEel currently - } - else -> null - } + getUvExecutable(fileSystem, null) } ) + val uvVenvValidator: FolderValidator

= FolderValidator( + fileSystem = fileSystem, + backProperty = uvVenvPath, + propertyGraph = propertyGraph, + defaultPathSupplier = { + val projectPath = projectPathFlows.projectPathWithDefault.first() + fileSystem.suggestVenv(projectPath) + }, + pathValidator = fileSystem::validateVenv + ) + override fun initialize(scope: CoroutineScope) { toolValidator.initialize(scope) + uvVenvValidator.initialize(scope) } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/uv/Uv.kt b/python/src/com/jetbrains/python/sdk/uv/Uv.kt index d7bc398dc75e..0422b9e54a90 100644 --- a/python/src/com/jetbrains/python/sdk/uv/Uv.kt +++ b/python/src/com/jetbrains/python/sdk/uv/Uv.kt @@ -7,18 +7,19 @@ import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.management.PyWorkspaceMember import com.jetbrains.python.packaging.management.PythonPackageInstallRequest +import com.jetbrains.python.sdk.add.v2.PathHolder import io.github.z4kn4fein.semver.Version import org.jetbrains.annotations.ApiStatus import java.nio.file.Path @ApiStatus.Internal -interface UvCli { - suspend fun runUv(workingDir: Path, vararg args: String): PyResult +interface UvCli

{ + suspend fun runUv(workingDir: Path, venvPath: P?, canChangeTomlOrLock: Boolean, vararg args: String): PyResult } @ApiStatus.Internal -interface UvLowLevel { - suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult +interface UvLowLevel

{ + suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult

suspend fun listUvPythons(): PyResult> suspend fun listSupportedPythonVersions(versionRequest: String? = null): PyResult> diff --git a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt index f8496bdefb4b..43d8833af97a 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvExt.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvExt.kt @@ -1,18 +1,39 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk.uv +import com.intellij.execution.target.FullPathOnTarget +import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.execution.target.TargetProgressIndicator +import com.intellij.execution.target.value.constant +import com.intellij.execution.target.value.getRelativeTargetPath +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.util.NlsSafe +import com.intellij.platform.eel.provider.getEelDescriptor +import com.intellij.platform.eel.provider.localEel +import com.intellij.platform.eel.provider.toEelApi import com.intellij.python.pyproject.PY_PROJECT_TOML import com.intellij.util.PathUtil import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory +import com.jetbrains.python.sdk.PythonSdkAdditionalData +import com.jetbrains.python.sdk.add.v2.FileSystem import com.jetbrains.python.sdk.add.v2.PathHolder import com.jetbrains.python.sdk.createSdk +import com.jetbrains.python.sdk.flavors.PyFlavorAndData import com.jetbrains.python.sdk.getOrCreateAdditionalData import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.sdk.uv.impl.createUvCli import com.jetbrains.python.sdk.uv.impl.createUvLowLevel +import com.jetbrains.python.sdk.uv.impl.detectUvExecutable +import com.jetbrains.python.target.PyTargetAwareAdditionalData +import com.jetbrains.python.target.PythonLanguageRuntimeConfiguration import io.github.z4kn4fein.semver.Version +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import java.nio.file.Path import kotlin.io.path.exists import kotlin.io.path.pathString @@ -23,7 +44,16 @@ internal val Sdk.isUv: Boolean if (!PythonSdkUtil.isPythonSdk(this)) { return false } - return getOrCreateAdditionalData() is UvSdkAdditionalData + return uvFlavorData != null + } + +internal val Sdk.uvFlavorData: UvSdkFlavorData? + get() { + return when (val data = getOrCreateAdditionalData()) { + is UvSdkAdditionalData -> data.flavorData + is PyTargetAwareAdditionalData -> data.flavorAndData.data as? UvSdkFlavorData + else -> null + } } internal val Sdk.uvUsePackageManagement: Boolean @@ -32,42 +62,275 @@ internal val Sdk.uvUsePackageManagement: Boolean return false } - val data = getOrCreateAdditionalData() as? UvSdkAdditionalData ?: return false - return data.usePip + return uvFlavorData?.usePip == true } -internal fun suggestedSdkName(basePath: Path): @NlsSafe String { - return "uv (${PathUtil.getFileName(basePath.pathString)})" +/** + * Execution context for UV SDK operations. + * Consolidates all PathHolder type-specific data needed to execute UV commands. + * + * Use [getUvExecutionContext] to create an instance from an SDK. + */ +internal sealed interface UvExecutionContext

{ + val workingDir: Path + val venvPath: P? + val fileSystem: FileSystem

+ val uvPath: P? + + data class Eel( + override val workingDir: Path, + override val venvPath: PathHolder.Eel?, + override val fileSystem: FileSystem.Eel, + override val uvPath: PathHolder.Eel?, + ) : UvExecutionContext + + data class Target( + override val workingDir: Path, + override val venvPath: PathHolder.Target?, + override val fileSystem: FileSystem.Target, + override val uvPath: PathHolder.Target?, + ) : UvExecutionContext + + suspend fun createUvCli(): PyResult> = createUvCli(uvPath, fileSystem).mapSuccess { uvCli -> + createUvLowLevel(workingDir, uvCli, fileSystem, venvPath) + } } -suspend fun setupNewUvSdkAndEnv( - workingDir: Path, - version: Version?, -): PyResult { - val toml = workingDir.resolve(PY_PROJECT_TOML) - val init = !toml.exists() +/** + * Operations helper for UV SDK creation code. + * Consolidates all PathHolder type-specific operations needed to create UV SDKs. + * + * Use [createUvPathOperations] factory to create an instance. + */ +sealed interface UvPathOperations

{ + val workingDir: Path + val venvPath: P? + val fileSystem: FileSystem

- val uv = createUvLowLevel(workingDir, createUvCli().getOr { return it }) - val envExecutable = uv.initializeEnvironment(init, version) - .getOr { - return it + /** + * Creates SDK additional data appropriate for this context type. + */ + fun createSdkAdditionalData( + workingDir: Path, + venvPath: P?, + usePip: Boolean, + uvPath: P, + ): PythonSdkAdditionalData + + /** + * Maps a path using target VFS mapper if needed. + */ + fun mapProbablyWslPath(path: P): P + + /** + * Checks if pyproject.toml exists at the working directory. + */ + suspend fun pyProjectTomlExists(): Boolean + + class Eel( + override val workingDir: Path, + override val venvPath: PathHolder.Eel?, + override val fileSystem: FileSystem.Eel, + ) : UvPathOperations { + override fun createSdkAdditionalData( + workingDir: Path, + venvPath: PathHolder.Eel?, + usePip: Boolean, + uvPath: PathHolder.Eel, + ): PythonSdkAdditionalData { + return UvSdkAdditionalData(workingDir, usePip, venvPath?.path, uvPath.path) } - return setupExistingEnvAndSdk(envExecutable, workingDir, false, workingDir) + override fun mapProbablyWslPath(path: PathHolder.Eel): PathHolder.Eel = path + + override suspend fun pyProjectTomlExists(): Boolean { + val toml = workingDir.resolve(PY_PROJECT_TOML) + return toml.exists() + } + } + + class Target( + override val workingDir: Path, + override val venvPath: PathHolder.Target?, + override val fileSystem: FileSystem.Target, + ) : UvPathOperations { + override fun createSdkAdditionalData( + workingDir: Path, + venvPath: PathHolder.Target?, + usePip: Boolean, + uvPath: PathHolder.Target, + ): PythonSdkAdditionalData { + val targetConfig = fileSystem.targetEnvironmentConfiguration + val flavorAndData = PyFlavorAndData(UvSdkFlavorData(workingDir, usePip, venvPath?.pathString, uvPath.pathString), UvSdkFlavor) + return PyTargetAwareAdditionalData(flavorAndData, targetConfig) + } + + override fun mapProbablyWslPath(path: PathHolder.Target): PathHolder.Target { + val targetConfig = fileSystem.targetEnvironmentConfiguration + val mapper = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(targetConfig) + val targetPath = mapper?.getTargetPath(Path.of(path.pathString)) ?: path.pathString + return PathHolder.Target(targetPath) + } + + override suspend fun pyProjectTomlExists(): Boolean { + val targetConfig = fileSystem.targetEnvironmentConfiguration + val mapper = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(targetConfig) + val mappedPathString = mapper?.getTargetPath(workingDir) ?: workingDir.pathString + val targetPath = constant(mappedPathString) + val tomlPath = targetPath.getRelativeTargetPath(PY_PROJECT_TOML) + val toml = tomlPath.apply(targetConfig.createEnvironmentRequest(project = null).prepareEnvironment(TargetProgressIndicator.EMPTY)) + return fileSystem.fileExists(PathHolder.Target(toml)) + } + } +} + +/** + * Creates a [UvPathOperations] instance for the given parameters. + * This factory consolidates the type dispatch in one place. + */ +// TODO PY-87712 Think about contracts +@Suppress("UNCHECKED_CAST") +internal fun

createUvPathOperations( + workingDir: Path, + venvPath: P?, + fileSystem: FileSystem

, +): UvPathOperations

{ + return when (fileSystem) { + is FileSystem.Eel -> UvPathOperations.Eel( + workingDir = workingDir, + venvPath = venvPath as? PathHolder.Eel, + fileSystem = fileSystem, + ) as UvPathOperations

+ is FileSystem.Target -> UvPathOperations.Target( + workingDir = workingDir, + venvPath = venvPath as? PathHolder.Target, + fileSystem = fileSystem, + ) as UvPathOperations

+ } +} + +private suspend fun createEelUvExecutionContext( + workingDir: Path, + venvPathString: String?, + uvPathString: String? +): UvExecutionContext.Eel { + val eelApi = workingDir.getEelDescriptor().toEelApi() + val fileSystem = FileSystem.Eel(eelApi) + val uvPath = detectUvExecutable(fileSystem, uvPathString) + return UvExecutionContext.Eel( + workingDir = workingDir, + venvPath = venvPathString?.let { PathHolder.Eel(Path.of(it)) }, + fileSystem = fileSystem, + uvPath = uvPath + ) +} + +private suspend fun createTargetUvExecutionContext( + workingDir: Path, + venvPathString: FullPathOnTarget?, + uvPathString: FullPathOnTarget?, + targetConfig: TargetEnvironmentConfiguration +): UvExecutionContext.Target { + val fileSystem = FileSystem.Target(targetConfig, PythonLanguageRuntimeConfiguration()) + val uvPath = detectUvExecutable(fileSystem, uvPathString) + return UvExecutionContext.Target( + workingDir = workingDir, + venvPath = venvPathString?.let { PathHolder.Target(it) }, + fileSystem = fileSystem, + uvPath = uvPath + ) +} + +internal fun Sdk.getUvExecutionContextAsync(scope: CoroutineScope, project: Project? = null): Deferred>? { + val data = sdkAdditionalData + val uvWorkingDirectory = uvFlavorData?.uvWorkingDirectory + val venvPathString = uvFlavorData?.venvPath + val uvPathString = uvFlavorData?.uvPath + + return when (data) { + is UvSdkAdditionalData -> { + val defaultWorkingDir = project?.basePath?.let { Path.of(it) } + val cwd = uvWorkingDirectory ?: defaultWorkingDir ?: return null + scope.async(start = CoroutineStart.LAZY) { + createEelUvExecutionContext(cwd, venvPathString, uvPathString) + } + } + is PyTargetAwareAdditionalData -> { + val targetConfig = data.targetEnvironmentConfiguration ?: return null + val cwd = uvWorkingDirectory ?: return null + scope.async(start = CoroutineStart.LAZY) { + createTargetUvExecutionContext(cwd, venvPathString, uvPathString, targetConfig) + } + } + else -> null + } +} + +@Service +private class MyService(val coroutineScope: CoroutineScope) + +/** + * Creates a [UvExecutionContext] from an SDK. + * This factory consolidates all PathHolder casts in one place for SDK consumption code. + * + * @param project Optional project for fallback working directory + * @return UvExecutionContext if the SDK is a valid UV SDK, null otherwise + */ +internal suspend fun Sdk.getUvExecutionContext(project: Project? = null): UvExecutionContext<*>? = + getUvExecutionContextAsync(service().coroutineScope, project)?.await() + +suspend fun setupNewUvSdkAndEnv(uvExecutable: Path, workingDir: Path, version: Version?): PyResult = + setupNewUvSdkAndEnv( + uvExecutable = PathHolder.Eel(uvExecutable), + workingDir = workingDir, + venvPath = null, + fileSystem = FileSystem.Eel(localEel), + version = version + ) + +suspend fun

setupNewUvSdkAndEnv( + uvExecutable: P, + workingDir: Path, + venvPath: P?, + fileSystem: FileSystem

, + version: Version?, +): PyResult { + val ops = createUvPathOperations(workingDir, venvPath, fileSystem) + + val shouldInitProject = !ops.pyProjectTomlExists() + val mappedUvExecutable = ops.mapProbablyWslPath(uvExecutable) + + val uv = createUvLowLevel(workingDir, createUvCli(mappedUvExecutable, fileSystem).getOr { return it }, fileSystem, venvPath) + val pythonBinary = uv.initializeEnvironment(shouldInitProject, version).getOr { return it } + return setupExistingEnvAndSdk(pythonBinary, mappedUvExecutable, workingDir, venvPath, fileSystem, false) } suspend fun setupExistingEnvAndSdk( - envExecutable: Path, + pythonBinary: Path, + uvPath: Path, envWorkingDir: Path, usePip: Boolean, - moduleDir: Path, -): PyResult { - val sdk = createSdk( - pythonBinaryPath = PathHolder.Eel(envExecutable), - associatedModulePath = moduleDir.toString(), - suggestedSdkName = suggestedSdkName(envWorkingDir), - sdkAdditionalData = UvSdkAdditionalData(envWorkingDir, usePip) +): PyResult = + setupExistingEnvAndSdk( + pythonBinary = PathHolder.Eel(pythonBinary), + uvPath = PathHolder.Eel(uvPath), + workingDir = envWorkingDir, + venvPath = null, + fileSystem = FileSystem.Eel(localEel), + usePip = usePip ) +suspend fun

setupExistingEnvAndSdk( + pythonBinary: P, + uvPath: P, + workingDir: Path, + venvPath: P?, + fileSystem: FileSystem

, + usePip: Boolean, +): PyResult { + val ops = createUvPathOperations(workingDir, venvPath, fileSystem) + val sdkAdditionalData = ops.createSdkAdditionalData(workingDir, venvPath, usePip, uvPath) + val sdkName = "uv (${PathUtil.getFileName(workingDir.pathString)})" + val sdk = createSdk(pythonBinary, sdkName, sdkAdditionalData) return sdk -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt index 2c5dbb962901..35b7d5bfff34 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvPackageManager.kt @@ -1,10 +1,11 @@ // 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.uv +import com.intellij.openapi.progress.runBlockingMaybeCancellable import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk -import com.intellij.util.cancelOnDispose import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.cancelOnDispose import com.jetbrains.python.PyBundle.message import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.PyResult @@ -23,21 +24,21 @@ import com.jetbrains.python.packaging.management.resolvePyProjectToml import com.jetbrains.python.packaging.pip.PipRepositoryManager import com.jetbrains.python.packaging.pyRequirement import com.jetbrains.python.packaging.utils.PyPackageCoroutine -import com.jetbrains.python.sdk.uv.impl.createUvCli -import com.jetbrains.python.sdk.uv.impl.createUvLowLevel +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import java.nio.file.Path -internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred: Deferred>) : PythonPackageManager(project, sdk) { +internal class UvPackageManager(project: Project, sdk: Sdk, uvExecutionContextDeferred: Deferred>) : PythonPackageManager(project, sdk) { override val repositoryManager: PythonRepositoryManager = PipRepositoryManager.getInstance(project) - private val uvLowLevel = uvLowLevelDeferred.also { it.cancelOnDispose(this) } + private lateinit var uvLowLevel: PyResult> + private val uvExecutionContextDeferred = uvExecutionContextDeferred.also { it.cancelOnDispose(this) } - private suspend fun withUv(action: suspend (UvLowLevel) -> PyResult): PyResult { - return when (val uvResult = uvLowLevel.await()) { + private suspend fun withUv(action: suspend (UvLowLevel<*>) -> PyResult): PyResult { + if (!this::uvLowLevel.isInitialized) { + uvLowLevel = uvExecutionContextDeferred.await().createUvCli() + } + + return when (val uvResult = uvLowLevel) { is Result.Success -> action(uvResult.result) is Result.Failure -> uvResult } @@ -96,7 +97,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred: /** * Categorizes packages into standalone packages and pyproject.toml declared packages. */ - private suspend fun categorizePackages(uv: UvLowLevel, packages: Array): PyResult, List>> { + private suspend fun categorizePackages(uv: UvLowLevel<*>, packages: Array): PyResult, List>> { val dependencyNames = uv.listTopLevelPackages().getOr { return it }.map { it.name } @@ -111,7 +112,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred: /** * Uninstalls standalone packages using UV package manager. */ - private suspend fun uninstallStandalonePackages(uv: UvLowLevel, packages: List): PyResult { + private suspend fun uninstallStandalonePackages(uv: UvLowLevel<*>, packages: List): PyResult { return if (packages.isNotEmpty()) { uv.uninstallPackages(packages.map { it.name }.toTypedArray()) } @@ -123,7 +124,7 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred: /** * Removes declared dependencies using UV package manager. */ - private suspend fun uninstallDeclaredPackages(uv: UvLowLevel, packages: List, workspaceMember: PyWorkspaceMember?): PyResult { + private suspend fun uninstallDeclaredPackages(uv: UvLowLevel<*>, packages: List, workspaceMember: PyWorkspaceMember?): PyResult { return if (packages.isNotEmpty()) { uv.removeDependencies(packages.map { it.name }.toTypedArray(), workspaceMember) } @@ -159,8 +160,9 @@ internal class UvPackageManager(project: Project, sdk: Sdk, uvLowLevelDeferred: } } + // TODO PY-87712 Double check for remotes override fun getDependencyFile(): VirtualFile? { - val uvWorkingDirectory = (sdk.sdkAdditionalData as? UvSdkAdditionalData)?.uvWorkingDirectory ?: return null + val uvWorkingDirectory = runBlockingMaybeCancellable { uvExecutionContextDeferred.await().workingDir } return resolvePyProjectToml(uvWorkingDirectory) } @@ -183,10 +185,7 @@ class UvPackageManagerProvider : PythonPackageManagerProvider { return null } - val uvWorkingDirectory = (sdk.sdkAdditionalData as UvSdkAdditionalData).uvWorkingDirectory ?: Path.of(project.basePath!!) - val uvLowLevel = PyPackageCoroutine.getScope(project).async(start = CoroutineStart.LAZY) { - createUvCli().mapSuccess { createUvLowLevel(uvWorkingDirectory, it) } - } - return UvPackageManager(project, sdk, uvLowLevel) + val uvExecutionContext = sdk.getUvExecutionContextAsync(PyPackageCoroutine.getScope(project), project) ?: return null + return UvPackageManager(project, sdk, uvExecutionContext) } } diff --git a/python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt b/python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt index 945beceed7b9..a4d0edaca303 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvSdkFlavorAndData.kt @@ -1,30 +1,38 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.sdk.uv +import com.intellij.execution.target.FullPathOnTarget +import com.intellij.execution.target.TargetedCommandLineBuilder +import com.intellij.openapi.projectRoots.Sdk import com.intellij.python.community.impl.uv.common.icons.PythonCommunityImplUVCommonIcons +import com.intellij.remote.RemoteSdkPropertiesPaths +import com.jetbrains.python.sdk.PySdkUtil import com.jetbrains.python.sdk.PythonSdkAdditionalData import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor +import com.jetbrains.python.sdk.flavors.PyFlavorAndData import com.jetbrains.python.sdk.flavors.PyFlavorData import com.jetbrains.python.sdk.flavors.PythonFlavorProvider import com.jetbrains.python.sdk.flavors.PythonSdkFlavor +import com.jetbrains.python.sdk.legacy.PythonSdkUtil import org.jdom.Element import java.nio.file.Path import javax.swing.Icon import kotlin.io.path.pathString - class UvSdkAdditionalData : PythonSdkAdditionalData { - val uvWorkingDirectory: Path? - val usePip: Boolean + internal val flavorData: UvSdkFlavorData - constructor(uvWorkingDirectory: Path? = null, usePip: Boolean = false) : super(UvSdkFlavor) { - this.uvWorkingDirectory = uvWorkingDirectory - this.usePip = usePip + constructor(uvWorkingDirectory: Path?, usePip: Boolean, venvPath: Path?, uvPath: Path?) : this(UvSdkFlavorData(uvWorkingDirectory, usePip, venvPath?.pathString, uvPath?.pathString)) + + private constructor(flavorData: UvSdkFlavorData) : super(PyFlavorAndData(flavorData, UvSdkFlavor)) { + this.flavorData = flavorData } - constructor(data: PythonSdkAdditionalData, uvWorkingDirectory: Path? = null, usePip: Boolean = false) : super(data) { - this.uvWorkingDirectory = uvWorkingDirectory - this.usePip = usePip + constructor(data: PythonSdkAdditionalData) : super(data) { + when (data) { + is UvSdkAdditionalData -> this.flavorData = data.flavorData + else -> this.flavorData = UvSdkFlavorData(null, false, null, null) + } } override fun save(element: Element) { @@ -32,12 +40,20 @@ class UvSdkAdditionalData : PythonSdkAdditionalData { element.setAttribute(IS_UV, "true") // keep backward compatibility with old data - if (uvWorkingDirectory?.pathString?.isNotBlank() == true) { - element.setAttribute(UV_WORKING_DIR, uvWorkingDirectory.pathString) + if (flavorData.uvWorkingDirectory?.pathString?.isNotBlank() == true) { + element.setAttribute(UV_WORKING_DIR, flavorData.uvWorkingDirectory.pathString) } - if (usePip) { - element.setAttribute(USE_PIP, usePip.toString()) + if (flavorData.usePip) { + element.setAttribute(USE_PIP, flavorData.usePip.toString()) + } + + if (flavorData.venvPath?.isNotBlank() == true) { + element.setAttribute(UV_VENV_PATH, flavorData.venvPath) + } + + if (flavorData.uvPath?.isNotBlank() == true) { + element.setAttribute(UV_TOOL_PATH, flavorData.uvPath) } } @@ -45,6 +61,8 @@ class UvSdkAdditionalData : PythonSdkAdditionalData { private const val IS_UV = "IS_UV" private const val UV_WORKING_DIR = "UV_WORKING_DIR" private const val USE_PIP = "USE_PIP" + private const val UV_VENV_PATH = "UV_VENV_PATH" + private const val UV_TOOL_PATH = "UV_TOOL_PATH" @JvmStatic fun load(element: Element): UvSdkAdditionalData? { @@ -52,7 +70,9 @@ class UvSdkAdditionalData : PythonSdkAdditionalData { element.getAttributeValue(IS_UV) == "true" -> { val uvWorkingDirectory = if (element.getAttributeValue(UV_WORKING_DIR).isNullOrEmpty()) null else Path.of(element.getAttributeValue(UV_WORKING_DIR)) val usePip = element.getAttributeValue(USE_PIP)?.toBoolean() ?: false - UvSdkAdditionalData(uvWorkingDirectory, usePip).apply { + val venvPath = if (element.getAttributeValue(UV_VENV_PATH).isNullOrEmpty()) null else Path.of(element.getAttributeValue(UV_VENV_PATH)) + val uvPath = if (element.getAttributeValue(UV_TOOL_PATH).isNullOrEmpty()) null else Path.of(element.getAttributeValue(UV_TOOL_PATH)) + UvSdkAdditionalData(uvWorkingDirectory, usePip, venvPath, uvPath).apply { load(element) } } @@ -67,9 +87,30 @@ class UvSdkAdditionalData : PythonSdkAdditionalData { } } -object UvSdkFlavor : CPythonSdkFlavor() { +// TODO PY-87712 Move to a separate storage +data class UvSdkFlavorData( + val uvWorkingDirectory: Path?, + val usePip: Boolean, + val venvPath: FullPathOnTarget?, + val uvPath: FullPathOnTarget?, +) : PyFlavorData { + + override fun prepareTargetCommandLine(sdk: Sdk, targetCommandLineBuilder: TargetedCommandLineBuilder) { + val interpreterPath = sdk.sdkAdditionalData?.let { (it as? RemoteSdkPropertiesPaths)?.interpreterPath } ?: sdk.homePath + if (interpreterPath.isNullOrBlank()) { + throw IllegalArgumentException("Sdk ${sdk} doesn't have interpreter path set") + } + targetCommandLineBuilder.setExePath(interpreterPath) + targetCommandLineBuilder.addEnvironmentVariable("UV_PROJECT_ENVIRONMENT", venvPath) + if (!PythonSdkUtil.isRemote(sdk)) { + PySdkUtil.activateVirtualEnv(sdk) + } + } +} + +object UvSdkFlavor : CPythonSdkFlavor() { override fun getIcon(): Icon = PythonCommunityImplUVCommonIcons.UV - override fun getFlavorDataClass(): Class = PyFlavorData.Empty::class.java + override fun getFlavorDataClass(): Class = UvSdkFlavorData::class.java override fun isValidSdkPath(pathStr: String): Boolean { return false diff --git a/python/src/com/jetbrains/python/sdk/uv/UvSystemPythonProvider.kt b/python/src/com/jetbrains/python/sdk/uv/UvSystemPythonProvider.kt index 93fa72f0bd95..b2415c72bdc1 100644 --- a/python/src/com/jetbrains/python/sdk/uv/UvSystemPythonProvider.kt +++ b/python/src/com/jetbrains/python/sdk/uv/UvSystemPythonProvider.kt @@ -9,18 +9,18 @@ import com.jetbrains.python.PyToolUIInfo import com.jetbrains.python.PythonBinary import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.PyResult -import com.jetbrains.python.sdk.uv.impl.createUvLowLevel -import com.jetbrains.python.sdk.uv.impl.hasUvExecutable +import com.jetbrains.python.sdk.uv.impl.createUvLowLevelLocal +import com.jetbrains.python.sdk.uv.impl.hasUvExecutableLocal import java.nio.file.Path internal class UvSystemPythonProvider : SystemPythonProvider { override suspend fun findSystemPythons(eelApi: EelApi): PyResult> { - if (eelApi != localEel || !hasUvExecutable()) { + if (eelApi != localEel || !hasUvExecutableLocal()) { // TODO: support for remote execution return Result.success(emptySet()) } - val uv = createUvLowLevel(Path.of(".")).getOr { return it } + val uv = createUvLowLevelLocal(Path.of(".")).getOr { return it } return uv.listUvPythons() } diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt index ed87c23942ce..d3a1d947840d 100644 --- a/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt +++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvCli.kt @@ -1,16 +1,23 @@ // 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.uv.impl +import com.intellij.execution.target.FullPathOnTarget import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.ui.ValidationInfo import com.intellij.platform.eel.EelApi import com.intellij.platform.eel.provider.localEel +import com.intellij.python.community.execService.BinOnEel +import com.intellij.python.community.execService.BinOnTarget +import com.intellij.python.community.execService.DownloadConfig +import com.intellij.python.community.execService.ZeroCodeStdoutTransformer import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.pathValidation.PlatformAndRoot +import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot import com.jetbrains.python.pathValidation.ValidationRequest import com.jetbrains.python.pathValidation.validateExecutableFile -import com.jetbrains.python.sdk.detectTool +import com.jetbrains.python.sdk.add.v2.FileSystem +import com.jetbrains.python.sdk.add.v2.PathHolder import com.jetbrains.python.sdk.runExecutableWithProgress import com.jetbrains.python.sdk.uv.UvCli import com.jetbrains.python.venvReader.VirtualEnvReader @@ -32,48 +39,110 @@ private var PropertiesComponent.uvPath: Path? setValue(UV_PATH_SETTING, value.toString()) } -private fun validateUvExecutable(uvPath: Path?): ValidationInfo? { +private fun

validateUvExecutable(uvPath: P?, platformAndRoot: PlatformAndRoot): ValidationInfo? { + val path = when (uvPath) { + is PathHolder.Eel -> uvPath.path.pathString + is PathHolder.Target -> uvPath.pathString + } return validateExecutableFile(ValidationRequest( - path = uvPath?.pathString, + path = path, fieldIsEmpty = PyBundle.message("python.sdk.uv.executable.not.found"), - // FIXME: support targets - platformAndRoot = PlatformAndRoot.local + platformAndRoot = platformAndRoot )) } -private suspend fun runUv(uv: Path, workingDir: Path, vararg args: String): PyResult { - return runExecutableWithProgress(uv, workingDir, - env = mapOf("VIRTUAL_ENV" to VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME), timeout = 10.minutes, args = args) +private suspend fun

runUv(uv: P, workingDir: Path, venvPath: P?, fileSystem: FileSystem

, canChangeTomlOrLock: Boolean, vararg args: String): PyResult { + val env = buildMap { + if (venvPath == null) { + put("VIRTUAL_ENV", VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) + } + else { + put("VIRTUAL_ENV", venvPath.toString()) + } + venvPath?.let { put("UV_PROJECT_ENVIRONMENT", it.toString()) } + } + val bin = when (uv) { + is PathHolder.Eel -> BinOnEel(uv.path, workingDir) + is PathHolder.Target -> BinOnTarget(uv.pathString, (fileSystem as FileSystem.Target).targetEnvironmentConfiguration, workingDir) + } + val downloadConfig = if (canChangeTomlOrLock) DownloadConfig(relativePaths = listOf("pyproject.toml", "uv.lock")) else null + return runExecutableWithProgress(bin, env = env, timeout = 10.minutes, args = args, transformer = ZeroCodeStdoutTransformer, downloadConfig = downloadConfig) } -private class UvCliImpl(val dispatcher: CoroutineDispatcher, val uv: Path) : UvCli { +private class UvCliImpl

(val dispatcher: CoroutineDispatcher, val uv: P, private val fileSystem: FileSystem

) : UvCli

{ - override suspend fun runUv(workingDir: Path, vararg args: String): PyResult { - return withContext(dispatcher) { - runUv(uv, workingDir, *args) + override suspend fun runUv(workingDir: Path, venvPath: P?, canChangeTomlOrLock: Boolean, vararg args: String): PyResult = withContext(dispatcher) { + runUv(uv, workingDir, venvPath, fileSystem, canChangeTomlOrLock, *args) + } +} + +suspend fun

detectUvExecutable(fileSystem: FileSystem

, pathFromSdk: FullPathOnTarget?): P? = detectTool("uv", fileSystem, pathFromSdk) + +private suspend fun

detectTool( + toolName: String, + fileSystem: FileSystem

, + pathFromSdk: FullPathOnTarget?, + additionalSearchPaths: List

= listOf(), +): P? = withContext(Dispatchers.IO) { + pathFromSdk?.let { fileSystem.parsePath(it) }?.successOrNull?.also { return@withContext it } + when (fileSystem) { + is FileSystem.Eel -> fileSystem.which(toolName) + is FileSystem.Target -> { + val binary = fileSystem.which(toolName) + if (binary != null) { + return@withContext binary + } + + val searchPaths: List = buildList { + fileSystem.getHomePath()?.also { + add("$it/.local/bin/uv") + } + + for (path in additionalSearchPaths) { + add(path.toString()) + } + } + + searchPaths.firstOrNull { fileSystem.fileExists(PathHolder.Target(it)) } + ?.let { fileSystem.parsePath(it).successOrNull } } } } -suspend fun detectUvExecutable(eel: EelApi): Path? = detectTool("uv", eel) - -suspend fun getUvExecutable(eel: EelApi = localEel): Path? { - return PropertiesComponent.getInstance().uvPath?.takeIf { it.exists() } ?: detectUvExecutable(eel) +suspend fun getUvExecutableLocal(eel: EelApi = localEel): Path? { + return PropertiesComponent.getInstance().uvPath?.takeIf { it.exists() } ?: detectUvExecutable(FileSystem.Eel(eel), null)?.path } -fun setUvExecutable(path: Path) { +// TODO PY-87712 check that path in SDK is from the same eel or something +suspend fun

getUvExecutable(fileSystem: FileSystem

, pathFromSdk: FullPathOnTarget?): P? { + val pathFromProperty = PropertiesComponent.getInstance().uvPath + return when (fileSystem) { + is FileSystem.Eel -> (pathFromProperty?.takeIf { fileSystem.eelApi == localEel && it.exists() }?.let { PathHolder.Eel(it) as P }) ?: detectUvExecutable(fileSystem, pathFromSdk) + is FileSystem.Target -> detectUvExecutable(fileSystem, pathFromSdk) + } +} + +fun setUvExecutableLocal(path: Path) { PropertiesComponent.getInstance().uvPath = path } -suspend fun hasUvExecutable(): Boolean { - return getUvExecutable() != null +suspend fun hasUvExecutableLocal(): Boolean { + return getUvExecutableLocal() != null } -suspend fun createUvCli(uv: Path? = null, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult { - val path = uv ?: getUvExecutable() - val error = validateUvExecutable(path) +suspend fun createUvCliLocal(uv: Path? = null, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult> { + return createUvCli(uv?.let { PathHolder.Eel(it) }, FileSystem.Eel(localEel), dispatcher) +} + +suspend fun

createUvCli(uv: P?, fileSystem: FileSystem

, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult> { + val path = uv ?: getUvExecutable(fileSystem, null) + val platformAndRoot = when (fileSystem) { + is FileSystem.Eel -> fileSystem.eelApi.getPlatformAndRoot() + is FileSystem.Target -> fileSystem.targetEnvironmentConfiguration.getPlatformAndRoot() + } + val error = validateUvExecutable(path, platformAndRoot) return if (error != null) { PyResult.localizedError(error.message) } - else PyResult.success(UvCliImpl(dispatcher, path!!)) + else PyResult.success(UvCliImpl(dispatcher, path!!, fileSystem)) } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt index 288d73d0fc82..0f5a4500e5df 100644 --- a/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt +++ b/python/src/com/jetbrains/python/sdk/uv/impl/UvLowLevel.kt @@ -5,6 +5,10 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.RuntimeJsonMappingException import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import com.intellij.execution.target.TargetProgressIndicator +import com.intellij.execution.target.value.constant +import com.intellij.execution.target.value.getRelativeTargetPath +import com.intellij.platform.eel.provider.localEel import com.jetbrains.python.PyBundle import com.jetbrains.python.errorProcessing.ExecError import com.jetbrains.python.errorProcessing.ExecErrorReason @@ -17,6 +21,8 @@ import com.jetbrains.python.packaging.common.PythonOutdatedPackage import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.management.PyWorkspaceMember import com.jetbrains.python.packaging.management.PythonPackageInstallRequest +import com.jetbrains.python.sdk.add.v2.FileSystem +import com.jetbrains.python.sdk.add.v2.PathHolder import com.jetbrains.python.sdk.uv.ScriptSyncCheckResult import com.jetbrains.python.sdk.uv.UvCli import com.jetbrains.python.sdk.uv.UvLowLevel @@ -26,17 +32,16 @@ import io.github.z4kn4fein.semver.Version import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.nio.file.Path -import kotlin.io.path.deleteIfExists import kotlin.io.path.exists -import kotlin.io.path.notExists +import kotlin.io.path.name import kotlin.io.path.pathString private const val NO_METADATA_MESSAGE = "does not contain a PEP 723 metadata tag" private const val OUTDATED_ENV_MESSAGE = "The environment is outdated" private val versionRegex = Regex("(\\d+\\.\\d+)\\.\\d+-.+\\s") -private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLevel { - override suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult { +private class UvLowLevelImpl

(private val cwd: Path, private val venvPath: P?, private val uvCli: UvCli

, private val fileSystem: FileSystem

) : UvLowLevel

{ + override suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult

{ val addPythonArg: (MutableList) -> Unit = { args -> version?.let { args.add("--python") @@ -47,31 +52,42 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev if (init) { val initArgs = mutableListOf("init") addPythonArg(initArgs) - initArgs.add("--no-readme") - initArgs.add("--no-pin-python") - initArgs.add("--vcs") - initArgs.add("none") + initArgs.add("--bare") + if (cwd.name.isNotBlank()) { + initArgs.add("--name") + initArgs.add(cwd.name) + } initArgs.add("--no-project") - - val notExistingFiles = listOf("hello.py", "main.py").filter { cwd.resolve(it).notExists() } - - uvCli.runUv(cwd, *initArgs.toTypedArray()) - .getOr { return it } - - notExistingFiles.forEach { cwd.resolve(it).deleteIfExists() } + uvCli.runUv(cwd, null, true, *initArgs.toTypedArray()).getOr { return it } } val venvArgs = mutableListOf("venv") + venvPath?.also { venvArgs += it.toString() } addPythonArg(venvArgs) - uvCli.runUv(cwd, *venvArgs.toTypedArray()) + uvCli.runUv(cwd, null, true, *venvArgs.toTypedArray()) .getOr { return it } if (!init) { - uvCli.runUv(cwd, "sync") + uvCli.runUv(cwd, venvPath, true, "sync") .getOr { return it } } - val path = VirtualEnvReader().findPythonInPythonRoot(cwd.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)) + // TODO PY-87712 Would be great to get rid of unsafe casts + val path: P? = when (fileSystem) { + is FileSystem.Eel -> { + VirtualEnvReader().findPythonInPythonRoot((venvPath as? PathHolder.Eel)?.path ?: cwd.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME)) + ?.let { fileSystem.resolvePythonBinary(PathHolder.Eel(it)) } as P? + } + is FileSystem.Target -> { + val pythonBinary = if (venvPath == null) { + val targetPath = constant(cwd.pathString) + val venvPath = targetPath.getRelativeTargetPath(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME) + venvPath.apply(fileSystem.targetEnvironmentConfiguration.createEnvironmentRequest(project = null).prepareEnvironment(TargetProgressIndicator.EMPTY)) + } else venvPath.toString() + fileSystem.resolvePythonBinary(PathHolder.Target(pythonBinary)) as P? + } + } + if (path == null) { return PyResult.localizedError(PyBundle.message("python.sdk.uv.failed.to.initialize.uv.environment")) } @@ -80,7 +96,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev } override suspend fun listUvPythons(): PyResult> { - var out = uvCli.runUv(cwd, "python", "dir") + var out = uvCli.runUv(cwd, venvPath, false, "python", "dir") .getOr { return it } val uvDir = tryResolvePath(out) @@ -89,7 +105,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev } // TODO: ask for json output format - out = uvCli.runUv(cwd, "python", "list", "--only-installed") + out = uvCli.runUv(cwd, venvPath, false, "python", "list", "--only-installed") .getOr { return it } val pythons = parseUvPythonList(uvDir, out) @@ -103,7 +119,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev args += versionRequest } - val out = uvCli.runUv(cwd, *args.toTypedArray()).getOr { return it } + val out = uvCli.runUv(cwd, venvPath, false, *args.toTypedArray()).getOr { return it } val matches = versionRegex.findAll(out) return PyResult.success( @@ -120,7 +136,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev } override suspend fun listPackages(): PyResult> { - val out = uvCli.runUv(cwd, "pip", "list", "--format", "json") + val out = uvCli.runUv(cwd, venvPath, false, "pip", "list", "--format", "json") .getOr { return it } data class PackageInfo(val name: String, val version: String) @@ -135,7 +151,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev } override suspend fun listOutdatedPackages(): PyResult> { - val out = uvCli.runUv(cwd, "pip", "list", "--outdated", "--format", "json") + val out = uvCli.runUv(cwd, venvPath, false, "pip", "list", "--outdated", "--format", "json") .getOr { return it } data class OutdatedPackageInfo(val name: String, val version: String, val latest_version: String) @@ -155,42 +171,42 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev } override suspend fun listTopLevelPackages(): PyResult> { - val out = uvCli.runUv(cwd, "tree", "--depth=1", "--locked") + val out = uvCli.runUv(cwd, venvPath, false, "tree", "--depth=1", "--locked") .getOr { return it } return PyExecResult.success(parsePackageList(out)) } override suspend fun listPackageRequirements(name: PythonPackage): PyResult> { - val out = uvCli.runUv(cwd, "pip", "show", name.name) + val out = uvCli.runUv(cwd, venvPath, false, "pip", "show", name.name) .getOr { return it } return PyExecResult.success(parsePackageRequirements(out)) } override suspend fun listPackageRequirementsTree(name: PythonPackage): PyResult { - val out = uvCli.runUv(cwd, "tree", "--package", name.name, "--locked") + val out = uvCli.runUv(cwd, venvPath, false, "tree", "--package", name.name, "--locked") .getOr { return it } return PyExecResult.success(out) } override suspend fun listProjectStructureTree(): PyResult { - val out = uvCli.runUv(cwd, "tree", "--locked") + val out = uvCli.runUv(cwd, venvPath, false, "tree", "--locked") .getOr { return it } return PyExecResult.success(out) } override suspend fun listAllPackagesTree(): PyResult { - val out = uvCli.runUv(cwd, "pip", "tree") + val out = uvCli.runUv(cwd, venvPath, false, "pip", "tree") .getOr { return it } return PyExecResult.success(out) } override suspend fun installPackage(name: PythonPackageInstallRequest, options: List): PyResult { - uvCli.runUv(cwd, "pip", "install", *name.formatPackageName(), *options.toTypedArray()) + uvCli.runUv(cwd, venvPath, true, "pip", "install", *name.formatPackageName(), *options.toTypedArray()) .getOr { return it } return PyExecResult.success(Unit) @@ -198,14 +214,14 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev override suspend fun uninstallPackages(pyPackages: Array): PyResult { // TODO: check if package is in dependencies and reject it - uvCli.runUv(cwd, "pip", "uninstall", *pyPackages) + uvCli.runUv(cwd, venvPath, true, "pip", "uninstall", *pyPackages) .getOr { return it } return PyExecResult.success(Unit) } override suspend fun addDependency(pyPackages: PythonPackageInstallRequest, options: List): PyResult { - uvCli.runUv(cwd, "add", *pyPackages.formatPackageName(), *options.toTypedArray()) + uvCli.runUv(cwd, venvPath, true, "add", *pyPackages.formatPackageName(), *options.toTypedArray()) .getOr { return it } return PyExecResult.success(Unit) @@ -219,7 +235,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev } args.addAll(pyPackages) - uvCli.runUv(cwd, *args.toTypedArray()) + uvCli.runUv(cwd, venvPath, true, *args.toTypedArray()) .getOr { return it } return PyExecResult.success(Unit) @@ -228,7 +244,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev override suspend fun isProjectSynced(inexact: Boolean): PyResult { val args = constructSyncArgs(inexact) - uvCli.runUv(cwd, *args.toTypedArray()) + uvCli.runUv(cwd, venvPath, false, *args.toTypedArray()) .onFailure { val stderr = tryExtractStderr(it) @@ -245,7 +261,7 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev override suspend fun isScriptSynced(inexact: Boolean, scriptPath: Path): PyResult { val args = constructSyncArgs(inexact) + listOf("--script", scriptPath.pathString) - uvCli.runUv(cwd, *args.toTypedArray()) + uvCli.runUv(cwd, venvPath, false, *args.toTypedArray()) .onFailure { val stderr = tryExtractStderr(it) @@ -302,11 +318,11 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev } override suspend fun sync(): PyResult { - return uvCli.runUv(cwd, "sync", "--all-packages", "--inexact") + return uvCli.runUv(cwd, venvPath, true, "sync", "--all-packages", "--inexact") } override suspend fun lock(): PyResult { - return uvCli.runUv(cwd, "lock") + return uvCli.runUv(cwd, venvPath, true, "lock") } suspend fun parsePackageList(input: String): List = withContext(Dispatchers.Default) { @@ -339,11 +355,14 @@ private class UvLowLevelImpl(val cwd: Path, private val uvCli: UvCli) : UvLowLev } } -fun createUvLowLevel(cwd: Path, uvCli: UvCli): UvLowLevel { - return UvLowLevelImpl(cwd, uvCli) -} +fun createUvLowLevelLocal(cwd: Path, uvCli: UvCli): UvLowLevel = + createUvLowLevel(cwd, uvCli, FileSystem.Eel(localEel), null) -suspend fun createUvLowLevel(cwd: Path): PyResult = createUvCli().mapSuccess { createUvLowLevel(cwd, it) } +fun

createUvLowLevel(cwd: Path, uvCli: UvCli

, fileSystem: FileSystem

, venvPath: P?): UvLowLevel

= + UvLowLevelImpl(cwd, venvPath, uvCli, fileSystem) + +suspend fun createUvLowLevelLocal(cwd: Path): PyResult> = + createUvCli(null, FileSystem.Eel(localEel)).mapSuccess { createUvLowLevelLocal(cwd, it) } private fun tryExtractStderr(err: PyError): String? = when (err) { @@ -354,4 +373,4 @@ private fun tryExtractStderr(err: PyError): String? = } } else -> null - } \ No newline at end of file + } diff --git a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfiguration.kt b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfiguration.kt index 7af1587b5b55..85b9ff7f7aec 100644 --- a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfiguration.kt +++ b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfiguration.kt @@ -18,7 +18,7 @@ import com.jetbrains.python.run.AbstractPythonRunConfiguration import com.jetbrains.python.sdk.associatedModulePath import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.sdk.pythonSdk -import com.jetbrains.python.sdk.uv.UvSdkAdditionalData +import com.jetbrains.python.sdk.uv.uvFlavorData import com.jetbrains.python.venvReader.tryResolvePath import org.jdom.Element import org.jetbrains.annotations.ApiStatus @@ -45,7 +45,7 @@ data class UvRunConfigurationOptions( get() = uvSdkKey?.let { PythonSdkUtil.findSdkByKey(it)} val workingDirectory: Path? - get() = (uvSdk?.sdkAdditionalData as? UvSdkAdditionalData)?.uvWorkingDirectory + get() = uvSdk?.uvFlavorData?.uvWorkingDirectory ?: tryResolvePath(uvSdk?.associatedModulePath) } diff --git a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationCli.kt b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationCli.kt index 8a702bc5b0a7..f1ecbb8ea97e 100644 --- a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationCli.kt +++ b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationCli.kt @@ -8,7 +8,7 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.run.PythonExecution import com.jetbrains.python.run.PythonToolModuleExecution import com.jetbrains.python.run.PythonToolScriptExecution -import com.jetbrains.python.sdk.uv.impl.getUvExecutable +import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal import org.jetbrains.annotations.ApiStatus import java.nio.file.Files import java.nio.file.Path @@ -17,7 +17,7 @@ import kotlin.io.path.pathString @ApiStatus.Internal @RequiresBackgroundThread(generateAssertion = false) suspend fun buildUvRunConfigurationCli(options: UvRunConfigurationOptions, isDebug: Boolean): PythonExecution { - val toolPath = requireNotNull(getUvExecutable()) { "Unable to find uv executable." } + val toolPath = requireNotNull(getUvExecutableLocal()) { "Unable to find uv executable." } val toolParams = mutableListOf("run") if (isDebug && !options.uvArgs.contains("--cache-dir")) { diff --git a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationState.kt b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationState.kt index fb03fa27326a..ba5eed792bca 100644 --- a/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationState.kt +++ b/python/src/com/jetbrains/python/sdk/uv/run/UvRunConfigurationState.kt @@ -17,11 +17,12 @@ import com.jetbrains.python.onFailure import com.jetbrains.python.run.PythonCommandLineState import com.jetbrains.python.run.PythonExecution import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest +import com.jetbrains.python.sdk.add.v2.PathHolder import com.jetbrains.python.sdk.uv.ScriptSyncCheckResult import com.jetbrains.python.sdk.uv.UvLowLevel -import com.jetbrains.python.sdk.uv.impl.createUvCli -import com.jetbrains.python.sdk.uv.impl.createUvLowLevel -import com.jetbrains.python.sdk.uv.impl.getUvExecutable +import com.jetbrains.python.sdk.uv.impl.createUvCliLocal +import com.jetbrains.python.sdk.uv.impl.createUvLowLevelLocal +import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal import org.jetbrains.annotations.ApiStatus import java.nio.file.Path @@ -74,10 +75,10 @@ fun canRun( var isError = false var isUnsynced = false runWithModalProgressBlocking(project, PyBundle.message("uv.run.configuration.state.progress.name")) { - val uvExecutable = getUvExecutable() + val uvExecutable = getUvExecutableLocal() if (workingDirectory != null && uvExecutable != null) { - val uv = createUvCli(uvExecutable).mapSuccess { createUvLowLevel(workingDirectory, it) }.getOrNull() + val uv = createUvCliLocal(uvExecutable).mapSuccess { createUvLowLevelLocal(workingDirectory, it) }.getOrNull() when (uv?.let { requiresSync(it, options, logger) }?.getOrNull()) { true -> isUnsynced = true @@ -99,7 +100,7 @@ fun canRun( @ApiStatus.Internal suspend fun requiresSync( - uv: UvLowLevel, + uv: UvLowLevel, options: UvRunConfigurationOptions, logger: Logger, ): Result { diff --git a/python/src/com/jetbrains/python/sdk/uv/run/UvRunToolProvider.kt b/python/src/com/jetbrains/python/sdk/uv/run/UvRunToolProvider.kt index 2ac79fcad538..fc46777a9af2 100644 --- a/python/src/com/jetbrains/python/sdk/uv/run/UvRunToolProvider.kt +++ b/python/src/com/jetbrains/python/sdk/uv/run/UvRunToolProvider.kt @@ -2,34 +2,33 @@ package com.jetbrains.python.sdk.uv.run import com.intellij.openapi.projectRoots.Sdk +import com.intellij.platform.eel.provider.localEel import com.jetbrains.python.PyBundle import com.jetbrains.python.run.features.PyRunToolData import com.jetbrains.python.run.features.PyRunToolId import com.jetbrains.python.run.features.PyRunToolParameters import com.jetbrains.python.run.features.PyRunToolProvider +import com.jetbrains.python.sdk.add.v2.FileSystem +import com.jetbrains.python.sdk.uv.getUvExecutionContext import com.jetbrains.python.sdk.uv.impl.getUvExecutable import com.jetbrains.python.sdk.uv.isUv -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock /** * PyRunToolProvider implementation that runs scripts/modules using `uv run`. */ internal class UvRunToolProvider : PyRunToolProvider { - override suspend fun getRunToolParameters(): PyRunToolParameters { - if (!runToolParameters.isCompleted) { - runToolParametersMutex.withLock { - if (!runToolParameters.isCompleted) { - runToolParameters.complete( - PyRunToolParameters(requireNotNull(getUvExecutable()?.toString()) { "Unable to find uv executable." }, listOf("run")) - ) - } - } + override suspend fun getRunToolParameters(sdk: Sdk): PyRunToolParameters { + val env = mutableMapOf() + val uvExecutionContext = sdk.getUvExecutionContext() + val fileSystem = uvExecutionContext?.fileSystem ?: FileSystem.Eel(localEel) + val uvExecutable = getUvExecutable(fileSystem, uvExecutionContext?.uvPath?.toString())?.toString() + // TODO PY-87712 Duplicated code for setting up uv envs + uvExecutionContext?.venvPath?.toString()?.let { + env += "VIRTUAL_ENV" to it + env += "UV_PROJECT_ENVIRONMENT" to it } - - return runToolParameters.await() + return PyRunToolParameters(requireNotNull(uvExecutable) { "Unable to find uv executable." }, listOf("run"), env, dropOldExe = true) } override val runToolData: PyRunToolData = PyRunToolData( @@ -41,11 +40,4 @@ internal class UvRunToolProvider : PyRunToolProvider { override val initialToolState: Boolean = true override fun isAvailable(sdk: Sdk): Boolean = sdk.isUv - - /** - * We use runToolParameters only if a tool provider is available. So we need to have a lazy initialization here - * to construct these parameters iff the validation has passed. - */ - private val runToolParameters = CompletableDeferred() - private val runToolParametersMutex = Mutex() } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/target/PythonLanguageRuntimeUI.kt b/python/src/com/jetbrains/python/target/PythonLanguageRuntimeUI.kt index 6bbeea78c7f4..170740827ebd 100644 --- a/python/src/com/jetbrains/python/target/PythonLanguageRuntimeUI.kt +++ b/python/src/com/jetbrains/python/target/PythonLanguageRuntimeUI.kt @@ -68,7 +68,7 @@ class PythonLanguageRuntimeUI( model = model, module = module, errorSink = ShowingMessageErrorSync, - limitExistingEnvironments = true, + limitExistingEnvironments = false, bestGuessCreateSdkInfo = CompletableDeferred(value = null) ) diff --git a/python/src/com/jetbrains/python/uv/packaging/UvPackageRequirementsTreeExtractor.kt b/python/src/com/jetbrains/python/uv/packaging/UvPackageRequirementsTreeExtractor.kt index 3f98b315520f..2ca078334460 100644 --- a/python/src/com/jetbrains/python/uv/packaging/UvPackageRequirementsTreeExtractor.kt +++ b/python/src/com/jetbrains/python/uv/packaging/UvPackageRequirementsTreeExtractor.kt @@ -18,17 +18,17 @@ import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequireme import com.jetbrains.python.packaging.packageRequirements.WorkspaceMemberPackageStructureNode import com.jetbrains.python.getOrNull import com.jetbrains.python.sdk.uv.UvLowLevel -import com.jetbrains.python.sdk.uv.UvSdkAdditionalData -import com.jetbrains.python.sdk.uv.impl.createUvLowLevel +import com.jetbrains.python.sdk.uv.getUvExecutionContext import com.jetbrains.python.sdk.uv.isUv import java.nio.file.Path -internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory: Path?, private val project: Project) : PythonPackageRequirementsTreeExtractor { +internal class UvPackageRequirementsTreeExtractor(private val sdk: Sdk, private val project: Project) : PythonPackageRequirementsTreeExtractor { override suspend fun extract(declaredPackageNames: Set): PackageStructureNode { - val uv = uvWorkingDirectory?.let { createUvLowLevel(it).getOrNull() } ?: return PackageCollectionPackageStructureNode(emptyList(), emptyList()) + val uvExecutionContext = sdk.getUvExecutionContext() ?: return PackageCollectionPackageStructureNode(emptyList(), emptyList()) + val uv = uvExecutionContext.createUvCli().getOr { return PackageCollectionPackageStructureNode(emptyList(), emptyList()) } - val workspaceTree = buildWorkspaceStructure(uv, declaredPackageNames) + val workspaceTree = buildWorkspaceStructure(uv, declaredPackageNames, uvExecutionContext.workingDir) if (workspaceTree != null) return workspaceTree val declaredPackages = declaredPackageNames.map { extractPackageTree(uv, it) } @@ -36,7 +36,7 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory return PackageCollectionPackageStructureNode(declaredPackages, undeclaredPackages) } - private suspend fun extractPackageTree(uv: UvLowLevel, packageName: String): PackageNode { + private suspend fun extractPackageTree(uv: UvLowLevel<*>, packageName: String): PackageNode { val output = uv.listPackageRequirementsTree(PythonPackage(packageName, "", false)).getOr { return createLeafNode(packageName) } @@ -46,8 +46,12 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory private fun createLeafNode(packageName: String): PackageNode = PackageNode(PyPackageName.from(packageName)) - private suspend fun buildWorkspaceStructure(uv: UvLowLevel, declaredPackageNames: Set): WorkspaceMemberPackageStructureNode? { - val (rootName, subMemberNames) = getWorkspaceLayout() ?: return null + private suspend fun buildWorkspaceStructure( + uv: UvLowLevel<*>, + declaredPackageNames: Set, + uvWorkingDirectory: Path, + ): WorkspaceMemberPackageStructureNode? { + val (rootName, subMemberNames) = getWorkspaceLayout(uvWorkingDirectory) ?: return null val allMemberNames = (setOf(rootName) + subMemberNames).mapTo(mutableSetOf()) { PyPackageName.from(it).name } @@ -70,8 +74,7 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory return PackageNode(name, filteredChildren.toMutableList(), group) } - private fun getWorkspaceLayout(): Pair>? { - val workspaceRoot = uvWorkingDirectory ?: return null + private fun getWorkspaceLayout(uvWorkingDirectory: Path): Pair>? { val modules = ModuleManager.getInstance(project).modules .filter { it.isPyProjectTomlBased } @@ -81,8 +84,8 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory for (module in modules) { val moduleDir = ModuleRootManager.getInstance(module).contentRoots.firstOrNull()?.toNioPath() ?: continue when { - moduleDir == workspaceRoot -> rootName = module.name - moduleDir.startsWith(workspaceRoot) -> subMemberNames.add(module.name) + moduleDir == uvWorkingDirectory -> rootName = module.name + moduleDir.startsWith(uvWorkingDirectory) -> subMemberNames.add(module.name) } } @@ -106,7 +109,7 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory } } - private suspend fun extractUndeclaredPackages(uv: UvLowLevel?, declaredPackageNames: Set): List { + private suspend fun extractUndeclaredPackages(uv: UvLowLevel<*>, declaredPackageNames: Set): List { val output = uv?.listAllPackagesTree()?.getOrNull() ?: return emptyList() return splitIntoPackageGroups(output.lines()).map { parseTree(it) } .filter { it.name.name !in declaredPackageNames } @@ -131,7 +134,6 @@ internal class UvPackageRequirementsTreeExtractor(private val uvWorkingDirectory internal class UvPackageRequirementsTreeExtractorProvider : PythonPackageRequirementsTreeExtractorProvider { override fun createExtractor(sdk: Sdk, project: Project): PythonPackageRequirementsTreeExtractor? { if (!sdk.isUv) return null - val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null - return UvPackageRequirementsTreeExtractor(data.uvWorkingDirectory, project) + return UvPackageRequirementsTreeExtractor(sdk, project) } } diff --git a/python/src/com/jetbrains/python/uv/packaging/UvPythonPackageRequiresExtractor.kt b/python/src/com/jetbrains/python/uv/packaging/UvPythonPackageRequiresExtractor.kt index eb33cc449be4..15ca04bf6546 100644 --- a/python/src/com/jetbrains/python/uv/packaging/UvPythonPackageRequiresExtractor.kt +++ b/python/src/com/jetbrains/python/uv/packaging/UvPythonPackageRequiresExtractor.kt @@ -8,16 +8,18 @@ import com.jetbrains.python.packaging.PyPackageName import com.jetbrains.python.packaging.common.PythonPackage import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequirementExtractor import com.jetbrains.python.packaging.packageRequirements.PythonPackageRequiresExtractorProvider -import com.jetbrains.python.sdk.baseDir -import com.jetbrains.python.sdk.uv.UvSdkAdditionalData -import com.jetbrains.python.sdk.uv.impl.createUvLowLevel -import java.nio.file.Path +import com.jetbrains.python.sdk.add.v2.PathHolder +import com.jetbrains.python.sdk.uv.UvExecutionContext +import com.jetbrains.python.sdk.uv.getUvExecutionContext +import com.jetbrains.python.sdk.uv.uvFlavorData -internal class UvPackageRequirementExtractor(private val uvWorkingDirectory: Path?) : PythonPackageRequirementExtractor { - override suspend fun extract(pkg: PythonPackage, module: Module): List { - val uvWorkingDirectory = uvWorkingDirectory ?: Path.of(module.baseDir?.path!!) - val uv = createUvLowLevel(uvWorkingDirectory).getOr { - thisLogger().info("cannot run uv: ${it.error}") +internal class UvPackageRequirementExtractor(private val sdk: Sdk) : PythonPackageRequirementExtractor { + override suspend fun extract(pkg: PythonPackage, module: Module): List = + sdk.getUvExecutionContext(module.project)?.let { extractWithContext(it, pkg) } ?: emptyList() + + private suspend fun

extractWithContext(context: UvExecutionContext

, pkg: PythonPackage): List { + val uv = context.createUvCli().getOr { + thisLogger().warn("cannot run uv: ${it.error}") return emptyList() } return uv.listPackageRequirements(pkg).getOr { @@ -29,7 +31,7 @@ internal class UvPackageRequirementExtractor(private val uvWorkingDirectory: Pat internal class UvPackageRequiresExtractorProvider : PythonPackageRequiresExtractorProvider { override fun createExtractor(sdk: Sdk): PythonPackageRequirementExtractor? { - val data = sdk.sdkAdditionalData as? UvSdkAdditionalData ?: return null - return UvPackageRequirementExtractor(data.uvWorkingDirectory) + sdk.uvFlavorData ?: return null + return UvPackageRequirementExtractor(sdk) } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt index 427aad8fe226..95db9417a083 100644 --- a/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/uv/sdk/configuration/PyUvSdkConfiguration.kt @@ -23,7 +23,7 @@ import com.jetbrains.python.sdk.persist import com.jetbrains.python.sdk.pyvenvContains import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService import com.jetbrains.python.sdk.setAssociationToModule -import com.jetbrains.python.sdk.uv.impl.getUvExecutable +import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk import com.jetbrains.python.sdk.uv.setupNewUvSdkAndEnv import com.jetbrains.python.venvReader.tryResolvePath @@ -61,7 +61,7 @@ internal class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension { module: Module, venvsInModule: List, ): EnvCheckerResult { - getUvExecutable() ?: return EnvCheckerResult.CannotConfigure + getUvExecutableLocal() ?: return EnvCheckerResult.CannotConfigure val intentionName = PyBundle.message("sdk.set.up.uv.environment", module.name) val envFound = getUvEnv(venvsInModule)?.findEnvOrNull(intentionName) return envFound ?: EnvCheckerResult.EnvNotFound(intentionName) @@ -79,22 +79,21 @@ internal class PyUvSdkConfiguration : PyProjectTomlConfigurationExtension { } ?: this private suspend fun createUv(module: Module, venvsInModule: List, envExists: Boolean): PyResult { + val uv = getUvExecutableLocal() ?: return PyResult.localizedError(PyBundle.message("sdk.cannot.find.uv.executable")) val sdkAssociatedModule = module.getSdkAssociatedModule() - val workingDir: Path? = tryResolvePath(sdkAssociatedModule.baseDir?.path) - if (workingDir == null) { - throw IllegalStateException("Can't determine working dir for the module") - } + val workingDir: Path = tryResolvePath(sdkAssociatedModule.baseDir?.path) + ?: throw IllegalStateException("Can't determine working dir for the module") val sdkSetupResult = if (envExists) { getUvEnv(venvsInModule)?.let { - setupExistingEnvAndSdk(it, workingDir, false, workingDir) + setupExistingEnvAndSdk(it, uv, workingDir, false) } ?: run { logger.warn("Can't find existing uv environment in project, but it was expected. " + "Probably it was deleted. New environment will be created") - setupNewUvSdkAndEnv(workingDir, null) + setupNewUvSdkAndEnv(uv, workingDir, null) } } - else setupNewUvSdkAndEnv(workingDir, null) + else setupNewUvSdkAndEnv(uv, workingDir, null) sdkSetupResult.onSuccess { withContext(Dispatchers.EDT) { diff --git a/python/src/com/jetbrains/python/uv/sdk/evolution/UvSelectSdkProvider.kt b/python/src/com/jetbrains/python/uv/sdk/evolution/UvSelectSdkProvider.kt index c8a27c94009b..12c9c1b69657 100644 --- a/python/src/com/jetbrains/python/uv/sdk/evolution/UvSelectSdkProvider.kt +++ b/python/src/com/jetbrains/python/uv/sdk/evolution/UvSelectSdkProvider.kt @@ -15,7 +15,7 @@ import com.jetbrains.python.PyBundle import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.sdk.baseDir -import com.jetbrains.python.sdk.uv.impl.getUvExecutable +import com.jetbrains.python.sdk.uv.impl.getUvExecutableLocal import com.jetbrains.python.venvReader.VirtualEnvReader import java.nio.file.Path @@ -24,7 +24,7 @@ internal class UvSelectSdkProvider : EvoSelectSdkProvider { override fun getTreeElement(evoModuleSdk: EvoModuleSdk): EvoTreeLazyNodeElement { val icon = PythonCommunityImplUVCommonIcons.UV return EvoTreeLazyNodeElement("uv", icon) { - getUvExecutable() ?: return@EvoTreeLazyNodeElement PyResult.localizedError(PyBundle.message("evolution.uv.executable.is.not.found")) + getUvExecutableLocal() ?: return@EvoTreeLazyNodeElement PyResult.localizedError(PyBundle.message("evolution.uv.executable.is.not.found")) val environments = VenvEvoSdkManager.findEnvironments(evoModuleSdk.module).getOr { return@EvoTreeLazyNodeElement it diff --git a/python/testFramework/src/com/jetbrains/python/tools/sdk.kt b/python/testFramework/src/com/jetbrains/python/tools/sdk.kt index 151c2f8c85bd..fa847618ce15 100644 --- a/python/testFramework/src/com/jetbrains/python/tools/sdk.kt +++ b/python/testFramework/src/com/jetbrains/python/tools/sdk.kt @@ -22,7 +22,7 @@ fun createUvPipVenvSdk(venvPython: PythonBinary, workingDirectory: Path?): Sdk { return SdkConfigurationUtil.setupSdk(emptyArray(), venvPython.refreshAndFindVirtualFileOrDirectory()!!, SdkType.findByName(PyNames.PYTHON_SDK_ID_NAME)!!, - UvSdkAdditionalData(workingDirectory, true), + UvSdkAdditionalData(workingDirectory, true, null, null), null) }