diff --git a/src/main/java/net/minecraftforge/gradle/internal/ForgeGradleProblems.java b/src/main/java/net/minecraftforge/gradle/internal/ForgeGradleProblems.java index 61218cf6e..2021dca84 100644 --- a/src/main/java/net/minecraftforge/gradle/internal/ForgeGradleProblems.java +++ b/src/main/java/net/minecraftforge/gradle/internal/ForgeGradleProblems.java @@ -194,6 +194,19 @@ void reportForgeMavenNotDeclared() { //endregion //endregion + //region Eclipse + void reportUnmergedSourceSets() { + report("source-sets-not-merged-for-eclipse", "Source Set outputs are not merged", spec -> spec + .details(""" + ForgeGradle was not configured to merge source set outputs, but this project is using Eclipse! + Unmerged source sets may cause problems when using the Eclipse launch configurations generated by ForgeGradle.""") + .severity(Severity.WARNING) + .solution("Set the 'net.minecraftforge.gradle.merge-source-sets' Gradle property to true.") + .solution("Do not use the 'eclipse' plugin if you (or no one in your team) is using Eclipse.") + .solution(HELP_MESSAGE)); + } + //endregion + //region Access Transformers void reportAccessTransformersNotApplied(Throwable e) { this.report("access-transformers-not-applied", "AccessTransformers plugin not applied", spec -> spec diff --git a/src/main/java/net/minecraftforge/gradle/internal/MinecraftExtensionImpl.java b/src/main/java/net/minecraftforge/gradle/internal/MinecraftExtensionImpl.java index a502c6812..c064d7705 100644 --- a/src/main/java/net/minecraftforge/gradle/internal/MinecraftExtensionImpl.java +++ b/src/main/java/net/minecraftforge/gradle/internal/MinecraftExtensionImpl.java @@ -21,6 +21,7 @@ import org.gradle.api.Action; import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ExternalModuleDependency; import org.gradle.api.artifacts.ExternalModuleDependencyBundle; @@ -36,9 +37,12 @@ import org.gradle.api.provider.Property; import org.gradle.api.provider.ProviderFactory; import org.gradle.api.reflect.TypeOf; +import org.gradle.api.tasks.TaskProvider; import org.gradle.plugins.ide.eclipse.model.EclipseModel; +import org.jetbrains.annotations.Nullable; import javax.inject.Inject; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -221,13 +225,22 @@ private void finish(Project project) { checkRepos(getRepositories()); var sourceSetsDir = this.getObjects().directoryProperty().value(this.getProjectLayout().getBuildDirectory().dir("sourceSets")); + var mergeSourceSets = this.problems.test("net.minecraftforge.gradle.merge-source-sets"); project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets().configureEach(sourceSet -> { - if (this.problems.test("net.minecraftforge.gradle.merge-source-sets")) { + if (mergeSourceSets) { // This is documented in SourceSetOutput's javadoc comment var unifiedDir = sourceSetsDir.dir(sourceSet.getName()); sourceSet.getOutput().setResourcesDir(unifiedDir); sourceSet.getJava().getDestinationDirectory().set(unifiedDir); } + + project.getPluginManager().withPlugin("eclipse", eclipsePlugin -> { + var eclipse = project.getExtensions().getByType(EclipseModel.class); + if (mergeSourceSets) + eclipse.getClasspath().setDefaultOutputDir(sourceSetsDir.getAsFile().get()); + else + problems.reportUnmergedSourceSets(); + }); }); if (!this.minecraftDependencies.isEmpty()) { @@ -250,6 +263,20 @@ private void finish(Project project) { } if (!this.runs.isEmpty() && !this.minecraftDependencies.isEmpty()) { + var genEclipseRuns = project.getTasks().register("genEclipseRuns", task -> { + task.setGroup("IDE"); + task.setDescription("Generates the run configuration launch files for Eclipse."); + }); + + File eclipseOutputDir; + var eclipse = project.getExtensions().findByType(EclipseModel.class); + if (eclipse != null) { + eclipse.synchronizationTasks(genEclipseRuns); + eclipseOutputDir = eclipse.getClasspath().getDefaultOutputDir(); + } else { + eclipseOutputDir = getProjectLayout().getProjectDirectory().dir("bin").getAsFile(); + } + var configurations = project.getConfigurations(); var sourceSets = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets(); @@ -269,7 +296,7 @@ private void finish(Project project) { var impl = (MinecraftDependencyImpl) minecraftDependency; this.runs.forEach(options -> { - var task = SlimeLauncherExec.register(project, sourceSet, options, impl.module.get(), impl.version.get(), impl.asPath.get(), impl.asString.get(), single); + var task = SlimeLauncherExec.register(project, sourceSet, options, impl.module.get(), impl.version.get(), impl.asPath.get(), impl.asString.get(), single, eclipseOutputDir); }); } }); diff --git a/src/main/java/net/minecraftforge/gradle/internal/SlimeLauncherEclipseConfiguration.java b/src/main/java/net/minecraftforge/gradle/internal/SlimeLauncherEclipseConfiguration.java new file mode 100644 index 000000000..1153705c6 --- /dev/null +++ b/src/main/java/net/minecraftforge/gradle/internal/SlimeLauncherEclipseConfiguration.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradle.internal; + +import com.google.gson.JsonIOException; +import com.google.gson.reflect.TypeToken; +import net.minecraftforge.gradle.SlimeLauncherOptions; +import net.minecraftforge.util.data.json.JsonData; +import net.minecraftforge.util.data.json.RunConfig; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.jvm.toolchain.JavaLauncher; +import org.gradle.plugins.ide.eclipse.model.EclipseModel; +import org.gradle.workers.WorkAction; +import org.gradle.workers.WorkParameters; +import org.gradle.workers.WorkerExecutor; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.inject.Inject; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +// This is mostly taken from ForgeGradle 6 but slimmed down to what we need +abstract class SlimeLauncherEclipseConfiguration extends DefaultTask implements ForgeGradleTask { + protected abstract @OutputFile RegularFileProperty getOutputFile(); + + protected abstract @Input Property getProjectName(); + + protected abstract @Input @Optional Property getEclipseProjectName(); + + protected abstract @Input Property getRunName(); + + protected abstract @Nested Property getJavaLauncher(); + + protected abstract @InputFiles @Classpath ConfigurableFileCollection getClasspath(); + + protected abstract @Input Property getMainClass(); + + protected abstract @Nested Property getOptions(); + + protected abstract @Internal DirectoryProperty getCacheDir(); + + protected abstract @InputFile RegularFileProperty getMetadataZip(); + + protected abstract @InputFile RegularFileProperty getRunsJson(); + + protected abstract @Inject ObjectFactory getObjects(); + + protected abstract @Inject ProviderFactory getProviders(); + + protected abstract @Inject ProjectLayout getProjectLayout(); + + protected abstract @Inject WorkerExecutor getWorkerExecutor(); + + final ForgeGradleProblems problems = this.getObjects().newInstance(ForgeGradleProblems.class); + + @Inject + public SlimeLauncherEclipseConfiguration() { + this.getProjectName().convention(this.getProject().getName()); + this.getEclipseProjectName().convention(getProviders().provider(() -> { + var eclipse = getProject().getExtensions().findByType(EclipseModel.class); + return eclipse == null ? null : eclipse.getProject().getName(); + })); + + var tool = this.getTool(Tools.SLIMELAUNCHER); + this.getClasspath().from(tool.getClasspath()); + this.getMainClass().set(tool.getMainClass()); + this.getJavaLauncher().set(Util.launcherFor(getProject(),tool.getJavaVersion())); + } + + @TaskAction + protected void exec() { + List args; + List jvmArgs; + MapProperty environment; + DirectoryProperty workingDir; + + //region Launcher Metadata Inheritance + Map configs = Map.of(); + try { + configs = JsonData.fromJson( + this.getRunsJson().getAsFile().get(), + new TypeToken<>() { } + ); + } catch (JsonIOException e) { + // continue + } + + var options = ((SlimeLauncherOptionsInternal) this.getOptions().get()).inherit(configs); + + args = new ArrayList<>(options.getArgs().getOrElse(List.of())); + jvmArgs = new ArrayList<>(options.getJvmArgs().getOrElse(List.of())); + if (!options.getClasspath().isEmpty()) + this.getClasspath().setFrom(options.getClasspath()); + if (options.getMinHeapSize().filter(Util::isPresent).isPresent()) + jvmArgs.add("-Xms" + options.getMinHeapSize().get()); + if (options.getMaxHeapSize().filter(Util::isPresent).isPresent()) + jvmArgs.add("-Xmx" + options.getMaxHeapSize().get()); + for (var property : options.getSystemProperties().getOrElse(Map.of()).entrySet()) + jvmArgs.add("-D" + property.getKey() + '=' + property.getValue()); + environment = options.getEnvironment(); + workingDir = options.getWorkingDir(); + //endregion + + //region Slime Launcher setup + args.addAll(0, List.of("--main", options.getMainClass().get(), + "--cache", this.getCacheDir().get().getAsFile().getAbsolutePath(), + "--metadata", this.getMetadataZip().get().getAsFile().getAbsolutePath(), + "--")); + + try { + Files.createDirectories(workingDir.get().getAsFile().toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + //endregion + + var queue = this.getWorkerExecutor().classLoaderIsolation(); + + queue.submit(Action.class, parameters -> { + parameters.getOutputFile().set(this.getOutputFile()); + parameters.getEclipseProjectName().set(this.getEclipseProjectName().orElse(this.getProjectName())); + parameters.getClasspath().setFrom(this.getClasspath()); + parameters.getMainClass().set(this.getMainClass().get()); + parameters.getArgs().set(args); + parameters.getJvmArgs().set(jvmArgs); + parameters.getWorkingDir().set(workingDir); + parameters.getEnvironment().set(environment); + parameters.getJavaHome().set(this.getJavaLauncher().map(j -> j.getMetadata().getInstallationPath())); + }); + } + + static abstract class Action implements WorkAction { + interface Parameters extends WorkParameters { + RegularFileProperty getOutputFile(); + + Property getEclipseProjectName(); + + ConfigurableFileCollection getClasspath(); + + Property getMainClass(); + + ListProperty getArgs(); + + ListProperty getJvmArgs(); + + DirectoryProperty getWorkingDir(); + + DirectoryProperty getJavaHome(); + + MapProperty getEnvironment(); + } + + @Inject + public Action() { } + + @Override + public void execute() { + var parameters = getParameters(); + + DocumentBuilder documentBuilder; + Transformer transformer; + try { + documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + transformer = TransformerFactory.newInstance().newTransformer(); + } catch (ParserConfigurationException | TransformerConfigurationException e) { + throw new RuntimeException(e); + } + + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + var launch = documentBuilder.newDocument(); + var rootElement = launch.createElement("launchConfiguration"); + + rootElement.setAttribute("type", "org.eclipse.jdt.launching.localJavaApplication"); + stringAttribute(launch, rootElement, "org.eclipse.jdt.launching.PROJECT_ATTR", parameters.getEclipseProjectName().get()); + stringAttribute(launch, rootElement, "org.eclipse.jdt.launching.MAIN_TYPE", parameters.getMainClass().get()); + stringAttribute(launch, rootElement, "org.eclipse.jdt.launching.VM_ARGUMENTS", String.join(" ", parameters.getJvmArgs().get())); + stringAttribute(launch, rootElement, "org.eclipse.jdt.launching.PROGRAM_ARGUMENTS", String.join(" ", parameters.getArgs().get())); + stringAttribute(launch, rootElement, "org.eclipse.jdt.launching.WORKING_DIRECTORY", parameters.getWorkingDir().getAsFile().get().getAbsolutePath()); + //stringAttribute(launch, rootElement, "org.eclipse.jdt.launching.JRE_CONTAINER", parameters.getJavaHome().getAsFile().get().getAbsolutePath()); + mapAttribute(launch, rootElement, "org.eclipse.debug.core.environmentVariables", parameters.getEnvironment().get()); + classpathAttribute(launch, rootElement, parameters.getClasspath()); + booleanAttribute(launch, rootElement, "org.eclipse.jdt.launching.DEFAULT_CLASSPATH", false); + + launch.appendChild(rootElement); + + var source = new DOMSource(launch); + var result = new StreamResult(parameters.getOutputFile().getAsFile().get()); + + try { + transformer.transform(source, result); + } catch (TransformerException e) { + throw new RuntimeException(e); + } + } + + private static void stringAttribute(Document document, Element parent, String key, Object value) { + var attribute = document.createElement("stringAttribute"); + + attribute.setAttribute("key", key); + attribute.setAttribute("value", value.toString()); + parent.appendChild(attribute); + } + + private static void booleanAttribute(Document document, Element parent, String key, boolean value) { + var attribute = document.createElement("booleanAttribute"); + + attribute.setAttribute("key", key); + attribute.setAttribute("value", Boolean.toString(value)); + parent.appendChild(attribute); + } + + private static void listAttribute(Document document, Element parent, String key, Iterable list) { + var attribute = document.createElement("listAttribute"); + attribute.setAttribute("key", key); + + for (var v : list) { + var listEntry = document.createElement("listEntry"); + listEntry.setAttribute("value", v.toString()); + attribute.appendChild(listEntry); + } + parent.appendChild(attribute); + } + + private static final String CLASSPATH_ENTRY_PREFIX = " "; + + private static void classpathAttribute(Document document, Element parent, FileCollection files) { + var attribute = document.createElement("listAttribute"); + attribute.setAttribute("key", "org.eclipse.jdt.launching.CLASSPATH"); + + for (var v : files.getFiles()) { + var listEntry = document.createElement("listEntry"); + listEntry.setAttribute("value", CLASSPATH_ENTRY_PREFIX + v + CLASSPATH_ENTRY_SUFFIX); + attribute.appendChild(listEntry); + } + parent.appendChild(attribute); + } + + private static void mapAttribute(Document document, Element parent, String key, Map map) { + var attribute = document.createElement("mapAttribute"); + attribute.setAttribute("key", key); + + for (var entry : map.entrySet()) { + var k = entry.getKey(); + var v = entry.getValue(); + + var mapEntry = document.createElement("mapEntry"); + mapEntry.setAttribute("key", k); + mapEntry.setAttribute("value", v.toString()); + attribute.appendChild(mapEntry); + } + parent.appendChild(attribute); + } + } +} diff --git a/src/main/java/net/minecraftforge/gradle/internal/SlimeLauncherExec.java b/src/main/java/net/minecraftforge/gradle/internal/SlimeLauncherExec.java index c349f8d26..dc78b9141 100644 --- a/src/main/java/net/minecraftforge/gradle/internal/SlimeLauncherExec.java +++ b/src/main/java/net/minecraftforge/gradle/internal/SlimeLauncherExec.java @@ -15,7 +15,6 @@ import org.gradle.api.attributes.Usage; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.reflect.HasPublicType; @@ -28,8 +27,10 @@ import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.toolchain.JavaLanguageVersion; import javax.inject.Inject; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; @@ -37,7 +38,7 @@ import java.util.Map; abstract class SlimeLauncherExec extends JavaExec implements ForgeGradleTask, HasPublicType { - static TaskProvider register(Project project, SourceSet sourceSet, SlimeLauncherOptionsImpl options, ModuleIdentifier module, String version, String asPath, String asString, boolean single) { + static TaskProvider register(Project project, SourceSet sourceSet, SlimeLauncherOptionsImpl options, ModuleIdentifier module, String version, String asPath, String asString, boolean single, File eclipseOutputDir) { TaskProvider metadata; { TaskProvider t; @@ -66,13 +67,37 @@ static TaskProvider register(Project project, SourceSet sourc metadata = t; } - var taskName = sourceSet.getTaskName("run", options.getName()) + (single ? "" : "for" + Util.dependencyToCamelCase(module)); - return project.getTasks().register(taskName, SlimeLauncherExec.class, task -> { + var taskNameSuffix = (single ? "" : "for" + Util.dependencyToCamelCase(module)); + var runTaskName = sourceSet.getTaskName("run", options.getName()) + taskNameSuffix; + var generateEclipseRunTaskName = sourceSet.getTaskName("genEclipseRun", options.getName()) + taskNameSuffix; + + var sourceSetOutputs = project.getObjects().fileCollection().from(sourceSet.getOutput().getResourcesDir(), sourceSet.getJava().getDestinationDirectory()); + var eclipseOutputs = project.getObjects().fileCollection().from(eclipseOutputDir); + var genEclipseRun = project.getTasks().register(generateEclipseRunTaskName, SlimeLauncherEclipseConfiguration.class, task -> { + task.getRunName().set(options.getName()); + task.setDescription("Generates the '%s' Slime Launcher run configuration for Eclipse.".formatted(options.getName())); + task.getOutputFile().set(task.getProjectLayout().getProjectDirectory().file(runTaskName + ".launch")); + + task.getClasspath() + .from(task.getObjects().fileCollection().from(task.getProviders().provider(sourceSet::getRuntimeClasspath))) + .minus(sourceSetOutputs) + .plus(eclipseOutputs); + + var caches = task.getObjects().directoryProperty().value(task.globalCaches().dir("slime-launcher/cache/%s".formatted(asPath))); + task.getCacheDir().set(caches.map(task.problems.ensureFileLocation())); + task.getMetadataZip().set(metadata.flatMap(SlimeLauncherMetadata::getMetadataZip)); + task.getRunsJson().set(metadata.flatMap(SlimeLauncherMetadata::getRunsJson)); + + task.getOptions().set(options); + }); + + project.getTasks().named("genEclipseRuns", task -> task.dependsOn(genEclipseRun)); + + return project.getTasks().register(runTaskName, SlimeLauncherExec.class, task -> { task.getRunName().set(options.getName()); task.setDescription("Runs the '%s' Slime Launcher run configuration.".formatted(options.getName())); task.classpath(task.getObjectFactory().fileCollection().from(task.getProviderFactory().provider(sourceSet::getRuntimeClasspath))); - task.getJavaLauncher().unset(); var caches = task.getObjectFactory().directoryProperty().value(task.globalCaches().dir("slime-launcher/cache/%s".formatted(asPath))); task.getCacheDir().set(caches.map(task.problems.ensureFileLocation())); @@ -93,10 +118,6 @@ static TaskProvider register(Project project, SourceSet sourc protected abstract @InputFile RegularFileProperty getRunsJson(); - protected abstract @Input @Optional Property getDelegateMainClass(); - - protected abstract @Input @Optional ListProperty getDelegateArgs(); - protected abstract @Input @Optional Property getClient(); private final ForgeGradleProblems problems = this.getObjectFactory().newInstance(ForgeGradleProblems.class); @@ -106,12 +127,10 @@ public SlimeLauncherExec() { this.setGroup("Slime Launcher"); var tool = this.getTool(Tools.SLIMELAUNCHER); - this.setClasspath(tool.getClasspath()); - if (tool.hasMainClass()) this.getMainClass().set(tool.getMainClass()); - this.getJavaLauncher().set(tool.getJavaLauncher()); + this.getJavaLauncher().set(Util.launcherFor(getProject(),tool.getJavaVersion())); this.getModularity().getInferModulePath().set(false); } @@ -138,9 +157,8 @@ public void exec() { var options = ((SlimeLauncherOptionsInternal) this.getOptions().get()).inherit(configs); - mainClass = this.getDelegateMainClass().orElse(options.getMainClass().filter(Util::isPresent)); + mainClass = options.getMainClass().filter(Util::isPresent); args = new ArrayList<>(options.getArgs().getOrElse(List.of())); - args.addAll(this.getDelegateArgs().getOrElse(List.of())); this.jvmArgs(options.getJvmArgs().get()); if (!options.getClasspath().isEmpty()) this.setClasspath(options.getClasspath());