diff --git a/CHANGELOG.md b/CHANGELOG.md index a4b08236..9cd2ef10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Version 1.8 * [#127](https://github.com/gradlex-org/java-module-dependencies/issues/127) Less configuration cache misses when modifying `module-info.java` (Thanks [TheGoesen](https://github.com/TheGoesen)) +* [#128](https://github.com/gradlex-org/java-module-dependencies/issues/128) Less configuration cache misses when using Settings plugin (Thanks [TheGoesen](https://github.com/TheGoesen)) ## Version 1.7.1 * Update module name mappings diff --git a/README.MD b/README.MD index eb1f4f32..b068540d 100644 --- a/README.MD +++ b/README.MD @@ -119,6 +119,10 @@ javaModules { // use instead of 'include(...)' group = "org.example" // group for all Modules plugin("java-library") // apply plugin to all Modules' subprojects module("app") { ... } // individualise Module (only if needed) + + // To optimze Configuration Cache hits: + exclusions.add("_.*") // do not inspect certain folders (regex) + requiresBuildFile // only look at folder containing a build.gradle(.kts) } versions("gradle/versions") // subproject configured as Platform Project diff --git a/src/main/java/org/gradlex/javamodule/dependencies/initialization/Directory.java b/src/main/java/org/gradlex/javamodule/dependencies/initialization/Directory.java index 9160e6ed..2f23cfd6 100644 --- a/src/main/java/org/gradlex/javamodule/dependencies/initialization/Directory.java +++ b/src/main/java/org/gradlex/javamodule/dependencies/initialization/Directory.java @@ -20,9 +20,11 @@ import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import javax.inject.Inject; import java.io.File; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; @@ -41,14 +43,17 @@ public abstract class Directory { */ public abstract ListProperty getPlugins(); - @Inject - protected abstract ObjectFactory getObjects(); @Inject public Directory(File root) { this.root = root; + getExclusions().convention(Arrays.asList("build", "\\..*")); + getRequiresBuildFile().convention(false); } + @Inject + protected abstract ObjectFactory getObjects(); + /** * {@link Module#plugin(String)} */ @@ -81,4 +86,19 @@ Module addModule(String subDirectory) { module.getPlugins().addAll(getPlugins()); return module; } + + /** + * Configure which folders should be ignored when searching for Modules. + * This can be tweaked to optimize the configuration cache hit ratio. + * Defaults to: 'build', '.*' + */ + public abstract ListProperty getExclusions(); + + /** + * Configure if only folders that contain a 'build.gradle' or 'build.gradle.kts' + * should be considered when searching for Modules. + * Setting this to true may improve configuration cache hit ratio if you know + * that all modules have build files in addition to the 'module-info.java' files. + */ + public abstract Property getRequiresBuildFile(); } diff --git a/src/main/java/org/gradlex/javamodule/dependencies/initialization/JavaModulesExtension.java b/src/main/java/org/gradlex/javamodule/dependencies/initialization/JavaModulesExtension.java index e0a6c94b..576d0379 100644 --- a/src/main/java/org/gradlex/javamodule/dependencies/initialization/JavaModulesExtension.java +++ b/src/main/java/org/gradlex/javamodule/dependencies/initialization/JavaModulesExtension.java @@ -27,11 +27,14 @@ import org.gradle.api.plugins.JavaApplication; import org.gradle.api.plugins.JavaPlatformExtension; import org.gradle.api.plugins.JavaPlatformPlugin; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; import org.gradlex.javamodule.dependencies.JavaModuleDependenciesExtension; import org.gradlex.javamodule.dependencies.JavaModuleDependenciesPlugin; import org.gradlex.javamodule.dependencies.JavaModuleVersionsPlugin; import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfo; import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfoCache; +import org.gradlex.javamodule.dependencies.internal.utils.ValueModuleDirectoryListing; import javax.annotation.Nullable; import javax.inject.Inject; @@ -47,6 +50,9 @@ public abstract class JavaModulesExtension { @Inject public abstract ObjectFactory getObjects(); + @Inject + public abstract ProviderFactory getProviders(); + @Inject public JavaModulesExtension(Settings settings) { this.settings = settings; @@ -85,22 +91,21 @@ public void directory(String directory, Action action) { Directory moduleDirectory = getObjects().newInstance(Directory.class, modulesDirectory); action.execute(moduleDirectory); - File[] projectDirs = modulesDirectory.listFiles(); - if (projectDirs == null) { - throw new RuntimeException("Failed to inspect: " + modulesDirectory); - } - for (Module module : moduleDirectory.customizedModules.values()) { includeModule(module, new File(modulesDirectory, module.getDirectory().get())); } - - for (File projectDir : projectDirs) { - if (!moduleDirectory.customizedModules.containsKey(projectDir.getName())) { - Module module = moduleDirectory.addModule(projectDir.getName()); - if (!module.getModuleInfoPaths().get().isEmpty()) { - // only auto-include if there is at least one module-info.java - includeModule(module, projectDir); - } + Provider> listProvider = getProviders().of(ValueModuleDirectoryListing.class, spec -> { + spec.getParameters().getExclusions().set(moduleDirectory.getExclusions()); + spec.getParameters().getExplicitlyConfiguredFolders().set(moduleDirectory.customizedModules.keySet()); + spec.getParameters().getDir().set(modulesDirectory); + spec.getParameters().getRequiresBuildFile().set(moduleDirectory.getRequiresBuildFile()); + }); + + for (String projectDir : listProvider.get()) { + Module module = moduleDirectory.addModule(projectDir); + if (!module.getModuleInfoPaths().get().isEmpty()) { + // only auto-include if there is at least one module-info.java + includeModule(module, new File(modulesDirectory, projectDir)); } } } diff --git a/src/main/java/org/gradlex/javamodule/dependencies/internal/utils/ValueModuleDirectoryListing.java b/src/main/java/org/gradlex/javamodule/dependencies/internal/utils/ValueModuleDirectoryListing.java new file mode 100644 index 00000000..e8422543 --- /dev/null +++ b/src/main/java/org/gradlex/javamodule/dependencies/internal/utils/ValueModuleDirectoryListing.java @@ -0,0 +1,64 @@ +/* + * Copyright the GradleX team. + * + * 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 + * + * http://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 org.gradlex.javamodule.dependencies.internal.utils; + +import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; +import org.gradle.api.provider.ValueSource; +import org.gradle.api.provider.ValueSourceParameters; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class ValueModuleDirectoryListing implements ValueSource, ValueModuleDirectoryListing.Parameter> { + + public interface Parameter extends ValueSourceParameters { + Property getDir(); + SetProperty getExplicitlyConfiguredFolders(); + SetProperty getExclusions(); + Property getRequiresBuildFile(); + } + + @Override + public List obtain() { + Path path = getParameters().getDir().get().toPath(); + try (Stream directoryStream = Files.find(path, 1, (unused, basicFileAttributes) -> basicFileAttributes.isDirectory())) { + return directoryStream + .filter(x -> !getParameters().getExplicitlyConfiguredFolders().get().contains(x.getFileName().toString())) + .filter(x -> getParameters().getExclusions().get().stream().noneMatch(r -> x.getFileName().toString().matches(r))) + .filter(x -> checkBuildFile(x, getParameters())) + .map(x -> x.getFileName().toString()) + .sorted() + .collect(Collectors.toList()); + + } catch (IOException e) { + throw new RuntimeException("Failed to inspect: " + path, e); + } + } + + private boolean checkBuildFile(Path x, Parameter parameters) { + if (!parameters.getRequiresBuildFile().get()) { + return true; + } + return Files.isRegularFile(x.resolve("build.gradle.kts")) || Files.isRegularFile(x.resolve("build.gradle")); + } +} diff --git a/src/test/groovy/org/gradlex/javamodule/dependencies/test/initialization/SettingsPluginTest.groovy b/src/test/groovy/org/gradlex/javamodule/dependencies/test/initialization/SettingsPluginTest.groovy index 1f578d11..6f91d965 100644 --- a/src/test/groovy/org/gradlex/javamodule/dependencies/test/initialization/SettingsPluginTest.groovy +++ b/src/test/groovy/org/gradlex/javamodule/dependencies/test/initialization/SettingsPluginTest.groovy @@ -90,13 +90,72 @@ class SettingsPluginTest extends Specification { result.getOutput().contains("Calculating task graph as no cached configuration is available for tasks: :app:compileJava") when: - runner.build() // https://github.com/gradlex-org/java-module-dependencies/issues/128 result = runner.build() then: result.getOutput().contains("Reusing configuration cache.") } + def "configurationCacheHitExtraDir"() { + given: + settingsFile << ''' + javaModules { + directory(".") { plugin("java-library") } + } + ''' + libModuleInfoFile << 'module abc.lib { }' + appModuleInfoFile << ''' + module org.gradlex.test.app { + requires abc.lib; + } + ''' + + def runner = runner(':app:compileJava') + when: + def result = runner.build() + + then: + result.getOutput().contains("Calculating task graph as no cached configuration is available for tasks: :app:compileJava") + + when: + new File(settingsFile.parentFile, ".thisShallBeIgnored").mkdir() + + result = runner.build() + + then: + result.getOutput().contains("Reusing configuration cache.") + } + + def "configurationCacheHitExtraNotIgnored"() { + given: + settingsFile << ''' + javaModules { + directory(".") { plugin("java-library") } + } + ''' + libModuleInfoFile << 'module abc.lib { }' + appModuleInfoFile << ''' + module org.gradlex.test.app { + requires abc.lib; + } + ''' + + def runner = runner(':app:compileJava') + when: + def result = runner.build() + + then: + result.getOutput().contains("Calculating task graph as no cached configuration is available for tasks: :app:compileJava") + + when: + new File(settingsFile.parentFile, "thisShallNotBeIgnored").mkdir() + + result = runner.build() + + then: + result.getOutput().contains("Calculating task graph as configuration cache cannot be reused because a build logic input of type 'ValueModuleDirectoryListing' has changed.") + } + def "configurationCacheHitIrrelevantChange"() { given: settingsFile << ''' @@ -119,7 +178,6 @@ class SettingsPluginTest extends Specification { result.getOutput().contains("Calculating task graph as no cached configuration is available for tasks: :app:compileJava") when: - runner.build() // https://github.com/gradlex-org/java-module-dependencies/issues/128 appModuleInfoFile.write(''' module org.gradlex.test.app { requires abc.lib; //This is a comment and should not break the configurationCache @@ -153,7 +211,6 @@ class SettingsPluginTest extends Specification { result.getOutput().contains("Calculating task graph as no cached configuration is available for tasks: :app:compileJava") when: - runner.build() // https://github.com/gradlex-org/java-module-dependencies/issues/128 appModuleInfoFile.write(''' module org.gradlex.test.app { //dependency removed; so thats indeed a configuration change