mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
PY-80890 Support uv for remotes
Also fixes PY-87708 (cherry picked from commit e5c119e9f06f57ecd34464d9bc23bf63b8b8d95e) IJ-MR-189717 GitOrigin-RevId: bf348b7e298e613aa94999e3cc97c2e1b8d4aa21
This commit is contained in:
committed by
intellij-monorepo-bot
parent
ea1ec35e53
commit
edcf151012
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<FullPathOnTarget> = 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<RelativePathOnTarget> = 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<String, String> = 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
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ internal class ExecServiceImpl private constructor() : ExecService {
|
||||
|
||||
private suspend fun create(binary: BinaryToExec, args: Args, options: ExecOptionsBase, scopeToBind: CoroutineScope? = null): Result<ProcessLauncher, ExecuteGetProcessError.EnvironmentError> {
|
||||
val scope = scopeToBind ?: ApplicationManager.getApplication().service<MyService>().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)
|
||||
|
||||
@@ -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<Path>): List<TargetEnvironment.UploadRoot> {
|
||||
override fun mapUploadRoots(
|
||||
request: TargetEnvironmentRequest,
|
||||
localDirs: Set<Path>,
|
||||
workingDirToDownload: Path?,
|
||||
): List<TargetEnvironment.UploadRoot> {
|
||||
val result = localDirs.map { localDir ->
|
||||
TargetEnvironment.UploadRoot(
|
||||
localRootPath = localDir,
|
||||
|
||||
@@ -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<String, String>,
|
||||
val usePty: TtySize?,
|
||||
)
|
||||
val downloadConfig: DownloadConfig? = null,
|
||||
)
|
||||
|
||||
@@ -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<Process, ExecErrorReason.CantStart> {
|
||||
|
||||
try {
|
||||
|
||||
@@ -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<Path>): List<TargetEnvironment.UploadRoot>
|
||||
fun mapUploadRoots(
|
||||
request: TargetEnvironmentRequest,
|
||||
localDirs: Set<Path>,
|
||||
workingDirToDownload: Path?
|
||||
): List<TargetEnvironment.UploadRoot>
|
||||
|
||||
/**
|
||||
* 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<TargetEnvironment.UploadRoot>,
|
||||
localDirs: Set<Path>,
|
||||
): List<TargetEnvironment.DownloadRoot> = 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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ suspend fun detectTool(
|
||||
}
|
||||
|
||||
paths.firstOrNull { it.isExecutable() }
|
||||
|
||||
}
|
||||
|
||||
private fun MutableList<Path>.addUnixPaths(eel: EelApi, binaryName: String) {
|
||||
|
||||
@@ -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<String, PythonOutdatedPackage> = emptyMap()
|
||||
|
||||
private fun createCachedDependencies(dependencyFile: VirtualFile): CachedValue<Deferred<PyResult<List<PythonPackage>>?>> =
|
||||
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<PyResult<List<PythonPackage>>?> {
|
||||
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<Deferred<PyResult<List<PythonPackage>>?>> {
|
||||
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<String> = emptyList()): PyResult<List<PythonPackage>> {
|
||||
suspend fun installPackage(
|
||||
installRequest: PythonPackageInstallRequest,
|
||||
options: List<String> = emptyList(),
|
||||
): PyResult<List<PythonPackage>> {
|
||||
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<String> = emptyList()): PyResult<List<PythonPackage>> {
|
||||
suspend fun installPackageDetached(
|
||||
installRequest: PythonPackageInstallRequest,
|
||||
options: List<String> = emptyList(),
|
||||
): PyResult<List<PythonPackage>> {
|
||||
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<String>): PyResult<Unit> =
|
||||
protected open suspend fun installPackageDetachedCommand(
|
||||
installRequest: PythonPackageInstallRequest,
|
||||
options: List<String>,
|
||||
): PyResult<Unit> =
|
||||
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<List<PythonPackage>>? {
|
||||
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<CachedValue<Deferred<PyResult<List<PythonPackage>>?>>>("PythonPackageManagerDependenciesCache")
|
||||
|
||||
@RequiresBackgroundThread
|
||||
fun forSdk(project: Project, sdk: Sdk): PythonPackageManager {
|
||||
val pythonPackageManagerService = project.service<PythonPackageManagerService>()
|
||||
@@ -332,7 +350,8 @@ abstract class PythonPackageManager(val project: Project, val sdk: Sdk) : Dispos
|
||||
}
|
||||
|
||||
@Topic.AppLevel
|
||||
val PACKAGE_MANAGEMENT_TOPIC: Topic<PythonPackageManagementListener> = Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN)
|
||||
val PACKAGE_MANAGEMENT_TOPIC: Topic<PythonPackageManagementListener> =
|
||||
Topic(PythonPackageManagementListener::class.java, Topic.BroadcastDirection.TO_DIRECT_CHILDREN)
|
||||
val RUNNING_PACKAGING_TASKS: Key<Boolean> = Key.create("PyPackageRequirementsInspection.RunningPackagingTasks")
|
||||
|
||||
@ApiStatus.Internal
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
fun PyRunToolProvider.getRunToolParametersForJvm(sdk: Sdk): PyRunToolParameters = runBlockingMaybeCancellable { getRunToolParameters(sdk) }
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
val envs: Map<String, String>,
|
||||
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.
|
||||
|
||||
@@ -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 <T> runExecutableWithProgress(
|
||||
vararg args: String,
|
||||
transformer: ProcessOutputTransformer<T>,
|
||||
execService: ExecService = ExecService(),
|
||||
processWeight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT
|
||||
processWeight: ConcurrentProcessWeight = ConcurrentProcessWeight.LIGHT,
|
||||
downloadConfig: DownloadConfig? = null,
|
||||
): PyResult<T> {
|
||||
val execOptions = ExecOptions(timeout = timeout, env = env, weight = processWeight)
|
||||
val execOptions = ExecOptions(timeout = timeout, env = env, weight = processWeight, downloadAfterExecution = downloadConfig)
|
||||
|
||||
val errorHandlerTransformer: ProcessOutputTransformer<T> = { output ->
|
||||
when {
|
||||
|
||||
@@ -262,6 +262,49 @@ suspend fun createSdk(
|
||||
?: PyResult.localizedError(PyBundle.message("python.sdk.failed.to.create.interpreter.title"))
|
||||
}
|
||||
|
||||
@Internal
|
||||
suspend fun <P : PathHolder> createSdk(
|
||||
pythonBinaryPath: P,
|
||||
suggestedSdkName: String,
|
||||
sdkAdditionalData: PythonSdkAdditionalData? = null,
|
||||
): PyResult<Sdk> {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<P : PathHolder> {
|
||||
val isLocal: Boolean
|
||||
|
||||
fun parsePath(raw: String): PyResult<P>
|
||||
fun validateExecutable(path: P): PyResult<Unit>
|
||||
suspend fun validateExecutable(path: P): PyResult<Unit>
|
||||
suspend fun fileExists(path: P): Boolean
|
||||
|
||||
/**
|
||||
* [pathToPython] has to be system (not venv) if set [requireSystemPython]
|
||||
@@ -93,6 +96,7 @@ sealed interface FileSystem<P : PathHolder> {
|
||||
|
||||
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<P : PathHolder> {
|
||||
PyResult.localizedError(e.localizedMessage)
|
||||
}
|
||||
|
||||
override fun validateExecutable(path: PathHolder.Eel): PyResult<Unit> {
|
||||
override suspend fun validateExecutable(path: PathHolder.Eel): PyResult<Unit> {
|
||||
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<P : PathHolder> {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fileExists(path: PathHolder.Eel): Boolean = path.path.exists()
|
||||
|
||||
override suspend fun validateVenv(homePath: PathHolder.Eel): PyResult<Unit> = 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<P : PathHolder> {
|
||||
}
|
||||
|
||||
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<P : PathHolder> {
|
||||
override val isLocal: Boolean = false
|
||||
|
||||
private val systemPythonCache = ArrayList<DetectedSelectableInterpreter<PathHolder.Target>>()
|
||||
private lateinit var shellImpl: PyResult<String>
|
||||
|
||||
override fun parsePath(raw: String): PyResult<PathHolder.Target> {
|
||||
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<Unit> = PyResult.success(Unit)
|
||||
override suspend fun validateExecutable(path: PathHolder.Target): PyResult<Unit> =
|
||||
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<Unit> = withContext(Dispatchers.IO) {
|
||||
val pythonBinaryPath = resolvePythonBinary(homePath)
|
||||
@@ -342,10 +360,35 @@ sealed interface FileSystem<P : PathHolder> {
|
||||
}
|
||||
|
||||
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<String> {
|
||||
if (!this::shellImpl.isInitialized) {
|
||||
shellImpl = getShellImpl()
|
||||
}
|
||||
return shellImpl
|
||||
}
|
||||
|
||||
private suspend fun getShellImpl(): PyResult<String> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
|
||||
open val state: AddInterpreterState<P> = AddInterpreterState(propertyGraph)
|
||||
|
||||
val condaViewModel: CondaViewModel<P> = CondaViewModel(fileSystem, propertyGraph, projectPathFlows)
|
||||
val uvViewModel: UvViewModel<P> = UvViewModel(fileSystem, propertyGraph)
|
||||
val uvViewModel: UvViewModel<P> = UvViewModel(fileSystem, propertyGraph, projectPathFlows)
|
||||
val pipenvViewModel: PipenvViewModel<P> = PipenvViewModel(fileSystem, propertyGraph)
|
||||
val poetryViewModel: PoetryViewModel<P> = PoetryViewModel(fileSystem, propertyGraph)
|
||||
val hatchViewModel: HatchViewModel<P> = HatchViewModel(fileSystem, propertyGraph, projectPathFlows)
|
||||
|
||||
@@ -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<P : PathHolder>(
|
||||
private val executableFlow = MutableStateFlow(model.uvViewModel.uvExecutable.get())
|
||||
private val pythonVersion: ObservableMutableProperty<Version?> = propertyGraph.property(null)
|
||||
private lateinit var versionComboBox: ComboBox<Version?>
|
||||
private lateinit var venvPathField: ValidatedPathField<Unit, P, ValidatedPath.Folder<P>>
|
||||
override val toolExecutable: ObservableProperty<ValidatedPath.Executable<P>?> = 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<P : PathHolder>(
|
||||
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<P : PathHolder>(
|
||||
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<P : PathHolder>(
|
||||
}
|
||||
|
||||
override suspend fun setupEnvSdk(moduleBasePath: Path): PyResult<Sdk> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<P : PathHolder>(model: PythonMutabl
|
||||
override val toolState: ToolValidator<P> = model.uvViewModel.toolValidator
|
||||
override val toolExecutable: ObservableProperty<ValidatedPath.Executable<P>?> = 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<Sdk> {
|
||||
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<DetectedSelectableInterpreter<P>> {
|
||||
val rootFolders = Files.walk(modulePath, 1)
|
||||
.filter(Files::isDirectory)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<P : PathHolder>(
|
||||
fileSystem: FileSystem<P>,
|
||||
propertyGraph: PropertyGraph,
|
||||
projectPathFlows: ProjectPathFlows,
|
||||
) : PythonToolViewModel {
|
||||
val uvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val uvVenvPath: ObservableMutableProperty<ValidatedPath.Folder<P>?> = propertyGraph.property(null)
|
||||
|
||||
val toolValidator: ToolValidator<P> = ToolValidator(
|
||||
fileSystem = fileSystem,
|
||||
@@ -24,17 +28,23 @@ class UvViewModel<P : PathHolder>(
|
||||
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<P> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>
|
||||
interface UvCli<P : PathHolder> {
|
||||
suspend fun runUv(workingDir: Path, venvPath: P?, canChangeTomlOrLock: Boolean, vararg args: String): PyResult<String>
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface UvLowLevel {
|
||||
suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<Path>
|
||||
interface UvLowLevel<P : PathHolder> {
|
||||
suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<P>
|
||||
|
||||
suspend fun listUvPythons(): PyResult<Set<Path>>
|
||||
suspend fun listSupportedPythonVersions(versionRequest: String? = null): PyResult<List<Version>>
|
||||
|
||||
@@ -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<P : PathHolder> {
|
||||
val workingDir: Path
|
||||
val venvPath: P?
|
||||
val fileSystem: FileSystem<P>
|
||||
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<PathHolder.Eel>
|
||||
|
||||
data class Target(
|
||||
override val workingDir: Path,
|
||||
override val venvPath: PathHolder.Target?,
|
||||
override val fileSystem: FileSystem.Target,
|
||||
override val uvPath: PathHolder.Target?,
|
||||
) : UvExecutionContext<PathHolder.Target>
|
||||
|
||||
suspend fun createUvCli(): PyResult<UvLowLevel<P>> = createUvCli(uvPath, fileSystem).mapSuccess { uvCli ->
|
||||
createUvLowLevel(workingDir, uvCli, fileSystem, venvPath)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setupNewUvSdkAndEnv(
|
||||
workingDir: Path,
|
||||
version: Version?,
|
||||
): PyResult<Sdk> {
|
||||
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<P : PathHolder> {
|
||||
val workingDir: Path
|
||||
val venvPath: P?
|
||||
val fileSystem: FileSystem<P>
|
||||
|
||||
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<PathHolder.Eel> {
|
||||
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<PathHolder.Target> {
|
||||
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 <P : PathHolder> createUvPathOperations(
|
||||
workingDir: Path,
|
||||
venvPath: P?,
|
||||
fileSystem: FileSystem<P>,
|
||||
): UvPathOperations<P> {
|
||||
return when (fileSystem) {
|
||||
is FileSystem.Eel -> UvPathOperations.Eel(
|
||||
workingDir = workingDir,
|
||||
venvPath = venvPath as? PathHolder.Eel,
|
||||
fileSystem = fileSystem,
|
||||
) as UvPathOperations<P>
|
||||
is FileSystem.Target -> UvPathOperations.Target(
|
||||
workingDir = workingDir,
|
||||
venvPath = venvPath as? PathHolder.Target,
|
||||
fileSystem = fileSystem,
|
||||
) as UvPathOperations<P>
|
||||
}
|
||||
}
|
||||
|
||||
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<UvExecutionContext<*>>? {
|
||||
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<MyService>().coroutineScope, project)?.await()
|
||||
|
||||
suspend fun setupNewUvSdkAndEnv(uvExecutable: Path, workingDir: Path, version: Version?): PyResult<Sdk> =
|
||||
setupNewUvSdkAndEnv(
|
||||
uvExecutable = PathHolder.Eel(uvExecutable),
|
||||
workingDir = workingDir,
|
||||
venvPath = null,
|
||||
fileSystem = FileSystem.Eel(localEel),
|
||||
version = version
|
||||
)
|
||||
|
||||
suspend fun <P : PathHolder> setupNewUvSdkAndEnv(
|
||||
uvExecutable: P,
|
||||
workingDir: Path,
|
||||
venvPath: P?,
|
||||
fileSystem: FileSystem<P>,
|
||||
version: Version?,
|
||||
): PyResult<Sdk> {
|
||||
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<Sdk> {
|
||||
val sdk = createSdk(
|
||||
pythonBinaryPath = PathHolder.Eel(envExecutable),
|
||||
associatedModulePath = moduleDir.toString(),
|
||||
suggestedSdkName = suggestedSdkName(envWorkingDir),
|
||||
sdkAdditionalData = UvSdkAdditionalData(envWorkingDir, usePip)
|
||||
): PyResult<Sdk> =
|
||||
setupExistingEnvAndSdk(
|
||||
pythonBinary = PathHolder.Eel(pythonBinary),
|
||||
uvPath = PathHolder.Eel(uvPath),
|
||||
workingDir = envWorkingDir,
|
||||
venvPath = null,
|
||||
fileSystem = FileSystem.Eel(localEel),
|
||||
usePip = usePip
|
||||
)
|
||||
|
||||
suspend fun <P : PathHolder> setupExistingEnvAndSdk(
|
||||
pythonBinary: P,
|
||||
uvPath: P,
|
||||
workingDir: Path,
|
||||
venvPath: P?,
|
||||
fileSystem: FileSystem<P>,
|
||||
usePip: Boolean,
|
||||
): PyResult<Sdk> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PyResult<UvLowLevel>>) : PythonPackageManager(project, sdk) {
|
||||
internal class UvPackageManager(project: Project, sdk: Sdk, uvExecutionContextDeferred: Deferred<UvExecutionContext<*>>) : PythonPackageManager(project, sdk) {
|
||||
override val repositoryManager: PythonRepositoryManager = PipRepositoryManager.getInstance(project)
|
||||
private val uvLowLevel = uvLowLevelDeferred.also { it.cancelOnDispose(this) }
|
||||
private lateinit var uvLowLevel: PyResult<UvLowLevel<*>>
|
||||
private val uvExecutionContextDeferred = uvExecutionContextDeferred.also { it.cancelOnDispose(this) }
|
||||
|
||||
private suspend fun <T> withUv(action: suspend (UvLowLevel) -> PyResult<T>): PyResult<T> {
|
||||
return when (val uvResult = uvLowLevel.await()) {
|
||||
private suspend fun <T> withUv(action: suspend (UvLowLevel<*>) -> PyResult<T>): PyResult<T> {
|
||||
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<out String>): PyResult<Pair<List<PyPackageName>, List<PyPackageName>>> {
|
||||
private suspend fun categorizePackages(uv: UvLowLevel<*>, packages: Array<out String>): PyResult<Pair<List<PyPackageName>, List<PyPackageName>>> {
|
||||
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<PyPackageName>): PyResult<Unit> {
|
||||
private suspend fun uninstallStandalonePackages(uv: UvLowLevel<*>, packages: List<PyPackageName>): PyResult<Unit> {
|
||||
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<PyPackageName>, workspaceMember: PyWorkspaceMember?): PyResult<Unit> {
|
||||
private suspend fun uninstallDeclaredPackages(uv: UvLowLevel<*>, packages: List<PyPackageName>, workspaceMember: PyWorkspaceMember?): PyResult<Unit> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PyFlavorData.Empty>() {
|
||||
// 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<UvSdkFlavorData>() {
|
||||
override fun getIcon(): Icon = PythonCommunityImplUVCommonIcons.UV
|
||||
override fun getFlavorDataClass(): Class<PyFlavorData.Empty> = PyFlavorData.Empty::class.java
|
||||
override fun getFlavorDataClass(): Class<UvSdkFlavorData> = UvSdkFlavorData::class.java
|
||||
|
||||
override fun isValidSdkPath(pathStr: String): Boolean {
|
||||
return false
|
||||
|
||||
@@ -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<Set<PythonBinary>> {
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <P : PathHolder> 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<String> {
|
||||
return runExecutableWithProgress(uv, workingDir,
|
||||
env = mapOf("VIRTUAL_ENV" to VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME), timeout = 10.minutes, args = args)
|
||||
private suspend fun <P : PathHolder> runUv(uv: P, workingDir: Path, venvPath: P?, fileSystem: FileSystem<P>, canChangeTomlOrLock: Boolean, vararg args: String): PyResult<String> {
|
||||
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<P : PathHolder>(val dispatcher: CoroutineDispatcher, val uv: P, private val fileSystem: FileSystem<P>) : UvCli<P> {
|
||||
|
||||
override suspend fun runUv(workingDir: Path, vararg args: String): PyResult<String> {
|
||||
return withContext(dispatcher) {
|
||||
runUv(uv, workingDir, *args)
|
||||
override suspend fun runUv(workingDir: Path, venvPath: P?, canChangeTomlOrLock: Boolean, vararg args: String): PyResult<String> = withContext(dispatcher) {
|
||||
runUv(uv, workingDir, venvPath, fileSystem, canChangeTomlOrLock, *args)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <P : PathHolder> detectUvExecutable(fileSystem: FileSystem<P>, pathFromSdk: FullPathOnTarget?): P? = detectTool("uv", fileSystem, pathFromSdk)
|
||||
|
||||
private suspend fun <P : PathHolder> detectTool(
|
||||
toolName: String,
|
||||
fileSystem: FileSystem<P>,
|
||||
pathFromSdk: FullPathOnTarget?,
|
||||
additionalSearchPaths: List<P> = 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<FullPathOnTarget> = 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 <P : PathHolder> getUvExecutable(fileSystem: FileSystem<P>, 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<UvCli> {
|
||||
val path = uv ?: getUvExecutable()
|
||||
val error = validateUvExecutable(path)
|
||||
suspend fun createUvCliLocal(uv: Path? = null, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult<UvCli<PathHolder.Eel>> {
|
||||
return createUvCli(uv?.let { PathHolder.Eel(it) }, FileSystem.Eel(localEel), dispatcher)
|
||||
}
|
||||
|
||||
suspend fun <P : PathHolder> createUvCli(uv: P?, fileSystem: FileSystem<P>, dispatcher: CoroutineDispatcher = Dispatchers.IO): PyResult<UvCli<P>> {
|
||||
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))
|
||||
}
|
||||
@@ -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<Path> {
|
||||
private class UvLowLevelImpl<P : PathHolder>(private val cwd: Path, private val venvPath: P?, private val uvCli: UvCli<P>, private val fileSystem: FileSystem<P>) : UvLowLevel<P> {
|
||||
override suspend fun initializeEnvironment(init: Boolean, version: Version?): PyResult<P> {
|
||||
val addPythonArg: (MutableList<String>) -> 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<Set<Path>> {
|
||||
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<List<PythonPackage>> {
|
||||
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<List<PythonOutdatedPackage>> {
|
||||
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<List<PythonPackage>> {
|
||||
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<List<PyPackageName>> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String>): PyResult<Unit> {
|
||||
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<out String>): PyResult<Unit> {
|
||||
// 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<String>): PyResult<Unit> {
|
||||
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<Boolean> {
|
||||
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<ScriptSyncCheckResult> {
|
||||
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<String> {
|
||||
return uvCli.runUv(cwd, "sync", "--all-packages", "--inexact")
|
||||
return uvCli.runUv(cwd, venvPath, true, "sync", "--all-packages", "--inexact")
|
||||
}
|
||||
|
||||
override suspend fun lock(): PyResult<String> {
|
||||
return uvCli.runUv(cwd, "lock")
|
||||
return uvCli.runUv(cwd, venvPath, true, "lock")
|
||||
}
|
||||
|
||||
suspend fun parsePackageList(input: String): List<PythonPackage> = 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<PathHolder.Eel>): UvLowLevel<PathHolder.Eel> =
|
||||
createUvLowLevel(cwd, uvCli, FileSystem.Eel(localEel), null)
|
||||
|
||||
suspend fun createUvLowLevel(cwd: Path): PyResult<UvLowLevel> = createUvCli().mapSuccess { createUvLowLevel(cwd, it) }
|
||||
fun <P : PathHolder> createUvLowLevel(cwd: Path, uvCli: UvCli<P>, fileSystem: FileSystem<P>, venvPath: P?): UvLowLevel<P> =
|
||||
UvLowLevelImpl(cwd, venvPath, uvCli, fileSystem)
|
||||
|
||||
suspend fun createUvLowLevelLocal(cwd: Path): PyResult<UvLowLevel<PathHolder.Eel>> =
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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<PathHolder.Eel>,
|
||||
options: UvRunConfigurationOptions,
|
||||
logger: Logger,
|
||||
): Result<Boolean, Unit> {
|
||||
|
||||
@@ -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<String, String>()
|
||||
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<PyRunToolParameters>()
|
||||
private val runToolParametersMutex = Mutex()
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class PythonLanguageRuntimeUI(
|
||||
model = model,
|
||||
module = module,
|
||||
errorSink = ShowingMessageErrorSync,
|
||||
limitExistingEnvironments = true,
|
||||
limitExistingEnvironments = false,
|
||||
bestGuessCreateSdkInfo = CompletableDeferred(value = null)
|
||||
)
|
||||
|
||||
|
||||
@@ -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<String>): 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<String>): WorkspaceMemberPackageStructureNode? {
|
||||
val (rootName, subMemberNames) = getWorkspaceLayout() ?: return null
|
||||
private suspend fun buildWorkspaceStructure(
|
||||
uv: UvLowLevel<*>,
|
||||
declaredPackageNames: Set<String>,
|
||||
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<String, List<String>>? {
|
||||
val workspaceRoot = uvWorkingDirectory ?: return null
|
||||
private fun getWorkspaceLayout(uvWorkingDirectory: Path): Pair<String, List<String>>? {
|
||||
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<String>): List<PackageNode> {
|
||||
private suspend fun extractUndeclaredPackages(uv: UvLowLevel<*>, declaredPackageNames: Set<String>): List<PackageNode> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PyPackageName> {
|
||||
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<PyPackageName> =
|
||||
sdk.getUvExecutionContext(module.project)?.let { extractWithContext(it, pkg) } ?: emptyList()
|
||||
|
||||
private suspend fun <P : PathHolder> extractWithContext(context: UvExecutionContext<P>, pkg: PythonPackage): List<PyPackageName> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PythonBinary>,
|
||||
): 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<PythonBinary>, envExists: Boolean): PyResult<Sdk> {
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user