Skip to content

Commit

Permalink
Refactor IDE integration (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte authored Oct 13, 2024
1 parent 050d1a0 commit 951a5c8
Show file tree
Hide file tree
Showing 9 changed files with 847 additions and 542 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package net.neoforged.moddevgradle.internal;

import net.neoforged.elc.configs.GradleLaunchConfig;
import net.neoforged.elc.configs.JavaApplicationLaunchConfig;
import net.neoforged.elc.configs.LaunchConfig;
import net.neoforged.elc.configs.LaunchGroup;
import net.neoforged.moddevgradle.dsl.ModModel;
import net.neoforged.moddevgradle.dsl.RunModel;
import net.neoforged.moddevgradle.internal.utils.ExtensionUtils;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.Directory;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.plugins.ide.eclipse.EclipsePlugin;
import org.gradle.plugins.ide.eclipse.model.Classpath;
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
import org.gradle.plugins.ide.eclipse.model.Library;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.stream.XMLStreamException;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
* Provides integration with Eclipse Buildship and VSCode extensions based on it.
*/
sealed class EclipseIntegration extends IdeIntegration permits VsCodeIntegration {
private static final Logger LOG = LoggerFactory.getLogger(EclipseIntegration.class);

protected final EclipseModel eclipseModel;

protected EclipseIntegration(Project project) {
super(project);
this.eclipseModel = getOrCreateEclipseModel(project);
LOG.debug("Configuring Eclipse model for Eclipse project '{}'.", eclipseModel.getProject().getName());
}

/**
* Attach a source artifact to a binary artifact if the IDE supports it.
*
* @param jarToSourceJarMapping Maps a classpath location containing classes to their source location.
* Locations are usually JAR files but may be folders.
*/
@Override
public void attachSources(Map<Provider<RegularFile>, Provider<RegularFile>> jarToSourceJarMapping) {

var fileClasspath = eclipseModel.getClasspath().getFile();
fileClasspath.whenMerged((Classpath classpath) -> {
for (var mapping : jarToSourceJarMapping.entrySet()) {
var classesPath = mapping.getKey().get().getAsFile();
var sourcesPath = mapping.getValue().get().getAsFile();

for (var entry : classpath.getEntries()) {
if (entry instanceof Library library && classesPath.equals(new File(library.getPath()))) {
library.setSourcePath(classpath.fileReference(sourcesPath));
}
}
}
});
}

@Override
protected void registerProjectSyncTask(TaskProvider<?> task) {
// Make sure our post-sync task runs on Eclipse
eclipseModel.synchronizationTasks(task);
}

@Override
public void configureRuns(Map<RunModel, TaskProvider<PrepareRun>> prepareRunTasks,
Iterable<RunModel> runs) {
// Set up runs if running under buildship and in VS Code
project.afterEvaluate(ignored -> {
for (var run : runs) {
var prepareTask = prepareRunTasks.get(run).get();
addEclipseLaunchConfiguration(project, run, prepareTask);
}
});
}

@Override
public void configureTesting(SetProperty<ModModel> loadedMods,
Property<ModModel> testedMod,
Provider<Directory> runArgsDir,
File gameDirectory,
Provider<RegularFile> programArgsFile,
Provider<RegularFile> vmArgsFile) {
// Eclipse has no concept of JUnit run templates. We cannot configure VM args or similar for all JUnit runs.
}

private static EclipseModel getOrCreateEclipseModel(Project project) {
// Set up stuff for Eclipse
var eclipseModel = ExtensionUtils.findExtension(project, "eclipse", EclipseModel.class);
if (eclipseModel == null) {
project.getPlugins().apply(EclipsePlugin.class);
eclipseModel = ExtensionUtils.findExtension(project, "eclipse", EclipseModel.class);
if (eclipseModel == null) {
throw new GradleException("Even after applying the Eclipse plugin, no 'eclipse' extension was present!");
}
}
return eclipseModel;
}

private void addEclipseLaunchConfiguration(Project project,
RunModel run,
PrepareRun prepareTask) {
if (!prepareTask.getEnabled()) {
LOG.info("Not creating Eclipse run {} since its prepare task {} is disabled", run, prepareTask);
return;
}

// Grab the eclipse model so we can extend it. -> Done on the root project so that the model is available to all subprojects.
// And so that post sync tasks are only run once for all subprojects.

var runIdeName = run.getIdeName().get();
var launchConfigName = runIdeName;
var eclipseProjectName = Objects.requireNonNullElse(eclipseModel.getProject().getName(), project.getName());

// If the user wants to run tasks before the actual execution, we create a launch group to facilitate that
if (!run.getTasksBefore().isEmpty()) {
// Rename the main launch to "Run " ...
launchConfigName = "Run " + runIdeName;

// Creates a launch config to run the preparation tasks
var prepareRunConfig = GradleLaunchConfig.builder(eclipseProjectName)
.tasks(run.getTasksBefore().stream().map(task -> task.get().getPath()).toArray(String[]::new))
.build();
var prepareRunLaunchName = "Prepare " + runIdeName;
writeEclipseLaunchConfig(project, prepareRunLaunchName, prepareRunConfig);

// This is the launch group that will first launch Gradle, and then the game
var withGradleTasksConfig = LaunchGroup.builder()
.entry(LaunchGroup.entry(prepareRunLaunchName)
.enabled(true)
.adoptIfRunning(false)
.mode(LaunchGroup.Mode.RUN)
// See https://github.com/eclipse/buildship/issues/1272
// for why we cannot just wait for termination
.action(LaunchGroup.Action.delay(2)))
.entry(LaunchGroup.entry(launchConfigName)
.enabled(true)
.adoptIfRunning(false)
.mode(LaunchGroup.Mode.INHERIT)
.action(LaunchGroup.Action.none()))
.build();
writeEclipseLaunchConfig(project, runIdeName, withGradleTasksConfig);
}

// This is the actual main launch configuration that launches the game
var config = JavaApplicationLaunchConfig.builder(eclipseProjectName)
.vmArgs(
RunUtils.escapeJvmArg(RunUtils.getArgFileParameter(prepareTask.getVmArgsFile().get())),
RunUtils.escapeJvmArg(getModFoldersProvider(project, run.getLoadedMods(), null).getArgument())
)
.args(RunUtils.escapeJvmArg(RunUtils.getArgFileParameter(prepareTask.getProgramArgsFile().get())))
.envVar(run.getEnvironment().get())
.workingDirectory(run.getGameDirectory().get().getAsFile().getAbsolutePath())
.build(RunUtils.DEV_LAUNCH_MAIN_CLASS);
writeEclipseLaunchConfig(project, launchConfigName, config);
}

protected static ModFoldersProvider getModFoldersProvider(Project project,
Provider<Set<ModModel>> modsProvider,
@Nullable Provider<ModModel> testedMod) {
var folders = RunUtils.buildModFolders(project, modsProvider, testedMod, (sourceSet, output) -> {
output.from(RunUtils.findSourceSetProject(project, sourceSet).getProjectDir().toPath()
.resolve("bin")
.resolve(sourceSet.getName()));
});

var modFoldersProvider = project.getObjects().newInstance(ModFoldersProvider.class);
modFoldersProvider.getModFolders().set(folders);
return modFoldersProvider;
}

private static void writeEclipseLaunchConfig(Project project, String name, LaunchConfig config) {
var file = project.file(".eclipse/configurations/" + name + ".launch");
file.getParentFile().mkdirs();
try (var writer = new FileWriter(file, false)) {
config.write(writer);
} catch (IOException e) {
throw new UncheckedIOException("Failed to write launch file: " + file, e);
} catch (XMLStreamException e) {
throw new RuntimeException("Failed to write launch file: " + file, e);
}
}
}
109 changes: 109 additions & 0 deletions src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package net.neoforged.moddevgradle.internal;

import net.neoforged.moddevgradle.dsl.ModModel;
import net.neoforged.moddevgradle.dsl.RunModel;
import net.neoforged.moddevgradle.internal.utils.ExtensionUtils;
import net.neoforged.moddevgradle.internal.utils.IdeDetection;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.file.Directory;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.TaskProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.Map;

sealed abstract class IdeIntegration permits IntelliJIntegration, EclipseIntegration, NoIdeIntegration {
private static final Logger LOG = LoggerFactory.getLogger(IdeIntegration.class);

/**
* A task we attach other tasks to that should run when the IDE reloads the projects.
*/
private final TaskProvider<Task> ideSyncTask;

protected final Project project;

public IdeIntegration(Project project) {
this.project = project;
this.ideSyncTask = project.getTasks().register("neoForgeIdeSync", task -> {
task.setGroup(ModDevPlugin.INTERNAL_TASK_GROUP);
task.setDescription("A utility task that is used to create necessary files when the Gradle project is synchronized with the IDE project.");
});
this.registerProjectSyncTask(ideSyncTask);
}

public static IdeIntegration of(Project project) {
var ideIntegration = ExtensionUtils.findExtension(project, "mdgInternalIdeIntegration", IdeIntegration.class);
if (ideIntegration == null) {
ideIntegration = createForProject(project);
project.getExtensions().add(IdeIntegration.class, "mdgInternalIdeIntegration", ideIntegration);
}
return ideIntegration;
}

private static IdeIntegration createForProject(Project project) {
if (IdeDetection.isVsCode()) {
// VSCode internally uses Eclipse and as such, we need to prioritize it over the pure Eclipse integration
LOG.debug("Activating VSCode integration for project {}.", project.getPath());
return new VsCodeIntegration(project);
} else if (IdeDetection.isEclipse()) {
LOG.debug("Activating Eclipse integration for project {}.", project.getPath());
return new EclipseIntegration(project);
} else if (IdeDetection.isIntelliJSync()) {
LOG.debug("Activating IntelliJ integration for project {}.", project.getPath());
return new IntelliJIntegration(project);
} else {
return new NoIdeIntegration(project);
}
}

/**
* Attach a source artifact to a binary artifact if the IDE supports it.
*
* @param jarToSourceJarMapping Maps a classpath location containing classes to their source location.
* Locations are usually JAR files but may be folders.
* @see #shouldUseCombinedSourcesAndClassesArtifact() This method will not work if the IDE doesn't support attaching sources.
*/
void attachSources(Map<Provider<RegularFile>, Provider<RegularFile>> jarToSourceJarMapping) {
}

/**
* Only IntelliJ needs the combined artifact.
* We also use this model when Gradle tasks are being run from IntelliJ, not only if IntelliJ is reloading
* the project.
* This prevents the classpath during debugging from the classpath detected by IntelliJ itself.
* For Eclipse, we can attach the sources via the Eclipse project model.
*/
boolean shouldUseCombinedSourcesAndClassesArtifact() {
return false;
}

/**
* Registers a task to be run when the IDE reloads the Gradle project.
*/
public final void runTaskOnProjectSync(TaskProvider<?> task) {
ideSyncTask.configure(ideSyncTask -> ideSyncTask.dependsOn(task));
}

/**
* To be implemented by specific IDE integrations to register a task to be run on reload with the IDE.
*/
protected abstract void registerProjectSyncTask(TaskProvider<?> task);

void configureRuns(Map<RunModel, TaskProvider<PrepareRun>> prepareRunTasks, Iterable<RunModel> runs) {
}

void configureTesting(SetProperty<ModModel> loadedMods,
Property<ModModel> testedMod,
Provider<Directory> runArgsDir,
File gameDirectory,
Provider<RegularFile> programArgsFile,
Provider<RegularFile> vmArgsFile) {
}

}
Loading

0 comments on commit 951a5c8

Please sign in to comment.