// Copyright 2015 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.workspace.maven; import static com.google.devtools.build.workspace.maven.ArtifactBuilder.InvalidArtifactCoordinateException; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.CharStreams; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.invoke.MethodHandles; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.Charset; import java.util.*; import java.util.logging.Logger; import javax.annotation.Nullable; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.model.*; import org.apache.maven.model.building.DefaultModelProcessor; import org.apache.maven.model.building.FileModelSource; import org.apache.maven.model.building.ModelSource; import org.apache.maven.model.io.DefaultModelReader; import org.apache.maven.model.locator.DefaultModelLocator; import org.apache.maven.model.resolution.UnresolvableModelException; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.util.artifact.JavaScopes; /** Resolves Maven dependencies. */ public class Resolver { private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName()); private static final String TOP_LEVEL_ARTIFACT = "pom.xml"; /** * The set of scopes whose artifacts are pulled into the transitive dependency tree. See * https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html for more * details on how maven handles this. */ private static final Set<String> INHERITED_SCOPES = Sets.newHashSet(JavaScopes.COMPILE, JavaScopes.RUNTIME); private static String unversionedCoordinate(Dependency dependency) { return dependency.getGroupId() + ":" + dependency.getArtifactId(); } private static String unversionedCoordinate(Exclusion exclusion) { return exclusion.getGroupId() + ":" + exclusion.getArtifactId(); } private final DefaultModelResolver modelResolver; // Mapping of maven_jar name to Rule. private final Map<String, Rule> deps; private final Map<String, String> restriction; private final VersionResolver versionResolver; @VisibleForTesting public Resolver( DefaultModelResolver modelResolver, VersionResolver versionResolver, List<Rule> aliases) { this.versionResolver = versionResolver; this.deps = Maps.newHashMap(); this.restriction = Maps.newHashMap(); this.modelResolver = modelResolver; aliases.forEach(alias -> addArtifact(alias, TOP_LEVEL_ARTIFACT)); } public Resolver(DefaultModelResolver resolver, List<Rule> aliases) { this(resolver, resolver.getVersionResolver(), aliases); } /** Returns all maven_jars. */ public Collection<Rule> getRules() { return deps.values(); } /** * Given a local path to a Maven project, this attempts to find the transitive dependencies of the * project. * * @param projectPath The path to search for Maven projects. * @param scopes The scopes to look up dependencies in. */ public String resolvePomDependencies(String projectPath, Set<String> scopes) { DefaultModelProcessor processor = new DefaultModelProcessor(); processor.setModelLocator(new DefaultModelLocator()); processor.setModelReader(new DefaultModelReader()); File pom = processor.locatePom(new File(projectPath)); FileModelSource pomSource = new FileModelSource(pom); // First resolve the model source locations. resolveSourceLocations(pomSource); // Next, fully resolve the models. Model model = modelResolver.getEffectiveModel(pomSource); if (model != null) { traverseDeps(model, scopes, Sets.newHashSet(), null); } return pom.getAbsolutePath(); } /** Resolves an artifact as a root of a dependency graph. */ public void resolveArtifact(String artifactCoord) { Artifact artifact; ModelSource modelSource; try { artifact = ArtifactBuilder.fromCoords(artifactCoord); modelSource = modelResolver.resolveModel(artifact); } catch (UnresolvableModelException | InvalidArtifactCoordinateException e) { logger.warning(e.getMessage()); return; } Rule rule = new Rule(artifact); rule.setRepository(modelSource.getLocation()); rule.setSha1(downloadSha1(rule)); deps.put(rule.name(), rule); // add the artifact rule to the workspace Model model = modelResolver.getEffectiveModel(modelSource); if (model != null) { traverseDeps(model, Sets.newHashSet(), Sets.newHashSet(), rule); } } /** * Resolves all dependencies from a given "model source," which could be either a URL or a local * file. */ @VisibleForTesting void traverseDeps(Model model, Set<String> scopes, Set<String> exclusions, Rule parent) { logger.info( "\tDownloading pom for " + model.getGroupId() + ":" + model.getArtifactId() + ":" + model.getVersion()); for (Repository repo : model.getRepositories()) { modelResolver.addRepository(repo); } if (model.getDependencyManagement() != null) { // Dependencies described in the DependencyManagement section of the pom override all others, // so resolve them first. for (Dependency dependency : model.getDependencyManagement().getDependencies()) { restriction.put( Rule.name(dependency.getGroupId(), dependency.getArtifactId()), dependency.getVersion()); } } for (Dependency dependency : model.getDependencies()) { addDependency(dependency, model, scopes, exclusions, parent); } } private void addDependency( Dependency dependency, Model model, Set<String> topLevelScopes, Set<String> exclusions, @Nullable Rule parent) { String scope = dependency.getScope(); // DependencyManagement dependencies don't have scope. if (scope != null) { if (parent == null) { // Top-level scopes get pulled in based on the user-provided scopes. if (!topLevelScopes.contains(scope)) { return; } } else { // TODO (bazel-devel): Relabel the scope of transitive dependencies so that they match how // maven relabels them as described here: // https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html if (!INHERITED_SCOPES.contains(scope)) { return; } } } if (dependency.isOptional()) { return; } if (exclusions.contains(unversionedCoordinate(dependency))) { return; } try { Rule artifactRule = new Rule(ArtifactBuilder.fromMavenDependency(dependency, versionResolver)); HashSet<String> localDepExclusions = Sets.newHashSet(exclusions); dependency .getExclusions() .forEach(exclusion -> localDepExclusions.add(unversionedCoordinate(exclusion))); boolean isNewDependency = addArtifact(artifactRule, model.toString()); if (isNewDependency) { ModelSource depModelSource = modelResolver.resolveModel( dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()); if (depModelSource != null) { artifactRule.setRepository(depModelSource.getLocation()); artifactRule.setSha1(downloadSha1(artifactRule)); Model depModel = modelResolver.getEffectiveModel(depModelSource); if (depModel != null) { traverseDeps(depModel, topLevelScopes, localDepExclusions, artifactRule); } } else { logger.warning("Could not get a model for " + dependency); } } if (parent == null) { addArtifact(artifactRule, TOP_LEVEL_ARTIFACT); } else { parent.addDependency(artifactRule); parent.getDependencies().addAll(artifactRule.getDependencies()); } } catch (UnresolvableModelException | InvalidArtifactCoordinateException e) { logger.warning( "Could not resolve dependency " + dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion() + ": " + e.getMessage()); } } /** Find the POM files for a given pom's parent(s) and submodules. */ private void resolveSourceLocations(FileModelSource fileModelSource) { Model model = modelResolver.getRawModel(fileModelSource); if (model == null) { return; } // Self. Parent parent = model.getParent(); if (model.getGroupId() == null) { model.setGroupId(parent.getGroupId()); } if (!modelResolver.putModelSource(model.getGroupId(), model.getArtifactId(), fileModelSource)) { return; } // Parent. File pomDirectory = new File(fileModelSource.getLocation()).getParentFile(); if (parent != null && !parent.getArtifactId().equals(model.getArtifactId())) { File parentPom; try { parentPom = new File(pomDirectory, parent.getRelativePath()).getCanonicalFile(); } catch (IOException e) { logger.warning( "Unable to get canonical path of " + pomDirectory + " and " + parent.getRelativePath()); return; } if (parentPom.exists()) { resolveSourceLocations(new FileModelSource(parentPom)); } } // Submodules. for (String module : model.getModules()) { resolveSourceLocations(new FileModelSource(new File(pomDirectory, module + "/pom.xml"))); } } /** * Adds the artifact to the map of deps, if it is not already there. Returns if the artifact was * newly added. If the artifact was in the list at a different version, adds an comment about the * desired version. */ private boolean addArtifact(Rule dependency, String parent) { String artifactName = dependency.name(); if (deps.containsKey(artifactName)) { Rule existingDependency = deps.get(artifactName); // Check that the versions are the same. if (!existingDependency.version().equals(dependency.version())) { existingDependency.addParent(parent + " wanted version " + dependency.version()); } else { existingDependency.addParent(parent + " got requested version"); } return false; } updateVersion(artifactName, dependency); deps.put(artifactName, dependency); dependency.addParent(parent); return true; } /** TODO: this should be removed once this uses Maven's own version resolution. */ private void updateVersion(String artifactName, Rule dependency) { VersionRange versionRange; if (!restriction.containsKey(artifactName)) { return; } String versionRestriction = restriction.get(artifactName); try { versionRange = VersionRange.createFromVersionSpec(versionRestriction); } catch (InvalidVersionSpecificationException e) { logger.warning( "Error parsing version " + versionRestriction + ": " + e.getLocalizedMessage()); // So that this isn't logged over and over. restriction.remove(artifactName); return; } if (!versionRange.containsVersion(new DefaultArtifactVersion(dependency.version()))) { try { dependency.setVersion( versionResolver.resolveVersion( dependency.groupId(), dependency.artifactId(), versionRestriction)); } catch (InvalidArtifactCoordinateException e) { logger.warning("Error setting version: " + e.getLocalizedMessage()); } } } static String getSha1Url(String url, String extension) { return url.replaceAll(".pom$", "." + extension + ".sha1"); } /** Downloads the SHA-1 for the given artifact. */ private String downloadSha1(Rule rule) { String sha1Url = getSha1Url(rule.getUrl(), rule.getArtifact().getExtension()); try { InputStream in = httpGet(sha1Url); return extractSha1(CharStreams.toString(new InputStreamReader(in, Charset.defaultCharset()))); } catch (IOException e) { logger.warning("Failed to download the sha1 at " + sha1Url); } return null; } static String extractSha1(String sha1Contents) { return sha1Contents.split("\\s+")[0]; } protected InputStream httpGet(String url) throws IOException { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setInstanceFollowRedirects(true); connection.connect(); return connection.getInputStream(); } }