1# Copyright (C) 2018 The Dagger Authors.
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"""Macros to simplify generating maven files.
16"""
17
18load("@google_bazel_common//tools/maven:pom_file.bzl", default_pom_file = "pom_file")
19load(":maven_info.bzl", "MavenInfo", "collect_maven_info")
20load("@google_bazel_common//tools/javadoc:javadoc.bzl", "javadoc_library")
21load("@google_bazel_common//tools/jarjar:jarjar.bzl", "jarjar_library")
22
23def pom_file(name, targets, artifact_name, artifact_id, packaging = None, **kwargs):
24    default_pom_file(
25        name = name,
26        targets = targets,
27        preferred_group_ids = [
28            "com.google.dagger",
29            "com.google",
30        ],
31        template_file = "//tools:pom-template.xml",
32        substitutions = {
33            "{artifact_name}": artifact_name,
34            "{artifact_id}": artifact_id,
35            "{packaging}": packaging or "jar",
36        },
37        excluded_artifacts = ["com.google.auto:auto-common"],
38        **kwargs
39    )
40
41def gen_maven_artifact(
42        name,
43        artifact_name,
44        artifact_coordinates,
45        artifact_target,
46        artifact_target_libs = None,
47        artifact_target_maven_deps = None,
48        artifact_target_maven_deps_banned = None,
49        testonly = 0,
50        pom_name = "pom",
51        packaging = None,
52        javadoc_srcs = None,
53        javadoc_root_packages = None,
54        javadoc_exclude_packages = None,
55        javadoc_android_api_level = None,
56        shaded_deps = None,
57        shaded_rules = None,
58        manifest = None,
59        lint_deps = None,
60        proguard_specs = None):
61    _gen_maven_artifact(
62        name,
63        artifact_name,
64        artifact_coordinates,
65        artifact_target,
66        artifact_target_libs,
67        artifact_target_maven_deps,
68        artifact_target_maven_deps_banned,
69        testonly,
70        pom_name,
71        packaging,
72        javadoc_srcs,
73        javadoc_root_packages,
74        javadoc_exclude_packages,
75        javadoc_android_api_level,
76        shaded_deps,
77        shaded_rules,
78        manifest,
79        lint_deps,
80        proguard_specs
81    )
82
83def _gen_maven_artifact(
84        name,
85        artifact_name,
86        artifact_coordinates,
87        artifact_target,
88        artifact_target_libs,
89        artifact_target_maven_deps,
90        artifact_target_maven_deps_banned,
91        testonly,
92        pom_name,
93        packaging,
94        javadoc_srcs,
95        javadoc_root_packages,
96        javadoc_exclude_packages,
97        javadoc_android_api_level,
98        shaded_deps,
99        shaded_rules,
100        manifest,
101        lint_deps,
102        proguard_specs):
103    """Generates the files required for a maven artifact.
104
105    This macro generates the following targets:
106        * ":pom": The pom file for the given target and deps
107        * ":<NAME>": The artifact file for the given target and deps
108        * ":<NAME>-src": The sources jar file for the given target and deps
109        * ":<NAME>-javadoc": The javadocs jar file for the given target and deps
110
111    This macro also validates a few things. First, it validates that the
112    given "target" is a maven artifact (i.e. the "tags" attribute contains
113    "maven_coordinates=..."). Second, it calculates the list of transitive
114    dependencies of the target that are not owned by another maven artifact,
115    and validates that the given "deps" matches exactly.
116
117    Args:
118      name: The name associated with the various output targets.
119      artifact_target: The target containing the maven_coordinates.
120      artifact_name: The name of the maven artifact.
121      artifact_coordinates: The coordinates of the maven artifact in the
122                            form: "<group_id>:<artifact_id>:<version>"
123      artifact_target_libs: The set of transitive libraries of the target.
124      artifact_target_maven_deps: The required maven deps of the target.
125      artifact_target_maven_deps_banned: The banned maven deps of the target.
126      testonly: True if the jar should be testonly.
127      packaging: The packaging of the maven artifact. E.g. "aar"
128      pom_name: The name of the pom file (or "pom" if absent).
129      javadoc_srcs: The srcs for the javadocs.
130      javadoc_root_packages: The root packages for the javadocs.
131      javadoc_exclude_packages: The packages to exclude from the javadocs.
132      javadoc_android_api_level: The android api level for the javadocs.
133      shaded_deps: The shaded deps for the jarjar.
134      shaded_rules: The shaded rules for the jarjar.
135      manifest: The AndroidManifest.xml to bundle in when packaing an 'aar'.
136      lint_deps: The lint targets to be bundled in when packaging an 'aar'.
137      proguard_specs: The proguard spec files to be bundled in when packaging an 'aar'
138    """
139
140    _validate_maven_deps(
141        name = name + "-validation",
142        testonly = 1,
143        target = artifact_target,
144        expected_artifact = artifact_coordinates,
145        expected_libs = artifact_target_libs,
146        expected_maven_deps = artifact_target_maven_deps,
147        banned_maven_deps = artifact_target_maven_deps_banned,
148    )
149
150    shaded_deps = shaded_deps or []
151    shaded_rules = shaded_rules or []
152    artifact_targets = [artifact_target] + (artifact_target_libs or [])
153    lint_deps = lint_deps or []
154
155    # META-INF resources files that can be combined by appending lines.
156    merge_meta_inf_files = [
157        "gradle/incremental.annotation.processors",
158    ]
159
160    artifact_id = artifact_coordinates.split(":")[1]
161    pom_file(
162        name = pom_name,
163        testonly = testonly,
164        artifact_id = artifact_id,
165        artifact_name = artifact_name,
166        packaging = packaging,
167        targets = artifact_targets,
168    )
169
170    if (packaging == "aar"):
171        jarjar_library(
172            name = name + "-classes",
173            testonly = testonly,
174            jars = artifact_targets + shaded_deps,
175            rules = shaded_rules,
176            merge_meta_inf_files = merge_meta_inf_files,
177        )
178        if lint_deps:
179            # jarjar all lint artifacts since an aar only contains a single lint.jar.
180            jarjar_library(
181                name = name + "-lint",
182                jars = lint_deps,
183            )
184            lint_jar_name = name + "-lint.jar"
185        else:
186            lint_jar_name = None
187
188        if proguard_specs:
189            # Concatenate all proguard rules since an aar only contains a single proguard.txt
190            native.genrule(
191                name = name + "-proguard",
192                srcs = proguard_specs,
193                outs = [name + "-proguard.txt"],
194                cmd = "cat $(SRCS) > $@",
195            )
196            proguard_file = name + "-proguard.txt"
197        else:
198            proguard_file = None
199
200        _package_android_library(
201            name = name + "-android-lib",
202            manifest = manifest,
203            classesJar = name + "-classes.jar",
204            lintJar = lint_jar_name,
205            proguardSpec = proguard_file,
206        )
207
208        # Copy intermediate outputs to final one.
209        native.genrule(
210            name = name,
211            srcs = [name + "-android-lib"],
212            outs = [name + ".aar"],
213            cmd = "cp $< $@",
214        )
215    else:
216        jarjar_library(
217            name = name,
218            testonly = testonly,
219            jars = artifact_targets + shaded_deps,
220            rules = shaded_rules,
221            merge_meta_inf_files = merge_meta_inf_files,
222        )
223
224    jarjar_library(
225        name = name + "-src",
226        testonly = testonly,
227        jars = [_src_jar(dep) for dep in artifact_targets],
228        merge_meta_inf_files = merge_meta_inf_files,
229    )
230
231    if javadoc_srcs != None:
232        javadoc_library(
233            name = name + "-javadoc",
234            srcs = javadoc_srcs,
235            testonly = testonly,
236            root_packages = javadoc_root_packages,
237            exclude_packages = javadoc_exclude_packages,
238            android_api_level = javadoc_android_api_level,
239            deps = artifact_targets,
240        )
241    else:
242        # Build an empty javadoc because Sonatype requires javadocs
243        # even if the jar is empty.
244        # https://central.sonatype.org/pages/requirements.html#supply-javadoc-and-sources
245        native.java_binary(
246            name = name + "-javadoc",
247        )
248
249def _src_jar(target):
250    if target.startswith(":"):
251        target = Label("//" + native.package_name() + target)
252    else:
253        target = Label(target)
254    return "//%s:lib%s-src.jar" % (target.package, target.name)
255
256def _validate_maven_deps_impl(ctx):
257    """Validates the given Maven target and deps
258
259    Validates that the given "target" is a maven artifact (i.e. the "tags"
260    attribute contains "maven_coordinates=..."). Second, it calculates the
261    list of transitive dependencies of the target that are not owned by
262    another maven artifact, and validates that the given "deps" matches
263    exactly.
264    """
265    target = ctx.attr.target
266    artifact = target[MavenInfo].artifact
267    if not artifact:
268        fail("\t[Error]: %s is not a maven artifact" % target.label)
269
270    if artifact != ctx.attr.expected_artifact:
271        fail(
272            "\t[Error]: %s expected artifact, %s, but was: %s" % (
273                target.label,
274                ctx.attr.expected_artifact,
275                artifact,
276            ),
277        )
278
279    all_transitive_deps = target[MavenInfo].all_transitive_deps.to_list()
280    maven_nearest_artifacts = target[MavenInfo].maven_nearest_artifacts.to_list()
281    maven_transitive_deps = target[MavenInfo].maven_transitive_deps.to_list()
282
283    expected_libs = [dep.label for dep in getattr(ctx.attr, "expected_libs", [])]
284    actual_libs = [dep for dep in all_transitive_deps if dep not in maven_transitive_deps]
285    _validate_list("artifact_target_libs", actual_libs, expected_libs)
286
287    expected_maven_deps = [dep for dep in getattr(ctx.attr, "expected_maven_deps", [])]
288    actual_maven_deps = [_strip_artifact_version(artifact) for artifact in maven_nearest_artifacts]
289    _validate_list(
290        "artifact_target_maven_deps",
291        actual_maven_deps,
292        expected_maven_deps,
293        ctx.attr.banned_maven_deps,
294    )
295
296def _validate_list(name, actual_list, expected_list, banned_list = []):
297    missing = sorted(['"{}",'.format(x) for x in actual_list if x not in expected_list])
298    if missing:
299        fail("\t[Error]: Found missing {}: \n\t\t".format(name) + "\n\t\t".join(missing))
300
301    extra = sorted(['"{}",'.format(x) for x in expected_list if x not in actual_list])
302    if extra:
303        fail("\t[Error]: Found extra {}: \n\t\t".format(name) + "\n\t\t".join(extra))
304
305    banned = sorted(['"{}",'.format(x) for x in actual_list if x in banned_list])
306    if banned:
307        fail("\t[Error]: Found banned {}: \n\t\t".format(name) + "\n\t\t".join(banned))
308
309def _strip_artifact_version(artifact):
310    return artifact.rsplit(":", 1)[0]
311
312_validate_maven_deps = rule(
313    implementation = _validate_maven_deps_impl,
314    attrs = {
315        "target": attr.label(
316            doc = "The target to generate a maven artifact for.",
317            aspects = [collect_maven_info],
318            mandatory = True,
319        ),
320        "expected_artifact": attr.string(
321            doc = "The artifact name of the target.",
322            mandatory = True,
323        ),
324        "expected_libs": attr.label_list(
325            doc = "The set of transitive libraries of the target, if any.",
326        ),
327        "expected_maven_deps": attr.string_list(
328            doc = "The required maven dependencies of the target, if any.",
329        ),
330        "banned_maven_deps": attr.string_list(
331            doc = "The required maven dependencies of the target, if any.",
332        ),
333    },
334)
335
336def _package_android_library_impl(ctx):
337    """A very, very simple Android Library (aar) packaging rule.
338
339    This rule only support packaging simple android libraries. No resources
340    support, assets, extra libs, nor jni. This rule is needed because
341    there is no 'JarJar equivalent' for AARs and some of our artifacts are
342    composed of sources spread across multiple android_library targets.
343
344    See: https://developer.android.com/studio/projects/android-library.html#aar-contents
345    """
346    inputs = [ctx.file.manifest, ctx.file.classesJar]
347    if ctx.file.lintJar:
348        inputs.append(ctx.file.lintJar)
349    if ctx.file.proguardSpec:
350        inputs.append(ctx.file.proguardSpec)
351
352    ctx.actions.run_shell(
353        inputs = inputs,
354        outputs = [ctx.outputs.aar],
355        command = """
356            TMPDIR="$(mktemp -d)"
357            cp {manifest} $TMPDIR/AndroidManifest.xml
358            cp {classesJar} $TMPDIR/classes.jar
359            if [[ -a {lintJar} ]]; then
360                cp {lintJar} $TMPDIR/lint.jar
361            fi
362            if [[ -a {proguardSpec} ]]; then
363                cp {proguardSpec} $TMPDIR/proguard.txt
364            fi
365            touch $TMPDIR/R.txt
366            zip -j {outputFile} $TMPDIR/*
367            """.format(
368            manifest = ctx.file.manifest.path,
369            classesJar = ctx.file.classesJar.path,
370            lintJar = ctx.file.lintJar.path if ctx.file.lintJar else "none",
371            proguardSpec = ctx.file.proguardSpec.path if ctx.file.proguardSpec else "none",
372            outputFile = ctx.outputs.aar.path,
373        ),
374    )
375
376_package_android_library = rule(
377    implementation = _package_android_library_impl,
378    attrs = {
379        "manifest": attr.label(
380            doc = "The AndroidManifest.xml file.",
381            allow_single_file = True,
382            mandatory = True,
383        ),
384        "classesJar": attr.label(
385            doc = "The classes.jar file.",
386            allow_single_file = True,
387            mandatory = True,
388        ),
389        "lintJar": attr.label(
390            doc = "The lint.jar file.",
391            allow_single_file = True,
392            mandatory = False,
393        ),
394        "proguardSpec": attr.label(
395            doc = "The proguard.txt file.",
396            allow_single_file = True,
397            mandatory = False,
398        ),
399    },
400    outputs = {
401        "aar": "%{name}.aar",
402    },
403)
404