diff --git a/python/python-hatch/src/com/jetbrains/python/hatch/sdk/HatchSdkAdditionalData.kt b/python/python-hatch/src/com/jetbrains/python/hatch/sdk/HatchSdkAdditionalData.kt index 00f32c5d5e33..aabe1733dd0b 100644 --- a/python/python-hatch/src/com/jetbrains/python/hatch/sdk/HatchSdkAdditionalData.kt +++ b/python/python-hatch/src/com/jetbrains/python/hatch/sdk/HatchSdkAdditionalData.kt @@ -33,6 +33,29 @@ class HatchSdkAdditionalData : PythonSdkAdditionalData { } } + override fun load(element: Element?) { + super.load(element) + if (element == null) return + + val legacyWorkingDirectory = getWorkingDirectory(element) + val legacyEnvironmentName = getEnvironmentName(element) + + val canonicalFlavorData = flavorAndData.data as? HatchSdkFlavorData + val effectiveFlavorData = HatchSdkFlavorData(canonicalFlavorData?.hatchEnvironmentName ?: legacyEnvironmentName) + if (flavorAndData.flavor != HatchSdkFlavor || canonicalFlavorData != effectiveFlavorData) { + setFlavorAndDataFromLegacy(PyFlavorAndData(effectiveFlavorData, HatchSdkFlavor)) + } + if (canonicalFlavorData?.hatchEnvironmentName != null && + legacyEnvironmentName != null && + canonicalFlavorData.hatchEnvironmentName != legacyEnvironmentName) { + markMigrationRequired() + } + + if (legacyWorkingDirectory != null && legacyWorkingDirectory != workingDirectory) { + markMigrationRequired() + } + } + companion object { private const val IS_HATCH = "IS_HATCH" private const val HATCH_WORKING_DIRECTORY = "HATCH_WORKING_DIR" @@ -41,14 +64,16 @@ class HatchSdkAdditionalData : PythonSdkAdditionalData { fun createIfHatch(element: Element): HatchSdkAdditionalData? { if (element.getAttributeValue(IS_HATCH) != "true") return null - val workingDirectory = element.getAttributeValue(HATCH_WORKING_DIRECTORY)?.takeIf { it.isNotBlank() }?.let { Path.of(it) } - ?: Path.of("") - val flavorData = HatchSdkFlavorData(element.getAttributeValue(HATCH_ENVIRONMENT_NAME)) - val data = HatchSdkAdditionalData(flavorData, workingDirectory).apply { + val workingDirectory = getWorkingDirectory(element) ?: Path.of("") + val flavorData = HatchSdkFlavorData(getEnvironmentName(element)) + return HatchSdkAdditionalData(flavorData, workingDirectory).apply { load(element) } - - return data } + + private fun getEnvironmentName(element: Element) = element.getAttributeValue(HATCH_ENVIRONMENT_NAME) + + private fun getWorkingDirectory(element: Element): Path? = + element.getAttributeValue(HATCH_WORKING_DIRECTORY)?.takeIf { it.isNotBlank() }?.let { Path.of(it) } } } diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkAdditionalData.java b/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkAdditionalData.java index 79303daf3ea4..fb0e71a73f45 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkAdditionalData.java +++ b/python/python-sdk/src/com/jetbrains/python/sdk/PythonSdkAdditionalData.java @@ -72,6 +72,7 @@ public class PythonSdkAdditionalData implements SdkAdditionalData { private String myRequirementsFile; private Path myLegacyRequiredTxtPath; private @NotNull Path myWorkingDirectory = EMPTY_WORKING_DIRECTORY; + private boolean myMigrationRequired; private final Gson myGson = new GsonBuilder().registerTypeAdapter(Path.class, new PathSerializer()).create(); @@ -243,10 +244,23 @@ public class PythonSdkAdditionalData implements SdkAdditionalData { return myFlavorAndData; } + @ApiStatus.Internal + protected final void setFlavorAndDataFromLegacy(@NotNull PyFlavorAndData flavorAndData) { + if (Objects.equals(myFlavorAndData, flavorAndData)) return; + myFlavorAndData = flavorAndData; + markMigrationRequired(); + } + + @ApiStatus.Internal + protected final void markMigrationRequired() { + myMigrationRequired = true; + } + @ApiStatus.Internal public final boolean migrateAdditionalData(@Nullable Path fallbackWorkingDirectory) { + boolean changed = myMigrationRequired; FlavorMigrationResult flavorMigration = migrateFlavorData(myFlavorAndData); - boolean changed = flavorMigration.changed(); + changed |= flavorMigration.changed(); Path requirementsWorkingDirectory = null; if (myRequirementsFile == null) { @@ -272,6 +286,7 @@ public class PythonSdkAdditionalData implements SdkAdditionalData { changed |= synchronizeFlavorWorkingDirectory(); } + myMigrationRequired = false; return changed; } @@ -284,7 +299,9 @@ public class PythonSdkAdditionalData implements SdkAdditionalData { } public void load(@Nullable Element element) { + Path legacyWorkingDirectory = myWorkingDirectory; myWorkingDirectory = EMPTY_WORKING_DIRECTORY; + myMigrationRequired = false; collectPaths(JDOMExternalizer.loadStringsList(element, PATHS_ADDED_BY_USER_ROOT, PATH_ADDED_BY_USER), myAddedPaths); collectPaths(JDOMExternalizer.loadStringsList(element, PATHS_REMOVED_BY_USER_ROOT, PATH_REMOVED_BY_USER), myExcludedPaths); collectPaths(JDOMExternalizer.loadStringsList(element, PATHS_TO_TRANSFER_ROOT, PATH_TO_TRANSFER), myPathsToTransfer); @@ -295,6 +312,10 @@ public class PythonSdkAdditionalData implements SdkAdditionalData { if (storedWorkingDirectory != null && !storedWorkingDirectory.isBlank()) { myWorkingDirectory = Path.of(storedWorkingDirectory); } + else if (!Objects.equals(legacyWorkingDirectory, EMPTY_WORKING_DIRECTORY)) { + myWorkingDirectory = legacyWorkingDirectory; + myMigrationRequired = true; + } myRequirementsFile = element.getAttributeValue(REQUIREMENTS_FILE); String legacyRequiredTxtPath = element.getAttributeValue(ASSOCIATED_REQUIRED_TXT_PATH); myLegacyRequiredTxtPath = legacyRequiredTxtPath == null ? null : Path.of(legacyRequiredTxtPath); diff --git a/python/python-test-env/core/src/com/intellij/python/test/env/core/CachingPyEnvironmentFactory.kt b/python/python-test-env/core/src/com/intellij/python/test/env/core/CachingPyEnvironmentFactory.kt index ebff27daa71b..d1840e8b5b1d 100644 --- a/python/python-test-env/core/src/com/intellij/python/test/env/core/CachingPyEnvironmentFactory.kt +++ b/python/python-test-env/core/src/com/intellij/python/test/env/core/CachingPyEnvironmentFactory.kt @@ -2,14 +2,18 @@ package com.intellij.python.test.env.core import org.jetbrains.annotations.ApiStatus +import java.nio.file.Path @ApiStatus.Internal class CachingPyEnvironmentFactory( private val wrapper: PyEnvironmentFactory, -) : PyEnvironmentFactory by wrapper { +) : PyEnvironmentFactory { private val cache = PyEnvironmentCache() + override val workingDir: Path + get() = wrapper.workingDir + override suspend fun createEnvironment(factory: PyEnvironmentFactory, spec: PyEnvironmentSpec<*>): PyEnvironment { return cache.getOrCreate(spec.toCacheKey()) { wrapper.createEnvironment(factory, spec) diff --git a/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkAdditionalData.kt b/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkAdditionalData.kt index d915a0ce8c6a..8c38cb9df1f0 100644 --- a/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkAdditionalData.kt +++ b/python/python-uv/backend/src/com/jetbrains/python/sdk/uv/UvSdkAdditionalData.kt @@ -46,6 +46,26 @@ class UvSdkAdditionalData : PythonSdkAdditionalData { } } + override fun load(element: Element?) { + super.load(element) + if (element == null) return + + val legacyFlavorData = getLegacyFlavorData(element) + val canonicalFlavorData = flavorAndData.data as? UvSdkFlavorData + val effectiveFlavorData = UvSdkFlavorData( + uvWorkingDirectory = canonicalFlavorData?.uvWorkingDirectory ?: legacyFlavorData.uvWorkingDirectory, + usePip = canonicalFlavorData?.usePip ?: legacyFlavorData.usePip, + venvPath = canonicalFlavorData?.venvPath ?: legacyFlavorData.venvPath, + uvPath = canonicalFlavorData?.uvPath ?: legacyFlavorData.uvPath, + ) + if (flavorAndData.flavor != UvSdkFlavor || canonicalFlavorData != effectiveFlavorData) { + setFlavorAndDataFromLegacy(PyFlavorAndData(effectiveFlavorData, UvSdkFlavor)) + } + if (canonicalFlavorData?.hasConflictsWith(legacyFlavorData) == true) { + markMigrationRequired() + } + } + companion object { private const val IS_UV = "IS_UV" private const val UV_WORKING_DIR = "UV_WORKING_DIR" @@ -57,22 +77,29 @@ class UvSdkAdditionalData : PythonSdkAdditionalData { fun load(element: Element): UvSdkAdditionalData? { return when { element.getAttributeValue(IS_UV) == "true" -> { - val uvWorkingDirectory = if (element.getAttributeValue(UV_WORKING_DIR).isNullOrEmpty()) { - Path.of("") - } - else { - Path.of(element.getAttributeValue(UV_WORKING_DIR)) - } - val usePip = element.getAttributeValue(USE_PIP)?.toBoolean() - val venvPath = element.getAttributeValue(UV_VENV_PATH)?.takeIf { it.isNotBlank() } - val uvPath = if (element.getAttributeValue(UV_TOOL_PATH).isNullOrEmpty()) null else element.getAttributeValue(UV_TOOL_PATH) - val legacyFlavorData = UvSdkFlavorData(uvWorkingDirectory, usePip, venvPath, uvPath) - UvSdkAdditionalData(legacyFlavorData, uvWorkingDirectory).apply { + val legacyFlavorData = getLegacyFlavorData(element) + UvSdkAdditionalData(legacyFlavorData, legacyFlavorData.uvWorkingDirectory ?: Path.of("")).apply { load(element) } } else -> null } } + + private fun getLegacyFlavorData(element: Element): UvSdkFlavorData = UvSdkFlavorData( + uvWorkingDirectory = element.getAttributeValue(UV_WORKING_DIR)?.takeIf { it.isNotBlank() }?.let { Path.of(it) }, + usePip = element.getAttributeValue(USE_PIP)?.toBoolean(), + venvPath = element.getAttributeValue(UV_VENV_PATH)?.takeIf { it.isNotBlank() }, + uvPath = element.getAttributeValue(UV_TOOL_PATH)?.takeIf { it.isNotBlank() }, + ) + + private fun UvSdkFlavorData.hasConflictsWith(legacyData: UvSdkFlavorData): Boolean = + valuesConflict(uvWorkingDirectory, legacyData.uvWorkingDirectory) || + valuesConflict(usePip, legacyData.usePip) || + valuesConflict(venvPath, legacyData.venvPath) || + valuesConflict(uvPath, legacyData.uvPath) + + private fun valuesConflict(canonicalValue: T?, legacyValue: T?): Boolean = + canonicalValue != null && legacyValue != null && canonicalValue != legacyValue } -} \ No newline at end of file +} diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkAdditionalDataMigrationActivity.kt b/python/src/com/jetbrains/python/sdk/PythonSdkAdditionalDataMigrationActivity.kt index d85613b27080..314bf22b1b32 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkAdditionalDataMigrationActivity.kt +++ b/python/src/com/jetbrains/python/sdk/PythonSdkAdditionalDataMigrationActivity.kt @@ -8,6 +8,7 @@ import com.intellij.openapi.project.modules import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.startup.ProjectActivity import com.intellij.util.concurrency.annotations.RequiresWriteLock import org.jetbrains.annotations.ApiStatus @@ -27,18 +28,18 @@ class PythonSdkAdditionalDataMigrationActivity : ProjectActivity, DumbAware { @RequiresWriteLock private fun migratePythonSdkAdditionalData(project: Project, sdks: List) { + val projectSdk = ProjectRootManager.getInstance(project).projectSdk for (sdk in sdks) { - val additionalData = sdk.pySdkAdditionalData val fallbackWorkingDirectory = sdk.associatedModuleNioPath ?: project.modules.firstNotNullOfOrNull { module -> module.takeIf { ModuleRootManager.getInstance(it).sdk == sdk }?.baseDir?.path?.let { Path.of(it) } } - ?: project.basePath?.let { Path.of(it) } + ?: project.basePath?.takeIf { projectSdk == sdk }?.let { Path.of(it) } + val modificator = sdk.sdkModificator + val additionalData = modificator.sdkAdditionalData as? PythonSdkAdditionalData ?: continue if (!additionalData.migrateAdditionalData(fallbackWorkingDirectory)) continue - sdk.sdkModificator.apply { - sdkAdditionalData = additionalData - commitChanges() - } + modificator.sdkAdditionalData = additionalData + modificator.commitChanges() } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index 538c29755a39..3f4bd69fc20d 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -282,7 +282,12 @@ data class EelFileSystem( } override fun resolvePythonHome(pythonHomeOrBinary: PathHolder.Eel): PathHolder.Eel { - return PathHolder.Eel(pythonHomeOrBinary.path.resolvePythonHome()) + val path = pythonHomeOrBinary.path + val fileName = path.fileName?.toString() + val parentName = path.parent?.fileName?.toString() + val isPythonBinary = fileName?.startsWith("python", ignoreCase = true) == true && + (parentName == "bin" || parentName.equals("scripts", ignoreCase = true)) + return if (isPythonBinary) PathHolder.Eel(path.resolvePythonHome()) else pythonHomeOrBinary } override fun getVenvName(pythonHome: PathHolder.Eel): String? { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt index e6cd84b46a9c..6cb3c6651cd2 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt @@ -105,6 +105,7 @@ private object ValidationInProgressExtension : ExtendableTextComponent.Extension internal class ValidatedPathField>( val fileSystem: FileSystem

, val pathValidator: PathValidator, + val canBeEdited: Boolean, browseFolderDialogTitle: @Nls String, isFileSelectionMode: Boolean, ) : TextFieldWithBrowseButton() { @@ -145,6 +146,7 @@ internal class ValidatedPathField>( } init { + setButtonVisible(canBeEdited) addDocumentListener(object : DocumentAdapter() { override fun textChanged(e: DocumentEvent) { textInputFlow.value = text @@ -190,7 +192,7 @@ internal class ValidatedPathField>( } else { editorMode.store(false) - isEnabled = true + isEnabled = canBeEdited pathValidator.backProperty.get()?.validationResult?.let { validationResult -> validationResult @@ -318,6 +320,7 @@ internal fun > Panel.validatablePath installAction: ActionLink? = null, isFileSelectionMode: Boolean = true, venvExistenceValidationState: ObservableProperty? = null, + canBeEdited: Boolean = true, ): ValidatedPathField { val validatedPathField = ValidatedPathField( @@ -325,6 +328,7 @@ internal fun > Panel.validatablePath pathValidator = pathValidator, browseFolderDialogTitle = labelText, isFileSelectionMode = isFileSelectionMode, + canBeEdited = canBeEdited, ) if (missingExecutableText != null && installAction != null && !fileSystem.isReadOnly) { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt index 30a33281e1e7..f41ae8bd0fce 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt @@ -200,11 +200,8 @@ private fun

Panel.addExecutableSelector( labelText = message("sdk.create.custom.venv.executable.path", "hatch"), missingExecutableText = message("sdk.create.custom.tool.not.detected", "hatch"), installAction = installHatchActionLink, + canBeEdited = model.fileSystem.toolPathCanBePersisted, ) - if (!model.fileSystem.toolPathCanBePersisted) { - executablePath.textField.isEditable = false - executablePath.setButtonVisible(false) - } return executablePath } diff --git a/python/testSrc/com/jetbrains/env/python/conda/PyCondaAdditionalDataTest.kt b/python/testSrc/com/jetbrains/env/python/conda/PyCondaAdditionalDataTest.kt index 455ec75bbcc3..02d7e771f8cf 100644 --- a/python/testSrc/com/jetbrains/env/python/conda/PyCondaAdditionalDataTest.kt +++ b/python/testSrc/com/jetbrains/env/python/conda/PyCondaAdditionalDataTest.kt @@ -2,6 +2,7 @@ package com.jetbrains.env.python.conda import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.testFramework.ProjectRule import com.jetbrains.python.sdk.PythonSdkAdditionalData import com.jetbrains.python.sdk.flavors.PyFlavorAndData @@ -32,7 +33,7 @@ class PyCondaAdditionalDataTest { ) ) val data = PythonSdkAdditionalData( - PyFlavorAndData(flavorData, CondaEnvSdkFlavor)) + PyFlavorAndData(flavorData, CondaEnvSdkFlavor), projectRule.project.basePath?.toNioPathOrNull()!!) val rootElement = Element("root") data.save(rootElement) diff --git a/python/testSrc/com/jetbrains/python/PythonMockSdk.java b/python/testSrc/com/jetbrains/python/PythonMockSdk.java index 57e2736902f7..9f0be55da9f9 100644 --- a/python/testSrc/com/jetbrains/python/PythonMockSdk.java +++ b/python/testSrc/com/jetbrains/python/PythonMockSdk.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -62,7 +63,8 @@ public final class PythonMockSdk { Sdk sdk = ProjectJdkTable.getInstance().createSdk(sdkName, sdkType); SdkModificator sdkModificator = sdk.getSdkModificator(); sdkModificator.setHomePath(sdkPath + "/bin/python"); - sdkModificator.setSdkAdditionalData(new PythonSdkAdditionalData(new PyFlavorAndData(PyFlavorData.Empty.INSTANCE, VirtualEnvSdkFlavor.getInstance()))); + sdkModificator.setSdkAdditionalData( + new PythonSdkAdditionalData(new PyFlavorAndData(PyFlavorData.Empty.INSTANCE, VirtualEnvSdkFlavor.getInstance()), Path.of(sdkPath))); sdkModificator.setVersionString(toVersionString(level)); createRoots(sdkPath, level).forEach(vFile -> {