From 25022619a39ad2ea26aee75c2cc094a2d67c23b8 Mon Sep 17 00:00:00 2001 From: Murhu Markus Date: Tue, 26 May 2026 11:45:14 +0300 Subject: [PATCH 1/2] Fix scala plugin REPL incompability; separately download and configure REPL for scala >= 3.8 --- gradle.properties | 8 +- .../apluscourses/model/component/Component.kt | 24 +++++ .../apluscourses/model/component/ScalaSdk.kt | 101 +++++++++++++----- 3 files changed, 101 insertions(+), 32 deletions(-) diff --git a/gradle.properties b/gradle.properties index 22dbc5acd..b512708b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,14 +5,14 @@ pluginRepositoryUrl=https://github.com/Aalto-LeTech/aplus-courses # SemVer format -> https://semver.org pluginVersion=4.4.1 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild=252 +pluginSinceBuild=253 # pluginUntilBuild=243.* # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension -platformType=IC -platformVersion=2025.2 +platformType=IU +platformVersion=2025.3 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP -platformPlugins=org.intellij.scala:2025.2.26 +platformPlugins=org.intellij.scala:2025.3.26 platformBundledPlugins=com.intellij.java # Gradle Releases -> https://github.com/gradle/gradle/releases gradleVersion=8.13 diff --git a/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Component.kt b/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Component.kt index c2b55f28f..a1aef3be6 100644 --- a/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Component.kt +++ b/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Component.kt @@ -2,7 +2,13 @@ package fi.aalto.cs.apluscourses.model.component import com.intellij.openapi.project.Project import fi.aalto.cs.apluscourses.services.CoursesClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Files import java.nio.file.Path +import java.nio.file.StandardCopyOption +import kotlin.io.path.createTempFile +import kotlin.io.path.nameWithoutExtension abstract class Component(val name: String, protected val project: Project) { var dependencyNames: Set? = null @@ -24,6 +30,24 @@ abstract class Component(val name: String, protected val project: Project) { CoursesClient.getInstance(project).downloadAndUnzip(zipUrl, extractPath, onlyPath) } + protected suspend fun downloadFile(url: String, target: Path): Path = withContext(Dispatchers.IO) { + Files.createDirectories(target.parent) + val tmp = Files.createTempFile( + target.parent, + target.fileName.toString(), + ".part" + ) + try { + CoursesClient.getInstance(project).download(url, tmp.toFile()) + Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) + } catch (t: Throwable) { + try { + Files.deleteIfExists(tmp) + } catch (_: Throwable) { } + throw t + } + } + protected abstract fun findDependencies(): Set abstract val platformObject: T? diff --git a/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/ScalaSdk.kt b/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/ScalaSdk.kt index db4c6b820..629894007 100644 --- a/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/ScalaSdk.kt +++ b/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/ScalaSdk.kt @@ -16,7 +16,10 @@ import org.jetbrains.plugins.scala.project.ScalaLanguageLevel import org.jetbrains.plugins.scala.project.ScalaLibraryProperties import org.jetbrains.plugins.scala.project.ScalaLibraryPropertiesState import org.jetbrains.plugins.scala.project.ScalaLibraryType +import java.nio.file.Files import java.nio.file.Path +import kotlin.collections.toTypedArray +import kotlin.io.path.isDirectory class ScalaSdk(private val scalaVersion: String, project: Project) : Library(scalaVersion, project) { @NonNls @@ -29,7 +32,6 @@ class ScalaSdk(private val scalaVersion: String, project: Project) : Library(sca CoursesLogger.info("Downloading Scala SDK $scalaVersion") val zipUrl = "https://github.com/lampepfl/dotty/releases/download/$versionNumber/scala3-$versionNumber.zip" - val sourcesUrl = "https://github.com/scala/scala3/archive/refs/tags/$versionNumber.zip" val path = libPath downloadAndUnzipZip(zipUrl, path) @@ -43,10 +45,31 @@ class ScalaSdk(private val scalaVersion: String, project: Project) : Library(sca val library = libraryTable.createLibrary(name, kind) val compilerClasspath = sdkPath.toFile().walkTopDown() - val scala2Version = - compilerClasspath.find { it.name.startsWith("scala-library") && it.extension == "jar" }?.nameWithoutExtension?.substringAfter( - "scala-library-" - ) + val libDir = sdkPath.resolve("lib") + val m2Dir = sdkPath.resolve("maven2") + val scala3Ver = versionNumber + + fun parseVersion(s: String) = s.split('.', '-', '_').mapNotNull { it.toIntOrNull() }.let { + Triple(it.getOrElse(0) { 0 }, it.getOrElse(1) { 0 }, it.getOrElse(2) { 0 }) + } + + fun isScala38Plus(ver: String): Boolean { + val (maj, min, _) = parseVersion(ver) + return maj > 3 || (maj == 3 && min >= 8) + } + + // Download REPL if scala ver >= 3.8.0 + // Unsure if actually required since this seems to be included as a dependency of the scala sdk and + // found under the maven2 directory, but better be safe than sorry + val replPath = libDir.resolve("scala3-repl_3-$scala3Ver.jar") + var replClasspath = emptyArray() + if (isScala38Plus(scala3Ver)) { + replClasspath = + compilerClasspath.filter { it.extension == "jar" }.map { it.toString() }.toList().toTypedArray() + val replUrl = + "https://repo1.maven.org/maven2/org/scala-lang/scala3-repl_3/$scala3Ver/scala3-repl_3-$scala3Ver.jar" + downloadFile(replUrl, replPath) + } edtWriteAction { val libraryModel = library.modifiableModel @@ -61,7 +84,7 @@ class ScalaSdk(private val scalaVersion: String, project: Project) : Library(sca LocalFileSystem.getInstance().protocol, FileUtil.toSystemDependentName(it.toString()) ) - }).toTypedArray(), emptyArray(), null + }).toTypedArray(), emptyArray(), null, replClasspath ) properties.loadState(newState) libraryEx.properties = properties @@ -77,40 +100,62 @@ class ScalaSdk(private val scalaVersion: String, project: Project) : Library(sca OrderRootType.CLASSES ) + if (isScala38Plus(scala3Ver)) { + newLibraryModel.addRoot( + VfsUtil.getUrlForLibraryRoot(compilerClasspath.find { it.name.startsWith("scala3-repl") && it.extension == "jar" }!!), + OrderRootType.CLASSES + ) + } + + newLibraryModel.commit() libraryTable.commit() VirtualFileManager.getInstance().syncRefresh() } + val stdlibVer: String = if (isScala38Plus(scala3Ver)) { + scala3Ver + } else { + fun findStdlibUnder(root: Path): String? { + if (!Files.exists(root) || !root.isDirectory()) return null + return Files.walk(root).use { stream -> + stream.filter { Files.isRegularFile(it) } + .map { it.fileName.toString() } + .filter { it.startsWith("scala-library-") && it.endsWith(".jar") } + .map { it.removeSuffix(".jar").substringAfter("scala-library-") } + .filter { it.startsWith("2.") } + .findFirst() + .orElse(null) + } + } + findStdlibUnder(libDir) + ?: findStdlibUnder(m2Dir) + ?: error("scala-library 2.x jar not found under $libDir or $m2Dir for Scala $scala3Ver") + } - val scala3SourcesPath = path.resolve("scala3-$versionNumber").resolve("src") - downloadAndUnzipZip( - sourcesUrl, - scala3SourcesPath, - "scala3-$versionNumber/library/src/" + val scala3LibSources = libDir.resolve("scala3-library_3-$scala3Ver-sources.jar") + downloadFile( + "https://repo1.maven.org/maven2/org/scala-lang/scala3-library_3/$scala3Ver/scala3-library_3-$scala3Ver-sources.jar", + scala3LibSources ) + val scalaStdlibSources = libDir.resolve("scala-library-$stdlibVer-sources.jar") + downloadFile( + "https://repo1.maven.org/maven2/org/scala-lang/scala-library/$stdlibVer/scala-library-$stdlibVer-sources.jar", + scalaStdlibSources + ) - val scala2SourcesUrl = "https://github.com/scala/scala/archive/refs/tags/v$scala2Version.zip" - fullPath.resolve("src").resolve("scala-$scala2Version") - downloadAndUnzipZip(scala2SourcesUrl, scala3SourcesPath, "scala-$scala2Version/src/library/") + // Attach sources jars to the library edtWriteAction { val libraryModel = library.modifiableModel - libraryModel.addRoot( - VfsUtil.getUrlForLibraryRoot( - path.resolve("scala3-$versionNumber").resolve("src").resolve("scala3-$versionNumber") - .resolve("library").resolve("src") - ), - OrderRootType.SOURCES - ) - libraryModel.addRoot( - VfsUtil.getUrlForLibraryRoot( - path.resolve("scala3-$versionNumber").resolve("src").resolve("scala-$scala2Version") - .resolve("src").resolve("library") - ), - OrderRootType.SOURCES - ) + fun addSourcesJar(path: Path) { + if (Files.exists(path)) { + libraryModel.addRoot(VfsUtil.getUrlForLibraryRoot(path.toFile()), OrderRootType.SOURCES) + } + } + addSourcesJar(scala3LibSources) + addSourcesJar(scalaStdlibSources) libraryModel.commit() VirtualFileManager.getInstance().syncRefresh() } From acf62421832eff329e57ae17f99105479715b111 Mon Sep 17 00:00:00 2001 From: Murhu Markus Date: Thu, 4 Jun 2026 10:39:56 +0300 Subject: [PATCH 2/2] Move downloadFile functionality to CoursesClient --- .../apluscourses/model/component/Component.kt | 18 ++--------------- .../apluscourses/model/component/ScalaSdk.kt | 1 + .../cs/apluscourses/services/CoursesClient.kt | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Component.kt b/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Component.kt index a1aef3be6..844d33bf7 100644 --- a/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Component.kt +++ b/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/Component.kt @@ -30,22 +30,8 @@ abstract class Component(val name: String, protected val project: Project) { CoursesClient.getInstance(project).downloadAndUnzip(zipUrl, extractPath, onlyPath) } - protected suspend fun downloadFile(url: String, target: Path): Path = withContext(Dispatchers.IO) { - Files.createDirectories(target.parent) - val tmp = Files.createTempFile( - target.parent, - target.fileName.toString(), - ".part" - ) - try { - CoursesClient.getInstance(project).download(url, tmp.toFile()) - Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) - } catch (t: Throwable) { - try { - Files.deleteIfExists(tmp) - } catch (_: Throwable) { } - throw t - } + protected suspend fun downloadFile(url: String, target: Path) { + CoursesClient.getInstance(project).downloadFile(url, target) } protected abstract fun findDependencies(): Set diff --git a/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/ScalaSdk.kt b/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/ScalaSdk.kt index 629894007..c28e02f22 100644 --- a/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/ScalaSdk.kt +++ b/src/main/kotlin/fi/aalto/cs/apluscourses/model/component/ScalaSdk.kt @@ -10,6 +10,7 @@ import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.util.application +import fi.aalto.cs.apluscourses.services.CoursesClient import fi.aalto.cs.apluscourses.utils.CoursesLogger import org.jetbrains.annotations.NonNls import org.jetbrains.plugins.scala.project.ScalaLanguageLevel diff --git a/src/main/kotlin/fi/aalto/cs/apluscourses/services/CoursesClient.kt b/src/main/kotlin/fi/aalto/cs/apluscourses/services/CoursesClient.kt index 4791314bd..fdf2a8553 100644 --- a/src/main/kotlin/fi/aalto/cs/apluscourses/services/CoursesClient.kt +++ b/src/main/kotlin/fi/aalto/cs/apluscourses/services/CoursesClient.kt @@ -31,7 +31,9 @@ import org.jetbrains.annotations.NonNls import java.io.File import java.io.IOException import java.nio.channels.WritableByteChannel +import java.nio.file.Files import java.nio.file.Path +import java.nio.file.StandardCopyOption import kotlin.io.path.createTempFile import kotlin.io.path.nameWithoutExtension @@ -140,6 +142,24 @@ class CoursesClient( .copyAndClose(file.also { it.parentFile.mkdirs(); it.createNewFile() }.sinkChannel()) } + suspend fun downloadFile(url: String, target: Path) { + withBackgroundProgress(project, MyBundle.message("aplusCourses")) { + reportSequentialProgress { reporter -> + val tempFile = createTempFile(target.nameWithoutExtension).toFile() + + reporter.indeterminateStep(MyBundle.message("services.progress.downloading", url)) { + download(url, tempFile) + Files.move( + tempFile.toPath(), + target, + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE + ) + } + } + } + } + suspend fun downloadAndUnzip( zipUrl: String, target: Path,