|
| 1 | +package dotty.tools.repl |
| 2 | + |
| 3 | +import scala.language.unsafeNulls |
| 4 | + |
| 5 | +import java.io.File |
| 6 | +import java.net.{URL, URLClassLoader} |
| 7 | +import scala.jdk.CollectionConverters.* |
| 8 | +import scala.util.control.NonFatal |
| 9 | + |
| 10 | +import coursierapi.{Repository, Dependency, MavenRepository} |
| 11 | +import com.virtuslab.using_directives.UsingDirectivesProcessor |
| 12 | +import com.virtuslab.using_directives.custom.model.{Path, StringValue, Value} |
| 13 | + |
| 14 | +/** Handles dependency resolution using Coursier for the REPL */ |
| 15 | +object DependencyResolver: |
| 16 | + |
| 17 | + /** Parse a dependency string of the form `org::artifact:version` or `org:artifact:version` |
| 18 | + * and return the (organization, artifact, version) triple if successful. |
| 19 | + * |
| 20 | + * Supports both Maven-style (single colon) and Scala-style (double colon) notation: |
| 21 | + * - Maven: `com.lihaoyi:scalatags_3:0.13.1` |
| 22 | + * - Scala: `com.lihaoyi::scalatags:0.13.1` (automatically appends _3) |
| 23 | + */ |
| 24 | + def parseDependency(dep: String): Option[(String, String, String)] = |
| 25 | + dep match |
| 26 | + case s"$org::$artifact:$version" => Some((org, s"${artifact}_3", version)) |
| 27 | + case s"$org:$artifact:$version" => Some((org, artifact, version)) |
| 28 | + case _ => |
| 29 | + System.err.println("Unable to parse dependency \"" + dep + "\"") |
| 30 | + None |
| 31 | + |
| 32 | + /** Extract all dependencies from using directives in source code */ |
| 33 | + def extractDependencies(sourceCode: String): List[String] = |
| 34 | + try |
| 35 | + val directives = new UsingDirectivesProcessor().extract(sourceCode.toCharArray) |
| 36 | + val deps = scala.collection.mutable.Buffer[String]() |
| 37 | + |
| 38 | + for |
| 39 | + directive <- directives.asScala |
| 40 | + (path, values) <- directive.getFlattenedMap.asScala |
| 41 | + do |
| 42 | + if path.getPath.asScala.toList == List("dep") then |
| 43 | + values.asScala.foreach { |
| 44 | + case strValue: StringValue => deps += strValue.get() |
| 45 | + case value => System.err.println("Unrecognized directive value " + value) |
| 46 | + } |
| 47 | + else |
| 48 | + System.err.println("Unrecognized directive " + path.getPath) |
| 49 | + |
| 50 | + deps.toList |
| 51 | + catch |
| 52 | + case NonFatal(e) => Nil // If parsing fails, fall back to empty list |
| 53 | + |
| 54 | + /** Resolve dependencies using Coursier Interface and return the classpath as a list of File objects */ |
| 55 | + def resolveDependencies(dependencies: List[(String, String, String)]): Either[String, List[File]] = |
| 56 | + if dependencies.isEmpty then Right(Nil) |
| 57 | + else |
| 58 | + try |
| 59 | + // Add Maven Central and Sonatype repositories |
| 60 | + val repos = Array( |
| 61 | + MavenRepository.of("https://repo1.maven.org/maven2"), |
| 62 | + MavenRepository.of("https://oss.sonatype.org/content/repositories/releases") |
| 63 | + ) |
| 64 | + |
| 65 | + // Create dependency objects |
| 66 | + val deps = dependencies |
| 67 | + .map { case (org, artifact, version) => Dependency.of(org, artifact, version) } |
| 68 | + .toArray |
| 69 | + |
| 70 | + val fetch = coursierapi.Fetch.create() |
| 71 | + .withRepositories(repos*) |
| 72 | + .withDependencies(deps*) |
| 73 | + |
| 74 | + Right(fetch.fetch().asScala.toList) |
| 75 | + |
| 76 | + catch |
| 77 | + case NonFatal(e) => |
| 78 | + Left(s"Failed to resolve dependencies: ${e.getMessage}") |
| 79 | + |
| 80 | + /** Add resolved dependencies to the compiler classpath and classloader. |
| 81 | + * Returns the new classloader. |
| 82 | + * |
| 83 | + * This follows the same pattern as the `:jar` command. |
| 84 | + */ |
| 85 | + def addToCompilerClasspath( |
| 86 | + files: List[File], |
| 87 | + prevClassLoader: ClassLoader, |
| 88 | + prevOutputDir: dotty.tools.io.AbstractFile |
| 89 | + )(using ctx: dotty.tools.dotc.core.Contexts.Context): AbstractFileClassLoader = |
| 90 | + import dotty.tools.dotc.classpath.ClassPathFactory |
| 91 | + import dotty.tools.dotc.core.SymbolLoaders |
| 92 | + import dotty.tools.dotc.core.Symbols.defn |
| 93 | + import dotty.tools.io.* |
| 94 | + import dotty.tools.runner.ScalaClassLoader.fromURLsParallelCapable |
| 95 | + |
| 96 | + // Create a classloader with all the resolved JAR files |
| 97 | + val urls = files.map(_.toURI.toURL).toArray |
| 98 | + val depsClassLoader = new URLClassLoader(urls, prevClassLoader) |
| 99 | + |
| 100 | + // Add each JAR to the compiler's classpath |
| 101 | + for file <- files do |
| 102 | + val jarFile = AbstractFile.getDirectory(file.getAbsolutePath) |
| 103 | + if jarFile != null then |
| 104 | + val jarClassPath = ClassPathFactory.newClassPath(jarFile) |
| 105 | + ctx.platform.addToClassPath(jarClassPath) |
| 106 | + SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath) |
| 107 | + |
| 108 | + // Create new classloader with previous output dir and resolved dependencies |
| 109 | + new dotty.tools.repl.AbstractFileClassLoader( |
| 110 | + prevOutputDir, |
| 111 | + depsClassLoader, |
| 112 | + ctx.settings.XreplInterruptInstrumentation.value |
| 113 | + ) |
| 114 | + |
| 115 | +end DependencyResolver |
0 commit comments