Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
uses: ./.github/actions/bazel
with:
bazel-command: build //...
cache-key: bazel-build-cache
cache-key: bazel-build-cache1
bazel-test:
runs-on: ubuntu-latest
steps:
Expand All @@ -60,7 +60,7 @@ jobs:
uses: ./.github/actions/bazel
with:
bazel-command: test //sample-android:sample-android-demo-free-debug.lint_test
cache-key: bazel-lint-cache
cache-key: bazel-lint-cache1

grazel-build:
runs-on: ubuntu-latest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import com.grab.grazel.gradle.dependencies.DefaultDependencyResolutionService
import com.grab.grazel.gradle.dependencies.DependenciesDataSource
import com.grab.grazel.gradle.dependencies.DependenciesModule
import com.grab.grazel.gradle.variant.AndroidVariantDataSource
import com.grab.grazel.gradle.variant.DefaultVariantCompressionService
import com.grab.grazel.gradle.variant.VariantBuilder
import com.grab.grazel.gradle.variant.VariantCompressor
import com.grab.grazel.gradle.variant.VariantMatcher
import com.grab.grazel.gradle.variant.VariantModule
import com.grab.grazel.hybrid.HybridBuildExecutor
Expand Down Expand Up @@ -91,11 +93,13 @@ internal interface GrazelComponent {

fun variantBuilder(): Lazy<VariantBuilder>
fun variantMatcher(): Lazy<VariantMatcher>
fun variantCompressor(): Lazy<VariantCompressor>

fun manifestValuesBuilder(): ManifestValuesBuilder

fun dependencyResolutionService(): GradleProvider<DefaultDependencyResolutionService>
fun dependencyGraphsService(): GradleProvider<DefaultDependencyGraphsService>
fun variantCompressionService(): GradleProvider<DefaultVariantCompressionService>
fun configurationDataSource(): Lazy<ConfigurationDataSource>
fun repositoryDataSource(): Lazy<RepositoryDataSource>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.grab.grazel.gradle.dependencies
import com.google.common.graph.Graphs
import com.google.common.graph.ImmutableValueGraph
import com.grab.grazel.gradle.variant.VariantGraphKey
import com.grab.grazel.gradle.variant.VariantType
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration

Expand Down Expand Up @@ -49,6 +50,18 @@ internal interface DependencyGraphs {
project: Project,
variantKey: VariantGraphKey
): Set<Project>

/**
* Merges variant graphs into a project-level dependency graph.
*
* @param variantTypeFilter Predicate to filter which variant types to include. Defaults to
* build graphs only (AndroidBuild, JvmBuild) to avoid artificial cycles from test
* dependencies.
* @return Map of projects to their direct dependencies across filtered variants.
*/
fun mergeToProjectGraph(
variantTypeFilter: (VariantType) -> Boolean = { it.isBuildGraph }
): Map<Project, Set<Project>>
}

internal class DefaultDependencyGraphs(
Expand Down Expand Up @@ -93,4 +106,22 @@ internal class DefaultDependencyGraphs(
project: Project,
variantKey: VariantGraphKey
): Set<Project> = variantGraphs[variantKey]?.successors(project)?.toSet() ?: emptySet()

override fun mergeToProjectGraph(
variantTypeFilter: (VariantType) -> Boolean
): Map<Project, Set<Project>> {
// Filter graphs by variant type
val filteredGraphs = variantGraphs.filterKeys { key ->
variantTypeFilter(key.variantType)
}

val allProjects = filteredGraphs.values.flatMap { it.nodes() }.toSet()
return allProjects.associateWith { project ->
filteredGraphs.values.flatMap { graph ->
if (graph.nodes().contains(project)) {
graph.successors(project)
} else emptySet()
}.toSet()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ package com.grab.grazel.gradle.dependencies
import com.grab.grazel.bazel.starlark.BazelDependency
import com.grab.grazel.gradle.isAndroid
import com.grab.grazel.gradle.isAndroidTest
import com.grab.grazel.gradle.variant.DefaultVariantCompressionService
import com.grab.grazel.gradle.variant.MatchedVariant
import com.grab.grazel.gradle.variant.nameSuffix
import com.grab.grazel.gradle.variant.resolveSuffix
import com.grab.grazel.util.GradleProvider
import org.gradle.api.Project
import javax.inject.Inject

internal class GradleDependencyToBazelDependency
@Inject
constructor() {
/**
* [matchedVariant] can only be null if and only if the [project] is a Java/Kotlin project
*/
constructor(
private val variantCompressionService: GradleProvider<DefaultVariantCompressionService>
) {
/** [matchedVariant] can only be null if and only if the [project] is a Java/Kotlin project */
fun map(
project: Project,
dependency: Project,
Expand All @@ -41,11 +44,18 @@ constructor() {
"please provide the variant for the android project=${project.name}"
)
}
if (dependency.isAndroid) {// project is an android project, dependent is also
if (dependency.isAndroid) {
val baseSuffix = variantCompressionService.get().resolveSuffix(
projectPath = dependency.path,
variantName = matchedVariant.variantName,
fallbackSuffix = matchedVariant.nameSuffix,
logger = project.logger
)

val suffix = if (dependency.isAndroidTest) {
"${matchedVariant.nameSuffix}_lib"
"${baseSuffix}_lib"
} else {
matchedVariant.nameSuffix
baseSuffix
}
BazelDependency.ProjectDependency(
dependency,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright 2022 Grabtaxi Holdings PTE LTD (GRAB)
*
* 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.grab.grazel.gradle.dependencies

import org.gradle.api.Project
import java.util.ArrayDeque

/**
* Topological sorting utility using Kahn's algorithm.
*/
internal object TopologicalSorter {
/**
* Returns projects in topological order (dependencies before dependents).
*
* @param graphs The dependency graphs to merge and sort
* @return List of projects ordered such that dependencies appear before dependents
* @throws IllegalStateException if a cycle is detected in the dependency graph
*/
fun sort(graphs: DependencyGraphs): List<Project> {
val merged = graphs.mergeToProjectGraph() // project → its dependencies

if (merged.isEmpty()) {
return emptyList()
}

// Build reverse map: project → projects that depend on it (dependents)
val dependents = mutableMapOf<Project, MutableSet<Project>>()
merged.keys.forEach { dependents[it] = mutableSetOf() }
merged.forEach { (project, dependencies) ->
dependencies.forEach { dep ->
dependents.getOrPut(dep) { mutableSetOf() }.add(project)
}
}

// In-degree = number of dependencies each project has
val inDegrees = merged.mapValues { (_, deps) -> deps.size }.toMutableMap()

// Queue starts with projects having zero dependencies (leaf nodes)
// Sort by path to ensure deterministic ordering
val queue = ArrayDeque(inDegrees.filterValues { it == 0 }.keys.sortedBy { it.path })
val ordered = mutableListOf<Project>()

// Process queue using Kahn's algorithm
while (queue.isNotEmpty()) {
val current = queue.removeFirst()
ordered.add(current)

// Collect newly ready projects and sort them deterministically
val newlyReady = mutableListOf<Project>()
dependents[current]?.forEach { dependent ->
val newDegree = inDegrees[dependent]!! - 1
inDegrees[dependent] = newDegree

// If in-degree reaches zero, all dependencies are processed
if (newDegree == 0) {
newlyReady.add(dependent)
}
}

// Add in sorted order to ensure deterministic results
newlyReady.sortedBy { it.path }.forEach { queue.add(it) }
}

// Detect cycles: if we didn't process all projects, there must be a cycle
check(ordered.size == merged.size) {
val unprocessed = merged.keys - ordered.toSet()
val cyclePath = findCycle(merged, unprocessed)
val cycleMessage = if (cyclePath.isNotEmpty()) {
"Cycle path: ${cyclePath.joinToString(" -> ") { it.path }}"
} else {
"Unable to determine exact cycle path"
}

buildString {
appendLine("Cycle detected in dependency graph.")
appendLine(cycleMessage)
appendLine()
append("Unprocessed projects (may include projects blocked by cycle): ")
append(unprocessed.map { it.path }.sorted())
}
}

return ordered
}

/**
* Finds an actual cycle path using iterative DFS.
*/
private fun findCycle(
graph: Map<Project, Set<Project>>,
unprocessed: Set<Project>
): List<Project> {
val visited = mutableSetOf<Project>()

for (startNode in unprocessed.sortedBy { it.path }) {
if (startNode in visited) continue

val recursionStack = mutableSetOf<Project>()
val parent = mutableMapOf<Project, Project?>()
val stack = ArrayDeque<Pair<Project, Iterator<Project>>>()

val startDeps = graph[startNode]?.filter { it in unprocessed }?.sortedBy { it.path } ?: emptyList()
stack.addLast(startNode to startDeps.iterator())
recursionStack.add(startNode)
parent[startNode] = null

while (stack.isNotEmpty()) {
val (current, iterator) = stack.last()

if (iterator.hasNext()) {
val neighbor = iterator.next()

if (neighbor in recursionStack) {
return reconstructCyclePath(neighbor, current, parent)
}

if (neighbor !in visited && neighbor in unprocessed) {
recursionStack.add(neighbor)
parent[neighbor] = current
val neighborDeps = graph[neighbor]?.filter { it in unprocessed }?.sortedBy { it.path } ?: emptyList()
stack.addLast(neighbor to neighborDeps.iterator())
}
} else {
stack.removeLast()
recursionStack.remove(current)
visited.add(current)
}
}
}
return emptyList()
}

private fun reconstructCyclePath(
cycleStart: Project,
cycleEnd: Project,
parent: Map<Project, Project?>
): List<Project> {
val path = mutableListOf<Project>()
var current: Project? = cycleEnd

while (current != null && current != cycleStart) {
path.add(current)
current = parent[current]
}
path.add(cycleStart)
path.reverse()
path.add(cycleStart)

return path
}
}
Loading