Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shared Caching for NeoForm Task Outputs #74

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ our official [Documentation](https://docs.neoforged.net/neogradle/docs/).

To see the latest available version of NeoGradle, visit the [NeoForged project page](https://projects.neoforged.net/neoforged/neogradle).

## Configuring Shared NeoForm Cache

NeoForm is the toolkit used to provide a Minecraft JAR-File suitable for compiling your mod against.
Since this is a rather resource-intensive task, the intermediary steps and final result of that
process can be cached outside the project folder.

The settings of this caching subsystem can be changed using [Gradle properties](https://docs.gradle.org/current/userguide/project_properties.html).

| Property | Description |
|----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `neogradle.subsystems.neoFormCache.enabled` | Can be used to fully disable the caching by setting this to `false`. The default is `true`. |
| `neogradle.subsystems.neoFormCache.CacheDirectory` | The path to a directory where the cache is stored. Defaults to `${GRADLE_USER_HOME}/caches/neoForm` (see [Gradle Directories](https://docs.gradle.org/current/userguide/directory_layout.html)) |

## Override Decompiler Settings

The settings used by the decompiler when preparing Minecraft dependencies can be overridden
Expand Down
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -218,5 +218,14 @@ subprojects.forEach { subProject ->
project.changelog.publish publication
}
}

evalSubProject.tasks.withType(org.gradle.api.publish.maven.tasks.AbstractPublishToMaven).configureEach { task ->
doLast {
if (task.state.didWork) {
MavenPublication publication = task.publication
println("Published ${publication.groupId}:${publication.artifactId}:${publication.version}")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.neoforged.gradle.common;

import net.neoforged.gradle.common.caching.CentralCacheService;
import net.neoforged.gradle.common.caching.SharedCacheService;
import net.neoforged.gradle.common.dependency.ExtraJarDependencyManager;
import net.neoforged.gradle.common.extensions.*;
import net.neoforged.gradle.common.extensions.dependency.creation.ProjectBasedDependencyCreator;
Expand All @@ -21,6 +22,7 @@
import net.neoforged.gradle.dsl.common.extensions.*;
import net.neoforged.gradle.dsl.common.extensions.dependency.replacement.DependencyReplacement;
import net.neoforged.gradle.dsl.common.extensions.repository.Repository;
import net.neoforged.gradle.dsl.common.extensions.subsystems.NeoFormCache;
import net.neoforged.gradle.dsl.common.extensions.subsystems.Subsystems;
import net.neoforged.gradle.dsl.common.runs.run.Run;
import net.neoforged.gradle.dsl.common.runs.type.RunType;
Expand All @@ -42,10 +44,11 @@
import java.util.Set;

public class CommonProjectPlugin implements Plugin<Project> {

public static final String ASSETS_SERVICE = "ng_assets";
public static final String LIBRARIES_SERVICE = "ng_libraries";

public static final String NEOFORM_CACHE_SERVICE = "ng_neoform_cache";

@Override
public void apply(Project project) {
//Apply the evaluation extension to monitor immediate execution of indirect tasks when evaluation already happened.
Expand All @@ -57,11 +60,11 @@ public void apply(Project project) {
project.getPluginManager().apply(IdeaPlugin.class);
project.getPluginManager().apply(IdeaExtPlugin.class);
project.getPluginManager().apply(EclipsePlugin.class);

//Register the assets service
CentralCacheService.register(project, ASSETS_SERVICE, FileCacheUtils.getAssetsCacheDirectory(project));
CentralCacheService.register(project, LIBRARIES_SERVICE, FileCacheUtils.getLibrariesCacheDirectory(project));

project.getExtensions().create("allRuntimes", RuntimesExtension.class);
project.getExtensions().create(IdeManagementExtension.class, "ideManager", IdeManagementExtension.class, project);
project.getExtensions().create(ArtifactDownloader.class, "artifactDownloader", ArtifactDownloaderExtension.class, project);
Expand All @@ -78,14 +81,21 @@ public void apply(Project project) {
extensionManager.registerExtension("mappings", Mappings.class, (p) -> p.getObjects().newInstance(MappingsExtension.class, p));
extensionManager.registerExtension("subsystems", Subsystems.class, (p) -> p.getObjects().newInstance(SubsystemsExtension.class, p));

// The shared cache can only be registered after the subsystems extension
SharedCacheService.register(project, NEOFORM_CACHE_SERVICE, params -> {
NeoFormCache cacheSettings = project.getExtensions().getByType(Subsystems.class).getNeoFormCache();
params.getEnabled().set(cacheSettings.getEnabled().orElse(params.getEnabled().get()));
params.getCacheDirectory().set(cacheSettings.getCacheDirectory().orElse(params.getCacheDirectory().get()));
});

OfficialNamingChannelConfigurator.getInstance().configure(project);

project.getTasks().register("handleNamingLicense", DisplayMappingsLicenseTask.class, task -> {
task.getLicense().set(project.provider(() -> {
final Mappings mappings = project.getExtensions().getByType(Mappings.class);
if (mappings.getChannel().get().getHasAcceptedLicense().get())
return null;

return mappings.getChannel().get().getLicenseText().get();
}));
});
Expand All @@ -107,15 +117,15 @@ public void apply(Project project) {
RunsConstants.Extensions.RUN_TYPES,
project.getObjects().domainObjectContainer(RunType.class, name -> project.getObjects().newInstance(RunType.class, name))
);

project.getExtensions().add(
RunsConstants.Extensions.RUNS,
project.getObjects().domainObjectContainer(Run.class, name -> RunsUtil.create(project, name))
);

IdeRunIntegrationManager.getInstance().setup(project);
}

private void applyAfterEvaluate(final Project project) {
RuntimesExtension runtimesExtension = project.getExtensions().getByType(RuntimesExtension.class);
runtimesExtension.bakeDefinitions();
Expand All @@ -139,9 +149,9 @@ private void applyAfterEvaluate(final Project project) {

if (run.getConfigureFromDependencies().get()) {
final RunImpl runImpl = (RunImpl) run;

final Set<CommonRuntimeDefinition<?>> definitionSet = new HashSet<>();

runImpl.getModSources().get().forEach(sourceSet -> {
try {
final Optional<CommonRuntimeDefinition<?>> definition = TaskDependencyUtils.findRuntimeDefinition(project, sourceSet);
Expand All @@ -150,14 +160,14 @@ private void applyAfterEvaluate(final Project project) {
throw new RuntimeException("Failed to configure run: " + run.getName() + " there are multiple runtime definitions found for the source set: " + sourceSet.getName(), e);
}
});

definitionSet.forEach(definition -> {
definition.configureRun(runImpl);
definition.configureRun(runImpl);
});
}
}
}));

IdeRunIntegrationManager.getInstance().apply(project);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package net.neoforged.gradle.common.caching;

import org.jetbrains.annotations.Nullable;

import java.nio.file.Path;
import java.nio.file.Paths;

public final class CacheKey {
private static final String CACHE_DOMAIN_ALL = "all";
@Nullable
private final String cacheDomain;
private final String hashCode;
private final String sourceMaterial;

CacheKey(@Nullable String cacheDomain, String hashCode, String sourceMaterial) {
this.cacheDomain = cacheDomain;
this.hashCode = hashCode;
this.sourceMaterial = sourceMaterial;
}

@Nullable
String getCacheDomain() {
return cacheDomain;
}

String getHashCode() {
return hashCode;
}

String getSourceMaterial() {
return sourceMaterial;
}

Path asPath(@Nullable String extension) {
String filename = hashCode;
if (extension != null) {
filename += "." + extension;
}

return Paths.get(cacheDomain != null ? cacheDomain : CACHE_DOMAIN_ALL, filename);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package net.neoforged.gradle.common.caching;

import net.neoforged.gradle.util.HashFunction;
import org.apache.commons.io.output.NullOutputStream;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Provides in-memory caching of file-hashes based on last-modification time and size.
*/
class FileHashing {
private final Map<Path, CachedHash> cachedHashes = new ConcurrentHashMap<>();

public byte[] getMd5Hash(Path path) {
return cachedHashes.compute(path, CachedHash::compute).hashValue;
}

private static final class CachedHash {
private final long lastModified;
private final long fileSize;
private final byte[] hashValue;

public static CachedHash compute(Path path, @Nullable CachedHash cachedHash) {
try {
// Instead of reading size + last modified separately, we use this function to make race conditions
// less likely. We still don't know if the underlying OS APIs return this information atomically,
// but if they do, we at least make use of that fact.
BasicFileAttributes attributes = Files.getFileAttributeView(path, BasicFileAttributeView.class)
.readAttributes();
long lastModified = attributes.lastModifiedTime().toMillis();
long fileSize = attributes.size();

if (cachedHash != null && cachedHash.lastModified == lastModified && cachedHash.fileSize == fileSize) {
return cachedHash;
}

// Compute the digest in a streaming fashion without reading the full file into memory
MessageDigest digest = HashFunction.MD5.get();
try (DigestOutputStream out = new DigestOutputStream(NullOutputStream.NULL_OUTPUT_STREAM, digest)) {
Files.copy(path, out);
}

return new CachedHash(digest.digest(), lastModified, fileSize);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

public CachedHash(byte[] hashValue, long lastModified, long fileSize) {
this.hashValue = hashValue;
this.lastModified = lastModified;
this.fileSize = fileSize;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.neoforged.gradle.common.caching;

import org.apache.commons.codec.binary.Hex;

import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

final class HashCodeBuilder {
private final FileHashing fileHashing;
private final StringBuilder sourceMaterial = new StringBuilder();
private final MessageDigest digest;

public HashCodeBuilder(FileHashing fileHashing) {
this.fileHashing = fileHashing;
// Relativize any path to gradle home or project root,
// which will work for anything but maven local dependencies
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Standard algorithm MD5 is missing.", e);
}
}

public void add(Path path) {
byte[] fileHash = fileHashing.getMd5Hash(path);
String fileHashHex = Hex.encodeHexString(fileHash);
add(fileHash, "HASHED-CONTENT(" + path + ") = " + fileHashHex);
}

public void add(String data) {
add(data.getBytes(StandardCharsets.UTF_8), "STRING(" + data + ")");
}

public void add(byte[] data, String sourceMaterial) {
digest.update(data);
this.sourceMaterial.append(sourceMaterial).append('\n');
}

public String buildHashCode() {
return Hex.encodeHexString(digest.digest());
}

public String buildSourceMaterial() {
return sourceMaterial.toString();
}
}
Loading