Skip to content

Commit 7c9e1cd

Browse files
committed
Improve compatibility with several project setups
Switch from CompilerManager to ProjectTaskManager (may increase compile time) Improved mechanism to find .class output path (test & prod) Compatibility with IDEA 2020.1 / Android Studio 3.6.3
1 parent 5df41aa commit 7c9e1cd

File tree

7 files changed

+264
-109
lines changed

7 files changed

+264
-109
lines changed

build.gradle

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ buildscript {
44
}
55

66
dependencies {
7-
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
7+
classpath 'com.github.ben-manes:gradle-versions-plugin:0.28.0'
88
}
99
}
1010

1111
plugins {
12-
id 'org.jetbrains.intellij' version '0.2.17'
13-
id 'org.jetbrains.kotlin.jvm' version '1.2.0'
14-
id 'org.sonarqube' version '2.6.1'
12+
id 'org.jetbrains.intellij' version '0.4.18'
13+
id 'org.jetbrains.kotlin.jvm' version '1.3.71'
14+
id 'org.sonarqube' version '2.8'
1515
}
1616

1717
apply plugin: 'com.github.ben-manes.versions'
@@ -26,14 +26,13 @@ repositories {
2626
dependencies {
2727
compile fileTree(dir: 'lib', include: '*.jar')
2828

29-
compile 'org.jetbrains.kotlin:kotlin-runtime'
30-
compile 'org.jetbrains.kotlin:kotlin-stdlib'
29+
compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
3130

32-
compile 'org.smali:baksmali:2.2.2'
31+
compile 'org.smali:baksmali:2.4.0'
3332
}
3433

3534
intellij {
36-
version = 'IC-143.2370.31'
35+
version = 'IC-2020.1'
3736

3837
downloadSources false
3938
updateSinceUntilBuild false
@@ -46,12 +45,15 @@ intellij {
4645
username 'ollide'
4746
password JETBRAINS_PASSWORD
4847
}
48+
49+
plugins = ['java']
4950
}
5051

5152
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
5253
kotlinOptions {
53-
jvmTarget = '1.6'
54-
apiVersion = '1.0'
54+
jvmTarget = '1.8'
55+
apiVersion = '1.2'
56+
languageVersion = '1.2'
5557
}
5658
}
5759

@@ -66,4 +68,4 @@ sonarqube {
6668
}
6769
}
6870

69-
version = '1.7-SNAPSHOT'
71+
version = '2.0.0-SNAPSHOT'

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists
6-
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

src/main/kotlin/org/ollide/java2smali/CompilerCallback.kt

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package org.ollide.java2smali
2+
3+
import com.intellij.openapi.command.WriteCommandAction
4+
import com.intellij.openapi.diagnostic.Logger
5+
import com.intellij.openapi.fileEditor.OpenFileDescriptor
6+
import com.intellij.openapi.module.Module
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.openapi.roots.*
9+
import com.intellij.openapi.vfs.LocalFileSystem
10+
import com.intellij.openapi.vfs.VirtualFile
11+
import com.intellij.openapi.vfs.VirtualFileManager
12+
import com.intellij.psi.PsiClassOwner
13+
import com.intellij.psi.PsiManager
14+
import com.intellij.task.ProjectTaskManager
15+
import com.intellij.util.SmartList
16+
import com.intellij.util.containers.OrderedSet
17+
import com.intellij.util.io.URLUtil
18+
import java.io.File
19+
import java.io.IOException
20+
import java.nio.file.Paths
21+
22+
class DexCompiler(private val vFile: VirtualFile, private val project: Project, private val module: Module) {
23+
24+
fun run() {
25+
buildModule {
26+
onProjectBuildComplete()
27+
}
28+
}
29+
30+
/**
31+
* To create a dex or smali file from the virtual file, we ne a compiled .class file
32+
* of the given virtual file to be present.
33+
*
34+
* Project structures and builds vary a lot (Android, Java, Kotlin, directory structure),
35+
* so instead of using the CompileManager, we trigger a general build task.
36+
*/
37+
private fun buildModule(callback: () -> Unit) {
38+
val projectTaskManager = ProjectTaskManager.getInstance(project)
39+
val buildTask = projectTaskManager.createModulesBuildTask(module, true, true, true)
40+
41+
val supportProjectTaskManager = SupportProjectTaskManager(projectTaskManager)
42+
supportProjectTaskManager.run(buildTask).onSuccess {
43+
if (it.hasErrors()) {
44+
LOG.warn("Module build failed, aborting dex/smali build.")
45+
} else {
46+
callback()
47+
}
48+
}
49+
}
50+
51+
private fun onProjectBuildComplete() {
52+
val file = PsiManager.getInstance(project).findFile(vFile) as PsiClassOwner
53+
54+
val fileOutputDirectory = getFileOutputDirectory(file)
55+
fileOutputDirectory.refresh(false, false)
56+
57+
val fileName = vFile.nameWithoutExtension
58+
val dexFilePath = Paths.get(fileOutputDirectory.path, fileName + DEX_EXTENSION).toString()
59+
60+
// CLASS -> DEX
61+
val targetFiles = getClassFiles(fileOutputDirectory, fileName)
62+
compileDexFile(targetFiles, dexFilePath)
63+
64+
// DEX -> SMALI
65+
val outputDir = getSourceRootFile().path
66+
WriteCommandAction.runWriteCommandAction(project) {
67+
Dex2SmaliHelper.disassembleDexFile(dexFilePath, outputDir)
68+
69+
// we've created the smali file(s) in our source file's directory
70+
// refresh directory synchronously and access children to let IDEA detect the file(s)
71+
val parent = vFile.parent
72+
parent.refresh(false, false)
73+
parent.children
74+
}
75+
76+
// get a VirtualFile by the IO path
77+
val smaliPath = vFile.path.substringBeforeLast('.') + SMALI_EXTENSION
78+
val virtualDexFile = LocalFileSystem.getInstance().findFileByIoFile(File(smaliPath)) ?: return
79+
80+
// use the VirtualFile to show the smali file in IDEA editor
81+
val openFileDescriptor = OpenFileDescriptor(project, virtualDexFile)
82+
openFileDescriptor.navigate(true)
83+
}
84+
85+
private fun getClassFiles(fileOutputDirectory: VirtualFile, fileName: String): Array<String> {
86+
val children = fileOutputDirectory.children ?: arrayOf()
87+
return children.filter {
88+
val baseName = it.nameWithoutExtension
89+
(baseName == fileName || baseName.startsWith("$fileName$")) && it.extension == CLASS
90+
}.map {
91+
it.path
92+
}.toTypedArray()
93+
}
94+
95+
private fun getFileOutputDirectory(file: PsiClassOwner): VirtualFile {
96+
// determine whether this is a production or test file
97+
val isProduction = module.getModuleScope(false).contains(vFile)
98+
99+
val pkg = file.packageName.replace('.', File.separatorChar)
100+
101+
// find the general output directory of the file's module (target, app/build/intermediates/javac/$variant/classes, ...)
102+
val possibleOutputDirectories = findModuleOutputDirectories(isProduction)
103+
LOG.debug("Possible output directories: ", possibleOutputDirectories.joinToString(","))
104+
105+
val virtualFileManager = VirtualFileManager.getInstance().getFileSystem(URLUtil.FILE_PROTOCOL)
106+
107+
val fileOutputDirectory = possibleOutputDirectories
108+
.asSequence()
109+
.map {
110+
val classFile = vFile.nameWithoutExtension + CLASS_EXTENSION
111+
val path = Paths.get(it, pkg, classFile).toString()
112+
virtualFileManager.refreshAndFindFileByPath(path)?.parent
113+
}
114+
.firstOrNull { it != null }
115+
116+
LOG.debug("Found output directory: $fileOutputDirectory")
117+
return fileOutputDirectory ?: throw IllegalStateException("Output directory not found")
118+
}
119+
120+
/**
121+
* @see <a href="https://github.com/JetBrains/intellij-community/blob/master/java/compiler/openapi/src/com/intellij/openapi/compiler/CompilerPaths.java">intellij-community/CompilerPaths.java</a>
122+
*/
123+
private fun findModuleOutputDirectories(production: Boolean): OrderedSet<String> {
124+
val outputPaths: MutableList<String> = mutableListOf()
125+
126+
val compilerExtension = CompilerModuleExtension.getInstance(module)
127+
if (production) {
128+
compilerExtension?.compilerOutputPath?.path?.let { outputPaths.add(it) }
129+
} else {
130+
compilerExtension?.compilerOutputPathForTests?.path?.let { outputPaths.add(it) }
131+
}
132+
133+
val moduleRootManager = ModuleRootManager.getInstance(module)
134+
for (handlerFactory in OrderEnumerationHandler.EP_NAME.extensions) {
135+
if (handlerFactory.isApplicable(module)) {
136+
val handler = handlerFactory.createHandler(module)
137+
val outputUrls: List<String> = SmartList()
138+
handler.addCustomModuleRoots(OrderRootType.CLASSES, moduleRootManager, outputUrls, production, !production)
139+
for (outputUrl in outputUrls) {
140+
outputPaths.add(VirtualFileManager.extractPath(outputUrl).replace('/', File.separatorChar))
141+
}
142+
}
143+
}
144+
return OrderedSet(outputPaths)
145+
}
146+
147+
private fun compileDexFile(compiledPaths: Array<String>, dexFile: String) {
148+
try {
149+
Class2DexHelper.dexClassFile(compiledPaths, dexFile)
150+
} catch (e: IOException) {
151+
e.printStackTrace()
152+
return
153+
}
154+
}
155+
156+
private fun getSourceRootFile(): VirtualFile {
157+
return ProjectRootManager.getInstance(project).fileIndex.getSourceRootForFile(vFile) as VirtualFile
158+
}
159+
160+
companion object {
161+
162+
private val LOG = Logger.getInstance(DexCompiler::class.java)
163+
164+
const val CLASS_EXTENSION = ".class"
165+
const val DEX_EXTENSION = ".dex"
166+
const val SMALI_EXTENSION = ".smali"
167+
const val CLASS = "class"
168+
}
169+
170+
}

src/main/kotlin/org/ollide/java2smali/GenerateAction.kt

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,45 @@ package org.ollide.java2smali
33
import com.intellij.openapi.actionSystem.AnAction
44
import com.intellij.openapi.actionSystem.AnActionEvent
55
import com.intellij.openapi.actionSystem.LangDataKeys
6-
import com.intellij.openapi.compiler.CompilerManager
6+
import com.intellij.openapi.diagnostic.Logger
77
import com.intellij.openapi.roots.ProjectRootManager
88
import com.intellij.openapi.vfs.VirtualFile
9-
import com.intellij.psi.PsiClassOwner
10-
import com.intellij.psi.PsiManager
119

1210
class GenerateAction : AnAction() {
1311

1412
override fun actionPerformed(e: AnActionEvent) {
15-
val vFile = getVirtualFileFromContext(e) ?: return
13+
LOG.debug("Action performed.")
1614

15+
val vFile = getVirtualFileFromEvent(e) ?: return
1716
val project = e.project!!
18-
val module = ProjectRootManager.getInstance(project).fileIndex.getModuleForFile(vFile) ?: return
19-
val file = PsiManager.getInstance(project).findFile(vFile) as PsiClassOwner
17+
val module = ProjectRootManager.getInstance(project).fileIndex.getModuleForFile(vFile)!!
2018

21-
// Compile the vFile's module
22-
val compilerCallback = CompilerCallback(module, file)
23-
CompilerManager.getInstance(project).compile(module, compilerCallback)
19+
DexCompiler(vFile, project, module).run()
2420
}
2521

2622
override fun update(e: AnActionEvent) {
2723
var enabled = false
2824

29-
val vFile = getVirtualFileFromContext(e)
30-
if (vFile != null) {
31-
val extension = vFile.fileType.defaultExtension
32-
val m = ProjectRootManager.getInstance(e.project!!).fileIndex.getModuleForFile(vFile)
33-
enabled = (JAVA == extension || KOTLIN == extension) && m != null
25+
getVirtualFileFromEvent(e)?.let {
26+
e.project?.let { project ->
27+
val m = ProjectRootManager.getInstance(project).fileIndex.getModuleForFile(it)
28+
val extension = it.fileType.defaultExtension
29+
enabled = (JAVA == extension || KOTLIN == extension) && m != null
30+
}
3431
}
3532
e.presentation.isEnabled = enabled
3633
}
3734

38-
private fun getVirtualFileFromContext(e: AnActionEvent): VirtualFile? {
35+
private fun getVirtualFileFromEvent(e: AnActionEvent): VirtualFile? {
3936
val psiFile = e.getData(LangDataKeys.PSI_FILE) ?: return null
4037
return psiFile.virtualFile
4138
}
4239

4340
companion object {
44-
private val JAVA = "java"
45-
private val KOTLIN = "kt"
41+
private val LOG = Logger.getInstance(GenerateAction::class.java)
42+
43+
private const val JAVA = "java"
44+
private const val KOTLIN = "kt"
4645
}
4746

4847
}

0 commit comments

Comments
 (0)