mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
Scary CI Builds.
This commit is contained in:
@@ -33,7 +33,9 @@ jobs:
|
|||||||
run: ./gradlew -p adapters/forge test -PuseLocalVolmLib=false --console=plain --stacktrace
|
run: ./gradlew -p adapters/forge test -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||||
- name: Run modded shared tests (NeoForge)
|
- name: Run modded shared tests (NeoForge)
|
||||||
run: ./gradlew -p adapters/neoforge test -PuseLocalVolmLib=false --console=plain --stacktrace
|
run: ./gradlew -p adapters/neoforge test -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||||
- name: Test modded artifact verifier
|
- name: Test artifact verifiers
|
||||||
run: ./gradlew -p buildSrc test --console=plain --stacktrace
|
run: ./gradlew -p buildSrc test --console=plain --stacktrace
|
||||||
|
- name: Verify Bukkit artifact
|
||||||
|
run: ./gradlew verifyBukkitArtifact --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||||
- name: Verify modded artifacts
|
- name: Verify modded artifacts
|
||||||
run: ./gradlew verifyModdedArtifacts --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
|
run: ./gradlew verifyModdedArtifacts --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||||
|
|||||||
+50
-1
@@ -48,6 +48,8 @@ project(':core') {
|
|||||||
version = rootProject.version
|
version = rootProject.version
|
||||||
}
|
}
|
||||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.2')
|
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.2')
|
||||||
|
// The Bukkit jar runs Paper 26.1.2 through 26.2 (compile-low pins); the loader jars stay 26.2-only.
|
||||||
|
String bukkitMinecraftRange = providers.gradleProperty('bukkitMinecraftRange').getOrElse('26.1.2-26.2')
|
||||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3')
|
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3')
|
||||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.0.4')
|
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.0.4')
|
||||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.12-beta')
|
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.12-beta')
|
||||||
@@ -67,7 +69,7 @@ Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
|||||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||||
}
|
}
|
||||||
String bukkitArtifactName = irisArtifactName('CraftBukkit', minecraftVersion)
|
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
|
||||||
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
|
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
|
||||||
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
||||||
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
||||||
@@ -477,6 +479,53 @@ tasks.register('verifyModdedArtifacts') {
|
|||||||
dependsOn('verifyFabricArtifact', 'verifyForgeArtifact', 'verifyNeoforgeArtifact')
|
dependsOn('verifyFabricArtifact', 'verifyForgeArtifact', 'verifyNeoforgeArtifact')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Entries the Bukkit artifact only ever reaches by name - NMS binding lookup, the SIMD kernels, the
|
||||||
|
// instrumentation agent, the JarScanner-driven command and service discovery, the Matter codec - plus
|
||||||
|
// the two plugin descriptors and the slimjar manifests. Nothing in the reference graph points at
|
||||||
|
// them, so a shadowJar exclude that drops one is silent until a server hits that path.
|
||||||
|
List<String> requiredBukkitArtifactEntries = [
|
||||||
|
'plugin.yml',
|
||||||
|
'paper-plugin.yml',
|
||||||
|
'slimjar.dat',
|
||||||
|
'slimjar-resolutions.dat',
|
||||||
|
'agent.jar',
|
||||||
|
'art/arcane/iris/Iris.class',
|
||||||
|
'art/arcane/iris/IrisBootstrap.class',
|
||||||
|
'art/arcane/iris/core/nms/INMS.class',
|
||||||
|
'art/arcane/iris/core/nms/v26_2_R1/NMSBinding.class',
|
||||||
|
'art/arcane/iris/core/nms/v1X/NMSBinding1X.class',
|
||||||
|
'art/arcane/iris/engine/platform/PlatformChunkGenerator.class',
|
||||||
|
'art/arcane/iris/core/lifecycle/WorldLifecycleStaging.class',
|
||||||
|
'art/arcane/iris/util/simd/VectorSimdKernels.class',
|
||||||
|
'art/arcane/iris/util/simd/VectorNoiseKernels2D.class',
|
||||||
|
'art/arcane/iris/util/project/agent/Agent.class',
|
||||||
|
'art/arcane/iris/util/common/misc/getHardware.class',
|
||||||
|
'art/arcane/iris/util/caffeine/cache/Caffeine.class',
|
||||||
|
'art/arcane/volmlib/util/io/JarScanner.class',
|
||||||
|
'art/arcane/volmlib/util/director/runtime/DirectorRuntimeEngine.class',
|
||||||
|
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
|
||||||
|
'art/arcane/volmlib/util/matter/Matter.class'
|
||||||
|
]
|
||||||
|
|
||||||
|
tasks.register('verifyBukkitArtifact') {
|
||||||
|
group = 'verification'
|
||||||
|
dependsOn('jar')
|
||||||
|
File artifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
|
||||||
|
inputs.file(artifact)
|
||||||
|
doLast {
|
||||||
|
BukkitArtifactVerifier.verify(artifact, requiredBukkitArtifactEntries, 17, 400, 16)
|
||||||
|
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('buildBukkit').configure {
|
||||||
|
dependsOn('verifyBukkitArtifact')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('iris').configure {
|
||||||
|
dependsOn('verifyBukkitArtifact')
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('buildFabric').configure {
|
tasks.named('buildFabric').configure {
|
||||||
dependsOn('verifyFabricArtifact')
|
dependsOn('verifyFabricArtifact')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import org.gradle.api.GradleException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Packaging gate for the CraftBukkit artifact. Every shadowJar exclude is silent at build time, so
|
||||||
|
* this re-derives the class reference graph over the shipped jar and fails on anything the excludes
|
||||||
|
* left dangling, plus asserts the entries that are only ever reached by name.
|
||||||
|
*/
|
||||||
|
public final class BukkitArtifactVerifier {
|
||||||
|
// Packages the artifact is expected to ship in full. A CONSTANT_Class entry naming a missing
|
||||||
|
// class under one of these is a NoClassDefFoundError waiting for the right code path.
|
||||||
|
private static final List<String> SHIPPED_PREFIXES = List.of(
|
||||||
|
"art/arcane/iris/",
|
||||||
|
"art/arcane/volmlib/",
|
||||||
|
"com/google/gson/",
|
||||||
|
"com/googlecode/concurrentlinkedhashmap/"
|
||||||
|
);
|
||||||
|
// Relocation targets for the libraries slimjar downloads and relocates at runtime. Compiled
|
||||||
|
// references to them are correct and the classes are correctly absent from the jar.
|
||||||
|
private static final List<String> RUNTIME_DOWNLOADED_PREFIXES = List.of(
|
||||||
|
"art/arcane/iris/util/paralithic/",
|
||||||
|
"art/arcane/iris/util/paper/",
|
||||||
|
"art/arcane/iris/util/kyori/",
|
||||||
|
"art/arcane/iris/util/metrics/",
|
||||||
|
"art/arcane/iris/util/sentry/",
|
||||||
|
"art/arcane/iris/util/maven/",
|
||||||
|
"art/arcane/iris/util/plexus/",
|
||||||
|
"art/arcane/iris/util/sisu/",
|
||||||
|
"art/arcane/iris/util/aether/",
|
||||||
|
"art/arcane/iris/util/guice/",
|
||||||
|
"art/arcane/iris/util/dom4j/",
|
||||||
|
"art/arcane/iris/util/jaxen/"
|
||||||
|
);
|
||||||
|
private static final String CAFFEINE_CACHE_PACKAGE = "art/arcane/iris/util/caffeine/cache/";
|
||||||
|
private static final String MATTER_SLICE_PACKAGE = "art/arcane/volmlib/util/matter/slices/";
|
||||||
|
private static final String LANGUAGE_DIRECTORY = "languages/";
|
||||||
|
|
||||||
|
private BukkitArtifactVerifier() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void verify(File artifact, List<String> requiredEntries, int minimumLocales,
|
||||||
|
int minimumCaffeineFactories, int minimumMatterSlices) {
|
||||||
|
if (!artifact.isFile()) {
|
||||||
|
throw new GradleException("Missing Bukkit Iris artifact: " + artifact.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (JarFile jar = new JarFile(artifact)) {
|
||||||
|
for (String requiredEntry : requiredEntries) {
|
||||||
|
if (jar.getJarEntry(requiredEntry) == null) {
|
||||||
|
throw new GradleException(artifact.getName() + " is missing " + requiredEntry
|
||||||
|
+ ". If this run also requested a task whose name contains \"test\", :core:processResources"
|
||||||
|
+ " was disabled for it and the artifact is incomplete by construction - build the jar in"
|
||||||
|
+ " its own invocation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> shippedClasses = new LinkedHashSet<>();
|
||||||
|
int locales = 0;
|
||||||
|
int caffeineFactories = 0;
|
||||||
|
int matterSlices = 0;
|
||||||
|
Enumeration<JarEntry> entries = jar.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
JarEntry entry = entries.nextElement();
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = entry.getName();
|
||||||
|
if (name.startsWith(LANGUAGE_DIRECTORY) && name.endsWith(".json")) {
|
||||||
|
locales++;
|
||||||
|
}
|
||||||
|
if (!name.endsWith(".class")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalName = name.substring(0, name.length() - ".class".length());
|
||||||
|
shippedClasses.add(internalName);
|
||||||
|
if (isGeneratedCaffeineFactory(internalName)) {
|
||||||
|
caffeineFactories++;
|
||||||
|
}
|
||||||
|
if (internalName.startsWith(MATTER_SLICE_PACKAGE)) {
|
||||||
|
matterSlices++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locales < minimumLocales) {
|
||||||
|
throw new GradleException(artifact.getName() + " ships " + locales + " locale files; expected at least "
|
||||||
|
+ minimumLocales);
|
||||||
|
}
|
||||||
|
// Caffeine picks its cache and node implementation with MethodHandles.Lookup.findClass on a
|
||||||
|
// name built from the builder's feature flags. Nothing references these statically, so only a
|
||||||
|
// population check can tell that an exclude or minimize() ate them.
|
||||||
|
if (caffeineFactories < minimumCaffeineFactories) {
|
||||||
|
throw new GradleException(artifact.getName() + " ships " + caffeineFactories
|
||||||
|
+ " generated Caffeine cache classes; expected at least " + minimumCaffeineFactories
|
||||||
|
+ ". Caffeine resolves these by name and cannot survive static pruning");
|
||||||
|
}
|
||||||
|
// Matter.read() resolves slice types from the canonical name stored in the payload.
|
||||||
|
if (matterSlices < minimumMatterSlices) {
|
||||||
|
throw new GradleException(artifact.getName() + " ships " + matterSlices
|
||||||
|
+ " Matter slice types; expected at least " + minimumMatterSlices
|
||||||
|
+ ". Matter payloads name these types directly");
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeMap<String, String> dangling = new TreeMap<>();
|
||||||
|
entries = jar.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
JarEntry entry = entries.nextElement();
|
||||||
|
if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String reference : ClassReferences.read(readEntryBytes(jar, entry))) {
|
||||||
|
if (shippedClasses.contains(reference)
|
||||||
|
|| !startsWithAny(reference, SHIPPED_PREFIXES)
|
||||||
|
|| startsWithAny(reference, RUNTIME_DOWNLOADED_PREFIXES)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dangling.putIfAbsent(reference, entry.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dangling.isEmpty()) {
|
||||||
|
StringBuilder message = new StringBuilder(artifact.getName())
|
||||||
|
.append(" references classes it does not ship. A shadowJar exclude removed a class that is")
|
||||||
|
.append(" still in use:");
|
||||||
|
dangling.entrySet().stream().limit(12).forEach(missing -> message.append("\n ")
|
||||||
|
.append(missing.getKey())
|
||||||
|
.append(" (referenced by ")
|
||||||
|
.append(missing.getValue())
|
||||||
|
.append(')'));
|
||||||
|
if (dangling.size() > 12) {
|
||||||
|
message.append("\n ... and ").append(dangling.size() - 12).append(" more");
|
||||||
|
}
|
||||||
|
throw new GradleException(message.toString());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new GradleException("Unable to verify Bukkit Iris artifact " + artifact.getAbsolutePath(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isGeneratedCaffeineFactory(String internalName) {
|
||||||
|
if (!internalName.startsWith(CAFFEINE_CACHE_PACKAGE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String simpleName = internalName.substring(CAFFEINE_CACHE_PACKAGE.length());
|
||||||
|
if (simpleName.isEmpty() || simpleName.indexOf('/') >= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < simpleName.length(); i++) {
|
||||||
|
if (simpleName.charAt(i) < 'A' || simpleName.charAt(i) > 'Z') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] readEntryBytes(JarFile jar, JarEntry entry) throws IOException {
|
||||||
|
try (InputStream input = jar.getInputStream(entry)) {
|
||||||
|
return input.readAllBytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean startsWithAny(String value, List<String> prefixes) {
|
||||||
|
for (String prefix : prefixes) {
|
||||||
|
if (value.startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the {@code CONSTANT_Class} entries of a class file. That is exactly the set of types the
|
||||||
|
* class links against - supertypes, field and method owners, casts, catches, constant class
|
||||||
|
* literals. Annotation types live in the annotation attributes as plain UTF-8 descriptors and are
|
||||||
|
* deliberately not reported: the JVM skips an annotation whose type is absent.
|
||||||
|
*/
|
||||||
|
public final class ClassReferences {
|
||||||
|
private static final int CONSTANT_UTF8 = 1;
|
||||||
|
private static final int CONSTANT_INTEGER = 3;
|
||||||
|
private static final int CONSTANT_FLOAT = 4;
|
||||||
|
private static final int CONSTANT_LONG = 5;
|
||||||
|
private static final int CONSTANT_DOUBLE = 6;
|
||||||
|
private static final int CONSTANT_CLASS = 7;
|
||||||
|
private static final int CONSTANT_STRING = 8;
|
||||||
|
private static final int CONSTANT_FIELDREF = 9;
|
||||||
|
private static final int CONSTANT_METHODREF = 10;
|
||||||
|
private static final int CONSTANT_INTERFACE_METHODREF = 11;
|
||||||
|
private static final int CONSTANT_NAME_AND_TYPE = 12;
|
||||||
|
private static final int CONSTANT_METHOD_HANDLE = 15;
|
||||||
|
private static final int CONSTANT_METHOD_TYPE = 16;
|
||||||
|
private static final int CONSTANT_DYNAMIC = 17;
|
||||||
|
private static final int CONSTANT_INVOKE_DYNAMIC = 18;
|
||||||
|
private static final int CONSTANT_MODULE = 19;
|
||||||
|
private static final int CONSTANT_PACKAGE = 20;
|
||||||
|
private static final int CLASS_FILE_MAGIC = 0xCAFEBABE;
|
||||||
|
|
||||||
|
private ClassReferences() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> read(byte[] classFile) {
|
||||||
|
Set<String> references = new LinkedHashSet<>();
|
||||||
|
if (classFile.length < 10 || readInt(classFile, 0) != CLASS_FILE_MAGIC) {
|
||||||
|
return references;
|
||||||
|
}
|
||||||
|
|
||||||
|
int constantCount = readUnsignedShort(classFile, 8);
|
||||||
|
String[] utf8 = new String[constantCount];
|
||||||
|
List<Integer> classNameIndexes = new ArrayList<>();
|
||||||
|
int offset = 10;
|
||||||
|
int index = 1;
|
||||||
|
while (index < constantCount) {
|
||||||
|
int tag = classFile[offset] & 0xFF;
|
||||||
|
offset++;
|
||||||
|
switch (tag) {
|
||||||
|
case CONSTANT_UTF8 -> {
|
||||||
|
int length = readUnsignedShort(classFile, offset);
|
||||||
|
offset += 2;
|
||||||
|
utf8[index] = new String(classFile, offset, length, StandardCharsets.UTF_8);
|
||||||
|
offset += length;
|
||||||
|
}
|
||||||
|
case CONSTANT_CLASS -> {
|
||||||
|
classNameIndexes.add(readUnsignedShort(classFile, offset));
|
||||||
|
offset += 2;
|
||||||
|
}
|
||||||
|
case CONSTANT_STRING, CONSTANT_METHOD_TYPE, CONSTANT_MODULE, CONSTANT_PACKAGE -> offset += 2;
|
||||||
|
case CONSTANT_METHOD_HANDLE -> offset += 3;
|
||||||
|
case CONSTANT_INTEGER, CONSTANT_FLOAT, CONSTANT_FIELDREF, CONSTANT_METHODREF,
|
||||||
|
CONSTANT_INTERFACE_METHODREF, CONSTANT_NAME_AND_TYPE, CONSTANT_DYNAMIC,
|
||||||
|
CONSTANT_INVOKE_DYNAMIC -> offset += 4;
|
||||||
|
case CONSTANT_LONG, CONSTANT_DOUBLE -> {
|
||||||
|
offset += 8;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
return references;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int nameIndex : classNameIndexes) {
|
||||||
|
if (nameIndex <= 0 || nameIndex >= constantCount) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = normalize(utf8[nameIndex]);
|
||||||
|
if (name != null) {
|
||||||
|
references.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return references;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String rawName) {
|
||||||
|
if (rawName == null || rawName.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = rawName;
|
||||||
|
while (name.startsWith("[")) {
|
||||||
|
name = name.substring(1);
|
||||||
|
}
|
||||||
|
if (name.startsWith("L") && name.endsWith(";")) {
|
||||||
|
name = name.substring(1, name.length() - 1);
|
||||||
|
}
|
||||||
|
return name.length() > 1 ? name : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readUnsignedShort(byte[] data, int offset) {
|
||||||
|
return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readInt(byte[] data, int offset) {
|
||||||
|
return ((data[offset] & 0xFF) << 24)
|
||||||
|
| ((data[offset + 1] & 0xFF) << 16)
|
||||||
|
| ((data[offset + 2] & 0xFF) << 8)
|
||||||
|
| (data[offset + 3] & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import org.gradle.api.GradleException;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.MethodVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class BukkitArtifactVerifierTest {
|
||||||
|
private static final String PLUGIN_DESCRIPTOR = "plugin.yml";
|
||||||
|
private static final String NMS_BINDING = "art/arcane/iris/core/nms/v26_2_R1/NMSBinding";
|
||||||
|
private static final List<String> REQUIRED_ENTRIES = List.of(PLUGIN_DESCRIPTOR, NMS_BINDING + ".class");
|
||||||
|
private static final int LOCALES = 2;
|
||||||
|
private static final int CAFFEINE_FACTORIES = 2;
|
||||||
|
private static final int MATTER_SLICES = 1;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsCompleteArtifact() throws Exception {
|
||||||
|
File artifact = createArtifact(validEntries());
|
||||||
|
|
||||||
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsMissingRequiredEntry() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.remove(PLUGIN_DESCRIPTOR);
|
||||||
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
||||||
|
MATTER_SLICES));
|
||||||
|
assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsExcludedClassThatIsStillReferenced() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.remove("art/arcane/volmlib/util/noise/CNG.class");
|
||||||
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
||||||
|
MATTER_SLICES));
|
||||||
|
assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsReferenceToRuntimeDownloadedLibrary() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.put("art/arcane/iris/Consumer.class",
|
||||||
|
classReferencing("art/arcane/iris/Consumer", "art/arcane/iris/util/kyori/adventure/text/Component"));
|
||||||
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsStrippedCaffeineFactories() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.remove("art/arcane/iris/util/caffeine/cache/SSMS.class");
|
||||||
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
||||||
|
MATTER_SLICES));
|
||||||
|
assertTrue(failure.getMessage().contains("generated Caffeine cache classes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsDroppedLocale() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.remove("languages/de_DE.json");
|
||||||
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
||||||
|
MATTER_SLICES));
|
||||||
|
assertTrue(failure.getMessage().contains("locale files"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsDroppedMatterSlice() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.remove("art/arcane/volmlib/util/matter/slices/BlockMatter.class");
|
||||||
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES,
|
||||||
|
MATTER_SLICES));
|
||||||
|
assertTrue(failure.getMessage().contains("Matter slice types"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ignoresAnnotationOnlyReferences() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.put("art/arcane/iris/Annotated.class",
|
||||||
|
classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue"));
|
||||||
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, CAFFEINE_FACTORIES, MATTER_SLICES);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, byte[]> validEntries() {
|
||||||
|
Map<String, byte[]> entries = new LinkedHashMap<>();
|
||||||
|
entries.put(PLUGIN_DESCRIPTOR, "name: Iris\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
entries.put("languages/de_DE.json", "{}".getBytes(StandardCharsets.UTF_8));
|
||||||
|
entries.put("languages/fr_FR.json", "{}".getBytes(StandardCharsets.UTF_8));
|
||||||
|
entries.put(NMS_BINDING + ".class",
|
||||||
|
classReferencing(NMS_BINDING, "art/arcane/volmlib/util/noise/CNG"));
|
||||||
|
entries.put("art/arcane/volmlib/util/noise/CNG.class", emptyClass("art/arcane/volmlib/util/noise/CNG"));
|
||||||
|
entries.put("art/arcane/volmlib/util/matter/slices/BlockMatter.class",
|
||||||
|
emptyClass("art/arcane/volmlib/util/matter/slices/BlockMatter"));
|
||||||
|
entries.put("art/arcane/iris/util/caffeine/cache/SSMS.class",
|
||||||
|
emptyClass("art/arcane/iris/util/caffeine/cache/SSMS"));
|
||||||
|
entries.put("art/arcane/iris/util/caffeine/cache/SSLMS.class",
|
||||||
|
emptyClass("art/arcane/iris/util/caffeine/cache/SSLMS"));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] emptyClass(String internalName) {
|
||||||
|
ClassWriter writer = new ClassWriter(0);
|
||||||
|
writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null);
|
||||||
|
writer.visitEnd();
|
||||||
|
return writer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] classReferencing(String internalName, String referenced) {
|
||||||
|
ClassWriter writer = new ClassWriter(0);
|
||||||
|
writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null);
|
||||||
|
MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "use", "()V", null, null);
|
||||||
|
method.visitCode();
|
||||||
|
method.visitTypeInsn(Opcodes.NEW, referenced);
|
||||||
|
method.visitInsn(Opcodes.POP);
|
||||||
|
method.visitInsn(Opcodes.RETURN);
|
||||||
|
method.visitMaxs(1, 0);
|
||||||
|
method.visitEnd();
|
||||||
|
writer.visitEnd();
|
||||||
|
return writer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] classAnnotatedWith(String internalName, String annotation) {
|
||||||
|
ClassWriter writer = new ClassWriter(0);
|
||||||
|
writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null);
|
||||||
|
writer.visitAnnotation('L' + annotation + ';', true).visitEnd();
|
||||||
|
writer.visitEnd();
|
||||||
|
return writer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createArtifact(Map<String, byte[]> entries) throws Exception {
|
||||||
|
File artifact = temporaryFolder.newFile("Iris-bukkit-" + System.nanoTime() + ".jar");
|
||||||
|
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(artifact))) {
|
||||||
|
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
|
||||||
|
jar.putNextEntry(new JarEntry(entry.getKey()));
|
||||||
|
jar.write(entry.getValue());
|
||||||
|
jar.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return artifact;
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-1
@@ -255,12 +255,59 @@ if (runningTestTasks) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VolmLib packages that Iris forked into art.arcane.iris.util.project.* or replaced outright. Every
|
||||||
|
// class in each of these packages is unreachable from every other class shipped in the Bukkit jar,
|
||||||
|
// and none of them is a reflective target: Matter.read() resolves only matter/slices, JarScanner
|
||||||
|
// only walks Iris' own packages, and the remaining VolmLib Class.forName calls name Bukkit/NMS or
|
||||||
|
// runtime-downloaded types. Package granularity is deliberate - a partially used package stays whole.
|
||||||
|
// verifyBukkitArtifact re-derives the reference graph over the shipped jar and fails on any dangling
|
||||||
|
// reference, so an exclude that stops being dead is a build failure rather than a runtime crash.
|
||||||
|
List<String> supersededVolmLibPackages = [
|
||||||
|
'art/arcane/volmlib/util/noise/**',
|
||||||
|
'art/arcane/volmlib/util/stream/ProceduralStream.class',
|
||||||
|
'art/arcane/volmlib/util/stream/BasicStream.class',
|
||||||
|
'art/arcane/volmlib/util/stream/arithmetic/**',
|
||||||
|
'art/arcane/volmlib/util/stream/convert/**',
|
||||||
|
'art/arcane/volmlib/util/stream/interpolation/**',
|
||||||
|
'art/arcane/volmlib/util/stream/sources/**',
|
||||||
|
'art/arcane/volmlib/util/stream/utility/**',
|
||||||
|
'art/arcane/volmlib/util/uniques/**',
|
||||||
|
'art/arcane/volmlib/util/bukkit/json/**',
|
||||||
|
'art/arcane/volmlib/util/bukkit/registry/**',
|
||||||
|
'art/arcane/volmlib/util/director/visual/**',
|
||||||
|
'art/arcane/volmlib/util/value/**',
|
||||||
|
'art/arcane/volmlib/util/api/**',
|
||||||
|
'art/arcane/volmlib/util/entity/**'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Annotation-only artifacts pulled in transitively by Gson and Caffeine. Their types appear solely
|
||||||
|
// in annotation attributes, which the JVM skips silently when the type is absent, so nothing loads
|
||||||
|
// or links against them at runtime.
|
||||||
|
List<String> annotationOnlyArtifacts = [
|
||||||
|
'com/google/errorprone/**',
|
||||||
|
'org/jspecify/**'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Publisher metadata for the shaded dependencies. Nothing reads these at runtime.
|
||||||
|
List<String> dependencyBuildMetadata = [
|
||||||
|
'META-INF/maven/**',
|
||||||
|
'META-INF/proguard/**',
|
||||||
|
'META-INF/versions/*/OSGI-INF/**'
|
||||||
|
]
|
||||||
|
|
||||||
tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).configure {
|
tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).configure {
|
||||||
dependsOn(embeddedAgentJar)
|
dependsOn(embeddedAgentJar)
|
||||||
mergeServiceFiles()
|
mergeServiceFiles()
|
||||||
//minimize()
|
// minimize() stays off. Caffeine reaches its 520 generated cache and node classes through
|
||||||
|
// MethodHandles.Lookup.findClass on a computed name, and the VolmLib command, GUI and hotload
|
||||||
|
// surfaces are only referenced from :adapters:bukkit:plugin, which is merged into the artifact
|
||||||
|
// after this task runs. Both would be stripped as unreachable. Excluding them from minimization
|
||||||
|
// leaves it reaching ~25KB of Gson internals, which is not worth a mode that fails silently.
|
||||||
relocate('io.github.slimjar', "${lib}.slimjar")
|
relocate('io.github.slimjar', "${lib}.slimjar")
|
||||||
exclude('modules/loader-agent.isolated-jar')
|
exclude('modules/loader-agent.isolated-jar')
|
||||||
|
exclude(supersededVolmLibPackages)
|
||||||
|
exclude(annotationOnlyArtifacts)
|
||||||
|
exclude(dependencyBuildMetadata)
|
||||||
from(embeddedAgentJar.map { it.archiveFile }) {
|
from(embeddedAgentJar.map { it.archiveFile }) {
|
||||||
rename { String ignored -> 'agent.jar' }
|
rename { String ignored -> 'agent.jar' }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user