aboutsummaryrefslogtreecommitdiffhomepage
path: root/tools/build_defs/repo/maven_rules.bzl
blob: 8e18b9ddd2e0fecf0d865c28638da934082437fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# 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)

def _file_exists(ctx, filename):
    return _execute(ctx, "[[ -f %s ]] && exit 0 || exit 1" % filename).return_code == 0

# 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 compatability 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")