summaryrefslogtreecommitdiff
path: root/java/compiler/impl/src/com/intellij/compiler/server/impl/BuildProcessClasspathManager.kt
blob: c06b439b19100bd0fdefb452c625e0b330c24d6a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.compiler.server.impl

import com.intellij.compiler.server.BuildProcessParametersProvider
import com.intellij.compiler.server.CompileServerPlugin
import com.intellij.diagnostic.PluginException
import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.PluginPathManager
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.PathUtil
import com.intellij.util.io.URLUtil
import com.intellij.util.text.VersionComparatorUtil
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
import org.jetbrains.jps.cmdline.ClasspathBootstrap
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.Attributes
import java.util.jar.JarFile

class BuildProcessClasspathManager(parentDisposable: Disposable) {
  @Volatile
  private var compileServerPluginsClasspath: List<String>? = null

  private val lastClasspathLock = Any()
  private var lastRawClasspath: List<String>? = null
  private var lastFilteredClasspath: List<String>? = null

  init {
    CompileServerPlugin.EP_NAME.addChangeListener({ compileServerPluginsClasspath = null }, parentDisposable)
  }

  fun getBuildProcessClasspath(project: Project): List<String> {
    val appClassPath = ClasspathBootstrap.getBuildProcessApplicationClasspath()
    val pluginClassPath = getBuildProcessPluginsClasspath(project)
    val rawClasspath = appClassPath + pluginClassPath
    synchronized(lastClasspathLock) {
      if (rawClasspath != lastRawClasspath) {
        if (LOG.isDebugEnabled) {
          LOG.debug("buildProcessAppClassPath: $appClassPath")
          LOG.debug("buildProcessPluginClassPath: $appClassPath")
        }

        lastRawClasspath = rawClasspath
        lastFilteredClasspath = filterOutOlderVersions(rawClasspath)
        if (LOG.isDebugEnabled && lastRawClasspath != lastFilteredClasspath) {
          LOG.debug("older versions of libraries were removed from classpath:")
          LOG.debug("original classpath: $lastRawClasspath")
          LOG.debug("actual classpath: $lastFilteredClasspath")
        }
      }
      return lastFilteredClasspath!!
    }
  }

  /**
   * For internal use only, use [getBuildProcessClasspath] to get full classpath instead.
   */
  @ApiStatus.Internal
  fun getBuildProcessPluginsClasspath(project: Project): List<String> {
    val dynamicClasspath = BuildProcessParametersProvider.EP_NAME.getExtensions(project).flatMapTo(ArrayList()) { it.classPath }
    return if (dynamicClasspath.isEmpty()) {
      staticClasspath
    }
    else {
      dynamicClasspath.addAll(staticClasspath)
      dynamicClasspath
    }
  }

  private val staticClasspath: List<String>
    get() {
      val classpath = compileServerPluginsClasspath ?: computeCompileServerPluginsClasspath()
      if (compileServerPluginsClasspath == null) {
        compileServerPluginsClasspath = classpath
      }
      return classpath
    }

  companion object {
    private val LOG = logger<BuildProcessClasspathManager>()

    private fun findClassesRoot(relativePath: String, plugin: IdeaPluginDescriptor, baseFile: Path): String? {
      val jarFile = baseFile.resolve("lib/$relativePath")
      if (Files.exists(jarFile)) {
        return jarFile.toString()
      }

      // ... 'plugin run configuration': all module outputs are copied to 'classes' folder
      val classesDir = baseFile.resolve("classes")
      if (Files.isDirectory(classesDir)) {
        return classesDir.toString()
      }

      // development mode
      if (PluginManagerCore.isRunningFromSources()) {
        // ... try "out/classes/production/<module-name>", assuming that JAR name was automatically generated from module name
        val fileName = FileUtilRt.getNameWithoutExtension(PathUtil.getFileName(relativePath))
        val moduleName = OLD_TO_NEW_MODULE_NAME[fileName] ?:
                         //try restoring module name from JAR name automatically generated by BaseLayout.convertModuleNameToFileName
                         ("intellij." + fileName.replace('-', '.'))
        var baseOutputDir = baseFile.parent
        if (baseOutputDir.fileName.toString() == "test") {
          baseOutputDir = baseOutputDir.parent.resolve("production")
        }
        val moduleDir = baseOutputDir.resolve(moduleName)
        if (Files.isDirectory(moduleDir)) {
          return moduleDir.toString()
        }
        // ... try "<plugin-dir>/lib/<jar-name>", assuming that <jar-name> is a module library committed to VCS
        val pluginDir = getPluginDir(plugin)
        if (pluginDir != null) {
          val libraryFile = File(pluginDir, "lib/" + PathUtil.getFileName(relativePath))
          if (libraryFile.exists()) {
            return libraryFile.path
          }
        }
        // ... look for <jar-name> on the classpath, assuming that <jar-name> is an external (read: Maven) library
        try {
          val urls = BuildProcessClasspathManager::class.java.classLoader.getResources(JarFile.MANIFEST_NAME).asSequence()
          val jarPath = urls.mapNotNull { URLUtil.splitJarUrl(it.file)?.first }.firstOrNull { PathUtil.getFileName(it) == relativePath }
          if (jarPath != null) {
            return jarPath
          }
        }
        catch (ignored: IOException) {
        }
      }
      LOG.error(PluginException("Cannot add '" + relativePath + "' from '" + plugin.name + ' ' + plugin.version + "'" + " to compiler classpath", plugin.pluginId))
      return null
    }

    private fun computeCompileServerPluginsClasspath(): List<String> {
      val classpath = ArrayList<String>()
      for (serverPlugin in CompileServerPlugin.EP_NAME.extensionList) {
        val pluginId = serverPlugin.pluginDescriptor.pluginId
        val plugin = PluginManagerCore.getPlugin(pluginId)
        LOG.assertTrue(plugin != null, pluginId)
        val baseFile = plugin!!.pluginPath
        if (Files.isRegularFile(baseFile)) {
          classpath.add(baseFile.toString())
        }
        else {
          StringUtil.split(serverPlugin.classpath, ";").mapNotNullTo(classpath) { findClassesRoot(it, plugin, baseFile)}
        }
      }
      return classpath
    }

    private fun getPluginDir(plugin: IdeaPluginDescriptor): File? {
      val pluginDirName = StringUtil.getShortName(plugin.pluginId.idString)
      val extraDir = System.getProperty("idea.external.build.development.plugins.dir")
      if (extraDir != null) {
        val extraDirFile = File(extraDir, pluginDirName)
        if (extraDirFile.isDirectory) {
          return extraDirFile
        }
      }
      var pluginHome = PluginPathManager.getPluginHome(pluginDirName)
      if (!pluginHome.isDirectory && StringUtil.isCapitalized(pluginDirName)) {
        pluginHome = PluginPathManager.getPluginHome(StringUtil.decapitalize(pluginDirName))
      }
      return if (pluginHome.isDirectory) pluginHome else null
    }

    private fun filterOutOlderVersions(classpath: List<String>): List<String> {
      data class JarInfo(val path: String, val title: String, val version: String)

      fun readTitleAndVersion(path: String): JarInfo? {
        val file = Path.of(path)
        if (!Files.isRegularFile(file) || !FileUtil.extensionEquals(file.fileName.toString(), "jar")) {
          return null
        }

        JarFile(file.toFile()).use {
          val attributes = it.manifest?.mainAttributes ?: return null
          val title = attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE) ?: return null
          val version = attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION) ?: return null
          return JarInfo(path, title, version)
        }
      }

      val jarInfos = classpath.mapNotNull(::readTitleAndVersion)
      val titleToInfo = jarInfos.groupBy { it.title }
      val pathToInfo = jarInfos.associateBy { it.path }
      return classpath.filter { path ->
        val pathInfo = pathToInfo[path] ?: return@filter true
        val sameTitle = titleToInfo[pathInfo.title]
        if (sameTitle == null || sameTitle.size <= 1) return@filter true
        sameTitle.all { VersionComparatorUtil.compare(it.version, pathInfo.version) <= 0 }
      }
    }

    @JvmStatic @TestOnly
    fun filterOutOlderVersionsForTests(classpath: List<String>): List<String> = filterOutOlderVersions(classpath)

    @JvmStatic
    fun getLauncherClasspath(project: Project): List<String> {
      return BuildProcessParametersProvider.EP_NAME.getExtensions(project).flatMap { it.launcherClassPath }
    }

    //todo[nik] this is a temporary compatibility fix; we should update plugin layout so JAR names correspond to module names instead.
    @Suppress("SpellCheckingInspection")
    private val OLD_TO_NEW_MODULE_NAME = hashMapOf(
      "kotlin-jps-plugin" to "kotlin.jps-plugin",
      "kotlin-jps-common" to "kotlin.jps-common",
      "kotlin-common" to "kotlin.common",
    )
  }
}