From 5479ae1acc14924e52c5644b1ace4b9e23fff18f Mon Sep 17 00:00:00 2001 From: Konstantin Krivopustov Date: Thu, 27 Aug 2026 10:48:06 +0400 Subject: [PATCH] cleanConf Gradle task deletes the wrong directory in projects with non-standard layout --- .../groovy/io/jmix/gradle/JmixPlugin.groovy | 33 +++- .../gradle/CleanConfFunctionalTest.groovy | 144 ++++++++++++++++++ .../modules/backend/app/build.gradle | 11 ++ .../src/main/resources/application.properties | 1 + .../settings.gradle | 6 + 5 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 jmix-gradle-plugin/src/test/groovy/io/jmix/gradle/CleanConfFunctionalTest.groovy create mode 100644 jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/modules/backend/app/build.gradle create mode 100644 jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/modules/backend/app/src/main/resources/application.properties create mode 100644 jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/settings.gradle diff --git a/jmix-gradle-plugin/src/main/groovy/io/jmix/gradle/JmixPlugin.groovy b/jmix-gradle-plugin/src/main/groovy/io/jmix/gradle/JmixPlugin.groovy index 6bbee36a4a..ca4f5ba613 100644 --- a/jmix-gradle-plugin/src/main/groovy/io/jmix/gradle/JmixPlugin.groovy +++ b/jmix-gradle-plugin/src/main/groovy/io/jmix/gradle/JmixPlugin.groovy @@ -27,6 +27,8 @@ import java.util.jar.Manifest class JmixPlugin implements Plugin { + public static final String DEFAULT_CONF_DIR = '.jmix/conf' + public static final String PROVIDED_RUNTIME_CONFIGURATION_NAME = 'providedRuntime' public static final String PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME = 'productionRuntimeClasspath' @@ -314,7 +316,7 @@ class JmixPlugin implements Plugin { def confDir = resolveConfDir(project, mainProperties) project.logger.lifecycle("Delete directory: {}", confDir) - delete "${confDir}" + delete confDir } else { project.logger.lifecycle("Resource directory not found") return @@ -396,7 +398,15 @@ class JmixPlugin implements Plugin { } } - private static String resolveConfDir(Project project, Properties mainProperties) { + /** + * Resolves the directory used by the application as 'jmix.core.conf-dir' at runtime. + * + * The runtime resolves this property against its working directory, which is the project directory + * both for the Gradle 'bootRun' task and for Studio run configurations. So relative paths and the + * '${user.dir}' placeholder are resolved against the project directory, not the build root directory: + * these are different directories when the application is a subproject with a custom 'projectDir'. + */ + private static File resolveConfDir(Project project, Properties mainProperties) { def profilesList = resolveActiveProfiles(project, mainProperties) def confDir = null @@ -404,7 +414,7 @@ class JmixPlugin implements Plugin { for (def profileName : profilesList) { project.logger.lifecycle("Check profile: {}", profileName) def profileProperties = loadProperties(project, profileName) - confDir = profileProperties.getProperty("jmix.core.conf-dir") ?: profileProperties.getProperty("jmix.core.confDir") ?: null + confDir = getConfDirProperty(profileProperties) if (confDir != null) { break } @@ -412,10 +422,23 @@ class JmixPlugin implements Plugin { } if (confDir == null) { - confDir = mainProperties.getProperty("jmix.core.conf-dir") ?: mainProperties.getProperty("jmix.core.confDir") ?: "${project.rootDir}/.jmix/conf" + confDir = getConfDirProperty(mainProperties) ?: DEFAULT_CONF_DIR } - return confDir + return project.file(expandPathPlaceholders(project, confDir)) + } + + private static String getConfDirProperty(Properties properties) { + return properties.getProperty("jmix.core.conf-dir") ?: properties.getProperty("jmix.core.confDir") ?: null + } + + /** + * Replaces the placeholders that the running application resolves in path properties. + * '${user.dir}' becomes the project directory because that is the working directory of the application. + */ + private static String expandPathPlaceholders(Project project, String path) { + return path.replace('${user.dir}', project.projectDir.absolutePath) + .replace('${user.home}', System.getProperty('user.home')) } private static List resolveActiveProfiles(Project project, Properties mainProperties) { diff --git a/jmix-gradle-plugin/src/test/groovy/io/jmix/gradle/CleanConfFunctionalTest.groovy b/jmix-gradle-plugin/src/test/groovy/io/jmix/gradle/CleanConfFunctionalTest.groovy new file mode 100644 index 0000000000..53b2713369 --- /dev/null +++ b/jmix-gradle-plugin/src/test/groovy/io/jmix/gradle/CleanConfFunctionalTest.groovy @@ -0,0 +1,144 @@ +/* + * Copyright 2026 Haulmont. + * + * 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 io.jmix.gradle + +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption + +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertTrue + +/** + * The 'cleanConf' task must delete the directory that the application actually uses at runtime. + * The runtime resolves 'jmix.core.conf-dir' against its working directory, which for 'bootRun' + * is the application module dir - not the build root dir. + */ +class CleanConfFunctionalTest { + + private static final String APP_MODULE_PATH = 'modules/backend/app' + + @TempDir + Path testProjectDir + + @Test + @Tag('slowTests') + void defaultConfDirIsResolvedAgainstApplicationModule() { + copyFixture('nonstandard-layout-conf-dir', testProjectDir) + + Path rootConfDir = createConfDir('.jmix/conf') + Path appConfDir = createAppConfDir('.jmix/conf') + + runCleanConf() + + assertFalse(Files.exists(appConfDir), "conf dir of the application module must be deleted: ${appConfDir}") + assertTrue(Files.exists(rootConfDir), "conf dir of the build root must be left alone: ${rootConfDir}") + } + + @Test + @Tag('slowTests') + void relativeConfDirPropertyIsResolvedAgainstApplicationModule() { + copyFixture('nonstandard-layout-conf-dir', testProjectDir) + appendAppProperty('jmix.core.conf-dir = custom-conf') + + Path rootConfDir = createConfDir('custom-conf') + Path appConfDir = createAppConfDir('custom-conf') + + runCleanConf() + + assertFalse(Files.exists(appConfDir), "conf dir of the application module must be deleted: ${appConfDir}") + assertTrue(Files.exists(rootConfDir), "conf dir of the build root must be left alone: ${rootConfDir}") + } + + @Test + @Tag('slowTests') + void userDirPlaceholderInConfDirPropertyIsExpanded() { + copyFixture('nonstandard-layout-conf-dir', testProjectDir) + appendAppProperty('jmix.core.conf-dir = ${user.dir}/.jmix/conf') + + Path rootConfDir = createConfDir('.jmix/conf') + Path appConfDir = createAppConfDir('.jmix/conf') + + runCleanConf() + + assertFalse(Files.exists(appConfDir), "conf dir of the application module must be deleted: ${appConfDir}") + assertTrue(Files.exists(rootConfDir), "conf dir of the build root must be left alone: ${rootConfDir}") + } + + @Test + @Tag('slowTests') + void absoluteConfDirPropertyIsUsedAsIs() { + copyFixture('nonstandard-layout-conf-dir', testProjectDir) + + Path absoluteConfDir = Files.createDirectories(testProjectDir.resolve('outside/conf')) + Files.writeString(absoluteConfDir.resolve('marker.txt'), 'marker') + appendAppProperty("jmix.core.conf-dir = ${absoluteConfDir.toString().replace('\\', '/')}") + + Path appConfDir = createAppConfDir('.jmix/conf') + + runCleanConf() + + assertFalse(Files.exists(absoluteConfDir), "conf dir given as an absolute path must be deleted: ${absoluteConfDir}") + assertTrue(Files.exists(appConfDir), "default conf dir must be left alone when the property is set: ${appConfDir}") + } + + private void runCleanConf() { + GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments(':app:cleanConf', '--stacktrace') + .withPluginClasspath() + .forwardOutput() + .build() + } + + private Path createConfDir(String relativePath) { + return createMarkedDir(testProjectDir.resolve(relativePath)) + } + + private Path createAppConfDir(String relativePath) { + return createMarkedDir(testProjectDir.resolve(APP_MODULE_PATH).resolve(relativePath)) + } + + private static Path createMarkedDir(Path dir) { + Files.createDirectories(dir) + Files.writeString(dir.resolve('marker.txt'), 'marker') + return dir + } + + private void appendAppProperty(String line) { + Path propertiesFile = testProjectDir.resolve("${APP_MODULE_PATH}/src/main/resources/application.properties") + Files.writeString(propertiesFile, "${Files.readString(propertiesFile)}\n${line}\n") + } + + private static void copyFixture(String name, Path target) { + Path source = Path.of(CleanConfFunctionalTest.getResource("/fixtures/${name}").toURI()) + + Files.walk(source).forEach { sourcePath -> + Path targetPath = target.resolve(source.relativize(sourcePath).toString()) + if (Files.isDirectory(sourcePath)) { + Files.createDirectories(targetPath) + } else { + Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING) + } + } + } +} diff --git a/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/modules/backend/app/build.gradle b/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/modules/backend/app/build.gradle new file mode 100644 index 0000000000..3241f8f140 --- /dev/null +++ b/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/modules/backend/app/build.gradle @@ -0,0 +1,11 @@ +plugins { + id 'io.jmix' + id 'java' +} + +jmix { + useBom = false + entitiesEnhancing { + enabled = false + } +} diff --git a/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/modules/backend/app/src/main/resources/application.properties b/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/modules/backend/app/src/main/resources/application.properties new file mode 100644 index 0000000000..ef7d1f5e6e --- /dev/null +++ b/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/modules/backend/app/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name = app diff --git a/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/settings.gradle b/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/settings.gradle new file mode 100644 index 0000000000..cd7b192a62 --- /dev/null +++ b/jmix-gradle-plugin/src/test/resources/fixtures/nonstandard-layout-conf-dir/settings.gradle @@ -0,0 +1,6 @@ +// The Jmix application is a subproject of the root build with a custom projectDir, +// so the application module dir differs from the build root dir. +rootProject.name = 'jmix-gradle-plugin-conf-dir' + +include ':app' +project(':app').projectDir = file('modules/backend/app')