blob: 4179b602aa83756641ecd3b9b60de7b52b2aaa13 [file] [log] [blame]
/*
* 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