From 93e85950441509eda5225e8d35055135daaf6d98 Mon Sep 17 00:00:00 2001 From: Colin Cross Date: Tue, 15 Aug 2017 13:34:18 -0700 Subject: [PATCH] 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 --- Android.bp | 1 + android/paths.go | 33 +++++++++++++++++++++++++++++++++ java/builder.go | 42 ++++++++++++++++++++++++++++++++++++++++++ java/config/kotlin.go | 25 +++++++++++++++++++++++++ java/java.go | 42 +++++++++++++++++++++++++++++++++++++++--- java/java_test.go | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 java/config/kotlin.go diff --git a/Android.bp b/Android.bp index 1153aac60..32b89d1a3 100644 --- a/Android.bp +++ b/Android.bp @@ -229,6 +229,7 @@ bootstrap_go_package { srcs: [ "java/config/config.go", "java/config/error_prone.go", + "java/config/kotlin.go", "java/config/makevars.go", ], } diff --git a/android/paths.go b/android/paths.go index 69a7b0d7c..09f760a57 100644 --- a/android/paths.go +++ b/android/paths.go @@ -303,6 +303,39 @@ outer: return list[:k] } +// HasExt returns true of any of the paths have extension ext, otherwise false +func (p Paths) HasExt(ext string) bool { + for _, path := range p { + if path.Ext() == ext { + return true + } + } + + return false +} + +// FilterByExt returns the subset of the paths that have extension ext +func (p Paths) FilterByExt(ext string) Paths { + ret := make(Paths, 0, len(p)) + for _, path := range p { + if path.Ext() == ext { + ret = append(ret, path) + } + } + return ret +} + +// FilterOutByExt returns the subset of the paths that do not have extension ext +func (p Paths) FilterOutByExt(ext string) Paths { + ret := make(Paths, 0, len(p)) + for _, path := range p { + if path.Ext() != ext { + ret = append(ret, path) + } + } + return ret +} + // WritablePaths is a slice of WritablePaths, used for multiple outputs. type WritablePaths []WritablePath diff --git a/java/builder.go b/java/builder.go index efe625665..a03f89271 100644 --- a/java/builder.go +++ b/java/builder.go @@ -50,6 +50,22 @@ var ( }, "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 @@ type javaBuilderFlags struct { 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 000000000..35f9e9d72 --- /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 770c9c1fd..3382cf289 100644 --- a/java/java.go +++ b/java/java.go @@ -198,6 +198,7 @@ var ( 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 @@ func (j *Module) deps(ctx android.BottomUpMutatorContext) { 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 @@ type deps struct { srcJars android.Paths systemModules android.Path aidlPreprocess android.OptionalPath + kotlinStdlib android.Paths } func (j *Module) collectDeps(ctx android.ModuleContext) deps { @@ -424,6 +432,8 @@ func (j *Module) collectDeps(ctx android.ModuleContext) deps { // 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 @@ func (j *Module) compile(ctx android.ModuleContext) { 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 @@ func (j *Module) compile(ctx android.ModuleContext) { // 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 472931317..d64688f6f 100644 --- a/java/java_test.go +++ b/java/java_test.go @@ -80,6 +80,7 @@ func testJavaWithEnv(t *testing.T, bp string, env map[string]string) *android.Te "android_stubs_current", "android_system_stubs_current", "android_test_stubs_current", + "kotlin-stdlib", } for _, extra := range extraModules { @@ -115,6 +116,7 @@ func testJavaWithEnv(t *testing.T, bp string, env map[string]string) *android.Te "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 TestGeneratedSources(t *testing.T) { } } +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 {