Skip to content
Merged
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
15 changes: 10 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Java 21
- name: Set up Java 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
java-version: "25"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.12.1"
gradle-version: "9.4.1"

- name: Set up Python
uses: actions/setup-python@v5
Expand All @@ -63,8 +63,13 @@ jobs:
- name: Download latest release fixture jars
run: gradle downloadLatestReleaseJars --no-daemon

- name: Run test suite
run: gradle test --no-daemon -PautoGenerateStubs
- name: Run 26x test suite
run: gradle test --no-daemon -PautoGenerateStubs -PparserProfile=26x

- name: Generate 26x JSON output
run: >
gradle run --no-daemon -PparserProfile=26x
--args="--input fixtures/addons/jars --output output/poc-scan --summary output/poc-scan/summary.json --profile 26x"

- name: Upload release summaries
if: always()
Expand Down
32 changes: 24 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ version = '0.1.0'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
}
}

// Mapping profile: selects which shim source dir is included in the main sourceSet.
// Run gradle with -PparserProfile=legacy to build the 1.21.x flavor; default is 26x.
ext.parserProfile = (project.findProperty('parserProfile') ?: '26x').toString().toLowerCase()
if (parserProfile != 'legacy' && parserProfile != '26x') {
throw new GradleException("Unknown parserProfile: ${parserProfile} (expected 'legacy' or '26x')")
}
def profileSourceDir = parserProfile == 'legacy' ? 'src/profile-legacy/java' : 'src/profile-26x/java'

repositories {
mavenCentral()
maven { url = uri('https://libraries.minecraft.net') }
Expand All @@ -21,8 +29,8 @@ repositories {

dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.3'
implementation 'org.ow2.asm:asm:9.7.1'
implementation 'org.ow2.asm:asm-commons:9.7.1'
implementation 'org.ow2.asm:asm:9.9'
implementation 'org.ow2.asm:asm-commons:9.9'
implementation 'org.slf4j:slf4j-api:2.0.16'
runtimeOnly 'org.slf4j:slf4j-simple:2.0.16'
implementation 'org.apache.logging.log4j:log4j-api:2.24.3'
Expand All @@ -31,20 +39,23 @@ dependencies {
implementation 'com.mojang:brigadier:1.0.18'
implementation 'org.spongepowered:mixin:0.8.5'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.withType(Test).configureEach {
useJUnitPlatform()
jvmArgs '-noverify'
systemProperty 'addonparser.profile', parserProfile
}

application {
mainClass = 'com.cope.addonparser.cli.Main'
applicationDefaultJvmArgs = ['-noverify']
applicationDefaultJvmArgs = ['-noverify', "-Daddonparser.profile=${project.parserProfile}"]
}

tasks.named('run') {
jvmArgs '-noverify'
systemProperty 'addonparser.profile', parserProfile
}

sourceSets {
Expand All @@ -56,13 +67,13 @@ sourceSets {

main {
java {
srcDirs += ['src/generated/java']
srcDirs += ['src/generated/java', profileSourceDir]
}
}
}

dependencies {
stubgenImplementation 'org.ow2.asm:asm:9.7.1'
stubgenImplementation 'org.ow2.asm:asm:9.9'
stubgenImplementation 'com.google.code.gson:gson:2.11.0'
}

Expand All @@ -71,11 +82,15 @@ tasks.register('generateStubs', JavaExec) {
mainClass = 'com.cope.addonparser.tools.StubGenerator'
args '--input-dir', 'fixtures/addons/jars',
'--output-dir', 'src/generated/java',
'--manual-class-list', 'tools/manual_classes.txt'
'--manual-class-list', 'tools/manual_classes.txt',
'--manual-source-dirs', files('src/main/java', profileSourceDir).asPath,
'--profile', parserProfile

// Incremental support: declare inputs and outputs
inputs.dir('fixtures/addons/jars')
inputs.file('tools/manual_classes.txt')
inputs.dir('src/main/java')
inputs.dir(profileSourceDir)
inputs.files(sourceSets.stubgen.output)
outputs.dir('src/generated/java')
}
Expand Down Expand Up @@ -139,7 +154,8 @@ tasks.named('clean') {
// AP-009: Spotless configuration for Google Java Style on manual sources
spotless {
java {
target 'src/main/java/**/*.java', 'src/test/java/**/*.java', 'src/stubgen/java/**/*.java'
target 'src/main/java/**/*.java', 'src/test/java/**/*.java', 'src/stubgen/java/**/*.java',
'src/profile-legacy/java/**/*.java', 'src/profile-26x/java/**/*.java'
targetExclude 'src/generated/java/**'
googleJavaFormat()
removeUnusedImports()
Expand Down
20 changes: 16 additions & 4 deletions src/main/java/com/cope/addonparser/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.cope.addonparser.model.JarScanResult;
import com.cope.addonparser.model.ScanSummary;
import com.cope.addonparser.profile.MappingProfile;
import com.cope.addonparser.scanner.AddonScanner;
import com.cope.addonparser.scanner.IsolatedScanner;
import com.cope.addonparser.scanner.RuntimeMode;
Expand All @@ -25,6 +26,10 @@ public static void main(String[] args) throws Exception {
return;
}

// Make profile visible to in-process consumers (e.g. ValueNormalizer) and forwarded
// subprocesses.
System.setProperty(MappingProfile.SYSTEM_PROPERTY, parsed.profile.cliValue());

List<Path> jars = collectJars(parsed.input);
if (jars.isEmpty()) {
System.err.println("No jar files found in: " + parsed.input);
Expand All @@ -40,9 +45,12 @@ public static void main(String[] args) throws Exception {
summary.jarCount = jars.size();

System.out.println("Runtime mode: " + parsed.mode);
System.out.println("Mapping profile: " + parsed.profile.cliValue());

try (AutoCloseable scanner =
parsed.mode == RuntimeMode.ISOLATED ? new IsolatedScanner() : new AddonScanner()) {
parsed.mode == RuntimeMode.ISOLATED
? new IsolatedScanner(parsed.profile)
: new AddonScanner(parsed.profile)) {
for (Path jar : jars) {
JarScanResult result =
parsed.mode == RuntimeMode.ISOLATED
Expand Down Expand Up @@ -115,15 +123,17 @@ private static List<Path> collectJars(Path input) throws Exception {

private static void printUsage() {
System.out.println(
"Usage: java -jar addon-parser.jar --input <jar-or-dir> [--output <dir>] [--summary <file>] [--mode isolated|legacy]");
"Usage: java -jar addon-parser.jar --input <jar-or-dir> [--output <dir>] [--summary <file>] [--mode isolated|legacy] [--profile legacy|26x]");
}

private record Args(Path input, Path outputDir, Path summaryFile, RuntimeMode mode) {
private record Args(
Path input, Path outputDir, Path summaryFile, RuntimeMode mode, MappingProfile profile) {
static Args parse(String[] args) {
Path input = null;
Path output = Paths.get("output");
Path summary = null;
RuntimeMode mode = RuntimeMode.LEGACY;
MappingProfile profile = MappingProfile.fromSystemProperty();

for (int i = 0; i < args.length; i++) {
String arg = args[i];
Expand All @@ -135,13 +145,15 @@ static Args parse(String[] args) {
summary = Paths.get(args[++i]);
} else if ("--mode".equals(arg) && i + 1 < args.length) {
mode = RuntimeMode.fromString(args[++i]);
} else if ("--profile".equals(arg) && i + 1 < args.length) {
profile = MappingProfile.fromString(args[++i]);
} else {
return null;
}
}

if (input == null) return null;
return new Args(input, output, summary, mode);
return new Args(input, output, summary, mode, profile);
}
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/cope/addonparser/profile/MappingProfile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.cope.addonparser.profile;

import java.util.Locale;

public enum MappingProfile {
LEGACY,
MOJMAP_26X;

public static final String SYSTEM_PROPERTY = "addonparser.profile";

public static MappingProfile fromString(String value) {
if (value == null || value.isBlank()) return MOJMAP_26X;
return switch (value.trim().toLowerCase(Locale.ROOT)) {
case "legacy", "1.21", "1.21.x", "yarn" -> LEGACY;
case "26x", "26", "26.1", "mojmap", "mojmap_26x" -> MOJMAP_26X;
default -> throw new IllegalArgumentException(
"Unknown mapping profile: " + value + " (expected 'legacy' or '26x')");
};
}

public static MappingProfile fromSystemProperty() {
return fromString(System.getProperty(SYSTEM_PROPERTY));
}

public String cliValue() {
return this == LEGACY ? "legacy" : "26x";
}
}
16 changes: 15 additions & 1 deletion src/main/java/com/cope/addonparser/scanner/AddonScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.cope.addonparser.model.ModuleDump;
import com.cope.addonparser.model.SettingDump;
import com.cope.addonparser.model.SettingGroupDump;
import com.cope.addonparser.profile.MappingProfile;
import com.cope.addonparser.util.ChildFirstClassLoader;
import com.cope.addonparser.util.FabricModParser;
import com.cope.addonparser.util.ValueNormalizer;
Expand Down Expand Up @@ -40,7 +41,20 @@ public class AddonScanner implements AutoCloseable {
private static final String KEEP_TMP_PROPERTY = "addonparser.keepTmp";
private static final Path DEFAULT_TMP_ROOT = Paths.get("tmp", "addon-parser-runtime");

public AddonScanner() {}
private final MappingProfile profile;

public AddonScanner() {
this(MappingProfile.fromSystemProperty());
}

public AddonScanner(MappingProfile profile) {
this.profile = profile;
System.setProperty(MappingProfile.SYSTEM_PROPERTY, profile.cliValue());
}

public MappingProfile profile() {
return profile;
}

public JarScanResult scan(Path jarPath) {
Path absoluteJar = jarPath.toAbsolutePath().normalize();
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/com/cope/addonparser/scanner/IsolatedScanner.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cope.addonparser.scanner;

import com.cope.addonparser.model.JarScanResult;
import com.cope.addonparser.profile.MappingProfile;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
Expand All @@ -22,13 +23,23 @@ public class IsolatedScanner implements AutoCloseable {
private static final String WORKER_MAIN = ScanWorker.class.getName();

private final long timeoutSeconds;
private final MappingProfile profile;

public IsolatedScanner() {
this(DEFAULT_TIMEOUT_SECONDS);
this(DEFAULT_TIMEOUT_SECONDS, MappingProfile.fromSystemProperty());
}

public IsolatedScanner(MappingProfile profile) {
this(DEFAULT_TIMEOUT_SECONDS, profile);
}

public IsolatedScanner(long timeoutSeconds) {
this(timeoutSeconds, MappingProfile.fromSystemProperty());
}

public IsolatedScanner(long timeoutSeconds, MappingProfile profile) {
this.timeoutSeconds = timeoutSeconds;
this.profile = profile;
}

public JarScanResult scan(Path jarPath) {
Expand All @@ -39,7 +50,7 @@ public JarScanResult scan(Path jarPath) {
result.jarPath = absoluteJar.toString();

try {
List<String> command = buildWorkerCommand(absoluteJar);
List<String> command = buildWorkerCommand(absoluteJar, profile);
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(false);

Expand Down Expand Up @@ -118,7 +129,7 @@ public JarScanResult scan(Path jarPath) {
}
}

private static List<String> buildWorkerCommand(Path jarPath) {
private static List<String> buildWorkerCommand(Path jarPath, MappingProfile profile) {
List<String> cmd = new ArrayList<>();
String javaHome = System.getProperty("java.home");
String javaBin = Path.of(javaHome, "bin", "java").toString();
Expand All @@ -145,8 +156,12 @@ private static List<String> buildWorkerCommand(Path jarPath) {
}
}

cmd.add("-D" + MappingProfile.SYSTEM_PROPERTY + "=" + profile.cliValue());

cmd.add(WORKER_MAIN);
cmd.add(jarPath.toString());
cmd.add("--profile");
cmd.add(profile.cliValue());

return cmd;
}
Expand Down
28 changes: 24 additions & 4 deletions src/main/java/com/cope/addonparser/scanner/ScanWorker.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.cope.addonparser.scanner;

import com.cope.addonparser.model.JarScanResult;
import com.cope.addonparser.profile.MappingProfile;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.PrintStream;
import java.nio.file.Path;
Expand All @@ -9,26 +10,45 @@
* Entry point for the isolated worker JVM process. Scans a single jar using {@link AddonScanner}
* and writes the result as JSON to stdout.
*
* <p>Usage: {@code java ... com.cope.addonparser.scanner.ScanWorker <jar-path>}
* <p>Usage: {@code java ... com.cope.addonparser.scanner.ScanWorker <jar-path> [--profile
* legacy|26x]}
*/
public final class ScanWorker {
private ScanWorker() {}

public static void main(String[] args) {
if (args.length < 1) {
System.err.println("Usage: ScanWorker <jar-path>");
System.err.println("Usage: ScanWorker <jar-path> [--profile legacy|26x]");
System.exit(2);
return;
}

Path jarPath = null;
MappingProfile profile = MappingProfile.fromSystemProperty();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("--profile".equals(arg) && i + 1 < args.length) {
profile = MappingProfile.fromString(args[++i]);
} else if (jarPath == null) {
jarPath = Path.of(arg);
}
}

if (jarPath == null) {
System.err.println("Usage: ScanWorker <jar-path> [--profile legacy|26x]");
System.exit(2);
return;
}

System.setProperty(MappingProfile.SYSTEM_PROPERTY, profile.cliValue());

// Redirect stdout early so addon code can't pollute the JSON output
PrintStream originalOut = System.out;
System.setOut(System.err);

Path jarPath = Path.of(args[0]);
JarScanResult result;

try (AddonScanner scanner = new AddonScanner()) {
try (AddonScanner scanner = new AddonScanner(profile)) {
result = scanner.scan(jarPath);
} catch (Throwable t) {
result = new JarScanResult();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ private boolean isParentFirst(String name) {
|| name.startsWith("sun.")
|| name.startsWith("org.meteordev.starscript.")
|| name.startsWith("meteordevelopment.starscript.")
|| name.startsWith("com.cope.addonparser.");
|| name.startsWith("com.cope.addonparser.")
|| name.equals("dev.jfronny.meteoradditions.util.IModule");
Comment on lines +371 to +372
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While dev.jfronny.meteoradditions.util.IModule has been added to the parent-first list, meteordevelopment.meteorclient.* classes are still missing. If an addon jar bundles any Meteor Client classes (even by accident), it could lead to ClassCastException when the scanner tries to interact with them. It is generally safer to include the entire meteordevelopment.meteorclient. package in the parent-first delegation list for this scanner to ensure type consistency between the scanner and the loaded addon.

        || name.startsWith("com.cope.addonparser.")
        || name.startsWith("meteordevelopment.meteorclient.")
        || name.equals("dev.jfronny.meteoradditions.util.IModule");

}
}
Loading
Loading