blob: d746bb074df4311cc8e20d3a1686e926b2ab4a95 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* https://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 org.apache.ivy.core.resolve;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.ivy.Ivy;
import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.LogOptions;
import org.apache.ivy.core.cache.ArtifactOrigin;
import org.apache.ivy.core.cache.DefaultResolutionCacheManager;
import org.apache.ivy.core.cache.ResolutionCacheManager;
import org.apache.ivy.core.event.EventManager;
import org.apache.ivy.core.event.download.PrepareDownloadEvent;
import org.apache.ivy.core.event.resolve.EndResolveEvent;
import org.apache.ivy.core.event.resolve.StartResolveEvent;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleId;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.report.ArtifactDownloadReport;
import org.apache.ivy.core.report.ConfigurationResolveReport;
import org.apache.ivy.core.report.DownloadReport;
import org.apache.ivy.core.report.DownloadStatus;
import org.apache.ivy.core.report.ResolveReport;
import org.apache.ivy.core.resolve.IvyNodeEviction.EvictionData;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.core.sort.SortEngine;
import org.apache.ivy.core.sort.SortOptions;
import org.apache.ivy.plugins.conflict.ConflictManager;
import org.apache.ivy.plugins.parser.ModuleDescriptorParser;
import org.apache.ivy.plugins.parser.ModuleDescriptorParserRegistry;
import org.apache.ivy.plugins.repository.url.URLResource;
import org.apache.ivy.plugins.resolver.DependencyResolver;
import org.apache.ivy.plugins.version.VersionMatcher;
import org.apache.ivy.util.Message;
import org.apache.ivy.util.filter.Filter;
/**
* The resolve engine which is the core of the dependency resolution mechanism used in Ivy. It
* features several resolve methods, some very simple, like {@link #resolve(File)} and
* {@link #resolve(URL)} which allow to simply resolve dependencies of a single module descriptor,
* or more complete one, like the {@link #resolve(ModuleDescriptor, ResolveOptions)} which allows to
* provide options to the resolution engine.
*
* @see ResolveOptions
*/
public class ResolveEngine {
private ResolveEngineSettings settings;
private EventManager eventManager;
private SortEngine sortEngine;
private DependencyResolver dictatorResolver;
/**
* Constructs a ResolveEngine.
*
* @param settings
* the settings to use to configure the engine. Must not be null.
* @param eventManager
* the event manager to use to send events about the resolution process. Must not be
* null.
* @param sortEngine
* the sort engine to use to sort modules before producing the dependency resolution
* report. Must not be null.
*/
public ResolveEngine(ResolveEngineSettings settings, EventManager eventManager,
SortEngine sortEngine) {
this.settings = settings;
this.eventManager = eventManager;
this.sortEngine = sortEngine;
}
/**
* Returns the currently configured dictator resolver, which when non null is used in place of
* any specified resolver in the {@link IvySettings}
*
* @return the currently configured dictator resolver, may be null.
*/
public DependencyResolver getDictatorResolver() {
return dictatorResolver;
}
/**
* Sets a dictator resolver, which is used in place of regular dependency resolver for
* subsequent dependency resolution by this engine.
*
* @param dictatorResolver
* the dictator resolver to use in this engine, null if regular settings should used
*/
public void setDictatorResolver(DependencyResolver dictatorResolver) {
this.dictatorResolver = dictatorResolver;
}
public ResolveReport resolve(File ivySource) throws ParseException, IOException {
return resolve(ivySource.toURI().toURL());
}
public ResolveReport resolve(URL ivySource) throws ParseException, IOException {
return resolve(ivySource, new ResolveOptions());
}
/**
* Resolves the module identified by the given mrid with its dependencies if transitive is set
* to true.
*
* @param mrid ModuleRevisionId
* @param options ResolveOptions
* @param changing boolean
* @return ResolveReport
* @throws ParseException if something goes wrong
* @throws IOException if something goes wrong
*/
public ResolveReport resolve(final ModuleRevisionId mrid, ResolveOptions options,
boolean changing) throws ParseException, IOException {
DefaultModuleDescriptor md;
ResolveOptions optionsToUse = new ResolveOptions(options);
if (options.useSpecialConfs()) {
// create new resolve options because this is a different resolve than the real resolve
// (which will be a resolve of a newCallerInstance module)
ResolvedModuleRevision rmr = findModule(mrid, new ResolveOptions(options));
if (rmr == null) {
Message.verbose("module not found " + mrid);
// we will continue the resolve anyway to get a nice error message back
// to the user, however reduce the amount of logging in this case
optionsToUse.setLog(LogOptions.LOG_DOWNLOAD_ONLY);
md = DefaultModuleDescriptor.newCallerInstance(mrid, new String[] {"default"},
options.isTransitive(), changing);
} else {
String[] confs = options.getConfs(rmr.getDescriptor());
md = DefaultModuleDescriptor.newCallerInstance(
ModuleRevisionId.newInstance(mrid, rmr.getId().getRevision()), confs,
options.isTransitive(), changing);
}
} else {
md = DefaultModuleDescriptor.newCallerInstance(mrid, options.getConfs(),
options.isTransitive(), changing);
}
return resolve(md, optionsToUse);
}
/**
* Resolve dependencies of a module described by an ivy file.
*
* @param ivySource URL
* @param options ResolveOptions
* @return ResolveReport
* @throws ParseException if something goes wrong
* @throws IOException if something goes wrong
*/
public ResolveReport resolve(URL ivySource, ResolveOptions options) throws ParseException,
IOException {
URLResource res = new URLResource(ivySource);
ModuleDescriptorParser parser = ModuleDescriptorParserRegistry.getInstance().getParser(res);
Message.verbose("using " + parser + " to parse " + ivySource);
ModuleDescriptor md = parser.parseDescriptor(settings, ivySource, options.isValidate());
String revision = options.getRevision();
if (revision == null && md.getResolvedModuleRevisionId().getRevision() == null) {
revision = Ivy.getWorkingRevision();
}
if (revision != null) {
md.setResolvedModuleRevisionId(ModuleRevisionId.newInstance(md.getModuleRevisionId(),
revision));
}
return resolve(md, options);
}
/**
* Resolve dependencies of a module described by a module descriptor.
* @param md ModuleDescriptor
* @param options ResolveOptions
* @return ResolveReport
* @throws ParseException if something goes wrong
* @throws IOException if something goes wrong
*/
public ResolveReport resolve(ModuleDescriptor md, ResolveOptions options)
throws ParseException, IOException {
DependencyResolver oldDictator = getDictatorResolver();
IvyContext context = IvyContext.getContext();
try {
String[] confs = options.getConfs(md);
options.setConfs(confs);
if (options.getResolveId() == null) {
options.setResolveId(ResolveOptions.getDefaultResolveId(md));
}
eventManager.fireIvyEvent(new StartResolveEvent(md, confs));
long start = System.currentTimeMillis();
if (ResolveOptions.LOG_DEFAULT.equals(options.getLog())) {
Message.info(":: resolving dependencies :: " + md.getResolvedModuleRevisionId()
+ (options.isTransitive() ? "" : " [not transitive]"));
Message.info("\tconfs: " + Arrays.asList(confs));
} else {
Message.verbose(":: resolving dependencies :: " + md.getResolvedModuleRevisionId()
+ (options.isTransitive() ? "" : " [not transitive]"));
Message.verbose("\tconfs: " + Arrays.asList(confs));
}
Message.verbose("\tvalidate = " + options.isValidate());
Message.verbose("\trefresh = " + options.isRefresh());
ResolveReport report = new ResolveReport(md, options.getResolveId());
ResolveData data = new ResolveData(this, options);
context.setResolveData(data);
// resolve dependencies
IvyNode[] dependencies = getDependencies(md, options, report);
report.setDependencies(Arrays.asList(dependencies), options.getArtifactFilter());
if (options.getCheckIfChanged()) {
report.checkIfChanged();
}
// produce resolved ivy file and ivy properties in cache
ResolutionCacheManager cacheManager = settings.getResolutionCacheManager();
cacheManager.saveResolvedModuleDescriptor(md);
// we store the resolved dependencies revisions and statuses per asked dependency
// revision id, for direct dependencies only.
// this is used by the deliver task to resolve dynamic revisions to static ones
File ivyPropertiesInCache = cacheManager.getResolvedIvyPropertiesInCache(md
.getResolvedModuleRevisionId());
if (cacheManager instanceof DefaultResolutionCacheManager) {
((DefaultResolutionCacheManager) cacheManager).assertInsideCache(ivyPropertiesInCache);
}
Properties props = new Properties();
if (dependencies.length > 0) {
Map<ModuleId, ModuleRevisionId> forcedRevisions = new HashMap<>();
for (IvyNode dependency : dependencies) {
if (dependency.getModuleRevision() != null
&& dependency.getModuleRevision().isForce()) {
forcedRevisions.put(dependency.getModuleId(),
dependency.getResolvedId());
}
}
IvyNode root = dependencies[0].getRoot();
Map<ModuleId, IvyNode> topLevelDeps = new HashMap<>();
for (IvyNode dependency : dependencies) {
if (!dependency.hasProblem()) {
DependencyDescriptor dd = dependency.getDependencyDescriptor(root);
if (dd != null) {
ModuleId orgMod = dependency.getModuleId();
topLevelDeps.put(orgMod, dependency);
}
}
}
for (IvyNode dependency : dependencies) {
if (!dependency.hasProblem() && !dependency.isCompletelyEvicted()) {
DependencyDescriptor dd = dependency.getDependencyDescriptor(root);
if (dd == null) {
ModuleId mid = dependency.getModuleId();
IvyNode tlDep = topLevelDeps.get(mid);
if (tlDep != null) {
dd = tlDep.getDependencyDescriptor(root);
}
}
if (dd != null) {
ModuleRevisionId depResolvedId = dependency.getResolvedId();
ModuleDescriptor depDescriptor = dependency.getDescriptor();
ModuleRevisionId depRevisionId = dd.getDependencyRevisionId();
ModuleRevisionId forcedRevisionId = forcedRevisions.get(dependency
.getModuleId());
if (dependency.getModuleRevision() != null
&& dependency.getModuleRevision().isForce()
&& !depResolvedId.equals(depRevisionId)
&& !settings.getVersionMatcher().isDynamic(depRevisionId)) {
// if we were forced to this revision and we
// are not a dynamic revision, reset to the
// asked revision
depResolvedId = depRevisionId;
depDescriptor = null;
}
if (depResolvedId == null) {
throw new NullPointerException("getResolvedId() is null for "
+ dependency.toString());
}
if (depRevisionId == null) {
throw new NullPointerException("getDependencyRevisionId() "
+ "is null for " + dd.toString());
}
String rev = depResolvedId.getRevision();
String forcedRev = forcedRevisionId == null ? rev : forcedRevisionId
.getRevision();
// The evicted modules have no description, so we can't put the status
String status = depDescriptor == null ? "?" : depDescriptor.getStatus();
Message.debug("storing dependency " + depResolvedId + " in props");
props.put(depRevisionId.encodeToString(), rev + " " + status + " "
+ forcedRev + " " + depResolvedId.getBranch());
}
}
}
}
FileOutputStream out = new FileOutputStream(ivyPropertiesInCache);
props.store(out, md.getResolvedModuleRevisionId() + " resolved revisions");
out.close();
Message.verbose("\tresolved ivy file produced in cache");
report.setResolveTime(System.currentTimeMillis() - start);
if (options.isDownload()) {
Message.verbose(":: downloading artifacts ::");
DownloadOptions downloadOptions = new DownloadOptions();
downloadOptions.setLog(options.getLog());
downloadArtifacts(report, options.getArtifactFilter(), downloadOptions);
}
if (options.isOutputReport()) {
outputReport(report, cacheManager, options);
}
Message.verbose("\tresolve done (" + report.getResolveTime() + "ms resolve - "
+ report.getDownloadTime() + "ms download)");
Message.sumupProblems();
eventManager.fireIvyEvent(new EndResolveEvent(md, confs, report));
return report;
} catch (RuntimeException ex) {
Message.debug(ex);
Message.error(ex.getMessage());
Message.sumupProblems();
throw ex;
} finally {
context.setResolveData(null);
setDictatorResolver(oldDictator);
}
}
public void outputReport(ResolveReport report, ResolutionCacheManager cacheMgr,
ResolveOptions options) throws IOException {
if (ResolveOptions.LOG_DEFAULT.equals(options.getLog())) {
Message.info(":: resolution report :: resolve " + report.getResolveTime() + "ms"
+ " :: artifacts dl " + report.getDownloadTime() + "ms");
} else {
Message.verbose(":: resolution report :: resolve " + report.getResolveTime() + "ms"
+ " :: artifacts dl " + report.getDownloadTime() + "ms");
}
report.setProblemMessages(Message.getProblems());
// output report
report.output(settings.getReportOutputters(), cacheMgr, options);
}
public void downloadArtifacts(ResolveReport report, Filter<Artifact> artifactFilter,
DownloadOptions options) {
long start = System.currentTimeMillis();
eventManager.fireIvyEvent(new PrepareDownloadEvent(report.getArtifacts().toArray(
new Artifact[report.getArtifacts().size()])));
long totalSize = 0;
for (IvyNode dependency : report.getDependencies()) {
checkInterrupted();
// download artifacts required in all asked configurations
if (!dependency.isCompletelyEvicted() && !dependency.hasProblem()
&& dependency.getModuleRevision() != null) {
DependencyResolver resolver = dependency.getModuleRevision()
.getArtifactResolver();
Artifact[] selectedArtifacts = dependency.getSelectedArtifacts(artifactFilter);
DownloadReport dReport = resolver.download(selectedArtifacts, options);
for (ArtifactDownloadReport adr : dReport.getArtifactsReports()) {
if (adr.getDownloadStatus() == DownloadStatus.FAILED) {
if (adr.getArtifact().getExtraAttribute("ivy:merged") != null) {
Message.warn("\tmerged artifact not found: " + adr.getArtifact()
+ ". It was required in "
+ adr.getArtifact().getExtraAttribute("ivy:merged"));
} else {
Message.warn("\t" + adr);
resolver.reportFailure(adr.getArtifact());
}
} else if (adr.getDownloadStatus() == DownloadStatus.SUCCESSFUL) {
totalSize += adr.getSize();
}
}
// update concerned reports
for (String dconf : dependency.getRootModuleConfigurations()) {
// the report itself is responsible to take into account only
// artifacts required in its corresponding configuration
// (as described by the Dependency object)
if (dependency.isEvicted(dconf)
|| dependency.isBlacklisted(dconf)) {
report.getConfigurationReport(dconf).addDependency(dependency);
} else {
report.getConfigurationReport(dconf).addDependency(dependency,
dReport);
}
}
}
}
report.setDownloadTime(System.currentTimeMillis() - start);
report.setDownloadSize(totalSize);
}
/**
* Download an artifact to the cache. Not used internally, useful especially for IDE plugins
* needing to download artifact one by one (for source or javadoc artifact, for instance).
* <p>
* Downloaded artifact file can be accessed using {@link ArtifactDownloadReport#getLocalFile()}.
* </p>
* <p>
* It is possible to track the progression of the download using classical ivy progress
* monitoring feature (see addTransferListener).
* </p>
*
* @param artifact
* the artifact to download
* @param options DownloadOptions
* @return a report concerning the download
* @see #download(ArtifactOrigin, DownloadOptions)
*/
public ArtifactDownloadReport download(Artifact artifact, DownloadOptions options) {
DependencyResolver resolver = settings.getResolver(artifact.getModuleRevisionId());
DownloadReport r = resolver.download(new Artifact[] {artifact}, options);
return r.getArtifactReport(artifact);
}
/**
* Locates an artifact in dependency resolvers, and return its location if it can be located and
* actually exists, or an unknown {@link ArtifactOrigin} in other cases.
*
* @param artifact
* the artifact to locate.
* @return the artifact location, should be tested with
* {@link ArtifactOrigin#isUnknown(ArtifactOrigin)} to check if the artifact has
* actually been located.
*/
public ArtifactOrigin locate(Artifact artifact) {
DependencyResolver resolver = settings.getResolver(artifact.getModuleRevisionId());
return resolver.locate(artifact);
}
/**
* Materialize an artifact already located.
* <p>
* Not used internally, useful especially for IDE plugins needing to download artifact one by
* one (for source or javadoc artifact, for instance).
* </p>
* <p>
* Materialized artifact file can be accessed using
* {@link ArtifactDownloadReport#getLocalFile()}.
* </p>
* <p>
* It is possible to track the progression of the download using classical ivy progress
* monitoring feature (see addTransferListener).
* </p>
*
* @param origin
* the artifact origin to materialize
* @param options DownloadOptions
* @return a report concerning the download
* @see #download(Artifact, DownloadOptions)
* @see #locate(Artifact)
*/
public ArtifactDownloadReport download(ArtifactOrigin origin, DownloadOptions options) {
DependencyResolver resolver = settings.getResolver(origin.getArtifact()
.getModuleRevisionId());
return resolver.download(origin, options);
}
/**
* Resolve the dependencies of a module without downloading corresponding artifacts. The module
* to resolve is given by its ivy file URL. This method requires appropriate configuration of
* the ivy instance, especially resolvers.
*
* @param ivySource
* url of the ivy file to use for dependency resolving
* @param options
* ditto
* @return an array of the resolved dependencies
* @throws ParseException
* if a parsing problem occurred in the ivy file
* @throws IOException
* if an IO problem was raised during ivy file parsing
*/
public IvyNode[] getDependencies(URL ivySource, ResolveOptions options) throws ParseException,
IOException {
return getDependencies(
ModuleDescriptorParserRegistry.getInstance().parseDescriptor(settings, ivySource,
options.isValidate()), options, null);
}
/**
* Resolve the dependencies of a module without downloading corresponding artifacts. The module
* to resolve is given by its module descriptor. This method requires appropriate configuration
* of the ivy instance, especially resolvers.
* <p>
* The <code>IvyNode</code>s are ordered from the most dependent to the less dependent, so that
* an IvyNode is always found in the list after all IvyNode depending directly on it.
*
* @param md
* the descriptor of the module for which we want to get dependencies - must not be
* null
* @param options
* the resolve options to use to resolve the dependencies
* @param report
* a resolve report to fill during resolution - may be null
* @return an array of the resolved Dependencies
*/
public IvyNode[] getDependencies(ModuleDescriptor md, ResolveOptions options,
ResolveReport report) {
// check parameters
if (md == null) {
throw new NullPointerException("module descriptor must not be null");
}
String[] confs = options.getConfs(md);
Collection<String> missingConfs = new ArrayList<>();
for (String conf : confs) {
if (conf == null) {
throw new NullPointerException("null conf not allowed: confs where: "
+ Arrays.asList(confs));
}
if (md.getConfiguration(conf) == null) {
missingConfs.add(" '" + conf + "' ");
}
}
if (!missingConfs.isEmpty()) {
throw new IllegalArgumentException("requested configuration"
+ (missingConfs.size() > 1 ? "s" : "") + " not found in "
+ md.getModuleRevisionId() + ": " + missingConfs);
}
IvyContext context = IvyContext.pushNewCopyContext();
try {
options.setConfs(confs);
Date reportDate = new Date();
ResolveData data = context.getResolveData();
if (data == null) {
data = new ResolveData(this, options);
context.setResolveData(data);
}
IvyNode rootNode = new IvyNode(data, md);
for (String conf : confs) {
Message.verbose("resolving dependencies for configuration '" + conf + "'");
ConfigurationResolveReport confReport = null;
if (report != null) {
confReport = report.getConfigurationReport(conf);
if (confReport == null) {
confReport = new ConfigurationResolveReport(this, md, conf, reportDate,
options);
report.addReport(conf, confReport);
}
}
// we reuse the same resolve data with a new report for each conf
data.setReport(confReport);
// update the root module conf we are about to fetch
VisitNode root = new VisitNode(data, rootNode, null, conf, null);
root.setRequestedConf(conf);
rootNode.updateConfsToFetch(Collections.singleton(conf));
// go fetch !
boolean fetched = false;
while (!fetched) {
try {
fetchDependencies(root, conf, new HashSet<String>(), false);
fetched = true;
} catch (RestartResolveProcess restart) {
Message.verbose("====================================================");
Message.verbose("= RESTARTING RESOLVE PROCESS");
Message.verbose("= " + restart.getMessage());
Message.verbose("====================================================");
}
}
// clean data
for (IvyNode dep : data.getNodes()) {
dep.clean();
}
}
// prune and reverse sort fetched dependencies
Collection<IvyNode> nodes = data.getNodes();
// use a Set to avoid duplicates, linked to preserve order
Collection<IvyNode> dependencies = new LinkedHashSet<>(nodes.size());
for (IvyNode node : nodes) {
if (node != null && !node.isRoot() && !node.isCompletelyBlacklisted()) {
dependencies.add(node);
}
}
List<IvyNode> sortedDependencies = sortEngine.sortNodes(dependencies,
SortOptions.SILENT);
Collections.reverse(sortedDependencies);
handleTransitiveEviction(md, confs, data, sortedDependencies);
return dependencies.toArray(new IvyNode[dependencies.size()]);
} finally {
IvyContext.popContext();
}
}
private void handleTransitiveEviction(ModuleDescriptor md, String[] confs, ResolveData data,
List<IvyNode> sortedDependencies) {
// handle transitive eviction now:
// if a module has been evicted then all its dependencies required only by it should be
// evicted too. Since nodes are now sorted from the more dependent to the less one, we
// can traverse the list and check only the direct parent and not all the ancestors
for (IvyNode node : sortedDependencies) {
if (!node.isCompletelyEvicted()) {
for (String conf : confs) {
IvyNodeCallers.Caller[] callers = node.getCallers(conf);
if (settings.debugConflictResolution()) {
Message.debug("checking if " + node.getId()
+ " is transitively evicted in " + conf);
}
boolean allEvicted = callers.length > 0;
for (IvyNodeCallers.Caller caller : callers) {
if (caller.getModuleRevisionId().equals(md.getModuleRevisionId())) {
// the caller is the root module itself, it can't be evicted
allEvicted = false;
break;
} else {
IvyNode callerNode = data.getNode(caller.getModuleRevisionId());
if (callerNode == null) {
Message.warn("ivy internal error: no node found for "
+ caller.getModuleRevisionId() + ": looked in "
+ data.getNodeIds() + " and root module id was "
+ md.getModuleRevisionId());
} else if (!callerNode.isEvicted(conf)) {
allEvicted = false;
break;
} else {
if (settings.debugConflictResolution()) {
Message.debug("caller " + callerNode.getId() + " of "
+ node.getId() + " is evicted");
}
}
}
}
if (allEvicted) {
Message.verbose("all callers are evicted for " + node + ": evicting too");
node.markEvicted(conf, null, null, null);
} else {
if (settings.debugConflictResolution()) {
Message.debug(node.getId()
+ " isn't transitively evicted, at least one caller was"
+ " not evicted");
}
}
}
}
}
}
private void fetchDependencies(VisitNode node, String conf, Set<String> fetchedSet, boolean shouldBePublic) {
checkInterrupted();
long start = System.currentTimeMillis();
if (node.getParent() != null) {
Message.verbose("== resolving dependencies " + node.getParent().getId() + "->"
+ node.getId() + " [" + node.getParentConf() + "->" + conf + "]");
} else {
Message.verbose("== resolving dependencies for " + node.getId() + " [" + conf + "]");
}
ResolveData data = node.getNode().getData();
VisitNode parentVisitNode = data.getCurrentVisitNode();
data.setCurrentVisitNode(node);
DependencyDescriptor dd = node.getDependencyDescriptor();
VersionMatcher versionMatcher = node.getNode().getData().getSettings().getVersionMatcher();
if (dd != null && !(node.getRoot() == node.getParent()
&& versionMatcher.isDynamic(dd.getDependencyRevisionId()))) {
/*
* we don't resolve conflicts before loading data for direct dependencies on dynamic
* revisions, so that direct dynamic revisions are always resolved, which is mandatory
* for proper replacement of dynamic revisions during 'deliver'
*/
resolveConflict(node, conf);
}
if (node.loadData(conf, shouldBePublic)) {
// we resolve conflict again now that we have all information loaded
// indeed in some cases conflict manager need more information than just asked
// dependency to take the decision
resolveConflict(node, conf);
if (!node.isEvicted() && !node.isCircular()) {
for (String rconf : node.getRealConfs(conf)) {
doFetchDependencies(node, rconf, fetchedSet);
}
}
} else if (!node.hasProblem()) {
// the node has not been loaded but hasn't problem: it was already loaded
// => we just have to update its dependencies data
if (!node.isEvicted() && !node.isCircular()) {
for (String rconf : node.getRealConfs(conf)) {
doFetchDependencies(node, rconf, fetchedSet);
}
}
}
if (node.isEvicted()) {
// update selected nodes with confs asked in evicted one
EvictionData ed = node.getEvictedData();
if (ed.getSelected() != null) {
for (IvyNode selected : ed.getSelected()) {
if (!selected.isLoaded()) {
// the node is not yet loaded, we can simply update its set of
// configurations to fetch
selected.updateConfsToFetch(Collections.singleton(conf));
} else {
// the node has already been loaded, we must fetch its dependencies in the
// required conf
fetchDependencies(node.gotoNode(selected), conf, fetchedSet, true);
}
}
}
}
if (settings.debugConflictResolution()) {
Message.debug(node.getId() + " => dependencies resolved in " + conf + " ("
+ (System.currentTimeMillis() - start) + "ms)");
}
data.setCurrentVisitNode(parentVisitNode);
}
private void doFetchDependencies(VisitNode node, String conf, Set<String> fetchedSet) {
Configuration c = node.getConfiguration(conf);
if (c == null) {
if (!node.isConfRequiredByMergedUsageOnly(conf)) {
Message.warn("configuration not found '" + conf + "' in " + node.getResolvedId()
+ ": ignoring");
if (node.getParent() != null) {
Message.warn("it was required from " + node.getParent().getResolvedId());
}
}
return;
}
// we handle the case where the asked configuration extends others:
// we have to first fetch the extended configurations
// first we check if this is the actual requested conf (not an extended one)
boolean requestedConfSet = false;
if (node.getRequestedConf() == null) {
node.setRequestedConf(conf);
requestedConfSet = true;
}
// now let's recurse in extended confs
String[] extendedConfs = c.getExtends();
if (extendedConfs.length > 0) {
node.updateConfsToFetch(Arrays.asList(extendedConfs));
}
for (String extendedConf : extendedConfs) {
fetchDependencies(node, extendedConf, fetchedSet, false);
}
// now we can actually resolve this configuration dependencies
if (!isDependenciesFetched(node.getNode(), conf, fetchedSet) && node.isTransitive()) {
for (VisitNode dep : node.getDependencies(conf)) {
dep.useRealNode(); // the node may have been resolved to another real one while
// resolving other deps
for (String rconf : dep.getRequiredConfigurations(node, conf)) {
fetchDependencies(dep, rconf, fetchedSet, true);
}
if (!dep.isEvicted() && !dep.hasProblem()) {
// if there are still confs to fetch (usually because they have
// been updated when evicting another module), we fetch them now
for (String fconf : dep.getConfsToFetch()) {
// shouldBeFixed=false to because some of those dependencies might
// be private when they were actually extending public conf.
// Should we keep two list of confs to fetch (private&public)?
// I don't think, visibility is already checked, and a change in the
// configuration between version might anyway have worse problems.
fetchDependencies(dep, fconf, fetchedSet, false);
}
}
}
markDependenciesFetched(node.getNode(), conf, fetchedSet);
}
// we have finished with this configuration, if it was the original requested conf
// we can clean it now
if (requestedConfSet) {
node.setRequestedConf(null);
}
}
/**
* Returns true if we've already fetched the dependencies for this node and configuration
*
* @param node
* node to check
* @param conf
* configuration to check
* @return true if we've already fetched this dependency
*/
private boolean isDependenciesFetched(IvyNode node, String conf, Set<String> fetchedSet) {
String key = getDependenciesFetchedKey(node, conf);
return fetchedSet.contains(key);
}
private void markDependenciesFetched(IvyNode node, String conf, Set<String> fetchedSet) {
String key = getDependenciesFetchedKey(node, conf);
fetchedSet.add(key);
}
private String getDependenciesFetchedKey(IvyNode node, String conf) {
ModuleRevisionId moduleRevisionId = node.getResolvedId();
return moduleRevisionId.getOrganisation() + "|" + moduleRevisionId.getName() + "|"
+ moduleRevisionId.getRevision() + "|" + conf;
}
private void resolveConflict(VisitNode node, String conf) {
resolveConflict(node, node.getParent(), conf, Collections.<IvyNode> emptySet());
}
/**
* Resolves conflict for the given node in the given ancestor. This method do conflict
* resolution in ancestor parents recursively, unless not necessary.
*
* @param node
* the node for which conflict resolution should be done
* @param ancestor
* the ancestor in which the conflict resolution should be done
* @param conf String
* @param toevict
* a collection of IvyNode to evict (as computed by conflict resolution in
* descendants of ancestor)
* @return true if conflict resolution has been done, false it can't be done yet
*/
private boolean resolveConflict(VisitNode node, VisitNode ancestor, String conf,
Collection<IvyNode> toevict) {
if (ancestor == null || node == ancestor) {
return true;
}
// check if job is not already done
if (checkConflictSolvedEvicted(node, ancestor)) {
// job is done and node is evicted, nothing to do
return true;
}
boolean debugConflictResolution = settings.debugConflictResolution();
if (checkConflictSolvedSelected(node, ancestor)) {
// job is done and node is selected, nothing to do for this ancestor, but we still have
// to check higher levels, for which conflict resolution might have been impossible
// before
if (resolveConflict(node, ancestor.getParent(), conf, toevict)) {
// now that conflict resolution is ok in ancestors
// we just have to check if the node wasn't previously evicted in root ancestor
EvictionData evictionData = node.getEvictionDataInRoot(node.getRootModuleConf(),
ancestor);
if (evictionData != null) {
// node has been previously evicted in an ancestor: we mark it as evicted
if (debugConflictResolution) {
Message.debug(node + " was previously evicted in root module conf "
+ node.getRootModuleConf());
}
node.markEvicted(evictionData);
if (debugConflictResolution) {
Message.debug("evicting " + node + " by " + evictionData);
}
}
return true;
} else {
return false;
}
}
// compute conflicts
Set<IvyNode> resolvedNodes = ancestor.getNode().getResolvedNodes(node.getModuleId(),
node.getRootModuleConf());
resolvedNodes.addAll(ancestor.getNode().getPendingConflicts(node.getRootModuleConf(),
node.getModuleId()));
Collection<IvyNode> conflicts = computeConflicts(node, ancestor, conf, toevict,
resolvedNodes);
ConflictManager conflictManager = null;
for (VisitNode current : ancestor.getPath()) {
ModuleDescriptor descriptor = current.getNode().getDescriptor();
if (descriptor == null) {
throw new IllegalStateException(
"impossible to get conflict manager when data has not been loaded. IvyNode = "
+ current.getNode());
}
conflictManager = descriptor.getConflictManager(node.getModuleId());
if (conflictManager != null) {
break;
}
}
if (conflictManager == null) {
conflictManager = settings.getConflictManager(node.getModuleId());
}
Collection<IvyNode> resolved = resolveConflicts(node, ancestor, conflicts, conflictManager);
if (resolved == null) {
if (debugConflictResolution) {
Message.debug("impossible to resolve conflicts for " + node + " in " + ancestor
+ " yet");
Message.debug("setting all nodes as pending conflicts for later conflict"
+ " resolution: " + conflicts);
}
ancestor.getNode().setPendingConflicts(node.getModuleId(), node.getRootModuleConf(),
conflicts);
return false;
}
if (debugConflictResolution) {
Message.debug("selected revisions for " + node + " in " + ancestor + ": " + resolved);
}
if (resolved.contains(node.getNode())) {
// node has been selected for the current parent
// handle previously selected nodes that are now evicted by this new node
toevict = resolvedNodes;
toevict.removeAll(resolved);
for (IvyNode te : toevict) {
te.markEvicted(node.getRootModuleConf(), ancestor.getNode(), conflictManager,
resolved);
if (debugConflictResolution) {
Message.debug("evicting " + te + " by "
+ te.getEvictedData(node.getRootModuleConf()));
}
}
// it's very important to update resolved and evicted nodes BEFORE recompute parent call
// to allow it to recompute its resolved collection with correct data
// if necessary
ancestor.getNode().setResolvedNodes(node.getModuleId(), node.getRootModuleConf(),
resolved);
Collection<IvyNode> evicted = new HashSet<>(ancestor.getNode().getEvictedNodes(
node.getModuleId(), node.getRootModuleConf()));
evicted.removeAll(resolved);
evicted.addAll(toevict);
ancestor.getNode().setEvictedNodes(node.getModuleId(), node.getRootModuleConf(),
evicted);
ancestor.getNode().setPendingConflicts(node.getModuleId(), node.getRootModuleConf(),
Collections.<IvyNode> emptySet());
return resolveConflict(node, ancestor.getParent(), conf, toevict);
} else {
// node has been evicted for the current parent
if (resolved.isEmpty()) {
if (debugConflictResolution) {
Message.verbose("conflict manager '" + conflictManager
+ "' evicted all revisions among " + conflicts);
}
}
// it's time to update parent resolved and evicted with what was found
Collection<IvyNode> evicted = new HashSet<>(ancestor.getNode().getEvictedNodes(
node.getModuleId(), node.getRootModuleConf()));
toevict.removeAll(resolved);
evicted.removeAll(resolved);
evicted.addAll(toevict);
evicted.add(node.getNode());
ancestor.getNode().setEvictedNodes(node.getModuleId(), node.getRootModuleConf(),
evicted);
ancestor.getNode().setPendingConflicts(node.getModuleId(), node.getRootModuleConf(),
Collections.<IvyNode> emptySet());
node.markEvicted(ancestor, conflictManager, resolved);
if (debugConflictResolution) {
Message.debug("evicting " + node + " by " + node.getEvictedData());
}
// if resolved changed we have to go up in the graph
Collection<IvyNode> prevResolved = ancestor.getNode().getResolvedNodes(
node.getModuleId(), node.getRootModuleConf());
boolean solved = true;
if (!prevResolved.equals(resolved)) {
ancestor.getNode().setResolvedNodes(node.getModuleId(), node.getRootModuleConf(),
resolved);
for (IvyNode sel : resolved) {
if (!prevResolved.contains(sel)) {
solved &= resolveConflict(node.gotoNode(sel), ancestor.getParent(), conf,
toevict);
}
}
}
return solved;
}
}
private Collection<IvyNode> resolveConflicts(VisitNode node, VisitNode ancestor,
Collection<IvyNode> conflicts, ConflictManager conflictManager) {
if (node.getParent() != ancestor
// we are not handling the direct parent
&& conflictManager == settings.getConflictManager(node.getModuleId())
// the conflict manager is the default one
&& node.getParent().getNode()
.getResolvedNodes(node.getModuleId(), node.getRootModuleConf())
.equals(conflicts)
// there is no new conflict in this ancestor
) {
// IVY-465 case
if (settings.debugConflictResolution()) {
Message.debug("no new conflicting revisions for " + node + " in " + ancestor + ": "
+ conflicts);
}
return conflicts;
} else {
if (settings.debugConflictResolution()) {
Message.debug("found conflicting revisions for " + node + " in " + ancestor + ": "
+ conflicts);
}
return conflictManager.resolveConflicts(ancestor.getNode(), conflicts);
}
}
/**
* Compute possible conflicts for a node, in the context of an ancestor (a node which has a
* dependency - direct or indirect - on the node for which conflicts should be computed.
*
* @param node
* the node for which conflicts should be computed
* @param ancestor
* the ancestor in which conflicts should be computed
* @param conf
* the configuration of the node in which conflicts should be computed
* @param toevict
* a collection of nodes which have been evicted during conflict resolution at lower
* level. It may be empty if no conflict resolution has occurred for this node yet,
* or if no node has been evicted.
* @param selectedNodes
* a collection of nodes selected during previous conflict resolution for the given
* node and ancestor. This collection is updated by this call, removing nodes which
* should be evicted.
* @return a collection of IvyNode which may be in conflict with the given node in the given
* ancestor. This collection always contain at least the given node.
*/
private Collection<IvyNode> computeConflicts(VisitNode node, VisitNode ancestor, String conf,
Collection<IvyNode> toevict, Collection<IvyNode> selectedNodes) {
Collection<IvyNode> conflicts = new LinkedHashSet<>();
conflicts.add(node.getNode());
/*
* We first try to remove all evicted nodes from the collection of selected nodes to update
* this collection. If the collection changes, it means that it contained evicted nodes, and
* thus is not up to date.
*/
boolean evictedInSelected = selectedNodes.removeAll(toevict);
/*
* Another case where we need to deeply compute selected nodes is when selectedNodes is
* empty (not computed yet) and we aren't in the context of the direct parent of the node.
*/
if (evictedInSelected
|| (selectedNodes.isEmpty() && !node.getParent().getNode()
.equals(ancestor.getNode()))) {
IvyContext context = IvyContext.getContext();
ResolveData data = context.getResolveData();
VisitNode oldVisitNode = data.getCurrentVisitNode();
data.setCurrentVisitNode(ancestor);
try {
// In this case we need to compute selected nodes again.
Collection<IvyNode> deps = ancestor.getNode().getDependencies(
node.getRootModuleConf(),
ancestor.getNode().getConfigurations(node.getRootModuleConf()),
ancestor.getRequestedConf());
for (IvyNode dep : deps) {
if (dep.getModuleId().equals(node.getModuleId())) {
conflicts.add(dep);
}
conflicts.addAll(dep.getResolvedNodes(node.getModuleId(),
node.getRootModuleConf()));
}
} finally {
data.setCurrentVisitNode(oldVisitNode);
}
} else if (selectedNodes.isEmpty()) {
/*
* No selected nodes at all yet, and we are in the context of the direct parent
* (otherwise previous block would have been reached). We can compute conflicts based on
* the parent direct dependencies in current root module conf.
*/
VisitNode parent = node.getParent();
Collection<IvyNode> parentDepIvyNodes = parent.getNode().getDependencies(
node.getRootModuleConf(),
parent.getNode().getConfigurations(node.getRootModuleConf()),
parent.getRequestedConf());
for (IvyNode parentDep : parentDepIvyNodes) {
if (parentDep.getModuleId().equals(node.getModuleId())) {
conflicts.add(parentDep);
}
}
} else {
conflicts.addAll(selectedNodes);
}
return conflicts;
}
private boolean checkConflictSolvedSelected(VisitNode node, VisitNode ancestor) {
if (ancestor.getResolvedRevisions(node.getModuleId()).contains(node.getResolvedId())) {
// resolve conflict has already be done with node with the same id
if (settings.debugConflictResolution()) {
Message.debug("conflict resolution already done for " + node + " in " + ancestor);
}
return true;
}
return false;
}
private boolean checkConflictSolvedEvicted(VisitNode node, VisitNode ancestor) {
if (ancestor.getEvictedRevisions(node.getModuleId()).contains(node.getResolvedId())) {
// resolve conflict has already be done with node with the same id
if (settings.debugConflictResolution()) {
Message.debug("conflict resolution already done for " + node + " in " + ancestor);
}
return true;
}
return false;
}
public ResolvedModuleRevision findModule(ModuleRevisionId id, ResolveOptions options) {
DependencyResolver r = settings.getResolver(id);
if (r == null) {
throw new IllegalStateException("no resolver found for " + id.getModuleId());
}
DefaultModuleDescriptor md = DefaultModuleDescriptor.newCallerInstance(id,
new String[] {"*"}, false, false);
if (options.getResolveId() == null) {
options.setResolveId(ResolveOptions.getDefaultResolveId(md));
}
try {
return r.getDependency(new DefaultDependencyDescriptor(id, true), new ResolveData(this,
options, new ConfigurationResolveReport(this, md, "default", null, options)));
} catch (ParseException e) {
throw new RuntimeException("problem while parsing repository module descriptor for "
+ id + ": " + e, e);
}
}
/**
* Mediates the given dependency descriptor according to given options.
* <p>
* The mediated dependency descriptor must return the actually requested module revision id when
* the method {@link DependencyDescriptor#getDependencyRevisionId()} is called.
* </p>
*
* @param dd
* the dependency descriptor for which the requested module revision id should be
* returned
* @param options
* the resolve options to use
* @return the mediated {@link DependencyDescriptor}.
*/
public DependencyDescriptor mediate(DependencyDescriptor dd, ResolveOptions options) {
if (dd == null) {
return null;
}
String resolveMode = options.getResolveMode() == null ? settings.getResolveMode(dd
.getDependencyId()) : options.getResolveMode();
if (ResolveOptions.RESOLVEMODE_DYNAMIC.equals(resolveMode)
&& !dd.getDynamicConstraintDependencyRevisionId().equals(
dd.getDependencyRevisionId())) {
// the dynamicRevId can contain a null branch, so make sure this
// has been replaced by the default branch (if any!)
return dd.clone(ModuleRevisionId.newInstance(dd
.getDynamicConstraintDependencyRevisionId(), dd
.getDynamicConstraintDependencyRevisionId().getRevision()));
} else {
return dd;
}
}
public EventManager getEventManager() {
return eventManager;
}
public ResolveEngineSettings getSettings() {
return settings;
}
public SortEngine getSortEngine() {
return sortEngine;
}
private void checkInterrupted() {
IvyContext.getContext().getIvy().checkInterrupted();
}
}