Skip to content

Commit

Permalink
Fix data file publishing (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matyrobbrt authored Aug 11, 2024
1 parent 815d9cb commit 3128c00
Show file tree
Hide file tree
Showing 13 changed files with 468 additions and 129 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ configurations {
dependencies {
compileOnly gradleApi()
compileOnly "com.intellij:annotations:9.0.4"
testCompileOnly "com.intellij:annotations:9.0.4"
shaded "com.google.code.gson:gson:2.11.0"
implementation "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext:1.1.8"
shaded "net.neoforged:EclipseLaunchConfigs:0.1.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,41 @@

import org.gradle.api.file.ConfigurableFileCollection;

import javax.inject.Inject;
import java.util.function.Consumer;

/**
* Holds data files (such as ATs) to be used or exposed.
*/
public abstract class DataFileCollection {
private final Consumer<Object> publishArtifactCallback;

@Inject
public DataFileCollection(Consumer<Object> publishArtifactCallback) {
this.publishArtifactCallback = publishArtifactCallback;
}

/**
* Add the given paths to the {@linkplain #getFiles() file collection}.
* <p>
* Using this method replaces any previously present default value.
* Please note that {@code src/main/resources/META-INF/accesstransformer.cfg} is automatically
* included for access transformers, if it exists.
*/
public void from(Object... paths) {
getFiles().from(paths);
}

/**
* Add the given paths to the {@linkplain #getPublished() published file collection}.
* <p>
* Using this method replaces any previously present default value.
* Configures the given files to be published alongside this project.
* This can include files that are also passed to {@link #from}, but is not required to.
* For allowed parameters, see {@link org.gradle.api.artifacts.dsl.ArtifactHandler}.
*/
public void publish(Object... paths) {
getPublished().from(paths);
public void publish(Object artifactNotation) {
publishArtifactCallback.accept(artifactNotation);
}

/**
* {@return the files this collection contains}
*/
public abstract ConfigurableFileCollection getFiles();

/**
* {@return the files that should be published and that can be consumed by dependents}
*/
public abstract ConfigurableFileCollection getPublished();
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
import org.gradle.api.tasks.TaskProvider;

import javax.inject.Inject;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
* This is the top-level {@code neoForge} extension, used to configure the moddev plugin.
Expand All @@ -35,31 +31,15 @@ public abstract class NeoForgeExtension {
private final DataFileCollection interfaceInjectionData;

@Inject
public NeoForgeExtension(Project project) {
public NeoForgeExtension(Project project, DataFileCollection accessTransformers, DataFileCollection interfaceInjectionData) {
this.project = project;
mods = project.container(ModModel.class);
runs = project.container(RunModel.class);
parchment = project.getObjects().newInstance(Parchment.class);
neoFormRuntime = project.getObjects().newInstance(NeoFormRuntime.class);
unitTest = project.getObjects().newInstance(UnitTest.class);

accessTransformers = project.getObjects().newInstance(DataFileCollection.class);
interfaceInjectionData = project.getObjects().newInstance(DataFileCollection.class);

getAccessTransformers().getFiles().convention(project.provider(() -> {
var collection = project.getObjects().fileCollection();

// Only return this when it actually exists
var mainSourceSet = ExtensionUtils.getSourceSets(project).getByName(SourceSet.MAIN_SOURCE_SET_NAME);
for (var resources : mainSourceSet.getResources().getSrcDirs()) {
var defaultPath = new File(resources, "META-INF/accesstransformer.cfg");
if (project.file(defaultPath).exists()) {
return collection.from(defaultPath.getAbsolutePath());
}
}

return collection;
}));
this.accessTransformers = accessTransformers;
this.interfaceInjectionData = interfaceInjectionData;
getValidateAccessTransformers().convention(false);
}

Expand Down
107 changes: 76 additions & 31 deletions src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@
import net.neoforged.vsclc.attribute.PathLike;
import net.neoforged.vsclc.attribute.ShortCmdBehaviour;
import net.neoforged.vsclc.writer.WritingMode;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.ConfigurablePublishArtifact;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ExternalModuleDependency;
import org.gradle.api.artifacts.ModuleDependency;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.AttributeContainer;
import org.gradle.api.attributes.Bundling;
import org.gradle.api.attributes.Category;
import org.gradle.api.attributes.DocsType;
Expand Down Expand Up @@ -75,7 +74,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -131,7 +129,42 @@ public void apply(Project project) {
// We use this directory to store intermediate files used during moddev
var modDevBuildDir = layout.getBuildDirectory().dir("moddev");

var extension = project.getExtensions().create(NeoForgeExtension.NAME, NeoForgeExtension.class);
// Create an access transformer configuration
var accessTransformers = dataFileConfiguration(
project,
"accessTransformers",
"AccessTransformers to widen visibility of Minecraft classes/fields/methods",
"accesstransformer"
);
accessTransformers.extension.getFiles().convention(project.provider(() -> {
var collection = project.getObjects().fileCollection();

// Only return this when it actually exists
var mainSourceSet = ExtensionUtils.getSourceSets(project).getByName(SourceSet.MAIN_SOURCE_SET_NAME);
for (var resources : mainSourceSet.getResources().getSrcDirs()) {
var defaultPath = new File(resources, "META-INF/accesstransformer.cfg");
if (project.file(defaultPath).exists()) {
return collection.from(defaultPath.getAbsolutePath());
}
}

return collection;
}));

// Create a configuration for grabbing interface injection data
var interfaceInjectionData = dataFileConfiguration(
project,
"interfaceInjectionData",
"Interface injection data adds extend/implements clauses for interfaces to Minecraft code at development time",
"interfaceinjection"
);

var extension = project.getExtensions().create(
NeoForgeExtension.NAME,
NeoForgeExtension.class,
accessTransformers.extension,
interfaceInjectionData.extension
);
var dependencyFactory = project.getDependencyFactory();

// When a NeoForge version is specified, we use the dependencies published by that, and otherwise
Expand Down Expand Up @@ -183,14 +216,6 @@ public void apply(Project project) {
});
});

// Create an access transformer configuration
var accessTransformers = dataFileConfiguration(project, "accessTransformers", "AccessTransformers to widen visibility of Minecraft classes/fields/methods",
"accesstransformer", extension.getAccessTransformers());

// Create a configuration for grabbing interface injection data
var interfaceInjectionData = dataFileConfiguration(project, "interfaceInjectionData", "Interface injection data adds extend/implements clauses for interfaces to Minecraft code at development time",
"interfaceinjection", extension.getInterfaceInjectionData());

// Add a filtered parchment repository automatically if enabled
var parchment = extension.getParchment();
var parchmentData = configurations.create("parchmentData", spec -> {
Expand All @@ -216,8 +241,8 @@ public void apply(Project project) {
task.setGroup(INTERNAL_TASK_GROUP);
task.setDescription("Creates the NeoForge and Minecraft artifacts by invoking NFRT.");

task.getAccessTransformers().from(accessTransformers);
task.getInterfaceInjectionData().from(interfaceInjectionData);
task.getAccessTransformers().from(accessTransformers.configuration);
task.getInterfaceInjectionData().from(interfaceInjectionData.configuration);
task.getValidateAccessTransformers().set(extension.getValidateAccessTransformers());
task.getParchmentData().from(parchmentData);
task.getParchmentEnabled().set(parchment.getEnabled());
Expand Down Expand Up @@ -1051,41 +1076,61 @@ private static void addVscodeLaunchConfiguration(Project project,
.withCurrentWorkingDirectory(PathLike.ofNio(run.getGameDirectory().get().getAsFile().toPath()));
}

private static Configuration dataFileConfiguration(Project project, String name, String description, String category, DataFileCollection collection) {
var depFactory = project.getDependencyFactory();
Action<AttributeContainer> attributeAction = attributes -> attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, category));
record DataFileCollectionWrapper(DataFileCollection extension, Configuration configuration) {
}

private static DataFileCollectionWrapper dataFileConfiguration(Project project, String name, String description, String category) {
var configuration = project.getConfigurations().create(name, spec -> {
spec.setDescription(description);
spec.setCanBeConsumed(false);
spec.setCanBeResolved(true);
spec.withDependencies(dependencies -> dependencies.add(depFactory.create(collection.getFiles())));
spec.attributes(attributes -> attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, category)));
});

var elementsConfiguration = project.getConfigurations().create(name + "Elements", spec -> {
spec.setDescription("Published data files for " + name);
spec.setCanBeConsumed(true);
spec.setCanBeResolved(false);
spec.withDependencies(dependencies -> dependencies.add(depFactory.create(collection.getPublished())));
spec.attributes(attributes -> attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, category)));
});

// Set up the publishing conditionally
AdhocComponentWithVariants java = (AdhocComponentWithVariants) project.getComponents().getByName("java");
// Set up the variant publishing conditionally
var java = (AdhocComponentWithVariants) project.getComponents().getByName("java");
java.addVariantsFromConfiguration(elementsConfiguration, variant -> {
// This should be invoked lazily, so checking if the artifacts are empty is fine:
// "The details object used to determine what to do with a configuration variant **when publishing**."
if (variant.getConfigurationVariant().getArtifacts().isEmpty()) {
variant.skip();
}
});

AtomicBoolean configured = new AtomicBoolean();
Runnable configurePublishing = () -> {
if (configured.compareAndSet(false, true)) {
java.addVariantsFromConfiguration(elementsConfiguration, variant -> {
var depFactory = project.getDependencyFactory();
Consumer<Object> publishCallback = new Consumer<>() {
ConfigurablePublishArtifact firstArtifact;
int artifactCount;

@Override
public void accept(Object artifactNotation) {
elementsConfiguration.getDependencies().add(depFactory.create(project.files(artifactNotation)));
project.getArtifacts().add(elementsConfiguration.getName(), artifactNotation, artifact -> {
if (firstArtifact == null) {
firstArtifact = artifact;
artifact.setClassifier(category);
artifactCount = 1;
} else {
if (artifactCount == 1) {
firstArtifact.setClassifier(category + artifactCount);
}
artifact.setClassifier(category + (++artifactCount));
}
});
}
};

elementsConfiguration.getAllArtifacts().configureEach(artifact -> configurePublishing.run());
elementsConfiguration.getArtifacts().configureEach(artifact -> configurePublishing.run());

configuration.attributes(attributeAction);
elementsConfiguration.attributes(attributeAction);
var extension = project.getObjects().newInstance(DataFileCollection.class, publishCallback);
configuration.getDependencies().add(depFactory.create(extension.getFiles()));

return configuration;
return new DataFileCollectionWrapper(extension, configuration);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package net.neoforged.moddevgradle.functional;

import org.intellij.lang.annotations.Language;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

abstract class AbstractFunctionalTest {
static final String DEFAULT_NEOFORGE_VERSION = "21.0.133-beta";

static final Map<String, String> DEFAULT_PLACEHOLDERS = Map.of(
"DEFAULT_NEOFORGE_VERSION", DEFAULT_NEOFORGE_VERSION
);

@TempDir
File testProjectDir;
File settingsFile;
File buildFile;

@BeforeEach
final void setBaseFiles() {
settingsFile = new File(testProjectDir, "settings.gradle");
buildFile = new File(testProjectDir, "build.gradle");
}

protected final void writeFile(File destination, String content) throws IOException {
Files.writeString(destination.toPath(), content);
}

protected final void writeProjectFile(String relativePath, String content) throws IOException {
var destination = testProjectDir.toPath().resolve(relativePath);
Files.createDirectories(destination.getParent());
Files.writeString(destination, content);
}

void writeGroovySettingsScript(@Language("gradle") String text, Object... args) throws IOException {
writeFile(settingsFile, interpolateTemplate(text, args));
}

void writeGroovyBuildScript(@Language("gradle") String text, Object... args) throws IOException {
writeFile(buildFile, interpolateTemplate(text, args));
}

private static String interpolateTemplate(@Language("gradle") String text, Object[] args) {
var m = Pattern.compile("\\{(\\d+|[A-Z_]+)}");
var body = m.matcher(text).replaceAll(matchResult -> {
Object arg;
var key = matchResult.group(1);
try {
int index = Integer.parseUnsignedInt(key);
arg = args[index];
} catch (NumberFormatException ignored) {
arg = DEFAULT_PLACEHOLDERS.get(key);
if (arg == null) {
throw new IllegalArgumentException("Invalid placeholder: " + key);
}
}
if (arg instanceof Path path) {
arg = path.toAbsolutePath().toString().replace('\\', '/');
} else if (arg instanceof File file) {
arg = file.getAbsolutePath().replace('\\', '/');
}
return Matcher.quoteReplacement(arg.toString());
});
return body;
}

protected final void clearProjectDir() {
clearContent(testProjectDir);
}

private void clearContent(File file) {
var content = file.listFiles();
if (content != null) {
for (var child : content) {
if (child.isDirectory()) {
clearContent(child);
}
child.delete();
}
}
}
}
Loading

0 comments on commit 3128c00

Please sign in to comment.