blob: 82be4407fe2cd67ab95f404c64bdcc746dfbb21f [file] [log] [blame]
# Copyright 2016 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.
# Implementations of Maven rules in Skylark:
# 1) maven_jar(name, artifact, repository, sha1, settings)
# The API of this is largely the same as the native maven_jar rule,
# except for the server attribute, which is not implemented. The optional
# settings supports passing a custom Maven settings.xml to download the JAR.
# 2) maven_aar(name, artifact, sha1, settings)
# The API of this rule is the same as maven_jar except that the artifact must
# be the Maven coordinate of an AAR and it does not support the historical
# repository and server attributes.
# 3) maven_dependency_plugin()
# This rule downloads the maven-dependency-plugin used internally
# for testing and the implementation for the fetching of artifacts.
#
# Maven coordinates are expected to be in this form:
# groupId:artifactId:version[:packaging][:classifier]
#
# Installation requirements prior to using this rule:
# 1) Maven binary: `mvn`
# 2) Maven plugin: `maven-dependency-plugin:2.10`
# Get it: $ mvn org.apache.maven.plugins:maven-dependency-plugin:2.10:get \
# -Dartifact=org.apache.maven.plugins:maven-dependency-plugin:2.10 \
# -Dmaven.repo.local=$HOME/.m2/repository # or specify your own local repository
"""Rules for retrieving Maven dependencies (experimental)"""
MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2"
# Binary dependencies needed for running the bash commands
DEPS = ["mvn", "openssl", "awk"]
MVN_PLUGIN = "org.apache.maven.plugins:maven-dependency-plugin:2.10"
def _execute(ctx, command):
return ctx.execute(["bash", "-c", """
set -ex
%s""" % command])
# Fail fast
def _check_dependencies(ctx):
for dep in DEPS:
if ctx.which(dep) == None:
fail("%s requires %s as a dependency. Please check your PATH." % (ctx.name, dep))
def _validate_attr(ctx):
if hasattr(ctx.attr, "server") and (ctx.attr.server != None):
fail("%s specifies a 'server' attribute which is currently not supported." % ctx.name)
def _artifact_dir(coordinates):
return "/".join(coordinates.group_id.split(".") +
[coordinates.artifact_id, coordinates.version])
# Creates a struct containing the different parts of an artifact's FQN.
# If the fully_qualified_name does not specify a packaging and the rule does
# not set a default packaging then JAR is assumed.
def _create_coordinates(fully_qualified_name, packaging = "jar"):
parts = fully_qualified_name.split(":")
classifier = None
if len(parts) == 3:
group_id, artifact_id, version = parts
# Updates the FQN with the default packaging so that the Maven plugin
# downloads the correct artifact.
fully_qualified_name = "%s:%s" % (fully_qualified_name, packaging)
elif len(parts) == 4:
group_id, artifact_id, version, packaging = parts
elif len(parts) == 5:
group_id, artifact_id, version, packaging, classifier = parts
else:
fail("Invalid fully qualified name for artifact: %s" % fully_qualified_name)
return struct(
fully_qualified_name = fully_qualified_name,
group_id = group_id,
artifact_id = artifact_id,
packaging = packaging,
classifier = classifier,
version = version,
)
# NOTE: Please use this method to define ALL paths that the maven_*
# rules use. Doing otherwise will lead to inconsistencies and/or errors.
#
# CONVENTION: *_path refers to files, *_dir refers to directories.
def _create_paths(ctx, coordinates):
"""Creates a struct that contains the paths to create the cache WORKSPACE"""
# e.g. guava-18.0.jar
artifact_filename = "%s-%s" % (
coordinates.artifact_id,
coordinates.version,
)
if coordinates.classifier:
artifact_filename += "-" + coordinates.classifier
artifact_filename += "." + coordinates.packaging
sha1_filename = "%s.sha1" % artifact_filename
# e.g. com/google/guava/guava/18.0
relative_artifact_dir = _artifact_dir(coordinates)
# The symlink to the actual artifact is stored in this dir, along with the
# BUILD file. The dir has the same name as the packaging to support syntax
# like @guava//jar and @google_play_services//aar.
symlink_dir = coordinates.packaging
m2 = ".m2"
m2_repo = "/".join([m2, "repository"]) # .m2/repository
return struct(
artifact_filename = artifact_filename,
sha1_filename = sha1_filename,
symlink_dir = ctx.path(symlink_dir),
# e.g. external/com_google_guava_guava/ \
# .m2/repository/com/google/guava/guava/18.0/guava-18.0.jar
artifact_path = ctx.path("/".join([m2_repo, relative_artifact_dir, artifact_filename])),
artifact_dir = ctx.path("/".join([m2_repo, relative_artifact_dir])),
sha1_path = ctx.path("/".join([m2_repo, relative_artifact_dir, sha1_filename])),
# e.g. external/com_google_guava_guava/jar/guava-18.0.jar
symlink_artifact_path = ctx.path("/".join([symlink_dir, artifact_filename])),
)
_maven_jar_build_file_template = """
# DO NOT EDIT: automatically generated BUILD file for maven_jar rule {rule_name}
java_import(
name = 'jar',
jars = ['{artifact_filename}'],
deps = [
{deps_string}
],
visibility = ['//visibility:public']
)
filegroup(
name = 'file',
srcs = ['{artifact_filename}'],
visibility = ['//visibility:public']
)\n"""
_maven_aar_build_file_template = """
# DO NOT EDIT: automatically generated BUILD file for maven_aar rule {rule_name}
aar_import(
name = 'aar',
aar = '{artifact_filename}',
deps = [
{deps_string}
],
visibility = ['//visibility:public'],
)
filegroup(
name = 'file',
srcs = ['{artifact_filename}'],
visibility = ['//visibility:public']
)\n"""
# Provides the syntax "@jar_name//jar" for dependencies
def _generate_build_file(ctx, template, paths):
deps_string = "\n".join(["'%s'," % dep for dep in ctx.attr.deps])
contents = template.format(
rule_name = ctx.name,
artifact_filename = paths.artifact_filename,
deps_string = deps_string,
)
ctx.file("%s/BUILD" % paths.symlink_dir, contents, False)
# Constructs the maven command to retrieve the dependencies from remote
# repositories using the dependency plugin, and executes it.
def _mvn_download(ctx, paths, fully_qualified_name):
# If a custom settings file exists, we'll use that. If not, Maven will use the default settings.
mvn_flags = ""
if hasattr(ctx.attr, "settings") and ctx.attr.settings != None:
ctx.symlink(ctx.attr.settings, "settings.xml")
mvn_flags += "-s %s " % "settings.xml"
# dependency:get step. Downloads the artifact into the local repository.
mvn_get = MVN_PLUGIN + ":get"
mvn_artifact = "-Dartifact=%s" % fully_qualified_name
mvn_transitive = "-Dtransitive=false"
if hasattr(ctx.attr, "repository") and ctx.attr.repository != "":
mvn_flags += "-Dmaven.repo.remote=%s " % ctx.attr.repository
command = " ".join(["mvn", mvn_flags, mvn_get, mvn_transitive, mvn_artifact])
exec_result = _execute(ctx, command)
if exec_result.return_code != 0:
fail("%s\n%s\nFailed to fetch Maven dependency" % (exec_result.stdout, exec_result.stderr))
# dependency:copy step. Moves the artifact from the local repository into //external.
mvn_copy = MVN_PLUGIN + ":copy"
mvn_output_dir = "-DoutputDirectory=%s" % paths.artifact_dir
command = " ".join(["mvn", mvn_flags, mvn_copy, mvn_artifact, mvn_output_dir])
exec_result = _execute(ctx, command)
if exec_result.return_code != 0:
fail("%s\n%s\nFailed to fetch Maven dependency" % (exec_result.stdout, exec_result.stderr))
def _check_sha1(ctx, paths, sha1):
actual_sha1 = _execute(ctx, "openssl sha1 %s | awk '{printf $2}'" % paths.artifact_path).stdout
if sha1.lower() != actual_sha1.lower():
fail(("{rule_name} has SHA-1 of {actual_sha1}, " +
"does not match expected SHA-1 ({expected_sha1})").format(
rule_name = ctx.name,
expected_sha1 = sha1,
actual_sha1 = actual_sha1,
))
else:
_execute(ctx, "echo %s %s > %s" % (sha1, paths.artifact_path, paths.sha1_path))
def _maven_artifact_impl(ctx, default_rule_packaging, build_file_template):
# Ensure that we have all of the dependencies installed
_check_dependencies(ctx)
# Provide warnings and errors about attributes
_validate_attr(ctx)
# Create a struct to contain the different parts of the artifact FQN
coordinates = _create_coordinates(ctx.attr.artifact, default_rule_packaging)
# Create a struct to store the relative and absolute paths needed for this rule
paths = _create_paths(ctx, coordinates)
_generate_build_file(
ctx = ctx,
template = build_file_template,
paths = paths,
)
if _execute(ctx, "mkdir -p %s" % paths.symlink_dir).return_code != 0:
fail("%s: Failed to create dirs in execution root.\n" % ctx.name)
# Download the artifact
_mvn_download(
ctx = ctx,
paths = paths,
fully_qualified_name = coordinates.fully_qualified_name,
)
if (ctx.attr.sha1 != ""):
_check_sha1(
ctx = ctx,
paths = paths,
sha1 = ctx.attr.sha1,
)
ctx.symlink(paths.artifact_path, paths.symlink_artifact_path)
_common_maven_rule_attrs = {
"artifact": attr.string(
default = "",
mandatory = True,
),
"sha1": attr.string(default = ""),
"settings": attr.label(default = None),
# Allow the user to specify deps for the generated java_import or aar_import
# since maven_jar and maven_aar do not automatically pull in transitive
# dependencies.
"deps": attr.label_list(),
}
def _maven_jar_impl(ctx):
_maven_artifact_impl(ctx, "jar", _maven_jar_build_file_template)
def _maven_aar_impl(ctx):
_maven_artifact_impl(ctx, "aar", _maven_aar_build_file_template)
maven_jar = repository_rule(
implementation = _maven_jar_impl,
attrs = dict(_common_maven_rule_attrs.items() + {
# Needed for compatibility reasons with the native maven_jar rule.
"repository": attr.string(default = ""),
"server": attr.label(default = None),
}.items()),
local = False,
)
maven_aar = repository_rule(
implementation = _maven_aar_impl,
attrs = _common_maven_rule_attrs,
local = False,
)
def _maven_dependency_plugin_impl(ctx):
_BUILD_FILE = """
# DO NOT EDIT: automatically generated BUILD file for maven_dependency_plugin
filegroup(
name = 'files',
srcs = glob(['**']),
visibility = ['//visibility:public']
)
"""
ctx.file("BUILD", _BUILD_FILE, False)
_SETTINGS_XML = """
<!-- # DO NOT EDIT: automatically generated settings.xml for maven_dependency_plugin -->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>{localRepository}</localRepository>
<mirrors>
<mirror>
<id>central</id>
<url>{mirror}</url>
<mirrorOf>*,default</mirrorOf>
</mirror>
</mirrors>
</settings>
""".format(
localRepository = ctx.path("repository"),
mirror = MAVEN_CENTRAL_URL,
)
settings_path = ctx.path("settings.xml")
ctx.file("%s" % settings_path, _SETTINGS_XML, False)
# Download the plugin with transitive dependencies
mvn_flags = "-s %s" % settings_path
mvn_get = MVN_PLUGIN + ":get"
mvn_artifact = "-Dartifact=%s" % MVN_PLUGIN
command = " ".join(["mvn", mvn_flags, mvn_get, mvn_artifact])
exec_result = _execute(ctx, command)
if exec_result.return_code != 0:
fail("%s\nFailed to fetch Maven dependency" % exec_result.stderr)
_maven_dependency_plugin = repository_rule(
implementation = _maven_dependency_plugin_impl,
)
def maven_dependency_plugin():
_maven_dependency_plugin(name = "m2")