/* * Copyright 2023 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. */ import org.gradle.api.GradleException import org.gradle.api.model.ObjectFactory import org.gradle.api.initialization.Settings import javax.annotation.Nullable import java.util.regex.Matcher import java.util.regex.Pattern /** * Tracks Gradle projects and their dependencies and provides functionality to get a subset of * projects with their dependencies. * * This class is shared between the main repository and the playground plugin (github). */ class ProjectDependencyGraph { private Settings settings; private boolean isPlayground; private boolean constraintsEnabled /** * A map of project path to a set of project paths referenced directly by this project. */ private Map> projectReferences = new HashMap>() /** * A map of project path to a set of project paths that directly depend on the key project. */ private Map> projectConsumers = new HashMap>() private Set publishedLibraryProjects = new HashSet<>() /** * A map of all project paths to their project directory. */ private Map allProjects = new HashMap() ProjectDependencyGraph(Settings settings, boolean isPlayground, boolean constraintsEnabled) { this.settings = settings this.isPlayground = isPlayground this.constraintsEnabled = constraintsEnabled } Set allProjectPaths() { return allProjects.keySet() } Map> allProjectConsumers() { return projectConsumers } /** * Adds the given pair to the list of known projects * * @param projectPath Gradle project path * @param projectDir Gradle project directory */ void addToAllProjects(String projectPath, File projectDir) { Set cached = projectReferences.get(projectPath) if (cached != null) { return } allProjects[projectPath] = projectDir Set parsedDependencies = extractReferencesFromBuildFile(projectPath, projectDir) projectReferences[projectPath] = parsedDependencies parsedDependencies.forEach { dependency -> def reverseLookupSet = projectConsumers[dependency] ?: new HashSet() reverseLookupSet.add(projectPath) projectConsumers[dependency] = reverseLookupSet } } /** * Returns a set of project path that includes the given `projectPath` as well as any other project * that directly or indirectly depends on `projectPath` */ Set findAllProjectsDependingOn(String projectPath) { Set result = new HashSet() ArrayDeque toBeTraversed = new ArrayDeque() toBeTraversed.add(projectPath) while (toBeTraversed.size() > 0) { def path = toBeTraversed.removeFirst() if (result.add(path)) { def dependants = projectConsumers[path] if (dependants != null) { toBeTraversed.addAll(dependants) } } } return result } /** * Returns a list of (projectPath -> projectDir) tuples that include the given filteredProjects * and all of their dependencies (including nested dependencies) * * @param projectPaths The projects which must be included * @return The list of project paths and their directories as a tuple */ List> getAllProjectsWithDependencies(Set projectPaths) { Set result = new HashSet() projectPaths.forEach { addReferences(it, result) } return result.collect { projectPath -> File projectDir = allProjects[projectPath] if (projectDir == null) { throw new GradleException("cannot find project directory for $projectPath") } new Tuple2(projectPath, projectDir) } } private void addReferences(String projectPath, Set target) { if (target.contains(projectPath)) { return // already added } target.add(projectPath) Set allReferences = getOutgoingReferences(projectPath) allReferences.forEach { addReferences(it, target) } } private Set getOutgoingReferences(String projectPath) { def references = projectReferences[projectPath] if (references == null) { throw new GradleException("Project $projectPath does not exist.\n" + "Please check the build.gradle file for your $projectPath project " + "and update the project dependencies.") } def implicitReferences = findImplicitReferences(projectPath) def constraintReferences = findConstraintReferences(projectPath) return references + implicitReferences + constraintReferences } /** * Finds sibling projects that will be needed for constraint publishing. This is necessary * for when androidx.constraints=true is set and automatic atomic group constraints are enabled * meaning that :foo:foo and :foo:foo-bar projects are required even if they don't reference * each other. * * @param projectPath The project path whose sibling projects will be found * @return The set of sibling projects that will be needed for constraint publishing */ private Set findConstraintReferences(String projectPath) { Set constraintReferences = new HashSet() if (!constraintsEnabled || !publishedLibraryProjects.contains(projectPath)) return constraintReferences def lastColon = projectPath.lastIndexOf(":") if (lastColon == -1) return constraintReferences allProjectPaths().forEach { if (it.startsWith(projectPath.substring(0, lastColon)) && publishedLibraryProjects.contains(it)) { constraintReferences.add(it) } } return constraintReferences } /** * Finds implicit dependencies of a project. This is necessary because when ":foo:bar" is * included in Gradle, it automatically also loads ":foo". * @param projectPath The project path whose implicit dependencies will be found * * @return The set of implicit dependencies for projectPath */ private Set findImplicitReferences(String projectPath) { Set implicitReferences = new HashSet() for (reference in projectReferences[projectPath]) { String[] segments = reference.substring(1).split(":") String subpath = "" for (int i = 0; i < segments.length; i++) { subpath += ":" + segments[i] if (allProjects.containsKey(subpath)) { implicitReferences.add(subpath) } } } return implicitReferences } /** * Find dependency paths from sourceProjectPaths to targetProjectPath. * @param sourceProjectPaths The project paths whose outgoing references will be traversed * @param targetProjectPath The target project path that will be checked for reachability * @return A list of strings where each item is a representation of a dependency path, in * the form of: "path1 -> path2 -> path3". This is intended to be human readable. */ List findPathsBetween(Set sourceProjectPaths, String targetProjectPath) { return sourceProjectPaths.collect { findPathsBetween(it, targetProjectPath, sourceProjectPaths - it) } - null } @Nullable String findPathsBetween( String sourceProjectPath, String targetProjectPath, Set visited ) { if (sourceProjectPath == targetProjectPath) { return targetProjectPath } if (visited.contains(sourceProjectPath)) { return null } Set myReferences = getOutgoingReferences(sourceProjectPath) Set subExclude = visited + sourceProjectPath for (String dependency : myReferences) { String path = findPathsBetween(dependency, targetProjectPath, subExclude) if (path != null) { return "$sourceProjectPath -> $path" } } return null } /** * Parses the build file in the given projectDir to find its project dependencies. * * @param projectPath The Gradle projectPath of the project * @param projectDir The project directory on the file system * @return Set of project paths that are dependent by the given project */ private Set extractReferencesFromBuildFile(String projectPath, File projectDir) { File buildFile = buildFileNames.findResult { buildFileName -> File candidate = new File(projectDir, buildFileName) return candidate.exists() ? candidate : null } Set links = new HashSet() if (buildFile != null) { def buildGradleProperty = settings.services.get(ObjectFactory).fileProperty() .fileValue(buildFile) def contents = settings.providers.fileContents(buildGradleProperty) .getAsText().get() for (line in contents.lines()) { Matcher m = projectReferencePattern.matcher(line) if (m.find()) { // ignore projectOrArtifact dependencies in playground def projectOrArtifact = m.group(1) == "projectOrArtifact" if (!isPlayground || !projectOrArtifact) { links.add(m.group("name")) } } if (multilineProjectReference.matcher(line).find()) { throw new IllegalStateException( "Multi-line project() references are not supported." + "Please fix $file.absolutePath" ) } Matcher targetProject = testProjectTarget.matcher(line) if (targetProject.find()) { links.add(targetProject.group(1)) } Matcher matcherInspection = inspection.matcher(line) if (matcherInspection && !isPlayground) { // inspection is not supported in playground links.add(matcherInspection.group(1)) } if (composePlugin.matcher(line).find()) { links.add(":compose:lint:internal-lint-checks") } if (publishedLibrary.matcher(line).find()) { publishedLibraryProjects.add(projectPath) } Matcher publishProject = publishProjectReference.matcher(line) if (publishProject.find()) { links.add(publishProject.group(1)) } } } else if (!projectDir.exists()) { // Remove file existence checking when https://github.com/gradle/gradle/issues/25531 is // fixed. // This option is supported so that development/simplify_build_failure.sh can try // deleting entire projects at once to identify the cause of a build failure if (System.getenv("ALLOW_MISSING_PROJECTS") == null) { throw new Exception("Path " + buildFile + " does not exist;" + "cannot include project " + projectPath + " ($projectDir)") } } return links } private static Pattern projectReferencePattern = Pattern.compile( "(project|projectOrArtifact)\\((path: )?[\"'](?\\S*)[\"'](, configuration: .*)?\\)" ) private static Pattern testProjectTarget = Pattern.compile("targetProjectPath = \"(.*)\"") private static Pattern multilineProjectReference = Pattern.compile("project\\(\$") private static Pattern inspection = Pattern.compile("packageInspector\\(project, \"(.*)\"\\)") private static Pattern composePlugin = Pattern.compile("id\\(\"AndroidXComposePlugin\"\\)") private static Pattern publishedLibrary = Pattern.compile( "(type = SoftwareType\\.(PUBLISHED_LIBRARY|GRADLE_PLUGIN|ANNOTATION_PROCESSOR|ANNOTATION_PROCESSOR_UTILS|OTHER_CODE_PROCESSOR" + "|STANDALONE_PUBLISHED_LINT|PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS" + "|PUBLISHED_TEST_LIBRARY|PUBLISHED_PROTO_LIBRARY|PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY)|" + "publish = Publish\\.SNAPSHOT_AND_RELEASE)" ) private static Pattern publishProjectReference = Pattern.compile("\"(.*):publish\"") private static List buildFileNames = ["build.gradle", "build.gradle.kts"] } ProjectDependencyGraph createProjectDependencyGraph(Settings settings, boolean constraintsEnabled) { return new ProjectDependencyGraph(settings, false /** isPlayground **/, constraintsEnabled) } // export a function to create ProjectDependencyGraph ext.createProjectDependencyGraph = this.&createProjectDependencyGraph ext.allProjectsConsumers = { ProjectDependencyGraph graph -> graph.allProjectConsumers() }