blob: 7a3b4e2a0876d29866bdfd645a364ef8f22d0ebd [file] [log] [blame]
/*
* Copyright 2015 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
package org.golang.mobile
import org.gradle.api.DefaultTask
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.gradle.api.tasks.compile.JavaCompile
import org.golang.mobile.OutputFileTask
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<Project> {
void apply(Project project) {
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
}
// 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,
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.javaCompile = variant.javaCompile
bindTask.bootClasspath = variant.javaCompile.options.bootClasspath
// TODO: Detect when updating the Java classes is redundant.
bindTask.outputs.upToDateWhen { false }
variant.registerJavaGeneratingTask(bindTask, new File(outputDir, "java"))
// 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
libTask.javaCompile = variant.javaCompile
// 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)
// Ensure gomobile bind completes before depending on its output.
variant.javaCompile.dependsOn(libTask)
}
}
class BindTask extends DefaultTask {
String bootClasspath
def run(String cmd, String cmdPath, List<String> cmdArgs) {
def pkg = project.gobind.pkg.trim()
def gopath = (project.gobind.GOPATH ?: System.getenv("GOPATH"))?.trim()
if (!pkg || !gopath) {
throw new GradleException('gobind.pkg and gobind.GOPATH must be set')
}
def paths = (gopath.split(File.pathSeparator).collect{ "$it/bin" } +
System.getenv("PATH").split(File.pathSeparator)).flatten()
// Default installation path of go distribution.
if (isWindows()) {
paths = paths + "c:\\Go\\bin"
} else {
paths = paths + "/usr/local/go/bin"
}
def exe = (cmdPath ?: findExecutable(cmd, paths))?.trim()
def gobin = (project.gobind.GO ?: findExecutable("go", paths))?.trim()
def gomobileFlags = project.gobind.GOMOBILEFLAGS?.trim()
if (!exe || !gobin) {
throw new GradleException('failed to find ${cmd}/go tools. Set gobind.GOBIND, gobind.GOMOBILE, and gobind.GO')
}
paths = [findDir(exe), findDir(gobin), paths].flatten()
def androidHome = ""
try {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
androidHome = properties.getProperty('sdk.dir')
} catch (all) {
logger.info("failed to load local.properties.")
}
if (!androidHome?.trim()) {
// fallback to ANDROID_HOME
androidHome = System.getenv("ANDROID_HOME")
}
project.exec {
executable(exe)
if (bootClasspath)
cmdArgs.addAll(["-bootclasspath", bootClasspath])
if (gomobileFlags) {
cmdArgs.addAll(gomobileFlags.split(" "))
}
cmdArgs.addAll(pkg.split(" "))
args(cmdArgs)
if (!androidHome?.trim()) {
throw new GradleException('Neither sdk.dir or ANDROID_HOME is set')
}
environment("GOPATH", gopath)
environment("GOOS", "android")
environment("PATH", paths.join(File.pathSeparator))
environment("ANDROID_HOME", androidHome)
}
}
def isWindows() {
return System.getProperty("os.name").startsWith("Windows")
}
def findExecutable(String name, ArrayList<String> paths) {
if (isWindows() && !name.endsWith(".exe")) {
name = name + ".exe"
}
for (p in paths) {
def f = new File(p + File.separator + name)
if (f.exists()) {
return p + File.separator + name
}
}
throw new GradleException('binary ' + name + ' is not found in $PATH (' + paths + ')')
}
def findDir(String binpath) {
if (!binpath) {
return ""
}
def f = new File(binpath)
return f.getParentFile().getAbsolutePath();
}
}
class GobindTask extends BindTask {
@OutputDirectory
File outputDir
JavaCompile javaCompile
@TaskAction
def gobind() {
run("gobind", project.gobind.GOBIND, ["-lang", "java", "-classpath", javaCompile.classpath.join(File.pathSeparator), "-outdir", outputDir.getAbsolutePath()])
}
}
class GomobileTask extends BindTask implements OutputFileTask {
@Optional
@OutputFile
File outputFile
@Optional
@OutputDirectory
File libsDir
JavaCompile javaCompile
@TaskAction
def gomobile() {
if (outputFile == null) {
outputFile = File.createTempFile("gobind-", ".aar")
}
def cmd = ["bind", "-i"]
// Add the generated R and databinding classes to the classpath.
if (javaCompile) {
def classpath = project.files(javaCompile.classpath, javaCompile.destinationDir)
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 = ""
// GOPATH: necessary for gomobile tool. (required)
def String GOPATH = System.getenv("GOPATH")
// GOARCH: (List of) GOARCH to include.
def String GOARCH = ""
// GO: path to go tool. (can omit if 'go' is in the paths visible by Android Studio)
def String GO = ""
// 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 = ""
}