Skip to content

Commit e4d2918

Browse files
committed
Detect duplicated init plugins
1 parent 5d4a04e commit e4d2918

11 files changed

+354
-11
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group = 'dev.nokee.init'
9-
version = '0.8.0'
9+
version = '0.8.0' // Change version in NokeeInitPlugin as well
1010

1111
tasks.named('processResources', ProcessResources) { task ->
1212
task.from(file('nokee.init.gradle')) {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package dev.nokee.init
2+
3+
import dev.gradleplugins.integtests.fixtures.AbstractGradleSpecification
4+
5+
import static dev.nokee.init.fixtures.GradleRunnerUtils.configurePluginClasspathAsInitScriptDependencies
6+
7+
class NokeeInitPluginConflictResolutionFunctionalTest extends AbstractGradleSpecification {
8+
def setup() {
9+
executer = executer.withGradleUserHomeDirectory(file('user-home'))
10+
}
11+
12+
private File writePreviousNokeeInitScript(File initScriptFile) {
13+
initScriptFile << '''
14+
initscript {
15+
repositories {
16+
maven { url = 'https://repo.nokeedev.net/release' }
17+
}
18+
dependencies {
19+
classpath 'dev.nokee.init:init.nokee.dev:0.7.0'
20+
}
21+
}
22+
apply plugin: dev.nokee.init.NokeeInitPlugin
23+
'''
24+
return initScriptFile
25+
}
26+
27+
private File writeCurrentNokeeInitScript(File initScriptFile) {
28+
initScriptFile << configurePluginClasspathAsInitScriptDependencies() << '''
29+
apply plugin: dev.nokee.init.NokeeInitPlugin
30+
'''
31+
return initScriptFile
32+
}
33+
34+
def "disable plugin before build listener executes"() {
35+
writePreviousNokeeInitScript(file('user-home/init.d/nokee.init.gradle'))
36+
executer = executer.usingInitScript(writeCurrentNokeeInitScript(file('init.gradle')))
37+
38+
expect:
39+
def result = succeeds('nokee')
40+
result.output.contains("")
41+
}
42+
43+
def "disable plugin right away when duplication is detected during application"() {
44+
writeCurrentNokeeInitScript(file('user-home/init.d/nokee.init.gradle'))
45+
executer = executer.usingInitScript(writePreviousNokeeInitScript(file('init.gradle')))
46+
47+
expect:
48+
def result = succeeds('nokee')
49+
result.output.contains("WARNING: Another Nokee init plugin is already loaded, disabling duplicate plugin loading.")
50+
}
51+
52+
def "does not disable any plugin when no class loader duplication"() {
53+
writeCurrentNokeeInitScript(file('user-home/init.d/nokee.init.gradle'))
54+
executer = executer.usingInitScript(writeCurrentNokeeInitScript(file('init.gradle')))
55+
56+
expect:
57+
def result = succeeds('nokee')
58+
!result.output.contains("WARNING")
59+
}
60+
}

src/main/java/dev/nokee/init/NokeeInitPlugin.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
package dev.nokee.init;
22

3-
import dev.nokee.init.internal.DisableableBuildListener;
3+
import dev.nokee.init.internal.DisableAware;
4+
import dev.nokee.init.internal.DisableAwareBuildListener;
45
import dev.nokee.init.internal.NokeeInitBuildListener;
5-
import dev.nokee.init.internal.RegisterNokeeTaskAction;
66
import dev.nokee.init.internal.versions.DefaultGradleVersionProvider;
77
import dev.nokee.init.internal.versions.GradleVersionProvider;
8-
import dev.nokee.init.internal.wrapper.RegisterWrapperTaskEnhancementAction;
8+
import lombok.val;
99
import org.gradle.api.Plugin;
1010
import org.gradle.api.invocation.Gradle;
11+
import org.gradle.api.logging.Logger;
12+
import org.gradle.api.logging.Logging;
1113
import org.gradle.util.GradleVersion;
1214

1315
import javax.inject.Inject;
1416

15-
public class NokeeInitPlugin implements Plugin<Gradle> {
17+
import java.util.function.Predicate;
18+
19+
import static dev.nokee.init.internal.DuplicateNokeeInitPluginLoadsDetectionAction.disableDuplicateNokeeInitPluginLoads;
20+
import static java.util.function.Predicate.isEqual;
21+
22+
public class NokeeInitPlugin implements Plugin<Gradle>, DisableAware {
23+
private static final Logger LOGGER = Logging.getLogger(NokeeInitPlugin.class);
1624
private static final GradleVersion MINIMUM_GRADLE_SUPPORTED = GradleVersion.version("6.2.1");
1725
private final GradleVersionProvider gradleVersionProvider;
26+
private DisableAware delegate = null;
1827

1928
@Inject
2029
protected NokeeInitPlugin() {
@@ -29,7 +38,30 @@ protected NokeeInitPlugin() {
2938
@Override
3039
public void apply(Gradle gradle) {
3140
if (MINIMUM_GRADLE_SUPPORTED.compareTo(gradleVersionProvider.get()) <= 0) {
32-
gradle.addBuildListener(new DisableableBuildListener(new NokeeInitBuildListener()));
41+
if (gradle.getPlugins().stream().anyMatch(isEqual(this).negate().and(isNokeeInitPlugin()))) {
42+
LOGGER.warn("WARNING: Another Nokee init plugin is already loaded, disabling duplicate plugin loading.");
43+
return;
44+
}
45+
val nokeeBuildListener = new DisableAwareBuildListener(new NokeeInitBuildListener());
46+
delegate = nokeeBuildListener;
47+
gradle.getPlugins().all(disableDuplicateNokeeInitPluginLoads());
48+
49+
gradle.addBuildListener(nokeeBuildListener);
3350
}
3451
}
52+
53+
private static Predicate<Object> isNokeeInitPlugin() {
54+
return it -> it.getClass().getSimpleName().equals("NokeeInitPlugin");
55+
}
56+
57+
@Override
58+
public void disable() {
59+
if (delegate != null) {
60+
delegate.disable();
61+
}
62+
}
63+
64+
public String getVersion() {
65+
return "0.8.0";
66+
}
3567
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package dev.nokee.init.internal;
2+
3+
public interface DisableAware {
4+
void disable();
5+
}

src/main/java/dev/nokee/init/internal/DisableableBuildListener.java renamed to src/main/java/dev/nokee/init/internal/DisableAwareBuildListener.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
import org.gradle.api.initialization.Settings;
77
import org.gradle.api.invocation.Gradle;
88

9-
public final class DisableableBuildListener extends BuildAdapter {
9+
public final class DisableAwareBuildListener extends BuildAdapter implements DisableAware {
1010
private final BuildListener delegate;
1111
private boolean disabled = false;
1212

13-
public DisableableBuildListener(BuildListener delegate) {
13+
public DisableAwareBuildListener(BuildListener delegate) {
1414
this.delegate = delegate;
1515
}
1616

17+
@Override
1718
public void disable() {
1819
disabled = true;
1920
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package dev.nokee.init.internal;
2+
3+
import org.gradle.api.Action;
4+
import org.gradle.api.Plugin;
5+
import org.gradle.api.logging.Logger;
6+
import org.gradle.api.logging.Logging;
7+
8+
public final class DuplicateNokeeInitPluginLoadsDetectionAction implements Action<Plugin> {
9+
private final Callback callback;
10+
private NokeeInitPluginContext previousPlugin = null;
11+
12+
DuplicateNokeeInitPluginLoadsDetectionAction(Callback callback) {
13+
this.callback = callback;
14+
}
15+
16+
public static Action<Plugin> disableDuplicateNokeeInitPluginLoads() {
17+
return new DuplicateNokeeInitPluginLoadsDetectionAction(new ResolveConflict());
18+
}
19+
20+
@Override
21+
public void execute(Plugin currentPlugin) {
22+
if (isNokeeInitPlugin(currentPlugin)) {
23+
if (previousPlugin != null) {
24+
previousPlugin = callback.onDuplicateLoads(previousPlugin, new NokeeInitPluginContext(currentPlugin));
25+
} else {
26+
previousPlugin = new NokeeInitPluginContext(currentPlugin);
27+
}
28+
}
29+
}
30+
31+
private static boolean isNokeeInitPlugin(Plugin plugin) {
32+
return plugin.getClass().getSimpleName().equals("NokeeInitPlugin");
33+
}
34+
35+
interface Callback {
36+
NokeeInitPluginContext onDuplicateLoads(NokeeInitPluginContext first, NokeeInitPluginContext second);
37+
}
38+
39+
private static final class ResolveConflict implements Callback {
40+
private static final Logger LOGGER = Logging.getLogger(ResolveConflict.class);
41+
@Override
42+
public NokeeInitPluginContext onDuplicateLoads(NokeeInitPluginContext first, NokeeInitPluginContext second) {
43+
LOGGER.warn("WARNING: Multiple Nokee init plugin loaded, keeping one of them.");
44+
LOGGER.warn(" \\-> Learn more at https://github.com/nokeedev/init.nokee.dev#runtime-conflict");
45+
if (first.canDisable()) {
46+
if (second.canDisable()) {
47+
if (first.getVersion().compareTo(second.getVersion()) < 0) {
48+
LOGGER.debug("Disabling " + first + " because " + second + " is older.");
49+
first.disable();
50+
return second;
51+
} else {
52+
LOGGER.debug("Disabling " + second + " because " + first + " is older.");
53+
second.disable();
54+
return first;
55+
}
56+
} else {
57+
LOGGER.debug("Disabling " + first + " because " + second + " cannot be disabled.");
58+
first.disable();
59+
return second;
60+
}
61+
} else {
62+
if (second.canDisable()) {
63+
LOGGER.debug("Disabling " + second + " because " + first + " cannot be disabled.");
64+
second.disable();
65+
return first;
66+
} else {
67+
LOGGER.debug("Keeping both as none can be disabled.");
68+
return second;
69+
}
70+
}
71+
}
72+
}
73+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package dev.nokee.init.internal;
2+
3+
import lombok.EqualsAndHashCode;
4+
import lombok.val;
5+
import org.gradle.util.VersionNumber;
6+
7+
import java.lang.reflect.InvocationTargetException;
8+
import java.util.Optional;
9+
10+
import static org.gradle.util.VersionNumber.parse;
11+
12+
@EqualsAndHashCode
13+
public final class NokeeInitPluginContext {
14+
private final Object plugin;
15+
16+
// We use Object here because both plugin will be coming from different classloader so the type is irrelevant
17+
public NokeeInitPluginContext(Object plugin) {
18+
this.plugin = plugin;
19+
}
20+
21+
public boolean canDisable() {
22+
try {
23+
plugin.getClass().getMethod("disable");
24+
return true;
25+
} catch (NoSuchMethodException e) {
26+
return false;
27+
}
28+
}
29+
30+
public void disable() {
31+
try {
32+
val method = plugin.getClass().getMethod("disable");
33+
method.invoke(plugin);
34+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
35+
throw new RuntimeException(e);
36+
}
37+
}
38+
39+
public VersionNumber getVersion() {
40+
try {
41+
val method = plugin.getClass().getMethod("getVersion");
42+
return Optional.ofNullable((String) method.invoke(plugin)).map(VersionNumber::parse).orElse(parse("0.0.0"));
43+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
44+
return parse("0.0.0");
45+
}
46+
}
47+
48+
49+
@Override
50+
public String toString() {
51+
return "plugin '" + plugin.getClass().getCanonicalName() + "' version '" + getVersion() + "'";
52+
}
53+
}

src/test/groovy/dev/nokee/init/NokeeInitPluginTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.nokee.init;
22

33
import org.gradle.api.invocation.Gradle;
4+
import org.gradle.testfixtures.ProjectBuilder;
45
import org.gradle.util.GradleVersion;
56
import org.junit.jupiter.api.Test;
67
import org.mockito.Mockito;
@@ -10,7 +11,7 @@
1011
import static org.mockito.Mockito.verifyNoInteractions;
1112

1213
class NokeeInitPluginTest {
13-
private final Gradle gradle = Mockito.mock(Gradle.class);
14+
private final Gradle gradle = Mockito.spy(ProjectBuilder.builder().build().getGradle());
1415

1516
@Test
1617
void doNothingOnUnsupportedVersion() {

src/test/groovy/dev/nokee/init/internal/DisableableBuildListenerTest.java renamed to src/test/groovy/dev/nokee/init/internal/DisableAwareBuildListenerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
import static org.mockito.Mockito.*;
1313

14-
class DisableableBuildListenerTest {
14+
class DisableAwareBuildListenerTest {
1515
private final BuildListener delegate = mock(BuildListener.class);
16-
private final DisableableBuildListener subject = new DisableableBuildListener(delegate);
16+
private final DisableAwareBuildListener subject = new DisableAwareBuildListener(delegate);
1717

1818
@Nested
1919
class Enabled {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package dev.nokee.init.internal;
2+
3+
import lombok.val;
4+
import org.gradle.api.Action;
5+
import org.gradle.api.Plugin;
6+
import org.gradle.api.invocation.Gradle;
7+
import org.junit.jupiter.api.Test;
8+
import org.mockito.Mockito;
9+
10+
import static org.mockito.Mockito.verify;
11+
import static org.mockito.Mockito.verifyNoInteractions;
12+
13+
class DuplicateNokeeInitPluginLoadsDetectionActionTest {
14+
private final DuplicateNokeeInitPluginLoadsDetectionAction.Callback callback = Mockito.mock(DuplicateNokeeInitPluginLoadsDetectionAction.Callback.class);
15+
private final Action<Plugin> subject = new DuplicateNokeeInitPluginLoadsDetectionAction(callback);
16+
17+
@Test
18+
void doesNotTriggerCallbackWhenOnlyOnePluginIsLoaded() {
19+
subject.execute(new NokeeInitPlugin());
20+
verifyNoInteractions(callback);
21+
}
22+
23+
@Test
24+
void triggerCallbackWhenTwoPluginIsLoaded() {
25+
val previousPlugin = new NokeeInitPlugin();
26+
val currentPlugin = new NokeeInitPlugin();
27+
subject.execute(previousPlugin);
28+
subject.execute(currentPlugin);
29+
verify(callback).onDuplicateLoads(new NokeeInitPluginContext(previousPlugin), new NokeeInitPluginContext(currentPlugin));
30+
}
31+
32+
@Test
33+
void doesNotTriggerCallbackWhenPluginIsNotNokeeInitPlugin() {
34+
subject.execute(new UnrelatedPlugin());
35+
subject.execute(new NokeeInitPlugin());
36+
subject.execute(new UnrelatedPlugin());
37+
verifyNoInteractions(callback);
38+
}
39+
40+
private final class UnrelatedPlugin implements Plugin<Gradle> {
41+
@Override
42+
public void apply(Gradle target) {}
43+
}
44+
45+
private final class NokeeInitPlugin implements Plugin<Gradle> {
46+
@Override
47+
public void apply(Gradle target) {}
48+
}
49+
}

0 commit comments

Comments
 (0)