From 2192a0d35b65623cef16e7ee6bb0eda407e450a2 Mon Sep 17 00:00:00 2001 From: Nikolay Chashnikov Date: Wed, 22 May 2024 11:19:54 +0200 Subject: [PATCH] [dev launcher] provide a way to locate resource files in module sources in dev launcher (RDCT-1405) To determine which modules should be compiled in IntellijDevLauncher, we need to load product-modules.xml and plugin.xml files. Currently, they are loaded via RuntimeModuleRepository from output directories, so obsolete variant may be loaded or file may not be found at all if modules containing these files aren't compiled yet. This change introduces the ModuleResourceFileFinder class, which can locate resource files in source directories instead. It doesn't use existing functionality for that to avoid adding additional modules to the system classloader. Also, it parses necessary *.iml files only to speed up the process. GitOrigin-RevId: d55083ba879a3ae8c7985ba6e5f0211ad3062959 --- .idea/modules.xml | 1 + intellij.idea.community.main.iml | 1 + .../src/ModuleResourceFileFinder.kt | 102 ++++++++++++++++++ .../intellij.tools.devLauncher.tests.iml | 16 +++ .../.idea/modules.xml | 10 ++ .../additional-resources/c.txt | 0 .../moduleResourceFinderProject/root.iml | 10 ++ .../simple/resources/a.txt | 0 .../simple/simple.iml | 13 +++ .../simple/src/b/b.txt | 0 .../simple/testSrc/c.txt | 0 .../withPrefix/resources/a.txt | 0 .../withPrefix/src/b/b.txt | 0 .../withPrefix/withPrefix.iml | 15 +++ .../testSrc/ModuleResourceFileFinderTest.kt | 39 +++++++ 15 files changed, 207 insertions(+) create mode 100644 tools/devLauncher/src/ModuleResourceFileFinder.kt create mode 100644 tools/devLauncher/tests/intellij.tools.devLauncher.tests.iml create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/.idea/modules.xml create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/additional-resources/c.txt create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/root.iml create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/resources/a.txt create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/simple.iml create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/src/b/b.txt create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/testSrc/c.txt create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/resources/a.txt create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/src/b/b.txt create mode 100644 tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/withPrefix.iml create mode 100644 tools/devLauncher/tests/testSrc/ModuleResourceFileFinderTest.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 0c5456a6960e..38d7655e9578 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -942,6 +942,7 @@ + diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml index bcdbb2d13d51..6abb38864e90 100644 --- a/intellij.idea.community.main.iml +++ b/intellij.idea.community.main.iml @@ -208,6 +208,7 @@ + diff --git a/tools/devLauncher/src/ModuleResourceFileFinder.kt b/tools/devLauncher/src/ModuleResourceFileFinder.kt new file mode 100644 index 000000000000..3c8cabee2eb2 --- /dev/null +++ b/tools/devLauncher/src/ModuleResourceFileFinder.kt @@ -0,0 +1,102 @@ +package com.intellij.tools.devLauncher + +import java.nio.file.Path +import javax.xml.stream.XMLInputFactory +import javax.xml.stream.XMLStreamConstants +import kotlin.io.path.* + +/** + * Provides a way to locate a resource file under source roots by its relative path in an IntelliJ project. + */ +class ModuleResourceFileFinder(private val projectDir: Path) { + private val moduleFiles: Map + + init { + val modulesXmlFile = projectDir.resolve(".idea/modules.xml") + require(modulesXmlFile.exists()) { ".idea/modules.xml not found in $projectDir"} + modulesXmlFile.inputStream().buffered().use { input -> + val moduleFilesMap = LinkedHashMap() + val reader = XMLInputFactory.newDefaultFactory().createXMLStreamReader(input) + while (reader.hasNext()) { + val event = reader.next() + if (event == XMLStreamConstants.START_ELEMENT && reader.localName == "module") { + val attributeName = reader.getAttributeLocalName(0) + require(attributeName == "fileurl") { "Unexpected first attribute in 'module' tag: $attributeName"} + val imlUrl = reader.getAttributeValue(0) + val prefix = "file://${'$'}PROJECT_DIR${'$'}/" + require(imlUrl.startsWith(prefix)) { "Unexpected format of URL: $imlUrl"} + val imlPath = projectDir.resolve(imlUrl.removePrefix(prefix)) + val fileName = imlPath.name + val suffix = ".iml" + require(fileName.endsWith(suffix)) { "Unexpected file extension in file path $imlPath" } + val moduleName = fileName.removeSuffix(suffix) + moduleFilesMap[moduleName] = imlPath + } + } + moduleFiles = moduleFilesMap + } + } + + fun findResourceFile(moduleName: String, relativePath: String): Path? { + for ((prefix, rootPath) in loadRootsWithPrefixes(moduleName)) { + val relativePathWithoutPrefix = when { + prefix == null -> relativePath + relativePath.startsWith("$prefix/") -> relativePath.removePrefix("$prefix/") + else -> continue + } + val file = rootPath.resolve(relativePathWithoutPrefix) + if (file.exists()) { + return file + } + } + return null + } + + private fun loadRootsWithPrefixes(moduleName: String): List> { + val imlPath = moduleFiles[moduleName] ?: error("Cannot find module '$moduleName' in project '$projectDir'") + require(imlPath.exists()) { "Module file $imlPath doesn't exist" } + val moduleDir = imlPath.parent + val rootsWithPrefixes = ArrayList>() + imlPath.inputStream().buffered().use { input -> + val reader = XMLInputFactory.newDefaultFactory().createXMLStreamReader(input) + while (reader.hasNext()) { + val event = reader.next() + if (event == XMLStreamConstants.START_ELEMENT && reader.localName == "sourceFolder") { + require(reader.attributeCount > 1) { "At least two attributes expected in 'sourceFolder' tag in $imlPath, but ${reader.attributeCount} found" } + val attributeName = reader.getAttributeLocalName(0) + require(attributeName == "url") { "Unexpected first attribute in 'sourceFolder' tag in $imlPath: $attributeName" } + val rootUrl = reader.getAttributeValue(0) + val prefix = "file://" + require(rootUrl.startsWith(prefix)) { "Unexpected format of URL: $rootUrl" } + val rootPath = Path(rootUrl.removePrefix(prefix).replace("${'$'}MODULE_DIR${'$'}", moduleDir.pathString)) + + val typeAttributeName = reader.getAttributeLocalName(1) + val typeAttributeValue = reader.getAttributeValue(1) + val directoryPrefix = when { + typeAttributeName == "isTestSource" && typeAttributeValue == "false" -> { + if (reader.attributeCount > 2 && reader.getAttributeLocalName(2) == "packagePrefix") { + reader.getAttributeValue(2).replace('.', '/').takeIf { it.isNotEmpty() } + } + else { + null + } + } + typeAttributeName == "type" && typeAttributeValue == "java-resource" -> { + if (reader.attributeCount > 2 && reader.getAttributeLocalName(2) == "relativeOutputPath") { + reader.getAttributeValue(2).removeSuffix("/").takeIf { it.isNotEmpty() } + } + else { + null + } + } + else -> { + continue + } + } + rootsWithPrefixes.add(directoryPrefix to rootPath) + } + } + } + return rootsWithPrefixes + } +} \ No newline at end of file diff --git a/tools/devLauncher/tests/intellij.tools.devLauncher.tests.iml b/tools/devLauncher/tests/intellij.tools.devLauncher.tests.iml new file mode 100644 index 000000000000..9cc2e470b043 --- /dev/null +++ b/tools/devLauncher/tests/intellij.tools.devLauncher.tests.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/.idea/modules.xml b/tools/devLauncher/tests/testData/moduleResourceFinderProject/.idea/modules.xml new file mode 100644 index 000000000000..48cfda831550 --- /dev/null +++ b/tools/devLauncher/tests/testData/moduleResourceFinderProject/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/additional-resources/c.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/additional-resources/c.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/root.iml b/tools/devLauncher/tests/testData/moduleResourceFinderProject/root.iml new file mode 100644 index 000000000000..768559d7e3ef --- /dev/null +++ b/tools/devLauncher/tests/testData/moduleResourceFinderProject/root.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/resources/a.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/resources/a.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/simple.iml b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/simple.iml new file mode 100644 index 000000000000..b9de36531b8b --- /dev/null +++ b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/simple.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/src/b/b.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/src/b/b.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/testSrc/c.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/simple/testSrc/c.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/resources/a.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/resources/a.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/src/b/b.txt b/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/src/b/b.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/withPrefix.iml b/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/withPrefix.iml new file mode 100644 index 000000000000..a574e901b2f2 --- /dev/null +++ b/tools/devLauncher/tests/testData/moduleResourceFinderProject/withPrefix/withPrefix.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/devLauncher/tests/testSrc/ModuleResourceFileFinderTest.kt b/tools/devLauncher/tests/testSrc/ModuleResourceFileFinderTest.kt new file mode 100644 index 000000000000..2bc342d0b7ca --- /dev/null +++ b/tools/devLauncher/tests/testSrc/ModuleResourceFileFinderTest.kt @@ -0,0 +1,39 @@ +package com.intellij.tools.devLauncher + +import com.intellij.openapi.application.ex.PathManagerEx +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.invariantSeparatorsPathString +import kotlin.io.path.relativeTo + +class ModuleResourceFileFinderTest { + private lateinit var projectDir: Path + private lateinit var finder: ModuleResourceFileFinder + + @BeforeEach + fun setUp() { + projectDir = Path(PathManagerEx.getCommunityHomePath()).resolve("tools/devLauncher/tests/testData/moduleResourceFinderProject") + finder = ModuleResourceFileFinder(projectDir) + } + + @Test + fun `simple roots`() { + assertPath("simple/resources/a.txt", finder.findResourceFile("simple", "a.txt")) + assertPath("simple/src/b/b.txt", finder.findResourceFile("simple", "b/b.txt")) + assertPath(null, finder.findResourceFile("simple", "c.txt")) + } + + @Test + fun `roots with prefixes`() { + assertPath("withPrefix/resources/a.txt", finder.findResourceFile("withPrefix", "prefix1/a.txt")) + assertPath("withPrefix/src/b/b.txt", finder.findResourceFile("withPrefix", "prefix2/b/b.txt")) + assertPath("additional-resources/c.txt", finder.findResourceFile("withPrefix", "prefix3/c.txt")) + } + + private fun assertPath(expectedPath: String?, file: Path?) { + assertEquals(expectedPath, file?.relativeTo(projectDir)?.invariantSeparatorsPathString) + } +} \ No newline at end of file