Ormlite is a library to do object-oriented access to sqlite. It requires that we generate an ormlite.txt
file which contains the processed configuration of the database objects in my android project.
In addition to my Android run configuration that will actually launch the Android app, I created an Application run configuration to just execute the main() method. This is how I’ve done it for like 5 years. But, after updating gradle I’m getting the following error:
> Configure project :app The JavaExec.main property has been deprecated. This is scheduled to be removed in Gradle 8.0. Please use the mainClass property instead. See https://docs.gradle.org/7.3.3/dsl/org.gradle.api.tasks.JavaExec.html#org.gradle.api.tasks.JavaExec:main for more details.
Now that gradle 7 won’t let me run my main() method the way I used to, what code am I supposed to use to let me run it and where do I put that code?
I have -no- idea how gradle actually works, I just follow instructions to copy/paste the snippets into my build.gradle that I am told to when I include things like crashlytics, or whatever. So, when I go look at the docs it suggests, I have -no- idea where I’m supposed to be putting the code it’s telling me to add. I’ve tried a million monkeys with a million Android Studios to try to guess at the right place, but I can’t get it to work.
I didn’t previously need to add any build.gradle code, it would just run through the Android Studio Run Configuration which referenced my app:
module for the classpath.
The way it worked before… In Android Studio, I just needed to create a simple class with a main() method that would call the proper ormlite function to generate the file:
public class DatabaseConfigUtil extends OrmLiteConfigUtil { public static void main(String[] args) throws SQLException, IOException { writeConfigFile("ormlite_config.txt"); // NON-NLS } }
And then create a Run Configuration in Android Studio of type “Application” and choose that class. Then when I ran that config, it would add all of the classes in my project to the classpath and then run the main() method. The code could read all of the classes in the classpath and generate the ormlite.txt
file.
Now that gradle 7 won’t let me run my main() method the way I used to, what code am I supposed to use to let me run it and where do I put that code?
Here is my app module build.gradle file:
buildscript { repositories { mavenCentral() // jcenter() maven { url 'https://maven.google.com/' name 'Google' } google() } } repositories { mavenCentral() mavenLocal() maven { url 'https://maven.google.com/' name 'Google' } } apply plugin: 'com.android.application' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' dependencies { // a bunch of stuff } android { // API 31 = Android 12.0 Snow Cone compileSdkVersion 31 defaultConfig { applicationId "com.mydomain.myapp" minSdkVersion 19 targetSdkVersion 31 versionCode 459 versionName "3.9" // Enabling multidex support. multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true javaCompileOptions { annotationProcessorOptions { arguments = [ "androidManifestFile": "$projectDir/src/main/AndroidManifest.xml".toString() ] } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } buildFeatures { viewBinding true } }
Advertisement
Answer
I spent the last day going through an online course on gradle, so that I could try to understand what was going wrong here. And after all of that, I’m convinced this is actually a bug in Android Studio. When I configure a “Run Configuration” in Android Studio, all that is really happening is that it is creating a gradle script in the background which will implement a gradle task for the Run Configuration that I created.
And that generated gradle script is wrong. It is using a deprecated property and so the gradle build is failing.
It is creating a gradle task of type JavaExec
and setting the main
property equal to the class name that holds my main() method, but the main
property has been renamed to mainClass
, so the generated code is incorrect and therefore fails.
I haven’t found a workaround yet, but I’m going to try to see if I can just create a gradle task on my own and run it manually.
UPDATE: I’ve finally figure out a workaround and was able to generate my ormlite configuration file. Strap in.
The essential core of my problem was that when I run my main() method, I need all of my android-based classes in my classpath. Normally, if you have a java gradle project, that is trivial because the standard output of a java gradle project is a jar
(or some classes). However, the output of an android project is an apk
which doesn’t contain your classes, it contains .dex
files.
I wasn’t able to figure out a way to build the android project and skip the dexing step (i.e. build all of the classes needed without smashing them down into .dex
files.
But I did find a way to un-dex the dex files: https://github.com/pxb1988/dex2jar
I downloaded the dex2jar code and followed the installation instructions to build all of the various code and scripts for the dex2jar tools. I loved the generated dex-tools
directory to my user home directory so that I could have it available in a central place. That gave me a script called d2j-dex2jar.sh
that would convert a .dex
file back into a .jar
file filled with .class
files, which was exactly what I needed.
Then I added the following to my app/build.gradle
file:
def userhome = System.properties['user.home'] def dex2jaroutputdir = "$buildDir/intermediates/fulljar" task convertdextojars { dependsOn('bundleDebug') doLast { def dexdir = "$buildDir/intermediates/dex/debug/mergeDexDebug" def dex2jar = "$userhome/dex-tools/d2j-dex2jar.sh" mkdir(dex2jaroutputdir) fileTree(include: ['*.dex'], dir: dexdir).each { def filename = it exec { workingDir dex2jaroutputdir commandLine dex2jar, '--force', "$filename" } } } }
This task depends on the android task bundleDebug
which would build the app-debug.apk
but I didn’t really need to use the apk
itself, because the intermediate files used to build the apk were available, so I could just use those. The .dex
files that would get added to the apk are stored in $buildDir/intermediates/dex/debug/mergeDexDebug
. I had classes.dex
, classes2.dex
, and classes3.dex
, but your mileage may vary.
The convertdextojars
task above, would create an output directory to hold my jars, then use fileTree()
to get a collection of all of the filenames matching *.dex
in the dexdir
. Then for each of those files, it would execute a shell command to de-dex the file. I set the exec working directory to my dex2jaroutputdir
because the d2j-dex2jar.sh
script will produce the output jar to the current working directory. I add the --force
flag to the command line so that it will overwrite the jars if they already exist.
This would get me a set of jars that had almost all the code I needed. It would include all of my android code, all of the code for my dependencies, but it wouldn’t include the android.os.* runtime classes.
Next, I added this code to my app/build.gradle
file:
task gendbconfig(type: JavaExec) { dependsOn('convertdextojars') doFirst { println "++++++++++++++++++++++++++++++++++++++++" fileTree(include: ['*.jar'], dir: dex2jaroutputdir).each { println "jar: $it" } } mainClass = 'com.kenny.stuff.DatabaseConfigUtil' classpath = fileTree(include: ['*.jar'], dir: dex2jaroutputdir) classpath = classpath.plus(files(android.getBootClasspath()[0])) workingDir = "$projectDir/src/main" }
The doFirst()
closure isn’t necessary but it was helpful to ensure that my incoming jar files were what I expected.
mainClass
is the class containing my main()
method.
The first classpath
line, builds the collection of those jars that we just generated, which got me almost to what I needed, but it was missing the android runtime classes themselves. And that’s what the second classpath
line does. android.getBootClasspath()[0]
resolves to /Users/kenny/Library/Android/sdk/platforms/android-31/android.jar
on my system.
As a side note, I tried using the notation classpath +=
instead of classpath = classpath.plus()
but the gradle build failed because of some object type issues.
I set the workingDir
to $projectDir/src/main
(which means app/src/main
, because the ormlite code that generates the config file will try to automatically write the config file to your res/raw/
directory if it can find as a subdirectory of the working directory.
Finally, I opened the Terminal window in Android Studio which automatically put me into the top project folder (one folder higher than the app
module/project. And I ran the command ./gradlew app:gendbconfig
which tells gradle to run the gendbconfig
task in the app
module.
And voila.
Writing configurations to /path/to/my/android/project/app/src/main/./res/raw/ormlite_config.txt