From 79b5fcdfc24bb5762049d212d37704adf5ebf8eb Mon Sep 17 00:00:00 2001 From: David Cowden Date: Fri, 20 Feb 2015 00:08:29 -0800 Subject: [PATCH] Add updateLock task 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. #42 --- README.md | 16 ++++- gradle.properties | 2 +- .../DependencyLockExtension.groovy | 1 + .../DependencyLockPlugin.groovy | 39 ++++++++--- .../tasks/GenerateLockTask.groovy | 8 +-- .../tasks/UpdateLockTask.groovy | 69 +++++++++++++++++++ 6 files changed, 116 insertions(+), 19 deletions(-) create mode 100644 src/main/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTask.groovy diff --git a/README.md b/README.md index 6c1d2731..19dc3a96 100644 --- a/README.md +++ b/README.md @@ -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 `:`, 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 @@ -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` @@ -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 @@ -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. 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 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. Defaults to empty. This list is used to list dependencies as ones that will never be locked. Strings should be of the format `:` * 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 } diff --git a/gradle.properties b/gradle.properties index bec0584f..65362284 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.2.1-SNAPSHOT +version=2.3.0-SNAPSHOT diff --git a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockExtension.groovy b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockExtension.groovy index d37bc17b..73959ba4 100644 --- a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockExtension.groovy +++ b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockExtension.groovy @@ -19,6 +19,7 @@ class DependencyLockExtension { String lockFile = 'dependencies.lock' Set configurationNames = ['testRuntime'] as Set Closure dependencyFilter = { String group, String name, String version -> true } + Set updateDependencies = [] as Set Set skippedDependencies = [] as Set boolean includeTransitives = false } diff --git a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockPlugin.groovy b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockPlugin.groovy index 23dd02ad..a9a2fbfa 100644 --- a/src/main/groovy/nebula/plugin/dependencylock/DependencyLockPlugin.groovy +++ b/src/main/groovy/nebula/plugin/dependencylock/DependencyLockPlugin.groovy @@ -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 @@ -42,18 +43,29 @@ class DependencyLockPlugin implements Plugin { } 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' @@ -61,8 +73,7 @@ class DependencyLockPlugin implements Plugin { } } - if (!taskGraph.hasTask(lockTask) && dependenciesLock.exists() && - !shouldIgnoreDependencyLock()) { + if (!hasLockingTask && dependenciesLock.exists() && !shouldIgnoreDependencyLock()) { applyLock(dependenciesLock, overrides) } else if (!shouldIgnoreDependencyLock()) { applyOverrides(overrides) @@ -104,13 +115,12 @@ class DependencyLockPlugin implements Plugin { } } - 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 @@ -122,8 +132,7 @@ class DependencyLockPlugin implements Plugin { 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) @@ -138,6 +147,14 @@ class DependencyLockPlugin implements Plugin { 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") diff --git a/src/main/groovy/nebula/plugin/dependencylock/tasks/GenerateLockTask.groovy b/src/main/groovy/nebula/plugin/dependencylock/tasks/GenerateLockTask.groovy index 021d4dec..2d673685 100644 --- a/src/main/groovy/nebula/plugin/dependencylock/tasks/GenerateLockTask.groovy +++ b/src/main/groovy/nebula/plugin/dependencylock/tasks/GenerateLockTask.groovy @@ -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/' + String description = 'Create a lock file in build/' Closure filter = { group, name, version -> true } Set configurationNames Set skippedDependencies = [] @@ -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 } @@ -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() @@ -188,7 +188,7 @@ class GenerateLockTask extends AbstractLockTask { } @EqualsAndHashCode - private static class LockKey { + static class LockKey { String group String artifact diff --git a/src/main/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTask.groovy b/src/main/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTask.groovy new file mode 100644 index 00000000..764566a4 --- /dev/null +++ b/src/main/groovy/nebula/plugin/dependencylock/tasks/UpdateLockTask.groovy @@ -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/' + Set dependencies + + @Option(option = "dependencies", description = "Specify which dependencies to update via a comma-separated list") + void setDependencies(String depsFromOption) { + setDependencies(depsFromOption.tokenize(',') as Set) + } + + void setDependencies(Set 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 + } +}