blob: 574b44afab1f39b0d06439c718ceffe24e9bde89 [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.
package com.google.devtools.build.lib.bazel.repository;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.rules.repository.WorkspaceAttributeMapper;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.vfs.Path;
import java.io.IOException;
import java.util.Map;
import java.util.StringJoiner;
import javax.annotation.Nullable;
import org.apache.maven.settings.Server;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.AuthenticationDigest;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
/**
* Downloader for JAR files from Maven repositories.
* TODO(jingwen): standardize interface between this and HttpDownloader
*/
public class MavenDownloader extends HttpDownloader {
public MavenDownloader(RepositoryCache repositoryCache) {
super(repositoryCache);
}
/**
* Download the Maven artifact to the output directory. Returns the path to the jar (and the
* srcjar if available).
*/
public JarPaths download(
String name,
WorkspaceAttributeMapper mapper,
Path outputDirectory,
MavenServerValue serverValue,
ExtendedEventHandler eventHandler)
throws IOException, EvalException {
String url = serverValue.getUrl();
Server server = serverValue.getServer();
// Initialize maven artifacts
String artifactCoords = mapper.get("artifact", Type.STRING);
String sha1 = retrieveSha1(name, "sha1", mapper);
String sha1Src = retrieveSha1(name, "sha1_src", mapper);
Artifact artifact;
try {
artifact = new DefaultArtifact(artifactCoords);
} catch (IllegalArgumentException e) {
throw new IOException(e.getMessage());
}
Artifact artifactWithSrcs = srcjarCoords(artifact);
boolean isCaching = repositoryCache.isEnabled() && KeyType.SHA1.isValid(sha1);
if (isCaching) {
Path downloadPath = getDownloadDestination(outputDirectory, artifact);
try {
Path cachedDestination = repositoryCache.get(sha1, downloadPath, KeyType.SHA1);
if (cachedDestination != null) {
Path cachedDestinationSrc = null;
if (sha1Src != null) {
Path downloadPathSrc = getDownloadDestination(outputDirectory, artifactWithSrcs);
cachedDestinationSrc = repositoryCache.get(sha1Src, downloadPathSrc, KeyType.SHA1);
}
return new JarPaths(cachedDestination, Optional.fromNullable(cachedDestinationSrc));
}
} catch (IOException e) {
eventHandler.handle(
Event.debug("RepositoryCache entry " + sha1 + " is invalid, replacing it..."));
// Ignore error trying to get. We'll just download again.
}
}
// Setup env for fetching jars
MavenConnector connector = new MavenConnector(outputDirectory.getPathString());
RepositorySystem system = connector.newRepositorySystem();
RepositorySystemSession session = connector.newRepositorySystemSession(system);
RemoteRepository repository =
new RemoteRepository.Builder(name, MavenServerValue.DEFAULT_ID, url)
.setAuthentication(new MavenAuthentication(server))
.build();
// Try fetching jar.
final Path jarDownload;
try {
artifact = downloadArtifact(artifact, repository, session, system);
} catch (ArtifactResolutionException e) {
throw new IOException("Failed to fetch Maven dependency: " + e.getMessage());
}
// Try also fetching srcjar.
try {
artifactWithSrcs = downloadArtifact(artifactWithSrcs, repository, session, system);
} catch (ArtifactResolutionException e) {
// Intentionally ignored - missing srcjar is not an error.
}
jarDownload = outputDirectory.getRelative(artifact.getFile().getAbsolutePath());
// Verify checksum.
if (!Strings.isNullOrEmpty(sha1)) {
RepositoryCache.assertFileChecksum(sha1, jarDownload, KeyType.SHA1);
}
Path srcjarDownload = null;
if (artifactWithSrcs.getFile() != null) {
srcjarDownload = outputDirectory.getRelative(artifactWithSrcs.getFile().getAbsolutePath());
if (!Strings.isNullOrEmpty(sha1Src)) {
RepositoryCache.assertFileChecksum(sha1Src, srcjarDownload, KeyType.SHA1);
}
}
if (isCaching) {
repositoryCache.put(sha1, jarDownload, KeyType.SHA1);
if (srcjarDownload != null && !Strings.isNullOrEmpty(sha1Src)) {
repositoryCache.put(sha1Src, srcjarDownload, KeyType.SHA1);
}
}
return new JarPaths(jarDownload, Optional.fromNullable(srcjarDownload));
}
private String retrieveSha1(String name, String attribute, WorkspaceAttributeMapper mapper)
throws EvalException, IOException {
String sha1 =
mapper.isAttributeValueExplicitlySpecified(attribute)
? mapper.get(attribute, Type.STRING)
: null;
if (sha1 != null && !KeyType.SHA1.isValid(sha1)) {
throw new IOException("Invalid SHA-1 for maven_jar " + name + ": '" + sha1 + "'");
}
return sha1;
}
private Path getDownloadDestination(Path outputDirectory, Artifact artifact) {
String groupIdPath = artifact.getGroupId().replace('.', '/');
String artifactId = artifact.getArtifactId();
String classifier = artifact.getClassifier();
String version = artifact.getVersion();
String filename = artifactId + '-' + version;
if (classifier.equals("sources")) {
filename += "-sources";
}
filename += '.' + artifact.getExtension();
StringJoiner joiner = new StringJoiner("/");
joiner.add(groupIdPath).add(artifactId).add(version).add(filename);
return outputDirectory.getRelative(joiner.toString());
}
private Artifact srcjarCoords(Artifact jar) {
return new DefaultArtifact(
jar.getGroupId(), jar.getArtifactId(), "sources", jar.getExtension(), jar.getVersion());
}
/*
* Set up request for and resolve (retrieve to local repo) artifact
*/
private Artifact downloadArtifact(
Artifact artifact,
RemoteRepository repository,
RepositorySystemSession session,
RepositorySystem system)
throws ArtifactResolutionException {
ArtifactRequest artifactRequest = new ArtifactRequest();
artifactRequest.setArtifact(artifact);
artifactRequest.setRepositories(ImmutableList.of(repository));
ArtifactResult artifactResult = system.resolveArtifact(session, artifactRequest);
return artifactResult.getArtifact();
}
/*
* Class for packaging srcjar and jar paths together when srcjar is available.
*/
static class JarPaths {
final Path jar;
@Nullable final Optional<Path> srcjar;
private JarPaths(Path jar, Optional<Path> srcjar) {
this.jar = jar;
this.srcjar = srcjar;
}
}
private static class MavenAuthentication implements Authentication {
private final Map<String, String> authenticationInfo;
private MavenAuthentication(Server server) {
ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String>builder();
// From https://maven.apache.org/settings.html: "If you use a private key to login to the
// server, make sure you omit the <password> element. Otherwise, the key will be ignored."
if (server.getPassword() != null) {
builder.put(AuthenticationContext.USERNAME, server.getUsername());
builder.put(AuthenticationContext.PASSWORD, server.getPassword());
} else if (server.getPrivateKey() != null) {
// getPrivateKey sounds like it returns the key, but it actually returns a path to it.
builder.put(AuthenticationContext.PRIVATE_KEY_PATH, server.getPrivateKey());
builder.put(AuthenticationContext.PRIVATE_KEY_PASSPHRASE, server.getPassphrase());
}
authenticationInfo = builder.build();
}
@Override
public void fill(
AuthenticationContext authenticationContext, String s, Map<String, String> map) {
for (Map.Entry<String, String> entry : authenticationInfo.entrySet()) {
authenticationContext.put(entry.getKey(), entry.getValue());
}
}
@Override
public void digest(AuthenticationDigest authenticationDigest) {
// No-op.
}
}
}