/* * Copyright (C) 2015 The Android Open Source Project * * 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.android.build.gradle.internal; import static com.android.SdkConstants.DOT_JAR; import static com.android.SdkConstants.EXT_ANDROID_PACKAGE; import static com.android.SdkConstants.EXT_JAR; import static com.android.builder.core.BuilderConstants.EXT_LIB_ARCHIVE; import static com.android.builder.core.ErrorReporter.EvaluationMode.STANDARD; import static com.android.builder.model.AndroidProject.FD_INTERMEDIATES; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.build.gradle.internal.dependency.JarInfo; import com.android.build.gradle.internal.dependency.LibInfo; import com.android.build.gradle.internal.dependency.LibraryDependencyImpl; import com.android.build.gradle.internal.dependency.ManifestDependencyImpl; import com.android.build.gradle.internal.dependency.VariantDependencies; import com.android.build.gradle.internal.model.MavenCoordinatesImpl; import com.android.build.gradle.internal.tasks.PrepareDependenciesTask; import com.android.build.gradle.internal.tasks.PrepareLibraryTask; import com.android.build.gradle.internal.variant.BaseVariantData; import com.android.build.gradle.internal.variant.BaseVariantOutputData; import com.android.builder.dependency.DependencyContainer; import com.android.builder.dependency.JarDependency; import com.android.builder.dependency.LibraryDependency; import com.android.builder.model.MavenCoordinates; import com.android.builder.model.SyncIssue; import com.android.utils.ILogger; import com.google.common.base.Joiner; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.gradle.api.CircularReferenceException; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownProjectException; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.SelfResolvingDependency; import org.gradle.api.artifacts.component.ComponentIdentifier; import org.gradle.api.artifacts.component.ComponentSelector; import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.gradle.api.artifacts.result.DependencyResult; import org.gradle.api.artifacts.result.ResolvedComponentResult; import org.gradle.api.artifacts.result.ResolvedDependencyResult; import org.gradle.api.artifacts.result.UnresolvedDependencyResult; import org.gradle.api.logging.Logging; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.specs.Specs; import org.gradle.util.GUtil; import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** * A manager to resolve configuration dependencies. */ public class DependencyManager { protected static final boolean DEBUG_DEPENDENCY = false; private Project project; private ExtraModelInfo extraModelInfo; private ILogger logger; final Map<LibraryDependencyImpl, PrepareLibraryTask> prepareTaskMap = Maps.newHashMap(); public DependencyManager(Project project, ExtraModelInfo extraModelInfo) { this.project = project; this.extraModelInfo = extraModelInfo; logger = new LoggerWrapper(Logging.getLogger(DependencyManager.class)); } /** * Returns the list of packaged local jars. */ public static List<File> getPackagedLocalJarFileList(DependencyContainer dependencyContainer) { List<JarDependency> jarDependencyList = dependencyContainer.getLocalDependencies(); Set<File> files = Sets.newHashSetWithExpectedSize(jarDependencyList.size()); for (JarDependency jarDependency : jarDependencyList) { if (jarDependency.isPackaged()) { files.add(jarDependency.getJarFile()); } } return Lists.newArrayList(files); } public void addDependencyToPrepareTask( @NonNull BaseVariantData<? extends BaseVariantOutputData> variantData, @NonNull PrepareDependenciesTask prepareDependenciesTask, @NonNull LibraryDependencyImpl lib) { PrepareLibraryTask prepareLibTask = prepareTaskMap.get(lib.getNonTransitiveRepresentation()); if (prepareLibTask != null) { prepareDependenciesTask.dependsOn(prepareLibTask); prepareLibTask.dependsOn(variantData.preBuildTask); } for (LibraryDependency childLib : lib.getDependencies()) { addDependencyToPrepareTask( variantData, prepareDependenciesTask, (LibraryDependencyImpl) childLib); } } public void resolveDependencies( @NonNull VariantDependencies variantDeps, @Nullable VariantDependencies testedVariantDeps, @Nullable String testedProjectPath) { Multimap<LibraryDependency, VariantDependencies> reverseMap = ArrayListMultimap.create(); resolveDependencyForConfig(variantDeps, testedVariantDeps, testedProjectPath, reverseMap); processLibraries(variantDeps.getLibraries(), reverseMap); } private void processLibraries( @NonNull Collection<LibraryDependencyImpl> libraries, @NonNull Multimap<LibraryDependency, VariantDependencies> reverseMap) { for (LibraryDependencyImpl lib : libraries) { setupPrepareLibraryTask(lib, reverseMap); //noinspection unchecked processLibraries( (Collection<LibraryDependencyImpl>) (List<?>) lib.getDependencies(), reverseMap); } } private void setupPrepareLibraryTask( @NonNull LibraryDependencyImpl libDependency, @NonNull Multimap<LibraryDependency, VariantDependencies> reverseMap) { Task task = maybeCreatePrepareLibraryTask(libDependency, project); // Use the reverse map to find all the configurations that included this android // library so that we can make sure they are built. // TODO fix, this is not optimum as we bring in more dependencies than we should. Collection<VariantDependencies> configDepList = reverseMap.get(libDependency); if (configDepList != null && !configDepList.isEmpty()) { for (VariantDependencies configDependencies: configDepList) { task.dependsOn(configDependencies.getCompileConfiguration().getBuildDependencies()); } } // check if this library is created by a parent (this is based on the // output file. // TODO Fix this as it's fragile /* This is a somewhat better way but it doesn't work in some project with weird setups... Project parentProject = DependenciesImpl.getProject(library.getBundle(), projects) if (parentProject != null) { String configName = library.getProjectVariant() if (configName == null) { configName = "default" } prepareLibraryTask.dependsOn parentProject.getPath() + ":assemble${configName.capitalize()}" } */ } /** * Handles the library and returns a task to "prepare" the library (ie unarchive it). The task * will be reused for all projects using the same library. * * @param library the library. * @param project the project * @return the prepare task. */ private PrepareLibraryTask maybeCreatePrepareLibraryTask( @NonNull LibraryDependencyImpl library, @NonNull Project project) { // create proper key for the map. library here contains all the dependencies which // are not relevant for the task (since the task only extract the aar which does not // include the dependencies. // However there is a possible case of a rewritten dependencies (with resolution strategy) // where the aar here could have different dependencies, in which case we would still // need the same task. // So we extract a LibraryBundle (no dependencies) from the LibraryDependencyImpl to // make the map key that doesn't take into account the dependencies. LibraryDependencyImpl key = library.getNonTransitiveRepresentation(); PrepareLibraryTask prepareLibraryTask = prepareTaskMap.get(key); if (prepareLibraryTask == null) { String bundleName = GUtil.toCamelCase(library.getName().replaceAll("\\:", " ")); prepareLibraryTask = project.getTasks().create( "prepare" + bundleName + "Library", PrepareLibraryTask.class); prepareLibraryTask.setDescription("Prepare " + library.getName()); prepareLibraryTask.setBundle(library.getBundle()); prepareLibraryTask.setExplodedDir(library.getBundleFolder()); prepareLibraryTask.setVariantName(""); prepareTaskMap.put(key, prepareLibraryTask); } return prepareLibraryTask; } private void resolveDependencyForConfig( @NonNull VariantDependencies variantDeps, @Nullable VariantDependencies testedVariantDeps, @Nullable String testedProjectPath, @NonNull Multimap<LibraryDependency, VariantDependencies> reverseMap) { Configuration compileClasspath = variantDeps.getCompileConfiguration(); Configuration packageClasspath = variantDeps.getPackageConfiguration(); // TODO - shouldn't need to do this - fix this in Gradle ensureConfigured(compileClasspath); ensureConfigured(packageClasspath); if (DEBUG_DEPENDENCY) { System.out.println(">>>>>>>>>>"); System.out.println( project.getName() + ":" + compileClasspath.getName() + "/" + packageClasspath.getName()); } Set<String> currentUnresolvedDependencies = Sets.newHashSet(); // TODO - defer downloading until required -- This is hard to do as we need the info to build the variant config. Map<ModuleVersionIdentifier, List<ResolvedArtifact>> artifacts = Maps.newHashMap(); collectArtifacts(compileClasspath, artifacts); collectArtifacts(packageClasspath, artifacts); // --- Handle the external/module dependencies --- // keep a map of modules already processed so that we don't go through sections of the // graph that have been seen elsewhere. Map<ModuleVersionIdentifier, List<LibInfo>> foundLibraries = Maps.newHashMap(); Map<ModuleVersionIdentifier, List<JarInfo>> foundJars = Maps.newHashMap(); // first get the compile dependencies. Note that in both case the libraries and the // jars are a graph. The list only contains the first level of dependencies, and // they themselves contain transitive dependencies (libraries can contain both, jars only // contains jars) List<LibInfo> compiledAndroidLibraries = Lists.newArrayList(); List<JarInfo> compiledJars = Lists.newArrayList(); Set<? extends DependencyResult> dependencyResultSet = compileClasspath.getIncoming() .getResolutionResult().getRoot().getDependencies(); for (DependencyResult dependencyResult : dependencyResultSet) { if (dependencyResult instanceof ResolvedDependencyResult) { addDependency( ((ResolvedDependencyResult) dependencyResult).getSelected(), variantDeps, compiledAndroidLibraries, compiledJars, foundLibraries, foundJars, artifacts, reverseMap, currentUnresolvedDependencies, testedProjectPath, Collections.<String>emptyList(), 0); } else if (dependencyResult instanceof UnresolvedDependencyResult) { ComponentSelector attempted = ((UnresolvedDependencyResult) dependencyResult).getAttempted(); if (attempted != null) { currentUnresolvedDependencies.add(attempted.toString()); } } } // then the packaged ones. List<LibInfo> packagedAndroidLibraries = Lists.newArrayList(); List<JarInfo> packagedJars = Lists.newArrayList(); dependencyResultSet = packageClasspath.getIncoming() .getResolutionResult().getRoot().getDependencies(); for (DependencyResult dependencyResult : dependencyResultSet) { if (dependencyResult instanceof ResolvedDependencyResult) { addDependency( ((ResolvedDependencyResult) dependencyResult).getSelected(), variantDeps, packagedAndroidLibraries, packagedJars, foundLibraries, foundJars, artifacts, reverseMap, currentUnresolvedDependencies, testedProjectPath, Collections.<String>emptyList(), 0); } else if (dependencyResult instanceof UnresolvedDependencyResult) { ComponentSelector attempted = ((UnresolvedDependencyResult) dependencyResult) .getAttempted(); if (attempted != null) { currentUnresolvedDependencies.add(attempted.toString()); } } } // now look through both results. // 1. Handle the compile and package list of Libraries. // For Libraries: // Only library projects can support provided aar. // However, package(publish)-only are still not supported (they don't make sense). // For now, provided only dependencies will be kept normally in the compile-graph. // However we'll want to not include them in the resource merging. // For Applications: // All Android libraries must be in both lists. // --- // Since we reuse the same instance of LibInfo for identical modules // we can simply run through each list and look for libs that are in only one. // While the list of library is actually a graph, it's fine to look only at the // top level ones since the transitive ones are in the same scope as the direct libraries. List<LibInfo> copyOfPackagedLibs = Lists.newArrayList(packagedAndroidLibraries); boolean isLibrary = extraModelInfo.isLibrary(); for (LibInfo lib : compiledAndroidLibraries) { if (!copyOfPackagedLibs.contains(lib)) { if (isLibrary || lib.isOptional()) { lib.setIsOptional(true); } else { //noinspection ConstantConditions variantDeps.getChecker().addSyncIssue(extraModelInfo.handleSyncError( lib.getResolvedCoordinates().toString(), SyncIssue.TYPE_NON_JAR_PROVIDED_DEP, String.format( "Project %s: provided dependencies can only be jars. %s is an Android Library.", project.getName(), lib.getResolvedCoordinates()))); } } else { copyOfPackagedLibs.remove(lib); } } // at this stage copyOfPackagedLibs should be empty, if not, error. for (LibInfo lib : copyOfPackagedLibs) { //noinspection ConstantConditions variantDeps.getChecker().addSyncIssue(extraModelInfo.handleSyncError( lib.getResolvedCoordinates().toString(), SyncIssue.TYPE_NON_JAR_PACKAGE_DEP, String.format( "Project %s: apk dependencies can only be jars. %s is an Android Library.", project.getName(), lib.getResolvedCoordinates()))); } // 2. merge jar dependencies with a single list where items have packaged/compiled properties. // since we reuse the same instance of a JarInfo for identical modules, we can use an // Identity set (ie both compiledJars and packagedJars will contain the same instance // if it's both compiled and packaged) Set<JarInfo> jarInfoSet = Sets.newIdentityHashSet(); // go through the graphs of dependencies (jars and libs) and gather all the transitive // jar dependencies. // At the same this we set the compiled/packaged properties. gatherJarDependencies(jarInfoSet, compiledJars, true /*compiled*/, false /*packaged*/); gatherJarDependencies(jarInfoSet, packagedJars, false /*compiled*/, true /*packaged*/); // at this step, we know that libraries have been checked and libraries can only // be in both compiled and packaged scope. gatherJarDependenciesFromLibraries(jarInfoSet, compiledAndroidLibraries); // the final list of JarDependency, created from the list of JarInfo. List<JarDependency> jars = Lists.newArrayListWithCapacity(jarInfoSet.size()); // if this is a test dependencies (ie tested dependencies is non null), override // packaged attributes for jars that are already in the tested dependencies in order to // not package them twice (since the VM loads the classes of both APKs in the same // classpath and refuses to load the same class twice) if (testedVariantDeps != null) { List<JarDependency> jarDependencies = testedVariantDeps.getJarDependencies(); // gather the tested dependencies Map<String, String> testedDeps = Maps.newHashMapWithExpectedSize(jarDependencies.size()); for (JarDependency jar : jarDependencies) { if (jar.isPackaged()) { MavenCoordinates coordinates = jar.getResolvedCoordinates(); //noinspection ConstantConditions testedDeps.put( computeVersionLessCoordinateKey(coordinates), coordinates.getVersion()); } } // now go through all the test dependencies and check we don't have the same thing. // Skip the ones that are already in the tested variant, and convert the rest // to the final immutable instance for (JarInfo jar : jarInfoSet) { if (jar.isPackaged()) { MavenCoordinates coordinates = jar.getResolvedCoordinates(); String testedVersion = testedDeps.get( computeVersionLessCoordinateKey(coordinates)); if (testedVersion != null) { // same artifact, skip packaging of the dependency in the test app, // whether the version is a match or not. // if the dependency is present in both tested and test artifact, // verify that they are the same version if (!testedVersion.equals(coordinates.getVersion())) { String artifactInfo = coordinates.getGroupId() + ":" + coordinates.getArtifactId(); variantDeps.getChecker().addSyncIssue(extraModelInfo.handleSyncError( artifactInfo, SyncIssue.TYPE_MISMATCH_DEP, String.format( "Conflict with dependency '%s'. Resolved versions for app (%s) and test app (%s) differ.", artifactInfo, testedVersion, coordinates.getVersion()))); } else { logger.info(String.format( "Removed '%s' from packaging of %s: Already in tested package.", coordinates, variantDeps.getName())); } } else { // new artifact, convert it. jars.add(jar.createJarDependency()); } } } } else { // just convert all of them to JarDependency for (JarInfo jarInfo : jarInfoSet) { jars.add(jarInfo.createJarDependency()); } } // --- Handle the local jar dependencies --- // also need to process local jar files, as they are not processed by the // resolvedConfiguration result. This only includes the local jar files for this project. Set<File> localCompiledJars = Sets.newHashSet(); for (Dependency dependency : compileClasspath.getAllDependencies()) { if (dependency instanceof SelfResolvingDependency && !(dependency instanceof ProjectDependency)) { Set<File> files = ((SelfResolvingDependency) dependency).resolve(); for (File f : files) { if (DEBUG_DEPENDENCY) { System.out.println("LOCAL compile: " + f.getName()); } // only accept local jar, no other types. if (!f.getName().toLowerCase(Locale.getDefault()).endsWith(DOT_JAR)) { variantDeps.getChecker().addSyncIssue(extraModelInfo.handleSyncError( f.getAbsolutePath(), SyncIssue.TYPE_NON_JAR_LOCAL_DEP, String.format( "Project %s: Only Jar-type local dependencies are supported. Cannot handle: %s", project.getName(), f.getAbsolutePath()))); } else { localCompiledJars.add(f); } } } } Set<File> localPackagedJars = Sets.newHashSet(); for (Dependency dependency : packageClasspath.getAllDependencies()) { if (dependency instanceof SelfResolvingDependency && !(dependency instanceof ProjectDependency)) { Set<File> files = ((SelfResolvingDependency) dependency).resolve(); for (File f : files) { if (DEBUG_DEPENDENCY) { System.out.println("LOCAL package: " + f.getName()); } // only accept local jar, no other types. if (!f.getName().toLowerCase(Locale.getDefault()).endsWith(DOT_JAR)) { variantDeps.getChecker().addSyncIssue(extraModelInfo.handleSyncError( f.getAbsolutePath(), SyncIssue.TYPE_NON_JAR_LOCAL_DEP, String.format( "Project %s: Only Jar-type local dependencies are supported. Cannot handle: %s", project.getName(), f.getAbsolutePath()))); } else { localPackagedJars.add(f); } } } } // loop through both the compiled and packaged jar to compute the list // of jars that are: compile-only, package-only, or both. Map<File, JarDependency> localJars = Maps.newHashMap(); for (File file : localCompiledJars) { localJars.put(file, new JarDependency( file, true /*compiled*/, localPackagedJars.contains(file) /*packaged*/, null /*resolvedCoordinates*/, null /*projectPath*/)); } for (File file : localPackagedJars) { if (!localCompiledJars.contains(file)) { localJars.put(file, new JarDependency( file, false /*compiled*/, true /*packaged*/, null /*resolvedCoordinates*/, null /*projectPath*/)); } } if (extraModelInfo.getMode() != STANDARD && compileClasspath.getResolvedConfiguration().hasError()) { for (String dependency : currentUnresolvedDependencies) { extraModelInfo.handleSyncError( dependency, SyncIssue.TYPE_UNRESOLVED_DEPENDENCY, String.format( "Unable to resolve dependency '%s'", dependency)); } } // convert the LibInfo in LibraryDependencyImpl and update the reverseMap // with the converted keys List<LibraryDependencyImpl> libList = convertLibraryInfoIntoDependency( compiledAndroidLibraries, reverseMap); if (DEBUG_DEPENDENCY) { for (LibraryDependency lib : libList) { System.out.println("LIB: " + lib); } for (JarDependency jar : jars) { System.out.println("JAR: " + jar); } for (JarDependency jar : localJars.values()) { System.out.println("LOCAL-JAR: " + jar); } } variantDeps.addLibraries(libList); variantDeps.addJars(jars); variantDeps.addLocalJars(localJars.values()); configureBuild(variantDeps); if (DEBUG_DEPENDENCY) { System.out.println(project.getName() + ":" + compileClasspath.getName() + "/" +packageClasspath.getName()); System.out.println("<<<<<<<<<<"); } } private static List<LibraryDependencyImpl> convertLibraryInfoIntoDependency( @NonNull List<LibInfo> libInfos, @NonNull Multimap<LibraryDependency, VariantDependencies> reverseMap) { List<LibraryDependencyImpl> list = Lists.newArrayListWithCapacity(libInfos.size()); // since the LibInfos is a graph and the previous "foundLibraries" map ensure we reuse // instance where applicable, we'll create a map to keep track of what we have already // converted. Map<LibInfo, LibraryDependencyImpl> convertedMap = Maps.newIdentityHashMap(); for (LibInfo libInfo : libInfos) { list.add(convertLibInfo(libInfo, reverseMap, convertedMap)); } return list; } private static LibraryDependencyImpl convertLibInfo( @NonNull LibInfo libInfo, @NonNull Multimap<LibraryDependency, VariantDependencies> reverseMap, @NonNull Map<LibInfo, LibraryDependencyImpl> convertedMap) { LibraryDependencyImpl convertedLib = convertedMap.get(libInfo); if (convertedLib == null) { // first, convert the children. @SuppressWarnings("unchecked") List<LibInfo> children = (List<LibInfo>) (List<?>) libInfo.getDependencies(); List<LibraryDependency> convertedChildren = Lists.newArrayListWithCapacity(children.size()); for (LibInfo child : children) { convertedChildren.add(convertLibInfo(child, reverseMap, convertedMap)); } // now convert the libInfo convertedLib = new LibraryDependencyImpl( libInfo.getBundle(), libInfo.getFolder(), convertedChildren, libInfo.getName(), libInfo.getProjectVariant(), libInfo.getProject(), libInfo.getRequestedCoordinates(), libInfo.getResolvedCoordinates(), libInfo.isOptional()); // add it to the map convertedMap.put(libInfo, convertedLib); // and update the reversemap // get the items associated with the libInfo. Put in a fresh list as the returned // collection is backed by the content of the map. Collection<VariantDependencies> values = Lists.newArrayList(reverseMap.get(libInfo)); reverseMap.removeAll(libInfo); reverseMap.putAll(convertedLib, values); } return convertedLib; } private static void gatherJarDependencies( Set<JarInfo> outJarInfos, Collection<JarInfo> inJarInfos, boolean compiled, boolean packaged) { for (JarInfo jarInfo : inJarInfos) { if (!outJarInfos.contains(jarInfo)) { outJarInfos.add(jarInfo); } if (compiled) { jarInfo.setCompiled(true); } if (packaged) { jarInfo.setPackaged(true); } gatherJarDependencies(outJarInfos, jarInfo.getDependencies(), compiled, packaged); } } private static void gatherJarDependenciesFromLibraries( Set<JarInfo> outJarInfos, Collection<LibInfo> inLibraryDependencies) { for (LibInfo libInfo : inLibraryDependencies) { gatherJarDependencies(outJarInfos, libInfo.getJarDependencies(), true, !libInfo.isOptional()); gatherJarDependenciesFromLibraries( outJarInfos, libInfo.getLibInfoDependencies()); } } private void ensureConfigured(Configuration config) { for (Dependency dependency : config.getAllDependencies()) { if (dependency instanceof ProjectDependency) { ProjectDependency projectDependency = (ProjectDependency) dependency; project.evaluationDependsOn(projectDependency.getDependencyProject().getPath()); try { ensureConfigured(projectDependency.getProjectConfiguration()); } catch (Throwable e) { throw new UnknownProjectException(String.format( "Cannot evaluate module %s : %s", projectDependency.getName(), e.getMessage()), e); } } } } private void collectArtifacts( Configuration configuration, Map<ModuleVersionIdentifier, List<ResolvedArtifact>> artifacts) { Set<ResolvedArtifact> allArtifacts; if (extraModelInfo.getMode() != STANDARD) { allArtifacts = configuration.getResolvedConfiguration().getLenientConfiguration().getArtifacts( Specs.satisfyAll()); } else { allArtifacts = configuration.getResolvedConfiguration().getResolvedArtifacts(); } for (ResolvedArtifact artifact : allArtifacts) { ModuleVersionIdentifier id = artifact.getModuleVersion().getId(); List<ResolvedArtifact> moduleArtifacts = artifacts.get(id); if (moduleArtifacts == null) { moduleArtifacts = Lists.newArrayList(); artifacts.put(id, moduleArtifacts); } if (!moduleArtifacts.contains(artifact)) { moduleArtifacts.add(artifact); } } } private static void printIndent(int indent, @NonNull String message) { for (int i = 0 ; i < indent ; i++) { System.out.print("\t"); } System.out.println(message); } private void addDependency( @NonNull ResolvedComponentResult resolvedComponentResult, @NonNull VariantDependencies configDependencies, @NonNull Collection<LibInfo> outLibraries, @NonNull List<JarInfo> outJars, @NonNull Map<ModuleVersionIdentifier, List<LibInfo>> alreadyFoundLibraries, @NonNull Map<ModuleVersionIdentifier, List<JarInfo>> alreadyFoundJars, @NonNull Map<ModuleVersionIdentifier, List<ResolvedArtifact>> artifacts, @NonNull Multimap<LibraryDependency, VariantDependencies> reverseMap, @NonNull Set<String> currentUnresolvedDependencies, @Nullable String testedProjectPath, @NonNull List<String> projectChain, int indent) { ModuleVersionIdentifier moduleVersion = resolvedComponentResult.getModuleVersion(); if (configDependencies.getChecker().excluded(moduleVersion)) { return; } if (moduleVersion.getName().equals("support-annotations") && moduleVersion.getGroup().equals("com.android.support")) { configDependencies.setAnnotationsPresent(true); } List<LibInfo> libsForThisModule = alreadyFoundLibraries.get(moduleVersion); List<JarInfo> jarsForThisModule = alreadyFoundJars.get(moduleVersion); if (libsForThisModule != null) { if (DEBUG_DEPENDENCY) { printIndent(indent, "FOUND LIB: " + moduleVersion.getName()); } outLibraries.addAll(libsForThisModule); for (LibInfo lib : libsForThisModule) { reverseMap.put(lib, configDependencies); } } else if (jarsForThisModule != null) { if (DEBUG_DEPENDENCY) { printIndent(indent, "FOUND JAR: " + moduleVersion.getName()); } outJars.addAll(jarsForThisModule); } else { if (DEBUG_DEPENDENCY) { printIndent(indent, "NOT FOUND: " + moduleVersion.getName()); } // new module! Might be a jar or a library // get the nested components first. List<LibInfo> nestedLibraries = Lists.newArrayList(); List<JarInfo> nestedJars = Lists.newArrayList(); Set<? extends DependencyResult> dependencies = resolvedComponentResult.getDependencies(); for (DependencyResult dependencyResult : dependencies) { if (dependencyResult instanceof ResolvedDependencyResult) { ResolvedComponentResult selected = ((ResolvedDependencyResult) dependencyResult).getSelected(); List<String> newProjectChain = projectChain; ComponentIdentifier identifier = selected.getId(); if (identifier instanceof ProjectComponentIdentifier) { String projectPath = ((ProjectComponentIdentifier) identifier).getProjectPath(); int index = projectChain.indexOf(projectPath); if (index != -1) { projectChain.add(projectPath); String path = Joiner .on(" -> ") .join(projectChain.subList(index, projectChain.size())); throw new CircularReferenceException( "Circular reference between projects: " + path); } newProjectChain = Lists.newArrayList(); newProjectChain.addAll(projectChain); newProjectChain.add(projectPath); } addDependency( selected, configDependencies, nestedLibraries, nestedJars, alreadyFoundLibraries, alreadyFoundJars, artifacts, reverseMap, currentUnresolvedDependencies, testedProjectPath, newProjectChain, indent + 1); } else if (dependencyResult instanceof UnresolvedDependencyResult) { ComponentSelector attempted = ((UnresolvedDependencyResult) dependencyResult).getAttempted(); if (attempted != null) { currentUnresolvedDependencies.add(attempted.toString()); } } } if (DEBUG_DEPENDENCY) { printIndent(indent, "BACK2: " + moduleVersion.getName()); printIndent(indent, "NESTED LIBS: " + nestedLibraries.size()); printIndent(indent, "NESTED JARS: " + nestedJars.size()); } // now loop on all the artifact for this modules. List<ResolvedArtifact> moduleArtifacts = artifacts.get(moduleVersion); ComponentIdentifier id = resolvedComponentResult.getId(); String gradlePath = (id instanceof ProjectComponentIdentifier) ? ((ProjectComponentIdentifier) id).getProjectPath() : null; if (moduleArtifacts != null) { for (ResolvedArtifact artifact : moduleArtifacts) { if (EXT_LIB_ARCHIVE.equals(artifact.getExtension())) { if (DEBUG_DEPENDENCY) { printIndent(indent, "TYPE: AAR"); } if (libsForThisModule == null) { libsForThisModule = Lists.newArrayList(); alreadyFoundLibraries.put(moduleVersion, libsForThisModule); } String path = computeArtifactPath(moduleVersion, artifact); String name = computeArtifactName(moduleVersion, artifact); if (DEBUG_DEPENDENCY) { printIndent(indent, "NAME: " + name); printIndent(indent, "PATH: " + path); } //def explodedDir = project.file("$project.rootProject.buildDir/${FD_INTERMEDIATES}/exploded-aar/$path") File explodedDir = project.file(project.getBuildDir() + "/" + FD_INTERMEDIATES + "/exploded-aar/" + path); @SuppressWarnings("unchecked") LibInfo libInfo = new LibInfo( artifact.getFile(), explodedDir, (List<LibraryDependency>) (List<?>) nestedLibraries, nestedJars, name, artifact.getClassifier(), gradlePath, null /*requestedCoordinates*/, new MavenCoordinatesImpl(artifact)); libsForThisModule.add(libInfo); outLibraries.add(libInfo); reverseMap.put(libInfo, configDependencies); } else if (EXT_JAR.equals(artifact.getExtension())) { if (DEBUG_DEPENDENCY) { printIndent(indent, "TYPE: JAR"); } // check this jar does not have a dependency on an library, as this would not work. if (!nestedLibraries.isEmpty()) { if (testedProjectPath != null && testedProjectPath.equals(gradlePath)) { // TODO: make sure this is a direct dependency and not a transitive one. // add nested libs as optional somehow... for (LibInfo lib : nestedLibraries) { lib.setIsOptional(true); } outLibraries.addAll(nestedLibraries); } else { configDependencies.getChecker() .addSyncIssue(extraModelInfo.handleSyncError( new MavenCoordinatesImpl(artifact).toString(), SyncIssue.TYPE_JAR_DEPEND_ON_AAR, String.format( "Module '%s' depends on one or more Android Libraries but is a jar", moduleVersion))); } } if (jarsForThisModule == null) { jarsForThisModule = Lists.newArrayList(); alreadyFoundJars.put(moduleVersion, jarsForThisModule); } JarInfo jarInfo = new JarInfo( artifact.getFile(), new MavenCoordinatesImpl(artifact), gradlePath, nestedJars); if (DEBUG_DEPENDENCY) { printIndent(indent, "JAR-INFO: " + jarInfo.toString()); } jarsForThisModule.add(jarInfo); outJars.add(jarInfo); } else if (EXT_ANDROID_PACKAGE.equals(artifact.getExtension())) { String name = computeArtifactName(moduleVersion, artifact); configDependencies.getChecker().addSyncIssue(extraModelInfo.handleSyncError( name, SyncIssue.TYPE_DEPENDENCY_IS_APK, String.format( "Dependency %s on project %s resolves to an APK archive " + "which is not supported as a compilation dependency. File: %s", name, project.getName(), artifact.getFile()))); } else if ("apklib".equals(artifact.getExtension())) { String name = computeArtifactName(moduleVersion, artifact); configDependencies.getChecker().addSyncIssue(extraModelInfo.handleSyncError( name, SyncIssue.TYPE_DEPENDENCY_IS_APKLIB, String.format( "Packaging for dependency %s is 'apklib' and is not supported. " + "Only 'aar' libraries are supported.", name))); } else { String name = computeArtifactName(moduleVersion, artifact); logger.warning(String.format( "Unrecognized dependency: '%s' (type: '%s', extension: '%s')", name, artifact.getType(), artifact.getExtension())); } } } if (DEBUG_DEPENDENCY) { printIndent(indent, "DONE: " + moduleVersion.getName()); } } } @NonNull private String computeArtifactPath( @NonNull ModuleVersionIdentifier moduleVersion, @NonNull ResolvedArtifact artifact) { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(normalize(logger, moduleVersion, moduleVersion.getGroup())) .append('/') .append(normalize(logger, moduleVersion, moduleVersion.getName())) .append('/') .append(normalize(logger, moduleVersion, moduleVersion.getVersion())); if (artifact.getClassifier() != null && !artifact.getClassifier().isEmpty()) { pathBuilder.append('/').append(normalize(logger, moduleVersion, artifact.getClassifier())); } return pathBuilder.toString(); } @NonNull private static String computeArtifactName( @NonNull ModuleVersionIdentifier moduleVersion, @NonNull ResolvedArtifact artifact) { StringBuilder nameBuilder = new StringBuilder(); nameBuilder.append(moduleVersion.getGroup()) .append(':') .append(moduleVersion.getName()) .append(':') .append(moduleVersion.getVersion()); if (artifact.getClassifier() != null && !artifact.getClassifier().isEmpty()) { nameBuilder.append(':').append(artifact.getClassifier()); } return nameBuilder.toString(); } /** * Normalize a path to remove all illegal characters for all supported operating systems. * {@see http://en.wikipedia.org/wiki/Filename#Comparison%5Fof%5Ffile%5Fname%5Flimitations} * * @param id the module coordinates that generated this path * @param path the proposed path name * @return the normalized path name */ static String normalize(ILogger logger, ModuleVersionIdentifier id, String path) { if (path == null || path.isEmpty()) { logger.info(String.format( "When unzipping library '%s:%s:%s, either group, name or version is empty", id.getGroup(), id.getName(), id.getVersion())); return path; } // list of illegal characters String normalizedPath = path.replaceAll("[%<>:\"/?*\\\\]", "@"); if (normalizedPath == null || normalizedPath.isEmpty()) { // if the path normalization failed, return the original path. logger.info(String.format( "When unzipping library '%s:%s:%s, the normalized '%s' is empty", id.getGroup(), id.getName(), id.getVersion(), path)); return path; } try { int pathPointer = normalizedPath.length() - 1; // do not end your path with either a dot or a space. String suffix = ""; while (pathPointer >= 0 && (normalizedPath.charAt(pathPointer) == '.' || normalizedPath.charAt(pathPointer) == ' ')) { pathPointer--; suffix += "@"; } if (pathPointer < 0) { throw new RuntimeException(String.format( "When unzipping library '%s:%s:%s, " + "the path '%s' cannot be transformed into a valid directory name", id.getGroup(), id.getName(), id.getVersion(), path)); } return normalizedPath.substring(0, pathPointer + 1) + suffix; } catch (Exception e) { logger.error(e, String.format( "When unzipping library '%s:%s:%s', " + "Path normalization failed for input %s", id.getGroup(), id.getName(), id.getVersion(), path)); return path; } } private void configureBuild(VariantDependencies configurationDependencies) { addDependsOnTaskInOtherProjects( project.getTasks().getByName(JavaBasePlugin.BUILD_NEEDED_TASK_NAME), true, JavaBasePlugin.BUILD_NEEDED_TASK_NAME, "compile"); addDependsOnTaskInOtherProjects( project.getTasks().getByName(JavaBasePlugin.BUILD_DEPENDENTS_TASK_NAME), false, JavaBasePlugin.BUILD_DEPENDENTS_TASK_NAME, "compile"); } @NonNull public static List<ManifestDependencyImpl> getManifestDependencies( List<LibraryDependency> libraries) { List<ManifestDependencyImpl> list = Lists.newArrayListWithCapacity(libraries.size()); for (LibraryDependency lib : libraries) { // get the dependencies List<ManifestDependencyImpl> children = getManifestDependencies(lib.getDependencies()); list.add(new ManifestDependencyImpl(lib.getName(), lib.getManifest(), children)); } return list; } /** * Adds a dependency on tasks with the specified name in other projects. The other projects * are determined from project lib dependencies using the specified configuration name. * These may be projects this project depends on or projects that depend on this project * based on the useDependOn argument. * * @param task Task to add dependencies to * @param useDependedOn if true, add tasks from projects this project depends on, otherwise * use projects that depend on this one. * @param otherProjectTaskName name of task in other projects * @param configurationName name of configuration to use to find the other projects */ private static void addDependsOnTaskInOtherProjects(final Task task, boolean useDependedOn, String otherProjectTaskName, String configurationName) { Project project = task.getProject(); final Configuration configuration = project.getConfigurations().getByName( configurationName); task.dependsOn(configuration.getTaskDependencyFromProjectDependency( useDependedOn, otherProjectTaskName)); } /** * Compute a version-less key representing the given coordinates. * @param coordinates the coordinate * @return the key. */ @NonNull private static String computeVersionLessCoordinateKey(@NonNull MavenCoordinates coordinates) { StringBuilder sb = new StringBuilder(coordinates.getGroupId()); sb.append(':').append(coordinates.getArtifactId()); if (coordinates.getClassifier() != null) { sb.append(':').append(coordinates.getClassifier()); } return sb.toString(); } }