mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-21 07:46:55 +00:00
Scary CI Builds.
This commit is contained in:
1 parent
68b842d5dc
commit
c40e1cf152
6 files changed
+574
-3
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user