From 062fe70189fc622285833311d241021be313680b Mon Sep 17 00:00:00 2001 From: jart Date: Tue, 8 Aug 2017 23:11:45 +0200 Subject: [PATCH] Introduce java_import_external This Skylark rule is a replacement for maven_jar. See also #1410 PiperOrigin-RevId: 164642813 --- tools/build_defs/repo/java.bzl | 279 +++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 tools/build_defs/repo/java.bzl diff --git a/tools/build_defs/repo/java.bzl b/tools/build_defs/repo/java.bzl new file mode 100644 index 00000000000000..7f3657bc695a43 --- /dev/null +++ b/tools/build_defs/repo/java.bzl @@ -0,0 +1,279 @@ +# Copyright 2017 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. + +"""Rules for defining external Java dependencies. + +java_import_external() replaces `maven_jar` and `http_jar`. It is the +recommended solution for defining third party Java dependencies that are +obtained from web servers. + +This solution offers high availability, low latency, and repository +scalability at the cost of simplicity. Tooling can be used to generate +the WORKSPACE definitions from Maven metadata. + +The default target in this BUILD file will always have the same name as +the repository itself. This means that other Bazel rules can depend on +it as `@repo//:repo` or `@repo` for short. + +### Setup + +Add the following to your `WORKSPACE` file: + +```python +load("@bazel_tools//tools/build_defs/repo:java.bzl", "java_import_external") +``` + +### Best Practices + +#### Downloading + +The recommended best practices for downloading Maven jars are as follows: + +1. Always follow release versions or pinned revisions. +2. Permanently mirror all dependencies to GCS or S3 as the first URL +3. Put the original URL in the GCS or S3 object name +4. Make the second URL the original repo1.maven.org URL +5. Make the third URL the maven.ibiblio.org mirror, if it isn't 404 +6. Always specify the sha256 checksum +7. Prefer http over https unless curl -I says the http URL redirects to https + +Bazel has one of the most sophisticated systems for downloading files of any +build system. Following these best practices will ensure that your codebase +takes full advantage of the level of reliability that Bazel able to offer. See +https://goo.gl/uQOE11 for more information. + +#### Selection + +Avoid using jars that bundle their dependencies. For example, a Maven jar for +the artifact com.initech:tps:1.0 should not contain a classes named +com.fakecorp.foo. Try to see if Initech distributes a tps jar that doesn't +bundle its dependencies. Then create a separate java_import_external() for each +one and have the first depend on the second. + +Sometimes jars are distributed with their dependencies shaded. What this means +is that com.initech.tps will contain classes like +com.initech.tps.shade.com.fakecorp.foo. This is less problematic, since it +won't lead to mysterious classpath conflicts. But it can lead to inefficient +use of space and make the license of the the end product more difficult to +determine. + +#### Licensing + +The following values for the licenses field are typically used. If a jar +contains multiple works with difference licenses, then only the most +restrictive one is listed, and the rest are noted in accompanying comments. + +The following are examples of how licenses could be categorized, ordered +by those with terms most permissive to least: + +- **unencumbered**: CC0, Unlicense +- **permissive**: Beerware +- **notice**: Apache, MIT, X11, BSD, ISC, ZPL, Unicode, JSON, Artistic +- **reciprocal**: MPL, CPL, EPL, Eclipse, APSL, IBMPL, CDDL +- **restricted**: GPL, LGPL, OSL, Sleepycat, QTPL, Java, QMail, NPL +- **by_exception_only**: AGPL, WTFPL + +### Naming + +Bazel repository names must match the following pattern: `[_0-9A-Za-z]+`. To +choose an appropriate name based on a Maven group and artifact ID, we recommend +an algorithm https://gist.github.com/jart/41bfd977b913c2301627162f1c038e55 which +can be best explained by the following examples: + +- com.google.guava:guava becomes com_google_guava +- commons-logging:commons-logging becomes commons_logging +- junit:junit becomes junit + +Adopting this naming convention will help maximize the chances that your +codebase will be able to successfully interoperate with other Bazel codebases +using Java. + +### Example + +Here is an example of a best practice definition of Google's Guava library: + +```python +java_import_external( + name = "com_google_guava", + licenses = ["notice"], # Apache 2.0 + jar_urls = [ + "http://bazel-mirror.storage.googleapis.com/repo1.maven.org/maven2/com/google/guava/guava/20.0/guava-20.0.jar", + "http://repo1.maven.org/maven2/com/google/guava/guava/20.0/guava-20.0.jar", + "http://maven.ibiblio.org/maven2/com/google/guava/guava/20.0/guava-20.0.jar", + ], + jar_sha256 = "36a666e3b71ae7f0f0dca23654b67e086e6c93d192f60ba5dfd5519db6c288c8", + deps = [ + "@com_google_code_findbugs_jsr305", + "@com_google_errorprone_error_prone_annotations", + ], +) + +java_import_external( + name = "com_google_code_findbugs_jsr305", + licenses = ["notice"], # BSD 3-clause + jar_urls = [ + "http://bazel-mirror.storage.googleapis.com/repo1.maven.org/maven2/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar", + "http://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar", + "http://maven.ibiblio.org/maven2/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar", + ], + jar_sha256 = "905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed", +) + +java_import_external( + name = "com_google_errorprone_error_prone_annotations", + licenses = ["notice"], # Apache 2.0 + jar_sha256 = "e7749ffdf03fb8ebe08a727ea205acb301c8791da837fee211b99b04f9d79c46", + jar_urls = [ + "http://bazel-mirror.storage.googleapis.com/repo1.maven.org/maven2/com/google/errorprone/error_prone_annotations/2.0.15/error_prone_annotations-2.0.15.jar", + "http://maven.ibiblio.org/maven2/com/google/errorprone/error_prone_annotations/2.0.15/error_prone_annotations-2.0.15.jar", + "http://repo1.maven.org/maven2/com/google/errorprone/error_prone_annotations/2.0.15/error_prone_annotations-2.0.15.jar", + ], +) +``` + +### Annotation Processors + +Defining jars that contain annotation processors requires a certain level of +trickery, which is best done by copying and pasting from codebases that have +already done it before. Please see the Google Nomulus and Bazel Closure Rules +codebases for examples in which java_import_external has been used to define +Dagger 2.0, AutoValue, and AutoFactory. + +Please note that certain care needs to be taken into consideration regarding +whether or not these annotation processors generate actual API, or simply +generate code that implements them. See the Bazel documentation for further +information. + +### Test Dependencies + +It is strongly recommended that the `testonly_` attribute be specified on +libraries that are intended for testing purposes. This is passed along to the +generated `java_library` rule in order to ensure that test code remains +disjoint from production code. + +### Provided Dependencies + +The feature in Bazel most analagous to Maven's provided scope is the neverlink +attribute. This should be used in rare circumstances when a distributed jar +will be loaded into a runtime environment where certain dependencies can be +reasonably expected to already be provided. +""" + +_HEADER = "# DO NOT EDIT: generated by java_import_external()" + +_PASS_PROPS = ( + "neverlink", + "testonly_", + "visibility", + "exports", + "runtime_deps", + "deps", +) + +def _java_import_external(repository_ctx): + """Implementation of `java_import_external` rule.""" + if (repository_ctx.attr.generated_linkable_rule_name and + not repository_ctx.attr.neverlink): + fail("Only use generated_linkable_rule_name if neverlink is set") + name = repository_ctx.attr.generated_rule_name or repository_ctx.name + urls = repository_ctx.attr.jar_urls + sha = repository_ctx.attr.jar_sha256 + path = repository_ctx.name + ".jar" + for url in urls: + if url.endswith(".jar"): + path = url[url.rindex("/") + 1:] + break + srcurls = repository_ctx.attr.srcjar_urls + srcsha = repository_ctx.attr.srcjar_sha256 + srcpath = repository_ctx.name + "-src.jar" if srcurls else "" + for url in srcurls: + if url.endswith(".jar"): + srcpath = url[url.rindex("/") + 1:].replace("-sources.jar", "-src.jar") + break + lines = [_HEADER, ""] + if repository_ctx.attr.default_visibility: + lines.append("package(default_visibility = %s)" % ( + repository_ctx.attr.default_visibility)) + lines.append("") + lines.append("licenses(%s)" % repr(repository_ctx.attr.licenses)) + lines.append("") + lines.extend(_make_java_import( + name, path, srcpath, repository_ctx.attr, _PASS_PROPS)) + if (repository_ctx.attr.neverlink and + repository_ctx.attr.generated_linkable_rule_name): + lines.extend(_make_java_import( + repository_ctx.attr.generated_linkable_rule_name, + path, + srcpath, + repository_ctx.attr, + [p for p in _PASS_PROPS if p != "neverlink"])) + extra = repository_ctx.attr.extra_build_file_content + if extra: + lines.append(extra) + if not extra.endswith("\n"): + lines.append("") + repository_ctx.download(urls, path, sha) + if srcurls: + repository_ctx.download(srcurls, srcpath, srcsha) + repository_ctx.file("BUILD", "\n".join(lines)) + repository_ctx.file("jar/BUILD", "\n".join([ + _HEADER, + "", + "package(default_visibility = %r)" % ( + repository_ctx.attr.visibility or + repository_ctx.attr.default_visibility), + "", + "alias(", + " name = \"jar\",", + " actual = \"@%s\"," % repository_ctx.name, + ")", + "", + ])) + +def _make_java_import(name, path, srcpath, attrs, props): + lines = [ + "java_import(", + " name = %s," % repr(name), + " jars = [%s]," % repr(path), + ] + if srcpath: + lines.append(" srcjar = %s," % repr(srcpath)) + for prop in props: + value = getattr(attrs, prop, None) + if value: + if prop.endswith("_"): + prop = prop[:-1] + lines.append(" %s = %s," % (prop, repr(value))) + lines.append(")") + lines.append("") + return lines + +java_import_external = repository_rule( + implementation=_java_import_external, + attrs={ + "licenses": attr.string_list(mandatory=True, allow_empty=False), + "jar_urls": attr.string_list(mandatory=True, allow_empty=False), + "jar_sha256": attr.string(mandatory=True), + "srcjar_urls": attr.string_list(), + "srcjar_sha256": attr.string(), + "deps": attr.string_list(), + "runtime_deps": attr.string_list(), + "testonly_": attr.bool(), + "exports": attr.string_list(), + "neverlink": attr.bool(), + "generated_rule_name": attr.string(), + "generated_linkable_rule_name": attr.string(), + "default_visibility": attr.string_list(default=["//visibility:public"]), + "extra_build_file_content": attr.string(), + })