,
+): ExecError = ExecErrorImpl(
+ exe = Exe.fromString(command),
+ args = args.toTypedArray(),
+ errorReason = ExecErrorReason.CantStart(null, exception.localizedMessage),
+ additionalMessageToUser = additionalMessageToUser,
+)
\ No newline at end of file
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/repository/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/repository/compat.kt
new file mode 100644
index 000000000..71b67e1b5
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/packaging/repository/compat.kt
@@ -0,0 +1,8 @@
+package com.jetbrains.python.packaging.repository
+
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+
+internal fun encodeCredentialsForUrl(login: String, password: String): String {
+ return "${URLEncoder.encode(login, StandardCharsets.UTF_8)}:${URLEncoder.encode(password, StandardCharsets.UTF_8)}"
+}
\ No newline at end of file
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/PythonSdkInstallBridge.java b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/PythonSdkInstallBridge.java
new file mode 100644
index 000000000..717c8177c
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/PythonSdkInstallBridge.java
@@ -0,0 +1,74 @@
+package com.jetbrains.python.sdk;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.projectRoots.Sdk;
+import kotlin.ResultKt;
+import kotlin.jvm.functions.Function0;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * Isolates access to Python 262's Kotlin-internal installable-SDK API.
+ *
+ * The declarations are public in JVM bytecode, but Kotlin intentionally hides them from other
+ * modules. Keeping the bridge in this platform-specific source set avoids reflection throughout
+ * the course creation flow and gives the rest of the plugin a stable SDK-shaped model.
+ */
+public final class PythonSdkInstallBridge {
+ private PythonSdkInstallBridge() {
+ }
+
+ public static final class Suggestion {
+ private final @NotNull PySdkToInstall delegate;
+ private final @NotNull String name;
+ private final @NotNull String version;
+
+ private Suggestion(@NotNull PySdkToInstall delegate) {
+ this.delegate = delegate;
+ name = delegate.getName();
+ version = delegate.getInstallation().getRelease().getVersion();
+ }
+
+ public @NotNull String getName() {
+ return name;
+ }
+
+ public @NotNull String getVersion() {
+ return version;
+ }
+ }
+
+ public static @NotNull List getSuggestions() {
+ return PySdkToInstallKt.getSdksToInstall().stream().map(Suggestion::new).toList();
+ }
+
+ public static @NotNull Sdk install(
+ @NotNull Suggestion suggestion,
+ @Nullable Module module,
+ @NotNull Function0 extends List extends Sdk>> existingSdks
+ ) throws Throwable {
+ Method installMethod = findInstallMethod();
+ final Object result;
+ try {
+ result = installMethod.invoke(suggestion.delegate, module, existingSdks);
+ }
+ catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ ResultKt.throwOnFailure(result);
+ return (Sdk)result;
+ }
+
+ private static @NotNull Method findInstallMethod() {
+ for (Method method : PySdkToInstall.class.getMethods()) {
+ if (method.getName().startsWith("install-") && method.getParameterCount() == 2) {
+ return method;
+ }
+ }
+ throw new IllegalStateException("Python SDK install method is not available");
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/add/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/add/compat.kt
new file mode 100644
index 000000000..b87d94380
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/add/compat.kt
@@ -0,0 +1,121 @@
+package com.jetbrains.python.sdk.add
+
+import com.intellij.openapi.application.AppUIExecutor
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.ModalityState
+import com.intellij.openapi.fileChooser.FileChooser
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.ui.ComboBox
+import com.intellij.openapi.ui.ComponentWithBrowseButton
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.ui.AnimatedIcon
+import com.intellij.ui.components.fields.ExtendableTextComponent
+import com.intellij.ui.components.fields.ExtendableTextField
+import com.jetbrains.python.sdk.PyDetectedSdk
+import com.jetbrains.python.sdk.PySdkListCellRenderer
+import com.jetbrains.python.sdk.PythonSdkType
+import javax.swing.JTextField
+import javax.swing.plaf.basic.BasicComboBoxEditor
+
+/**
+ * Minimal reimplementation of `com.jetbrains.python.sdk.add.PySdkPathChoosingComboBox`,
+ * which was removed from the Python plugin in 262 together with the whole v1 "add SDK" UI.
+ *
+ * Combobox items are plain [Sdk] instances.
+ */
+class PySdkPathChoosingComboBox : ComponentWithBrowseButton>(ComboBox(), null) {
+
+ private val busyIconExtension: ExtendableTextComponent.Extension =
+ ExtendableTextComponent.Extension { AnimatedIcon.Default.INSTANCE }
+
+ private val busyEditor: BasicComboBoxEditor = object : BasicComboBoxEditor() {
+ override fun createEditorComponent(): JTextField = ExtendableTextField().apply { isEditable = false }
+ }
+
+ init {
+ childComponent.renderer = PySdkListCellRenderer()
+ addActionListener {
+ val descriptor = PythonSdkType.getInstance().homeChooserDescriptor
+ FileChooser.chooseFiles(descriptor, null, null) { chosenFiles ->
+ val virtualFile = chosenFiles.firstOrNull() ?: return@chooseFiles
+ val path = FileUtil.toSystemDependentName(virtualFile.path)
+ childComponent.selectedItem = items.find { it.homePath == path } ?: PyDetectedSdk(path).also { addSdkItemOnTop(it) }
+ }
+ }
+ }
+
+ val selectedSdk: Sdk?
+ get() = childComponent.selectedItem as? Sdk
+
+ val items: List
+ get() = (0 until childComponent.itemCount).mapNotNull { childComponent.getItemAt(it) as? Sdk }
+
+ fun addSdkItem(sdk: Sdk) {
+ childComponent.addItem(sdk)
+ }
+
+ private fun addSdkItemOnTop(sdk: Sdk) {
+ childComponent.insertItemAt(sdk, 0)
+ }
+
+ fun setBusy(busy: Boolean) {
+ if (busy) {
+ childComponent.isEditable = true
+ childComponent.editor = busyEditor
+ (busyEditor.editorComponent as ExtendableTextField).addExtension(busyIconExtension)
+ }
+ else {
+ (busyEditor.editorComponent as ExtendableTextField).removeExtension(busyIconExtension)
+ childComponent.isEditable = false
+ }
+ repaint()
+ }
+}
+
+/**
+ * Kept for source compatibility with previous platform versions where the combobox could contain
+ * a "new sdk" item. This reimplementation never adds such an item.
+ */
+class NewPySdkComboBoxItem
+
+/**
+ * Copy-pasted from intellij sources.
+ * In 261 marked as deprecated to be removed, removed in 262.
+ * TODO: rewrite
+ */
+fun addInterpretersAsync(
+ sdkComboBox: PySdkPathChoosingComboBox,
+ sdkObtainer: () -> List,
+ onAdded: (List) -> Unit,
+) {
+ ApplicationManager.getApplication().executeOnPooledThread {
+ val executor = AppUIExecutor.onUiThread(ModalityState.any())
+ executor.execute { sdkComboBox.setBusy(true) }
+ var sdks = emptyList()
+ try {
+ sdks = sdkObtainer()
+ }
+ finally {
+ executor.execute {
+ sdkComboBox.setBusy(false)
+ sdkComboBox.removeAllItems()
+ sdks.forEach(sdkComboBox::addSdkItem)
+ onAdded(sdks)
+ }
+ }
+ }
+}
+
+/**
+ * Keeps [NewPySdkComboBoxItem] if it is present in the combobox.
+ */
+private fun PySdkPathChoosingComboBox.removeAllItems() {
+ if (childComponent.itemCount > 0 && childComponent.getItemAt(0) is NewPySdkComboBoxItem) {
+ while (childComponent.itemCount > 1) {
+ childComponent.removeItemAt(1)
+ }
+ }
+ else {
+ childComponent.removeAllItems()
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/compat.kt
new file mode 100644
index 000000000..6fd007686
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/compat.kt
@@ -0,0 +1,58 @@
+package com.jetbrains.python.sdk
+
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.module.ModuleUtil
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl
+import com.intellij.openapi.util.UserDataHolder
+import com.intellij.openapi.vfs.VirtualFile
+import com.jetbrains.python.sdk.skeleton.PySkeletonUtil
+import java.nio.file.Files
+import java.nio.file.Paths
+
+internal fun Project.excludeInnerVirtualEnv(sdk: Sdk) {
+ val binary = sdk.homeDirectory ?: return
+ ModuleUtil.findModuleForFile(binary, this)?.excludeInnerVirtualEnv(sdk)
+}
+
+/**
+ * Replacement for `com.jetbrains.python.sdk.findBaseSdks` removed in 262.
+ *
+ * The original implementation also returned system-wide SDKs from [existingSdks];
+ * all call sites in this plugin pass an empty list, so only detection is performed here.
+ */
+@Suppress("DEPRECATION_ERROR")
+fun findBaseSdks(existingSdks: List, module: Module?, context: UserDataHolder): List {
+ return detectSystemWideSdks(module, existingSdks, context)
+}
+
+/**
+ * Replacement for the `com.jetbrains.python.sdk.sdkSeemsValid` extension, which is not available in 262.
+ */
+val Sdk.sdkSeemsValid: Boolean
+ get() = isSdkSeemsValid
+
+/**
+ * In 262 installable Python interpreters are no longer SDK instances and their API is internal.
+ * Adapt them to the SDK-based course wizard while keeping all access to the internal API in the
+ * Java bridge (Java does not enforce Kotlin's module-level `internal` visibility).
+ */
+fun getSdksToInstall(): List = PythonSdkInstallBridge.getSuggestions().map(::PySdkToInstallCompat)
+
+internal class PySdkToInstallCompat(
+ val suggestion: PythonSdkInstallBridge.Suggestion
+) : ProjectJdkImpl(
+ suggestion.name,
+ PythonSdkType.getInstance(),
+ null,
+ suggestion.version,
+)
+
+internal fun Sdk.adminPermissionsNeeded(): Boolean {
+ val pathToCheck = sitePackagesDirectory?.path ?: homePath ?: return false
+ return !Files.isWritable(Paths.get(pathToCheck))
+}
+
+private val Sdk.sitePackagesDirectory: VirtualFile?
+ get() = PySkeletonUtil.getSitePackagesDirectory(this)
diff --git a/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/configuration/compat.kt b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/configuration/compat.kt
new file mode 100644
index 000000000..16a9c1dea
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/com/jetbrains/python/sdk/configuration/compat.kt
@@ -0,0 +1,112 @@
+package com.jetbrains.python.sdk.configuration
+
+import com.intellij.execution.ExecutionException
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.progress.Task
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.ProjectJdkTable
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.util.UserDataHolder
+import com.intellij.openapi.util.UserDataHolderBase
+import com.intellij.openapi.vfs.StandardFileSystems
+import com.intellij.platform.ide.progress.ModalTaskOwner
+import com.intellij.platform.ide.progress.runWithModalProgressBlocking
+import com.intellij.util.concurrency.annotations.RequiresEdt
+import com.jetbrains.python.packaging.PyPackageManagers
+import com.jetbrains.python.packaging.PyTargetEnvCreationManager
+import com.jetbrains.python.sdk.PythonSdkType
+import com.jetbrains.python.sdk.baseDir
+import com.jetbrains.python.sdk.excludeInnerVirtualEnv
+import com.jetbrains.python.sdk.impl.PySdkBundle
+import com.jetbrains.python.sdk.setAssociationToModule
+import com.jetbrains.python.sdk.setAssociationToPath
+import com.jetbrains.python.sdk.targetEnvConfiguration
+import com.jetbrains.python.target.ui.TargetPanelExtension
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Reimplementation of `com.jetbrains.python.sdk.configuration.createVirtualEnvAndSdkSynchronously`:
+ * in 262 its building blocks (`installSdkIfNeeded`, `createSdkByGenerateTask`, `pyModalBlocking`,
+ * `PyTargetAwareAdditionalData.getInterpreterVersion`, `setAssociationToModuleAsync`) became internal
+ * or were removed from the Python plugin.
+ *
+ * TODO: 262 — only local base SDKs are supported, the Targets API branch was dropped.
+ */
+@ApiStatus.Internal
+@RequiresEdt
+@Suppress("UNUSED_PARAMETER")
+fun createVirtualEnvAndSdkSynchronously(
+ baseSdk: Sdk,
+ existingSdks: List,
+ venvRoot: String,
+ projectBasePath: String?,
+ project: Project?,
+ module: Module?,
+ context: UserDataHolder = UserDataHolderBase(),
+ inheritSitePackages: Boolean = false,
+ makeShared: Boolean = false,
+ targetPanelExtension: TargetPanelExtension? = null,
+): Sdk {
+ if (baseSdk.targetEnvConfiguration != null) {
+ // TODO: 262 — target-based SDK creation APIs (`createSdkForTarget` and friends) are internal now
+ throw ExecutionException("Creating virtual environments for target-based SDKs is not supported")
+ }
+
+ // Historically `installSdkIfNeeded` installed Python first when `baseSdk` was a `PySdkToInstall`.
+ // On 262 `PySdkToInstall` is internal and never reaches this method, so `baseSdk` is always an installed SDK.
+ val installedSdk: Sdk = baseSdk
+
+ val projectPath = projectBasePath ?: module?.baseDir?.path ?: project?.basePath
+ val task = object : Task.WithResult(project, PySdkBundle.message("python.creating.venv.title"), false) {
+ override fun compute(indicator: ProgressIndicator): String {
+ indicator.isIndeterminate = true
+ val sdk = if (installedSdk is Disposable && Disposer.isDisposed(installedSdk)) {
+ ProjectJdkTable.getInstance().findJdk(installedSdk.name)!!
+ }
+ else {
+ installedSdk
+ }
+
+ try {
+ return PyTargetEnvCreationManager(sdk).createVirtualEnv(venvRoot, inheritSitePackages)
+ }
+ finally {
+ PyPackageManagers.getInstance().clearCache(sdk)
+ }
+ }
+ }
+ val venvSdk = createSdkByGenerateTask(task, existingSdks)
+
+ if (!makeShared) {
+ when {
+ module != null -> runWithModalProgressBlocking(ModalTaskOwner.guess(), "...") { venvSdk.setAssociationToModule(module) }
+ projectPath != null -> runWithModalProgressBlocking(ModalTaskOwner.guess(), "...") { venvSdk.setAssociationToPath(projectPath) }
+ }
+ }
+
+ project?.excludeInnerVirtualEnv(venvSdk)
+
+ // Note: previous versions of this compat code constructed (but never ran) a background task
+ // storing the preferred virtual env base path in `PySdkSettings`, so it is intentionally omitted here.
+
+ return venvSdk
+}
+
+/**
+ * Replacement for `com.jetbrains.python.sdk.createSdkByGenerateTask` removed in 262:
+ * runs [generateSdkHomePath] under a progress indicator and sets up an SDK for the resulting interpreter path.
+ */
+private fun createSdkByGenerateTask(
+ generateSdkHomePath: Task.WithResult,
+ existingSdks: List,
+): Sdk {
+ val homePath = ProgressManager.getInstance().run(generateSdkHomePath)
+ val homeFile = StandardFileSystems.local().refreshAndFindFileByPath(homePath)
+ ?: throw ExecutionException("Python interpreter is not found at $homePath")
+ return SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile, PythonSdkType.getInstance(), null, null)
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt b/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt
new file mode 100644
index 000000000..ce6a36944
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/compatibilityUtils.kt
@@ -0,0 +1,16 @@
+package org.hyperskill.academy.python.learning
+
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.platform.ide.progress.runWithModalProgressBlocking
+import com.jetbrains.python.sdk.setAssociationToModule
+
+// Note: unlike previous versions, this file does not provide `installRequiredPackages(reporter, packageManager, requirements)`:
+// it had no callers, and the APIs it relied on (`PythonPackageManager.installPackage`, `repositoryManager`, `toInstallRequest`)
+// became internal in 262. Package installation goes through `PyEduUtils.installRequiredPackages(project, sdk)` instead.
+
+internal fun setAssociationToModule(sdk: Sdk, module: Module) {
+ runWithModalProgressBlocking(module.project, "") {
+ sdk.setAssociationToModule(module)
+ }
+}
diff --git a/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/newproject/compat.kt b/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/newproject/compat.kt
new file mode 100644
index 000000000..b9a24afe9
--- /dev/null
+++ b/intellij-plugin/hs-Python/branches/262/src/org/hyperskill/academy/python/learning/newproject/compat.kt
@@ -0,0 +1,27 @@
+package org.hyperskill.academy.python.learning.newproject
+
+import com.intellij.openapi.application.invokeAndWaitIfNeeded
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.projectRoots.Sdk
+import com.jetbrains.python.sdk.PySdkToInstallCompat
+import com.jetbrains.python.sdk.PythonSdkInstallBridge
+import com.jetbrains.python.sdk.detectSystemWideSdks
+
+private val LOG = logger()
+
+@Suppress("DEPRECATION_ERROR")
+internal fun installSdkIfSuggested(sdk: Sdk?): Sdk? {
+ if (sdk !is PySdkToInstallCompat) return null
+
+ return invokeAndWaitIfNeeded {
+ try {
+ PythonSdkInstallBridge.install(sdk.suggestion, null) {
+ detectSystemWideSdks(null, emptyList())
+ }
+ }
+ catch (e: Throwable) {
+ LOG.warn(e)
+ null
+ }
+ }
+}
diff --git a/intellij-plugin/hs-Python/build.gradle.kts b/intellij-plugin/hs-Python/build.gradle.kts
index 16cd5d373..0890bc4b1 100644
--- a/intellij-plugin/hs-Python/build.gradle.kts
+++ b/intellij-plugin/hs-Python/build.gradle.kts
@@ -7,6 +7,7 @@ dependencies {
// needed to load `org.toml.lang plugin` for Python plugin in tests
val ideVersion = if (isRiderIDE) ideaVersion else baseVersion
intellijIde(ideVersion)
+ bundledModulesSince(ideVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(pythonPlugin)
testIntellijPlugins(tomlPlugin)
diff --git a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyCourseProjectGenerator.kt b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyCourseProjectGenerator.kt
index 6583b3852..caec582a6 100644
--- a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyCourseProjectGenerator.kt
+++ b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyCourseProjectGenerator.kt
@@ -1,7 +1,6 @@
package org.hyperskill.academy.python.learning.newproject
import com.intellij.openapi.application.WriteAction
-import com.intellij.openapi.application.invokeAndWaitIfNeeded
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
@@ -31,22 +30,12 @@ open class PyCourseProjectGenerator(
onConfigurationFinished: () -> Unit
) {
var sdk = projectSettings.sdk
- if (sdk is PySdkToInstall) {
- val selectedSdk = sdk
-
- @Suppress("UnstableApiUsage")
- val installedSdk = invokeAndWaitIfNeeded {
- selectedSdk.install(null) {
- detectSystemWideSdks(null, emptyList())
- }.getOrElse {
- LOG.warn(it)
- null
- }
- }
- if (installedSdk != null) {
- createAndAddVirtualEnv(project, projectSettings, installedSdk)
- sdk = projectSettings.sdk
- }
+ // Platform-dependent: installs Python first if the selected SDK is an "install Python" suggestion.
+ // See `installSdkIfSuggested` implementations in `branches//src`.
+ val installedSdk = installSdkIfSuggested(sdk)
+ if (installedSdk != null) {
+ createAndAddVirtualEnv(project, projectSettings, installedSdk)
+ sdk = projectSettings.sdk
}
else if (sdk is PySdkToCreateVirtualEnv) {
val homePath = sdk.homePath ?: error("Home path is not passed during fake python sdk creation")
@@ -66,7 +55,7 @@ open class PyCourseProjectGenerator(
super.afterProjectGenerated(project, projectSettings, openCourseParams, onConfigurationFinished)
}
- private fun createAndAddVirtualEnv(project: Project, settings: PyProjectSettings, baseSdk: PyDetectedSdk) {
+ private fun createAndAddVirtualEnv(project: Project, settings: PyProjectSettings, baseSdk: Sdk) {
val virtualEnvPath = project.basePath + "/.idea/VirtualEnvironment"
val existingSdks = PyConfigurableInterpreterList.getInstance(null).allPythonSdks
val module = ModuleManager.getInstance(project).sortedModules.firstOrNull()
diff --git a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyLanguageSettings.kt b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyLanguageSettings.kt
index 95ec51443..2c6745097 100644
--- a/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyLanguageSettings.kt
+++ b/intellij-plugin/hs-Python/src/org/hyperskill/academy/python/learning/newproject/PyLanguageSettings.kt
@@ -170,8 +170,9 @@ open class PyLanguageSettings : LanguageSettings() {
is PyDetectedSdk -> {
// PyDetectedSdk has empty `sdk.versionString`, so we should manually get language level from homePath if it exists
+ // `PythonSdkFlavor.getFlavor` is used instead of the `sdkFlavor` extension because the latter is internal since 262
homePath?.let {
- sdkFlavor.getLanguageLevel(it)
+ PythonSdkFlavor.getFlavor(this)?.getLanguageLevel(it)
} ?: LanguageLevel.getDefault()
}
@@ -179,7 +180,7 @@ open class PyLanguageSettings : LanguageSettings() {
val flavor = PythonSdkFlavor.getFlavor(this)
homePath?.let {
flavor?.getLanguageLevel(it)
- } ?: LanguageLevel.getDefault()
+ } ?: versionString?.let(LanguageLevel::fromPythonVersion) ?: LanguageLevel.getDefault()
}
}
}
diff --git a/intellij-plugin/hs-Rust/build.gradle.kts b/intellij-plugin/hs-Rust/build.gradle.kts
index 6a08a4ca4..6b670c449 100644
--- a/intellij-plugin/hs-Rust/build.gradle.kts
+++ b/intellij-plugin/hs-Rust/build.gradle.kts
@@ -6,6 +6,7 @@ dependencies {
intellijPlatform {
val ideVersion = if (!isIdeaIDE && !isClionIDE) ideaVersion else baseVersion
intellijIde(ideVersion)
+ bundledModulesSince(ideVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(rustPlugins)
}
diff --git a/intellij-plugin/hs-Scala/build.gradle.kts b/intellij-plugin/hs-Scala/build.gradle.kts
index 17403eb7b..fc6e251b1 100644
--- a/intellij-plugin/hs-Scala/build.gradle.kts
+++ b/intellij-plugin/hs-Scala/build.gradle.kts
@@ -5,6 +5,7 @@ plugins {
dependencies {
intellijPlatform {
intellijIde(ideaVersion)
+ bundledModulesSince(ideaVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(jvmPlugins)
intellijPlugins(scalaPlugin)
diff --git a/intellij-plugin/hs-core/branches/262/resources/META-INF/platform-modules.xml b/intellij-plugin/hs-core/branches/262/resources/META-INF/platform-modules.xml
new file mode 100644
index 000000000..fc67c8504
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/resources/META-INF/platform-modules.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt
new file mode 100644
index 000000000..65feb01b2
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeDialogCustomizerCompat.kt
@@ -0,0 +1,5 @@
+package org.hyperskill.academy.platform
+
+import com.intellij.openapi.vcs.merge.MergeDialogCustomizer
+
+abstract class MergeDialogCustomizerCompat : MergeDialogCustomizer()
diff --git a/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt
new file mode 100644
index 000000000..75c32e461
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/MergeModelBaseCompat.kt
@@ -0,0 +1,12 @@
+package org.hyperskill.academy.platform
+
+import com.intellij.diff.merge.MergeModelBase
+import com.intellij.openapi.editor.Document
+import com.intellij.openapi.project.Project
+
+abstract class MergeModelBaseCompat(
+ project: Project,
+ document: Document,
+) : MergeModelBase(project, document) {
+ override fun onChangeUpdated(index: Int) {}
+}
diff --git a/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt
new file mode 100644
index 000000000..28cc744d0
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/src/org/hyperskill/academy/platform/OpenProjectTaskCompat.kt
@@ -0,0 +1,52 @@
+package org.hyperskill.academy.platform
+
+import com.intellij.ide.impl.OpenProjectTask
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.project.Project
+
+/**
+ * Compatibility helper to construct OpenProjectTask for 253+.
+ */
+object OpenProjectTaskCompat {
+
+ @JvmStatic
+ fun buildForOpen(
+ forceOpenInNewFrame: Boolean,
+ isNewProject: Boolean,
+ isProjectCreatedWithWizard: Boolean,
+ runConfigurators: Boolean,
+ projectName: String?,
+ projectToClose: Project?,
+ beforeInit: ((Project) -> Unit)? = null,
+ preparedToOpen: ((Project, Module) -> Unit)? = null
+ ): OpenProjectTask {
+ return OpenProjectTask(
+ forceOpenInNewFrame = forceOpenInNewFrame,
+ forceReuseFrame = false,
+ projectToClose = projectToClose,
+ isNewProject = isNewProject,
+ useDefaultProjectAsTemplate = true,
+ project = null,
+ projectName = projectName,
+ showWelcomeScreen = true,
+ callback = null,
+ line = -1,
+ column = -1,
+ isRefreshVfsNeeded = false,
+ runConfigurators = runConfigurators,
+ runConversionBeforeOpen = true,
+ projectWorkspaceId = null,
+ projectFrameTypeId = null,
+ isProjectCreatedWithWizard = isProjectCreatedWithWizard,
+ preloadServices = false,
+ beforeInit = if (beforeInit != null) { { beforeInit(it) } } else null,
+ beforeOpen = null,
+ preparedToOpen = if (preparedToOpen != null) { { preparedToOpen(it.project, it) } } else null,
+ preventIprLookup = false,
+ processorChooser = null,
+ implOptions = null,
+ projectRootDir = null,
+ createModule = true
+ )
+ }
+}
diff --git a/intellij-plugin/hs-core/branches/262/testSrc/org/hyperskill/academy/learning/compatibilityUtils.kt b/intellij-plugin/hs-core/branches/262/testSrc/org/hyperskill/academy/learning/compatibilityUtils.kt
new file mode 100644
index 000000000..56b22a27d
--- /dev/null
+++ b/intellij-plugin/hs-core/branches/262/testSrc/org/hyperskill/academy/learning/compatibilityUtils.kt
@@ -0,0 +1,23 @@
+package org.hyperskill.academy.learning
+
+import com.intellij.ide.plugins.IdeaPluginDescriptorImpl
+import com.intellij.ide.plugins.PluginMainDescriptor
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+
+internal fun collectFromModules(
+ pluginDescriptor: IdeaPluginDescriptorImpl,
+ collect: (moduleDescriptor: IdeaPluginDescriptorImpl) -> Unit
+) {
+ // In 2025.3, contentModules is only available on PluginMainDescriptor
+ if (pluginDescriptor is PluginMainDescriptor) {
+ for (module in pluginDescriptor.contentModules) {
+ (module as? IdeaPluginDescriptorImpl)?.let { collect(it) }
+ }
+ }
+}
+
+// In 2025.3, ActionUtil.updateAction was removed. Use action.update(e) directly.
+internal fun updateAction(action: AnAction, e: AnActionEvent) {
+ action.update(e)
+}
diff --git a/intellij-plugin/hs-core/build.gradle.kts b/intellij-plugin/hs-core/build.gradle.kts
index 1124b71e2..2c2c383b2 100644
--- a/intellij-plugin/hs-core/build.gradle.kts
+++ b/intellij-plugin/hs-core/build.gradle.kts
@@ -21,6 +21,14 @@ dependencies {
intellijPlatform {
intellijIde(baseVersion)
bundledModules("intellij.platform.vcs.impl")
+ bundledModulesSince(
+ baseVersion, 262,
+ "intellij.platform.smRunner",
+ "intellij.platform.testRunner",
+ "intellij.platform.ui.jcef",
+ "intellij.libraries.jcef",
+ "intellij.platform.sqlite",
+ )
testIntellijPlugins(commonTestPlugins)
testIntellijPlatformFramework(project)
}
diff --git a/intellij-plugin/hs-core/resources/META-INF/hs-core.xml b/intellij-plugin/hs-core/resources/META-INF/hs-core.xml
index 1003f4ce2..a64dd0a17 100644
--- a/intellij-plugin/hs-core/resources/META-INF/hs-core.xml
+++ b/intellij-plugin/hs-core/resources/META-INF/hs-core.xml
@@ -6,6 +6,19 @@
+
+ com.intellij.modules.jcef
+
+
+
+
+
+
messages.EduCoreBundle
diff --git a/intellij-plugin/hs-core/resources/META-INF/jcef-support.xml b/intellij-plugin/hs-core/resources/META-INF/jcef-support.xml
new file mode 100644
index 000000000..e9f4a7dec
--- /dev/null
+++ b/intellij-plugin/hs-core/resources/META-INF/jcef-support.xml
@@ -0,0 +1 @@
+
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/EduSettings.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/EduSettings.kt
index 3387f761d..978134eee 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/EduSettings.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/EduSettings.kt
@@ -14,6 +14,17 @@ import org.hyperskill.academy.learning.stepik.StepikUser
import org.hyperskill.academy.learning.stepik.StepikUserInfo
import org.jdom.Element
+// Since 2026.2 JCEF is provided by the separate user-disableable `com.intellij.modules.jcef` plugin,
+// so its classes may be completely missing from the classpath
+fun isJCEFSupported(): Boolean {
+ return try {
+ JBCefApp.isSupported()
+ }
+ catch (e: LinkageError) {
+ false
+ }
+}
+
@State(name = "HsSettings", storages = [Storage("hyperskill.xml")])
class EduSettings : PersistentStateComponent {
@Transient
@@ -80,7 +91,7 @@ class EduSettings : PersistentStateComponent {
}
private fun initialJavaUiLibrary(): JavaUILibrary {
- return if (JBCefApp.isSupported()) {
+ return if (isJCEFSupported()) {
JavaUILibrary.JCEF
}
else {
diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/SwitchTaskPanelAction.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/SwitchTaskPanelAction.kt
index 9499ad974..edb5755d4 100644
--- a/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/SwitchTaskPanelAction.kt
+++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/learning/actions/SwitchTaskPanelAction.kt
@@ -8,12 +8,12 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.ui.dsl.builder.*
-import com.intellij.ui.jcef.JBCefApp
import org.hyperskill.academy.learning.EduSettings
import org.hyperskill.academy.learning.EduUtilsKt.isEduProject
import org.hyperskill.academy.learning.JavaUILibrary
import org.hyperskill.academy.learning.JavaUILibrary.JCEF
import org.hyperskill.academy.learning.JavaUILibrary.SWING
+import org.hyperskill.academy.learning.isJCEFSupported
import org.hyperskill.academy.learning.messages.EduCoreBundle
import org.hyperskill.academy.learning.taskToolWindow.ui.TaskToolWindowFactory
import org.hyperskill.academy.learning.taskToolWindow.ui.TaskToolWindowView
@@ -69,7 +69,7 @@ class SwitchTaskPanelAction : DumbAwareAction(EduCoreBundle.lazyMessage("action.
private fun collectAvailableJavaUiLibraries(): List {
val availableJavaUiLibraries = mutableListOf(SWING)
- if (JBCefApp.isSupported()) {
+ if (isJCEFSupported()) {
availableJavaUiLibraries += JCEF
}
return availableJavaUiLibraries
diff --git a/intellij-plugin/hs-jvm-core/build.gradle.kts b/intellij-plugin/hs-jvm-core/build.gradle.kts
index 7a8120ee9..d94f159e5 100644
--- a/intellij-plugin/hs-jvm-core/build.gradle.kts
+++ b/intellij-plugin/hs-jvm-core/build.gradle.kts
@@ -7,6 +7,7 @@ plugins {
dependencies {
intellijPlatform {
intellijIde(ideaVersion)
+ bundledModulesSince(ideaVersion, 262, "intellij.platform.smRunner", "intellij.platform.testRunner")
intellijPlugins(jvmPlugins)
testIntellijPlatformFramework(project, TestFrameworkType.Plugin.Java)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 378bd9a76..a98920dbb 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,16 @@
+pluginManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ maven("https://oss.sonatype.org/content/repositories/snapshots/")
+ }
+}
+
+plugins {
+ // Allows Gradle to automatically download JDKs required by toolchains (e.g. JDK 25 for platform 2026.2+)
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
+
rootProject.name = "hyperskill-plugin"
include(
"hs-edu-format",
@@ -38,11 +51,3 @@ buildCache {
directory = File(rootDir, ".gradle/build-cache")
}
}
-
-pluginManagement {
- repositories {
- mavenCentral()
- gradlePluginPortal()
- maven("https://oss.sonatype.org/content/repositories/snapshots/")
- }
-}