| /* |
| * 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<String, Set<String>> projectReferences = new HashMap<String, Set<String>>() |
| |
| /** |
| * A map of project path to a set of project paths that directly depend on the key project. |
| */ |
| private Map<String, Set<String>> projectConsumers = new HashMap<String, Set<String>>() |
| |
| private Set<String> publishedLibraryProjects = new HashSet<>() |
| |
| /** |
| * A map of all project paths to their project directory. |
| */ |
| private Map<String, File> allProjects = new HashMap<String, File>() |
| |
| ProjectDependencyGraph(Settings settings, boolean isPlayground, boolean constraintsEnabled) { |
| this.settings = settings |
| this.isPlayground = isPlayground |
| this.constraintsEnabled = constraintsEnabled |
| } |
| |
| Set<String> allProjectPaths() { |
| return allProjects.keySet() |
| } |
| |
| /** |
| * 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<String> cached = projectReferences.get(projectPath) |
| if (cached != null) { |
| return |
| } |
| allProjects[projectPath] = projectDir |
| Set<String> parsedDependencies = extractReferencesFromBuildFile(projectPath, projectDir) |
| projectReferences[projectPath] = parsedDependencies |
| parsedDependencies.forEach { dependency -> |
| def reverseLookupSet = projectConsumers[dependency] ?: new HashSet<String>() |
| 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<String> findAllProjectsDependingOn(String projectPath) { |
| Set<String> result = new HashSet<String>() |
| ArrayDeque<String> toBeTraversed = new ArrayDeque<String>() |
| 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<Tuple2<String, File>> getAllProjectsWithDependencies(Set<String> projectPaths) { |
| Set<String> result = new HashSet<String>() |
| 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<String> target) { |
| if (target.contains(projectPath)) { |
| return // already added |
| } |
| target.add(projectPath) |
| Set<String> allReferences = getOutgoingReferences(projectPath) |
| allReferences.forEach { |
| addReferences(it, target) |
| } |
| } |
| |
| private Set<String> 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<String> findConstraintReferences(String projectPath) { |
| Set<String> 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<String> findImplicitReferences(String projectPath) { |
| Set<String> 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<String> findPathsBetween(Set<String> sourceProjectPaths, String targetProjectPath) { |
| return sourceProjectPaths.collect { |
| findPathsBetween(it, targetProjectPath, sourceProjectPaths - it) |
| } - null |
| } |
| |
| @Nullable |
| String findPathsBetween( |
| String sourceProjectPath, String targetProjectPath, Set<String> visited |
| ) { |
| if (sourceProjectPath == targetProjectPath) { |
| return targetProjectPath |
| } |
| if (visited.contains(sourceProjectPath)) { |
| return null |
| } |
| Set<String> myReferences = getOutgoingReferences(sourceProjectPath) |
| Set<String> subExclude = visited + sourceProjectPath |
| for (String dependency : myReferences) { |
| String path = findPathsBetween(dependency, targetProjectPath, subExclude) |
| if (path != null) { |
| return "$sourceProjectPath -> $path" |
| } |
| } |
| return null |
| } |
| |
| /** |
| * Parses the build.gradle 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<String> extractReferencesFromBuildFile(String projectPath, File projectDir) { |
| File buildGradle = new File(projectDir, "build.gradle") |
| Set<String> links = new HashSet<String>() |
| if (buildGradle.exists()) { |
| def buildGradleProperty = settings.services.get(ObjectFactory).fileProperty() |
| .fileValue(buildGradle) |
| 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) |
| } |
| } |
| } 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 " + buildGradle + " does not exist;" + |
| "cannot include project " + projectPath + " ($projectDir)") |
| } |
| } |
| return links |
| } |
| |
| private static Pattern projectReferencePattern = Pattern.compile( |
| "(project|projectOrArtifact)\\((path: )?[\"'](?<name>\\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 = LibraryType\\.(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)" |
| ) |
| } |
| |
| ProjectDependencyGraph createProjectDependencyGraph(Settings settings, boolean constraintsEnabled) { |
| return new ProjectDependencyGraph(settings, false /** isPlayground **/, constraintsEnabled) |
| } |
| // export a function to create ProjectDependencyGraph |
| ext.createProjectDependencyGraph = this.&createProjectDependencyGraph |