# Copyright 2022 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. """Bazel java_binary rule""" load("@bazel_skylib//lib:paths.bzl", "paths") load("@rules_cc//cc:find_cc_toolchain.bzl", "use_cc_toolchain") load("//java/common:java_semantics.bzl", "semantics") load( "//java/common/rules:android_lint.bzl", "android_lint_subrule", ) load("//java/common/rules:java_binary.bzl", "BASIC_JAVA_BINARY_ATTRIBUTES") load("//java/common/rules:rule_util.bzl", "merge_attrs") load("//java/common/rules/impl:java_binary_deploy_jar.bzl", "create_deploy_archives") load("//java/common/rules/impl:java_binary_impl.bzl", "basic_java_binary") load("//java/common/rules/impl:java_helper.bzl", "helper") load("//java/private:java_info.bzl", "JavaInfo") def _bazel_java_binary_impl(ctx): return bazel_base_binary_impl(ctx, is_test_rule_class = False) + helper.executable_providers(ctx) def bazel_base_binary_impl(ctx, is_test_rule_class): """Common implementation for binaries and tests Args: ctx: (RuleContext) is_test_rule_class: (bool) Returns: [Provider] """ deps = _collect_all_targets_as_deps(ctx, classpath_type = "compile_only") runtime_deps = _collect_all_targets_as_deps(ctx) main_class = _check_and_get_main_class(ctx) coverage_main_class = main_class coverage_config = helper.get_coverage_config(ctx, _get_coverage_runner(ctx)) if coverage_config: main_class = coverage_config.main_class launcher_info = _get_launcher_info(ctx) executable = _get_executable(ctx) feature_config = helper.get_feature_config(ctx) if feature_config: strip_as_default = helper.should_strip_as_default(ctx, feature_config) else: # No C++ toolchain available. strip_as_default = False providers, default_info, jvm_flags = basic_java_binary( ctx, deps, runtime_deps, ctx.files.resources, main_class, coverage_main_class, coverage_config, launcher_info, executable, strip_as_default, is_test_rule_class = is_test_rule_class, ) if ctx.attr.use_testrunner: if semantics.find_java_runtime_toolchain(ctx).version >= 17: jvm_flags.append("-Djava.security.manager=allow") test_class = ctx.attr.test_class if hasattr(ctx.attr, "test_class") else "" if test_class == "": test_class = helper.primary_class(ctx) if test_class == None: fail("cannot determine test class. You might want to rename the " + " rule or add a 'test_class' attribute.") jvm_flags.extend([ "-ea", "-Dbazel.test_suite=" + helper.shell_escape(test_class), ]) java_attrs = providers["InternalDeployJarInfo"].java_attrs if executable: _create_stub(ctx, java_attrs, launcher_info.launcher, executable, jvm_flags, main_class, coverage_main_class) runfiles = default_info.runfiles if executable: runtime_toolchain = semantics.find_java_runtime_toolchain(ctx) runfiles = runfiles.merge(ctx.runfiles(transitive_files = runtime_toolchain.files)) test_support = helper.get_test_support(ctx) if test_support: runfiles = runfiles.merge(test_support[DefaultInfo].default_runfiles) providers["DefaultInfo"] = DefaultInfo( files = default_info.files, runfiles = runfiles, executable = default_info.executable, ) info = providers.pop("InternalDeployJarInfo") create_deploy_archives( ctx, info.java_attrs, launcher_info, main_class, coverage_main_class, info.strip_as_default, add_exports = info.add_exports, add_opens = info.add_opens, ) return providers.values() def _get_coverage_runner(ctx): if ctx.configuration.coverage_enabled and ctx.attr.create_executable: toolchain = semantics.find_java_toolchain(ctx) runner = toolchain.jacocorunner if not runner: fail("jacocorunner not set in java_toolchain: %s" % toolchain.label) runner_jar = runner.executable # wrap the jar in JavaInfo so we can add it to deps for java_common.compile() return JavaInfo(output_jar = runner_jar, compile_jar = runner_jar) return None def _collect_all_targets_as_deps(ctx, classpath_type = "all"): deps = helper.collect_all_targets_as_deps(ctx, classpath_type = classpath_type) if classpath_type == "compile_only" and ctx.fragments.java.enforce_explicit_java_test_deps(): return deps test_support = helper.get_test_support(ctx) if test_support: deps.append(test_support) return deps def _check_and_get_main_class(ctx): create_executable = ctx.attr.create_executable main_class = _get_main_class(ctx) if not create_executable and main_class: fail("main class must not be specified when executable is not created") if create_executable and not main_class: if not ctx.attr.srcs: fail("need at least one of 'main_class' or Java source files") main_class = helper.primary_class(ctx) if main_class == None: fail("main_class was not provided and cannot be inferred: " + "source path doesn't include a known root (java, javatests, src, testsrc)") return _get_main_class(ctx) def _get_main_class(ctx): if not ctx.attr.create_executable: return None main_class = _get_main_class_from_rule(ctx) if main_class == "": main_class = helper.primary_class(ctx) return main_class def _get_main_class_from_rule(ctx): main_class = ctx.attr.main_class if main_class: return main_class if ctx.attr.use_testrunner: return "com.google.testing.junit.runner.BazelTestRunner" return main_class def _get_launcher_info(ctx): launcher = helper.launcher_artifact_for_target(ctx) return struct( launcher = launcher, unstripped_launcher = launcher, runfiles = [], runtime_jars = [], jvm_flags = [], classpath_resources = [], ) def _get_executable(ctx): if not ctx.attr.create_executable: return None executable_name = ctx.label.name if helper.is_target_platform_windows(ctx): executable_name = executable_name + ".exe" return ctx.actions.declare_file(executable_name) def _create_stub(ctx, java_attrs, launcher, executable, jvm_flags, main_class, coverage_main_class): java_runtime_toolchain = semantics.find_java_runtime_toolchain(ctx) java_executable = helper.get_java_executable(ctx, java_runtime_toolchain, launcher) workspace_name = ctx.workspace_name workspace_prefix = workspace_name + ("/" if workspace_name else "") runfiles_enabled = helper.runfiles_enabled(ctx) coverage_enabled = ctx.configuration.coverage_enabled test_support = helper.get_test_support(ctx) test_support_jars = test_support[JavaInfo].transitive_runtime_jars if test_support else depset() classpath = depset( transitive = [ java_attrs.runtime_classpath, test_support_jars if ctx.fragments.java.enforce_explicit_java_test_deps() else depset(), ], ) if helper.is_target_platform_windows(ctx): jvm_flags_for_launcher = [] for flag in jvm_flags: jvm_flags_for_launcher.extend(ctx.tokenize(flag)) return _create_windows_exe_launcher(ctx, java_executable, classpath, main_class, jvm_flags_for_launcher, runfiles_enabled, executable) if runfiles_enabled: prefix = "" if helper.is_absolute_target_platform_path(ctx, java_executable) else "${JAVA_RUNFILES}/" java_bin = "JAVABIN=${JAVABIN:-" + prefix + java_executable + "}" else: java_bin = "JAVABIN=${JAVABIN:-$(rlocation " + java_executable + ")}" td = ctx.actions.template_dict() td.add_joined( "%classpath%", classpath, map_each = lambda file: _format_classpath_entry(runfiles_enabled, workspace_prefix, file), join_with = ctx.configuration.host_path_separator, format_joined = "\"%s\"", allow_closure = True, ) ctx.actions.expand_template( template = ctx.file._stub_template, output = executable, substitutions = { "%runfiles_manifest_only%": "" if runfiles_enabled else "1", "%workspace_prefix%": workspace_prefix, "%javabin%": java_bin, "%needs_runfiles%": "0" if helper.is_absolute_target_platform_path(ctx, java_runtime_toolchain.java_executable_exec_path) else "1", "%set_jacoco_metadata%": "", "%set_jacoco_main_class%": "export JACOCO_MAIN_CLASS=" + coverage_main_class if coverage_enabled else "", "%set_jacoco_java_runfiles_root%": "export JACOCO_JAVA_RUNFILES_ROOT=${JAVA_RUNFILES}/" + workspace_prefix if coverage_enabled else "", "%java_start_class%": helper.shell_escape(main_class), "%jvm_flags%": " ".join(jvm_flags), }, computed_substitutions = td, is_executable = True, ) return executable def _format_classpath_entry(runfiles_enabled, workspace_prefix, file): if runfiles_enabled: return "${RUNPATH}" + file.short_path return "$(rlocation " + paths.normalize(workspace_prefix + file.short_path) + ")" def _create_windows_exe_launcher(ctx, java_executable, classpath, main_class, jvm_flags_for_launcher, runfiles_enabled, executable): launch_info = ctx.actions.args().use_param_file("%s", use_always = True).set_param_file_format("multiline") launch_info.add("binary_type=Java") launch_info.add(ctx.workspace_name, format = "workspace_name=%s") launch_info.add("1" if runfiles_enabled else "0", format = "symlink_runfiles_enabled=%s") launch_info.add(java_executable, format = "java_bin_path=%s") launch_info.add(main_class, format = "java_start_class=%s") launch_info.add_joined(classpath, map_each = _short_path, join_with = ";", format_joined = "classpath=%s", omit_if_empty = False) launch_info.add_joined(jvm_flags_for_launcher, join_with = "\t", format_joined = "jvm_flags=%s", omit_if_empty = False) launch_info.add(semantics.find_java_runtime_toolchain(ctx).java_home_runfiles_path, format = "jar_bin_path=%s/bin/jar.exe") # TODO(b/295221112): Change to use the "launcher" attribute (only windows use a fixed _launcher attribute) launcher_artifact = ctx.executable._launcher ctx.actions.run( executable = ctx.executable._windows_launcher_maker, inputs = [launcher_artifact], outputs = [executable], arguments = [launcher_artifact.path, launch_info, executable.path], use_default_shell_env = True, ) return executable def _short_path(file): return file.short_path def _compute_test_support(use_testrunner): return Label(semantics.JAVA_TEST_RUNNER_LABEL) if use_testrunner else None def make_binary_rule(implementation, *, doc, attrs, executable = False, test = False, initializer = None): return rule( implementation = implementation, initializer = initializer, doc = doc, attrs = attrs, executable = executable, test = test, fragments = ["cpp", "java"], provides = [JavaInfo], toolchains = [semantics.JAVA_TOOLCHAIN] + use_cc_toolchain() + ( [semantics.JAVA_RUNTIME_TOOLCHAIN] if executable or test else [] ), # TODO(hvd): replace with filegroups? outputs = { "classjar": "%{name}.jar", "sourcejar": "%{name}-src.jar", "deploysrcjar": "%{name}_deploy-src.jar", "deployjar": "%{name}_deploy.jar", "unstrippeddeployjar": "%{name}_deploy.jar.unstripped", }, exec_groups = { "cpp_link": exec_group(toolchains = use_cc_toolchain()), }, subrules = [android_lint_subrule], ) BASE_BINARY_ATTRS = merge_attrs( BASIC_JAVA_BINARY_ATTRIBUTES, { "resource_strip_prefix": attr.string( doc = """ The path prefix to strip from Java resources.
If specified, this path prefix is stripped from every file in the resources
attribute. It is an error for a resource file not to be under this directory. If not
specified (the default), the path of resource file is determined according to the same
logic as the Java package of source files. For example, a source file at
stuff/java/foo/bar/a.txt will be located at foo/bar/a.txt.
Builds a Java archive ("jar file"), plus a wrapper shell script with the same name as the rule.
The wrapper shell script uses a classpath that includes, among other things, a jar file for each
library on which the binary depends. When running the wrapper shell script, any nonempty
JAVABIN environment variable will take precedence over the version specified via
Bazel's --java_runtime_version flag.
The wrapper script accepts several unique flags. Refer to
//src/main/java/com/google/devtools/build/lib/bazel/rules/java/java_stub_template.txt
for a list of configurable flags and environment variables accepted by the wrapper.
name.jar: A Java archive, containing the class files and other
resources corresponding to the binary's direct dependencies.name-src.jar: An archive containing the sources ("source
jar").name_deploy.jar: A Java archive suitable for deployment (only
built if explicitly requested).
Building the <name>_deploy.jar target for your rule
creates a self-contained jar file with a manifest that allows it to be run with the
java -jar command or with the wrapper script's --singlejar
option. Using the wrapper script is preferred to java -jar because it
also passes the JVM flags and the options
to load native libraries.
The deploy jar contains all the classes that would be found by a classloader that searched the classpath from the binary's wrapper script from beginning to end. It also contains the native libraries needed for dependencies. These are automatically loaded into the JVM at runtime.
If your target specifies a launcher attribute, then instead of being a normal JAR file, the _deploy.jar will be a native binary. This will contain the launcher plus any native (C++) dependencies of your rule, all linked into a static binary. The actual jar file's bytes will be appended to that native binary, creating a single binary blob containing both the executable and the Java code. You can execute the resulting jar file directly like you would execute any native binary.
name_deploy-src.jar: An archive containing the sources
collected from the transitive closure of the target. These will match the classes in the
deploy.jar except where jars have no matching source jar.
It is good practice to use the name of the source file that is the main entry point of the
application (minus the extension). For example, if your entry point is called
Main.java, then your name could be Main.
A deps attribute is not allowed in a java_binary rule without
srcs; such a rule requires a
main_class provided by
runtime_deps.
The following code snippet illustrates a common mistake:
java_binary(
name = "DontDoThis",
srcs = [
...,
"GeneratedJavaFile.java", # a generated .java file
],
deps = [":generating_rule",], # rule that generates that file
)
Do this instead:
java_binary(
name = "DoThisInstead",
srcs = [
...,
":generating_rule",
],
)
""",
attrs = merge_attrs(
BASE_BINARY_ATTRS,
({} if executable else {
"args": attr.string_list(),
"output_licenses": attr.string_list(),
}),
),
executable = executable,
)
java_binary = make_java_binary(executable = True)