• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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