Skip to content

Commit

Permalink
Add updateLock task
Browse files Browse the repository at this point in the history
It is now possible to update only a subset of the locked dependencies
rather than updating them all at once by regenerating the lock file.

 nebula-plugins#42
  • Loading branch information
David Cowden committed Feb 21, 2015
1 parent ca96962 commit 8a8e8be
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 19 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ To include, add the following to your build.gradle

Command line overrides via `-PdependencyLock.override` or `-PdependencyLock.overrideFile` will apply.

* generateLock - Generate a lock file into the build directory, any existing `dependency.lock` file will be ignored
* saveLock - copies generated lock into the project directory
* generateLock - Generate a lock file into the build directory. Any existing `dependency.lock` file will be ignored.
* updateLock - Update dependencies from the lock file into the build directory. By default, this task does the same thing as the `generateLock` task except no dependencies will be removed from the lock file, only updated. This task also exposes an option `--dependencies` allowing the user to specify a comma-separated list, in the format `<group>:<artifact>`, of dependencies to update.
* saveLock - Copy the generated lock into the project directory.
* commitLock - If a [gradle-scm-plugin](https://github.com/nebula-plugins/gradle-scm-plugin) implementation is applied. Will commit dependencies.lock to the configured SCM. Exists only on the rootProject. Assumes scm root is at the same level as the root build.gradle.

#### Common Command Line Overrides
Expand All @@ -49,6 +50,8 @@ Revert to normal gradle behavior even with plugin applied.

### Common Workflows

Generate lock:

1. `./gradlew generateLock saveLock`
2. `./gradlew test`
3. if 2 passes `./gradlew commitLock`
Expand All @@ -59,6 +62,11 @@ or
2. `./gradlew -PdependencyLock.useGeneratedLock=true test`
3. `./gradlew saveLock commitLock`

Update lock (the lock must still be saved/committed):

* `./gradlew updateLock --dependencies com.example:foo,com.example:bar`


### Extensions Provided

#### dependencyLock Extension
Expand All @@ -68,15 +76,17 @@ or
* lockFile - This field takes a String. The default is `dependencies.lock`. This filename will be what is generated by `generateLock` and read when locking dependencies.
* configurationNames - This field takes a List<String>. Defaults to the `testRuntime` conf which will include `compile`, `runtime`, and `testCompile`. These will be the configurations that are read when locking.
* dependencyFilter - This field can be assigned a Closure that is used to filter the set of top-level dependencies as they are retrieved from the configurations. This happens before overrides are applied and before any dependencies are skipped. The Closure must accept the dependency's `group`, `name`, and `version` as its 3 parameters. The default implementation returns `true`, meaning all dependencies are used.
* updateDependencies - This field takes a List<String> denoting the dependencies that should be updated when the `updateLock` task is run. If any dependencies are specified via the `--dependencies` option, this field is ignored. If any dependencies are listed during execution of the `updateLock` task either via the `--dependencies` option or this field, the `dependencyFilter` is bypassed.
* skippedDependencies - This field takes a List<String>. Defaults to empty. This list is used to list dependencies as ones that will never be locked. Strings should be of the format `<group>:<artifact>`
* includeTransitives - This field is a boolean. Defaults to false. False will only lock direct dependencies. True will lock the entire transitive graph.

Use the extension if you wish to configure. Each project where gradle-dependency-lock will have its own dependencyLock extension.
Use the extension if you wish to configure. Each project where gradle-dependency-lock is applied will have its own dependencyLock extension.

dependencyLock {
lockFile = 'dependencies.lock'
configurationNames = ['testRuntime']
dependencyFilter = { String group, String name, String version -> true }
updateDependencies = []
skippedDependencies = []
includeTransitives = false
}
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=2.2.1-SNAPSHOT
version=2.3.0-SNAPSHOT
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class DependencyLockExtension {
String lockFile = 'dependencies.lock'
Set<String> configurationNames = ['testRuntime'] as Set
Closure dependencyFilter = { String group, String name, String version -> true }
Set<String> updateDependencies = [] as Set
Set<String> skippedDependencies = [] as Set
boolean includeTransitives = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import groovy.json.JsonSlurper
import nebula.plugin.dependencylock.tasks.CommitLockTask
import nebula.plugin.dependencylock.tasks.GenerateLockTask
import nebula.plugin.dependencylock.tasks.SaveLockTask
import nebula.plugin.dependencylock.tasks.UpdateLockTask
import nebula.plugin.scm.ScmPlugin
import org.gradle.api.GradleException
import org.gradle.api.Plugin
Expand All @@ -42,27 +43,37 @@ class DependencyLockPlugin implements Plugin<Project> {
}

Map overrides = loadOverrides()
GenerateLockTask lockTask = configureLockTask(clLockFileName, extension, overrides)

GenerateLockTask genLockTask = project.tasks.create('generateLock', GenerateLockTask)
configureLockTask(genLockTask, clLockFileName, extension, overrides)
if (project.hasProperty('dependencyLock.useGeneratedLock')) {
clLockFileName = lockTask.getDependenciesLock().path
clLockFileName = genLockTask.getDependenciesLock().path
logger.lifecycle(clLockFileName)
}
SaveLockTask saveTask = configureSaveTask(lockTask, extension)

UpdateLockTask updateLockTask = project.tasks.create('updateLock', UpdateLockTask)
configureLockTask(updateLockTask, clLockFileName, extension, overrides)
configureUpdateTask(updateLockTask, extension)

SaveLockTask saveTask = configureSaveTask(clLockFileName, extension)
saveTask.mustRunAfter genLockTask, updateLockTask

configureCommitTask(clLockFileName, saveTask, extension, commitExtension)

project.gradle.taskGraph.whenReady { taskGraph ->
File dependenciesLock = new File(project.projectDir, clLockFileName ?: extension.lockFile)

if (taskGraph.hasTask(lockTask)) {
def hasLockingTask = taskGraph.hasTask(genLockTask) || taskGraph.hasTask(updateLockTask)

if (hasLockingTask) {
project.configurations.all {
resolutionStrategy {
cacheDynamicVersionsFor 0, 'seconds'
}
}
}

if (!taskGraph.hasTask(lockTask) && dependenciesLock.exists() &&
!shouldIgnoreDependencyLock()) {
if (!hasLockingTask && dependenciesLock.exists() && !shouldIgnoreDependencyLock()) {
applyLock(dependenciesLock, overrides)
} else if (!shouldIgnoreDependencyLock()) {
applyOverrides(overrides)
Expand Down Expand Up @@ -104,13 +115,12 @@ class DependencyLockPlugin implements Plugin<Project> {
}
}

private SaveLockTask configureSaveTask(GenerateLockTask lockTask, DependencyLockExtension extension) {
private SaveLockTask configureSaveTask(String clLockFileName, DependencyLockExtension extension) {
SaveLockTask saveTask = project.tasks.create('saveLock', SaveLockTask)
saveTask.conventionMapping.with {
generatedLock = { lockTask.dependenciesLock }
generatedLock = { new File(project.buildDir, clLockFileName ?: extension.lockFile) }
outputLock = { new File(project.projectDir, extension.lockFile) }
}
saveTask.mustRunAfter lockTask
saveTask.outputs.upToDateWhen {
if (saveTask.generatedLock.exists() && saveTask.outputLock.exists()) {
saveTask.generatedLock.text == saveTask.outputLock.text
Expand All @@ -122,8 +132,7 @@ class DependencyLockPlugin implements Plugin<Project> {
saveTask
}

private GenerateLockTask configureLockTask(String clLockFileName, DependencyLockExtension extension, Map overrides) {
GenerateLockTask lockTask = project.tasks.create('generateLock', GenerateLockTask)
private GenerateLockTask configureLockTask(GenerateLockTask lockTask, String clLockFileName, DependencyLockExtension extension, Map overrides) {
lockTask.conventionMapping.with {
dependenciesLock = {
new File(project.buildDir, clLockFileName ?: extension.lockFile)
Expand All @@ -138,6 +147,14 @@ class DependencyLockPlugin implements Plugin<Project> {
lockTask
}

private configureUpdateTask(UpdateLockTask lockTask, DependencyLockExtension extension) {
// You can't read a property at the same time you define the convention mapping ∞
def updatesFromOption = lockTask.dependencies
lockTask.conventionMapping.dependencies = { updatesFromOption ?: extension.updateDependencies }

lockTask
}

void applyOverrides(Map overrides) {
if (project.hasProperty('dependencyLock.overrideFile')) {
logger.info("Using override file ${project['dependencyLock.overrideFile']} to lock dependencies")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.tasks.TaskAction

class GenerateLockTask extends AbstractLockTask {
String description = 'Create a lock file in build/<configured name>'
String description = 'Create a lock file in build/<specified name>'
Closure filter = { group, name, version -> true }
Set<String> configurationNames
Set<String> skippedDependencies = []
Expand Down Expand Up @@ -51,7 +51,7 @@ class GenerateLockTask extends AbstractLockTask {
def filteredExternalDependencies = externalDependencies.findAll { Dependency dependency ->
filter(dependency.group, dependency.name, dependency.version)
}
filteredExternalDependencies.each { Dependency dependency ->
filteredExternalDependencies.each { ExternalDependency dependency ->
def key = new LockKey(group: dependency.group, artifact: dependency.name)
deps[key].requested = dependency.version
}
Expand Down Expand Up @@ -149,7 +149,7 @@ class GenerateLockTask extends AbstractLockTask {
deps[key].transitive << parent
}

private void writeLock(deps) {
void writeLock(deps) {
def strings = deps.findAll { !getSkippedDependencies().contains(it.key.toString()) }
.collect { LockKey k, Map v -> stringifyLock(k, v) }
strings = strings.sort()
Expand Down Expand Up @@ -188,7 +188,7 @@ class GenerateLockTask extends AbstractLockTask {
}

@EqualsAndHashCode
private static class LockKey {
static class LockKey {
String group
String artifact

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package nebula.plugin.dependencylock.tasks

import groovy.json.JsonSlurper
import org.apache.log4j.spi.LoggerFactory
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging

import java.lang.Override
import org.gradle.api.GradleException
import org.gradle.api.internal.tasks.options.Option

/**
* The update task is a generate task, it simply reads in the old locked dependencies and then overwrites the desired
* dependencies per user request.
*/
class UpdateLockTask extends GenerateLockTask {
private static Logger logger = Logging.getLogger(UpdateLockTask)

String description = 'Apply updates to a preexisting lock file and write to build/<specified name>'
Set<String> dependencies

@Option(option = "dependencies", description = "Specify which dependencies to update via a comma-separated list")
void setDependencies(String depsFromOption) {
setDependencies(depsFromOption.tokenize(',').toSet())
}

void setDependencies(Set<String> dependencyList) {
this.dependencies = dependencyList
}

@Override
void lock() {
// If the user specifies dependencies to update, ignore any filter specified by the build file and use our
// own generated from the list of dependencies.
def updates = getDependencies()

if (updates) {
filter = { group, artifact, version ->
updates.contains("${group}:${artifact}".toString())
}
}
super.lock()
}

@Override
void writeLock(updatedDeps) {
File currentLock = new File(project.projectDir, dependenciesLock.name)
def lockedDeps = loadLock(currentLock)
super.writeLock(lockedDeps + (updatedDeps as Map))
}

private static loadLock(File lock) {
def lockKeyMap = [:].withDefault { [transitive: [] as Set, firstLevelTransitive: [] as Set, childrenVisited: false] }

try {
def json = new JsonSlurper().parseText(lock.text)
json.each { key, value ->
def (group, artifact) = key.tokenize(':')
lockKeyMap.put(new LockKey(group: group, artifact: artifact), value)
}
} catch (ex) {
logger.debug('Unreadable json file: ' + lock.text)
logger.error('JSON unreadable')
throw new GradleException("${lock.name} is unreadable or invalid json, terminating run", ex)
}

lockKeyMap
}
}

0 comments on commit 8a8e8be

Please sign in to comment.