1# Copyright 2022 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Common util functions for java_* rules""" 16 17load("@bazel_skylib//lib:paths.bzl", "paths") 18load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain") 19load("@rules_cc//cc/common:cc_common.bzl", "cc_common") 20load("@rules_cc//cc/common:cc_helper.bzl", "cc_helper") 21load("//java/common:java_semantics.bzl", "semantics") 22 23# copybara: default visibility 24 25def _collect_all_targets_as_deps(ctx, classpath_type = "all"): 26 deps = [] 27 if not classpath_type == "compile_only": 28 if hasattr(ctx.attr, "runtime_deps"): 29 deps.extend(ctx.attr.runtime_deps) 30 if hasattr(ctx.attr, "exports"): 31 deps.extend(ctx.attr.exports) 32 33 deps.extend(ctx.attr.deps or []) 34 35 launcher = _filter_launcher_for_target(ctx) 36 if launcher: 37 deps.append(launcher) 38 39 return deps 40 41def _filter_launcher_for_target(ctx): 42 # create_executable=0 disables the launcher 43 if hasattr(ctx.attr, "create_executable") and not ctx.attr.create_executable: 44 return None 45 46 # use_launcher=False disables the launcher 47 if hasattr(ctx.attr, "use_launcher") and not ctx.attr.use_launcher: 48 return None 49 50 # BUILD rule "launcher" attribute 51 if ctx.attr.launcher and cc_common.launcher_provider in ctx.attr.launcher: 52 return ctx.attr.launcher 53 54 return None 55 56def _launcher_artifact_for_target(ctx): 57 launcher = _filter_launcher_for_target(ctx) 58 if not launcher: 59 return None 60 files = launcher[DefaultInfo].files.to_list() 61 if len(files) != 1: 62 fail("%s expected a single artifact in %s" % (ctx.label, launcher)) 63 return files[0] 64 65def _check_and_get_main_class(ctx): 66 create_executable = ctx.attr.create_executable 67 use_testrunner = ctx.attr.use_testrunner 68 main_class = ctx.attr.main_class 69 70 if not create_executable and use_testrunner: 71 fail("cannot have use_testrunner without creating an executable") 72 if not create_executable and main_class: 73 fail("main class must not be specified when executable is not created") 74 if create_executable and not use_testrunner: 75 if not main_class: 76 if not ctx.attr.srcs: 77 fail("need at least one of 'main_class', 'use_testrunner' or Java source files") 78 main_class = _primary_class(ctx) 79 if main_class == None: 80 fail("main_class was not provided and cannot be inferred: " + 81 "source path doesn't include a known root (java, javatests, src, testsrc)") 82 if not create_executable: 83 return None 84 if not main_class: 85 if use_testrunner: 86 main_class = "com.google.testing.junit.runner.GoogleTestRunner" 87 else: 88 main_class = _primary_class(ctx) 89 return main_class 90 91def _primary_class(ctx): 92 if ctx.attr.srcs: 93 main = ctx.label.name + ".java" 94 for src in ctx.files.srcs: 95 if src.basename == main: 96 return _full_classname(_strip_extension(src)) 97 return _full_classname(_get_relative(ctx.label.package, ctx.label.name)) 98 99def _strip_extension(file): 100 return file.dirname + "/" + ( 101 file.basename[:-(1 + len(file.extension))] if file.extension else file.basename 102 ) 103 104# TODO(b/193629418): once out of builtins, create a canonical implementation and remove duplicates in depot 105def _full_classname(path): 106 java_segments = _java_segments(path) 107 return ".".join(java_segments) if java_segments != None else None 108 109def _java_segments(path): 110 if path.startswith("/"): 111 fail("path must not be absolute: '%s'" % path) 112 segments = path.split("/") 113 root_idx = -1 114 for idx, segment in enumerate(segments): 115 if segment in ["java", "javatests", "src", "testsrc"]: 116 root_idx = idx 117 break 118 if root_idx < 0: 119 return None 120 is_src = "src" == segments[root_idx] 121 check_mvn_idx = root_idx if is_src else -1 122 if (root_idx == 0 or is_src): 123 for i in range(root_idx + 1, len(segments) - 1): 124 segment = segments[i] 125 if "src" == segment or (is_src and (segment in ["java", "javatests"])): 126 next = segments[i + 1] 127 if next in ["com", "org", "net"]: 128 root_idx = i 129 elif "src" == segment: 130 check_mvn_idx = i 131 break 132 133 if check_mvn_idx >= 0 and check_mvn_idx < len(segments) - 2: 134 next = segments[check_mvn_idx + 1] 135 if next in ["main", "test"]: 136 next = segments[check_mvn_idx + 2] 137 if next in ["java", "resources"]: 138 root_idx = check_mvn_idx + 2 139 return segments[(root_idx + 1):] 140 141def _concat(*lists): 142 result = [] 143 for list in lists: 144 result.extend(list) 145 return result 146 147def _get_shared_native_deps_path( 148 linker_inputs, 149 link_opts, 150 linkstamps, 151 build_info_artifacts, 152 features, 153 is_test_target_partially_disabled_thin_lto): 154 """ 155 Returns the path of the shared native library. 156 157 The name must be generated based on the rule-specific inputs to the link actions. At this point 158 this includes order-sensitive list of linker inputs and options collected from the transitive 159 closure and linkstamp-related artifacts that are compiled during linking. All those inputs can 160 be affected by modifying target attributes (srcs/deps/stamp/etc). However, target build 161 configuration can be ignored since it will either change output directory (in case of different 162 configuration instances) or will not affect anything (if two targets use same configuration). 163 Final goal is for all native libraries that use identical linker command to use same output 164 name. 165 166 <p>TODO(bazel-team): (2010) Currently process of identifying parameters that can affect native 167 library name is manual and should be kept in sync with the code in the 168 CppLinkAction.Builder/CppLinkAction/Link classes which are responsible for generating linker 169 command line. Ideally we should reuse generated command line for both purposes - selecting a 170 name of the native library and using it as link action payload. For now, correctness of the 171 method below is only ensured by validations in the CppLinkAction.Builder.build() method. 172 """ 173 174 fp = "" 175 for artifact in linker_inputs: 176 fp += artifact.short_path 177 fp += str(len(link_opts)) 178 for opt in link_opts: 179 fp += opt 180 for artifact in linkstamps: 181 fp += artifact.short_path 182 for artifact in build_info_artifacts: 183 fp += artifact.short_path 184 for feature in features: 185 fp += feature 186 187 # Sharing of native dependencies may cause an ActionConflictException when ThinLTO is 188 # disabled for test and test-only targets that are statically linked, but enabled for other 189 # statically linked targets. This happens in case the artifacts for the shared native 190 # dependency are output by actions owned by the non-test and test targets both. To fix 191 # this, we allow creation of multiple artifacts for the shared native library - one shared 192 # among the test and test-only targets where ThinLTO is disabled, and the other shared among 193 # other targets where ThinLTO is enabled. 194 fp += "1" if is_test_target_partially_disabled_thin_lto else "0" 195 196 fingerprint = "%x" % hash(fp) 197 return "_nativedeps/" + fingerprint 198 199def _check_and_get_one_version_attribute(ctx, attr): 200 value = getattr(semantics.find_java_toolchain(ctx), attr) 201 return value 202 203def _jar_and_target_arg_mapper(jar): 204 # Emit pretty labels for targets in the main repository. 205 label = str(jar.owner) 206 if label.startswith("@@//"): 207 label = label.lstrip("@") 208 return jar.path + "," + label 209 210def _get_feature_config(ctx): 211 cc_toolchain = find_cc_toolchain(ctx, mandatory = False) 212 if not cc_toolchain: 213 return None 214 feature_config = cc_common.configure_features( 215 ctx = ctx, 216 cc_toolchain = cc_toolchain, 217 requested_features = ctx.features + ["java_launcher_link", "static_linking_mode"], 218 unsupported_features = ctx.disabled_features, 219 ) 220 return feature_config 221 222def _should_strip_as_default(ctx, feature_config): 223 fission_is_active = ctx.fragments.cpp.fission_active_for_current_compilation_mode() 224 create_per_obj_debug_info = fission_is_active and cc_common.is_enabled( 225 feature_name = "per_object_debug_info", 226 feature_configuration = feature_config, 227 ) 228 compilation_mode = ctx.var["COMPILATION_MODE"] 229 strip_as_default = create_per_obj_debug_info and compilation_mode == "opt" 230 231 return strip_as_default 232 233def _get_coverage_config(ctx, runner): 234 toolchain = semantics.find_java_toolchain(ctx) 235 if not ctx.configuration.coverage_enabled: 236 return None 237 runner = runner if ctx.attr.create_executable else None 238 manifest = ctx.actions.declare_file("runtime_classpath_for_coverage/%s/runtime_classpath.txt" % ctx.label.name) 239 singlejar = toolchain.single_jar 240 return struct( 241 runner = runner, 242 main_class = "com.google.testing.coverage.JacocoCoverageRunner", 243 manifest = manifest, 244 env = { 245 "JAVA_RUNTIME_CLASSPATH_FOR_COVERAGE": manifest.path, 246 "SINGLE_JAR_TOOL": singlejar.executable.path, 247 }, 248 support_files = [manifest, singlejar.executable], 249 ) 250 251def _get_java_executable(ctx, java_runtime_toolchain, launcher): 252 java_executable = launcher.short_path if launcher else java_runtime_toolchain.java_executable_runfiles_path 253 if not _is_absolute_target_platform_path(ctx, java_executable): 254 java_executable = ctx.workspace_name + "/" + java_executable 255 return paths.normalize(java_executable) 256 257def _has_target_constraints(ctx, constraints): 258 # Constraints is a label_list. 259 for constraint in constraints: 260 constraint_value = constraint[platform_common.ConstraintValueInfo] 261 if ctx.target_platform_has_constraint(constraint_value): 262 return True 263 return False 264 265def _is_target_platform_windows(ctx): 266 return _has_target_constraints(ctx, ctx.attr._windows_constraints) 267 268def _is_absolute_target_platform_path(ctx, path): 269 if _is_target_platform_windows(ctx): 270 return len(path) > 2 and path[1] == ":" 271 return path.startswith("/") 272 273def _runfiles_enabled(ctx): 274 return ctx.configuration.runfiles_enabled() 275 276def _get_test_support(ctx): 277 if ctx.attr.create_executable and ctx.attr.use_testrunner: 278 return ctx.attr._test_support 279 return None 280 281def _test_providers(ctx): 282 test_providers = [] 283 if _has_target_constraints(ctx, ctx.attr._apple_constraints): 284 test_providers.append(testing.ExecutionInfo({"requires-darwin": ""})) 285 286 test_env = {} 287 test_env.update(cc_helper.get_expanded_env(ctx, {})) 288 289 coverage_config = _get_coverage_config( 290 ctx, 291 runner = None, # we only need the environment 292 ) 293 if coverage_config: 294 test_env.update(coverage_config.env) 295 test_providers.append(testing.TestEnvironment( 296 environment = test_env, 297 inherited_environment = ctx.attr.env_inherit, 298 )) 299 300 return test_providers 301 302def _executable_providers(ctx): 303 if ctx.attr.create_executable: 304 return [RunEnvironmentInfo(cc_helper.get_expanded_env(ctx, {}))] 305 return [] 306 307def _resource_mapper(file): 308 root_relative_path = paths.relativize( 309 path = file.path, 310 start = paths.join(file.root.path, file.owner.workspace_root), 311 ) 312 return "%s:%s" % ( 313 file.path, 314 semantics.get_default_resource_path(root_relative_path, segment_extractor = _java_segments), 315 ) 316 317def _create_single_jar( 318 actions, 319 toolchain, 320 output, 321 sources = depset(), 322 resources = depset(), 323 mnemonic = "JavaSingleJar", 324 progress_message = "Building singlejar jar %{output}", 325 build_target = None, 326 output_creator = None): 327 """Register singlejar action for the output jar. 328 329 Args: 330 actions: (actions) ctx.actions 331 toolchain: (JavaToolchainInfo) The java toolchain 332 output: (File) Output file of the action. 333 sources: (depset[File]) The jar files to merge into the output jar. 334 resources: (depset[File]) The files to add to the output jar. 335 mnemonic: (str) The action identifier 336 progress_message: (str) The action progress message 337 build_target: (Label) The target label to stamp in the manifest. Optional. 338 output_creator: (str) The name of the tool to stamp in the manifest. Optional, 339 defaults to 'singlejar' 340 Returns: 341 (File) Output file which was used for registering the action. 342 """ 343 args = actions.args() 344 args.set_param_file_format("shell").use_param_file("@%s", use_always = True) 345 args.add("--output", output) 346 args.add_all( 347 [ 348 "--compression", 349 "--normalize", 350 "--exclude_build_data", 351 "--warn_duplicate_resources", 352 ], 353 ) 354 args.add_all("--sources", sources) 355 args.add_all("--resources", resources, map_each = _resource_mapper) 356 357 args.add("--build_target", build_target) 358 args.add("--output_jar_creator", output_creator) 359 360 actions.run( 361 mnemonic = mnemonic, 362 progress_message = progress_message, 363 executable = toolchain.single_jar, 364 toolchain = semantics.JAVA_TOOLCHAIN_TYPE, 365 inputs = depset(transitive = [resources, sources]), 366 tools = [toolchain.single_jar], 367 outputs = [output], 368 arguments = [args], 369 ) 370 return output 371 372# TODO(hvd): use skylib shell.quote() 373def _shell_escape(s): 374 """Shell-escape a string 375 376 Quotes a word so that it can be used, without further quoting, as an argument 377 (or part of an argument) in a shell command. 378 379 Args: 380 s: (str) the string to escape 381 382 Returns: 383 (str) the shell-escaped string 384 """ 385 if not s: 386 # Empty string is a special case: needs to be quoted to ensure that it 387 # gets treated as a separate argument. 388 return "''" 389 for c in s.elems(): 390 # We do this positively so as to be sure we don't inadvertently forget 391 # any unsafe characters. 392 if not c.isalnum() and c not in "@%-_+:,./": 393 return "'" + s.replace("'", "'\\''") + "'" 394 return s 395 396def _detokenize_javacopts(opts): 397 """Detokenizes a list of options to a depset. 398 399 Args: 400 opts: ([str]) the javac options to detokenize 401 402 Returns: 403 (depset[str]) depset of detokenized options 404 """ 405 return depset( 406 [" ".join([_shell_escape(opt) for opt in opts])], 407 order = "preorder", 408 ) 409 410def _derive_output_file(ctx, base_file, *, name_suffix = "", extension = None, extension_suffix = ""): 411 """Declares a new file whose name is derived from the given file 412 413 This method allows appending a suffix to the name (before extension), changing 414 the extension or appending a suffix after the extension. The new file is declared 415 as a sibling of the given base file. At least one of the three options must be 416 specified. It is an error to specify both `extension` and `extension_suffix`. 417 418 Args: 419 ctx: (RuleContext) the rule context. 420 base_file: (File) the file from which to derive the resultant file. 421 name_suffix: (str) Optional. The suffix to append to the name before the 422 extension. 423 extension: (str) Optional. The new extension to use (without '.'). By default, 424 the base_file's extension is used. 425 extension_suffix: (str) Optional. The suffix to append to the base_file's extension 426 427 Returns: 428 (File) the derived file 429 """ 430 if not name_suffix and not extension_suffix and not extension: 431 fail("At least one of name_suffix, extension or extension_suffix is required") 432 if extension and extension_suffix: 433 fail("only one of extension or extension_suffix can be specified") 434 if extension == None: 435 extension = base_file.extension 436 new_basename = paths.replace_extension(base_file.basename, name_suffix + "." + extension + extension_suffix) 437 return ctx.actions.declare_file(new_basename, sibling = base_file) 438 439def _is_stamping_enabled(ctx, stamp): 440 if ctx.configuration.is_tool_configuration(): 441 return 0 442 if stamp == 1 or stamp == 0: 443 return stamp 444 445 # stamp == -1 / auto 446 return int(ctx.configuration.stamp_binaries()) 447 448def _get_relative(path_a, path_b): 449 if paths.is_absolute(path_b): 450 return path_b 451 return paths.normalize(paths.join(path_a, path_b)) 452 453def _tokenize_javacopts(ctx = None, opts = []): 454 """Tokenizes a list or depset of options to a list. 455 456 Iff opts is a depset, we reverse the flattened list to ensure right-most 457 duplicates are preserved in their correct position. 458 459 If the ctx parameter is omitted, a slow, but pure Starlark, implementation 460 of shell tokenization is used. Otherwise, tokenization is performed using 461 ctx.tokenize() which has significantly better performance (up to 100x for 462 large options lists). 463 464 Args: 465 ctx: (RuleContext|None) the rule context 466 opts: (depset[str]|[str]) the javac options to tokenize 467 Returns: 468 [str] list of tokenized options 469 """ 470 if hasattr(opts, "to_list"): 471 opts = reversed(opts.to_list()) 472 if ctx: 473 return [ 474 token 475 for opt in opts 476 for token in ctx.tokenize(opt) 477 ] 478 else: 479 # TODO: optimize and use the pure Starlark implementation in cc_helper 480 return semantics.tokenize_javacopts(opts) 481 482helper = struct( 483 collect_all_targets_as_deps = _collect_all_targets_as_deps, 484 filter_launcher_for_target = _filter_launcher_for_target, 485 launcher_artifact_for_target = _launcher_artifact_for_target, 486 check_and_get_main_class = _check_and_get_main_class, 487 primary_class = _primary_class, 488 strip_extension = _strip_extension, 489 concat = _concat, 490 get_shared_native_deps_path = _get_shared_native_deps_path, 491 check_and_get_one_version_attribute = _check_and_get_one_version_attribute, 492 jar_and_target_arg_mapper = _jar_and_target_arg_mapper, 493 get_feature_config = _get_feature_config, 494 should_strip_as_default = _should_strip_as_default, 495 get_coverage_config = _get_coverage_config, 496 get_java_executable = _get_java_executable, 497 is_absolute_target_platform_path = _is_absolute_target_platform_path, 498 is_target_platform_windows = _is_target_platform_windows, 499 runfiles_enabled = _runfiles_enabled, 500 get_test_support = _get_test_support, 501 test_providers = _test_providers, 502 executable_providers = _executable_providers, 503 create_single_jar = _create_single_jar, 504 shell_escape = _shell_escape, 505 detokenize_javacopts = _detokenize_javacopts, 506 tokenize_javacopts = _tokenize_javacopts, 507 derive_output_file = _derive_output_file, 508 is_stamping_enabled = _is_stamping_enabled, 509 get_relative = _get_relative, 510) 511