From aa732ea93ad5ea733339f23af64122e118dd82bc Mon Sep 17 00:00:00 2001 From: hsyed Date: Fri, 2 Feb 2018 19:28:42 +0000 Subject: [PATCH] mixed-mode rework, allign flags with javabuilder, don't use temp dir - declare class dir, reorganize source. --- kotlin/rules/compile.bzl | 9 +- .../ruleskotlin/workers/BazelWorker.java | 4 +- .../jvm/actions => }/BuildAction.java | 3 +- .../ruleskotlin/workers/CompileResult.java | 120 ++++++++++++++++ .../workers/{compilers/jvm => }/Context.java | 33 +++-- .../{compilers/jvm/Flag.java => Flags.java} | 21 ++- .../ruleskotlin/workers/JavaBuilderFlags.java | 61 ++++++++ ...pilerBuilder.java => KotlinToolchain.java} | 38 ++--- .../io/bazel/ruleskotlin/workers/Meta.java | 67 +++++++++ .../compilers/jvm/KotlinJvmBuilder.java | 35 +++-- .../workers/compilers/jvm/Locations.java | 6 +- .../workers/compilers/jvm/Meta.java | 43 ------ .../workers/compilers/jvm/Metas.java | 42 ++++++ .../jvm/actions/CreateOutputJar.java | 52 +++++++ .../jvm/actions/GenerateJdepsFile.java | 14 +- .../compilers/jvm/actions/Initialize.java | 108 ++++++++++++++ .../jvm/actions/JavaMainCompile.java | 52 +++++++ .../jvm/actions/KotlinCreateClassJar.java | 46 ------ .../jvm/actions/KotlinMainCompile.java | 100 +++++-------- .../KotlinRenderClassCompileResult.java | 59 ++++++++ .../utils/KotlinCompilerOutputProcessor.java | 45 ++++-- .../workers/compilers/jvm/utils/Utils.java | 85 ----------- .../ruleskotlin/workers/utils/IOUtils.java | 133 ++++++++++++++++++ tests/smoke/BUILD | 11 +- tests/smoke/basic_tests.py | 21 ++- tests/smoke/hellojava/HelloWorldJava.java | 23 +++ .../{Another.java => MessageHolderKotlin.kt} | 8 +- .../propagation/CompileTimeDependent.java | 1 - 28 files changed, 917 insertions(+), 323 deletions(-) rename kotlin/workers/src/io/bazel/ruleskotlin/workers/{compilers/jvm/actions => }/BuildAction.java (85%) create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/CompileResult.java rename kotlin/workers/src/io/bazel/ruleskotlin/workers/{compilers/jvm => }/Context.java (66%) rename kotlin/workers/src/io/bazel/ruleskotlin/workers/{compilers/jvm/Flag.java => Flags.java} (67%) create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/JavaBuilderFlags.java rename kotlin/workers/src/io/bazel/ruleskotlin/workers/{compilers/jvm/utils/KotlinPreloadedCompilerBuilder.java => KotlinToolchain.java} (69%) create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/Meta.java delete mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Meta.java create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Metas.java create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/CreateOutputJar.java create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/Initialize.java create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/JavaMainCompile.java delete mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinCreateClassJar.java create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinRenderClassCompileResult.java delete mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/Utils.java create mode 100644 kotlin/workers/src/io/bazel/ruleskotlin/workers/utils/IOUtils.java create mode 100644 tests/smoke/hellojava/HelloWorldJava.java rename tests/smoke/hellojava/{Another.java => MessageHolderKotlin.kt} (87%) diff --git a/kotlin/rules/compile.bzl b/kotlin/rules/compile.bzl index 80c591c4a..460de388f 100644 --- a/kotlin/rules/compile.bzl +++ b/kotlin/rules/compile.bzl @@ -40,9 +40,12 @@ def _kotlin_do_compile_action(ctx, output_jar, compile_jars, opts): by the caller -- kotlin-reflect could be optional. opts: struct containing Kotlin compilation options. """ + compiler_output_base=ctx.actions.declare_directory(ctx.label.name + "." + "kotlinc") + args = [ - "--label", ctx.label, - "--output_classjar", output_jar.path, + "--target_label", ctx.label, + "--compiler_output_base", compiler_output_base.path, + "--output", output_jar.path, "--output_jdeps", ctx.outputs.jdeps.path, "--classpath", ":".join([f.path for f in compile_jars.to_list()]), "--sources", ":".join([f.path for f in ctx.files.srcs]), @@ -79,7 +82,7 @@ def _kotlin_do_compile_action(ctx, output_jar, compile_jars, opts): ctx.action( mnemonic = "KotlinCompile", inputs = compile_inputs, - outputs = [output_jar, ctx.outputs.jdeps], + outputs = [output_jar, ctx.outputs.jdeps, compiler_output_base], executable = ctx.executable._kotlinw, execution_requirements = {"supports-workers": "1"}, arguments = ["@" + args_file.path], diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/BazelWorker.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/BazelWorker.java index 14b9db3ef..6ee7f1b9b 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/BazelWorker.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/BazelWorker.java @@ -17,7 +17,7 @@ import com.google.devtools.build.lib.worker.WorkerProtocol; -import io.bazel.ruleskotlin.workers.compilers.jvm.utils.Utils; +import io.bazel.ruleskotlin.workers.utils.IOUtils; import java.io.*; import java.nio.file.Files; @@ -135,7 +135,7 @@ private List loadArguments(List args, boolean isWorker) { } private boolean wasInterrupted(Throwable e) { - Throwable cause = Utils.getRootCause(e); + Throwable cause = IOUtils.getRootCause(e); if (cause instanceof InterruptedException || cause instanceof InterruptedIOException) { output.println("Terminating worker due to interrupt signal"); diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/BuildAction.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/BuildAction.java similarity index 85% rename from kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/BuildAction.java rename to kotlin/workers/src/io/bazel/ruleskotlin/workers/BuildAction.java index 7039fb57f..79dcd5262 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/BuildAction.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/BuildAction.java @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.bazel.ruleskotlin.workers.compilers.jvm.actions; +package io.bazel.ruleskotlin.workers; -import io.bazel.ruleskotlin.workers.compilers.jvm.Context; import java.util.function.Function; diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/CompileResult.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/CompileResult.java new file mode 100644 index 000000000..49ea3652c --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/CompileResult.java @@ -0,0 +1,120 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +@FunctionalInterface +public interface CompileResult { + /** + * The status of this operation. + */ + default int status() { + return 0; + } + + default Optional error() { + return Optional.empty(); + } + + default void propogateError(String message) throws RuntimeException { + error().ifPresent(e -> { + throw new RuntimeException(message, e); + }); + } + + static CompileResult just(final int status) { + return new CompileResult() { + @Override + public int status() { + return status; + } + + @Override + public Integer render(Context ctx) { + return status; + } + }; + } + + static CompileResult error(final Exception error) { + return new CompileResult() { + @Override + public int status() { + return -1; + } + + @Override + public Optional error() { + return Optional.of(error); + } + + @Override + public Integer render(Context ctx) { + throw new RuntimeException(error); + } + }; + } + + static CompileResult deferred(final int status, Function renderer) { + return new CompileResult() { + @Override + public int status() { + return status; + } + + @Override + public Integer render(Context ctx) { + return renderer.apply(ctx); + } + }; + } + + final class Meta extends io.bazel.ruleskotlin.workers.Meta { + public Meta(String id) { + super(id); + } + + public CompileResult run(final Context ctx, Function op) { + CompileResult result; + try { + result = CompileResult.just(op.apply(ctx)); + } catch (Exception e) { + result = CompileResult.error(e); + } + return result; + } + + public CompileResult runAndBind(final Context ctx, Function op) { + CompileResult res = run(ctx, op); + bind(ctx, res); + return res; + } + + public CompileResult runAndBind(final Context ctx, Supplier op) { + return runAndBind(ctx, (c) -> op.get()); + } + } + + /** + * Materialise the output of the compile result. + * + * @return the new status of the compile operation, this shouldn't make a failing status pass, but it could fail a compile operation. + */ + Integer render(Context ctx); +} diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Context.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/Context.java similarity index 66% rename from kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Context.java rename to kotlin/workers/src/io/bazel/ruleskotlin/workers/Context.java index 6ceb27d88..5bb4fadfe 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Context.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/Context.java @@ -13,17 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.bazel.ruleskotlin.workers.compilers.jvm; +package io.bazel.ruleskotlin.workers; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; public class Context { - private final EnumMap args = new EnumMap<>(Flag.class); + private final EnumMap args = new EnumMap<>(Flags.class); private final Map, Object> meta = new HashMap<>(); - private static final Map ALL_FIELDS_MAP = Arrays.stream(Flag.values()).collect(Collectors.toMap(x -> x.name, x -> x)); - private static final Flag[] MANDATORY_FIELDS = Arrays.stream(Flag.values()).filter(x -> x.mandatory).toArray(Flag[]::new); + private static final Map ALL_FIELDS_MAP = Arrays.stream(Flags.values()).collect(Collectors.toMap(x -> x.name, x -> x)); + private static final Flags[] MANDATORY_FIELDS = Arrays.stream(Flags.values()).filter(x -> x.mandatory).toArray(Flags[]::new); private Context(List args) { if (args.size() % 2 != 0) { @@ -33,27 +35,27 @@ private Context(List args) { for (int i = 0; i < args.size() / 2; i++) { String flag = args.get(i * 2); String value = args.get((i * 2) + 1); - Flag field = ALL_FIELDS_MAP.get(flag); + Flags field = ALL_FIELDS_MAP.get(flag); if (field == null) { throw new RuntimeException("unrecognised arg: " + flag); } this.args.put(field, value); } - for (Flag mandatoryField : MANDATORY_FIELDS) { + for (Flags mandatoryField : MANDATORY_FIELDS) { if (!this.args.containsKey(mandatoryField)) { throw new RuntimeException("mandatory arg missing: " + mandatoryField.name); } } } - static Context from(List args) { + public static Context from(List args) { return new Context(args); } - public EnumMap copyOfArgsContaining(Flag... fields) { - EnumMap result = new EnumMap<>(Flag.class); - for (Flag field : fields) { + public EnumMap of(Flags... fields) { + EnumMap result = new EnumMap<>(Flags.class); + for (Flags field : fields) { String value = args.get(field); if (value != null) { result.put(field, value); @@ -62,7 +64,15 @@ public EnumMap copyOfArgsContaining(Flag... fields) { return result; } - String get(Flag field) { + public interface Action extends Consumer { + } + + public void apply(Action... consumers) { + Stream.of(consumers).forEach(c -> c.accept(this)); + } + + + String get(Flags field) { return args.get(field); } @@ -70,6 +80,7 @@ String get(Flag field) { T get(Meta key) { return (T) meta.get(key); } + @SuppressWarnings("unchecked") T putIfAbsent(Meta key, T value) { return (T) meta.putIfAbsent(key, value); diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Flag.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/Flags.java similarity index 67% rename from kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Flag.java rename to kotlin/workers/src/io/bazel/ruleskotlin/workers/Flags.java index f827d1fbd..7aaa65a2f 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Flag.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/Flags.java @@ -13,14 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.bazel.ruleskotlin.workers.compilers.jvm; +package io.bazel.ruleskotlin.workers; -public enum Flag { - LABEL("--label", null, true), - OUTPUT_CLASSJAR("--output_classjar", "-d", true), + +public enum Flags { + // flags that line up with the java builder. + LABEL(JavaBuilderFlags.TARGET_LABEL.flag, null, true), + OUTPUT_CLASSJAR(JavaBuilderFlags.OUTPUT.flag, null, true), + SOURCES(JavaBuilderFlags.SOURCES.flag, null, true), + CLASSPATH(JavaBuilderFlags.CLASSPATH.flag, "-cp", true), + + // flags that could be aligned with the java builder. OUTPUT_JDEPS("--output_jdeps", null, true), - CLASSPATH("--classpath", "-cp", true), - SOURCES("--sources", null, true), + COMPILER_OUTPUT_BASE("--compiler_output_base", null, true), + + // flags for kotlin. KOTLIN_API_VERSION("--kotlin_api_version", "-api-version", false), KOTLIN_LANGUAGE_VERSION("--kotlin_language_version", "-language-version", false), KOTLIN_JVM_TARGET("--kotlin_jvm_target", "-jvm-target", false); @@ -29,7 +36,7 @@ public enum Flag { public final String kotlinFlag; final boolean mandatory; - Flag(String name, String kotlinName, boolean mandatory) { + Flags(String name, String kotlinName, boolean mandatory) { this.name = name; this.kotlinFlag = kotlinName; this.mandatory = mandatory; diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/JavaBuilderFlags.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/JavaBuilderFlags.java new file mode 100644 index 000000000..dcecc408e --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/JavaBuilderFlags.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers; + +/** + * Flags used by the java builder. + */ +@SuppressWarnings("unused") +public enum JavaBuilderFlags { + TARGET_LABEL("--target_label"), + CLASSPATH("--classpath"), + JAVAC_OPTS("--javacopts"), + DEPENDENCIES("--dependencies"), + DIRECT_DEPENDENCIES("--direct_dependencies"), + DIRECT_DEPENDENCY("--direct_dependency"), + INDIRECT_DEPENDENCY("--indirect_dependency"), + STRICT_JAVA_DEPS("--strict_java_deps"), + OUTPUT_DEPS_PROTO("--output_deps_proto"), + DEPS_ARTIFACTS("--deps_artifacts"), + REDUCE_CLASSPATH("--reduce_classpath"), + SOURCEGEN_DIR("--sourcegendir"), + GENERATED_SOURCES_OUTPUT("--generated_sources_output"), + OUTPUT_MANIFEST_PROTO("--output_manifest_proto"), + SOURCES("--sources"), + SOURCE_ROOTS("--source_roots"), + SOURCE_JARS("--source_jars"), + SOURCE_PATH("--sourcepath"), + BOOT_CLASSPATH("--bootclasspath"), + PROCESS_PATH("--processorpath"), + PROCESSORS("--processors"), + EXT_CLASSPATH("--extclasspath"), + EXT_DIR("--extdir"), + OUTPUT("--output"), + NATIVE_HEADER_OUTPUT("--native_header_output"), + CLASSDIR("--classdir"), + TEMPDIR("--tempdir"), + GENDIR("--gendir"), + POST_PROCESSOR("--post_processor"), + COMPRESS_JAR("--compress_jar"), + RULE_KIND("--rule_kind"), + TEST_ONLY("--testonly"); + + public final String flag; + + JavaBuilderFlags(String flag) { + this.flag = flag; + } +} diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/KotlinPreloadedCompilerBuilder.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/KotlinToolchain.java similarity index 69% rename from kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/KotlinPreloadedCompilerBuilder.java rename to kotlin/workers/src/io/bazel/ruleskotlin/workers/KotlinToolchain.java index f08cf98c4..4f8e5c17f 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/KotlinPreloadedCompilerBuilder.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/KotlinToolchain.java @@ -13,42 +13,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.bazel.ruleskotlin.workers.compilers.jvm.utils; +package io.bazel.ruleskotlin.workers; import io.bazel.ruleskotlin.workers.compilers.jvm.Locations; import org.jetbrains.kotlin.preloading.ClassPreloadingUtils; import org.jetbrains.kotlin.preloading.Preloader; -import java.io.File; +import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.Method; import java.nio.file.Paths; -import java.util.List; import java.util.function.BiFunction; -import java.util.stream.Collectors; -import java.util.stream.Stream; -public final class KotlinPreloadedCompilerBuilder { +public final class KotlinToolchain { private static final Object[] NO_ARGS = new Object[]{}; + private final ClassLoader classLoader; - private static final List PRELOAD_JARS = Stream.concat( - Locations.KOTLIN_REPO.verifiedRelativeFiles(Paths.get("lib", "kotlin-compiler.jar")), - Locations.JAVA_HOME.verifiedRelativeFiles(Paths.get("lib", "tools.jar")) - ).collect(Collectors.toList()); + public KotlinToolchain() throws IOException { + this.classLoader = ClassPreloadingUtils.preloadClasses( + Locations.KOTLIN_REPO.verifiedRelativeFiles( + Paths.get("lib", "kotlin-compiler.jar") + ), + Preloader.DEFAULT_CLASS_NUMBER_ESTIMATE, + Thread.currentThread().getContextClassLoader(), + null + ); + } + + public interface KotlinCompiler extends BiFunction { + } /** - * Load the Kotlin compiler and the javac tools.jar into a Preloading classloader. The Kotlin compiler is invoked reflectively to eventually allow + * Load the Kotlin compiler and the javac tools.jar into a Preloading classLoader. The Kotlin compiler is invoked reflectively to eventually allow * toolchain replacement. */ - public static BiFunction build() { + public KotlinCompiler kotlinCompiler() { try { - ClassLoader classLoader = ClassPreloadingUtils.preloadClasses( - PRELOAD_JARS, - Preloader.DEFAULT_CLASS_NUMBER_ESTIMATE, - Thread.currentThread().getContextClassLoader(), - null - ); - Class compilerClass = classLoader.loadClass("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler"); Class exitCodeClass = classLoader.loadClass("org.jetbrains.kotlin.cli.common.ExitCode"); diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/Meta.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/Meta.java new file mode 100644 index 000000000..94a071293 --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/Meta.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers; + +import java.util.Optional; + +public class Meta { + private final String id; + + private final T defaultValue; + + public Meta(String id) { + this.id = id; + this.defaultValue = null; + } + + @SuppressWarnings("unused") + private Meta(String id, T defaultValue) { + this.id = id; + this.defaultValue = defaultValue; + } + + /** + * Gets a mandatory value. + */ + public T mustGet(Context ctx) { + T res = ctx.get(this); + if(res == null) { + assert defaultValue != null : "mandatory meta parameter missing in context and does not have a default value"; + return defaultValue; + } + return res; + } + + /** + * Gets an optional value, if it has not been bound the default value is used. + */ + public Optional get(Context ctx) { + T res = ctx.get(this); + if( res != null) { + return Optional.of(res); + } else if(defaultValue != null) { + return Optional.of(defaultValue); + } else { + return Optional.empty(); + } + } + + public void bind(Context ctx, T value) { + if (ctx.putIfAbsent(this, value) != null) { + throw new RuntimeException("attempting to change bound meta variable " + id); + } + } +} diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/KotlinJvmBuilder.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/KotlinJvmBuilder.java index 71b881e26..d6bdf853e 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/KotlinJvmBuilder.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/KotlinJvmBuilder.java @@ -16,24 +16,35 @@ package io.bazel.ruleskotlin.workers.compilers.jvm; -import io.bazel.ruleskotlin.workers.BazelWorker; -import io.bazel.ruleskotlin.workers.CommandLineProgram; -import io.bazel.ruleskotlin.workers.compilers.jvm.actions.BuildAction; -import io.bazel.ruleskotlin.workers.compilers.jvm.actions.GenerateJdepsFile; -import io.bazel.ruleskotlin.workers.compilers.jvm.actions.KotlinCreateClassJar; -import io.bazel.ruleskotlin.workers.compilers.jvm.actions.KotlinMainCompile; +import io.bazel.ruleskotlin.workers.*; +import io.bazel.ruleskotlin.workers.compilers.jvm.actions.*; +import java.io.IOException; import java.util.List; /** * Bazel Kotlin Compiler worker. */ public final class KotlinJvmBuilder implements CommandLineProgram { - private static final BuildAction[] compileActions = new BuildAction[] { - KotlinMainCompile.INSTANCE, - KotlinCreateClassJar.INSTANCE, - GenerateJdepsFile.INSTANCE, - }; + private final BuildAction[] compileActions; + + private KotlinJvmBuilder() { + KotlinToolchain kotlinToolchain; + try { + kotlinToolchain = new KotlinToolchain(); + } catch (IOException e) { + throw new RuntimeException("could not initialize toolchain", e); + } + + compileActions = new BuildAction[]{ + Initialize.INSTANCE, + new KotlinMainCompile(kotlinToolchain), + new JavaMainCompile(), + KotlinRenderClassCompileResult.INSTANCE, + CreateOutputJar.INSTANCE, + GenerateJdepsFile.INSTANCE, + }; + } @Override public Integer apply(List args) { @@ -41,7 +52,7 @@ public Integer apply(List args) { Integer exitCode = 0; for (BuildAction action : compileActions) { exitCode = action.apply(context); - if(exitCode != 0) + if (exitCode != 0) break; } return exitCode; diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Locations.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Locations.java index f90e03197..c877564f8 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Locations.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Locations.java @@ -19,6 +19,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; public enum Locations { @@ -38,8 +40,8 @@ public final File resolveVerified(String... parts) { /** * Return a stream of paths that are known to exists relative to this location. */ - public final Stream verifiedRelativeFiles(Path... paths) { - return Stream.of(paths).map(relative -> verified(path.resolve(relative))); + public final List verifiedRelativeFiles(Path... paths) { + return Stream.of(paths).map(relative -> verified(path.resolve(relative))).collect(Collectors.toList()); } private File verified(Path target) { diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Meta.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Meta.java deleted file mode 100644 index f6f0c9cef..000000000 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Meta.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.compilers.jvm; - -import java.io.File; -import java.util.Optional; - -/** - * Meta is a key to some compilation state, it is stored in a {@link Context}. A meta is meant for setting up state for other actions. - */ -public final class Meta { - // if present contains the directory that classes were compiled to. - public static final Meta COMPILE_TO_DIRECTORY = new Meta<>("compile_to_jar"); - - private final String id; - - private Meta(String id) { - this.id = id; - } - - public Optional get(Context ctx) { - return Optional.ofNullable(ctx.get(this)); - } - - public void bind(Context ctx, T value) { - if (ctx.putIfAbsent(this, value) != null) { - throw new RuntimeException("attempting to change bound meta variable " + id); - } - } -} \ No newline at end of file diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Metas.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Metas.java new file mode 100644 index 000000000..eb8ff5496 --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/Metas.java @@ -0,0 +1,42 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.compilers.jvm; + +import io.bazel.ruleskotlin.workers.CompileResult; +import io.bazel.ruleskotlin.workers.Meta; + +import java.nio.file.Path; +import java.util.List; + +/** + * Meta is a key to some compilation state,. + */ +public class Metas { + // mandatory: the package part of the label. + public static final Meta PKG = new Meta<>("package"); + // mandatory: The target part of the label. + public static final Meta TARGET = new Meta<>("target"); + // mandatory: the class staging directory. + public static final Meta CLASSES_DIRECTORY = new Meta<>("class_directory"); + // mandatory: If this is non empty then it is a mixed mode operation. + public static final Meta> JAVA_SOURCES = new Meta<>("java_sources"); + // mandatory: + public static final Meta> ALL_SOURCES = new Meta<>("all_sources"); + // mandatory: + public static final CompileResult.Meta KOTLINC_RESULT = new CompileResult.Meta("kotlin_compile_result"); + // optional: when not a mixed mode operation. + public static final CompileResult.Meta JAVAC_RESULT = new CompileResult.Meta("javac_compile_result"); +} \ No newline at end of file diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/CreateOutputJar.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/CreateOutputJar.java new file mode 100644 index 000000000..aba021e4d --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/CreateOutputJar.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.compilers.jvm.actions; + + +import io.bazel.ruleskotlin.workers.BuildAction; +import io.bazel.ruleskotlin.workers.Context; +import io.bazel.ruleskotlin.workers.Flags; +import io.bazel.ruleskotlin.workers.compilers.jvm.Locations; +import io.bazel.ruleskotlin.workers.compilers.jvm.Metas; +import io.bazel.ruleskotlin.workers.utils.IOUtils; + +import java.util.Arrays; +import java.util.List; + +/** + * Create a jar from the classes. + */ +public final class CreateOutputJar implements BuildAction { + public static final CreateOutputJar INSTANCE = new CreateOutputJar(); + private static final String JAR_TOOL_PATH = Locations.JAVA_HOME.resolveVerified("bin", "jar").toString(); + + private CreateOutputJar() { + } + + @Override + public Integer apply(Context ctx) { + try { + List command = Arrays.asList(JAR_TOOL_PATH, + "cf", Flags.OUTPUT_CLASSJAR.get(ctx), + "-C", Metas.CLASSES_DIRECTORY.mustGet(ctx).toString(), + "."); + IOUtils.executeAndAwaitSuccess(10, command); + } catch (Exception e) { + throw new RuntimeException("unable to create class jar", e); + } + return 0; + } +} diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/GenerateJdepsFile.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/GenerateJdepsFile.java index dfcc20414..683cce57e 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/GenerateJdepsFile.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/GenerateJdepsFile.java @@ -16,10 +16,11 @@ package io.bazel.ruleskotlin.workers.compilers.jvm.actions; import com.google.devtools.build.lib.view.proto.Deps; -import io.bazel.ruleskotlin.workers.compilers.jvm.Context; +import io.bazel.ruleskotlin.workers.BuildAction; +import io.bazel.ruleskotlin.workers.Context; import io.bazel.ruleskotlin.workers.compilers.jvm.Locations; import io.bazel.ruleskotlin.workers.compilers.jvm.utils.JdepsParser; -import io.bazel.ruleskotlin.workers.compilers.jvm.utils.Utils; +import io.bazel.ruleskotlin.workers.utils.IOUtils; import java.io.FileOutputStream; import java.nio.file.Files; @@ -28,7 +29,8 @@ import java.util.List; import java.util.function.Predicate; -import static io.bazel.ruleskotlin.workers.compilers.jvm.Flag.*; +import static io.bazel.ruleskotlin.workers.Flags.*; + public final class GenerateJdepsFile implements BuildAction { private static final String JDEPS_PATH = Locations.JAVA_HOME.resolveVerified("bin", "jdeps").toString(); @@ -52,7 +54,7 @@ public Integer apply(Context ctx) { output = OUTPUT_JDEPS.get(ctx); Deps.Dependencies jdepsContent; try { - List jdepLines = Utils.waitForOutput(new String[]{JDEPS_PATH, "-cp", classPath, classJar}, System.err); + List jdepLines = IOUtils.executeAndWaitOutput(10, JDEPS_PATH, "-cp", classPath, classJar); jdepsContent = JdepsParser.parse( LABEL.get(ctx), classJar, @@ -61,7 +63,7 @@ public Integer apply(Context ctx) { IS_KOTLIN_IMPLICIT ); } catch (Exception e) { - throw new RuntimeException("error reading or parsing jdeps file", Utils.getRootCause(e)); + throw new RuntimeException("error reading or parsing jdeps file", IOUtils.getRootCause(e)); } try { @@ -71,7 +73,7 @@ public Integer apply(Context ctx) { jdepsContent.writeTo(fileOutputStream); } } catch (Exception e) { - throw new RuntimeException("error writing out jdeps file", Utils.getRootCause(e)); + throw new RuntimeException("error writing out jdeps file", IOUtils.getRootCause(e)); } return 0; diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/Initialize.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/Initialize.java new file mode 100644 index 000000000..2e175bcf7 --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/Initialize.java @@ -0,0 +1,108 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.compilers.jvm.actions; + + +import io.bazel.ruleskotlin.workers.BuildAction; +import io.bazel.ruleskotlin.workers.Context; +import io.bazel.ruleskotlin.workers.Flags; +import io.bazel.ruleskotlin.workers.Meta; +import io.bazel.ruleskotlin.workers.compilers.jvm.Metas; +import io.bazel.ruleskotlin.workers.utils.IOUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Should be the first step, does mandatory pre-processing. + */ +public final class Initialize implements BuildAction { + public static final Initialize INSTANCE = new Initialize(); + + private Initialize() { + } + + @Override + public Integer apply(Context ctx) { + ctx.apply( + Initialize::initializeAndBindBindDirectories, + Initialize::bindLabelComponents, + Initialize::bindSources + ); + return 0; + } + + private static void bindSources(Context ctx) { + List javaSources = new ArrayList<>(); + List allSources = new ArrayList<>(); + for (String src : Flags.SOURCES.get(ctx).split(":")) { + if (src.endsWith(".java")) { + javaSources.add(src); + allSources.add(src); + } else if (src.endsWith(".kt")) { + allSources.add(src); + } else { + throw new RuntimeException("unrecognised file type: " + src); + } + } + Metas.JAVA_SOURCES.bind(ctx, Collections.unmodifiableList(javaSources)); + Metas.ALL_SOURCES.bind(ctx, Collections.unmodifiableList(allSources)); + } + + private static void initializeAndBindBindDirectories(Context ctx) { + Path outputBase; + + try { + outputBase = Files.createDirectories(Paths.get(Flags.COMPILER_OUTPUT_BASE.get(ctx))); + } catch (IOException e) { + throw new RuntimeException("could not create compiler output base", e); + } + + try { + IOUtils.purgeDirectory(outputBase); + } catch (IOException e) { + throw new RuntimeException("could not purge output directory", e); + } + + createAndBindComponentDirectory(ctx, outputBase, Metas.CLASSES_DIRECTORY, "_classes"); + } + + private static void createAndBindComponentDirectory(Context ctx, Path outputBase, Meta key, String component) { + try { + key.bind(ctx, Files.createDirectories(outputBase.resolve(component))); + } catch (IOException e) { + throw new RuntimeException("could not create subdirectory for component " + component, e); + } + } + + /** + * parses the label, sets up the meta elements and returns the target part. + */ + private static void bindLabelComponents(Context ctx) { + String label = Flags.LABEL.get(ctx); + String[] parts = label.split(":"); + if (parts.length != 2) { + throw new RuntimeException("the label " + label + " is invalid"); + } + Metas.PKG.bind(ctx, parts[0]); + Metas.TARGET.bind(ctx, parts[1]); + } +} diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/JavaMainCompile.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/JavaMainCompile.java new file mode 100644 index 000000000..da6da7832 --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/JavaMainCompile.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.compilers.jvm.actions; + +import io.bazel.ruleskotlin.workers.BuildAction; +import io.bazel.ruleskotlin.workers.Context; +import io.bazel.ruleskotlin.workers.Flags; +import io.bazel.ruleskotlin.workers.compilers.jvm.*; +import io.bazel.ruleskotlin.workers.utils.IOUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Simple java compile action that invokes javac directly and simply. + */ +public final class JavaMainCompile implements BuildAction { + private static final String JAVAC_PATH = Locations.JAVA_HOME.resolveVerified("bin", "javac").toString(); + + public JavaMainCompile() {} + + @Override + public Integer apply(Context ctx) { + List javaSources = Metas.JAVA_SOURCES.mustGet(ctx); + if (!javaSources.isEmpty()) { + List args = new ArrayList<>(); + String classesDirectory = Metas.CLASSES_DIRECTORY.mustGet(ctx).toString(); + Collections.addAll(args, + JAVAC_PATH, "-cp", classesDirectory + "/:" + Flags.CLASSPATH.get(ctx), + "-d", classesDirectory + ); + args.addAll(javaSources); + Metas.JAVAC_RESULT.runAndBind(ctx, () -> IOUtils.executeAndAwait(30, args)); + } + return 0; + } +} + diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinCreateClassJar.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinCreateClassJar.java deleted file mode 100644 index 4edc9e923..000000000 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinCreateClassJar.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.compilers.jvm.actions; - -import io.bazel.ruleskotlin.workers.compilers.jvm.Context; -import io.bazel.ruleskotlin.workers.compilers.jvm.Flag; -import io.bazel.ruleskotlin.workers.compilers.jvm.Locations; -import io.bazel.ruleskotlin.workers.compilers.jvm.Meta; -import io.bazel.ruleskotlin.workers.compilers.jvm.utils.Utils; - -/** - * If classes for the main artifact were compiled to an intermediate temp directory turn them into a jar and clean up. - */ -public final class KotlinCreateClassJar implements BuildAction { - public static final KotlinCreateClassJar INSTANCE = new KotlinCreateClassJar(); - private static final String JAR_TOOL_PATH = Locations.JAVA_HOME.resolveVerified("bin", "jar").toString(); - - private KotlinCreateClassJar() {} - - @Override - public Integer apply(Context ctx) { - Meta.COMPILE_TO_DIRECTORY.get(ctx).ifPresent((classDirectory) -> { - try { - String classJarPath = Flag.OUTPUT_CLASSJAR.get(ctx); - Utils.waitForSuccess(new String[]{JAR_TOOL_PATH, "cf", classJarPath, "-C", classDirectory.toString(), "."}, System.err); - Utils.deleteDirectory(classDirectory.toPath()); - } catch (Exception e) { - throw new RuntimeException("unable to create class jar", e); - } - }); - return 0; - } -} diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinMainCompile.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinMainCompile.java index 6e69f9ea9..ad467fe06 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinMainCompile.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinMainCompile.java @@ -15,54 +15,40 @@ */ package io.bazel.ruleskotlin.workers.compilers.jvm.actions; -import io.bazel.ruleskotlin.workers.compilers.jvm.Context; -import io.bazel.ruleskotlin.workers.compilers.jvm.Flag; -import io.bazel.ruleskotlin.workers.compilers.jvm.Locations; -import io.bazel.ruleskotlin.workers.compilers.jvm.Meta; +import io.bazel.ruleskotlin.workers.*; +import io.bazel.ruleskotlin.workers.compilers.jvm.Metas; import io.bazel.ruleskotlin.workers.compilers.jvm.utils.KotlinCompilerOutputProcessor; -import io.bazel.ruleskotlin.workers.compilers.jvm.utils.KotlinPreloadedCompilerBuilder; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.file.Files; + import java.util.ArrayList; import java.util.Collections; -import java.util.EnumMap; import java.util.List; -import java.util.function.BiFunction; /** * Either compiles to a jar directly or when performing mixed-mode-compilation compiles to a temp directory first. + *

+ * Mixed-Mode: + *

+ * The Kotlin compiler is not suited for javac compilation as of 1.2.21. The errors are not conveyed directly and would need to be preprocessed, also javac + * invocations Configured via Kotlin use eager analysis in some corner cases this can result in classpath exceptions from the Java Compiler.. */ public final class KotlinMainCompile implements BuildAction { - public static final KotlinMainCompile INSTANCE = new KotlinMainCompile(KotlinPreloadedCompilerBuilder.build()); - - private static final String - JAVAC_PATH = Locations.JAVA_HOME.resolveVerified("bin", "javac").toString(); + private final KotlinToolchain.KotlinCompiler kotlinCompiler; - private static final String - X_COMPILE_JAVA_FLAG = "-Xcompile-java", - X_JAVAC_ARGUMENTS_FLAG = "-Xjavac-arguments", - X_USE_JAVAC_FLAG = "-Xuse-javac"; + public KotlinMainCompile(KotlinToolchain toolchains) { + this.kotlinCompiler = toolchains.kotlinCompiler(); + } /** * Default fields that are directly mappable to kotlin compiler args. */ - private static final Flag[] COMPILE_MAPPED_FLAGS = new Flag[]{ - Flag.OUTPUT_CLASSJAR, - Flag.CLASSPATH, - Flag.KOTLIN_API_VERSION, - Flag.KOTLIN_LANGUAGE_VERSION, - Flag.KOTLIN_JVM_TARGET + private static final Flags[] COMPILE_MAPPED_FLAGS = new Flags[]{ + Flags.CLASSPATH, + Flags.KOTLIN_API_VERSION, + Flags.KOTLIN_LANGUAGE_VERSION, + Flags.KOTLIN_JVM_TARGET }; - private final BiFunction compiler; - - private KotlinMainCompile(BiFunction compiler) { - this.compiler = compiler; - } - /** * Evaluate the compilation context and add Metadata to the ctx if needed. * @@ -70,46 +56,32 @@ private KotlinMainCompile(BiFunction compiler) { */ private static String[] setupCompileContext(Context ctx) { List args = new ArrayList<>(); - EnumMap compileMappedFields = ctx.copyOfArgsContaining(COMPILE_MAPPED_FLAGS); - String[] sources = Flag.SOURCES.get(ctx).split(":"); - - for (String source : sources) { - if (source.endsWith(".java")) { - try { - // Redirect the kotlin and java compilers to a temp directory. - File temporaryClassOutputDirectory = Files.createTempDirectory("kotlinCompile").toFile(); - Meta.COMPILE_TO_DIRECTORY.bind(ctx, temporaryClassOutputDirectory); - compileMappedFields.put(Flag.OUTPUT_CLASSJAR, temporaryClassOutputDirectory.toString()); - Collections.addAll(args, - X_COMPILE_JAVA_FLAG, - X_USE_JAVAC_FLAG + "=" + JAVAC_PATH, - X_JAVAC_ARGUMENTS_FLAG + "=-d=" + temporaryClassOutputDirectory.toString()); - break; - } catch (IOException e) { - throw new RuntimeException("could not create temp directory for kotlin compile operation", e); - } - } - } - compileMappedFields.forEach((field, arg) -> Collections.addAll(args, field.kotlinFlag, arg)); - Collections.addAll(args, sources); + Collections.addAll(args, "-d", Metas.CLASSES_DIRECTORY.mustGet(ctx).toString()); + ctx.of(COMPILE_MAPPED_FLAGS).forEach((field, arg) -> Collections.addAll(args, field.kotlinFlag, arg)); + args.addAll(Metas.ALL_SOURCES.mustGet(ctx)); return args.toArray(new String[args.size()]); } @Override public Integer apply(Context ctx) { - KotlinCompilerOutputProcessor outputProcessor = KotlinCompilerOutputProcessor.delegatingTo(System.out); - try { - Integer exitCode = compiler.apply(setupCompileContext(ctx), outputProcessor.getCollector()); - if (exitCode < 2) { - // 1 is a standard compilation error - // 2 is an internal error - // 3 is the script execution error + KotlinCompilerOutputProcessor outputProcessor; + outputProcessor = new KotlinCompilerOutputProcessor.ForKotlinC(System.out); + + final Integer exitCode = kotlinCompiler.apply(setupCompileContext(ctx), outputProcessor.getCollector()); + if (exitCode < 2) { + // 1 is a standard compilation error + // 2 is an internal error + // 3 is the script execution error + + // give javac a chance to process the java sources. + Metas.KOTLINC_RESULT.bind(ctx, CompileResult.deferred(exitCode, (c) -> { + outputProcessor.process(); return exitCode; - } else { - throw new RuntimeException("KotlinMainCompile returned terminal error code: " + exitCode); - } - } finally { + })); + return 0; + } else { outputProcessor.process(); + throw new RuntimeException("KotlinMainCompile returned terminal error code: " + exitCode); } } } diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinRenderClassCompileResult.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinRenderClassCompileResult.java new file mode 100644 index 000000000..76c86746b --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/actions/KotlinRenderClassCompileResult.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.compilers.jvm.actions; + + +import io.bazel.ruleskotlin.workers.BuildAction; +import io.bazel.ruleskotlin.workers.CompileResult; +import io.bazel.ruleskotlin.workers.Context; +import io.bazel.ruleskotlin.workers.compilers.jvm.Metas; + +import java.util.Optional; + + +/** + * Render the result of class compilation. This is a separate step at the moment for mixed mode compilation scenarios. If there is an error in Java sources in + * a large mixed mode package the Kotlin errors don't make any sense and overwhelm the console and intellij. The {@link KotlinMainCompile} step binds a deferred + * renderer and proceeds to lets javac compile the java sources. The step below merges the result of the two actions. + */ +public final class KotlinRenderClassCompileResult implements BuildAction { + public static final KotlinRenderClassCompileResult INSTANCE = new KotlinRenderClassCompileResult(); + + private KotlinRenderClassCompileResult() { + } + + @Override + public Integer apply(Context ctx) { + CompileResult kotlincResult = Metas.KOTLINC_RESULT.mustGet(ctx); + Optional javacResult = Metas.JAVAC_RESULT.get(ctx); + if (!javacResult.isPresent()) { + return kotlincResult.render(ctx); + } else { + try { + javacResult.get().propogateError("javac failed"); + if (kotlincResult.status() != 0) { + return kotlincResult.status(); + } else if (javacResult.get().status() != 0) { + // treat all javac statuses as non terminal compile errors. + return 1; + } + return 0; + } finally { + kotlincResult.render(ctx); + } + } + } +} diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/KotlinCompilerOutputProcessor.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/KotlinCompilerOutputProcessor.java index a8c18bad1..f40d07187 100644 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/KotlinCompilerOutputProcessor.java +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/KotlinCompilerOutputProcessor.java @@ -17,6 +17,7 @@ import java.io.*; import java.nio.file.Paths; +import java.util.stream.Collectors; /** @@ -25,29 +26,51 @@ */ // The kotlin compiler produces absolute file paths but the intellij plugin expects workspace root relative paths to // render errors. -public class KotlinCompilerOutputProcessor { - private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); +public abstract class KotlinCompilerOutputProcessor { + private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // Get the absolute path to ensure the sandbox root is resolved. private final String executionRoot = Paths.get("").toAbsolutePath().toString() + File.separator; - private final PrintStream delegate; - + final PrintStream delegate; private KotlinCompilerOutputProcessor(PrintStream delegate) { this.delegate = delegate; } - public static KotlinCompilerOutputProcessor delegatingTo(PrintStream delegate) { - return new KotlinCompilerOutputProcessor(delegate); - } - public PrintStream getCollector() { return new PrintStream(byteArrayOutputStream); } + public static class ForKotlinC extends KotlinCompilerOutputProcessor { + public ForKotlinC(PrintStream delegate) { + super(delegate); + } + + @Override + protected boolean processLine(String line) { + delegate.println(trimExecutionRootPrefix(line)); + return true; + } + } + + + final String trimExecutionRootPrefix(String toPrint) { + // trim off the workspace component + if (toPrint.startsWith(executionRoot)) { + return toPrint.replaceFirst(executionRoot, ""); + } + return toPrint; + } + + protected abstract boolean processLine(String line); + public void process() { - new BufferedReader(new InputStreamReader(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()))) - .lines() - .forEach(line -> delegate.println(line.replace(executionRoot, ""))); + for (String s : new BufferedReader(new InputStreamReader(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()))) + .lines().collect(Collectors.toList())) { + boolean shouldContinue = processLine(s); + if(!shouldContinue) { + break; + } + } delegate.flush(); } } diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/Utils.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/Utils.java deleted file mode 100644 index 3303d265e..000000000 --- a/kotlin/workers/src/io/bazel/ruleskotlin/workers/compilers/jvm/utils/Utils.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.compilers.jvm.utils; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Collectors; - -public final class Utils { - public static List waitForOutput(String[] command, PrintStream err) { - try { - ProcessBuilder builder = new ProcessBuilder(command); - Process process = builder.start(); - try (BufferedReader processError = new BufferedReader(new InputStreamReader(process.getErrorStream())); - BufferedReader output = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - while (true) { - String line = processError.readLine(); - if (line == null) - break; - err.println(line); - } - if (process.waitFor() != 0) { - throw new RuntimeException("non-zero return: " + process.exitValue()); - } - return output.lines().collect(Collectors.toList()); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static void waitForSuccess(String[] command, PrintStream err) { - try { - ProcessBuilder builder = new ProcessBuilder(command); - Process process = builder.start(); - try (BufferedReader in = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { - while (true) { - String line = in.readLine(); - if (line == null) - break; - err.println(line); - } - if (process.waitFor() != 0) { - throw new RuntimeException("non-zero return: " + process.exitValue()); - } - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - - public static void deleteDirectory(Path directory) throws IOException { - Files.walk(directory) - .map(Path::toFile) - .sorted((o1, o2) -> -o1.compareTo(o2)) - .forEach(File::delete); - - } - - public static Throwable getRootCause(Throwable e) { - Throwable cause; - Throwable result = e; - - while (null != (cause = result.getCause()) && (result != cause)) { - result = cause; - } - return result; - } -} diff --git a/kotlin/workers/src/io/bazel/ruleskotlin/workers/utils/IOUtils.java b/kotlin/workers/src/io/bazel/ruleskotlin/workers/utils/IOUtils.java new file mode 100644 index 000000000..d4e83eb5b --- /dev/null +++ b/kotlin/workers/src/io/bazel/ruleskotlin/workers/utils/IOUtils.java @@ -0,0 +1,133 @@ +/* + * Copyright 2018 The Bazel Authors. 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 io.bazel.ruleskotlin.workers.utils; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public final class IOUtils { + // sort this one out + public static List executeAndWaitOutput(int timeoutSeconds, String... command) { + try { + ProcessBuilder builder = new ProcessBuilder(command).redirectError(ProcessBuilder.Redirect.INHERIT); + Process process = builder.start(); + ArrayList al = new ArrayList<>(); + CompletableFuture streamReader = null; + try (BufferedReader output = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + streamReader = CompletableFuture.runAsync(() -> { + while (true) { + try { + String line = output.readLine(); + if (line == null) + break; + al.add(line); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }); + executeAwait(timeoutSeconds, process); + return al; + } finally { + if (streamReader != null && !streamReader.isDone()) { + streamReader.cancel(true); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static int executeAwait(int timeoutSeconds, Process process) throws TimeoutException { + try { + if (!process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { + throw new TimeoutException(); + } + return process.exitValue(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + if (process.isAlive()) { + process.destroy(); + } + } + } + + public static int executeAndAwait(int timeoutSeconds, List args) { + BufferedReader is = null; + BufferedReader es = null; + try { + ProcessBuilder builder = new ProcessBuilder(args.toArray(new String[args.size()])); + builder.redirectInput(ProcessBuilder.Redirect.PIPE); + builder.redirectError(ProcessBuilder.Redirect.PIPE); + Process process = builder.start(); + is = new BufferedReader(new InputStreamReader(process.getInputStream())); + es = new BufferedReader(new InputStreamReader(process.getErrorStream())); + return executeAwait(timeoutSeconds, process); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + drainStreamTo(System.out, is); + drainStreamTo(System.err, es); + } + } + + private static void drainStreamTo(PrintStream writer, BufferedReader reader) { + if (reader != null) { + reader.lines().forEach(writer::println); + try { + reader.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + public static void executeAndAwaitSuccess(int timeoutSeconds, List command) { + int status = executeAndAwait(timeoutSeconds, command); + if (status != 0) { + throw new RuntimeException("process failed with status: " + status); + } + } + + + public static void purgeDirectory(Path directory) throws IOException { + File directoryAsFile = directory.toFile(); + Files.walk(directory) + .map(Path::toFile) + .sorted((o1, o2) -> -o1.compareTo(o2)) + .filter(file -> !directoryAsFile.equals(file)) // nasty + .forEach(file -> { + assert !file.delete(); + }); + } + + public static Throwable getRootCause(Throwable e) { + Throwable cause; + Throwable result = e; + + while (null != (cause = result.getCause()) && (result != cause)) { + result = cause; + } + return result; + } +} diff --git a/tests/smoke/BUILD b/tests/smoke/BUILD index 794b455b8..99a28d70c 100644 --- a/tests/smoke/BUILD +++ b/tests/smoke/BUILD @@ -102,8 +102,13 @@ kotlin_binary( data=glob(["data/*"]), ) -kotlin_binary( +kotlin_library( name = "hellojava", - srcs = ["hellojava/HelloWorld.kt", "hellojava/MessageHolder.java"], - main_class = "hellojava.HelloWorldKt" + srcs = glob(["hellojava/*.kt", "hellojava/*.java"]), +) + +kotlin_library( + name = "hellojava_withmerge", + resources = glob(["resourcejar/**"]), + srcs = glob(["hellojava/*.kt", "hellojava/*.java"]), ) \ No newline at end of file diff --git a/tests/smoke/basic_tests.py b/tests/smoke/basic_tests.py index 610a96c12..3f9771283 100644 --- a/tests/smoke/basic_tests.py +++ b/tests/smoke/basic_tests.py @@ -58,8 +58,25 @@ def test_export_rt_propagation_via_dep(self): self.buildLaunchExpectingSuccess("propagation_rt_via_runtime_deps_consumer") def test_mixed_mode_compilation(self): - self.buildLaunchExpectingSuccess("hellojava") - + jar = self.buildJarGetZipFile("hellojava", "jar") + self.assertJarContains( + jar, + "hellojava/HelloWorldJava.class", + "hellojava/MessageHolderKotlin.class", + "hellojava/MessageHolder.class", + "hellojava/HelloWorldKt.class" + ) + + def test_mixed_mode_compilation_with_merge(self): + jar = self.buildJarGetZipFile("hellojava_withmerge", "jar") + self.assertJarContains( + jar, + "hellojava/HelloWorldJava.class", + "hellojava/MessageHolderKotlin.class", + "hellojava/MessageHolder.class", + "hellojava/HelloWorldKt.class", + "tests/smoke/resourcejar/pkg/file.txt" + ) # re-enable this test, and ensure the srcjar includes java sources when mixed mode. # def test_srcjar(self): # jar = self.buildJarGetZipFile("testresources", "srcjar") diff --git a/tests/smoke/hellojava/HelloWorldJava.java b/tests/smoke/hellojava/HelloWorldJava.java new file mode 100644 index 000000000..4aae5ae7c --- /dev/null +++ b/tests/smoke/hellojava/HelloWorldJava.java @@ -0,0 +1,23 @@ +/* + * Copyright 2018 The Bazel Authors. 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 hellojava; + +public class HelloWorldJava { + public static void main(String[] args) { + System.out.println(MessageHolderKotlin.INSTANCE.hello()); + } +} diff --git a/tests/smoke/hellojava/Another.java b/tests/smoke/hellojava/MessageHolderKotlin.kt similarity index 87% rename from tests/smoke/hellojava/Another.java rename to tests/smoke/hellojava/MessageHolderKotlin.kt index a8c2eda76..d7e511266 100644 --- a/tests/smoke/hellojava/Another.java +++ b/tests/smoke/hellojava/MessageHolderKotlin.kt @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package hellojava -package hellojava; - -public class Another { -} +object MessageHolderKotlin { + fun hello() = "hello from kotlin" +} \ No newline at end of file diff --git a/tests/smoke/propagation/CompileTimeDependent.java b/tests/smoke/propagation/CompileTimeDependent.java index e50502507..6d4770652 100644 --- a/tests/smoke/propagation/CompileTimeDependent.java +++ b/tests/smoke/propagation/CompileTimeDependent.java @@ -17,7 +17,6 @@ import org.junit.Test; - public class CompileTimeDependent { @Test public void justSoIcanUseTheTestAnnotation() {