Skip to content

Commit

Permalink
Add support to update root gradle.properties
Browse files Browse the repository at this point in the history
  • Loading branch information
asodja committed May 24, 2020
1 parent a1dbfc8 commit a13cae6
Show file tree
Hide file tree
Showing 20 changed files with 522 additions and 123 deletions.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ apply {
```

### Multi-project usage
In case you have a Multi-project build and you have some common dependency configuration in some common file in root project
(like *.gradle file), you should apply plugin to all projects. Easiest way to do this is with `allprojects` block like:
```
plugins {
id 'se.patrikerdes.use-latest-versions' version '0.2.13'
id 'com.github.ben-manes.versions' version '0.21.0'
}
allprojects {
apply plugin: 'se.patrikerdes.use-latest-versions'
apply plugin: 'com.github.ben-manes.versions'
}
```
This is because `se.patrikerdes.use-latest-versions` plugin scans files for every project separately.

In case you handle dependencies per project separately this is not needed and you can apply plugin just to selected projects.

## Example

Given this build.gradle file:
Expand Down Expand Up @@ -143,7 +161,22 @@ dependencies {
### useLatestVersions

```bash
# gradle useLatestVersions
gradle useLatestVersions

# Configuration and default values:
useLatestVersions {
# A whitelist of dependencies to update, in the format of group:name
# Equal to command line: --update-dependency=[values]
updateWhitelist = []
# A blacklist of dependencies to update, in the format of group:name
# Equal to command line: --ignore-dependency=[values]
updateBlacklist = []
# When enabled, root project gradle.properties will also be populated with
# versions from subprojects in multi-project build
# Equal to command line: --updateRootProperties
updateRootProperties = false
}

```

Updates module and plugin versions in all *.gradle files in the project root folder or any subfolder to the latest
Expand Down
38 changes: 37 additions & 1 deletion src/main/groovy/se/patrikerdes/Common.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.gradle.api.Task

import java.nio.file.Paths
import java.util.regex.Matcher
import java.util.regex.Pattern

@CompileStatic
class Common {
Expand Down Expand Up @@ -48,7 +49,9 @@ class Common {

static void getVariablesFromMatches(Matcher variableMatch, Map<String, String> versionVariables,
DependencyUpdate update, Set problemVariables) {
if (variableMatch.size() == 1) {
// File can have more dependencies with same version variable
// We anyway check that versions of dependencies for that variable are the same
if (variableMatch.size() >= 1) {
String variableName = ((List) variableMatch[0])[1]
if (versionVariables.containsKey(variableName) &&
versionVariables[variableName as String] != update.newVersion) {
Expand Down Expand Up @@ -117,5 +120,38 @@ class Common {
}
outputDir
}

static void updateVersionVariables(Map<String, String> gradleFileContents, List<String> dotGradleFileNames,
Map<String, String> versionVariables) {
for (String dotGradleFileName in dotGradleFileNames) {
for (versionVariable in versionVariables) {
gradleFileContents[dotGradleFileName] =
gradleFileContents[dotGradleFileName].replaceAll(
variableDefinitionMatchStringForFileName(versionVariable.key, dotGradleFileName),
newVariableDefinitionString(versionVariable.value))
}
}
}

static String variableDefinitionMatchStringForFileName(String variable, String fileName) {
String splitter = File.separator.replace('\\', '\\\\')
if (fileName.split(splitter).last() == 'gradle.properties') {
return gradlePropertiesVariableDefinitionMatchString(variable)
}
variableDefinitionMatchString(variable)
}

static String variableDefinitionMatchString(String variable) {
'(' + Pattern.quote(variable) + "[ \t]*=[ \t]*[\"'])(.*)([\"'])"
}

static String gradlePropertiesVariableDefinitionMatchString(String variable) {
'(' + Pattern.quote(variable) + '[ \t]*=[ \t]*)(.*)([ \t]*)'
}

static String newVariableDefinitionString(String newVersion) {
'$1' + newVersion + '$3'
}

}

68 changes: 68 additions & 0 deletions src/main/groovy/se/patrikerdes/InternalAggregateRootTask.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package se.patrikerdes

import static se.patrikerdes.UseLatestVersionsPlugin.USE_LATEST_VERSIONS

import org.gradle.api.Project
import groovy.json.JsonSlurper
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class InternalAggregateRootTask extends DefaultTask {

InternalAggregateRootTask() {
description = 'Internal task that aggregates versions of all projects to root. ' +
'Currently it updates just gradle.properties in root. Don\'t run it as separate task'
}

@TaskAction
void internalAggregateRootTask() {
List<String> versionVariablesFiles = project.gradle.taskGraph.allTasks
.findAll { it.name == USE_LATEST_VERSIONS }
.collectMany { getVersionVariablesFiles(it.project) }
List<String> dotGradleFileNames =
new FileNameFinder().getFileNames(project.projectDir.absolutePath, 'gradle.properties')

Map<String, String> gradleFileContents = dotGradleFileNames.collectEntries {
[(it): new File(it).getText('UTF-8')]
}
Map<String, String> versionVariables = readVersionVariables(versionVariablesFiles)
Common.updateVersionVariables(gradleFileContents, dotGradleFileNames, versionVariables)

// Write all files back
for (dotGradleFileName in dotGradleFileNames) {
new File(dotGradleFileName).setText(gradleFileContents[dotGradleFileName], 'UTF-8')
}

// Delete temp files in build folder
for (String versionVariablesFile in versionVariablesFiles) {
new File(versionVariablesFile).delete()
}
}

List<String> getVersionVariablesFiles(Project project) {
String buildDir = project.buildDir.absolutePath
new FileNameFinder().getFileNames(buildDir, 'useLatestVersions/version-variables.json')
}

Map<String, String> readVersionVariables(List<String> versionVariablesFiles) {
Map<String, String> versionVariables = [:]
List<String> problemVariables = []
for (String versionVariablesFile in versionVariablesFiles) {
Map<String, String> variables = new JsonSlurper().parseText(new File(versionVariablesFile).text)
variables.forEach { k, v ->
if (versionVariables.containsKey(k) && versionVariables.get(k) != v) {
println("A problem was detected: the variable $k has different versions in different projects, " +
"root gradle.properties value won't be be changed.")
problemVariables.add(k)
} else {
versionVariables.put(k, v)
}
}
}
for (problemVariable in problemVariables) {
versionVariables.remove(problemVariable)
}
versionVariables
}

}
35 changes: 33 additions & 2 deletions src/main/groovy/se/patrikerdes/UseLatestVersionsPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,43 @@ package se.patrikerdes
import groovy.transform.CompileStatic
import org.gradle.api.Project
import org.gradle.api.Plugin
import org.gradle.api.Task

@CompileStatic
class UseLatestVersionsPlugin implements Plugin<Project> {

static final String DEPENDENCY_UPDATES = 'dependencyUpdates'
static final String USE_LATEST_VERSIONS = 'useLatestVersions'
static final String USE_LATEST_VERSIONS_CHECK = 'useLatestVersionsCheck'
static final String INTERNAL_ROOT_AGGREGATE = 'internalRootAggregate'

void apply(Project project) {
System.setProperty('outputFormatter', 'json,xml,plain')
project.task('useLatestVersions', type: UseLatestVersionsTask, dependsOn: 'dependencyUpdates')
project.task('useLatestVersionsCheck', type: UseLatestVersionsCheckTask, dependsOn: 'dependencyUpdates')
Task rootAggregate = setupRootAggregateTask(project)
setupUseLatestVersions(project, rootAggregate)
setupUseLatestVersionsCheck(project)
}

Task setupRootAggregateTask(Project project) {
Set<Task> tasks = project.rootProject.getTasksByName(INTERNAL_ROOT_AGGREGATE, false)
if (tasks.isEmpty()) {
// This handles both cases: when UseLatestVersionsPlugin is applied
// to root project and subprojects or when it is applied only to subprojects
return project.rootProject.task(INTERNAL_ROOT_AGGREGATE, type: InternalAggregateRootTask)
}
tasks[0]
}

void setupUseLatestVersions(Project project, Task rootAggregate) {
Task useLatestVersions = project.task(USE_LATEST_VERSIONS, type: UseLatestVersionsTask)
useLatestVersions.dependsOn(DEPENDENCY_UPDATES)
useLatestVersions.finalizedBy(rootAggregate)
rootAggregate.mustRunAfter(useLatestVersions)
}

void setupUseLatestVersionsCheck(Project project) {
Task useLatestVersionCheck = project.task(USE_LATEST_VERSIONS_CHECK, type: UseLatestVersionsCheckTask)
useLatestVersionCheck.dependsOn(DEPENDENCY_UPDATES)
}

}
64 changes: 28 additions & 36 deletions src/main/groovy/se/patrikerdes/UseLatestVersionsTask.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import static se.patrikerdes.Common.getCurrentDependencies
import static se.patrikerdes.Common.getDependencyUpdatesJsonReportFilePath
import static se.patrikerdes.Common.getOutDatedDependencies

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.options.Option
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.options.Option

import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.regex.Matcher
import java.util.regex.Pattern

@CompileStatic
class UseLatestVersionsTask extends DefaultTask {
Expand All @@ -30,24 +30,17 @@ class UseLatestVersionsTask extends DefaultTask {
description = 'A blacklist of dependencies to update, in the format of group:name')
List<String> updateBlacklist = Collections.emptyList()

@Input
@Option(option='update-root-properties',
description = 'Update root project gradle.properties with subprojects versions in multi-project build')
boolean updateRootProperties

UseLatestVersionsTask() {
description = 'Updates module and plugin versions in all *.gradle and *.gradle.kts files to the latest ' +
'available versions.'
group = 'Help'
}

String variableDefinitionMatchString(String variable) {
'(' + Pattern.quote(variable) + "[ \t]*=[ \t]*[\"'])(.*)([\"'])"
}

String gradlePropertiesVariableDefinitionMatchString(String variable) {
'(' + Pattern.quote(variable) + '[ \t]*=[ \t]*)(.*)([ \t]*)'
}

String newVariableDefinitionString(String newVersion) {
'$1' + newVersion + '$3'
}

@TaskAction
void useLatestVersions() {
validateExclusiveWhiteOrBlacklist()
Expand All @@ -59,6 +52,11 @@ class UseLatestVersionsTask extends DefaultTask {
dotGradleFileNames += new FileNameFinder().getFileNames(project.projectDir.absolutePath, '**/*.gradle.kts')
dotGradleFileNames += new FileNameFinder().getFileNames(project.projectDir.absolutePath, '**/gradle.properties')
dotGradleFileNames += new FileNameFinder().getFileNames(project.projectDir.absolutePath, 'buildSrc/**/*.kt')
String rootGradleProperties = new File(project.rootDir.absolutePath, 'gradle.properties').absolutePath
if (updateRootProperties && project != project.rootProject) {
// Append so we don't update variables if defined in multiple files
dotGradleFileNames += rootGradleProperties
}

// Exclude any files that belong to sub-projects
List<String> subprojectPaths = project.subprojects.collect { it.projectDir.absolutePath }
Expand Down Expand Up @@ -91,11 +89,22 @@ class UseLatestVersionsTask extends DefaultTask {

updateModuleVersions(gradleFileContents, dotGradleFileNames, dependencyUpdates)
updatePluginVersions(gradleFileContents, dotGradleFileNames, dependencyUpdates)
updateVariables(gradleFileContents, dotGradleFileNames, dependencyUpdates, dependencyStables)
Map<String, String> versionVariables = getVersionVariables(gradleFileContents, dotGradleFileNames,
dependencyUpdates, dependencyStables)
Common.updateVersionVariables(gradleFileContents, dotGradleFileNames, versionVariables)

// Write all files back
for (dotGradleFileName in dotGradleFileNames) {
new File(dotGradleFileName).setText(gradleFileContents[dotGradleFileName], 'UTF-8')
if (dotGradleFileName != rootGradleProperties) {
// Root Gradle properties are handled in
// internalAggregateRoot task that reads version-variables.json
new File(dotGradleFileName).setText(gradleFileContents[dotGradleFileName], 'UTF-8')
}
}

if (project == project.rootProject || updateRootProperties) {
new File(project.buildDir, 'useLatestVersions/version-variables.json')
.write(new JsonBuilder(versionVariables).toPrettyString())
}
}

Expand Down Expand Up @@ -143,12 +152,11 @@ class UseLatestVersionsTask extends DefaultTask {
}
}

void updateVariables(Map<String, String> gradleFileContents, List<String> dotGradleFileNames,
Map<String, String> getVersionVariables(Map<String, String> gradleFileContents, List<String> dotGradleFileNames,
List<DependencyUpdate> dependencyUpdates, List<DependencyUpdate> dependencyStables) {
Set problemVariables = []
Map<String, String> versionVariables = Common.findVariables(dotGradleFileNames,
dependencyUpdates + dependencyStables, gradleFileContents, problemVariables)

for (problemVariable in problemVariables) {
versionVariables.remove(problemVariable)
}
Expand All @@ -160,7 +168,7 @@ class UseLatestVersionsTask extends DefaultTask {
for (String dotGradleFileName in dotGradleFileNames) {
for (variableName in versionVariables.keySet()) {
Matcher variableDefinitionMatch = gradleFileContents[dotGradleFileName] =~
variableDefinitionMatchStringForFileName(variableName, dotGradleFileName)
Common.variableDefinitionMatchStringForFileName(variableName, dotGradleFileName)
if (variableDefinitionMatch.size() == 1) {
if (variableDefinitions.contains(variableName)) {
// The variable is assigned to in more than one file
Expand All @@ -183,23 +191,7 @@ class UseLatestVersionsTask extends DefaultTask {
versionVariables.remove(problemVariable)
}

// Update variables
for (String dotGradleFileName in dotGradleFileNames) {
for (versionVariable in versionVariables) {
gradleFileContents[dotGradleFileName] =
gradleFileContents[dotGradleFileName].replaceAll(
variableDefinitionMatchStringForFileName(versionVariable.key, dotGradleFileName),
newVariableDefinitionString(versionVariable.value))
}
}
}

String variableDefinitionMatchStringForFileName(String variable, String fileName) {
String splitter = File.separator.replace('\\', '\\\\')
if (fileName.split(splitter).last() == 'gradle.properties') {
return gradlePropertiesVariableDefinitionMatchString(variable)
}
variableDefinitionMatchString(variable)
versionVariables
}

void saveDependencyUpdatesReport(File dependencyUpdatesJsonReportFile) {
Expand Down
19 changes: 19 additions & 0 deletions src/test/groovy/se/patrikerdes/BaseFunctionalTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import spock.lang.Specification

class BaseFunctionalTest extends Specification {

// Latest Ben Manes Versions plugin supports only gradle >= 5.0
// and after update useLatestVersionsCheck() fails when gradle < 5.0
protected static final String BLACKLIST_VERSION_PLUGIN_UPDATE = """
useLatestVersions {
updateBlacklist = ['com.github.ben-manes.versions']
}
useLatestVersionsCheck {
updateBlacklist = ['com.github.ben-manes.versions']
}
"""

@Rule
protected final TemporaryFolder testProjectDir = new TemporaryFolder()
protected File buildFile
Expand Down Expand Up @@ -63,6 +74,14 @@ class BaseFunctionalTest extends Specification {
.buildAndFail()
}

BuildResult useLatestVersionsUpdatingRootProperties() {
GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments('useLatestVersions', '--update-root-properties')
.withPluginClasspath()
.build()
}

BuildResult useLatestVersions(String gradleVersion) {
GradleRunner.create()
.withProjectDir(testProjectDir.root)
Expand Down
Loading

0 comments on commit a13cae6

Please sign in to comment.