A Gradle plugin that helps you guard against unintentional dependency changes.
Comparison to a baseline occurs whenever you run the dependencyGuard
Gradle task.
A small single version bump of androidx.activity
from 1.3.1
-> 1.4.0
causes many libraries to transitively update.
For a given configuration, you may never want junit
to be shipped. You can prevent this by modifying the allowedFilter
rule to return !it.contains("junit")
for your situation.
As platform engineers, we do a lot of library upgrades, and needed insight into how dependencies were changing over time. Any small change can have a large impact. On large teams, it's not possible to track all dependency changes in a practical way, so we needed tooling to help. This is a tool that surfaces these changes to any engineer making dependency changes, and allows them to re-baseline if it's intentional. This provides us with historical reference on when dependencies (including transitive) are changed for a given configuration.
- Surface transitive and non-transitive dependencies changes for our production build configurations. This helps during version bumps and when new libraries are added.
- Provide the developer with the ability to easily accept changes as needed.
- Add deny-listing for production build configurations to explicitly block some dependencies from production.
- Include a record of the change in Git when their code is merged to easily diagnose/debug crashes in the future.
- Be deterministic. De-duplicate entries, and order alphabetically.
- Accidentally shipping a testing dependency to production because
implementation
was used instead oftestImplementation
- @handstandsam- Dependency Guard has List and Tree baseline formats that can compared against to identify changes. If changes occur, and they are expected, you can re-baseline.
- Dependency versions were transitively upgraded which worked fine typically, but caused a runtime crash later that was hard to figure out why it happened.
- You would see the diff off dependencies in Git since the last working version and be able to have quickly track down the issue.
- After adding
Ktor 2.0.0
, it transitively upgraded toKotlin 1.6.20
which caused incompatibilities withJetpack Compose 1.1.1
which requiresKotlin 1.6.10
- @joreilly- During large upgrades it is important to see how a single version bump will impact the rest of your application.
- Upgrading to the Android Gradle Plugin transitively downgraded protobuf plugin which caused issues - @AutonomousApps
// sample/app/build.gradle.kts
plugins {
id("com.dropbox.dependency-guard") version "0.4.1"
}
This will show a list of available configuration(s) for this Gradle module.
You can choose the configurations you want to monitor.
We suggest monitoring your release configuration to know what is included in production builds. You can choose to monitor any classpath configuration with Dependency Guard.
// sample/app/build.gradle.kts
dependencyGuard {
// All dependencies included in Production Release APK
configuration("releaseRuntimeClasspath")
}
NOTE: Checkout the "Adding to the Buildscript Classpath" section below if the plugin can't be resolved.
It's suggested to run in your CI or pre-commit hooks to detect these changes. If this task is not run, then you aren't getting the full benefit of this plugin.
Bump Version and Detect Change
If any dependencies have changed, you will be provided with the ./gradlew dependencyGuardBaseline
task to update the baseline and intentionally accept these new changes.
If you have explicit test or debugging dependencies you never want to ship, you can create rules for them here.
dependencyGuard {
configuration("releaseRuntimeClasspath") {
allowedFilter = {
// Disallow dependencies with a name containing "junit"
!it.contains("junit")
}
}
}
By default, Dependency Guard tracks modules and artifacts in a list format is generated at dependencies/${configurationName}.txt
.
org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10
org.jetbrains.kotlin:kotlin-stdlib:1.6.10
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2
org.jetbrains:annotations:13.0
You can choose to not track modules or artifacts:
dependencyGuard {
configuration("releaseRuntimeClasspath") {
// What is included in the list report
artifacts = true // Defaults to true
modules = false // Defaults to false
}
}
The builtin Dependencies task from Gradle is :dependencies
. The tree format support for Dependency Guard leverages this under the hood and targets it for a specific configuration.
The dependency-tree-diff
library in extremely helpful tool, but is impractical for many projects in Continuous Integration since it has a lot of custom setup to enable it. (related discussion). dependency-tree-diff
's diffing logic is actually used by Dependency Guard to highlight changes in trees.
The tree format proved to be very helpful, but as we looked at it from a daily usage standpoint, we found that the tree format created more noise than signal. It's super helpful to use for development, but we wouldn't recommend storing the tree format in Git, especially in large projects as it gets noisy, and it becomes ignored. In brainstorming with @joshfein, it seemed like a simple ordered, de-duplicated list of dependencies were better for storing in CI.
dependencies/releaseRuntimeClasspath.tree.txt
------------------------------------------------------------
Project ':sample:app'
------------------------------------------------------------
releaseRuntimeClasspath - Resolved configuration for runtime for variant: release
\--- project :sample:module1
+--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.6.10
| | +--- org.jetbrains:annotations:13.0
| | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10
| \--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.10 (*)
\--- project :sample:module2
+--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 (*)
\--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2
\--- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2
+--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.30 -> 1.6.10 (*)
\--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.30 -> 1.6.10
(*) - dependencies omitted (listed previously)
A web-based, searchable dependency report is available by adding the --scan option.
Enable the tree format for a configuration with the following option:
dependencyGuard {
configuration("releaseRuntimeClasspath") {
tree = true // Enable Tree Format
}
}
- Production App Configurations (what you ship)
- Production Build Configurations (what you use to build your app)
Changes to either one of these can change your resulting application.
Dependency Guard writes a baseline file containing all your transitive dependencies for a given configuration that should be checked into git.
Under the hood, we're leveraging the same logic that the Gradle dependencies
task uses and displays in build scans, but creating a process which can guard against changes.
A baseline file is created for each build configuration you want to guard.
If the dependencies do change, you'll get easy to read warnings to help you visualize the differences, and accept them by re-baselining. This new baseline will be visible in your Git history to help understand when dependencies changed (including transitive).
dependencyGuard {
configuration("releaseRuntimeClasspath") {
// What is included in the list report
artifacts = true // Defaults to true
modules = false // Defaults to false
// Tree Report
tree = false // Defaults to false
// Filter through dependencies and return true if allowed. Build will fail if unallowed.
allowedFilter = {dependencyName: String ->
return true // Defaults to true
}
// Modify a dependency name or remove it (by returning null) from the baseline file
baselineMap = { dependencyName: String ->
return dependencyName // Defaults to return itself
}
}
}
The Dependency Guard plugin adds a few tasks for you to use in your Gradle Builds. Your continuous integration environment would run the dependencyGuard
task to ensure things did not change, and require developers to re-baseline using the dependencyGuardBaseline
tasks when changes were intentional.
Compare against the configured transitive dependency report baselines, or generate them if they don't exist.
This task is added to any project that you apply the plugin to. The plugin should only be applied to project you are interested in tracking transitive dependencies for.
This task overwrites the dependencies in the dependencies
directory in your module.
Baseline files are created in the "dependencies" folder in your module. The following reports are created for the :sample:app
module by running ./gradlew :sample:app:dependencyGuardBaseline
Snapshot versions are under development and change, but can be used by adding in the snapshot repository
// Root build.gradle
buildscript {
repositories {
mavenCentral()
google()
gradlePluginPortal()
// SNAPSHOT Versions of Dependency Guard
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots" }
}
dependencies {
classpath("com.dropbox.dependency-guard:dependency-guard:0.4.1")
}
}
Copyright (c) 2022 Dropbox, Inc.
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.