Skip to content

Commit

Permalink
Set plugin versions (#32 implements #10)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Jan 28, 2023
2 parents c84b70a + 96c8113 commit be81aca
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- New `BlowdryerSetup.setPluginsBlockTo` for setting plugin versions ([docs](https://github.com/diffplug/blowdryer#plugin-versions)). ([#32](https://github.com/diffplug/blowdryer/pull/32) implements [#10](https://github.com/diffplug/blowdryer/issues/10))
### Fixed
- Fix `BlowdryerSetup.localJar` on Windows. ([#31](https://github.com/diffplug/blowdryer/pull/31))

Expand Down
70 changes: 66 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,7 @@ If the property isn't set, you'll get a nice error message describing what was m

#### Script plugin gotchas

Script plugins can't `import` any classes that were loaded from a third-party plugin on the `build.gradle` classpath<sup>[1](#myfootnote1)</sup>. There is an easy workaround described in [#10](https://github.com/diffplug/blowdryer/issues/10), along with our long-term plans for a fix.

<a name="myfootnote1"><sup>1</sup></a> see [gradle/gradle#4007](https://github.com/gradle/gradle/issues/4007) and [gradle/gradle#1262](https://github.com/gradle/gradle/issues/1262) for history and details
Script plugins can't `import` any classes that were loaded from a third-party plugin on the `build.gradle` classpath. There is an easy workaround, which is to declare all plugins and their versions in the `settings.gradle` file. Blowdryer includes a mechanism for centralizing plugins and their versions, see [plugin versions](#plugin-versions) below.

## Dev workflow

Expand Down Expand Up @@ -160,6 +158,69 @@ blowdryerSetup {

To pull this jar from a maven repository, see [#21](https://github.com/diffplug/blowdryer/issues/21).

## Plugin versions

We recommend that your `settings.gradle` should look like this:

```gradle
plugins {
id 'com.diffplug.blowdryerSetup' version '1.6.0'
id 'acme.java' version '1.0.0' apply false
id 'acme.kotlin' version '2.0.0' apply false
}
blowdryerSetup {
github('acme/blowdryer-acme', 'tag', 'v1.4.5')
setPluginsBlockTo {
file('plugin.versions')
}
}
```

First note that every plugin has `apply false` except for `com.diffplug.blowdryerSetup`. That is on purpose. We need to apply `blowdryerSetup` so that we can use the `blowdryerSetup {}` block, and we need to do `apply false` on the other plugins because we're just putting them on the classpath, not actually using them (yet).

The second thing to note is `setPluginsBlockTo { file('plugin.versions') }`. That means that if you go to `github.com/acme/blowdryer-acme` and then open the `v1.4.5` tab and then go into the `src/main/resources` folder, you will find a file called `plugin.versions`. And the content of that file will be

```gradle
id 'com.diffplug.blowdryerSetup' version '1.6.0'
id 'acme.java' version '1.0.0' apply false
id 'acme.kotlin' version '2.0.0' apply false
```

Blowdryer is using the same immutable file mechanism described earlier, but this time it's using it to set just that one section of your `settings.gradle` using a workflow very similar to the [`spotlessCheck` / `spotlessApply` idea](https://github.com/diffplug/spotless/blob/main/plugin-gradle/README.md).

### Updating plugin versions

The workflow goes like this:

1. Enter `devLocal` mode (demonstrated [above](#dev-workflow))
2. Update the `plugin.versions` file
3. When you try to run your build, you will get an error
- > settings.gradle plugins block has the wrong content. Add -DsetPluginVersions to overwrite
4. Add `-DsetPluginVersions` to your command line
5. You'll get another error
- > settings.gradle plugins block was written successfully. Plugin versions have been updated, try again.
6. Now the plugins block will be up-to-date and your next build will succeed

### Tweaking the `plugin.versions`

It doesn't *have* to be called `plugin.versions`, it's just using the `干.file` mechanism and sticking that file in. So you could have `plugin-java.versions` and `plugin-kotlin.versions`. Also, you have other methods you can call:

```gradle
setPluginsBlockTo {
file('plugin.versions')
file('kotlin-extras.versions')
add(" id 'special-plugin-for-just-this-project' version '1.0.0'")
remove(" id 'acme.java' version '1.0.0' apply false")
replace('1.7.20', '1.8.0') // update Kotlin version but only for this build
}
```

### Compared to version catalogs

Recent versions of Gradle shipped a flexible [version catalog](https://docs.gradle.org/current/userguide/platforms.html) feature. You can use that in combination with blowdryer's `setPluginsBlockTo`. The problem is that every plugin you use throughout the build still has to be declared in the `settings.gradle` with `apply false`. Just having the version in the catalog isn't enough. See [script plugin gotchas](#script-plugin-gotchas) above for the gory classloader details.

Disappointingly, you can't use `libs.versions.toml` inside the `settings.gradle` file, which is exactly the place that we need it.

## API Reference

You have to apply the `com.diffplug.blowdryerSetup` plugin in your `settings.gradle`. But you don't actually have to `apply plugin: 'com.diffplug.blowdryer'` in your `build.gradle`, you can also just use these static methods (even in `settings.gradle` or inside the code of other plugins).
Expand All @@ -176,8 +237,9 @@ static File 干.immutableUrl(String guaranteedImmutableUrl, String fileSuffix)
// 干.immutableUrl('https://foo.org/?file=blah.foo&rev=7', '.foo') returns a file which ends in `.foo`
```

- [javadoc `BlowdryerSetup`](https://javadoc.io/static/com.diffplug/blowdryer/1.6.0/com/diffplug/blowdryer/BlowdryerSetup.html)
- [javadoc `Blowdryer`](https://javadoc.io/static/com.diffplug/blowdryer/1.6.0/com/diffplug/blowdryer/Blowdryer.html)
- [javadoc `BlowdryerSetup`](https://javadoc.io/static/com.diffplug/blowdryer/1.6.0/com/diffplug/blowdryer/BlowdryerSetup.html)
- [javadoc `BlowdryerSetup.PluginsBlock`](https://javadoc.io/static/com.diffplug/blowdryer/1.6.0/com/diffplug/blowdryer/BlowdryerSetup.html)

If you do `apply plugin: 'com.diffplug.blowdryer'` then every project gets an extension object ([code](https://github.com/diffplug/blowdryer/blob/master/src/main/java/com/diffplug/blowdryer/BlowdryerPlugin.java)) where the project field has been filled in for you, which is why we don't pass it explicitly in the examples before this section. If you don't apply the plugin, you can still call these static methods and pass `project` explicitly for the `proj()` methods.

Expand Down
78 changes: 74 additions & 4 deletions src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
import com.google.gson.Gson;
import groovy.lang.Closure;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
Expand All @@ -35,6 +37,8 @@
import okhttp3.Request.Builder;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.jetbrains.annotations.NotNull;

/** Configures where {@link Blowdryer#file(String)} downloads files from. */
Expand All @@ -48,12 +52,12 @@ public class BlowdryerSetup {
private static final String HTTP_PROTOCOL = "http://";
private static final String HTTPS_PROTOCOL = "https://";

private final File referenceDirectory;
private final File rootDir;

/** Pass in the directory that will be used to resolve string arguments to devLocal. */
public BlowdryerSetup(File referenceDirectory) {
public BlowdryerSetup(File rootDir) {
Blowdryer.setResourcePluginNull(); // because of gradle daemon
this.referenceDirectory = referenceDirectory;
this.rootDir = rootDir;
}

private static final String REPO_SUBFOLDER_DEFAULT = "src/main/resources";
Expand Down Expand Up @@ -381,7 +385,7 @@ public void devLocal(Object devPath) {
if (devPath instanceof File) {
devPathFile = (File) devPath;
} else if (devPath instanceof String) {
devPathFile = new File(referenceDirectory, (String) devPath);
devPathFile = new File(rootDir, (String) devPath);
} else {
throw new IllegalArgumentException("Expected a String or File, was a " + devPath.getClass());
}
Expand Down Expand Up @@ -412,4 +416,70 @@ private static String encodeUrlPart(String part) {
}
}

//////////////////////////////////////////////////
// set the plugins block inside settings.gradle //
//////////////////////////////////////////////////
public static class PluginsBlock {
private StringBuilder totalContent = new StringBuilder();

public void file(String file) throws IOException {
add(readFile(Blowdryer.file(file)));
}

public void add(String line) {
totalContent.append(line.replace("\r", ""));
if (!line.endsWith("\n")) {
totalContent.append('\n');
}
}

public void remove(String line) {
replace(line + "\n", "");
}

public void replace(String in, String out) {
String current = totalContent.toString();
String replaced = current.replace(in.replace("\r", ""), out);
if (current.equals(replaced)) {
throw new IllegalArgumentException("Doesn't contain " + in + "\n\n" + current);
}
totalContent.setLength(0);
totalContent.append(replaced);
}

String desiredContent() {
String content = totalContent.toString();
while (content.endsWith("\n")) {
content = content.substring(0, content.length() - 1);
}
return content;
}
}

public void setPluginsBlockTo(Action<PluginsBlock> versionSetter) throws IOException {
File settingsDotGradle = new File(rootDir, "settings.gradle");
PluginsBlockParsed parsed = new PluginsBlockParsed(readFile(settingsDotGradle));
PluginsBlock versions = new PluginsBlock();
versionSetter.execute(versions);
if (parsed.inPlugins.equals(versions.desiredContent())) {
return;
}
if (System.getProperty("setPluginVersions") != null) {
parsed.setPluginContent(versions.desiredContent());
Files.write(settingsDotGradle.toPath(), parsed.contentCorrectEndings().getBytes());
throw new GradleException("settings.gradle plugins block was written successfully. Plugin versions have been updated, try again.");
} else if (System.getProperty("ignorePluginVersions") != null) {
System.err.println("wrong plugins in settings.gradle, ignoring because of -DignorePluginVersions");
} else {
throw new GradleException("settings.gradle plugins block has the wrong content.\n" +
" Add -DsetPluginVersions to overwrite\n" +
" Add -DignorePluginVersions to ignore\n" +
" https://github.com/diffplug/blowdryer#plugin-versions for more info.\n\n" + "" +
"DESIRED:\n" + versions.desiredContent() + "\n\nACTUAL:\n" + parsed.inPlugins);
}
}

private static String readFile(File file) throws IOException {
return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
}
}
63 changes: 63 additions & 0 deletions src/main/java/com/diffplug/blowdryer/PluginsBlockParsed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 DiffPlug
*
* 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
*
* https://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.diffplug.blowdryer;

public class PluginsBlockParsed {
boolean isWindowsNewline;
String beforePlugins;
String inPlugins;
String afterPlugins;

private static final String PLUGINS_OPEN = "\nplugins {\n";
private static final String PLUGINS_CLOSE = "\n}\n";

private static String escape(String input) {
return input.replace("\n", "⏎");
}

PluginsBlockParsed(String dirty) {
isWindowsNewline = dirty.indexOf("\r\n") != -1;
String unix = isWindowsNewline ? dirty.replace("\r\n", "\n") : dirty;

int pluginsStart = unix.indexOf(PLUGINS_OPEN);
if (pluginsStart == -1) {
throw new IllegalArgumentException("Couldn't find " + escape(PLUGINS_OPEN));
}
beforePlugins = unix.substring(0, pluginsStart);
int pluginsEnd = unix.indexOf("\n}\n", pluginsStart + PLUGINS_OPEN.length());
if (pluginsEnd == -1) {
throw new IllegalArgumentException("Couldn't find " + escape(PLUGINS_CLOSE) + " after " + escape(PLUGINS_OPEN));
}
inPlugins = unix.substring(pluginsStart + PLUGINS_OPEN.length(), pluginsEnd);
afterPlugins = unix.substring(pluginsEnd + PLUGINS_CLOSE.length());
}

public String inPluginsUnix() {
return inPlugins;
}

public String contentUnix() {
return beforePlugins + PLUGINS_OPEN + inPlugins + PLUGINS_CLOSE + afterPlugins;
}

public String contentCorrectEndings() {
return isWindowsNewline ? contentUnix().replace("\n", "\r\n") : contentUnix();
}

public void setPluginContent(String desiredContent) {
inPlugins = desiredContent;
}
}
99 changes: 99 additions & 0 deletions src/test/java/com/diffplug/blowdryer/PluginsBlockParsedTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (C) 2023 DiffPlug
*
* 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
*
* https://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.diffplug.blowdryer;

import java.io.IOException;
import org.assertj.core.api.Assertions;
import org.junit.Test;

public class PluginsBlockParsedTest extends ResourceHarness {
private static String content(String insidePlugins) {
return "pluginManagement {\r\n" +
" repositories {\r\n" +
" mavenCentral()\r\n" +
" gradlePluginPortal()\r\n" +
" }\r\n" +
"}\r\n" +
"plugins {\r\n" +
insidePlugins +
"}\r\n" +
"rootProject.name = 'blowdryer'\r\n";
}

@Test
public void parser() {
String insidePlugins = " // https://plugins.gradle.org/plugin/com.gradle.plugin-publish\r\n" +
" id 'com.gradle.plugin-publish' version '0.20.0' apply false\r\n" +
" // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md\r\n" +
" id 'dev.equo.ide' version '0.12.1' apply false\r\n" +
" // https://github.com/gradle-nexus/publish-plugin/releases\r\n" +
" id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false\r\n";
String input = content(insidePlugins);
PluginsBlockParsed parsed = new PluginsBlockParsed(input);
Assertions.assertThat(parsed.contentCorrectEndings()).isEqualTo(input);
Assertions.assertThat(parsed.beforePlugins).isEqualTo("pluginManagement {\n" +
" repositories {\n" +
" mavenCentral()\n" +
" gradlePluginPortal()\n" +
" }\n" +
"}");
Assertions.assertThat(parsed.inPlugins).isEqualTo(" // https://plugins.gradle.org/plugin/com.gradle.plugin-publish\n" +
" id 'com.gradle.plugin-publish' version '0.20.0' apply false\n" +
" // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md\n" +
" id 'dev.equo.ide' version '0.12.1' apply false\n" +
" // https://github.com/gradle-nexus/publish-plugin/releases\n" +
" id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false");
Assertions.assertThat(parsed.afterPlugins).isEqualTo("rootProject.name = 'blowdryer'\n");
}

@Test
public void test() throws IOException {
String insidePlugins = " // https://plugins.gradle.org/plugin/com.gradle.plugin-publish\r\n" +
" id 'com.gradle.plugin-publish' version '0.20.0' apply false\r\n" +
" // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md\r\n" +
" id 'dev.equo.ide' version '0.12.1' apply false\r\n" +
" // https://github.com/gradle-nexus/publish-plugin/releases\r\n" +
" id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false\r\n";
String input = content(insidePlugins);
write("settings.gradle", input);

BlowdryerSetup setup = new BlowdryerSetup(rootFolder());

// this should succeed
setup.setPluginsBlockTo(pluginVersions -> {
pluginVersions.add(insidePlugins);
});

// this should fail
Assertions.assertThatThrownBy(() -> setup.setPluginsBlockTo(pluginVersions -> {
pluginVersions.add("TEST");
})).hasMessage("settings.gradle plugins block has the wrong content.\n" +
" Add -DsetPluginVersions to overwrite\n" +
" Add -DignorePluginVersions to ignore\n" +
" https://github.com/diffplug/blowdryer#plugin-versions for more info.\n" +
"\n" +
"DESIRED:\n" +
"TEST\n" +
"\n" +
"ACTUAL:\n" +
" // https://plugins.gradle.org/plugin/com.gradle.plugin-publish\n" +
" id 'com.gradle.plugin-publish' version '0.20.0' apply false\n" +
" // https://github.com/equodev/equo-ide/blob/main/plugin-gradle/CHANGELOG.md\n" +
" id 'dev.equo.ide' version '0.12.1' apply false\n" +
" // https://github.com/gradle-nexus/publish-plugin/releases\n" +
" id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' apply false");
}
}

0 comments on commit be81aca

Please sign in to comment.