mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-52688 fix legacy Python SDK metadata migration
Track when effective SDK data comes from legacy fields so migration is committed even when the loaded values already match. Keep canonical Hatch metadata authoritative while using legacy attributes as fallbacks. Migrate a writable SDK copy and avoid using the current project directory as a fallback for unrelated global SDKs. (cherry picked from commit db1227249757deb32c45b817beadf18c1dd3db8e) IJ-MR-215198 GitOrigin-RevId: 38ff5b9c5d0cb6a5d4b0988519deb0f9d9519962
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a18ab8dd28
commit
8b9a94a301
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Vendored
+5
-1
@@ -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)
|
||||
|
||||
@@ -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 <T : Any> valuesConflict(canonicalValue: T?, legacyValue: T?): Boolean =
|
||||
canonicalValue != null && legacyValue != null && canonicalValue != legacyValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Sdk>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -105,6 +105,7 @@ private object ValidationInProgressExtension : ExtendableTextComponent.Extension
|
||||
internal class ValidatedPathField<T, P : PathHolder, VP : ValidatedPath<T, P>>(
|
||||
val fileSystem: FileSystem<P>,
|
||||
val pathValidator: PathValidator<T, P, VP>,
|
||||
val canBeEdited: Boolean,
|
||||
browseFolderDialogTitle: @Nls String,
|
||||
isFileSelectionMode: Boolean,
|
||||
) : TextFieldWithBrowseButton() {
|
||||
@@ -145,6 +146,7 @@ internal class ValidatedPathField<T, P : PathHolder, VP : ValidatedPath<T, P>>(
|
||||
}
|
||||
|
||||
init {
|
||||
setButtonVisible(canBeEdited)
|
||||
addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
textInputFlow.value = text
|
||||
@@ -190,7 +192,7 @@ internal class ValidatedPathField<T, P : PathHolder, VP : ValidatedPath<T, P>>(
|
||||
}
|
||||
else {
|
||||
editorMode.store(false)
|
||||
isEnabled = true
|
||||
isEnabled = canBeEdited
|
||||
|
||||
pathValidator.backProperty.get()?.validationResult?.let { validationResult ->
|
||||
validationResult
|
||||
@@ -318,6 +320,7 @@ internal fun <T, P : PathHolder, VP : ValidatedPath<T, P>> Panel.validatablePath
|
||||
installAction: ActionLink? = null,
|
||||
isFileSelectionMode: Boolean = true,
|
||||
venvExistenceValidationState: ObservableProperty<VenvExistenceValidationState>? = null,
|
||||
canBeEdited: Boolean = true,
|
||||
): ValidatedPathField<T, P, VP> {
|
||||
|
||||
val validatedPathField = ValidatedPathField(
|
||||
@@ -325,6 +328,7 @@ internal fun <T, P : PathHolder, VP : ValidatedPath<T, P>> Panel.validatablePath
|
||||
pathValidator = pathValidator,
|
||||
browseFolderDialogTitle = labelText,
|
||||
isFileSelectionMode = isFileSelectionMode,
|
||||
canBeEdited = canBeEdited,
|
||||
)
|
||||
|
||||
if (missingExecutableText != null && installAction != null && !fileSystem.isReadOnly) {
|
||||
|
||||
@@ -200,11 +200,8 @@ private fun <P : PathHolder> 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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
Reference in New Issue
Block a user