diff --git a/src/main/kotlin/lsifjava/BuildToolInterface.kt b/src/main/kotlin/lsifjava/BuildToolInterface.kt index 3dd08a74..9519c739 100644 --- a/src/main/kotlin/lsifjava/BuildToolInterface.kt +++ b/src/main/kotlin/lsifjava/BuildToolInterface.kt @@ -4,54 +4,86 @@ import org.gradle.tooling.GradleConnector import org.gradle.tooling.model.eclipse.EclipseProject import org.gradle.tooling.model.idea.IdeaProject import org.gradle.tooling.model.idea.IdeaSingleEntryLibraryDependency +import java.io.File import java.nio.file.Path import java.nio.file.Paths interface BuildToolInterface { - fun getClasspaths(): List - fun getSourceDirectories(): List> - fun javaSourceVersions(): List + val classpaths: List + val sourceDirectories: List> + val javaSourceVersions: List } // TODO(nsc) exclusions? lazy eval? class GradleInterface(private val projectDir: CanonicalPath): AutoCloseable, BuildToolInterface { - private val projectConnection by lazy { + private val initScriptName = "projectClasspathFinder.gradle" + + private val artifactPattern by lazy(LazyThreadSafetyMode.NONE) { + "lsifjava (.+)(?:\r?\n)".toRegex() + } + + private val projectConnection by lazy(LazyThreadSafetyMode.NONE) { GradleConnector.newConnector() .forProjectDirectory(projectDir.path.toFile()) .connect() } - private val eclipseModel by lazy { + private val eclipseModel by lazy(LazyThreadSafetyMode.NONE) { projectConnection.getModel(EclipseProject::class.java) } - private val ideaModel by lazy { + private val ideaModel by lazy(LazyThreadSafetyMode.NONE) { projectConnection.getModel(IdeaProject::class.java) } // is this even *correct*? Is the order the same? - override fun getClasspaths(): List { - val eclipseClasspaths = getClasspathsFromEclipse() - return ideaModel.children.mapIndexed { i, it -> - Classpath(it.dependencies - .filterIsInstance() - .map { it.file.canonicalPath } - .toSet() - ) + eclipseClasspaths[i] - } - } + override val classpaths: List get() { + val initScriptClasspath = kotlin.runCatching { getClasspathFromInitScript() }.getOrDefault(Classpath(setOf())) + val eclipseClasspaths = kotlin.runCatching { eclipseClasspath() }.getOrDefault(listOf()) - private fun getClasspathsFromEclipse() = eclipseClasspath(eclipseModel) + val ideaClasspath = kotlin.runCatching { ideaClasspath() }.getOrDefault(listOf()) - private fun eclipseClasspath(project: EclipseProject): List { + return ideaClasspath.mapIndexed {i, it -> it + eclipseClasspaths[i] + initScriptClasspath } + } + + private fun ideaClasspath() = ideaModel.children.map { it -> + Classpath(it.dependencies + .filterIsInstance() + .map { it.file.canonicalPath } + .toSet() + ) + } + + private fun eclipseClasspath(project: EclipseProject = eclipseModel): List { val classPaths = arrayListOf() classPaths += Classpath(project.classpath.map { it.file.canonicalPath }.toSet()) project.children.forEach { eclipseClasspath(it).toCollection(classPaths) } return classPaths } - override fun getSourceDirectories(): List> { + private fun getClasspathFromInitScript(): Classpath { + val config = File.createTempFile("lsifjava", ".gradle") + config.deleteOnExit() + + config.bufferedWriter().use { configWriter -> + ClassLoader.getSystemResourceAsStream(initScriptName)!!.bufferedReader().use { configReader -> + configReader.copyTo(configWriter) + } + } + + // Unix only for now. To be revisited + val (stdout, stderr) = execAndReadStdoutAndStderr("./gradlew -I ${config.absolutePath} lsifjavaAllGradleDeps", projectDir.path) + + val artifacts = artifactPattern.findAll(stdout) + .mapNotNull { it.groups[1] } + .map { Paths.get(it.value).toFile().canonicalPath } + .toSet() + + return Classpath(artifacts) + } + + override val sourceDirectories: List> get() { return ideaModel.children.flatMap { module -> module.contentRoots.map { root -> root.sourceDirectories.map { Paths.get(it.directory.canonicalPath) } + @@ -78,7 +110,7 @@ class GradleInterface(private val projectDir: CanonicalPath): AutoCloseable, Bui return sourceDirs } - override fun javaSourceVersions() = javaSourceVersion(eclipseModel) + override val javaSourceVersions: List get() = javaSourceVersion(eclipseModel) // get rid of String?, use parent version or else fallback private fun javaSourceVersion(project: EclipseProject): List { @@ -89,12 +121,4 @@ class GradleInterface(private val projectDir: CanonicalPath): AutoCloseable, Bui } override fun close() = projectConnection.close() -} - -inline class Classpath(private val classpaths: Set) { - operator fun plus(other: Set) = Classpath(classpaths.union(other)) - - operator fun plus(other: Classpath) = Classpath(classpaths.union(other.classpaths)) - - override fun toString() = classpaths.joinToString(":") } \ No newline at end of file diff --git a/src/main/kotlin/lsifjava/FileCollector.kt b/src/main/kotlin/lsifjava/FileCollector.kt index 9a9511fb..c4f212ae 100644 --- a/src/main/kotlin/lsifjava/FileCollector.kt +++ b/src/main/kotlin/lsifjava/FileCollector.kt @@ -22,8 +22,9 @@ fun buildIndexerMap( ): Map { val indexers = mutableMapOf() - val classpaths = buildToolInterface.getClasspaths() - val sourceVersions = buildToolInterface.javaSourceVersions() + val classpaths = buildToolInterface.classpaths.merge() + + val sourceVersions = buildToolInterface.javaSourceVersions val fileBuildInfo = Channel() @@ -39,21 +40,24 @@ fun buildIndexerMap( indexers, emitter, javacDiagListener, verbose, ) } - }.join() + } } return indexers } +// NOTE: classpaths is the total collection of the found classpaths for all +// sub-projects. This is a bit of a brute force mash together but it appears to please +// the javac private fun CoroutineScope.launchFileTreeWalkers( buildToolInterface: BuildToolInterface, fileBuildInfoChannel: Channel, - classpaths: List, + classpaths: Classpath, sourceVersions: List, ) = launch(Dispatchers.IO) { - buildToolInterface.getSourceDirectories().forEachIndexed { i, paths -> + buildToolInterface.sourceDirectories.forEachIndexed { i, paths -> launch { - val collector = AsyncFileCollector(fileBuildInfoChannel, classpaths[i], sourceVersions[i], this) + val collector = AsyncFileCollector(fileBuildInfoChannel, classpaths, sourceVersions[i], this) paths.asSequence().filter { Files.exists(it) }.forEach { Files.walkFileTree(it, collector) } } } diff --git a/src/main/kotlin/lsifjava/UtilTypes.kt b/src/main/kotlin/lsifjava/UtilTypes.kt index 2ba7501d..b1125aca 100644 --- a/src/main/kotlin/lsifjava/UtilTypes.kt +++ b/src/main/kotlin/lsifjava/UtilTypes.kt @@ -1,5 +1,9 @@ package lsifjava +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.ByteArrayOutputStream import java.io.Writer import java.nio.file.Path @@ -13,3 +17,39 @@ object NoopWriter: Writer() { override fun flush() = Unit override fun write(p0: CharArray, p1: Int, p2: Int) = Unit } + +inline class Classpath(private val classpaths: Set) { + operator fun plus(other: Set) = Classpath(classpaths.union(other)) + + operator fun plus(other: Classpath) = Classpath(classpaths.union(other.classpaths)) + + fun size() = classpaths.size + + override fun toString() = classpaths.joinToString(":") +} + +fun List.merge() = when(this.isEmpty()) { + false -> this.reduce { acc, classpath -> acc + classpath } + else -> { + println("no classpaths were inferred, symbol resolution for external dependencies may fail.") + Classpath(setOf()) + } +} + +fun execAndReadStdoutAndStderr(shellCommand: String, directory: Path): Pair { + val process = Runtime.getRuntime().exec(shellCommand, null, directory.toFile()) + val output = ByteArrayOutputStream().writer() + val errors = ByteArrayOutputStream().writer() + + runBlocking(Dispatchers.IO) { + launch { + process.inputStream.bufferedReader().use { it.copyTo(output) } + } + + launch { + process.errorStream.bufferedReader().use { it.copyTo(errors) } + } + } + + return Pair(output.toString(), errors.toString()) +} \ No newline at end of file diff --git a/src/main/resources/projectClasspathFinder.gradle b/src/main/resources/projectClasspathFinder.gradle new file mode 100644 index 00000000..4dbe1584 --- /dev/null +++ b/src/main/resources/projectClasspathFinder.gradle @@ -0,0 +1,44 @@ +allprojects { project -> + task lsifjavaAllGradleDeps { + doLast { + if (project.hasProperty('android')) { + project.android.getBootClasspath().each { + println "lsifjava $it" + } + if (project.android.hasProperty('applicationVariants')) { + project.android.applicationVariants.all { variant -> + try { + variant.getCompileClasspath().each { + println "lsifjava $it" + } + } catch(ignored){} + } + } + } else { + // Print the list of all dependencies jar files. + project.configurations.findAll { + it.metaClass.respondsTo(it, "isCanBeResolved") ? it.isCanBeResolved() : false + }.each { + it.resolve().each { + def inspected = it.inspect() + + if (inspected.endsWith("jar")) { + if (!inspected.contains("zip!")) { + println "lsifjava $it" + } + } else if (inspected.endsWith("aar")) { + // If the dependency is an AAR file we try to determine the location + // of the classes.jar file in the exploded aar folder. + def splitted = inspected.split("/") + def namespace = splitted[-5] + def name = splitted[-4] + def version = splitted[-3] + def explodedPath = "$project.buildDir/intermediates/exploded-aar/$namespace/$name/$version/jars/classes.jar" + println "lsifjava $explodedPath" + } + } + } + } + } + } +} \ No newline at end of file