blob: 7d9354384babc3f465df5ec6bbbf7eeda882ade1 [file] [log] [blame]
# Copyright 2019 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
"""A parser of the dependency tree from Coursier or maven_install.json.
This file contains parsing functions to turn a JSON-like dependency tree
into target declarations (jvm_import) for the final @maven//:BUILD file.
"""
load(
"//private:coursier_utilities.bzl",
"PLATFORM_CLASSIFIER",
"escape",
"get_classifier",
"get_packaging",
"is_maven_local_path",
"match_group_and_artifact",
"strip_packaging_and_classifier",
"strip_packaging_and_classifier_and_version",
"to_repository_name",
)
load("//private/lib:coordinates.bzl", "to_purl", "unpack_coordinates")
load("//private/lib:urls.bzl", "scheme_and_host")
def _genrule_copy_artifact_from_http_file(artifact, visibilities):
http_file_repository = to_repository_name(artifact["coordinates"])
file = artifact.get("out", artifact["file"])
genrule = [
"copy_file(",
" name = \"%s_extension\"," % http_file_repository,
" src = \"@%s//file\"," % http_file_repository,
" out = \"%s\"," % file,
# Windows doesn't care about the executable bit for executables, but copy_file
# will fail in some cases (disable this line and run ` bazel test //tests/unit/build_tests:all_artifacts`
# to see this failing
" allow_symlink = %s," % ("False" if file.endswith(".exe") else "True"),
]
if get_packaging(artifact["coordinates"]) == "exe":
genrule.append(" is_executable = True,")
genrule.extend([
" visibility = [%s]" % (",".join(["\"%s\"" % v for v in visibilities])),
")",
])
return "\n".join(genrule)
def _deduplicate_list(items):
seen_items = {}
unique_items = []
for item in items:
if item not in seen_items:
seen_items[item] = True
unique_items.append(item)
return unique_items
def _find_repository_url(artifact_url, repositories):
longest_match = None
for repository in repositories:
if artifact_url.startswith(repository):
if len(repository) > len(longest_match or ""):
longest_match = repository
return longest_match
def _get_maven_url(artifact_urls):
if len(artifact_urls) == 0:
return None
# We want to use the Maven Central repo if it's there
# since so much of the world expects that.
for url in artifact_urls:
if url.startswith("https://repo1.maven.org/maven2/"):
return url
# Return anything
return artifact_urls[0]
def _generate_target(
repository_ctx,
jar_versionless_target_labels,
explicit_artifacts,
srcjar_paths,
labels_to_override,
repository_urls,
neverlink_artifacts,
testonly_artifacts,
exclusions,
default_visibilities,
artifact):
to_return = []
simple_coord = strip_packaging_and_classifier_and_version(artifact["coordinates"])
target_label = escape(simple_coord)
artifact_path = artifact["file"]
# 1. Generate the rule class.
#
# (jvm|aar)_import(
#
packaging = artifact_path.split(".").pop()
if packaging == "jar":
# Regular `java_import` invokes ijar on all JARs, causing some Scala and
# Kotlin compile interface JARs to be incorrect. We replace java_import
# with a simple jvm_import Starlark rule that skips ijar.
import_rule = "jvm_import"
jar_versionless_target_labels.append(target_label)
elif packaging == "aar":
import_rule = "aar_import"
jar_versionless_target_labels.append(target_label)
elif packaging in ["dylib", "so", "dll"]:
import_rule = "java_library"
else:
fail("Unsupported packaging type: " + packaging)
target_import_string = [import_rule + "("]
# 2. Generate the target label.
#
# java_import(
# name = "org_hamcrest_hamcrest_library",
#
target_import_string.append("\tname = \"%s\"," % target_label)
# 3. Generate the jars/aar attribute to the relative path of the artifact.
# Optionally generate srcjar attr too.
#
#
# java_import(
# name = "org_hamcrest_hamcrest_library",
# jars = ["https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3.jar"],
# srcjar = "https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3-sources.jar",
#
is_dylib = False
if packaging == "jar":
target_import_string.append("\tjar = \"%s\"," % artifact_path)
if srcjar_paths != None and target_label in srcjar_paths:
target_import_string.append("\tsrcjar = \"%s\"," % srcjar_paths[target_label])
elif packaging == "aar":
target_import_string.append("\taar = \"%s\"," % artifact_path)
if srcjar_paths != None and target_label in srcjar_paths:
target_import_string.append("\tsrcjar = \"%s\"," % srcjar_paths[target_label])
elif packaging in ["so", "dylib", "dll"]:
is_dylib = True
jar_versionless_target_labels.append(target_label)
dylib = simple_coord.split(":")[-1] + "." + packaging
to_return.append(
"""
copy_file(
name = "{dylib}_extension",
src = "@{repository}//file",
out = "{dylib}",
allow_symlink = True,
visibility = ["//visibility:public"],
)""".format(
dylib = dylib,
repository = to_repository_name(artifact["coordinates"]),
),
)
# 4. Generate the deps attribute with references to other target labels.
#
# java_import(
# name = "org_hamcrest_hamcrest_library",
# jars = ["https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3.jar"],
# srcjar = "https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3-sources.jar",
# deps = [
# ":org_hamcrest_hamcrest_core",
# ],
#
if is_dylib:
target_import_string.append("\truntime_deps = [")
else:
target_import_string.append("\tdeps = [")
# Dedupe dependencies here. Sometimes coursier will return "x.y:z:aar:version" and "x.y:z:version" in the
# same list of dependencies.
target_import_labels = []
for dep in artifact["deps"]:
if get_packaging(dep) == "json":
continue
stripped_dep = strip_packaging_and_classifier_and_version(dep)
dep_target_label = escape(stripped_dep)
# If we have matching artifacts with platform classifiers, skip adding this dependency.
# See https://github.com/bazelbuild/rules_jvm_external/issues/686
if match_group_and_artifact(artifact["coordinates"], dep) and \
get_classifier(artifact["coordinates"]) in PLATFORM_CLASSIFIER and \
get_classifier(dep) in PLATFORM_CLASSIFIER:
continue
# Coursier returns cyclic dependencies sometimes. Handle it here.
# See https://github.com/bazelbuild/rules_jvm_external/issues/172
if dep_target_label != target_label:
if dep_target_label in labels_to_override:
dep_target_label = labels_to_override.get(dep_target_label)
else:
dep_target_label = ":" + dep_target_label
target_import_labels.append("\t\t\"%s\",\n" % dep_target_label)
target_import_labels = _deduplicate_list(target_import_labels)
target_import_string.append("".join(target_import_labels) + "\t],")
# 5. Add a tag with the original maven coordinates for use generating pom files
# For use with this rule https://github.com/google/bazel-common/blob/f1115e0f777f08c3cdb115526c4e663005bec69b/tools/maven/pom_file.bzl#L177
#
# java_import(
# name = "org_hamcrest_hamcrest_library",
# jars = ["https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3.jar"],
# srcjar = "https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3-sources.jar",
# deps = [
# ":org_hamcrest_hamcrest_core",
# ],
# tags = [
# "maven_coordinates=org.hamcrest:hamcrest.library:1.3"],
# "maven_url=https://repo1.maven.org/maven/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar",
# ],
coordinates = artifact.get("maven_coordinates", artifact["coordinates"])
maven_url = _get_maven_url(artifact["urls"])
target_import_string.append("\ttags = [")
target_import_string.append("\t\t\"maven_coordinates=%s\"," % coordinates)
if len(artifact["urls"]):
target_import_string.append("\t\t\"maven_url=%s\"," % maven_url)
repository_url = _find_repository_url(maven_url, repository_urls)
if repository_url:
target_import_string.append("\t\t\"maven_repository=%s\"," % repository_url)
else:
target_import_string.append("\t\t\"maven_url=None\",")
if neverlink_artifacts.get(simple_coord):
target_import_string.append("\t\t\"maven:compile-only\",")
if artifact.get("sha256"):
target_import_string.append("\t\t\"maven_sha256=%s\"," % artifact["sha256"])
if simple_coord in exclusions:
for exclusion in exclusions[simple_coord]:
target_import_string.append("\t\t\"maven_exclusion=%s\"," % exclusion)
target_import_string.append("\t],")
if packaging == "jar":
target_import_string.append("\tmaven_coordinates = \"%s\"," % coordinates)
if len(artifact["urls"]):
target_import_string.append("\tmaven_url = \"%s\"," % maven_url)
package_metadata_name = "%s_package_metadata" % target_label
target_import_string.append("\tapplicable_licenses = [\":{}\"],".format(package_metadata_name))
to_return.append("""
package_metadata(
name = {package_metadata_name},
purl = {purl},
visibility = ["//visibility:public"],
)
""".format(
package_metadata_name = repr(package_metadata_name),
purl = repr(to_purl(coordinates, scheme_and_host(maven_url))),
))
else:
unpacked = unpack_coordinates(coordinates)
url = maven_url if len(artifact["urls"]) else None
package_info_name = "%s_package_info" % target_label
package_metadata_name = "%s_package_metadata" % target_label
target_import_string.append("\tapplicable_licenses = [\n\t\t\":{}\",\n\t\t\":{}\",\n\t],".format(package_info_name, package_metadata_name))
to_return.append("""
package_info(
name = {name},
package_name = {coordinates},
package_url = {url},
package_version = {version},
)
package_metadata(
name = {package_metadata_name},
purl = {purl},
visibility = ["//visibility:public"],
)
""".format(
coordinates = repr(coordinates),
name = repr(package_info_name),
package_metadata_name = repr(package_metadata_name),
purl = repr(to_purl(coordinates, scheme_and_host(url))),
url = repr(url),
version = repr(unpacked.version),
))
# 6. If `neverlink` is True in the artifact spec, add the neverlink attribute to make this artifact
# available only as a compile time dependency.
#
# java_import(
# name = "org_hamcrest_hamcrest_library",
# jars = ["https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3.jar"],
# srcjar = "https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3-sources.jar",
# deps = [
# ":org_hamcrest_hamcrest_core",
# ],
# tags = [
# "maven_coordinates=org.hamcrest:hamcrest.library:1.3"],
# "maven_url=https://repo1.maven.org/maven/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar",
# "maven:compile-only",
# ],
# neverlink = True,
if neverlink_artifacts.get(simple_coord):
target_import_string.append("\tneverlink = True,")
# 7. If `testonly` is True in the artifact spec, add the testonly attribute to make this artifact
# available only as a test dependency.
#
# java_import(
# name = "org_hamcrest_hamcrest_library",
# jars = ["https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3.jar"],
# srcjar = "https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3-sources.jar",
# deps = [
# ":org_hamcrest_hamcrest_core",
# ],
# tags = [
# "maven_coordinates=org.hamcrest:hamcrest.library:1.3"],
# "maven_url=https://repo1.maven.org/maven/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar",
# "maven:compile-only",
# ],
# neverlink = True,
# testonly = True,
if testonly_artifacts.get(simple_coord):
target_import_string.append("\ttestonly = True,")
# 8. If `strict_visibility` is True in the artifact spec, define public
# visibility only for non-transitive dependencies.
#
# java_import(
# name = "org_hamcrest_hamcrest_library",
# jars = ["https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3.jar"],
# srcjar = "https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3-sources.jar",
# deps = [
# ":org_hamcrest_hamcrest_core",
# ],
# tags = [
# "maven_coordinates=org.hamcrest:hamcrest.library:1.3"],
# "maven_url=https://repo1.maven.org/maven/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar",
# "maven:compile-only",
# ],
# neverlink = True,
# testonly = True,
# visibility = ["//visibility:public"],
target_visibilities = []
if not repository_ctx.attr.strict_visibility or explicit_artifacts.get(simple_coord):
target_visibilities.append("//visibility:public")
elif repository_ctx.attr.generate_compat_repositories:
target_visibilities.append("@%s//:__subpackages__" % target_label)
else:
target_visibilities.extend(repository_ctx.attr.strict_visibility_value)
target_import_string.append("\tvisibility = [%s]," % (",".join(["\"%s\"" % t for t in target_visibilities])))
alias_visibility = "\tvisibility = [%s],\n" % (",".join(["\"%s\"" % t for t in target_visibilities]))
# 9. Finish the java_import rule.
#
# java_import(
# name = "org_hamcrest_hamcrest_library",
# jars = ["https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3.jar"],
# srcjar = "https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3-sources.jar",
# deps = [
# ":org_hamcrest_hamcrest_core",
# ],
# tags = [
# "maven_coordinates=org.hamcrest:hamcrest.library:1.3"],
# "maven_url=https://repo1.maven.org/maven/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar",
# "maven:compile-only",
# ],
# neverlink = True,
# testonly = True,
# )
target_import_string.append(")")
to_return.append("\n".join(target_import_string))
# 10. Create a versionless alias target
#
# alias(
# name = "org_hamcrest_hamcrest_library_1_3",
# actual = "org_hamcrest_hamcrest_library",
# )
versioned_target_alias_label = escape(strip_packaging_and_classifier(artifact["coordinates"]))
to_return.append("alias(\n\tname = \"%s\",\n\tactual = \"%s\",\n%s)" %
(versioned_target_alias_label, target_label, alias_visibility))
for annotation_processor in artifact.get("annotation_processors", []):
to_return.append(
"""java_plugin(
\tname = "{name}",{testonly}
\tdeps = [":{jar_target}"],
\tgenerates_api = True,
processor_class = "{processor_class}",
{alias_visibility})""".format(
name = "{}__java_plugin__{}".format(target_label, escape(annotation_processor)),
testonly = "\n\ttestonly = True," if testonly_artifacts.get(simple_coord) else "",
jar_target = target_label,
processor_class = annotation_processor,
alias_visibility = alias_visibility,
),
)
# 11. If using maven_install.json, use a genrule to copy the file from the http_file
# repository into this repository.
#
# genrule(
# name = "org_hamcrest_hamcrest_library_1_3_extension",
# srcs = ["@org_hamcrest_hamcrest_library_1_3//file"],
# outs = ["@maven//:v1/https/repo1.maven.org/maven2/org/hamcrest/hamcrest-library/1.3/hamcrest-library-1.3.jar"],
# cmd = "cp $< $@",
# )
if repository_ctx.attr.maven_install_json:
to_return.append(_genrule_copy_artifact_from_http_file(artifact, default_visibilities))
return to_return
# Generate BUILD file with jvm_import and aar_import for each artifact in
# the transitive closure, with their respective deps mapped to the resolved
# tree.
#
# Made function public for testing.
def _generate_imports(repository_ctx, dependencies, explicit_artifacts, neverlink_artifacts, testonly_artifacts, exclusions, override_targets, skip_maven_local_dependencies):
repository_urls = [json.decode(repository)["repo_url"] for repository in repository_ctx.attr.repositories]
# The list of java_import/aar_import declaration strings to be joined at the end
all_imports = []
# A dictionary (set) of coordinates. This is to ensure we don't generate
# duplicate labels
#
# seen_imports :: string -> bool
seen_imports = {}
# A list of versionless target labels for jar artifacts. This is used for
# generating a compatibility layer for repositories. For example, if we generate
# @maven//:junit_junit, we also generate @junit_junit//jar as an alias to it.
jar_versionless_target_labels = []
labels_to_override = {}
for coord in override_targets:
labels_to_override.update({escape(coord): override_targets.get(coord)})
default_visibilities = repository_ctx.attr.strict_visibility_value if repository_ctx.attr.strict_visibility else ["//visibility:public"]
# First collect a map of target_label to their srcjar relative paths, and symlink the srcjars if needed.
# We will use this map later while generating target declaration strings with the "srcjar" attr.
srcjar_paths = None
if repository_ctx.attr.fetch_sources:
srcjar_paths = {}
for artifact in dependencies:
if get_classifier(artifact["coordinates"]) == "sources":
artifact_path = artifact["file"]
# Skip the maven local dependencies if requested
if skip_maven_local_dependencies and is_maven_local_path(artifact_path):
continue
if artifact_path != None and artifact_path not in seen_imports:
seen_imports[artifact_path] = True
target_label = escape(strip_packaging_and_classifier_and_version(artifact["coordinates"]))
srcjar_paths[target_label] = artifact_path
if repository_ctx.attr.maven_install_json:
all_imports.append(_genrule_copy_artifact_from_http_file(artifact, default_visibilities))
# Iterate through the list of artifacts, and generate the target declaration strings.
for artifact in dependencies:
artifact_path = artifact["file"]
# Skip the maven local dependencies if requested
if skip_maven_local_dependencies and is_maven_local_path(artifact_path):
continue
simple_coord = strip_packaging_and_classifier_and_version(artifact["coordinates"])
packaging = get_packaging(artifact["coordinates"])
target_label = escape(simple_coord)
visibility = ""
if repository_ctx.attr.strict_visibility and explicit_artifacts.get(simple_coord):
visibility = "[\"//visibility:public\"]"
else:
visibility = "[%s]" % (",".join(["\"%s\"" % v for v in default_visibilities]))
if target_label in seen_imports:
# Skip if we've seen this target label before. Every versioned artifact is uniquely mapped to a target label.
pass
elif repository_ctx.attr.fetch_sources and get_classifier(artifact["coordinates"]) == "sources":
# We already processed the sources above, so skip them here.
pass
elif repository_ctx.attr.fetch_javadoc and get_classifier(artifact["coordinates"]) == "javadoc":
seen_imports[target_label] = True
all_imports.append(
"filegroup(\n\tname = \"%s\",\n\tsrcs = [\"%s\"],\n\ttags = [\"javadoc\"],\n\tvisibility = %s,\n)" % (target_label, artifact_path, visibility),
)
elif packaging in ("exe", "json"):
seen_imports[target_label] = True
versioned_target_alias_label = "%s_extension" % to_repository_name(artifact["coordinates"])
all_imports.append(
"alias(\n\tname = \"%s\",\n\tactual = \"%s\",\n\tvisibility = %s,\n)" % (target_label, versioned_target_alias_label, visibility),
)
if repository_ctx.attr.maven_install_json:
all_imports.append(_genrule_copy_artifact_from_http_file(artifact, default_visibilities))
elif target_label in labels_to_override:
# Override target labels with the user provided mapping, instead of generating
# a jvm_import/aar_import based on information in dep_tree.
seen_imports[target_label] = True
all_imports.append(
"alias(\n\tname = \"%s\",\n\tactual = \"%s\",\n\tvisibility = %s,)" % (target_label, labels_to_override.get(target_label), visibility),
)
if repository_ctx.attr.maven_install_json:
# Provide the downloaded artifact as a file target.
all_imports.append(_genrule_copy_artifact_from_http_file(artifact, default_visibilities))
raw_artifact = dict(artifact)
raw_artifact["coordinates"] = "original_" + artifact["coordinates"]
raw_artifact["maven_coordinates"] = artifact["coordinates"]
raw_artifact["out"] = "original_" + artifact["file"]
all_imports.extend(_generate_target(
repository_ctx,
jar_versionless_target_labels,
explicit_artifacts,
srcjar_paths,
labels_to_override,
repository_urls,
neverlink_artifacts,
testonly_artifacts,
exclusions,
default_visibilities,
raw_artifact,
))
elif artifact_path != None and packaging != "pom":
seen_imports[target_label] = True
all_imports.extend(_generate_target(
repository_ctx,
jar_versionless_target_labels,
explicit_artifacts,
srcjar_paths,
labels_to_override,
repository_urls,
neverlink_artifacts,
testonly_artifacts,
exclusions,
default_visibilities,
artifact,
))
else: # artifact_path == None or packaging == "pom":
# Special case for certain artifacts that only come with a POM file.
# Such artifacts "aggregate" their dependencies, so they don't have
# a JAR for download.
#
# Note that there are other possible reasons that the artifact_path is None:
#
# https://github.com/bazelbuild/rules_jvm_external/issues/70
# https://github.com/bazelbuild/rules_jvm_external/issues/74
#
#
# This can be due to the artifact being of a type that's unknown to
# Coursier. This is increasingly rare as we add more types to
# SUPPORTED_PACKAGING_TYPES. It's also increasingly
# uncommon relatively to POM-only / parent artifacts. So when we
# encounter an artifact without a filepath, we assume that it's a
# parent artifact that just exports its dependencies, instead of
# failing.
seen_imports[target_label] = True
target_import_string = ["java_library("]
target_import_string.append("\tname = \"%s\"," % target_label)
target_import_string.append("\texports = [")
target_import_labels = []
for dep in artifact.get("deps", []):
dep_target_label = escape(strip_packaging_and_classifier_and_version(dep))
# Coursier returns cyclic dependencies sometimes. Handle it here.
# See https://github.com/bazelbuild/rules_jvm_external/issues/172
if dep_target_label != target_label:
target_import_labels.append("\t\t\":%s\",\n" % dep_target_label)
target_import_labels = _deduplicate_list(target_import_labels)
target_import_string.append("".join(target_import_labels) + "\t],")
coordinates = artifact.get("maven_coordinates", artifact["coordinates"])
target_import_string.append("\ttags = [\"maven_coordinates=%s\"]," % coordinates)
alias_visibility = "\tvisibility = %s,\n" % visibility
target_import_string.append("\tvisibility = %s," % visibility)
target_import_string.append(")")
all_imports.append("\n".join(target_import_string))
versioned_target_alias_label = escape(strip_packaging_and_classifier(artifact["coordinates"]))
all_imports.append("alias(\n\tname = \"%s\",\n\tactual = \"%s\",\n%s)" %
(versioned_target_alias_label, target_label, alias_visibility))
return ("\n".join(all_imports), jar_versionless_target_labels)
parser = struct(
generate_imports = _generate_imports,
)