Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 28 additions & 19 deletions src/main/groovy/io/openliberty/tools/gradle/tasks/DevTask.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import io.openliberty.tools.gradle.utils.DevTaskHelper
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.internal.file.DefaultFilePropertyFactory
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
Expand Down Expand Up @@ -1271,14 +1270,26 @@ class DevTask extends AbstractFeatureTask {
void action() {
initializeDefaultValues();

SourceSet mainSourceSet = project.sourceSets.main;
SourceSet testSourceSet = project.sourceSets.test;
SourceSet mainSourceSet = project.sourceSets.findByName('main');
SourceSet testSourceSet = project.sourceSets.findByName('test');

sourceDirectory = mainSourceSet.java.srcDirs.iterator().next()
testSourceDirectory = testSourceSet.java.srcDirs.iterator().next()
DefaultFilePropertyFactory.DefaultDirectoryVar outputDirectory = mainSourceSet.java.classesDirectory;
DefaultFilePropertyFactory.DefaultDirectoryVar testOutputDirectory = testSourceSet.java.classesDirectory;
List<File> resourceDirs = mainSourceSet.resources.srcDirs.toList();
if (mainSourceSet != null) {
sourceDirectory = mainSourceSet.java.srcDirs.iterator().next()
} else {
sourceDirectory = new File(project.projectDir, "src/main/java")
Comment thread
cherylking marked this conversation as resolved.
}
if (testSourceSet != null) {
testSourceDirectory = testSourceSet.java.srcDirs.iterator().next()
} else {
testSourceDirectory = new File(project.projectDir, "src/test/java")
}
File outputDirectory = mainSourceSet != null
? mainSourceSet.java.classesDirectory.asFile.get()
: new File(project.getLayout().getBuildDirectory().getAsFile().get(), "classes/java/main");
File testOutputDirectory = testSourceSet != null
? testSourceSet.java.classesDirectory.asFile.get()
: new File(project.getLayout().getBuildDirectory().getAsFile().get(), "classes/java/test");
List<File> resourceDirs = mainSourceSet != null ? mainSourceSet.resources.srcDirs.toList() : new ArrayList<File>();

File serverDirectory = getServerDir(project);
String serverName = server.name;
Expand All @@ -1300,7 +1311,7 @@ class DevTask extends AbstractFeatureTask {
} // else TODO check if the container is already running?

if (resourceDirs.isEmpty()) {
File defaultResourceDir = new File(project.getRootDir() + "/src/main/resources");
File defaultResourceDir = new File(project.projectDir, "src/main/resources");
logger.info("No resource directory detected, using default directory: " + defaultResourceDir);
resourceDirs.add(defaultResourceDir);
}
Expand Down Expand Up @@ -1464,7 +1475,7 @@ class DevTask extends AbstractFeatureTask {
// which is where the server.xml is located if a specific serverXmlFile
// configuration parameter is not specified.
try {
util.watchFiles(outputDirectory.get().asFile, testOutputDirectory.get().asFile, executor, serverXMLFile,
util.watchFiles(outputDirectory, testOutputDirectory, executor, serverXMLFile,
project.liberty.server.bootstrapPropertiesFile, project.liberty.server.jvmOptionsFile);
} catch (PluginScenarioException e) {
if (e.getMessage() != null) {
Expand All @@ -1483,18 +1494,16 @@ class DevTask extends AbstractFeatureTask {
// we are directly calling compileJava task, which internally takes the compiler options
// from task definition or command line arguments
JavaCompilerOptions upstreamCompilerOptions = new JavaCompilerOptions();
SourceSet mainSourceSet = dependencyProject.sourceSets.main;
SourceSet testSourceSet = dependencyProject.sourceSets.test;
DefaultFilePropertyFactory.DefaultDirectoryVar outputDirectory = mainSourceSet.java.classesDirectory;
DefaultFilePropertyFactory.DefaultDirectoryVar testOutputDirectory = testSourceSet.java.classesDirectory;
SourceSet mainSourceSet = dependencyProject.sourceSets.findByName('main');
SourceSet testSourceSet = dependencyProject.sourceSets.findByName('test');
Set<String> compileArtifacts = new HashSet<String>();
Set<String> testArtifacts = new HashSet<String>();
File upstreamSourceDir = new File(mainSourceSet.java.sourceDirectories.asPath)
File upstreamOutputDir = outputDirectory.asFile.get();
File upstreamTestSourceDir = new File(testSourceSet.java.sourceDirectories.asPath);
File upstreamTestOutputDir = testOutputDirectory.asFile.get();
File upstreamSourceDir = mainSourceSet != null ? new File(mainSourceSet.java.sourceDirectories.asPath) : new File(dependencyProject.projectDir, "src/main/java");
File upstreamOutputDir = mainSourceSet != null ? mainSourceSet.java.classesDirectory.asFile.get() : new File(dependencyProject.layout.buildDirectory.asFile.get(), "classes/java/main");
File upstreamTestSourceDir = testSourceSet != null ? new File(testSourceSet.java.sourceDirectories.asPath) : new File(dependencyProject.projectDir, "src/test/java");
File upstreamTestOutputDir = testSourceSet != null ? testSourceSet.java.classesDirectory.asFile.get() : new File(dependencyProject.layout.buildDirectory.asFile.get(), "classes/java/test");
// resource directories
List<File> upstreamResourceDirs = mainSourceSet.resources.srcDirs.toList();
List<File> upstreamResourceDirs = mainSourceSet != null ? mainSourceSet.resources.srcDirs.toList() : new ArrayList<File>();
//get gradle project properties. It is observed that project properties contain all gradle properties
// properties are overridden automatically with the highest precedence
// in gradle, we are only using skipTests
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* (C) Copyright IBM Corporation 2026.
*
* 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.openliberty.tools.gradle

import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Test

import static org.junit.Assert.assertTrue

/**
* Integration test for the fix to:
* "Could not get unknown property 'main' for SourceSet container"
*
* Reproduces the exact user scenario: an EAR submodule that applies ONLY
* 'ear' + 'liberty' plugins (no 'java' plugin), while WAR and JAR submodules
* each apply 'java' themselves.
*
* Prior to the fix, DevTask.groovy accessed project.sourceSets.main using
* Groovy dynamic property syntax, which throws on Gradle 9 when the 'java'
* plugin is not applied (so 'main' source set does not exist).
*
* The fix switches to sourceSets.findByName('main') / findByName('test'),
* which returns null safely, and falls back to conventional default paths.
*
* This test verifies that libertyDev starts successfully on such a project
* and that hot-recompilation of a JAR module Java file still works.
*/
class TestMultiModuleEarNoJavaPluginDevMode extends BaseDevTest {

static File resourceDir = new File("build/resources/test/multi-module-loose-ear-no-java-plugin-test")
static File buildDir = new File(integTestDir, "/multi-module-loose-ear-no-java-plugin-test")
static String buildFilename = "build.gradle"

@BeforeClass
static void setup() throws IOException, InterruptedException, FileNotFoundException {
createDir(buildDir)
createTestProject(buildDir, resourceDir, buildFilename)
new File(buildDir, "build").mkdirs()

// Start libertyDev with --skipTests so we don't need test infrastructure.
// This exercises the fixed code path in DevTask.action() and
// DevTask.getProjectModules() for an EAR project without 'java' plugin.
runDevMode("--skipTests", buildDir)
}

/**
* Primary regression test: libertyDev must start without throwing
* "Could not get unknown property 'main' for SourceSet container"
*
* If the bug is present, setup() would have failed with a BuildException
* before this test even runs. The fact that the server reached dev mode
* proves the fix is effective.
*/
@Test
void earModuleWithoutJavaPluginStartsDevMode() throws Exception {
// Server startup is verified in setup() via verifyLogMessage("Liberty is running in dev mode.").
// This test documents the intent and provides a named regression marker.
assertTrue("libertyDev should be running in dev mode",
verifyLogMessage(5000, "Liberty is running in dev mode."))
}

/**
* Verify that modifying a Java source file in the JAR submodule
* (which DOES have the 'java' plugin) triggers recompilation while
* the EAR module's absence of sourceSets.main does not cause a crash.
*/
@Test
void modifyJavaFileInJarSubmoduleTriggersRecompilation() throws Exception {
File srcGreeter = new File(buildDir,
"jar/src/main/java/io/openliberty/guides/multimodules/lib/Greeter.java")
File targetGreeter = new File(buildDir,
"jar/build/classes/java/main/io/openliberty/guides/multimodules/lib/Greeter.class")

assertTrue("Greeter.java source file must exist", srcGreeter.exists())
assertTrue("Greeter.class must exist after initial build", targetGreeter.exists())

long lastModified = targetGreeter.lastModified()
waitLongEnough()

// Append a no-op comment to trigger the file watcher
BufferedWriter javaWriter = new BufferedWriter(new FileWriter(srcGreeter, true))
javaWriter.append(" // recompile trigger")
javaWriter.close()

assertTrue("Greeter.class should be recompiled after source change",
waitForCompilation(targetGreeter, lastModified, 12000))
}

/**
* Verify that modifying the EAR build.gradle triggers the expected
* "change detected in build file" dev mode warning.
*/
@Test
void modifyEarBuildGradleTriggersWarning() throws Exception {
waitLongEnough()

File earBuildGradle = new File(buildDir, "ear/build.gradle")
assertTrue("ear/build.gradle must exist", earBuildGradle.exists())

BufferedWriter buildWriter = new BufferedWriter(new FileWriter(earBuildGradle, true))
buildWriter.append(" // testing")
buildWriter.close()

assertTrue("Expected build file change warning in dev mode output",
verifyLogMessage(12000,
"A change was detected in a build file. The libertyDev task could not determine if a server restart is required. To restart server, type 'r' and press Enter."))
}

@AfterClass
static void cleanUpAfterClass() throws Exception {
String stdout = getContents(logFile, "Dev mode std output")
System.out.println(stdout)
String stderr = getContents(errFile, "Dev mode std error")
System.out.println(stderr)
cleanUpAfterClass(true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
allprojects {
group = 'io.openliberty.guides'
version = '1.0-SNAPSHOT'
}

// Intentionally NOT applying the 'java' plugin to all subprojects here.
// The 'ear' submodule only has 'ear' + 'liberty' plugins applied.
// This reproduces the user scenario that caused:
// "Could not get unknown property 'main' for SourceSet container"
// The fix in DevTask.groovy uses sourceSets.findByName('main') which
// returns null instead of throwing when 'java' is not applied.
subprojects {
repositories {
mavenLocal()
mavenCentral()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// This EAR module intentionally applies ONLY 'ear' and 'liberty' plugins —
// no 'java' plugin is applied here or from the root build.gradle.
// This reproduces the exact user scenario that triggered:
// "Could not get unknown property 'main' for SourceSet container"
// when running 'libertyDev' on Gradle 9 with liberty-gradle-plugin 4.x.
//
apply plugin: 'ear'
apply plugin: 'liberty'

description = 'EAR Module'

buildscript {
repositories {
mavenLocal()
mavenCentral()
maven {
url = 'https://central.sonatype.com/repository/maven-snapshots/'
}
}
dependencies {
classpath "io.openliberty.tools:liberty-gradle-plugin:$lgpVersion"
}
}

dependencies {
deploy project(path: ':war', configuration: 'warOnly')
deploy project(path: ':jar', configuration: 'jarOnly')
libertyRuntime group: runtimeGroup, name: kernelArtifactId, version: runtimeVersion
}

ear {
archiveFileName = rootProject.name + "-" + getArchiveBaseName().get() + "-" + rootProject.version + '.' + getArchiveExtension().get()
deploymentDescriptor {
webModule('ejb-war-1.0-SNAPSHOT.war', '/converter')
}
}

liberty {
server {
name = "ejbServer"
deploy {
apps = [ear]
copyLibsDirectory = file("${project.getLayout().getBuildDirectory().getAsFile().get()}/copy/libs")
}
verifyAppStartTimeout = 30
looseApplication = true
}
}

deploy.dependsOn 'ear'
ear.dependsOn ':jar:jar', ':war:war'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WLP_LOGGING_CONSOLE_LOGLEVEL=INFO
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<server description="Sample Liberty server">

<featureManager>
<feature>servlet-4.0</feature>
</featureManager>

<variable name="http.port" defaultValue="9082" />
<variable name="https.port" defaultValue="9447" />

<httpEndpoint host="*" httpPort="${http.port}"
httpsPort="${https.port}" id="defaultHttpEndpoint" />

<enterpriseApplication id="ejb-ear-1.0-SNAPSHOT"
location="ejb-ear-1.0-SNAPSHOT.ear"
name="ejb-ear-1.0-SNAPSHOT">
</enterpriseApplication>
</server>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

apply plugin: 'java'

description = 'JAR Module'

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
}

// Create a custom configuration that only includes the JAR artifact
configurations {
jarOnly
}

artifacts {
jarOnly jar
}

jar {
archiveFileName = rootProject.name + "-" + getArchiveBaseName().get() + "-" + rootProject.version + '.' + getArchiveExtension().get()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.openliberty.guides.multimodules.lib;

public class Greeter {

public static String greet(String name) {
return "Hello, " + name + "!";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
rootProject.name = 'ejb'
include ':jar'
include ':war'
include ':ear'

project(':jar').projectDir = "$rootDir/jar" as File
project(':war').projectDir = "$rootDir/war" as File
project(':ear').projectDir = "$rootDir/ear" as File
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

apply plugin: 'java'
apply plugin: 'war'

description = 'WAR Module'

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
compileOnly 'javax.servlet:javax.servlet-api:4.0.1'
implementation project(path: ':jar', configuration: 'jarOnly')
}

// Create a custom configuration that only includes the WAR artifact
configurations {
warOnly
}

artifacts {
warOnly war
}

war {
archiveFileName = rootProject.name + "-" + getArchiveBaseName().get() + "-" + rootProject.version + '.' + getArchiveExtension().get()
}

war.dependsOn ':jar:jar'
Loading
Loading