blob: 671b1f301d6230d9a6117dc362c227e2c3a2125d [file] [log] [blame]
Googler1a617332017-01-07 16:48:16 +00001# Copyright 2016 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# Implementation of AndroidStudio-specific information collecting aspect.
16
17# A map to convert JavaApiFlavor to ProtoLibraryLegacyJavaIdeInfo.ApiFlavor
18_api_flavor_to_id = {
19 "FLAVOR_NONE": 0,
20 "FLAVOR_IMMUTABLE": 1,
21 "FLAVOR_MUTABLE": 2,
22 "FLAVOR_BOTH": 3,
23}
24
25# Compile-time dependency attributes, grouped by type.
26DEPS = struct(
27 label = [
28 "binary_under_test", # From android_test
29 "java_lib", # From proto_library
30 "_proto1_java_lib", # From proto_library
31 "_junit", # From android_robolectric_test
32 "_cc_toolchain", # From C rules
33 "module_target",
34 "_java_toolchain", # From java rules
35 "test",
36 ],
37 label_list = [
38 "deps",
39 "exports",
40 "_robolectric", # From android_robolectric_test
41 ],
42)
43
44# Run-time dependency attributes, grouped by type.
45RUNTIME_DEPS = struct(
46 label = [],
47 label_list = [
48 "runtime_deps",
49 ],
50)
51
52# All dependency attributes along which the aspect propagates, grouped by type.
53ALL_DEPS = struct(
54 label = DEPS.label + RUNTIME_DEPS.label,
55 label_list = DEPS.label_list + RUNTIME_DEPS.label_list,
56)
57
58LEGACY_RESOURCE_ATTR = "resources"
59
60##### Helpers
61
62def struct_omit_none(**kwargs):
63 """A replacement for standard `struct` function that omits the fields with None value."""
64 d = {name: kwargs[name] for name in kwargs if kwargs[name] != None}
65 return struct(**d)
66
67def artifact_location(file):
68 """Creates an ArtifactLocation proto from a File."""
69 if file == None:
70 return None
71
72 return struct_omit_none(
73 relative_path = get_relative_path(file),
74 is_source = file.is_source,
75 is_external = is_external(file.owner),
76 root_execution_path_fragment = file.root.path if not file.is_source else None,
77 )
78
79def is_external(label):
80 """Determines whether a label corresponds to an external artifact."""
81 return label.workspace_root.startswith("external")
82
83def get_relative_path(artifact):
84 """A temporary workaround to find the root-relative path from an artifact.
85
86 This is required because 'short_path' is incorrect for external source artifacts.
87
88 Args:
89 artifact: the input artifact
90 Returns:
91 string: the root-relative path for this artifact.
92 """
93 # TODO(bazel-team): remove this workaround when Artifact::short_path is fixed.
94 if artifact.is_source and is_external(artifact.owner) and artifact.short_path.startswith(".."):
95 # short_path is '../repo_name/path', we want 'external/repo_name/path'
96 return "external" + artifact.short_path[2:]
97 return artifact.short_path
98
99def source_directory_tuple(resource_file):
100 """Creates a tuple of (source directory, is_source, root execution path)."""
101 return (
102 str(android_common.resource_source_directory(resource_file)),
103 resource_file.is_source,
104 resource_file.root.path if not resource_file.is_source else None
105 )
106
107def all_unique_source_directories(resources):
108 """Builds a list of ArtifactLocation protos.
109
110 This is done for all source directories for a list of Android resources.
111 """
112 # Sets can contain tuples, but cannot contain structs.
113 # Use set of tuples to unquify source directories.
114 source_directory_tuples = set([source_directory_tuple(file) for file in resources])
115 return [struct_omit_none(relative_path = relative_path,
116 is_source = is_source,
117 root_execution_path_fragment = root_execution_path_fragment)
118 for (relative_path, is_source, root_execution_path_fragment) in source_directory_tuples]
119
120def build_file_artifact_location(ctx):
121 """Creates an ArtifactLocation proto representing a location of a given BUILD file."""
122 return struct(
123 relative_path = ctx.build_file_path,
124 is_source = True,
125 is_external = is_external(ctx.label)
126 )
127
128def library_artifact(java_output):
129 """Creates a LibraryArtifact representing a given java_output."""
130 if java_output == None or java_output.class_jar == None:
131 return None
132 return struct_omit_none(
133 jar = artifact_location(java_output.class_jar),
134 interface_jar = artifact_location(java_output.ijar),
135 source_jar = artifact_location(java_output.source_jar),
136 )
137
138def annotation_processing_jars(annotation_processing):
139 """Creates a LibraryArtifact representing Java annotation processing jars."""
140 return struct_omit_none(
141 jar = artifact_location(annotation_processing.class_jar),
142 source_jar = artifact_location(annotation_processing.source_jar),
143 )
144
145def jars_from_output(output):
146 """Collect jars for intellij-resolve-files from Java output."""
147 if output == None:
148 return []
149 return [jar
150 for jar in [output.class_jar, output.ijar, output.source_jar]
151 if jar != None and not jar.is_source]
152
153# TODO(salguarnieri) Remove once skylark provides the path safe string from a PathFragment.
154def replace_empty_path_with_dot(pathString):
155 return "." if len(pathString) == 0 else pathString
156
157def sources_from_target(context):
158 """
159 Get the list of sources from a target as artifact locations.
160
161 Returns the list of sources as artifact locations for a target or an empty list if no sources are
162 present.
163 """
164
165 if hasattr(context.rule.attr, "srcs"):
166 return [artifact_location(file)
167 for src in context.rule.attr.srcs
168 for file in src.files]
169 return []
170
171def collect_targets_from_attrs(rule_attrs, attrs):
172 """Returns a list of targets from the given attributes."""
173 list_deps = [dep for attr_name in attrs.label_list
174 if hasattr(rule_attrs, attr_name)
175 for dep in getattr(rule_attrs, attr_name)]
176
177 scalar_deps = [getattr(rule_attrs, attr_name) for attr_name in attrs.label
178 if hasattr(rule_attrs, attr_name)]
179
180 return [dep for dep in (list_deps + scalar_deps) if is_valid_aspect_target(dep)]
181
182def collect_transitive_exports(targets):
183 """Build a union of all export dependencies."""
184 result = set()
185 for dep in targets:
186 result = result | dep.export_deps
187 return result
188
189def get_legacy_resource_dep(rule_attrs):
190 """Gets the legacy 'resources' attribute."""
191 legacy_resource_target = None
192 if hasattr(rule_attrs, LEGACY_RESOURCE_ATTR):
193 dep = getattr(rule_attrs, LEGACY_RESOURCE_ATTR)
194 # resources can sometimes be a list attribute, in which case we don't want it
195 if dep and is_valid_aspect_target(dep):
196 legacy_resource_target = dep
197 return legacy_resource_target
198
199def targets_to_labels(targets):
200 """Returns a set of label strings for the given targets."""
201 return set([str(target.label) for target in targets])
202
203def list_omit_none(value):
204 """Returns a list of the value, or the empty list if None."""
205 return [value] if value else []
206
207def is_valid_aspect_target(target):
208 """Returns whether the target has had the aspect run on it."""
209 return hasattr(target, "intellij_aspect")
210
211super_secret_rule_name = "".join(["gen", "m", "p", "m"]) # Take that, leak test
212is_bazel = not hasattr(native, super_secret_rule_name)
213def tool_label(label_str):
214 """Returns a label that points to a blaze/bazel tool.
215
216 Will be removed once the aspect is migrated out of core.
217 """
218 return Label("@bazel_tools" + label_str if is_bazel else label_str)
219
220##### Builders for individual parts of the aspect output
221
222def build_py_ide_info(target, ctx):
223 """Build PyIdeInfo.
224
225 Returns a tuple of (PyIdeInfo proto, a set of intellij-resolve-files).
226 (or (None, empty set) if the target is not a python rule).
227 """
228 if not hasattr(target, "py"):
229 return (None, set())
230
231 sources = sources_from_target(ctx)
232 transitive_sources = target.py.transitive_sources
233
234 py_ide_info = struct_omit_none(
235 sources = sources,
236 )
237 return (py_ide_info, transitive_sources)
238
239def build_c_ide_info(target, ctx):
240 """Build CIdeInfo.
241
242 Returns a tuple of (CIdeInfo proto, a set of intellij-resolve-files).
243 (or (None, empty set) if the target is not a C rule).
244 """
245 if not hasattr(target, "cc"):
246 return (None, set())
247
248 sources = sources_from_target(ctx)
249
250 target_includes = []
251 if hasattr(ctx.rule.attr, "includes"):
252 target_includes = ctx.rule.attr.includes
253 target_defines = []
254 if hasattr(ctx.rule.attr, "defines"):
255 target_defines = ctx.rule.attr.defines
256 target_copts = []
257 if hasattr(ctx.rule.attr, "copts"):
258 target_copts = ctx.rule.attr.copts
259
260 cc_provider = target.cc
261
262 c_ide_info = struct_omit_none(
263 source = sources,
264 target_include = target_includes,
265 target_define = target_defines,
266 target_copt = target_copts,
267 transitive_include_directory = cc_provider.include_directories,
268 transitive_quote_include_directory = cc_provider.quote_include_directories,
269 transitive_define = cc_provider.defines,
270 transitive_system_include_directory = cc_provider.system_include_directories,
271 )
272 intellij_resolve_files = cc_provider.transitive_headers
273 return (c_ide_info, intellij_resolve_files)
274
275def build_c_toolchain_ide_info(target, ctx):
276 """Build CToolchainIdeInfo.
277
278 Returns a pair of (CToolchainIdeInfo proto, a set of intellij-resolve-files).
279 (or (None, empty set) if the target is not a cc_toolchain rule).
280 """
281
282 if ctx.rule.kind != "cc_toolchain":
283 return (None, set())
284
285 # This should exist because we requested it in our aspect definition.
286 cc_fragment = ctx.fragments.cpp
287
288 c_toolchain_ide_info = struct_omit_none(
289 target_name = cc_fragment.target_gnu_system_name,
290 base_compiler_option = cc_fragment.compiler_options(ctx.features),
291 c_option = cc_fragment.c_options,
292 cpp_option = cc_fragment.cxx_options(ctx.features),
293 link_option = cc_fragment.link_options,
294 unfiltered_compiler_option = cc_fragment.unfiltered_compiler_options(ctx.features),
295 preprocessor_executable = replace_empty_path_with_dot(
296 str(cc_fragment.preprocessor_executable)),
297 cpp_executable = str(cc_fragment.compiler_executable),
298 built_in_include_directory = [str(d)
299 for d in cc_fragment.built_in_include_directories],
300 )
301 return (c_toolchain_ide_info, set())
302
303def build_java_ide_info(target, ctx):
304 """
305 Build JavaIdeInfo.
306
307 Returns a pair of
308 (JavaIdeInfo proto, a set of ide-info-files, a set of intellij-resolve-files).
309 (or (None, empty set, empty set) if the target is not Java rule).
310 """
311 if not hasattr(target, "java") or ctx.rule.kind == "proto_library":
312 return (None, set(), set())
313
314 ide_info_files = set()
315 sources = sources_from_target(ctx)
316
317 jars = [library_artifact(output) for output in target.java.outputs.jars]
318 output_jars = [jar for output in target.java.outputs.jars for jar in jars_from_output(output)]
319 intellij_resolve_files = set(output_jars)
320
321 gen_jars = []
322 if target.java.annotation_processing and target.java.annotation_processing.enabled:
323 gen_jars = [annotation_processing_jars(target.java.annotation_processing)]
324 intellij_resolve_files = intellij_resolve_files | set([
325 jar for jar in [target.java.annotation_processing.class_jar,
326 target.java.annotation_processing.source_jar]
327 if jar != None and not jar.is_source])
328
329 jdeps = artifact_location(target.java.outputs.jdeps)
330
331 java_sources, gen_java_sources, srcjars = divide_java_sources(ctx)
332
333 # HACK -- android_library rules with the resources attribute do not support srcjar inputs
334 # to the filtered gen jar generation, because we don't want all resource classes in this jar.
335 # This can be removed once android_resources is deleted
336 if hasattr(ctx.rule.attr, LEGACY_RESOURCE_ATTR) and ctx.rule.kind.startswith("android_"):
337 srcjars = []
338
339 package_manifest = None
340 if java_sources:
341 package_manifest = build_java_package_manifest(ctx, target, java_sources, ".java-manifest")
342 ide_info_files = ide_info_files | set([package_manifest])
343
344 filtered_gen_jar = None
345 if java_sources and (gen_java_sources or srcjars):
346 filtered_gen_jar, filtered_gen_resolve_files = build_filtered_gen_jar(
347 ctx,
348 target,
349 gen_java_sources,
350 srcjars
351 )
352 intellij_resolve_files = intellij_resolve_files | filtered_gen_resolve_files
353
354 java_ide_info = struct_omit_none(
355 sources = sources,
356 jars = jars,
357 jdeps = jdeps,
358 generated_jars = gen_jars,
359 package_manifest = artifact_location(package_manifest),
360 filtered_gen_jar = filtered_gen_jar,
361 )
362 return (java_ide_info, ide_info_files, intellij_resolve_files)
363
364def build_java_package_manifest(ctx, target, source_files, suffix):
365 """Builds the java package manifest for the given source files."""
366 output = ctx.new_file(target.label.name + suffix)
367
368 args = []
369 args += ["--output_manifest", output.path]
370 args += ["--sources"]
371 args += [":".join([f.root.path + "," + f.short_path for f in source_files])]
372 argfile = ctx.new_file(ctx.configuration.bin_dir,
373 target.label.name + suffix + ".params")
374 ctx.file_action(output=argfile, content="\n".join(args))
375
376 ctx.action(
377 inputs = source_files + [argfile],
378 outputs = [output],
379 executable = ctx.executable._package_parser,
380 arguments = ["@" + argfile.path],
381 mnemonic = "JavaPackageManifest",
382 progress_message = "Parsing java package strings for " + str(target.label),
383 )
384 return output
385
386def build_filtered_gen_jar(ctx, target, gen_java_sources, srcjars):
387 """Filters the passed jar to contain only classes from the given manifest."""
388 jar_artifacts = []
389 source_jar_artifacts = []
390 for jar in target.java.outputs.jars:
391 if jar.ijar:
392 jar_artifacts.append(jar.ijar)
393 elif jar.class_jar:
394 jar_artifacts.append(jar.class_jar)
395 if jar.source_jar:
396 source_jar_artifacts.append(jar.source_jar)
397
398 filtered_jar = ctx.new_file(target.label.name + "-filtered-gen.jar")
399 filtered_source_jar = ctx.new_file(target.label.name + "-filtered-gen-src.jar")
400 args = []
401 args += ["--filter_jars"]
402 args += [":".join([jar.path for jar in jar_artifacts])]
403 args += ["--filter_source_jars"]
404 args += [":".join([jar.path for jar in source_jar_artifacts])]
405 args += ["--filtered_jar", filtered_jar.path]
406 args += ["--filtered_source_jar", filtered_source_jar.path]
407 if gen_java_sources:
408 args += ["--keep_java_files"]
409 args += [":".join([java_file.path for java_file in gen_java_sources])]
410 if srcjars:
411 args += ["--keep_source_jars"]
412 args += [":".join([source_jar.path for source_jar in srcjars])]
413 ctx.action(
414 inputs = jar_artifacts + source_jar_artifacts + gen_java_sources + srcjars,
415 outputs = [filtered_jar, filtered_source_jar],
416 executable = ctx.executable._jar_filter,
417 arguments = args,
418 mnemonic = "JarFilter",
419 progress_message = "Filtering generated code for " + str(target.label),
420 )
421 output_jar = struct(
422 jar=artifact_location(filtered_jar),
423 source_jar=artifact_location(filtered_source_jar),
424 )
425 intellij_resolve_files = set([filtered_jar, filtered_source_jar])
426 return output_jar, intellij_resolve_files
427
428def divide_java_sources(ctx):
429 """Divide sources into plain java, generated java, and srcjars."""
430
431 java_sources = []
432 gen_java_sources = []
433 srcjars = []
434 if hasattr(ctx.rule.attr, "srcs"):
435 srcs = ctx.rule.attr.srcs
436 for src in srcs:
437 for f in src.files:
438 if f.basename.endswith(".java"):
439 if f.is_source:
440 java_sources.append(f)
441 else:
442 gen_java_sources.append(f)
443 elif f.basename.endswith(".srcjar"):
444 srcjars.append(f)
445
446 return java_sources, gen_java_sources, srcjars
447
448def build_android_ide_info(target, ctx, legacy_resource_label):
449 """Build AndroidIdeInfo.
450
451 Returns a pair of (AndroidIdeInfo proto, a set of intellij-resolve-files).
452 (or (None, empty set) if the target is not Android rule).
453 """
454 if not hasattr(target, "android"):
455 return (None, set())
456
457 android = target.android
458 android_ide_info = struct_omit_none(
459 java_package = android.java_package,
460 idl_import_root = android.idl.import_root if hasattr(android.idl, "import_root") else None,
461 manifest = artifact_location(android.manifest),
462 apk = artifact_location(android.apk),
463 dependency_apk = [artifact_location(apk) for apk in android.apks_under_test],
464 has_idl_sources = android.idl.output != None,
465 idl_jar = library_artifact(android.idl.output),
466 generate_resource_class = android.defines_resources,
467 resources = all_unique_source_directories(android.resources),
468 resource_jar = library_artifact(android.resource_jar),
469 legacy_resources = legacy_resource_label,
470 )
471 intellij_resolve_files = set(jars_from_output(android.idl.output))
472
473 if android.manifest and not android.manifest.is_source:
474 intellij_resolve_files = intellij_resolve_files | set([android.manifest])
475
476 return (android_ide_info, intellij_resolve_files)
477
478def build_test_info(target, ctx):
479 """Build TestInfo"""
480 if not is_test_rule(ctx):
481 return None
482 return struct_omit_none(
483 size = ctx.rule.attr.size,
484 )
485
486def is_test_rule(ctx):
487 kind_string = ctx.rule.kind
488 return kind_string.endswith("_test")
489
490def build_proto_library_legacy_java_ide_info(target, ctx):
491 """Build ProtoLibraryLegacyJavaIdeInfo."""
492 if not hasattr(target, "proto_legacy_java"):
493 return None
494 proto_info = target.proto_legacy_java.legacy_info
495 return struct_omit_none(
496 api_version = proto_info.api_version,
497 api_flavor = _api_flavor_to_id[proto_info.api_flavor],
498 jars1 = [library_artifact(output) for output in proto_info.jars1],
499 jars_mutable = [library_artifact(output) for output in proto_info.jars_mutable],
500 jars_immutable = [library_artifact(output) for output in proto_info.jars_immutable],
501 )
502
503def build_java_toolchain_ide_info(target):
504 """Build JavaToolchainIdeInfo."""
505 if not hasattr(target, "java_toolchain"):
506 return None
507 toolchain_info = target.java_toolchain
508 return struct_omit_none(
509 source_version = toolchain_info.source_version,
510 target_version = toolchain_info.target_version,
511 )
512
513##### Main aspect function
514
515def _aspect_impl(target, ctx):
516 """Aspect implementation function."""
517 rule_attrs = ctx.rule.attr
518
519 # Collect direct dependencies
520 direct_dep_targets = collect_targets_from_attrs(rule_attrs, DEPS)
521
522 # Add exports from direct dependencies
523 exported_deps_from_deps = collect_transitive_exports(direct_dep_targets)
524 compiletime_deps = targets_to_labels(direct_dep_targets) | exported_deps_from_deps
525
526 # Propagate my own exports
527 export_deps = set()
528 if hasattr(target, "java"):
529 export_deps = set([str(l) for l in target.java.transitive_exports])
530 # Empty android libraries export all their dependencies.
531 if ctx.rule.kind == "android_library":
532 if not hasattr(rule_attrs, "srcs") or not ctx.rule.attr.srcs:
533 export_deps = export_deps | compiletime_deps
534
535 # runtime_deps
536 runtime_dep_targets = collect_targets_from_attrs(rule_attrs, RUNTIME_DEPS)
537 runtime_deps = targets_to_labels(runtime_dep_targets)
538
539 # resources
540 legacy_resource_target = get_legacy_resource_dep(rule_attrs)
541 legacy_resource_label = str(legacy_resource_target.label) if legacy_resource_target else None
542
543 # Roll up files from my prerequisites
544 prerequisites = direct_dep_targets + runtime_dep_targets + list_omit_none(legacy_resource_target)
545 intellij_info_text = set()
546 intellij_resolve_files = set()
547 intellij_compile_files = target.output_group("files_to_compile_INTERNAL_")
548 for dep in prerequisites:
549 intellij_info_text = intellij_info_text | dep.intellij_info_files.intellij_info_text
550 intellij_resolve_files = intellij_resolve_files | dep.intellij_info_files.intellij_resolve_files
551
552 # Collect python-specific information
553 (py_ide_info, py_resolve_files) = build_py_ide_info(target, ctx)
554 intellij_resolve_files = intellij_resolve_files | py_resolve_files
555
556 # Collect C-specific information
557 (c_ide_info, c_resolve_files) = build_c_ide_info(target, ctx)
558 intellij_resolve_files = intellij_resolve_files | c_resolve_files
559
560 (c_toolchain_ide_info, c_toolchain_resolve_files) = build_c_toolchain_ide_info(target, ctx)
561 intellij_resolve_files = intellij_resolve_files | c_toolchain_resolve_files
562
563 # Collect Java-specific information
564 (java_ide_info, java_ide_info_files, java_resolve_files) = build_java_ide_info(
565 target, ctx)
566 intellij_info_text = intellij_info_text | java_ide_info_files
567 intellij_resolve_files = intellij_resolve_files | java_resolve_files
568
569 # Collect Android-specific information
570 (android_ide_info, android_resolve_files) = build_android_ide_info(
571 target, ctx, legacy_resource_label)
572 intellij_resolve_files = intellij_resolve_files | android_resolve_files
573
574 # legacy proto_library support
575 proto_library_legacy_java_ide_info = build_proto_library_legacy_java_ide_info(target, ctx)
576
577 # java_toolchain
578 java_toolchain_ide_info = build_java_toolchain_ide_info(target)
579
580 # Collect test info
581 test_info = build_test_info(target, ctx)
582
583 # Build TargetIdeInfo proto
584 info = struct_omit_none(
585 label = str(target.label),
586 kind_string = ctx.rule.kind,
587 dependencies = list(compiletime_deps),
588 runtime_deps = list(runtime_deps),
589 build_file_artifact_location = build_file_artifact_location(ctx),
590 c_ide_info = c_ide_info,
591 c_toolchain_ide_info = c_toolchain_ide_info,
592 java_ide_info = java_ide_info,
593 android_ide_info = android_ide_info,
594 tags = ctx.rule.attr.tags,
595 test_info = test_info,
596 proto_library_legacy_java_ide_info = proto_library_legacy_java_ide_info,
597 java_toolchain_ide_info = java_toolchain_ide_info,
598 py_ide_info = py_ide_info,
599 )
600
601 # Output the ide information file.
602 output = ctx.new_file(target.label.name + ".intellij-info.txt")
603 ctx.file_action(output, info.to_proto())
604 intellij_info_text = intellij_info_text | set([output])
605
606 # Return providers.
607 return struct_omit_none(
608 intellij_aspect = True,
609 output_groups = {
610 "intellij-info-text" : intellij_info_text,
611 "intellij-resolve" : intellij_resolve_files,
612 "intellij-compile": intellij_compile_files,
613 },
614 intellij_info_files = struct(
615 intellij_info_text = intellij_info_text,
616 intellij_resolve_files = intellij_resolve_files,
617 ),
618 export_deps = export_deps,
619 )
620
621intellij_info_aspect = aspect(
622 attrs = {
623 "_package_parser": attr.label(
624 default = tool_label("//tools/android:PackageParser"),
625 cfg = "host",
626 executable = True,
627 allow_files = True),
628 "_jar_filter": attr.label(
629 default = tool_label("//tools/android:JarFilter"),
630 cfg = "host",
631 executable = True,
632 allow_files = True),
633 },
634 attr_aspects = ALL_DEPS.label + ALL_DEPS.label_list + [LEGACY_RESOURCE_ATTR],
635 fragments = ["cpp"],
636 implementation = _aspect_impl,
637)