diff --git a/misc/androidstudio/README.md b/misc/androidstudio/README.md index d9abe0d..cbd187c 100644 --- a/misc/androidstudio/README.md +++ b/misc/androidstudio/README.md @@ -18,14 +18,17 @@ gobind { // Optional list of architectures. Defaults to all supported architectures. GOARCH="arm amd64" - // Absolute path to the gomobile binary + // Absolute path to the gomobile binary. Optional. GOMOBILE "/mypath/bin/gomobile" - // Absolute path to the go binary + // Absolute path to the gomobile binary. Optional. + GOBIND "/mypath/bin/gobind" + + // Absolute path to the go binary. Optional. GO "/usr/local/go/bin/go" - // Pass extra parameters to command line - // GOMOBILEFLAGS "-javapkg my.java.package" + // Pass extra parameters to command line. Optional. + GOMOBILEFLAGS "-javapkg my.java.package" } diff --git a/misc/androidstudio/build.gradle b/misc/androidstudio/build.gradle index 545a885..2f04c3a 100644 --- a/misc/androidstudio/build.gradle +++ b/misc/androidstudio/build.gradle @@ -27,7 +27,7 @@ dependencies { testCompile 'junit:junit:4.11' } -version = '0.2.6' +version = '0.2.7' pluginBundle { website = 'https://golang.org/x/mobile' diff --git a/misc/androidstudio/src/main/groovy/org/golang/mobile/GobindPlugin.groovy b/misc/androidstudio/src/main/groovy/org/golang/mobile/GobindPlugin.groovy index 3f1a332..37f57d4 100644 --- a/misc/androidstudio/src/main/groovy/org/golang/mobile/GobindPlugin.groovy +++ b/misc/androidstudio/src/main/groovy/org/golang/mobile/GobindPlugin.groovy @@ -11,7 +11,11 @@ import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Plugin import org.gradle.api.Task +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.golang.mobile.OutputFileTask @@ -21,31 +25,81 @@ import org.golang.mobile.AARPublishArtifact * GobindPlugin configures the default project that builds .AAR file * from a go package, using gomobile bind command. * For gomobile bind command, see https://golang.org/x/mobile/cmd/gomobile + * + * If the project has the android or android library plugin loaded, GobindPlugin + * hooks into the android build lifecycle in two steps. First, the Java classes are + * generated and registered with the android plugin. Then, when the databinding + * classes and the R classes are generated and compiled, the GobindPlugin generates + * the JNI libraries. By splitting the binding in two steps, the Android databinding + * machinery can resolve Go classes, and Go code can access the resulting databinding + * classes as well as the R resource classes. */ class GobindPlugin implements Plugin { void apply(Project project) { - project.configurations.create("default") project.extensions.create('gobind', GobindExtension) + // If the android or android library plugin is loaded, integrate + // directly with the android build cycle + if (project.plugins.hasPlugin("android") || + project.plugins.hasPlugin("com.android.application")) { + project.android.applicationVariants.all { variant -> + handleVariant(project, variant) + } + return + } + if (project.plugins.hasPlugin("android-library") || + project.plugins.hasPlugin("com.android.library")) { + project.android.libraryVariants.all { variant -> + handleVariant(project, variant) + } + return + } - Task gobindTask = project.tasks.create("gobind", GobindTask) - gobindTask.outputFile = project.file(project.name+".aar") + // Library mode: generate and declare the .aar file for parent + // projects to include. + project.configurations.create("default") + + Task gomobileTask = project.tasks.create("gobind", GomobileTask) + gomobileTask.outputFile = project.file(project.name+".aar") project.artifacts.add("default", new AARPublishArtifact( 'mylib', null, - gobindTask)) + gomobileTask)) Task cleanTask = project.tasks.create("clean", { project.delete(project.name+".aar") }) } + + private static void handleVariant(Project project, def variant) { + File outputDir = project.file("$project.buildDir/generated/source/gobind/$variant.dirName") + // First, generate the Java classes with the gobind tool. + Task bindTask = project.tasks.create("gobind${variant.name.capitalize()}", GobindTask) + bindTask.outputDir = outputDir + bindTask.classpath = variant.javaCompile.classpath + bindTask.bootClasspath = variant.javaCompile.options.bootClasspath + // TODO: Detect when updating the Java classes is redundant. + bindTask.outputs.upToDateWhen { false } + variant.registerJavaGeneratingTask(bindTask, outputDir) + // Then, generate the JNI libraries with the gomobile tool. + Task libTask = project.tasks.create("gomobile${variant.name.capitalize()}", GomobileTask) + libTask.bootClasspath = variant.javaCompile.options.bootClasspath + // Add the R and databinding classes to the gomobile classpath. + libTask.classpath = project.files(variant.javaCompile.classpath, variant.javaCompile.destinationDir) + // Dump the JNI libraries in the known project jniLibs directory. + // TODO: Use a directory below build for the libraries instead. Adding a jni directory to the jniLibs + // property of android.sourceSets only works, but only if the directory changes every build. + libTask.libsDir = project.file("src/main/jniLibs") + // TODO: Detect when building the existing JNI libraries is redundant. + libTask.outputs.upToDateWhen { false } + libTask.dependsOn(bindTask) + variant.javaCompile.finalizedBy(libTask) + } } -class GobindTask extends DefaultTask implements OutputFileTask { - @OutputFile - File outputFile +class BindTask extends DefaultTask { + String bootClasspath - @TaskAction - def gobind() { + def run(String cmd, String cmdPath, List cmdArgs) { def pkg = project.gobind.pkg.trim() def gopath = (project.gobind.GOPATH ?: System.getenv("GOPATH"))?.trim() if (!pkg || !gopath) { @@ -61,16 +115,15 @@ class GobindTask extends DefaultTask implements OutputFileTask { paths = paths + "/usr/local/go/bin" } - def gomobile = (project.gobind.GOMOBILE ?: findExecutable("gomobile", paths))?.trim() + def exe = (cmdPath ?: findExecutable(cmd, paths))?.trim() def gobin = (project.gobind.GO ?: findExecutable("go", paths))?.trim() def gomobileFlags = project.gobind.GOMOBILEFLAGS?.trim() - def goarch = project.gobind.GOARCH?.trim() - if (!gomobile || !gobin) { - throw new GradleException('failed to find gomobile/go tools. Set gobind.GOMOBILE and gobind.GO') + if (!exe || !gobin) { + throw new GradleException('failed to find ${cmd}/go tools. Set gobind.GOBIND, gobind.GOMOBILE, and gobind.GO') } - paths = [findDir(gomobile), findDir(gobin), paths].flatten() + paths = [findDir(exe), findDir(gobin), paths].flatten() def androidHome = "" try { @@ -86,20 +139,16 @@ class GobindTask extends DefaultTask implements OutputFileTask { } project.exec { - executable(gomobile) + executable(exe) - def cmd = ["bind", "-i", "-o", project.name+".aar", "-target"] - if (goarch) { - cmd = cmd+goarch.split(" ").collect{ 'android/'+it }.join(",") - } else { - cmd << "android" - } + if (bootClasspath) + cmdArgs.addAll(["-bootclasspath", bootClasspath]) if (gomobileFlags) { - cmd.addAll(gomobileFlags.split(" ")) + cmdArgs.addAll(gomobileFlags.split(" ")) } - cmd.addAll(pkg.split(" ")) + cmdArgs.addAll(pkg.split(" ")) - args(cmd) + args(cmdArgs) if (!androidHome?.trim()) { throw new GradleException('Neither sdk.dir or ANDROID_HOME is set') } @@ -136,6 +185,65 @@ class GobindTask extends DefaultTask implements OutputFileTask { } } +class GobindTask extends BindTask { + @OutputDirectory + File outputDir + + FileCollection classpath + + @TaskAction + def gobind() { + run("gobind", project.gobind.GOBIND, ["-lang", "java", "-classpath", classpath.join(File.pathSeparator), "-outdir", outputDir.getAbsolutePath()]) + } +} + +class GomobileTask extends BindTask implements OutputFileTask { + @Optional + @OutputFile + File outputFile + + @Optional + @OutputDirectory + File libsDir + + FileCollection classpath + + @TaskAction + def gomobile() { + if (outputFile == null) { + outputFile = File.createTempFile("gobind-", ".aar") + } + def cmd = ["bind", "-i"] + if (classpath) { + cmd << "-classpath" + cmd << classpath.join(File.pathSeparator) + } + cmd << "-o" + cmd << outputFile.getAbsolutePath() + cmd << "-target" + def goarch = project.gobind.GOARCH?.trim() + if (goarch) { + cmd = cmd+goarch.split(" ").collect{ 'android/'+it }.join(",") + } else { + cmd << "android" + } + run("gomobile", project.gobind.GOMOBILE, cmd) + // If libsDir is set, unpack (only) the JNI libraries to it. + if (libsDir != null) { + project.delete project.fileTree(dir: libsDir, include: '*/libgojni.so') + def zipFile = new java.util.zip.ZipFile(outputFile) + zipFile.entries().findAll { !it.directory && it.name.startsWith("jni/") }.each { + def libFile = new File(libsDir, it.name.substring(4)) + libFile.parentFile.mkdirs() + zipFile.getInputStream(it).withStream { + libFile.append(it) + } + } + outputFile.delete() + } + } +} + class GobindExtension { // Package to bind. Separate multiple packages with spaces. (required) def String pkg = "" @@ -152,6 +260,9 @@ class GobindExtension { // GOMOBILE: path to gomobile binary. (can omit if 'gomobile' is under GOPATH) def String GOMOBILE = "" + // GOBIND: path to gobind binary. (can omit if 'gobind' is under GOPATH) + def String GOBIND = "" + // GOMOBILEFLAGS: extra flags to be passed to gomobile command. (optional) def String GOMOBILEFLAGS = "" }