VolmLib pin removal

- The build resolves VolmLib master-SNAPSHOT when no sibling checkout is substituted
- The native runtime manifest, roots and keep rules come from the embedded natives
- The packed jar natives are verified against the embedded provider shas
- Every jar carries META-INF/volmit/volmlib.properties
- The build logs the resolved VolmLib once
- publishedNativeRuntime, the loader coordinate flags, mavenLocal and publish-volmlib are gone
This commit is contained in:
Brian Neumann-Fopiano
2026-09-29 09:33:26 -04:00
parent f60538343e
commit bf11d44370
20 changed files with 386 additions and 426 deletions
+1
View File
@@ -32,6 +32,7 @@ dependencies {
implementation(gradleApi())
implementation('org.ow2.asm:asm:9.10.1')
implementation('org.apache.commons:commons-compress:1.28.0')
implementation('org.tukaani:xz:1.10')
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
testImplementation('junit:junit:4.13.2')
}
@@ -1,7 +1,7 @@
import org.gradle.api.GradleException;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.tukaani.xz.XZInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -11,6 +11,7 @@ import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HexFormat;
import java.util.List;
@@ -21,46 +22,32 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public final class NativeRuntimeArtifacts {
public static final String MANIFEST = "META-INF/iris/native-runtime.properties";
public static final String RESOURCES = "META-INF/iris/native/";
private static final String PAYLOAD = "META-INF/volmit/runtime.jar.xz";
private static final String GROUP = "com.github.VolmitSoftware.VolmLib";
private static final String NATIVE = "art/arcane/volmlib/nativelib/";
private NativeRuntimeArtifacts() {
}
public static Map<String, File> publishedArtifacts(Map<ComponentIdentifier, File> resolved,
List<String> modules, String version) {
Map<String, File> artifacts = new TreeMap<>();
for (Map.Entry<ComponentIdentifier, File> entry : resolved.entrySet()) {
if (!(entry.getKey() instanceof ModuleComponentIdentifier component)
|| !GROUP.equals(component.getGroup()) || !version.equals(component.getVersion())
|| !modules.contains(component.getModule())) {
throw new GradleException("Native runtime providers must resolve to published coordinates at "
+ version + ": " + entry.getKey().getDisplayName());
}
if (artifacts.put(component.getModule(), entry.getValue()) != null) {
throw new GradleException("Duplicate native runtime provider: " + component.getModule());
}
}
if (!artifacts.keySet().equals(new TreeSet<>(modules))) {
throw new GradleException("Published native runtime providers differ: " + artifacts.keySet()
+ "; expected " + modules);
}
return artifacts;
}
public static void generate(Map<String, File> artifacts, String version, File output, File rules) throws IOException {
public static void generate(Map<String, File> natives, List<String> modules, File output, File rules) throws IOException {
requireModules(natives, modules);
Map<String, File> providers = new TreeMap<>(natives);
List<String> manifest = new ArrayList<>();
manifest.add("modules=" + String.join(",", artifacts.keySet()));
manifest.add("repository=https://jitpack.io/");
manifest.add("modules=" + String.join(",", providers.keySet()));
manifest.add("storage=embedded");
Set<String> retained = new TreeSet<>();
for (Map.Entry<String, File> artifact : artifacts.entrySet()) {
String module = artifact.getKey();
manifest.add(module + ".coordinate=" + GROUP + ":" + module + ":" + version);
manifest.add(module + ".sha256=" + digest(artifact.getValue()));
try (JarFile jar = new JarFile(artifact.getValue())) {
for (Map.Entry<String, File> provider : providers.entrySet()) {
String module = provider.getKey();
String digest = digest(Files.readAllBytes(provider.getValue().toPath()));
manifest.add(module + ".coordinate=" + GROUP + ":" + module + ":embedded-" + digest);
manifest.add(module + ".sha256=" + digest);
try (JarFile jar = new JarFile(provider.getValue())) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
@@ -92,31 +79,15 @@ public final class NativeRuntimeArtifacts {
Files.write(rules.toPath(), keep, StandardCharsets.UTF_8);
}
public static void verify(File artifact, List<String> adapters, String version) throws IOException {
public static void verify(File artifact, List<String> modules, Map<String, File> natives) throws IOException {
requireModules(natives, modules);
try (JarFile jar = new JarFile(artifact)) {
JarEntry entry = jar.getJarEntry(MANIFEST);
if (entry == null) {
throw new GradleException("Missing native runtime dependency manifest");
}
Properties manifest = new Properties();
try (InputStream input = jar.getInputStream(entry)) {
manifest.load(input);
}
Set<String> expected = new TreeSet<>();
expected.add("native-common");
for (String adapter : adapters) {
expected.add("native-" + adapter);
}
Set<String> actual = new TreeSet<>(List.of(manifest.getProperty("modules", "").split(",")));
if (!expected.equals(actual)) {
throw new GradleException("Native dependency modules differ: " + actual + "; expected " + expected);
}
for (String module : expected) {
String coordinate = GROUP + ":" + module + ":" + version;
if (!coordinate.equals(manifest.getProperty(module + ".coordinate"))
|| !manifest.getProperty(module + ".sha256", "").matches("[0-9a-f]{64}")) {
throw new GradleException("Invalid native runtime dependency: " + module);
}
verifyManifest(input.readAllBytes(), natives);
}
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
@@ -128,6 +99,82 @@ public final class NativeRuntimeArtifacts {
}
}
public static void verifyPacked(File artifact, List<String> modules, Map<String, File> natives) throws IOException {
requireModules(natives, modules);
Map<String, byte[]> contents;
try (JarFile jar = new JarFile(artifact)) {
JarEntry payload = jar.getJarEntry(PAYLOAD);
if (payload == null) {
contents = read(Files.newInputStream(artifact.toPath()));
} else {
try (InputStream input = new XZInputStream(jar.getInputStream(payload))) {
contents = read(new ByteArrayInputStream(input.readAllBytes()));
}
JarEntry outer = jar.getJarEntry(MANIFEST);
byte[] embedded = contents.get(MANIFEST);
if (outer != null && embedded != null) {
try (InputStream input = jar.getInputStream(outer)) {
if (!Arrays.equals(input.readAllBytes(), embedded)) {
throw new GradleException("Packed native runtime manifests differ");
}
}
}
}
}
byte[] manifest = contents.get(MANIFEST);
if (manifest == null) {
throw new GradleException("Missing native runtime dependency manifest");
}
verifyManifest(manifest, natives);
for (String module : natives.keySet()) {
byte[] provider = contents.get(RESOURCES + module + ".jar");
if (provider == null) {
throw new GradleException("Missing embedded native provider: " + module);
}
if (!digest(provider).equals(digest(Files.readAllBytes(natives.get(module).toPath())))) {
throw new GradleException("Embedded native provider differs from the resolved build: " + module);
}
}
}
private static void verifyManifest(byte[] bytes, Map<String, File> natives) throws IOException {
Properties manifest = new Properties();
manifest.load(new ByteArrayInputStream(bytes));
Set<String> actual = new TreeSet<>(List.of(manifest.getProperty("modules", "").split(",")));
if (!natives.keySet().equals(actual)) {
throw new GradleException("Native dependency modules differ: " + actual + "; expected " + natives.keySet());
}
if (!"embedded".equals(manifest.getProperty("storage"))) {
throw new GradleException("Native runtime providers must be embedded: " + manifest.getProperty("storage"));
}
for (Map.Entry<String, File> provider : natives.entrySet()) {
String module = provider.getKey();
String digest = digest(Files.readAllBytes(provider.getValue().toPath()));
if (!digest.equals(manifest.getProperty(module + ".sha256"))
|| !(GROUP + ":" + module + ":embedded-" + digest).equals(manifest.getProperty(module + ".coordinate"))) {
throw new GradleException("Native runtime manifest does not describe the embedded provider: " + module);
}
}
}
private static void requireModules(Map<String, File> natives, List<String> modules) {
if (!natives.keySet().equals(new TreeSet<>(modules))) {
throw new GradleException("Embedded native providers differ: " + natives.keySet() + "; expected " + modules);
}
}
private static Map<String, byte[]> read(InputStream source) throws IOException {
Map<String, byte[]> contents = new TreeMap<>();
try (ZipInputStream archive = new ZipInputStream(source)) {
for (ZipEntry entry = archive.getNextEntry(); entry != null; entry = archive.getNextEntry()) {
if (!entry.isDirectory()) {
contents.put(entry.getName(), archive.readAllBytes());
}
}
}
return contents;
}
private static boolean implementation(String name) {
if (!name.startsWith(NATIVE)) {
return false;
@@ -137,9 +184,9 @@ public final class NativeRuntimeArtifacts {
|| relative.matches("v[0-9_]+R[0-9]+/.*");
}
private static String digest(File file) throws IOException {
private static String digest(byte[] bytes) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(file.toPath())));
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException(exception);
}
@@ -1,12 +1,15 @@
import org.gradle.api.GradleException;
import org.gradle.api.artifacts.ModuleIdentifier;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.tukaani.xz.LZMA2Options;
import org.tukaani.xz.XZOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -15,122 +18,149 @@ import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeRuntimeArtifactsTest {
@Test
public void acceptsOnlyRequestedPublishedCoordinates() {
File common = new File("common.jar");
File provider = new File("provider.jar");
Map<ComponentIdentifier, File> resolved = Map.of(
module("native-common", "revision"), common,
module("native-v26_2_R1", "revision"), provider);
assertEquals(Map.of("native-common", common, "native-v26_2_R1", provider),
NativeRuntimeArtifacts.publishedArtifacts(resolved,
List.of("native-common", "native-v26_2_R1"), "revision"));
}
private static final List<String> MODULES = List.of("native-common", "native-v26_2_R1");
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@Test
public void rejectsCompositeProjectArtifacts() {
ComponentIdentifier project = () -> "project :VolmLib:native-common";
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.publishedArtifacts(
Map.of(project, new File("native-common-local-SNAPSHOT.jar")), List.of("native-common"), "revision"));
}
@Test
public void rejectsUnexpectedCoordinates() {
List<ComponentIdentifier> invalid = List.of(
new PublishedModule("other.group", "native-common", "revision"),
module("native-common", "other-revision"),
module("native-v26_3_R1", "revision"));
for (ComponentIdentifier component : invalid) {
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.publishedArtifacts(
Map.of(component, new File("provider.jar")), List.of("native-common"), "revision"));
}
}
@Test
public void rejectsMissingProviders() {
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.publishedArtifacts(
Map.of(module("native-common", "revision"), new File("common.jar")),
List.of("native-common", "native-v26_2_R1"), "revision"));
}
@Test
public void recordsExactDependenciesAndRetainsSharedProviderReferences() throws Exception {
Path directory = Files.createTempDirectory("native-artifacts");
File provider = directory.resolve("provider.jar").toFile();
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, "art/arcane/volmlib/nativelib/v26_2_R1/terrain/Provider", null,
"art/arcane/volmlib/util/collection/KList", null);
writer.visitEnd();
try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(provider.toPath()))) {
jar.putNextEntry(new JarEntry("art/arcane/volmlib/nativelib/v26_2_R1/terrain/Provider.class"));
jar.write(writer.toByteArray());
jar.closeEntry();
}
File rules = directory.resolve("keep.pro").toFile();
NativeRuntimeArtifacts.generate(Map.of("native-common", provider, "native-v26_2_R1", provider),
"revision", directory.toFile(), rules);
public void describesTheEmbeddedProvidersAndRetainsTheirSharedReferences() throws Exception {
Map<String, File> natives = natives();
File output = temporary.newFolder("generated");
File rules = new File(output, "keep.pro");
NativeRuntimeArtifacts.generate(natives, MODULES, output, rules);
Properties manifest = new Properties();
try (Reader reader = Files.newBufferedReader(directory.resolve(NativeRuntimeArtifacts.MANIFEST))) {
try (Reader reader = Files.newBufferedReader(output.toPath().resolve(NativeRuntimeArtifacts.MANIFEST))) {
manifest.load(reader);
}
String digest = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(Files.readAllBytes(provider.toPath())));
assertEquals(digest, manifest.getProperty("native-common.sha256"));
assertEquals(digest, manifest.getProperty("native-v26_2_R1.sha256"));
assertEquals("native-common,native-v26_2_R1", manifest.getProperty("modules"));
assertEquals("embedded", manifest.getProperty("storage"));
assertFalse(manifest.containsKey("repository"));
for (String module : MODULES) {
String digest = digest(Files.readAllBytes(natives.get(module).toPath()));
assertEquals(digest, manifest.getProperty(module + ".sha256"));
assertEquals("com.github.VolmitSoftware.VolmLib:" + module + ":embedded-" + digest,
manifest.getProperty(module + ".coordinate"));
}
assertTrue(Files.readString(rules.toPath()).contains("-keep class art.arcane.volmlib.util.collection.KList { *; }"));
File plugin = directory.resolve("plugin.jar").toFile();
try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(plugin.toPath()))) {
jar.putNextEntry(new JarEntry(NativeRuntimeArtifacts.MANIFEST));
Files.copy(directory.resolve(NativeRuntimeArtifacts.MANIFEST), jar);
jar.closeEntry();
}
NativeRuntimeArtifacts.verify(plugin, List.of("v26_2_R1"), "revision");
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verify(plugin, List.of("v26_3_R1"), "revision"));
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verify(plugin, List.of("v26_2_R1"), "other"));
assertEquals(List.of("art/arcane/volmlib/util/collection/KList"),
Files.readAllLines(output.toPath().resolve("META-INF/volmit/runtime-roots.list")));
}
private static ModuleComponentIdentifier module(String module, String version) {
return new PublishedModule("com.github.VolmitSoftware.VolmLib", module, version);
@Test
public void rejectsAProviderSetThatDiffersFromTheRuntimeModules() throws Exception {
Map<String, File> natives = natives();
natives.remove("native-v26_2_R1");
File output = temporary.newFolder("generated");
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.generate(natives, MODULES, output,
new File(output, "keep.pro")));
}
private record PublishedModule(String group, String module, String version)
implements ModuleComponentIdentifier, ModuleIdentifier {
@Override
public String getGroup() {
return group;
}
@Test
public void verifiesThePluginManifestAgainstTheProvidersBeingEmbedded() throws Exception {
Map<String, File> natives = natives();
File plugin = plugin(natives, Map.of());
NativeRuntimeArtifacts.verify(plugin, MODULES, natives);
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verify(plugin,
List.of("native-common", "native-v26_3_R1"), natives));
Map<String, File> rebuilt = new TreeMap<>(natives);
rebuilt.put("native-v26_2_R1", provider("rebuilt.jar", "art/arcane/volmlib/util/collection/KMap"));
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verify(plugin, MODULES, rebuilt));
}
@Override
public String getModule() {
return module;
}
@Test
public void rejectsProviderImplementationsBundledOutsideTheEmbeddedJars() throws Exception {
Map<String, File> natives = natives();
File plugin = plugin(natives, Map.of("art/arcane/volmlib/nativelib/v26_2_R1/terrain/Provider.class", new byte[0]));
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verify(plugin, MODULES, natives));
}
@Override
public String getVersion() {
return version;
}
@Test
public void verifiesTheEmbeddedProviderBytesInsideThePackedRuntime() throws Exception {
Map<String, File> natives = natives();
Map<String, byte[]> runtime = runtime(natives, natives);
NativeRuntimeArtifacts.verifyPacked(packed(runtime), MODULES, natives);
NativeRuntimeArtifacts.verifyPacked(jar("ordinary.jar", runtime), MODULES, natives);
Map<String, File> rebuilt = new TreeMap<>(natives);
rebuilt.put("native-v26_2_R1", provider("rebuilt.jar", "art/arcane/volmlib/util/collection/KMap"));
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verifyPacked(packed(runtime(natives, rebuilt)),
MODULES, natives));
Map<String, byte[]> missing = new TreeMap<>(runtime);
missing.remove(NativeRuntimeArtifacts.RESOURCES + "native-v26_2_R1.jar");
assertThrows(GradleException.class, () -> NativeRuntimeArtifacts.verifyPacked(packed(missing), MODULES, natives));
}
@Override
public ModuleIdentifier getModuleIdentifier() {
return this;
}
private Map<String, File> natives() throws IOException {
Map<String, File> natives = new TreeMap<>();
natives.put("native-common", provider("common.jar", "java/lang/Object"));
natives.put("native-v26_2_R1", provider("v26_2_R1.jar", "art/arcane/volmlib/util/collection/KList"));
return natives;
}
@Override
public String getName() {
return module;
}
private File provider(String name, String superclass) throws IOException {
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, "art/arcane/volmlib/nativelib/v26_2_R1/terrain/Provider", null,
superclass, null);
writer.visitEnd();
return jar(name, Map.of("art/arcane/volmlib/nativelib/v26_2_R1/terrain/Provider.class", writer.toByteArray()));
}
@Override
public String getDisplayName() {
return group + ":" + module + ":" + version;
private File plugin(Map<String, File> natives, Map<String, byte[]> extra) throws IOException {
File generated = temporary.newFolder();
NativeRuntimeArtifacts.generate(natives, MODULES, generated, new File(generated, "keep.pro"));
Map<String, byte[]> contents = new TreeMap<>(extra);
contents.put(NativeRuntimeArtifacts.MANIFEST,
Files.readAllBytes(generated.toPath().resolve(NativeRuntimeArtifacts.MANIFEST)));
return jar("plugin.jar", contents);
}
private Map<String, byte[]> runtime(Map<String, File> described, Map<String, File> embedded) throws IOException {
File generated = temporary.newFolder();
NativeRuntimeArtifacts.generate(described, MODULES, generated, new File(generated, "keep.pro"));
Map<String, byte[]> contents = new TreeMap<>();
contents.put(NativeRuntimeArtifacts.MANIFEST,
Files.readAllBytes(generated.toPath().resolve(NativeRuntimeArtifacts.MANIFEST)));
for (Map.Entry<String, File> provider : embedded.entrySet()) {
contents.put(NativeRuntimeArtifacts.RESOURCES + provider.getKey() + ".jar",
Files.readAllBytes(provider.getValue().toPath()));
}
return contents;
}
private File packed(Map<String, byte[]> runtime) throws IOException {
ByteArrayOutputStream compressed = new ByteArrayOutputStream();
try (XZOutputStream xz = new XZOutputStream(compressed, new LZMA2Options(1))) {
xz.write(Files.readAllBytes(jar("payload.jar", runtime).toPath()));
}
Map<String, byte[]> outer = new TreeMap<>();
outer.put(NativeRuntimeArtifacts.MANIFEST, runtime.get(NativeRuntimeArtifacts.MANIFEST));
outer.put("META-INF/volmit/runtime.jar.xz", compressed.toByteArray());
return jar("packed.jar", outer);
}
private File jar(String name, Map<String, byte[]> contents) throws IOException {
Path file = temporary.newFolder().toPath().resolve(name);
try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(file))) {
for (Map.Entry<String, byte[]> entry : contents.entrySet()) {
jar.putNextEntry(new JarEntry(entry.getKey()));
jar.write(entry.getValue());
jar.closeEntry();
}
}
return file.toFile();
}
private static String digest(byte[] bytes) throws Exception {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
}
}