diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index e005007..e83f726 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: os: [ubuntu, macos, windows] + fail-fast: false runs-on: ${{ matrix.os }}-latest steps: - name: standardize git line endings @@ -21,7 +22,9 @@ jobs: - uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: | + 25.0.3 + 17.0.19 - uses: gradle/actions/setup-gradle@v6 with: build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8322c1b..8ad5ea9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,9 @@ jobs: - uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: | + 25.0.3 + 17.0.19 - uses: gradle/actions/setup-gradle@v6 with: build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use" diff --git a/build.gradle.kts b/build.gradle.kts index a5ef2da..3253d75 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,15 @@ publishingConventions { testingConventions { testGradleVersions("7.4", "7.6.5", "8.0.2", "8.14.2") } -tasks.test { +tasks.withType().configureEach { inputs.property("operatingSystemName", System.getProperty("os.name")) inputs.property("operatingSystemArch", System.getProperty("os.arch")) + environment["JAVA_25"] = + javaToolchains + .launcherFor { languageVersion = JavaLanguageVersion.of(25) } + .get() + .metadata + .installationPath + .asFile + .absolutePath } diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties index 20fe571..e6421e2 100644 --- a/gradle/gradle-daemon-jvm.properties +++ b/gradle/gradle-daemon-jvm.properties @@ -1 +1,2 @@ +# 17.0.19 toolchainVersion=17 \ No newline at end of file diff --git a/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java b/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java index 9d67f47..4996324 100644 --- a/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java +++ b/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java @@ -45,6 +45,7 @@ import org.gradle.util.GradleVersion; import org.gradlex.javamodule.packaging.internal.HostIdentification; import org.gradlex.javamodule.packaging.model.Target; +import org.gradlex.javamodule.packaging.tasks.FatModuleJar; import org.gradlex.javamodule.packaging.tasks.Jpackage; import org.gradlex.javamodule.packaging.tasks.ValidateHostSystemAction; import org.jspecify.annotations.NullMarked; @@ -56,6 +57,7 @@ public abstract class JavaModulePackagingExtension { private static final Attribute JAVA_MODULE_ATTRIBUTE = Attribute.of("javaModule", Boolean.class); private static final String INTERNAL = "internal"; private static final String JPACKAGE = "jpackage"; + private static final String FAT_MODULE_JAR = "fatModuleJar"; private final Project project; @@ -95,7 +97,6 @@ public JavaModulePackagingExtension(Project project) { /** * Retrieve the target with the given 'label'. If the target does not yet exist, it will be created. */ - @SuppressWarnings("unused") public Target target(String label) { return target(label, target -> {}); } @@ -120,7 +121,6 @@ public Target target(String label, Action action) { /** * Configure all targets for the given OS. */ - @SuppressWarnings("unused") public void targetsWithOs(String operatingSystem, Action action) { NamedDomainObjectSet matches = targets.matching(t -> t.getOperatingSystem().isPresent() @@ -132,7 +132,6 @@ public void targetsWithOs(String operatingSystem, Action action) * Set a 'primary target'. Standard Gradle tasks that are not bound to a specific target – like 'assemble' – use * this 'primary target'. */ - @SuppressWarnings("unused") public Target primaryTarget(Target target) { SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); ConfigurationContainer configurations = project.getConfigurations(); @@ -155,7 +154,6 @@ public Target primaryTarget(Target target) { /** * Set a test suite to be 'multi-target'. This registers an additional 'test' task for each target. */ - @SuppressWarnings({"unused", "UnstableApiUsage"}) public TestSuite multiTargetTestSuite(TestSuite testSuite) { if (!(testSuite instanceof JvmTestSuite)) { return testSuite; @@ -183,7 +181,18 @@ public TestSuite multiTargetTestSuite(TestSuite testSuite) { return testSuite; } + void maybeAddSingleDefaultTarget(Target target) { + if (targets.isEmpty()) { + targets.add(target); + newTarget(target, true); + } + } + private void newTarget(Target target) { + newTarget(target, false); + } + + private void newTarget(Target target, boolean singleDefault) { target.getPackageTypes().convention(target.getOperatingSystem().map(os -> { switch (os) { case WINDOWS: @@ -200,35 +209,41 @@ private void newTarget(Target target) { SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); sourceSets.all(sourceSet -> { - Configuration internal = maybeCreateInternalConfiguration(); - configurations.create( - target.getName() + capitalize(sourceSet.getCompileClasspathConfigurationName()), c -> { - c.setCanBeConsumed(false); - setInvisible(c); - configureJavaStandardAttributes(c, Usage.JAVA_API); - configureTargetAttributes(c, target); - c.extendsFrom( - configurations.getByName(sourceSet.getImplementationConfigurationName()), - configurations.getByName(sourceSet.getCompileOnlyConfigurationName()), - internal); - }); - Configuration runtimeClasspath = configurations.create( - target.getName() + capitalize(sourceSet.getRuntimeClasspathConfigurationName()), c -> { - c.setCanBeConsumed(false); - setInvisible(c); - configureJavaStandardAttributes(c, Usage.JAVA_RUNTIME); - configureTargetAttributes(c, target); - c.extendsFrom( - configurations.getByName(sourceSet.getImplementationConfigurationName()), - configurations.getByName(sourceSet.getRuntimeOnlyConfigurationName()), - internal); - }); + Configuration runtimeClasspath; + if (singleDefault) { + runtimeClasspath = configurations.getByName(sourceSet.getRuntimeClasspathConfigurationName()); + } else { + Configuration internal = maybeCreateInternalConfiguration(); + configurations.create( + target.getName() + capitalize(sourceSet.getCompileClasspathConfigurationName()), c -> { + c.setCanBeConsumed(false); + setInvisible(c); + configureJavaStandardAttributes(c, Usage.JAVA_API); + configureTargetAttributes(c, target); + c.extendsFrom( + configurations.getByName(sourceSet.getImplementationConfigurationName()), + configurations.getByName(sourceSet.getCompileOnlyConfigurationName()), + internal); + }); + runtimeClasspath = configurations.create( + target.getName() + capitalize(sourceSet.getRuntimeClasspathConfigurationName()), c -> { + c.setCanBeConsumed(false); + setInvisible(c); + configureJavaStandardAttributes(c, Usage.JAVA_RUNTIME); + configureTargetAttributes(c, target); + c.extendsFrom( + configurations.getByName(sourceSet.getImplementationConfigurationName()), + configurations.getByName(sourceSet.getRuntimeOnlyConfigurationName()), + internal); + }); + } if (SourceSet.isMain(sourceSet)) { project.getPlugins() .withType( ApplicationPlugin.class, - p -> registerTargetSpecificTasks(target, sourceSet.getJarTaskName(), runtimeClasspath)); + p -> registerTargetSpecificTasks( + target, singleDefault, sourceSet.getJarTaskName(), runtimeClasspath)); } }); } @@ -268,13 +283,16 @@ private void configureTargetAttributes(Configuration resolvable, Target target) target.getArchitecture().map(name -> getObjects().named(MachineArchitecture.class, name))); } - private void registerTargetSpecificTasks(Target target, String applicationJarTask, Configuration runtimeClasspath) { + private void registerTargetSpecificTasks( + Target target, boolean singleDefaultTarget, String applicationJarTask, Configuration runtimeClasspath) { TaskContainer tasks = project.getTasks(); JavaPluginExtension java = project.getExtensions().getByType(JavaPluginExtension.class); JavaApplication application = project.getExtensions().getByType(JavaApplication.class); TaskProvider jpackage = tasks.register(JPACKAGE + capitalize(target.getName()), Jpackage.class, t -> { + t.setDescription("Runs 'jpackage' command to create an image or s package."); + t.getJavaInstallation() .convention(getJavaToolchains() .compilerFor(java.getToolchain()) @@ -318,12 +336,43 @@ private void registerTargetSpecificTasks(Target target, String applicationJarTas t.setJvmArgs(application.getApplicationDefaultJvmArgs()); t.classpath(tasks.named("jar"), runtimeClasspath); }); - maybeAddJpackageLifecycleTask(tasks, target, jpackage); + + registerFatModuleJarTask(target, singleDefaultTarget, applicationJarTask, runtimeClasspath); + + maybeAddJpackageLifecycleTask(target, jpackage); + } + + private void registerFatModuleJarTask( + Target target, boolean singleDefaultTarget, String applicationJarTask, Configuration runtimeClasspath) { + TaskContainer tasks = project.getTasks(); + JavaApplication application = project.getExtensions().getByType(JavaApplication.class); + ConfigurationContainer configurations = project.getConfigurations(); + + String taskName = FAT_MODULE_JAR + (singleDefaultTarget ? "" : capitalize(target.getName())); + tasks.register(taskName, FatModuleJar.class, t -> { + String classifier = singleDefaultTarget ? "all" : "all-" + target.getName(); + + t.setDescription("Assembles a fat jar archive containing the complete module path and a launcher."); + + t.getMainModule().convention(application.getMainModule()); + t.getMainClass().convention(application.getMainClass()); + + t.getLauncherPath().from(configurations.named("fatModuleJarLauncherPath")); + t.getLauncherMainClass().convention("build.jenesis.launcher.Launcher"); + + t.getModulePath().from(tasks.named(applicationJarTask)); + t.getModulePath().from(runtimeClasspath); + + t.setZip64(true); + + t.getArchiveClassifier().set(classifier); + }); } - private void maybeAddJpackageLifecycleTask( - TaskContainer tasks, Target target, TaskProvider targetJpackage) { - // if a task already exists, do nothing to avoid conflciting with other plugins + private void maybeAddJpackageLifecycleTask(Target target, TaskProvider targetJpackage) { + TaskContainer tasks = project.getTasks(); + + // if a task already exists, do nothing to avoid conflict with other plugins TaskProvider jpackage; if (tasks.getNames().contains(JPACKAGE)) { jpackage = tasks.named(JPACKAGE); diff --git a/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingPlugin.java b/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingPlugin.java index 13ef016..b5c443a 100644 --- a/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingPlugin.java +++ b/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingPlugin.java @@ -1,22 +1,26 @@ // SPDX-License-Identifier: Apache-2.0 package org.gradlex.javamodule.packaging; -import javax.inject.Inject; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.attributes.Usage; import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.jvm.toolchain.JavaToolchainService; import org.gradle.util.GradleVersion; import org.gradlex.javamodule.packaging.internal.HostIdentification; +import org.gradlex.javamodule.packaging.model.Target; import org.jspecify.annotations.NullMarked; @NullMarked public abstract class JavaModulePackagingPlugin implements Plugin { - @Inject - protected abstract JavaToolchainService getJavaToolchains(); + private static final String DEFAULT_FATJAR_LAUNCHER = "build.jenesis:build.jenesis.launcher:0.3.0"; @Override public void apply(Project project) { @@ -36,6 +40,34 @@ public void apply(Project project) { .getProjectDirectory() .dir(mainResources.getSrcDirs().iterator().next().getParent() + "/resourcesPackage"))); javaModulePackaging.getVerbose().convention(false); - javaModulePackaging.primaryTarget(HostIdentification.hostTarget(project.getObjects())); + + Target hostTarget = HostIdentification.hostTarget(project.getObjects()); + javaModulePackaging.primaryTarget(hostTarget); + project.afterEvaluate(__ -> javaModulePackaging.maybeAddSingleDefaultTarget(hostTarget)); + + registerFatModuleJarLauncherScope(project); + } + + private void registerFatModuleJarLauncherScope(Project project) { + ObjectFactory objects = project.getObjects(); + ConfigurationContainer configurations = project.getConfigurations(); + DependencyHandler dependencies = project.getDependencies(); + + Provider fatModuleJarLauncher = configurations.register("fatModuleJarLauncher", c -> { + c.setCanBeConsumed(false); + c.setCanBeResolved(false); + c.withDependencies(deps -> { + if (deps.isEmpty()) { + deps.add(dependencies.create(DEFAULT_FATJAR_LAUNCHER)); + } + }); + }); + + configurations.register("fatModuleJarLauncherPath", c -> { + c.setCanBeConsumed(false); + c.setCanBeResolved(true); + c.extendsFrom(fatModuleJarLauncher.get()); + c.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_RUNTIME)); + }); } } diff --git a/src/main/java/org/gradlex/javamodule/packaging/internal/HostIdentification.java b/src/main/java/org/gradlex/javamodule/packaging/internal/HostIdentification.java index 10435b9..745c0e1 100644 --- a/src/main/java/org/gradlex/javamodule/packaging/internal/HostIdentification.java +++ b/src/main/java/org/gradlex/javamodule/packaging/internal/HostIdentification.java @@ -24,7 +24,7 @@ public static void validateHostSystem(String arch, String os) { } public static Target hostTarget(ObjectFactory objects) { - Target host = objects.newInstance(Target.class, "host"); + Target host = objects.newInstance(Target.class, hostOs()); host.getOperatingSystem().convention(hostOs()); host.getArchitecture().convention(normalizeArch(System.getProperty("os.arch"))); return host; diff --git a/src/main/java/org/gradlex/javamodule/packaging/tasks/FatModuleJar.java b/src/main/java/org/gradlex/javamodule/packaging/tasks/FatModuleJar.java new file mode 100644 index 0000000..3c83588 --- /dev/null +++ b/src/main/java/org/gradlex/javamodule/packaging/tasks/FatModuleJar.java @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +package org.gradlex.javamodule.packaging.tasks; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singletonMap; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import javax.inject.Inject; +import org.gradle.api.file.ArchiveOperations; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.internal.file.FileOperations; +import org.gradle.api.internal.file.copy.CopySpecInternal; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.WorkResult; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.work.DisableCachingByDefault; + +@DisableCachingByDefault(because = "Not worth caching") +public abstract class FatModuleJar extends Jar { + + @Classpath + public abstract ConfigurableFileCollection getModulePath(); + + @Input + public abstract Property getMainModule(); + + @Input + public abstract Property getMainClass(); + + @Classpath + public abstract ConfigurableFileCollection getLauncherPath(); + + @Input + public abstract Property getLauncherMainClass(); + + @Inject + protected abstract ArchiveOperations getArchives(); + + // Should be the public 'FileSystemOperations', but 'copySpec()' was only introduced in 8.5 + @Inject + protected abstract FileOperations getFiles(); + + @Override + protected void copy() { + getManifest() + .attributes(singletonMap("Main-Class", getLauncherMainClass().get())); + + CopySpecInternal extendedSpec = (CopySpecInternal) getFiles().copySpec(); + extendedSpec.with(getRootSpec()); + + File applicationProperties = writeApplicationProperties(); + extendedSpec.from(applicationProperties); + + extendedSpec.from(getArchives() + .zipTree(getLauncherPath().getSingleFile()) + .matching(f -> f.exclude("META-INF/MANIFEST.MF"))); + + extendedSpec.into("modulepath", pathFolder -> { + for (File jar : getModulePath()) { + pathFolder.into( + nameWithoutExtension(jar), + moduleFolder -> moduleFolder.from(getArchives().zipTree(jar))); + } + }); + + // based on 'super()' + WorkResult didWork = createCopyActionExecuter().execute(extendedSpec, createCopyAction()); + setDidWork(didWork.getDidWork()); + } + + private File writeApplicationProperties() { + File applicationProperties = new File(getTemporaryDir(), "application.properties"); + try { + Files.createDirectories(applicationProperties.toPath().getParent()); + Files.write( + applicationProperties.toPath(), + String.format( + "mainModule=%s\nmainClass=%s", + getMainModule().get(), getMainClass().get()) + .getBytes(UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + return applicationProperties; + } + + private String nameWithoutExtension(File file) { + return file.getName().substring(0, file.getName().lastIndexOf('.')); + } +} diff --git a/src/test/java/org/gradlex/javamodule/packaging/test/FatModuleJarTest.java b/src/test/java/org/gradlex/javamodule/packaging/test/FatModuleJarTest.java new file mode 100644 index 0000000..695cd9a --- /dev/null +++ b/src/test/java/org/gradlex/javamodule/packaging/test/FatModuleJarTest.java @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: Apache-2.0 +package org.gradlex.javamodule.packaging.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.gradlex.javamodule.packaging.test.fixture.GradleBuild.runsOnWindows; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import org.gradlex.javamodule.packaging.test.fixture.GradleBuild; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FatModuleJarTest { + + GradleBuild build = new GradleBuild(); + + @BeforeEach + void setup() { + build.appBuildFile.appendText(""" + version = "1.0" + dependencies { + implementation("org.apache.commons:commons-csv:1.14.1") + } + """); + build.appModuleInfoFile.writeText(""" + module org.example.app { + requires org.apache.commons.codec; + requires org.apache.commons.csv; + requires org.apache.commons.io; + } + """); + build.file("app/src/main/java/org/example/app/Main.java").writeText(""" + package org.example.app; + public class Main { + public static void main(String... args) { + boolean isInFatJar = Main.class.getProtectionDomain().getCodeSource().getLocation().toString() + .endsWith("app-1.0-all.jar!/modulepath/app-1.0/"); + System.out.println(Main.class.getModule().getName() + " / " + isInFatJar); + } + } + """); + } + + @Test + void offers_one_fat_jar_task_for_the_default_target() { + build.build(":app:fatModuleJar"); + assertThat(build.projectDir.file("app/build/libs/app-1.0-all.jar").getAsPath()) + .isRegularFile(); + } + + @Test + void offers_one_fat_jar_task_per_target() { + var macOsJar = + build.projectDir.file("app/build/libs/app-1.0-all-macos.jar").getAsPath(); + var windowsJar = + build.projectDir.file("app/build/libs/app-1.0-all-windows.jar").getAsPath(); + var linuxJar = + build.projectDir.file("app/build/libs/app-1.0-all-ubuntu.jar").getAsPath(); + + build.appBuildFile.appendText(""" + javaModulePackaging { + target("macos") { + operatingSystem.set("macos") + architecture.set("aarch64") + } + target("ubuntu") { + operatingSystem.set("linux") + architecture.set("x86-64") + } + target("windows") { + operatingSystem.set("windows") + architecture.set("x86-64") + } + } + """); + + build.build(":app:fatModuleJarMacos"); + assertThat(macOsJar).isRegularFile(); + + build.build(":app:fatModuleJarWindows"); + assertThat(windowsJar).isRegularFile(); + + build.build(":app:fatModuleJarUbuntu"); + assertThat(linuxJar).isRegularFile(); + + assertThat(macOsJar).hasSize(1082396); + assertThat(macOsJar).hasSameBinaryContentAs(windowsJar); + assertThat(macOsJar).hasSameBinaryContentAs(linuxJar); + } + + @Test + void target_specific_fat_jars_differ_if_they_package_target_specific_modules() { + var macOsJar = + build.projectDir.file("app/build/libs/app-1.0-all-macos.jar").getAsPath(); + var windowsJar = + build.projectDir.file("app/build/libs/app-1.0-all-windows.jar").getAsPath(); + var linuxJar = + build.projectDir.file("app/build/libs/app-1.0-all-ubuntu.jar").getAsPath(); + + build.appBuildFile.appendText(""" + dependencies { + implementation("org.openjfx:javafx-base:17") + } + jvmDependencyConflicts { + patch { + module("org.openjfx:javafx-base") { + addTargetPlatformVariant("", "none", "none") + addTargetPlatformVariant("linux", OperatingSystemFamily.LINUX, MachineArchitecture.X86_64) + addTargetPlatformVariant("linux-aarch64", OperatingSystemFamily.LINUX, "aarch64") + addTargetPlatformVariant("mac", OperatingSystemFamily.MACOS, MachineArchitecture.X86_64) + addTargetPlatformVariant("mac-aarch64", OperatingSystemFamily.MACOS, "aarch64") + addTargetPlatformVariant("win", OperatingSystemFamily.WINDOWS, MachineArchitecture.X86_64) + } + } + } + """); + + build.appBuildFile.appendText(""" + javaModulePackaging { + target("macos") { + operatingSystem.set("macos") + architecture.set("aarch64") + } + target("ubuntu") { + operatingSystem.set("linux") + architecture.set("x86-64") + } + target("windows") { + operatingSystem.set("windows") + architecture.set("x86-64") + } + } + """); + + build.build(":app:fatModuleJarMacos"); + assertThat(macOsJar).isRegularFile(); + + build.build(":app:fatModuleJarWindows"); + assertThat(windowsJar).isRegularFile(); + + build.build(":app:fatModuleJarUbuntu"); + assertThat(linuxJar).isRegularFile(); + + assertThat(macOsJar).hasSize(1894824); + assertThat(windowsJar).hasSize(1883723); + assertThat(linuxJar).hasSize(1886498); + } + + @Test + void fat_jar_runs() throws IOException, InterruptedException { + build.build(":app:fatModuleJar"); + + String javaCmd = System.getenv("JAVA_25") + "/bin/java" + (runsOnWindows() ? ".exe" : ""); + + var fatJar = build.projectDir + .file("app/build/libs/app-1.0-all.jar") + .getAsPath() + .toAbsolutePath() + .toString(); + + System.out.println(javaCmd + " -jar " + fatJar); + String[] command = {javaCmd, "-jar", fatJar}; + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + + var reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + assertThat(line).isEqualTo("org.example.app / true"); + } + assertThat(process.waitFor()).isEqualTo(0); + } +} diff --git a/src/test/java/org/gradlex/javamodule/packaging/test/fixture/GradleBuild.java b/src/test/java/org/gradlex/javamodule/packaging/test/fixture/GradleBuild.java index 0fa0eca..3cf99bb 100644 --- a/src/test/java/org/gradlex/javamodule/packaging/test/fixture/GradleBuild.java +++ b/src/test/java/org/gradlex/javamodule/packaging/test/fixture/GradleBuild.java @@ -37,6 +37,15 @@ public GradleBuild(Path dir) { this.libBuildFile = file("lib/build.gradle.kts"); this.libModuleInfoFile = file("lib/src/main/java/module-info.java"); + var makeReproducible = GRADLE_VERSION_UNDER_TEST == null ? "" : """ + tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + dirMode = "0755".toInt(8) + fileMode = "0644".toInt(8) + } + """; + settingsFile.writeText(""" dependencyResolutionManagement { repositories.mavenCentral() } includeBuild(".") @@ -46,6 +55,7 @@ public GradleBuild(Path dir) { appBuildFile.writeText(""" plugins { id("org.gradlex.java-module-packaging") + id("org.gradlex.jvm-dependency-conflict-resolution") version "2.5" id("application") } group = "org.example" @@ -56,7 +66,8 @@ public GradleBuild(Path dir) { mainModule.set("org.example.app") mainClass.set("org.example.app.Main") } - """); + %s + """.formatted(makeReproducible)); file("app/src/main/java/org/example/app/Main.java").writeText(""" package org.example.app; @@ -123,7 +134,7 @@ public GradleRunner runner(boolean projectIsolation, String... args) { .getInputArguments() .toString() .contains("-agentlib:jdwp"); - List latestFeaturesArgs = GRADLE_VERSION_UNDER_TEST != null || !projectIsolation + List latestFeaturesArgs = GRADLE_VERSION_UNDER_TEST != null || !projectIsolation || debugMode ? List.of() : List.of("--configuration-cache", "-Dorg.gradle.unsafe.isolated-projects=true"); Stream standardArgs = Stream.of("-s", "--warning-mode=all");