Initial kotlin support

Allow java libraries to specify .kt sources, precompile them with
kotlin, and then pass them in the classpath to javac.

Bug: 65219535
Test: java_test.go
Change-Id: Ife22b6ef82ced9ec26a9e5392b2dadacbb16546f
diff --git a/java/builder.go b/java/builder.go
index efe6256..a03f892 100644
--- a/java/builder.go
+++ b/java/builder.go
@@ -50,6 +50,22 @@
 		},
 		"javacFlags", "sourcepath", "bootClasspath", "classpath", "outDir", "annoDir", "javaVersion")
 
+	kotlinc = pctx.AndroidGomaStaticRule("kotlinc",
+		blueprint.RuleParams{
+			// TODO(ccross): kotlinc doesn't support @ file for arguments, which will limit the
+			// maximum number of input files, especially on darwin.
+			Command: `rm -rf "$outDir" && mkdir -p "$outDir" && ` +
+				`${config.KotlincCmd} $classpath $kotlincFlags ` +
+				`-jvm-target $javaVersion -d $outDir $in && ` +
+				`${config.SoongZipCmd} -jar -o $out -C $outDir -D $outDir`,
+			CommandDeps: []string{
+				"${config.KotlincCmd}",
+				"${config.KotlinCompilerJar}",
+				"${config.SoongZipCmd}",
+			},
+		},
+		"kotlincFlags", "classpath", "outDir", "javaVersion")
+
 	errorprone = pctx.AndroidStaticRule("errorprone",
 		blueprint.RuleParams{
 			Command: `rm -rf "$outDir" "$annoDir" && mkdir -p "$outDir" "$annoDir" && ` +
@@ -131,10 +147,36 @@
 	aidlFlags     string
 	javaVersion   string
 
+	kotlincFlags     string
+	kotlincClasspath classpath
+
 	protoFlags   string
 	protoOutFlag string
 }
 
+func TransformKotlinToClasses(ctx android.ModuleContext, outputFile android.WritablePath,
+	srcFiles android.Paths, srcJars classpath,
+	flags javaBuilderFlags) {
+
+	classDir := android.PathForModuleOut(ctx, "classes-kt")
+
+	inputs := append(android.Paths(nil), srcFiles...)
+	inputs = append(inputs, srcJars...)
+
+	ctx.ModuleBuild(pctx, android.ModuleBuildParams{
+		Rule:        kotlinc,
+		Description: "kotlinc",
+		Output:      outputFile,
+		Inputs:      inputs,
+		Args: map[string]string{
+			"classpath":    flags.kotlincClasspath.JavaClasspath(),
+			"kotlincFlags": flags.kotlincFlags,
+			"outDir":       classDir.String(),
+			"javaVersion":  flags.javaVersion,
+		},
+	})
+}
+
 func TransformJavaToClasses(ctx android.ModuleContext, outputFile android.WritablePath,
 	srcFiles android.Paths, srcJars classpath,
 	flags javaBuilderFlags, deps android.Paths) {
diff --git a/java/config/kotlin.go b/java/config/kotlin.go
new file mode 100644
index 0000000..35f9e9d
--- /dev/null
+++ b/java/config/kotlin.go
@@ -0,0 +1,25 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+var (
+	KotlinStdlibJar = "external/kotlinc/lib/kotlin-stdlib.jar"
+)
+
+func init() {
+	pctx.SourcePathVariable("KotlincCmd", "external/kotlinc/bin/kotlinc")
+	pctx.SourcePathVariable("KotlinCompilerJar", "external/kotlinc/lib/kotlin-compiler.jar")
+	pctx.SourcePathVariable("KotlinStdlibJar", KotlinStdlibJar)
+}
diff --git a/java/java.go b/java/java.go
index 770c9c1..3382cf2 100644
--- a/java/java.go
+++ b/java/java.go
@@ -198,6 +198,7 @@
 	bootClasspathTag = dependencyTag{name: "bootclasspath"}
 	systemModulesTag = dependencyTag{name: "system modules"}
 	frameworkResTag  = dependencyTag{name: "framework-res"}
+	kotlinStdlibTag  = dependencyTag{name: "kotlin-stdlib"}
 )
 
 type sdkDep struct {
@@ -326,6 +327,12 @@
 	if j.hasSrcExt(".proto") {
 		protoDeps(ctx, &j.protoProperties)
 	}
+
+	if j.hasSrcExt(".kt") {
+		// TODO(ccross): move this to a mutator pass that can tell if generated sources contain
+		// Kotlin files
+		ctx.AddDependency(ctx.Module(), kotlinStdlibTag, "kotlin-stdlib")
+	}
 }
 
 func hasSrcExt(srcs []string, ext string) bool {
@@ -373,6 +380,7 @@
 	srcJars            android.Paths
 	systemModules      android.Path
 	aidlPreprocess     android.OptionalPath
+	kotlinStdlib       android.Paths
 }
 
 func (j *Module) collectDeps(ctx android.ModuleContext) deps {
@@ -424,6 +432,8 @@
 				// generated by framework-res.apk
 				// TODO(ccross): aapt java files should go in a src jar
 			}
+		case kotlinStdlibTag:
+			deps.kotlinStdlib = dep.ClasspathFiles()
 		default:
 			panic(fmt.Errorf("unknown dependency %q for %q", otherName, ctx.ModuleName()))
 		}
@@ -492,7 +502,33 @@
 
 	var jars android.Paths
 
-	if len(srcFiles) > 0 {
+	if srcFiles.HasExt(".kt") {
+		// If there are kotlin files, compile them first but pass all the kotlin and java files
+		// kotlinc will use the java files to resolve types referenced by the kotlin files, but
+		// won't emit any classes for them.
+
+		flags.kotlincFlags = "-no-stdlib"
+		if ctx.Device() {
+			flags.kotlincFlags += " -no-jdk"
+		}
+
+		flags.kotlincClasspath = append(flags.kotlincClasspath, deps.kotlinStdlib...)
+		flags.kotlincClasspath = append(flags.kotlincClasspath, deps.classpath...)
+
+		kotlinJar := android.PathForModuleOut(ctx, "classes-kt.jar")
+		TransformKotlinToClasses(ctx, kotlinJar, srcFiles, srcJars, flags)
+		if ctx.Failed() {
+			return
+		}
+
+		// Make javac rule depend on the kotlinc rule
+		flags.classpath = append(flags.classpath, kotlinJar)
+		// Jar kotlin classes into the final jar after javac
+		jars = append(jars, kotlinJar)
+		jars = append(jars, deps.kotlinStdlib...)
+	}
+
+	if javaSrcFiles := srcFiles.FilterByExt(".java"); len(javaSrcFiles) > 0 {
 		var extraJarDeps android.Paths
 		if ctx.AConfig().IsEnvTrue("RUN_ERROR_PRONE") {
 			// If error-prone is enabled, add an additional rule to compile the java files into
@@ -501,13 +537,13 @@
 			// TODO(ccross): Once we always compile with javac9 we may be able to conditionally
 			//    enable error-prone without affecting the output class files.
 			errorprone := android.PathForModuleOut(ctx, "classes-errorprone.list")
-			RunErrorProne(ctx, errorprone, srcFiles, srcJars, flags)
+			RunErrorProne(ctx, errorprone, javaSrcFiles, srcJars, flags)
 			extraJarDeps = append(extraJarDeps, errorprone)
 		}
 
 		// Compile java sources into .class files
 		classes := android.PathForModuleOut(ctx, "classes-compiled.jar")
-		TransformJavaToClasses(ctx, classes, srcFiles, srcJars, flags, extraJarDeps)
+		TransformJavaToClasses(ctx, classes, javaSrcFiles, srcJars, flags, extraJarDeps)
 		if ctx.Failed() {
 			return
 		}
diff --git a/java/java_test.go b/java/java_test.go
index 4729313..d64688f 100644
--- a/java/java_test.go
+++ b/java/java_test.go
@@ -80,6 +80,7 @@
 		"android_stubs_current",
 		"android_system_stubs_current",
 		"android_test_stubs_current",
+		"kotlin-stdlib",
 	}
 
 	for _, extra := range extraModules {
@@ -115,6 +116,7 @@
 		"a.java":     nil,
 		"b.java":     nil,
 		"c.java":     nil,
+		"b.kt":       nil,
 		"a.jar":      nil,
 		"b.jar":      nil,
 		"res/a":      nil,
@@ -613,6 +615,38 @@
 	}
 }
 
+func TestKotlin(t *testing.T) {
+	ctx := testJava(t, `
+		java_library {
+			name: "foo",
+                        srcs: ["a.java", "b.kt"],
+		}
+		`)
+
+	kotlinc := ctx.ModuleForTests("foo", "android_common").Rule("kotlinc")
+	javac := ctx.ModuleForTests("foo", "android_common").Rule("javac")
+	jar := ctx.ModuleForTests("foo", "android_common").Output("classes.jar")
+
+	if len(kotlinc.Inputs) != 2 || kotlinc.Inputs[0].String() != "a.java" ||
+		kotlinc.Inputs[1].String() != "b.kt" {
+		t.Errorf(`foo kotlinc inputs %v != ["a.java", "b.kt"]`, kotlinc.Inputs)
+	}
+
+	if len(javac.Inputs) != 1 || javac.Inputs[0].String() != "a.java" {
+		t.Errorf(`foo inputs %v != ["a.java"]`, javac.Inputs)
+	}
+
+	if !strings.Contains(javac.Args["classpath"], kotlinc.Output.String()) {
+		t.Errorf("foo classpath %v does not contain %q",
+			javac.Args["classpath"], kotlinc.Output.String())
+	}
+
+	if !inList(kotlinc.Output.String(), jar.Inputs.Strings()) {
+		t.Errorf("foo jar inputs %v does not contain %q",
+			jar.Inputs.Strings(), kotlinc.Output.String())
+	}
+}
+
 func fail(t *testing.T, errs []error) {
 	if len(errs) > 0 {
 		for _, err := range errs {